From 8ee2c44550677b1f03c71f3bce49d72a206d495b Mon Sep 17 00:00:00 2001 From: Christian Dahmen Date: Sat, 18 Jan 2025 00:24:24 +0100 Subject: [PATCH 001/153] Fixed DNRGBW --- wled00/udp.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 8ba9a1a7a..2e87d9d05 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -605,8 +605,8 @@ void handleNotifications() return; } - //UDP realtime: 1 warls 2 drgb 3 drgbw - if (udpIn[0] > 0 && udpIn[0] < 5) + //UDP realtime: 1 warls 2 drgb 3 drgbw 4 dnrgb 5 dnrgbw + if (udpIn[0] > 0 && udpIn[0] < 6) { realtimeIP = (isSupp) ? notifier2Udp.remoteIP() : notifierUdp.remoteIP(); DEBUG_PRINTLN(realtimeIP); From 27d3420ad7e81fb78e30f3833579b02d37a12e2b Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Wed, 22 Jan 2025 09:31:51 +0000 Subject: [PATCH 002/153] Build each usermod in isolation --- .github/workflows/usermods.yml | 70 ++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .github/workflows/usermods.yml diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml new file mode 100644 index 000000000..58973e956 --- /dev/null +++ b/.github/workflows/usermods.yml @@ -0,0 +1,70 @@ +name: Usermod CI + +on: + push: +# paths: +# - usermods/** + +jobs: + + get_usermod_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 -n "usermods=\"" >> $GITHUB_OUTPUT + find usermods/ -name library.json | xargs dirname | xargs -n 1 basename | xargs echo >> $GITHUB_OUTPUT + echo "\"" >> $GITHUB_OUTPUT + outputs: + usermods: ${{ steps.envs.outputs.usermods }} + + + build: + name: Build Enviornments + runs-on: ubuntu-latest + needs: get_usermod_envs + strategy: + fail-fast: false + matrix: + environment: [usermod] + usermod: ${{ needs.get_usermod_envs.outputs.usermods }} + 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: Add usermods environment + run: | + cp -v usermods/platformio_override.usermods.ini platformio_override.ini + echo -n "custom_usermods = ${{ matrix.usermod }}" >> platformio_override.ini + + - name: Build firmware + run: pio run -e ${{ matrix.environment }} From 0e7d5dd0133e027e0a794dd1401a6bfa91275416 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Wed, 22 Jan 2025 09:38:30 +0000 Subject: [PATCH 003/153] Build each usermod in isolation --- .github/workflows/usermods.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index 58973e956..0b51a3935 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -21,9 +21,8 @@ jobs: - name: Get default environments id: envs run: | - echo -n "usermods=\"" >> $GITHUB_OUTPUT - find usermods/ -name library.json | xargs dirname | xargs -n 1 basename | xargs echo >> $GITHUB_OUTPUT - echo "\"" >> $GITHUB_OUTPUT + mods=`find usermods/ -name library.json | xargs dirname | xargs -n 1 basename | xargs echo` + echo -n "usermods=\"$mods\"" >> $GITHUB_OUTPUT outputs: usermods: ${{ steps.envs.outputs.usermods }} From 04c7eace09bcd64ef3d946df8b97d203d638fc37 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Wed, 22 Jan 2025 09:49:06 +0000 Subject: [PATCH 004/153] Use JSON for usermods list --- .github/workflows/usermods.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index 0b51a3935..124367224 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -8,7 +8,7 @@ on: jobs: get_usermod_envs: - name: Gather Environments + name: Gather Usermods runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -21,7 +21,7 @@ jobs: - name: Get default environments id: envs run: | - mods=`find usermods/ -name library.json | xargs dirname | xargs -n 1 basename | xargs echo` + mods=`find usermods/ -name library.json | xargs dirname | xargs -n 1 basename | jq -R | jq --slurp -c` echo -n "usermods=\"$mods\"" >> $GITHUB_OUTPUT outputs: usermods: ${{ steps.envs.outputs.usermods }} @@ -35,7 +35,7 @@ jobs: fail-fast: false matrix: environment: [usermod] - usermod: ${{ needs.get_usermod_envs.outputs.usermods }} + usermod: ${{ fromJSON(needs.get_usermod_envs.outputs.usermods) }} steps: - uses: actions/checkout@v4 - name: Set up Node.js From b3af04d3ca1dfb6a3936d5ff1ac5b5e5fa5fadcb Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Wed, 22 Jan 2025 09:49:42 +0000 Subject: [PATCH 005/153] Use JSON for usermods list --- .github/workflows/usermods.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index 124367224..efd5296df 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -22,7 +22,7 @@ jobs: id: envs run: | mods=`find usermods/ -name library.json | xargs dirname | xargs -n 1 basename | jq -R | jq --slurp -c` - echo -n "usermods=\"$mods\"" >> $GITHUB_OUTPUT + echo "usermods=$mods >> $GITHUB_OUTPUT outputs: usermods: ${{ steps.envs.outputs.usermods }} From b1b2eead26d7bae5a600994d127e539412d4b47e Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Wed, 22 Jan 2025 09:53:24 +0000 Subject: [PATCH 006/153] Use JSON for usermods list --- .github/workflows/usermods.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index efd5296df..ec5f9876a 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -21,8 +21,7 @@ jobs: - name: Get default environments id: envs run: | - mods=`find usermods/ -name library.json | xargs dirname | xargs -n 1 basename | jq -R | jq --slurp -c` - echo "usermods=$mods >> $GITHUB_OUTPUT + echo "usermods=$(find usermods/ -name library.json | xargs dirname | xargs -n 1 basename | jq -R | jq --slurp -c)" >> $GITHUB_OUTPUT outputs: usermods: ${{ steps.envs.outputs.usermods }} From 8d4c9119b49f956397d4c86d37cfbd3e962a6a6f Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Wed, 22 Jan 2025 09:55:34 +0000 Subject: [PATCH 007/153] Fix typo in env name --- .github/workflows/usermods.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index ec5f9876a..21fef2485 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -33,7 +33,7 @@ jobs: strategy: fail-fast: false matrix: - environment: [usermod] + environment: [usermods] usermod: ${{ fromJSON(needs.get_usermod_envs.outputs.usermods) }} steps: - uses: actions/checkout@v4 From 7d48bba926aa932ae453901a3f8f2714a753dc80 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Wed, 22 Jan 2025 10:09:51 +0000 Subject: [PATCH 008/153] build usermod_esp32 --- .github/workflows/usermods.yml | 2 +- usermods/platformio_override.usermods.ini | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 usermods/platformio_override.usermods.ini diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index 21fef2485..9a81d464c 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -33,7 +33,7 @@ jobs: strategy: fail-fast: false matrix: - environment: [usermods] + environment: [usermod_esp32] usermod: ${{ fromJSON(needs.get_usermod_envs.outputs.usermods) }} steps: - uses: actions/checkout@v4 diff --git a/usermods/platformio_override.usermods.ini b/usermods/platformio_override.usermods.ini new file mode 100644 index 000000000..611dc0d8b --- /dev/null +++ b/usermods/platformio_override.usermods.ini @@ -0,0 +1,11 @@ +[env:usermod_esp32] +board = esp32dev +platform = ${esp32_idf_V4.platform} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_USERMOD\" + -DTOUCH_CS=9 + -DMQTTSWITCHPINS=8 +lib_deps = ${esp32_idf_V4.lib_deps} +monitor_filters = esp32_exception_decoder +board_build.flash_mode = dio + From 74672e21303e489b8db32253fdb78837badbf975 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Wed, 22 Jan 2025 10:13:36 +0000 Subject: [PATCH 009/153] Verify each usermod on change --- .github/workflows/usermods.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index 9a81d464c..fed79f574 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -2,8 +2,8 @@ name: Usermod CI on: push: -# paths: -# - usermods/** + paths: + - usermods/** jobs: From 99108f9eff7587f762e69fb8880bf1d209aa332e Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Wed, 22 Jan 2025 10:15:35 +0000 Subject: [PATCH 010/153] Swap ordering to see if naming is then clearer --- .github/workflows/usermods.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index fed79f574..7467d9915 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -33,8 +33,8 @@ jobs: strategy: fail-fast: false matrix: - environment: [usermod_esp32] usermod: ${{ fromJSON(needs.get_usermod_envs.outputs.usermods) }} + environment: [usermod_esp32] steps: - uses: actions/checkout@v4 - name: Set up Node.js From 199529a031c7ef0d56c1c79fcb1b3a13c38a0f2e Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Wed, 22 Jan 2025 10:16:56 +0000 Subject: [PATCH 011/153] Also run if the workflow changes --- .github/workflows/usermods.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index 7467d9915..02a404ba1 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -4,6 +4,7 @@ on: push: paths: - usermods/** + - .github/workflows/usermods.yml jobs: From 2012317bc9bc43184da9e1a81897410df701ec00 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 3 Mar 2025 06:57:16 +0100 Subject: [PATCH 012/153] initial version, basically working but repetitive patterns, work in progress --- usermods/audioreactive/audio_reactive.h | 2 +- wled00/FX.cpp | 75 ++++----- wled00/fcn_declare.h | 9 ++ wled00/util.cpp | 195 +++++++++++++++++++++++- wled00/wled.cpp | 180 ++++++++++++++++++++++ 5 files changed, 421 insertions(+), 40 deletions(-) diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index e6b620098..d4b4b2d59 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -870,7 +870,7 @@ class AudioReactive : public Usermod { const int AGC_preset = (soundAgc > 0)? (soundAgc-1): 0; // make sure the _compiler_ knows this value will not change while we are inside the function #ifdef WLED_DISABLE_SOUND - micIn = inoise8(millis(), millis()); // Simulated analog read + micIn = perlin8(millis(), millis()); // Simulated analog read micDataReal = micIn; #else #ifdef ARDUINO_ARCH_ESP32 diff --git a/wled00/FX.cpp b/wled00/FX.cpp index e5132da57..a17d48603 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -2183,7 +2183,7 @@ static const char _data_FX_MODE_BPM[] PROGMEM = "Bpm@!;!;!;;sx=64"; uint16_t mode_fillnoise8() { if (SEGENV.call == 0) SEGENV.step = hw_random(); for (unsigned i = 0; i < SEGLEN; i++) { - unsigned index = inoise8(i * SEGLEN, SEGENV.step + i * SEGLEN); + unsigned index = perlin8(i * SEGLEN, SEGENV.step + i * SEGLEN); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } SEGENV.step += beatsin8_t(SEGMENT.speed, 1, 6); //10,1,4 @@ -2203,7 +2203,7 @@ uint16_t mode_noise16_1() { unsigned real_x = (i + shift_x) * scale; // the x position of the noise field swings @ 17 bpm unsigned real_y = (i + shift_y) * scale; // the y position becomes slowly incremented uint32_t real_z = SEGENV.step; // the z position becomes quickly incremented - unsigned noise = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down + unsigned noise = perlin16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down unsigned index = sin8_t(noise * 3); // map LED color based on noise data SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); @@ -2221,7 +2221,7 @@ uint16_t mode_noise16_2() { for (unsigned i = 0; i < SEGLEN; i++) { unsigned shift_x = SEGENV.step >> 6; // x as a function of time uint32_t real_x = (i + shift_x) * scale; // calculate the coordinates within the noise field - unsigned noise = inoise16(real_x, 0, 4223) >> 8; // get the noise data and scale it down + unsigned noise = perlin16(real_x, 0, 4223) >> 8; // get the noise data and scale it down unsigned index = sin8_t(noise * 3); // map led color based on noise data SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0, noise)); @@ -2242,7 +2242,7 @@ uint16_t mode_noise16_3() { uint32_t real_x = (i + shift_x) * scale; // calculate the coordinates within the noise field uint32_t real_y = (i + shift_y) * scale; // based on the precalculated positions uint32_t real_z = SEGENV.step*8; - unsigned noise = inoise16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down + unsigned noise = perlin16(real_x, real_y, real_z) >> 8; // get the noise data and scale it down unsigned index = sin8_t(noise * 3); // map led color based on noise data SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0, noise)); @@ -2257,7 +2257,7 @@ static const char _data_FX_MODE_NOISE16_3[] PROGMEM = "Noise 3@!;!;!;;pal=35"; uint16_t mode_noise16_4() { uint32_t stp = (strip.now * SEGMENT.speed) >> 7; for (unsigned i = 0; i < SEGLEN; i++) { - int index = inoise16(uint32_t(i) << 12, stp); + int index = perlin16(uint32_t(i) << 12, stp); SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } return FRAMETIME; @@ -4119,7 +4119,7 @@ static uint16_t phased_base(uint8_t moder) { // We're making si *phase += SEGMENT.speed/32.0; // You can change the speed of the wave. AKA SPEED (was .4) 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. + if (moder == 1) modVal = (perlin8(i*10 + i*10) /16); // Let's randomize our mod length with some Perlin noise. unsigned val = (i+1) * allfreq; // This sets the frequency of the waves. The +1 makes sure that led 0 is used. if (modVal == 0) modVal = 1; val += *phase * (i % modVal +1) /2; // This sets the varying phase change of the waves. By Andrew Tuline. @@ -4188,7 +4188,7 @@ uint16_t mode_noisepal(void) { // Slow noise if (SEGMENT.palette > 0) palettes[0] = SEGPALETTE; 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. + unsigned index = perlin8(i*scale, SEGENV.aux0+i*scale); // Get a value from the noise function. I'm using both x and y axis. SEGMENT.setPixelColor(i, ColorFromPalette(palettes[0], index, 255, LINEARBLEND)); // Use my own palette. } @@ -4799,7 +4799,7 @@ 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++) { - unsigned locn = inoise16(strip.now*128/(260-SEGMENT.speed)+i*15000, strip.now*128/(260-SEGMENT.speed)); // Get a new pixel location from moving noise. + unsigned locn = perlin16(strip.now*128/(260-SEGMENT.speed)+i*15000, strip.now*128/(260-SEGMENT.speed)); // Get a new pixel location from moving noise. unsigned pixloc = map(locn, 50*256, 192*256, 0, SEGLEN-1); // Map that to the length of the strand, and ensure we don't go over. SEGMENT.setPixelColor(pixloc, SEGMENT.color_from_palette(pixloc%255, false, PALETTE_SOLID_WRAP, 0)); } @@ -5057,7 +5057,7 @@ uint16_t mode_2Dfirenoise(void) { // firenoise2d. By Andrew Tuline 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. + indexx = perlin8(j*yscale*rows/255, i*xscale+strip.now/4); // We're moving along our Perlin map. SEGMENT.setPixelColorXY(j, i, ColorFromPalette(pal, min(i*indexx/11, 225U), i*255/rows, LINEARBLEND)); // With that value, look up the 8 bit colour palette value and assign it to the current LED. } // for i } // for j @@ -5450,11 +5450,11 @@ uint16_t mode_2Dmetaballs(void) { // Metaballs by Stefan Petrick. Cannot have float speed = 0.25f * (1+(SEGMENT.speed>>6)); // get some 2 random moving points - int x2 = map(inoise8(strip.now * speed, 25355, 685), 0, 255, 0, cols-1); - int y2 = map(inoise8(strip.now * speed, 355, 11685), 0, 255, 0, rows-1); + int x2 = map(perlin8(strip.now * speed, 25355, 685), 0, 255, 0, cols-1); + int y2 = map(perlin8(strip.now * speed, 355, 11685), 0, 255, 0, rows-1); - int x3 = map(inoise8(strip.now * speed, 55355, 6685), 0, 255, 0, cols-1); - int y3 = map(inoise8(strip.now * speed, 25355, 22685), 0, 255, 0, rows-1); + int x3 = map(perlin8(strip.now * speed, 55355, 6685), 0, 255, 0, cols-1); + int y3 = map(perlin8(strip.now * speed, 25355, 22685), 0, 255, 0, rows-1); // and one Lissajou function int x1 = beatsin8_t(23 * speed, 0, cols-1); @@ -5510,7 +5510,7 @@ uint16_t mode_2Dnoise(void) { // By Andrew Tuline for (int y = 0; y < rows; y++) { for (int x = 0; x < cols; x++) { - uint8_t pixelHue8 = inoise8(x * scale, y * scale, strip.now / (16 - SEGMENT.speed/16)); + uint8_t pixelHue8 = perlin8(x * scale, y * scale, strip.now / (16 - SEGMENT.speed/16)); SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, pixelHue8)); } } @@ -5532,10 +5532,10 @@ uint16_t mode_2DPlasmaball(void) { // By: Stepko https://edito SEGMENT.fadeToBlackBy(SEGMENT.custom1>>2); uint_fast32_t t = (strip.now * 8) / (256 - SEGMENT.speed); // optimized to avoid float for (int i = 0; i < cols; i++) { - unsigned thisVal = inoise8(i * 30, t, t); + unsigned thisVal = perlin8(i * 30, t, t); unsigned thisMax = map(thisVal, 0, 255, 0, cols-1); for (int j = 0; j < rows; j++) { - unsigned thisVal_ = inoise8(t, j * 30, t); + unsigned thisVal_ = perlin8(t, j * 30, t); unsigned thisMax_ = map(thisVal_, 0, 255, 0, rows-1); int x = (i + thisMax_ - cols / 2); int y = (j + thisMax - cols / 2); @@ -5580,7 +5580,7 @@ uint16_t mode_2DPolarLights(void) { // By: Kostyantyn Matviyevskyy https for (int x = 0; x < cols; x++) { for (int y = 0; y < rows; y++) { SEGENV.step++; - 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 palindex = qsub8(perlin8((SEGENV.step%2) + x * _scale, y * 16 + SEGENV.step % 16, SEGENV.step / _speed), fabsf((float)rows / 2.0f - (float)y) * adjustHeight); uint8_t palbrightness = palindex; if(SEGMENT.check1) palindex = 255 - palindex; //flip palette SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(palindex, false, false, 255, palbrightness)); @@ -5697,7 +5697,8 @@ uint16_t mode_2DSunradiation(void) { // By: ldirko https://edi uint8_t someVal = SEGMENT.speed/4; // Was 25. for (int j = 0; j < (rows + 2); j++) { for (int i = 0; i < (cols + 2); i++) { - byte col = (inoise8_raw(i * someVal, j * someVal, t)) / 2; + //byte col = (inoise8_raw(i * someVal, j * someVal, t)) / 2; + byte col = ((int16_t)perlin8(i * someVal, j * someVal, t) - 0x7F) / 3; bump[index++] = col; } } @@ -6232,7 +6233,7 @@ uint16_t mode_2Dplasmarotozoom() { int index = j*cols; for (int i = 0; i < cols; i++) { if (SEGMENT.check1) plasma[index+i] = (i * 4 ^ j * 4) + ms / 6; - else plasma[index+i] = inoise8(i * 40, j * 40, ms); + else plasma[index+i] = perlin8(i * 40, j * 40, ms); } } @@ -6395,10 +6396,10 @@ uint16_t mode_2DWaverly(void) { long t = strip.now / 2; for (int i = 0; i < cols; i++) { - unsigned thisVal = (1 + SEGMENT.intensity/64) * inoise8(i * 45 , t , t)/2; + unsigned thisVal = (1 + SEGMENT.intensity/64) * perlin8(i * 45 , t , t)/2; // use audio if available if (um_data) { - thisVal /= 32; // reduce intensity of inoise8() + thisVal /= 32; // reduce intensity of perlin8() thisVal *= volumeSmth; } int thisMax = map(thisVal, 0, 512, 0, rows); @@ -6477,7 +6478,7 @@ uint16_t mode_gravcenter_base(unsigned mode) { } else if(mode == 2) { //Gravimeter for (int i=0; itopLED > 0) { @@ -6499,7 +6500,7 @@ uint16_t mode_gravcenter_base(unsigned mode) { } else { //Gravcenter for (int i=0; iSEGLEN/2) maxLen = SEGLEN/2; 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. + uint8_t index = perlin8(i*volumeSmth+SEGENV.aux0, SEGENV.aux1+i*volumeSmth); // Get a value from the noise function. I'm using both x and y axis. SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(index, false, PALETTE_SOLID_WRAP, 0)); } @@ -6649,7 +6650,7 @@ uint16_t mode_noisefire(void) { // Noisefire. By Andrew Tuline. if (SEGENV.call == 0) SEGMENT.fill(BLACK); 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. + unsigned index = perlin8(i*SEGMENT.speed/64,strip.now*SEGMENT.speed/64*SEGLEN/255); // X location is constant, but we move along the Y at the rate of millis(). By Andrew Tuline. index = (255 - i*256/SEGLEN) * index/(256-SEGMENT.intensity); // Now we need to scale index so that it gets blacker as we get close to one of the ends. // This is a simple y=mx+b equation that's been scaled. index/128 is another scaling. @@ -6680,7 +6681,7 @@ uint16_t mode_noisemeter(void) { // Noisemeter. By Andrew Tuline. if (maxLen > SEGLEN) maxLen = SEGLEN; for (unsigned i=0; i> 8; + uint8_t data = perlin16(noisecoord[0] + ioffset, noisecoord[1] + joffset, noisecoord[2]) >> 8; noise3d[XY(i,j)] = scale8(noise3d[XY(i,j)], smoothness) + scale8(data, 255 - smoothness); } } @@ -8051,7 +8052,7 @@ uint16_t mode_particlefire(void) { if (SEGMENT.call % 10 == 0) SEGENV.aux1++; // move in noise y direction so noise does not repeat as often // add wind force to all particles - int8_t windspeed = ((int16_t)(inoise8(SEGENV.aux0, SEGENV.aux1) - 127) * SEGMENT.custom2) >> 7; + int8_t windspeed = ((int16_t)(perlin8(SEGENV.aux0, SEGENV.aux1) - 127) * SEGMENT.custom2) >> 7; PartSys->applyForce(windspeed, 0); } SEGENV.step++; @@ -8060,7 +8061,7 @@ uint16_t mode_particlefire(void) { if (SEGMENT.call % map(firespeed, 0, 255, 4, 15) == 0) { for (i = 0; i < PartSys->usedParticles; i++) { if (PartSys->particles[i].y < PartSys->maxY / 4) { // do not apply turbulance everywhere -> bottom quarter seems a good balance - int32_t curl = ((int32_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y, SEGENV.step << 4) - 127); + int32_t curl = ((int32_t)perlin8(PartSys->particles[i].x, PartSys->particles[i].y, SEGENV.step << 4) - 127); PartSys->particles[i].vx += (curl * (firespeed + 10)) >> 9; } } @@ -8277,8 +8278,8 @@ uint16_t mode_particlebox(void) { SEGENV.aux0 -= increment; if (SEGMENT.check1) { // random, use perlin noise - xgravity = ((int16_t)inoise8(SEGENV.aux0) - 127); - ygravity = ((int16_t)inoise8(SEGENV.aux0 + 10000) - 127); + xgravity = ((int16_t)perlin8(SEGENV.aux0) - 127); + ygravity = ((int16_t)perlin8(SEGENV.aux0 + 10000) - 127); // scale the gravity force xgravity = (xgravity * SEGMENT.custom1) / 128; ygravity = (ygravity * SEGMENT.custom1) / 128; @@ -8349,11 +8350,11 @@ uint16_t mode_particleperlin(void) { uint32_t scale = 16 - ((31 - SEGMENT.custom3) >> 1); uint16_t xnoise = PartSys->particles[i].x / scale; // position in perlin noise, scaled by slider uint16_t ynoise = PartSys->particles[i].y / scale; - int16_t baseheight = inoise8(xnoise, ynoise, SEGENV.aux0); // noise value at particle position + int16_t baseheight = perlin8(xnoise, ynoise, SEGENV.aux0); // noise value at particle position PartSys->particles[i].hue = baseheight; // color particles to perlin noise value if (SEGMENT.call % 8 == 0) { // do not apply the force every frame, is too chaotic - int8_t xslope = (baseheight + (int16_t)inoise8(xnoise - 10, ynoise, SEGENV.aux0)); - int8_t yslope = (baseheight + (int16_t)inoise8(xnoise, ynoise - 10, SEGENV.aux0)); + int8_t xslope = (baseheight + (int16_t)perlin8(xnoise - 10, ynoise, SEGENV.aux0)); + int8_t yslope = (baseheight + (int16_t)perlin8(xnoise, ynoise - 10, SEGENV.aux0)); PartSys->applyForce(i, xslope, yslope); } } @@ -9721,7 +9722,7 @@ uint16_t mode_particleBalance(void) { int32_t increment = (SEGMENT.speed >> 6) + 1; SEGENV.aux0 += increment; if (SEGMENT.check3) // random, use perlin noise - xgravity = ((int16_t)inoise8(SEGENV.aux0) - 128); + xgravity = ((int16_t)perlin8(SEGENV.aux0) - 128); else // sinusoidal xgravity = (int16_t)cos8(SEGENV.aux0) - 128;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGENV.aux0) // scale the force @@ -10073,7 +10074,7 @@ uint16_t mode_particle1Dsonicstream(void) { else PartSys->particles[i].ttl = 0; } if (SEGMENT.check1) // modulate colors by mid frequencies - PartSys->particles[i].hue += (mids * inoise8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies + PartSys->particles[i].hue += (mids * perlin8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies } if (loudness > threshold) { diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 6584d524e..4d5557fda 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -516,6 +516,15 @@ void enumerateLedmaps(); [[gnu::hot]] uint8_t get_random_wheel_index(uint8_t pos); [[gnu::hot, gnu::pure]] float mapf(float x, float in_min, float in_max, float out_min, float out_max); uint32_t hashInt(uint32_t s); +int32_t perlin1D_raw(uint32_t x); +int32_t perlin2D_raw(uint32_t x, uint32_t y); +int32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z); +uint8_t perlin8(uint16_t x); +uint8_t perlin8(uint16_t x, uint16_t y); +uint8_t perlin8(uint16_t x, uint16_t y, uint16_t z); +uint16_t perlin16(uint32_t x); +uint16_t perlin16(uint32_t x, uint32_t y); +uint16_t perlin16(uint32_t x, uint32_t y, uint32_t z); // 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) diff --git a/wled00/util.cpp b/wled00/util.cpp index 16af85e71..d5341a96e 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -506,12 +506,12 @@ um_data_t* simulateSound(uint8_t simulationId) break; case UMS_10_13: for (int i = 0; i<16; i++) - fftResult[i] = inoise8(beatsin8_t(90 / (i+1), 0, 200)*15 + (ms>>10), ms>>3); + fftResult[i] = perlin8(beatsin8_t(90 / (i+1), 0, 200)*15 + (ms>>10), ms>>3); volumeSmth = fftResult[8]; break; case UMS_14_3: for (int i = 0; i<16; i++) - fftResult[i] = inoise8(beatsin8_t(120 / (i+1), 10, 30)*10 + (ms>>14), ms>>3); + fftResult[i] = perlin8(beatsin8_t(120 / (i+1), 10, 30)*10 + (ms>>14), ms>>3); volumeSmth = fftResult[8]; break; } @@ -618,3 +618,194 @@ int32_t hw_random(int32_t lowerlimit, int32_t upperlimit) { uint32_t diff = upperlimit - lowerlimit; return hw_random(diff) + lowerlimit; } + +/* + * Fixed point integer based Perlin noise functions by @dedehai + * Note: optimized for speed and to mimic fastled inoise functions, not for accuracy or best randomness + */ + +// hash based gradient (speed is key) +static inline __attribute__((always_inline)) uint32_t perlinHash(uint32_t x) { + //x ^= x >> 15; //11? + //x *= 0x85ebca6b; + //x ^= x >> 7; + + //x *= 0xc2b2ae35; + //x ^= x >> 17; + //return x; + + //version from above, does not look too good + //x = ((x >> 16) ^ x) * 0x45d9f3b; + //x = ((x >> 16) ^ x) * 0x45d9f3b; + //return (x >> 16) ^ x; + +//found this online at https://github.com/skeeto/hash-prospector +// allegedly even better than the above murmur hash + x ^= x >> 16; + x *= 0x7feb352d; + x ^= x >> 15; + x *= 0x846ca68b; + x ^= x >> 16; + return x; + + +} + +//int8_t slopes[8] = {-4,-3,-2,-1,1,2,3,4}; +static inline __attribute__((always_inline)) int32_t cornergradient(uint32_t h) { + int grad = (h & 0x0F) - 8; // +7 to -8 + // int grad = slopes[h & 0x7]; // +1 or -1 (better mimics fastled but much slower, can be optimized by passing an array pointer or making the array global + //int grad = (h & 0x07) - 4; // +3 to -4 + //int grad = (h & 0x03) - 2; // +1 to -2 + //int grad = (h & 0x07) * (h & 0x10 ? -1 : 1); // symmetrical, much (!) slower + //return slopes[h & 0x7]; // lookup table is also very slow... + return grad; +} + +// Gradient functions for 1D, 2D and 3D Perlin noise note: forcing inline produces smaller code and makes it 3x faster! +static inline __attribute__((always_inline)) int32_t gradient1D(uint32_t x0, int32_t dx) { + int32_t grad = cornergradient(perlinHash(x0)); + return (grad * dx) >> 1; +} + +static inline __attribute__((always_inline)) int32_t gradient2D(uint32_t x0, int32_t dx, uint32_t y0, int32_t dy) { + uint32_t hashx = perlinHash(x0); + //uint32_t hashy = perlinHash(hashx ^ y0); + uint32_t hashy = perlinHash(y0 + 1013904223UL); + int32_t gradx = cornergradient(hashx); + int32_t grady = cornergradient(hashy); + return (gradx * dx + grady * dy) >> 2; +} + +static inline __attribute__((always_inline)) int32_t gradient3D(uint32_t x0, int32_t dx, uint32_t y0, int32_t dy, uint32_t z0, int32_t dz) { + //uint32_t hashx = perlinHash(x0); + //uint32_t hashy = perlinHash(hashx ^ y0); + //uint32_t hashz = perlinHash(hashy ^ z0); + + uint32_t hashx = perlinHash(x0); + uint32_t hashy = perlinHash(y0 + 1013904223UL); + uint32_t hashz = perlinHash(z0 + 1664525UL); + + int32_t gradx = cornergradient(hashx); + int32_t grady = cornergradient(hashy); + int32_t gradz = cornergradient(hashz); + return (gradx * dx + grady * dy + gradz * dz) >> 4; +} + +// fast cubic smoothstep: t*(3 - 2t²), optimized for fixed point +static uint32_t smoothstep(const uint32_t t) { + uint32_t t_squared = (t * t) >> 16; + uint32_t factor = (3 << 16) - ((t << 1)); + return (t_squared * factor) >> 17; // scale for best resolution without overflow +} + +// simple linear interpolation for fixed-point values, scaled for perlin noise use +static inline int32_t lerpPerlin(int32_t a, int32_t b, int32_t t) { + return a + (((b - a) * t) >> 15); +} + +// 1D Perlin noise function that returns a value in range of approximately -32768 to +32768 +int32_t perlin1D_raw(uint32_t x) { + // integer and fractional part coordinates + int32_t x0 = x >> 16; + int32_t x1 = x0 + 1; + int32_t dx0 = x & 0xFFFF; + int32_t dx1 = dx0 - 0xFFFF; + // gradient values for the two corners + int32_t g0 = gradient1D(x0, dx0); + int32_t g1 = gradient1D(x1, dx1); + // interpolate and smooth function + int32_t t = smoothstep(dx0); + int32_t noise = lerpPerlin(g0, g1, t); + return noise; +} + +// 2D Perlin noise function that returns a value in range of approximately -32768 to +32768 +int32_t perlin2D_raw(uint32_t x, uint32_t y) { + int32_t x0 = x >> 16; + int32_t y0 = y >> 16; + int32_t x1 = x0 + 1; + int32_t y1 = y0 + 1; + int32_t dx0 = x & 0xFFFF; + int32_t dy0 = y & 0xFFFF; + int32_t dx1 = dx0 - 0xFFFF; + int32_t dy1 = dy0 - 0xFFFF; + + int32_t g00 = gradient2D(x0, dx0, y0, dy0); + int32_t g10 = gradient2D(x1, dx1, y0, dy0); + int32_t g01 = gradient2D(x0, dx0, y1, dy1); + int32_t g11 = gradient2D(x1, dx1, y1, dy1); + + uint32_t tx = smoothstep(dx0); + uint32_t ty = smoothstep(dy0); + + int32_t nx0 = lerpPerlin(g00, g10, tx); + int32_t nx1 = lerpPerlin(g01, g11, tx); + + int32_t noise = lerpPerlin(nx0, nx1, ty); + return noise; +} + +// 2D Perlin noise function that returns a value in range of approximately -40000 to +40000 +int32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z) { + int32_t x0 = x >> 16; + int32_t y0 = y >> 16; + int32_t z0 = z >> 16; + int32_t x1 = x0 + 1; + int32_t y1 = y0 + 1; + int32_t z1 = z0 + 1; + + int32_t dx0 = x & 0xFFFF; + int32_t dy0 = y & 0xFFFF; + int32_t dz0 = z & 0xFFFF; + int32_t dx1 = dx0 - 0xFFFF; + int32_t dy1 = dy0 - 0xFFFF; + int32_t dz1 = dz0 - 0xFFFF; + + int32_t g000 = gradient3D(x0, dx0, y0, dy0, z0, dz0); + int32_t g001 = gradient3D(x0, dx0, y0, dy0, z1, dz1); + int32_t g010 = gradient3D(x0, dx0, y1, dy1, z0, dz0); + int32_t g011 = gradient3D(x0, dx0, y1, dy1, z1, dz1); + int32_t g100 = gradient3D(x1, dx1, y0, dy0, z0, dz0); + int32_t g101 = gradient3D(x1, dx1, y0, dy0, z1, dz1); + int32_t g110 = gradient3D(x1, dx1, y1, dy1, z0, dz0); + int32_t g111 = gradient3D(x1, dx1, y1, dy1, z1, dz1); + + uint32_t tx = smoothstep(dx0); + uint32_t ty = smoothstep(dy0); + uint32_t tz = smoothstep(dz0); + + int32_t nx0 = lerpPerlin(g000, g100, tx); + int32_t nx1 = lerpPerlin(g010, g110, tx); + int32_t nx2 = lerpPerlin(g001, g101, tx); + int32_t nx3 = lerpPerlin(g011, g111, tx); + int32_t ny0 = lerpPerlin(nx0, nx1, ty); + int32_t ny1 = lerpPerlin(nx2, nx3, ty); + + int32_t noise = lerpPerlin(ny0, ny1, tz); + return noise; +} +// scaling functions for fastled replacement +uint8_t perlin8(uint16_t x) { + return (perlin1D_raw(uint32_t(x) << 8) >> 8) + 0x7F; +} + +uint8_t perlin8(uint16_t x, uint16_t y) { + return uint8_t((perlin2D_raw(uint32_t(x)<<8, uint32_t(y)<<8) >> 8) + 0x7F); +} + +uint8_t perlin8(uint16_t x, uint16_t y, uint16_t z) { + return ((perlin3D_raw(uint32_t(x)<<8, uint32_t(y)<<8, uint32_t(z)<<8) * 85) >> 15) + 0x7F; +} + +uint16_t perlin16(uint32_t x) { + return perlin1D_raw(x) + 0x7FFF; +} + +uint16_t perlin16(uint32_t x, uint32_t y) { + return perlin2D_raw(x, y) + 0x7FFF; +} + +uint16_t perlin16(uint32_t x, uint32_t y, uint32_t z) { + return ((perlin3D_raw(x, y, z) * 70) >> 6) + 0x7FFF; +} \ No newline at end of file diff --git a/wled00/wled.cpp b/wled00/wled.cpp index bafdf58a9..e242d68f5 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -340,6 +340,186 @@ void WLED::setup() DEBUG_PRINTLN(F("arduino-esp32 v1.0.x\n")); // we can't say in more detail. #endif + + uint32_t start; + uint32_t end; + uint32_t time; + uint8_t offset = hw_random(); + + for(int i = 0; i < 0xFFFFF; i+=800) { + Serial.print(inoise16(i, offset, (offset >> 3))); Serial.print(","); //x + Serial.print(inoise16(offset, i, (offset >> 3))); Serial.print(","); //y + Serial.print(inoise16(offset, (offset >> 3), i)); Serial.print(","); //z + Serial.print(perlin16(i, offset, (offset >> 3))); Serial.print(","); //x + Serial.print(perlin16(offset, i, (offset >> 3))); Serial.print(","); //y + Serial.print(perlin16(offset, (offset >> 3), i)); Serial.print(","); //z + Serial.print(inoise16(i, offset+i/4, i*2 + (offset >> 3))); Serial.print(","); //mixed mode + Serial.println(perlin16(i, offset+i/4, i*2 + (offset >> 3))); + } +/* + for(int i = 0; i < 0x2FFFF; i+=100) { + uint32_t pos = i + offset; + Serial.print(inoise8_raw((pos)>>3, (pos)>>3)); Serial.print(","); + Serial.print(inoise8((pos)>>3, (pos)>>3, (pos)>>3)); Serial.print(","); + Serial.print(inoise16(pos*20, pos*30)); Serial.print(","); + //Serial.print(inoise16_raw(pos*20, pos*30, pos*40)); Serial.print(","); + Serial.print(inoise16(pos*20, pos*20, pos*20)); Serial.print(","); + //Serial.print(((perlin1D_raw(pos*20)* 85)>>7) + 0x7FFF); Serial.print(","); + Serial.print(perlin1D_raw(pos*20)); Serial.print(","); + Serial.print(perlin2D_raw(pos*20, pos*20)); Serial.print(","); + //Serial.print(perlin2D_raw(pos*20, pos*30) + 0x7FFF); Serial.print(","); + Serial.println(perlin3D_raw(pos*20, pos*20, pos*20)); + //Serial.println(((perlin3D_raw(pos*20, pos*30, pos*40) * 85)>>7) + 0x7FFF); + }*/ + +/* + for(int i = 0; i < 0xF0000; i+=55) { + Serial.print(inoise8_raw(i,i+5648) / 2); Serial.print(","); // +/-32 ? + Serial.print(((int16_t)perlin8(i,i+5648) - 0x7F) >> 2); Serial.print(","); + Serial.print(inoise8(i,i/3,i/5)); Serial.print(","); + Serial.print(perlin8(i,i/3,i/5)); Serial.print(","); + Serial.print(inoise8(i,i/3)); Serial.print(","); + Serial.print(perlin8(i,i/3)); Serial.print(","); + Serial.print(inoise8(i)); Serial.print(","); + Serial.println(perlin8(i)); + } +*/ + int32_t minval=0xFFFFF; + int32_t maxval=0; + start = micros(); + for(int i = 0; i < 0xFFFFF; i+=100) { + uint16_t pos = i + offset; + int32_t noiseval = inoise8_raw(pos); + if(noiseval < minval) minval = noiseval; + if(noiseval > maxval) maxval = noiseval; + } + end = micros(); + time = end - start; + Serial.print("time: "); Serial.print(time); + Serial.print(" inoise8_raw min: "); Serial.print(minval); Serial.print(" max: "); Serial.println(maxval); + + minval=0xFFFFF; + maxval=0; + /* + start = micros(); + for(int i = 0; i < 0xFFFFFF; i+=100) { + uint32_t pos = i + offset; + //int32_t noiseval = inoise16(pos, pos+4684165, pos+985685); + int32_t noiseval = inoise16(hw_random(), hw_random(), hw_random()); + if(noiseval < minval) minval = noiseval; + if(noiseval > maxval) maxval = noiseval; + } + end = micros(); + time = end - start; + Serial.print("time: "); Serial.print(time); + Serial.print(" inoise16_3D min: "); Serial.print(minval); Serial.print(" max: "); Serial.println(maxval); +*/ + minval=0xFFFFF; + maxval=0; + start = micros(); + for(int i = 0; i < 0xFFFFFFF; i+=100) { + uint32_t pos = i + offset; + int32_t noiseval = perlin1D_raw( hw_random()); + if(noiseval < minval) minval = noiseval; + if(noiseval > maxval) maxval = noiseval; + } + end = micros(); + time = end - start; + Serial.print("time: "); Serial.print(time); +Serial.print(" perlin1D_raw min: "); Serial.print(minval); Serial.print(" max: "); Serial.println(maxval); + minval=0xFFFFF; + maxval=0; + start = micros(); + for(int i = 0; i < 0xFFFFFFF; i+=100) { + uint32_t pos = i + offset; + //int32_t noiseval = perlin2D_raw(pos, pos+6846354); + int32_t noiseval = perlin2D_raw( hw_random(), hw_random()); + if(noiseval < minval) minval = noiseval; + if(noiseval > maxval) maxval = noiseval; + } + end = micros(); + time = end - start; + Serial.print("time: "); Serial.print(time); + Serial.print(" perlin2D_raw min: "); Serial.print(minval); Serial.print(" max: "); Serial.println(maxval); + minval=0xFFFFF; + maxval=0; + for(int i = 0; i < 0xFFFFFFF; i+=100) { + uint32_t pos = i + offset; + //int32_t noiseval = perlin3D_raw(pos, pos+46845, pos+654684); + //int32_t noiseval = perlin3D_raw(hw_random(), hw_random(), hw_random()); + int32_t noiseval = perlin16(hw_random(), hw_random(), hw_random()); + if(noiseval < minval) minval = noiseval; + if(noiseval > maxval) maxval = noiseval; + } + end = micros(); + time = end - start; + Serial.print("time: "); Serial.print(time); + Serial.print(" perlin16 min: "); Serial.print(minval); Serial.print(" max: "); Serial.println(maxval); + + volatile uint32_t temp; + start = micros(); + for(int i = 0; i < 100000; i++){ + temp += inoise8(i); + } + end = micros(); + time = end - start; + Serial.print("inoise8: "); + Serial.print(temp); + Serial.print("time: "); + Serial.println(time); + start = micros(); + for(int i = 0; i < 100000; i++){ + temp += inoise16(i,i<<1,i<<2); + } + end = micros(); + time = end - start; + Serial.print("inoise16:"); + Serial.print(temp); + Serial.print("time: "); + Serial.println(time); + start = micros(); + for(int i = 0; i < 100000; i++){ + temp += perlin1D_raw(i); + } + end = micros(); + time = end - start; + Serial.print("perlin1D:"); + Serial.print(temp); + Serial.print("time: "); + Serial.println(time); + start = micros(); + for(int i = 0; i < 100000; i++){ + temp += perlin2D_raw(i,i*33); + } + end = micros(); + time = end - start; + Serial.print("perlin2D:"); + Serial.print(temp); + Serial.print("time: "); + Serial.println(time); + start = micros(); + for(int i = 0; i < 100000; i++){ + temp += perlin16(i,i*33,i*17); + } + end = micros(); + time = end - start; + Serial.print("perlin16:"); + Serial.print(temp); + Serial.print("time: "); + Serial.println(time); + + start = micros(); + for(int i = 0; i < 100000; i++){ + temp += perlin3D_raw(i,i*33,i*17); + } + end = micros(); + time = end - start; + Serial.print("perlin3D raw:"); + Serial.print(temp); + Serial.print("time: "); + Serial.println(time); + + 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 From 9553425374431d7adfff15c16a9bd22cdd52f2c6 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 4 Mar 2025 07:55:41 +0100 Subject: [PATCH 013/153] some speed improvements using better hash, scaling is still off... needs a proper scaling analysis of all steps in all resolutions to minimize errors. --- wled00/util.cpp | 113 ++++++++++++++++++++---------------------------- wled00/wled.cpp | 56 ++++++++++++++++-------- 2 files changed, 85 insertions(+), 84 deletions(-) diff --git a/wled00/util.cpp b/wled00/util.cpp index d5341a96e..e7dbbca23 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -624,84 +624,63 @@ int32_t hw_random(int32_t lowerlimit, int32_t upperlimit) { * Note: optimized for speed and to mimic fastled inoise functions, not for accuracy or best randomness */ -// hash based gradient (speed is key) -static inline __attribute__((always_inline)) uint32_t perlinHash(uint32_t x) { - //x ^= x >> 15; //11? - //x *= 0x85ebca6b; - //x ^= x >> 7; - - //x *= 0xc2b2ae35; - //x ^= x >> 17; - //return x; - - //version from above, does not look too good - //x = ((x >> 16) ^ x) * 0x45d9f3b; - //x = ((x >> 16) ^ x) * 0x45d9f3b; - //return (x >> 16) ^ x; - -//found this online at https://github.com/skeeto/hash-prospector -// allegedly even better than the above murmur hash - x ^= x >> 16; - x *= 0x7feb352d; - x ^= x >> 15; - x *= 0x846ca68b; - x ^= x >> 16; - return x; - - -} - -//int8_t slopes[8] = {-4,-3,-2,-1,1,2,3,4}; -static inline __attribute__((always_inline)) int32_t cornergradient(uint32_t h) { - int grad = (h & 0x0F) - 8; // +7 to -8 - // int grad = slopes[h & 0x7]; // +1 or -1 (better mimics fastled but much slower, can be optimized by passing an array pointer or making the array global - //int grad = (h & 0x07) - 4; // +3 to -4 - //int grad = (h & 0x03) - 2; // +1 to -2 - //int grad = (h & 0x07) * (h & 0x10 ? -1 : 1); // symmetrical, much (!) slower - //return slopes[h & 0x7]; // lookup table is also very slow... - return grad; -} - // Gradient functions for 1D, 2D and 3D Perlin noise note: forcing inline produces smaller code and makes it 3x faster! static inline __attribute__((always_inline)) int32_t gradient1D(uint32_t x0, int32_t dx) { - int32_t grad = cornergradient(perlinHash(x0)); - return (grad * dx) >> 1; + //uint32_t hash ^= hash >> 16; + //hash *= 0x7feb352d; + //hash ^= hash >> 15; + //hash *= 0x846ca68b; + //hash ^= hash >> 16; + uint32_t hash = (x0 * 73856093); + hash ^= hash >> 15; + hash *= 0x92C3412B; + hash ^= hash >> 13; + int32_t gradx = (hash & 0xFF) - 128; // +127 to -128 + return (gradx * dx) >> 7; } static inline __attribute__((always_inline)) int32_t gradient2D(uint32_t x0, int32_t dx, uint32_t y0, int32_t dy) { - uint32_t hashx = perlinHash(x0); - //uint32_t hashy = perlinHash(hashx ^ y0); - uint32_t hashy = perlinHash(y0 + 1013904223UL); - int32_t gradx = cornergradient(hashx); - int32_t grady = cornergradient(hashy); - return (gradx * dx + grady * dy) >> 2; + //uint32_t hash = perlinHash(x0 ^ perlinHash(y0)); + // much faster and still decent entropy + uint32_t hash = (x0 * 73856093) ^ (y0 * 19349663); + hash ^= hash >> 15; + hash *= 0x92C3412B; + hash ^= hash >> 13; + // calculate gradients for each corner from hash value + int32_t gradx = (hash & 0xFF) - 128; // +127 to -128 + int32_t grady = ((hash>>7) & 0xFF) - 128; + return (gradx * dx + grady * dy) >> 9; } static inline __attribute__((always_inline)) int32_t gradient3D(uint32_t x0, int32_t dx, uint32_t y0, int32_t dy, uint32_t z0, int32_t dz) { - //uint32_t hashx = perlinHash(x0); - //uint32_t hashy = perlinHash(hashx ^ y0); - //uint32_t hashz = perlinHash(hashy ^ z0); + //uint32_t hash = perlinHash(x0 ^ perlinHash(y0 ^ perlinHash(z0))); - uint32_t hashx = perlinHash(x0); - uint32_t hashy = perlinHash(y0 + 1013904223UL); - uint32_t hashz = perlinHash(z0 + 1664525UL); + // fast and good entropy hash from corner coordinates + uint32_t hash = x0 * 0x68E31DA4 + y0 * 0xB5297A4D + z0 * 0x1B56C4E9; + hash ^= hash >> 8; + hash += hash << 3; + hash ^= hash >> 16; + // calculate gradients for each corner from hash value + //int32_t gradx = (hash & 0x07) - 4; // +3 to -4 + //int32_t grady = ((hash>>3) & 0x07) - 4; + //int32_t gradz = ((hash>>6) & 0x07) - 4; - int32_t gradx = cornergradient(hashx); - int32_t grady = cornergradient(hashy); - int32_t gradz = cornergradient(hashz); - return (gradx * dx + grady * dy + gradz * dz) >> 4; + int32_t gradx = (hash & 0xFF) - 128; // +127 to -128 + int32_t grady = ((hash>>7) & 0xFF) - 128; + int32_t gradz = ((hash>>14) & 0xFF) - 128; + return (gradx * dx + grady * dy + gradz * dz) >> 10; } // fast cubic smoothstep: t*(3 - 2t²), optimized for fixed point static uint32_t smoothstep(const uint32_t t) { uint32_t t_squared = (t * t) >> 16; uint32_t factor = (3 << 16) - ((t << 1)); - return (t_squared * factor) >> 17; // scale for best resolution without overflow + return (t_squared * factor) >> 16; } // simple linear interpolation for fixed-point values, scaled for perlin noise use static inline int32_t lerpPerlin(int32_t a, int32_t b, int32_t t) { - return a + (((b - a) * t) >> 15); + return a + (((b - a) * t) >> 16); } // 1D Perlin noise function that returns a value in range of approximately -32768 to +32768 @@ -710,13 +689,13 @@ int32_t perlin1D_raw(uint32_t x) { int32_t x0 = x >> 16; int32_t x1 = x0 + 1; int32_t dx0 = x & 0xFFFF; - int32_t dx1 = dx0 - 0xFFFF; + int32_t dx1 = dx0 - 0x10000; // gradient values for the two corners int32_t g0 = gradient1D(x0, dx0); int32_t g1 = gradient1D(x1, dx1); // interpolate and smooth function - int32_t t = smoothstep(dx0); - int32_t noise = lerpPerlin(g0, g1, t); + int32_t tx = smoothstep(dx0); + int32_t noise = lerpPerlin(g0, g1, tx); return noise; } @@ -728,8 +707,8 @@ int32_t perlin2D_raw(uint32_t x, uint32_t y) { int32_t y1 = y0 + 1; int32_t dx0 = x & 0xFFFF; int32_t dy0 = y & 0xFFFF; - int32_t dx1 = dx0 - 0xFFFF; - int32_t dy1 = dy0 - 0xFFFF; + int32_t dx1 = dx0 - 0x10000; + int32_t dy1 = dy0 - 0x10000; int32_t g00 = gradient2D(x0, dx0, y0, dy0); int32_t g10 = gradient2D(x1, dx1, y0, dy0); @@ -758,9 +737,9 @@ int32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z) { int32_t dx0 = x & 0xFFFF; int32_t dy0 = y & 0xFFFF; int32_t dz0 = z & 0xFFFF; - int32_t dx1 = dx0 - 0xFFFF; - int32_t dy1 = dy0 - 0xFFFF; - int32_t dz1 = dz0 - 0xFFFF; + int32_t dx1 = dx0 - 0x10000; + int32_t dy1 = dy0 - 0x10000; + int32_t dz1 = dz0 - 0x10000; int32_t g000 = gradient3D(x0, dx0, y0, dy0, z0, dz0); int32_t g001 = gradient3D(x0, dx0, y0, dy0, z1, dz1); @@ -807,5 +786,5 @@ uint16_t perlin16(uint32_t x, uint32_t y) { } uint16_t perlin16(uint32_t x, uint32_t y, uint32_t z) { - return ((perlin3D_raw(x, y, z) * 70) >> 6) + 0x7FFF; + return (perlin3D_raw(x, y, z)>>1) + 0x7FFF; } \ No newline at end of file diff --git a/wled00/wled.cpp b/wled00/wled.cpp index e242d68f5..de36908ca 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -345,7 +345,7 @@ void WLED::setup() uint32_t end; uint32_t time; uint8_t offset = hw_random(); - +/* for(int i = 0; i < 0xFFFFF; i+=800) { Serial.print(inoise16(i, offset, (offset >> 3))); Serial.print(","); //x Serial.print(inoise16(offset, i, (offset >> 3))); Serial.print(","); //y @@ -354,8 +354,10 @@ void WLED::setup() Serial.print(perlin16(offset, i, (offset >> 3))); Serial.print(","); //y Serial.print(perlin16(offset, (offset >> 3), i)); Serial.print(","); //z Serial.print(inoise16(i, offset+i/4, i*2 + (offset >> 3))); Serial.print(","); //mixed mode - Serial.println(perlin16(i, offset+i/4, i*2 + (offset >> 3))); - } + Serial.print(perlin16(i, offset+i/4, i*2 + (offset >> 3))); Serial.print(","); + Serial.println(perlin3D_raw(i, offset+i/4, i*2 + (offset >> 3))); //raw + }*/ + /* for(int i = 0; i < 0x2FFFF; i+=100) { uint32_t pos = i + offset; @@ -387,7 +389,7 @@ void WLED::setup() int32_t minval=0xFFFFF; int32_t maxval=0; start = micros(); - for(int i = 0; i < 0xFFFFF; i+=100) { + for(int i = 0; i < 0xFFFFF; i+=50) { uint16_t pos = i + offset; int32_t noiseval = inoise8_raw(pos); if(noiseval < minval) minval = noiseval; @@ -400,9 +402,9 @@ void WLED::setup() minval=0xFFFFF; maxval=0; - /* + start = micros(); - for(int i = 0; i < 0xFFFFFF; i+=100) { + for(int i = 0; i < 0xFFFFFF; i+=50) { uint32_t pos = i + offset; //int32_t noiseval = inoise16(pos, pos+4684165, pos+985685); int32_t noiseval = inoise16(hw_random(), hw_random(), hw_random()); @@ -413,26 +415,27 @@ void WLED::setup() time = end - start; Serial.print("time: "); Serial.print(time); Serial.print(" inoise16_3D min: "); Serial.print(minval); Serial.print(" max: "); Serial.println(maxval); -*/ + minval=0xFFFFF; maxval=0; start = micros(); - for(int i = 0; i < 0xFFFFFFF; i+=100) { + for(int i = 0; i < 0xFFFFFFF; i+=5) { uint32_t pos = i + offset; - int32_t noiseval = perlin1D_raw( hw_random()); + //int32_t noiseval = perlin16(hw_random()); + int32_t noiseval = perlin1D_raw(hw_random()); if(noiseval < minval) minval = noiseval; if(noiseval > maxval) maxval = noiseval; } end = micros(); time = end - start; Serial.print("time: "); Serial.print(time); -Serial.print(" perlin1D_raw min: "); Serial.print(minval); Serial.print(" max: "); Serial.println(maxval); +Serial.print(" perlin1D raw min: "); Serial.print(minval); Serial.print(" max: "); Serial.println(maxval); minval=0xFFFFF; maxval=0; start = micros(); - for(int i = 0; i < 0xFFFFFFF; i+=100) { + for(int i = 0; i < 0xFFFFFFF; i+=5) { uint32_t pos = i + offset; - //int32_t noiseval = perlin2D_raw(pos, pos+6846354); + //int32_t noiseval = perlin16( hw_random(), hw_random()); int32_t noiseval = perlin2D_raw( hw_random(), hw_random()); if(noiseval < minval) minval = noiseval; if(noiseval > maxval) maxval = noiseval; @@ -440,10 +443,12 @@ Serial.print(" perlin1D_raw min: "); Serial.print(minval); Serial.print(" max: " end = micros(); time = end - start; Serial.print("time: "); Serial.print(time); - Serial.print(" perlin2D_raw min: "); Serial.print(minval); Serial.print(" max: "); Serial.println(maxval); + Serial.print(" perlin2D raw min: "); Serial.print(minval); Serial.print(" max: "); Serial.println(maxval); + + minval=0xFFFFF; maxval=0; - for(int i = 0; i < 0xFFFFFFF; i+=100) { + for(int i = 0; i < 0xFFFFFFF; i+=5) { uint32_t pos = i + offset; //int32_t noiseval = perlin3D_raw(pos, pos+46845, pos+654684); //int32_t noiseval = perlin3D_raw(hw_random(), hw_random(), hw_random()); @@ -456,6 +461,23 @@ Serial.print(" perlin1D_raw min: "); Serial.print(minval); Serial.print(" max: " Serial.print("time: "); Serial.print(time); Serial.print(" perlin16 min: "); Serial.print(minval); Serial.print(" max: "); Serial.println(maxval); + minval=0xFFFFF; + maxval=0; + for(int i = 0; i < 0xFFFFFFF; i+=5) { + uint32_t pos = i + offset; + //int32_t noiseval = perlin3D_raw(pos, pos+46845, pos+654684); + int32_t noiseval = perlin3D_raw(hw_random(), hw_random(), hw_random()); + //int32_t noiseval = perlin16(hw_random(), hw_random(), hw_random()); + if(noiseval < minval) minval = noiseval; + if(noiseval > maxval) maxval = noiseval; + } + end = micros(); + time = end - start; + Serial.print("time: "); Serial.print(time); + Serial.print(" perlin3D_raw min: "); Serial.print(minval); Serial.print(" max: "); Serial.println(maxval); + + + volatile uint32_t temp; start = micros(); for(int i = 0; i < 100000; i++){ @@ -483,7 +505,7 @@ Serial.print(" perlin1D_raw min: "); Serial.print(minval); Serial.print(" max: " } end = micros(); time = end - start; - Serial.print("perlin1D:"); + Serial.print("perlin1Draw:"); Serial.print(temp); Serial.print("time: "); Serial.println(time); @@ -493,7 +515,7 @@ Serial.print(" perlin1D_raw min: "); Serial.print(minval); Serial.print(" max: " } end = micros(); time = end - start; - Serial.print("perlin2D:"); + Serial.print("perlin2Draw:"); Serial.print(temp); Serial.print("time: "); Serial.println(time); @@ -503,7 +525,7 @@ Serial.print(" perlin1D_raw min: "); Serial.print(minval); Serial.print(" max: " } end = micros(); time = end - start; - Serial.print("perlin16:"); + Serial.print("perlin163D:"); Serial.print(temp); Serial.print("time: "); Serial.println(time); From 5e8073022bff7df19a54de4afc39dd5aa257ea26 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 7 Mar 2025 06:39:49 +0100 Subject: [PATCH 014/153] 3D works but needs finetuning to match old looks --- wled00/util.cpp | 31 ++++++++++++------------------- wled00/wled.cpp | 18 +++++++++--------- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/wled00/util.cpp b/wled00/util.cpp index e7dbbca23..c04915031 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -653,34 +653,27 @@ static inline __attribute__((always_inline)) int32_t gradient2D(uint32_t x0, int } static inline __attribute__((always_inline)) int32_t gradient3D(uint32_t x0, int32_t dx, uint32_t y0, int32_t dy, uint32_t z0, int32_t dz) { - //uint32_t hash = perlinHash(x0 ^ perlinHash(y0 ^ perlinHash(z0))); - - // fast and good entropy hash from corner coordinates - uint32_t hash = x0 * 0x68E31DA4 + y0 * 0xB5297A4D + z0 * 0x1B56C4E9; - hash ^= hash >> 8; - hash += hash << 3; - hash ^= hash >> 16; - // calculate gradients for each corner from hash value - //int32_t gradx = (hash & 0x07) - 4; // +3 to -4 - //int32_t grady = ((hash>>3) & 0x07) - 4; - //int32_t gradz = ((hash>>6) & 0x07) - 4; + // fast and good entropy hash from corner coordinates + uint32_t h = (x0 * 0x68E31DA4) ^ (y0 * 0xB5297A4D) ^ (z0 * 0x1B56C4E9); + h ^= h >> 15; + h = h * 0x92C3412B + (h >> 13); - int32_t gradx = (hash & 0xFF) - 128; // +127 to -128 - int32_t grady = ((hash>>7) & 0xFF) - 128; - int32_t gradz = ((hash>>14) & 0xFF) - 128; - return (gradx * dx + grady * dy + gradz * dz) >> 10; + int32_t gradx = (h & 0xFF) - 128; // +127 to -128 + int32_t grady = ((h>>7) & 0xFF) - 128; + int32_t gradz = ((h>>14) & 0xFF) - 128; + return (gradx * dx + grady * dy + gradz * dz) >> 8; // 25bit >> 8bit -> result is signed 17bit max } -// fast cubic smoothstep: t*(3 - 2t²), optimized for fixed point +// fast cubic smoothstep: t*(3 - 2t²), optimized for fixed point, scaled to avoid overflows static uint32_t smoothstep(const uint32_t t) { uint32_t t_squared = (t * t) >> 16; uint32_t factor = (3 << 16) - ((t << 1)); - return (t_squared * factor) >> 16; + return (t_squared * factor) >> 19; } // simple linear interpolation for fixed-point values, scaled for perlin noise use static inline int32_t lerpPerlin(int32_t a, int32_t b, int32_t t) { - return a + (((b - a) * t) >> 16); + return a + (((b - a) * t) >> 13); } // 1D Perlin noise function that returns a value in range of approximately -32768 to +32768 @@ -786,5 +779,5 @@ uint16_t perlin16(uint32_t x, uint32_t y) { } uint16_t perlin16(uint32_t x, uint32_t y, uint32_t z) { - return (perlin3D_raw(x, y, z)>>1) + 0x7FFF; + return perlin3D_raw(x, y, z) + 0x7FFF; // scale to signed 16bit range and offset } \ No newline at end of file diff --git a/wled00/wled.cpp b/wled00/wled.cpp index de36908ca..639b7fe0c 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -345,18 +345,18 @@ void WLED::setup() uint32_t end; uint32_t time; uint8_t offset = hw_random(); -/* - for(int i = 0; i < 0xFFFFF; i+=800) { + + for(int i = 0; i < 0xFFFFF; i+=500) { Serial.print(inoise16(i, offset, (offset >> 3))); Serial.print(","); //x Serial.print(inoise16(offset, i, (offset >> 3))); Serial.print(","); //y Serial.print(inoise16(offset, (offset >> 3), i)); Serial.print(","); //z Serial.print(perlin16(i, offset, (offset >> 3))); Serial.print(","); //x Serial.print(perlin16(offset, i, (offset >> 3))); Serial.print(","); //y Serial.print(perlin16(offset, (offset >> 3), i)); Serial.print(","); //z - Serial.print(inoise16(i, offset+i/4, i*2 + (offset >> 3))); Serial.print(","); //mixed mode - Serial.print(perlin16(i, offset+i/4, i*2 + (offset >> 3))); Serial.print(","); + Serial.print(inoise16(i, offset+i/2, i + (offset >> 3))); Serial.print(","); //mixed mode + Serial.print(perlin16(i, offset+i/2, i + (offset >> 3))); Serial.print(","); Serial.println(perlin3D_raw(i, offset+i/4, i*2 + (offset >> 3))); //raw - }*/ + } /* for(int i = 0; i < 0x2FFFF; i+=100) { @@ -419,7 +419,7 @@ void WLED::setup() minval=0xFFFFF; maxval=0; start = micros(); - for(int i = 0; i < 0xFFFFFFF; i+=5) { + for(int i = 0; i < 0xFFFFFFF; i+=50) { uint32_t pos = i + offset; //int32_t noiseval = perlin16(hw_random()); int32_t noiseval = perlin1D_raw(hw_random()); @@ -433,7 +433,7 @@ Serial.print(" perlin1D raw min: "); Serial.print(minval); Serial.print(" max: " minval=0xFFFFF; maxval=0; start = micros(); - for(int i = 0; i < 0xFFFFFFF; i+=5) { + for(int i = 0; i < 0xFFFFFFF; i+=50) { uint32_t pos = i + offset; //int32_t noiseval = perlin16( hw_random(), hw_random()); int32_t noiseval = perlin2D_raw( hw_random(), hw_random()); @@ -448,7 +448,7 @@ Serial.print(" perlin1D raw min: "); Serial.print(minval); Serial.print(" max: " minval=0xFFFFF; maxval=0; - for(int i = 0; i < 0xFFFFFFF; i+=5) { + for(int i = 0; i < 0xFFFFFFF; i+=50) { uint32_t pos = i + offset; //int32_t noiseval = perlin3D_raw(pos, pos+46845, pos+654684); //int32_t noiseval = perlin3D_raw(hw_random(), hw_random(), hw_random()); @@ -463,7 +463,7 @@ Serial.print(" perlin1D raw min: "); Serial.print(minval); Serial.print(" max: " minval=0xFFFFF; maxval=0; - for(int i = 0; i < 0xFFFFFFF; i+=5) { + for(int i = 0; i < 0xFFFFFFF; i+=50) { uint32_t pos = i + offset; //int32_t noiseval = perlin3D_raw(pos, pos+46845, pos+654684); int32_t noiseval = perlin3D_raw(hw_random(), hw_random(), hw_random()); From 4ecc531998463d6895fbe5e7e620690cc702b04c Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 8 Mar 2025 12:48:27 +0100 Subject: [PATCH 015/153] updated scaling, improved hashing, updated rotozoomer to not use a buffer --- wled00/FX.cpp | 23 ++++----- wled00/fcn_declare.h | 6 +-- wled00/util.cpp | 110 ++++++++++++++++++++++++------------------- wled00/wled.cpp | 58 ++++++++++++++++------- 4 files changed, 113 insertions(+), 84 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a17d48603..1bba265dc 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -6221,22 +6221,12 @@ uint16_t mode_2Dplasmarotozoom() { const int cols = SEG_W; const int rows = SEG_H; - unsigned dataSize = SEGMENT.length() + sizeof(float); + unsigned dataSize = 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)); unsigned ms = strip.now/15; - // plasma - for (int j = 0; j < rows; j++) { - int index = j*cols; - for (int i = 0; i < cols; i++) { - if (SEGMENT.check1) plasma[index+i] = (i * 4 ^ j * 4) + ms / 6; - else plasma[index+i] = perlin8(i * 40, j * 40, ms); - } - } - // rotozoom float f = (sin_t(*a/2)+((128-SEGMENT.intensity)/128.0f)+1.1f)/1.5f; // scale factor float kosinus = cos_t(*a) * f; @@ -6245,9 +6235,14 @@ uint16_t mode_2Dplasmarotozoom() { float u1 = i * kosinus; float v1 = i * sinus; 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, 255)); + unsigned u = abs8(u1 - j * sinus) % cols; + unsigned v = abs8(v1 + j * kosinus) % rows; + byte plasma; + if (SEGMENT.check1) plasma = (u * 4 ^ v * 4) + ms / 6; + else plasma = perlin8(u * 40, v * 40, ms); + //else plasma = inoise8(u * SEGMENT.intensity, v * SEGMENT.intensity, ms); + //SEGMENT.setPixelColorXY(i, j, SEGMENT.color_from_palette(plasma[v*cols+u], false, PALETTE_SOLID_WRAP, 255)); + SEGMENT.setPixelColorXY(i, j, SEGMENT.color_from_palette(plasma, false, PALETTE_SOLID_WRAP, 255)); } } *a -= 0.03f + float(SEGENV.speed-128)*0.0002f; // rotation speed diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 4d5557fda..7ee87eaee 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -516,9 +516,9 @@ void enumerateLedmaps(); [[gnu::hot]] uint8_t get_random_wheel_index(uint8_t pos); [[gnu::hot, gnu::pure]] float mapf(float x, float in_min, float in_max, float out_min, float out_max); uint32_t hashInt(uint32_t s); -int32_t perlin1D_raw(uint32_t x); -int32_t perlin2D_raw(uint32_t x, uint32_t y); -int32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z); +int32_t perlin1D_raw(uint32_t x, bool is16bit = false); +int32_t perlin2D_raw(uint32_t x, uint32_t y, bool is16bit = false); +int32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z, bool is16bit = false); uint8_t perlin8(uint16_t x); uint8_t perlin8(uint16_t x, uint16_t y); uint8_t perlin8(uint16_t x, uint16_t y, uint16_t z); diff --git a/wled00/util.cpp b/wled00/util.cpp index c04915031..33973303b 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -623,64 +623,63 @@ int32_t hw_random(int32_t lowerlimit, int32_t upperlimit) { * Fixed point integer based Perlin noise functions by @dedehai * Note: optimized for speed and to mimic fastled inoise functions, not for accuracy or best randomness */ +#define PERLIN_SHIFT 1 + +// calculate gradient for corner from hash value +static inline __attribute__((always_inline)) int32_t hashToGradient(uint32_t h) { + // using more steps yields more "detailed" perlin noise but looks less like the original fastled version (adjust PERLIN_SHIFT to compensate) + //return (h & 0xFF) - 128; // use PERLIN_SHIFT 7 + //return (h & 0x0F) - 8; // use PERLIN_SHIFT 3 + //return (h & 0x07) - 4; // use PERLIN_SHIFT 2 + return (h & 0x03) - 2; // use PERLIN_SHIFT 1 +} // Gradient functions for 1D, 2D and 3D Perlin noise note: forcing inline produces smaller code and makes it 3x faster! static inline __attribute__((always_inline)) int32_t gradient1D(uint32_t x0, int32_t dx) { - //uint32_t hash ^= hash >> 16; - //hash *= 0x7feb352d; - //hash ^= hash >> 15; - //hash *= 0x846ca68b; - //hash ^= hash >> 16; - uint32_t hash = (x0 * 73856093); - hash ^= hash >> 15; - hash *= 0x92C3412B; - hash ^= hash >> 13; - int32_t gradx = (hash & 0xFF) - 128; // +127 to -128 - return (gradx * dx) >> 7; + uint32_t h = x0 * 0x27D4EB2D; + h ^= h >> 15; + h *= 0x92C3412B; + h ^= h >> 13; + h ^= h >> 7; + return (hashToGradient(h) * dx) >> PERLIN_SHIFT; } static inline __attribute__((always_inline)) int32_t gradient2D(uint32_t x0, int32_t dx, uint32_t y0, int32_t dy) { - //uint32_t hash = perlinHash(x0 ^ perlinHash(y0)); - // much faster and still decent entropy - uint32_t hash = (x0 * 73856093) ^ (y0 * 19349663); - hash ^= hash >> 15; - hash *= 0x92C3412B; - hash ^= hash >> 13; - // calculate gradients for each corner from hash value - int32_t gradx = (hash & 0xFF) - 128; // +127 to -128 - int32_t grady = ((hash>>7) & 0xFF) - 128; - return (gradx * dx + grady * dy) >> 9; + uint32_t h = (x0 * 0x27D4EB2D) ^ (y0 * 0xB5297A4D); + h ^= h >> 15; + h *= 0x92C3412B; + h ^= h >> 13; + return (hashToGradient(h) * dx + hashToGradient(h>>PERLIN_SHIFT) * dy) >> (1 + PERLIN_SHIFT); } static inline __attribute__((always_inline)) int32_t gradient3D(uint32_t x0, int32_t dx, uint32_t y0, int32_t dy, uint32_t z0, int32_t dz) { - // fast and good entropy hash from corner coordinates - uint32_t h = (x0 * 0x68E31DA4) ^ (y0 * 0xB5297A4D) ^ (z0 * 0x1B56C4E9); + // fast and good entropy hash from corner coordinates + uint32_t h = (x0 * 0x27D4EB2D) ^ (y0 * 0xB5297A4D) ^ (z0 * 0x1B56C4E9); h ^= h >> 15; - h = h * 0x92C3412B + (h >> 13); - - int32_t gradx = (h & 0xFF) - 128; // +127 to -128 - int32_t grady = ((h>>7) & 0xFF) - 128; - int32_t gradz = ((h>>14) & 0xFF) - 128; - return (gradx * dx + grady * dy + gradz * dz) >> 8; // 25bit >> 8bit -> result is signed 17bit max + h *= 0x92C3412B; + h ^= h >> 13; + return ((hashToGradient(h) * dx + hashToGradient(h>>(1+PERLIN_SHIFT)) * dy + hashToGradient(h>>(1 + 2*PERLIN_SHIFT)) * dz) * 85) >> (8 + PERLIN_SHIFT); // scale to 16bit, x*85 >> 8 = x/3 } // fast cubic smoothstep: t*(3 - 2t²), optimized for fixed point, scaled to avoid overflows static uint32_t smoothstep(const uint32_t t) { uint32_t t_squared = (t * t) >> 16; uint32_t factor = (3 << 16) - ((t << 1)); - return (t_squared * factor) >> 19; + return (t_squared * factor) >> 18; // scale to avoid overflows } // simple linear interpolation for fixed-point values, scaled for perlin noise use static inline int32_t lerpPerlin(int32_t a, int32_t b, int32_t t) { - return a + (((b - a) * t) >> 13); + return a + (((b - a) * t) >> 14); // match scaling with smoothstep to yield 16.16bit values } // 1D Perlin noise function that returns a value in range of approximately -32768 to +32768 -int32_t perlin1D_raw(uint32_t x) { +int32_t perlin1D_raw(uint32_t x, bool is16bit) { // integer and fractional part coordinates int32_t x0 = x >> 16; int32_t x1 = x0 + 1; + if(is16bit) x1 = x1 & 0xFF; // wrap back to zero at 0xFF instead of 0xFFFF + int32_t dx0 = x & 0xFFFF; int32_t dx1 = dx0 - 0x10000; // gradient values for the two corners @@ -693,11 +692,17 @@ int32_t perlin1D_raw(uint32_t x) { } // 2D Perlin noise function that returns a value in range of approximately -32768 to +32768 -int32_t perlin2D_raw(uint32_t x, uint32_t y) { +int32_t perlin2D_raw(uint32_t x, uint32_t y, bool is16bit) { int32_t x0 = x >> 16; int32_t y0 = y >> 16; int32_t x1 = x0 + 1; int32_t y1 = y0 + 1; + + if(is16bit) { + x1 = x1 & 0xFF; // wrap back to zero at 0xFF instead of 0xFFFF + y1 = y1 & 0xFF; + } + int32_t dx0 = x & 0xFFFF; int32_t dy0 = y & 0xFFFF; int32_t dx1 = dx0 - 0x10000; @@ -718,8 +723,7 @@ int32_t perlin2D_raw(uint32_t x, uint32_t y) { return noise; } -// 2D Perlin noise function that returns a value in range of approximately -40000 to +40000 -int32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z) { +int32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z, bool is16bit) { int32_t x0 = x >> 16; int32_t y0 = y >> 16; int32_t z0 = z >> 16; @@ -727,6 +731,12 @@ int32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z) { int32_t y1 = y0 + 1; int32_t z1 = z0 + 1; + if(is16bit) { + x1 = x1 & 0xFF; // wrap back to zero at 0xFF instead of 0xFFFF + y1 = y1 & 0xFF; + z1 = z1 & 0xFF; + } + int32_t dx0 = x & 0xFFFF; int32_t dy0 = y & 0xFFFF; int32_t dz0 = z & 0xFFFF; @@ -758,26 +768,28 @@ int32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z) { return noise; } // scaling functions for fastled replacement -uint8_t perlin8(uint16_t x) { - return (perlin1D_raw(uint32_t(x) << 8) >> 8) + 0x7F; -} - -uint8_t perlin8(uint16_t x, uint16_t y) { - return uint8_t((perlin2D_raw(uint32_t(x)<<8, uint32_t(y)<<8) >> 8) + 0x7F); -} - -uint8_t perlin8(uint16_t x, uint16_t y, uint16_t z) { - return ((perlin3D_raw(uint32_t(x)<<8, uint32_t(y)<<8, uint32_t(z)<<8) * 85) >> 15) + 0x7F; -} uint16_t perlin16(uint32_t x) { - return perlin1D_raw(x) + 0x7FFF; + //return ((perlin1D_raw(x) * 1168) >> 10) + 0x7FFF; //scale to 16bit and offset (full range) + return ((perlin1D_raw(x) * 895) >> 10) + 34616; //scale to 16bit and offset (fastled range) } uint16_t perlin16(uint32_t x, uint32_t y) { - return perlin2D_raw(x, y) + 0x7FFF; + return ((perlin2D_raw(x, y) * 1359) >> 10) + 31508; //scale to 16bit and offset (empirical values with some overflow safety margin) } uint16_t perlin16(uint32_t x, uint32_t y, uint32_t z) { - return perlin3D_raw(x, y, z) + 0x7FFF; // scale to signed 16bit range and offset + return ((perlin3D_raw(x, y, z) * 1923) >> 10) + 31290; //scale to 16bit and offset (empirical values with some overflow safety margin) +} + +uint8_t perlin8(uint16_t x) { + return (((perlin1D_raw((uint32_t)x << 8, true) * 1168) >> 10) + 0x7FFF) >> 8; +} + +uint8_t perlin8(uint16_t x, uint16_t y) { + return (((perlin2D_raw((uint32_t)x << 8, (uint32_t)y << 8, true) * 1359) >> 10) + 31508) >> 8; +} + +uint8_t perlin8(uint16_t x, uint16_t y, uint16_t z) { + return (((perlin3D_raw((uint32_t)x << 8, (uint32_t)y << 8, (uint32_t)z << 8, true) * 1923) >> 10) + 31290) >> 8; //scale to 8bit } \ No newline at end of file diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 639b7fe0c..ea615f3cb 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -344,19 +344,41 @@ void WLED::setup() uint32_t start; uint32_t end; uint32_t time; - uint8_t offset = hw_random(); + uint8_t offset = hw_random()+hw_random(); + delay(2000); + /* +//online serial plotter: https://sekigon-gonnoc.github.io/web-serial-plotter/ format is "valueA:213423, ValueB:123123, \n" + for(int i = 0; i < 0xFFFFFFF; i+=10) { + //Serial.print(inoise16(i, offset, (offset >> 3))); Serial.print(" "); //x + //Serial.print(inoise16(offset, i, (offset >> 3))); Serial.print(" "); //y + //Serial.print(inoise16(offset, (offset >> 3), i)); Serial.print(" "); //z + //Serial.print(perlin16(i, offset, (offset >> 3))); Serial.print(" "); //x + //Serial.print(perlin16(offset, i, (offset >> 3))); Serial.print(" "); //y + //Serial.print(perlin16(offset, (offset >> 3), i)); Serial.print(" "); //z + + //Serial.print("Fastled:");Serial.print(inoise16(i, offset+i/2, i + (offset >> 3))); Serial.print(", "); //mixed mode + //Serial.print("New:");Serial.println(perlin16(i, offset+i/2, i + (offset >> 3)));// Serial.println(", "); + + //Serial.print("Fastled:");Serial.print(inoise16(i, offset+i/2)); Serial.print(", "); //mixed mode + //Serial.print("New:");Serial.println(perlin16(i, offset+i/2));// Serial.println(", "); - for(int i = 0; i < 0xFFFFF; i+=500) { - Serial.print(inoise16(i, offset, (offset >> 3))); Serial.print(","); //x - Serial.print(inoise16(offset, i, (offset >> 3))); Serial.print(","); //y - Serial.print(inoise16(offset, (offset >> 3), i)); Serial.print(","); //z - Serial.print(perlin16(i, offset, (offset >> 3))); Serial.print(","); //x - Serial.print(perlin16(offset, i, (offset >> 3))); Serial.print(","); //y - Serial.print(perlin16(offset, (offset >> 3), i)); Serial.print(","); //z - Serial.print(inoise16(i, offset+i/2, i + (offset >> 3))); Serial.print(","); //mixed mode - Serial.print(perlin16(i, offset+i/2, i + (offset >> 3))); Serial.print(","); - Serial.println(perlin3D_raw(i, offset+i/4, i*2 + (offset >> 3))); //raw - } + //Serial.print("Fastled:");Serial.print(inoise16(i)); Serial.print(", "); //mixed mode + //Serial.print("New:");Serial.println(perlin16(i));// Serial.println(", "); + + Serial.print("Fastled3D:");Serial.print(inoise8(i, offset+i/2, i + (offset >> 3))); Serial.print(", "); //mixed mode + Serial.print("New3D:");Serial.print(perlin8(i, offset+i/2, i + (offset >> 3)));// Serial.println(", "); + Serial.print(", "); + Serial.print("Fastled2D:");Serial.print(inoise8(i, offset+i/2)); Serial.print(", "); //mixed mode + Serial.print("New2D:");Serial.print(perlin8(i, offset+i/2));// Serial.println(", "); + Serial.print(", "); + Serial.print("Fastled1D:");Serial.print(inoise8(i)); Serial.print(", "); //mixed mode + Serial.print("New1D:");Serial.println(perlin8(i));// Serial.println(", "); + + //Serial.print(inoise16(i, offset+i/2, i + (offset >> 3))); Serial.print(","); //mixed mode + //Serial.println(perlin16(i, offset+i/2, i + (offset >> 3)));// Serial.println(", "); + //delay(10); + // Serial.println(perlin3D_raw(i, offset+i/4, i*2 + (offset >> 3))); //raw + }*/ /* for(int i = 0; i < 0x2FFFF; i+=100) { @@ -419,10 +441,10 @@ void WLED::setup() minval=0xFFFFF; maxval=0; start = micros(); - for(int i = 0; i < 0xFFFFFFF; i+=50) { + for(int i = 0; i < 0xFFFFFF; i+=50) { uint32_t pos = i + offset; //int32_t noiseval = perlin16(hw_random()); - int32_t noiseval = perlin1D_raw(hw_random()); + int32_t noiseval = perlin1D_raw(hw_random(),false); if(noiseval < minval) minval = noiseval; if(noiseval > maxval) maxval = noiseval; } @@ -433,7 +455,7 @@ Serial.print(" perlin1D raw min: "); Serial.print(minval); Serial.print(" max: " minval=0xFFFFF; maxval=0; start = micros(); - for(int i = 0; i < 0xFFFFFFF; i+=50) { + for(int i = 0; i < 0xFFFFFF; i+=50) { uint32_t pos = i + offset; //int32_t noiseval = perlin16( hw_random(), hw_random()); int32_t noiseval = perlin2D_raw( hw_random(), hw_random()); @@ -448,7 +470,7 @@ Serial.print(" perlin1D raw min: "); Serial.print(minval); Serial.print(" max: " minval=0xFFFFF; maxval=0; - for(int i = 0; i < 0xFFFFFFF; i+=50) { + for(int i = 0; i < 0xFFFFFF; i+=50) { uint32_t pos = i + offset; //int32_t noiseval = perlin3D_raw(pos, pos+46845, pos+654684); //int32_t noiseval = perlin3D_raw(hw_random(), hw_random(), hw_random()); @@ -463,10 +485,10 @@ Serial.print(" perlin1D raw min: "); Serial.print(minval); Serial.print(" max: " minval=0xFFFFF; maxval=0; - for(int i = 0; i < 0xFFFFFFF; i+=50) { + for(int i = 0; i < 0xFFFFFF; i+=50) { uint32_t pos = i + offset; //int32_t noiseval = perlin3D_raw(pos, pos+46845, pos+654684); - int32_t noiseval = perlin3D_raw(hw_random(), hw_random(), hw_random()); + int32_t noiseval = perlin3D_raw(hw_random(), hw_random(), hw_random(),false); //int32_t noiseval = perlin16(hw_random(), hw_random(), hw_random()); if(noiseval < minval) minval = noiseval; if(noiseval > maxval) maxval = noiseval; From 95dcb03f6dcc058e3e7c56460a39ffe09b56e1e2 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 12 Mar 2025 06:56:33 +0100 Subject: [PATCH 016/153] updated scaling --- wled00/fcn_declare.h | 6 +- wled00/util.cpp | 28 ++++-- wled00/wled.cpp | 225 ------------------------------------------- 3 files changed, 23 insertions(+), 236 deletions(-) diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 7ee87eaee..9bc323d7f 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -519,12 +519,12 @@ uint32_t hashInt(uint32_t s); int32_t perlin1D_raw(uint32_t x, bool is16bit = false); int32_t perlin2D_raw(uint32_t x, uint32_t y, bool is16bit = false); int32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z, bool is16bit = false); -uint8_t perlin8(uint16_t x); -uint8_t perlin8(uint16_t x, uint16_t y); -uint8_t perlin8(uint16_t x, uint16_t y, uint16_t z); uint16_t perlin16(uint32_t x); uint16_t perlin16(uint32_t x, uint32_t y); uint16_t perlin16(uint32_t x, uint32_t y, uint32_t z); +uint8_t perlin8(uint16_t x); +uint8_t perlin8(uint16_t x, uint16_t y); +uint8_t perlin8(uint16_t x, uint16_t y, uint16_t z); // fast (true) random numbers using hardware RNG, all functions return values in the range lowerlimit to upperlimit-1 // note: for true random numbers with high entropy, do not call faster than every 200ns (5MHz) diff --git a/wled00/util.cpp b/wled00/util.cpp index 33973303b..07c0db121 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -631,7 +631,7 @@ static inline __attribute__((always_inline)) int32_t hashToGradient(uint32_t h) //return (h & 0xFF) - 128; // use PERLIN_SHIFT 7 //return (h & 0x0F) - 8; // use PERLIN_SHIFT 3 //return (h & 0x07) - 4; // use PERLIN_SHIFT 2 - return (h & 0x03) - 2; // use PERLIN_SHIFT 1 + return (h & 0x03) - 2; // use PERLIN_SHIFT 1 -> closest to original fastled version } // Gradient functions for 1D, 2D and 3D Perlin noise note: forcing inline produces smaller code and makes it 3x faster! @@ -658,6 +658,17 @@ static inline __attribute__((always_inline)) int32_t gradient3D(uint32_t x0, int h ^= h >> 15; h *= 0x92C3412B; h ^= h >> 13; + +/* + // fastled version: 25% slower but gives original "look" + h = h&15; + int32_t u = h<8?dx:dy; + int32_t v = h<4?dy:h==12||h==14?dx:dz; + if(h&1) { u = -u; } + if(h&2) { v = -v; } + return (u >> 1) + (v >> 1) + (u & 0x1); +*/ + // closer to actual perlin version return ((hashToGradient(h) * dx + hashToGradient(h>>(1+PERLIN_SHIFT)) * dy + hashToGradient(h>>(1 + 2*PERLIN_SHIFT)) * dz) * 85) >> (8 + PERLIN_SHIFT); // scale to 16bit, x*85 >> 8 = x/3 } @@ -665,7 +676,7 @@ static inline __attribute__((always_inline)) int32_t gradient3D(uint32_t x0, int static uint32_t smoothstep(const uint32_t t) { uint32_t t_squared = (t * t) >> 16; uint32_t factor = (3 << 16) - ((t << 1)); - return (t_squared * factor) >> 18; // scale to avoid overflows + return (t_squared * factor) >> 18; // scale to avoid overflows and give best resolution } // simple linear interpolation for fixed-point values, scaled for perlin noise use @@ -771,25 +782,26 @@ int32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z, bool is16bit) { uint16_t perlin16(uint32_t x) { //return ((perlin1D_raw(x) * 1168) >> 10) + 0x7FFF; //scale to 16bit and offset (full range) - return ((perlin1D_raw(x) * 895) >> 10) + 34616; //scale to 16bit and offset (fastled range) + //return ((perlin1D_raw(x) * 895) >> 10) + 34616; //scale to 16bit and offset (fastled range) -> 8 steps + return ((perlin1D_raw(x) * 1159) >> 10) + 32803; //scale to 16bit and offset (fastled range) -> 8 steps } uint16_t perlin16(uint32_t x, uint32_t y) { - return ((perlin2D_raw(x, y) * 1359) >> 10) + 31508; //scale to 16bit and offset (empirical values with some overflow safety margin) + return ((perlin2D_raw(x, y) * 1537) >> 10) + 32725; //scale to 16bit and offset (empirical values with some overflow safety margin) } uint16_t perlin16(uint32_t x, uint32_t y, uint32_t z) { - return ((perlin3D_raw(x, y, z) * 1923) >> 10) + 31290; //scale to 16bit and offset (empirical values with some overflow safety margin) + return ((perlin3D_raw(x, y, z) * 1731) >> 10) + 33147; //scale to 16bit and offset (empirical values with some overflow safety margin) } uint8_t perlin8(uint16_t x) { - return (((perlin1D_raw((uint32_t)x << 8, true) * 1168) >> 10) + 0x7FFF) >> 8; + return (((perlin1D_raw((uint32_t)x << 8, true) * 1353) >> 10) + 32769) >> 8; } uint8_t perlin8(uint16_t x, uint16_t y) { - return (((perlin2D_raw((uint32_t)x << 8, (uint32_t)y << 8, true) * 1359) >> 10) + 31508) >> 8; + return (((perlin2D_raw((uint32_t)x << 8, (uint32_t)y << 8, true) * 1620) >> 10) + 32771) >> 8; } uint8_t perlin8(uint16_t x, uint16_t y, uint16_t z) { - return (((perlin3D_raw((uint32_t)x << 8, (uint32_t)y << 8, (uint32_t)z << 8, true) * 1923) >> 10) + 31290) >> 8; //scale to 8bit + return (((perlin3D_raw((uint32_t)x << 8, (uint32_t)y << 8, (uint32_t)z << 8, true) * 2015) >> 10) + 33168) >> 8; //scale to 8bit } \ No newline at end of file diff --git a/wled00/wled.cpp b/wled00/wled.cpp index ea615f3cb..1d42e8c11 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -339,231 +339,6 @@ void WLED::setup() #else DEBUG_PRINTLN(F("arduino-esp32 v1.0.x\n")); // we can't say in more detail. #endif - - - uint32_t start; - uint32_t end; - uint32_t time; - uint8_t offset = hw_random()+hw_random(); - delay(2000); - /* -//online serial plotter: https://sekigon-gonnoc.github.io/web-serial-plotter/ format is "valueA:213423, ValueB:123123, \n" - for(int i = 0; i < 0xFFFFFFF; i+=10) { - //Serial.print(inoise16(i, offset, (offset >> 3))); Serial.print(" "); //x - //Serial.print(inoise16(offset, i, (offset >> 3))); Serial.print(" "); //y - //Serial.print(inoise16(offset, (offset >> 3), i)); Serial.print(" "); //z - //Serial.print(perlin16(i, offset, (offset >> 3))); Serial.print(" "); //x - //Serial.print(perlin16(offset, i, (offset >> 3))); Serial.print(" "); //y - //Serial.print(perlin16(offset, (offset >> 3), i)); Serial.print(" "); //z - - //Serial.print("Fastled:");Serial.print(inoise16(i, offset+i/2, i + (offset >> 3))); Serial.print(", "); //mixed mode - //Serial.print("New:");Serial.println(perlin16(i, offset+i/2, i + (offset >> 3)));// Serial.println(", "); - - //Serial.print("Fastled:");Serial.print(inoise16(i, offset+i/2)); Serial.print(", "); //mixed mode - //Serial.print("New:");Serial.println(perlin16(i, offset+i/2));// Serial.println(", "); - - //Serial.print("Fastled:");Serial.print(inoise16(i)); Serial.print(", "); //mixed mode - //Serial.print("New:");Serial.println(perlin16(i));// Serial.println(", "); - - Serial.print("Fastled3D:");Serial.print(inoise8(i, offset+i/2, i + (offset >> 3))); Serial.print(", "); //mixed mode - Serial.print("New3D:");Serial.print(perlin8(i, offset+i/2, i + (offset >> 3)));// Serial.println(", "); - Serial.print(", "); - Serial.print("Fastled2D:");Serial.print(inoise8(i, offset+i/2)); Serial.print(", "); //mixed mode - Serial.print("New2D:");Serial.print(perlin8(i, offset+i/2));// Serial.println(", "); - Serial.print(", "); - Serial.print("Fastled1D:");Serial.print(inoise8(i)); Serial.print(", "); //mixed mode - Serial.print("New1D:");Serial.println(perlin8(i));// Serial.println(", "); - - //Serial.print(inoise16(i, offset+i/2, i + (offset >> 3))); Serial.print(","); //mixed mode - //Serial.println(perlin16(i, offset+i/2, i + (offset >> 3)));// Serial.println(", "); - //delay(10); - // Serial.println(perlin3D_raw(i, offset+i/4, i*2 + (offset >> 3))); //raw - }*/ - -/* - for(int i = 0; i < 0x2FFFF; i+=100) { - uint32_t pos = i + offset; - Serial.print(inoise8_raw((pos)>>3, (pos)>>3)); Serial.print(","); - Serial.print(inoise8((pos)>>3, (pos)>>3, (pos)>>3)); Serial.print(","); - Serial.print(inoise16(pos*20, pos*30)); Serial.print(","); - //Serial.print(inoise16_raw(pos*20, pos*30, pos*40)); Serial.print(","); - Serial.print(inoise16(pos*20, pos*20, pos*20)); Serial.print(","); - //Serial.print(((perlin1D_raw(pos*20)* 85)>>7) + 0x7FFF); Serial.print(","); - Serial.print(perlin1D_raw(pos*20)); Serial.print(","); - Serial.print(perlin2D_raw(pos*20, pos*20)); Serial.print(","); - //Serial.print(perlin2D_raw(pos*20, pos*30) + 0x7FFF); Serial.print(","); - Serial.println(perlin3D_raw(pos*20, pos*20, pos*20)); - //Serial.println(((perlin3D_raw(pos*20, pos*30, pos*40) * 85)>>7) + 0x7FFF); - }*/ - -/* - for(int i = 0; i < 0xF0000; i+=55) { - Serial.print(inoise8_raw(i,i+5648) / 2); Serial.print(","); // +/-32 ? - Serial.print(((int16_t)perlin8(i,i+5648) - 0x7F) >> 2); Serial.print(","); - Serial.print(inoise8(i,i/3,i/5)); Serial.print(","); - Serial.print(perlin8(i,i/3,i/5)); Serial.print(","); - Serial.print(inoise8(i,i/3)); Serial.print(","); - Serial.print(perlin8(i,i/3)); Serial.print(","); - Serial.print(inoise8(i)); Serial.print(","); - Serial.println(perlin8(i)); - } -*/ - int32_t minval=0xFFFFF; - int32_t maxval=0; - start = micros(); - for(int i = 0; i < 0xFFFFF; i+=50) { - uint16_t pos = i + offset; - int32_t noiseval = inoise8_raw(pos); - if(noiseval < minval) minval = noiseval; - if(noiseval > maxval) maxval = noiseval; - } - end = micros(); - time = end - start; - Serial.print("time: "); Serial.print(time); - Serial.print(" inoise8_raw min: "); Serial.print(minval); Serial.print(" max: "); Serial.println(maxval); - - minval=0xFFFFF; - maxval=0; - - start = micros(); - for(int i = 0; i < 0xFFFFFF; i+=50) { - uint32_t pos = i + offset; - //int32_t noiseval = inoise16(pos, pos+4684165, pos+985685); - int32_t noiseval = inoise16(hw_random(), hw_random(), hw_random()); - if(noiseval < minval) minval = noiseval; - if(noiseval > maxval) maxval = noiseval; - } - end = micros(); - time = end - start; - Serial.print("time: "); Serial.print(time); - Serial.print(" inoise16_3D min: "); Serial.print(minval); Serial.print(" max: "); Serial.println(maxval); - - minval=0xFFFFF; - maxval=0; - start = micros(); - for(int i = 0; i < 0xFFFFFF; i+=50) { - uint32_t pos = i + offset; - //int32_t noiseval = perlin16(hw_random()); - int32_t noiseval = perlin1D_raw(hw_random(),false); - if(noiseval < minval) minval = noiseval; - if(noiseval > maxval) maxval = noiseval; - } - end = micros(); - time = end - start; - Serial.print("time: "); Serial.print(time); -Serial.print(" perlin1D raw min: "); Serial.print(minval); Serial.print(" max: "); Serial.println(maxval); - minval=0xFFFFF; - maxval=0; - start = micros(); - for(int i = 0; i < 0xFFFFFF; i+=50) { - uint32_t pos = i + offset; - //int32_t noiseval = perlin16( hw_random(), hw_random()); - int32_t noiseval = perlin2D_raw( hw_random(), hw_random()); - if(noiseval < minval) minval = noiseval; - if(noiseval > maxval) maxval = noiseval; - } - end = micros(); - time = end - start; - Serial.print("time: "); Serial.print(time); - Serial.print(" perlin2D raw min: "); Serial.print(minval); Serial.print(" max: "); Serial.println(maxval); - - - minval=0xFFFFF; - maxval=0; - for(int i = 0; i < 0xFFFFFF; i+=50) { - uint32_t pos = i + offset; - //int32_t noiseval = perlin3D_raw(pos, pos+46845, pos+654684); - //int32_t noiseval = perlin3D_raw(hw_random(), hw_random(), hw_random()); - int32_t noiseval = perlin16(hw_random(), hw_random(), hw_random()); - if(noiseval < minval) minval = noiseval; - if(noiseval > maxval) maxval = noiseval; - } - end = micros(); - time = end - start; - Serial.print("time: "); Serial.print(time); - Serial.print(" perlin16 min: "); Serial.print(minval); Serial.print(" max: "); Serial.println(maxval); - - minval=0xFFFFF; - maxval=0; - for(int i = 0; i < 0xFFFFFF; i+=50) { - uint32_t pos = i + offset; - //int32_t noiseval = perlin3D_raw(pos, pos+46845, pos+654684); - int32_t noiseval = perlin3D_raw(hw_random(), hw_random(), hw_random(),false); - //int32_t noiseval = perlin16(hw_random(), hw_random(), hw_random()); - if(noiseval < minval) minval = noiseval; - if(noiseval > maxval) maxval = noiseval; - } - end = micros(); - time = end - start; - Serial.print("time: "); Serial.print(time); - Serial.print(" perlin3D_raw min: "); Serial.print(minval); Serial.print(" max: "); Serial.println(maxval); - - - - volatile uint32_t temp; - start = micros(); - for(int i = 0; i < 100000; i++){ - temp += inoise8(i); - } - end = micros(); - time = end - start; - Serial.print("inoise8: "); - Serial.print(temp); - Serial.print("time: "); - Serial.println(time); - start = micros(); - for(int i = 0; i < 100000; i++){ - temp += inoise16(i,i<<1,i<<2); - } - end = micros(); - time = end - start; - Serial.print("inoise16:"); - Serial.print(temp); - Serial.print("time: "); - Serial.println(time); - start = micros(); - for(int i = 0; i < 100000; i++){ - temp += perlin1D_raw(i); - } - end = micros(); - time = end - start; - Serial.print("perlin1Draw:"); - Serial.print(temp); - Serial.print("time: "); - Serial.println(time); - start = micros(); - for(int i = 0; i < 100000; i++){ - temp += perlin2D_raw(i,i*33); - } - end = micros(); - time = end - start; - Serial.print("perlin2Draw:"); - Serial.print(temp); - Serial.print("time: "); - Serial.println(time); - start = micros(); - for(int i = 0; i < 100000; i++){ - temp += perlin16(i,i*33,i*17); - } - end = micros(); - time = end - start; - Serial.print("perlin163D:"); - Serial.print(temp); - Serial.print("time: "); - Serial.println(time); - - start = micros(); - for(int i = 0; i < 100000; i++){ - temp += perlin3D_raw(i,i*33,i*17); - } - end = micros(); - time = end - start; - Serial.print("perlin3D raw:"); - Serial.print(temp); - Serial.print("time: "); - Serial.println(time); - - 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 From 229e7b940fb5df89391a20de598e48bb3a238ec7 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 12 Mar 2025 19:58:32 +0100 Subject: [PATCH 017/153] added ranges, removed unused code --- wled00/util.cpp | 40 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/wled00/util.cpp b/wled00/util.cpp index 07c0db121..ac8a16207 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -627,10 +627,10 @@ int32_t hw_random(int32_t lowerlimit, int32_t upperlimit) { // calculate gradient for corner from hash value static inline __attribute__((always_inline)) int32_t hashToGradient(uint32_t h) { - // using more steps yields more "detailed" perlin noise but looks less like the original fastled version (adjust PERLIN_SHIFT to compensate) - //return (h & 0xFF) - 128; // use PERLIN_SHIFT 7 - //return (h & 0x0F) - 8; // use PERLIN_SHIFT 3 - //return (h & 0x07) - 4; // use PERLIN_SHIFT 2 + // using more steps yields more "detailed" perlin noise but looks less like the original fastled version (adjust PERLIN_SHIFT to compensate, also changes range and needs proper adustment) + // return (h & 0xFF) - 128; // use PERLIN_SHIFT 7 + // return (h & 0x0F) - 8; // use PERLIN_SHIFT 3 + // return (h & 0x07) - 4; // use PERLIN_SHIFT 2 return (h & 0x03) - 2; // use PERLIN_SHIFT 1 -> closest to original fastled version } @@ -658,17 +658,6 @@ static inline __attribute__((always_inline)) int32_t gradient3D(uint32_t x0, int h ^= h >> 15; h *= 0x92C3412B; h ^= h >> 13; - -/* - // fastled version: 25% slower but gives original "look" - h = h&15; - int32_t u = h<8?dx:dy; - int32_t v = h<4?dy:h==12||h==14?dx:dz; - if(h&1) { u = -u; } - if(h&2) { v = -v; } - return (u >> 1) + (v >> 1) + (u & 0x1); -*/ - // closer to actual perlin version return ((hashToGradient(h) * dx + hashToGradient(h>>(1+PERLIN_SHIFT)) * dy + hashToGradient(h>>(1 + 2*PERLIN_SHIFT)) * dz) * 85) >> (8 + PERLIN_SHIFT); // scale to 16bit, x*85 >> 8 = x/3 } @@ -684,7 +673,7 @@ static inline int32_t lerpPerlin(int32_t a, int32_t b, int32_t t) { return a + (((b - a) * t) >> 14); // match scaling with smoothstep to yield 16.16bit values } -// 1D Perlin noise function that returns a value in range of approximately -32768 to +32768 +// 1D Perlin noise function that returns a value in range of -24691 to 24689 int32_t perlin1D_raw(uint32_t x, bool is16bit) { // integer and fractional part coordinates int32_t x0 = x >> 16; @@ -702,7 +691,7 @@ int32_t perlin1D_raw(uint32_t x, bool is16bit) { return noise; } -// 2D Perlin noise function that returns a value in range of approximately -32768 to +32768 +// 2D Perlin noise function that returns a value in range of -20633 to 20629 int32_t perlin2D_raw(uint32_t x, uint32_t y, bool is16bit) { int32_t x0 = x >> 16; int32_t y0 = y >> 16; @@ -734,6 +723,7 @@ int32_t perlin2D_raw(uint32_t x, uint32_t y, bool is16bit) { return noise; } +// 3D Perlin noise function that returns a value in range of -16788 to 16381 int32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z, bool is16bit) { int32_t x0 = x >> 16; int32_t y0 = y >> 16; @@ -778,30 +768,28 @@ int32_t perlin3D_raw(uint32_t x, uint32_t y, uint32_t z, bool is16bit) { int32_t noise = lerpPerlin(ny0, ny1, tz); return noise; } -// scaling functions for fastled replacement +// scaling functions for fastled replacement uint16_t perlin16(uint32_t x) { - //return ((perlin1D_raw(x) * 1168) >> 10) + 0x7FFF; //scale to 16bit and offset (full range) - //return ((perlin1D_raw(x) * 895) >> 10) + 34616; //scale to 16bit and offset (fastled range) -> 8 steps - return ((perlin1D_raw(x) * 1159) >> 10) + 32803; //scale to 16bit and offset (fastled range) -> 8 steps + return ((perlin1D_raw(x) * 1159) >> 10) + 32803; //scale to 16bit and offset (fastled range: about 4838 to 60766) } uint16_t perlin16(uint32_t x, uint32_t y) { - return ((perlin2D_raw(x, y) * 1537) >> 10) + 32725; //scale to 16bit and offset (empirical values with some overflow safety margin) + return ((perlin2D_raw(x, y) * 1537) >> 10) + 32725; //scale to 16bit and offset (fastled range: about 1748 to 63697) } uint16_t perlin16(uint32_t x, uint32_t y, uint32_t z) { - return ((perlin3D_raw(x, y, z) * 1731) >> 10) + 33147; //scale to 16bit and offset (empirical values with some overflow safety margin) + return ((perlin3D_raw(x, y, z) * 1731) >> 10) + 33147; //scale to 16bit and offset (fastled range: about 4766 to 60840) } uint8_t perlin8(uint16_t x) { - return (((perlin1D_raw((uint32_t)x << 8, true) * 1353) >> 10) + 32769) >> 8; + return (((perlin1D_raw((uint32_t)x << 8, true) * 1353) >> 10) + 32769) >> 8; //scale to 16 bit, offset, then scale to 8bit } uint8_t perlin8(uint16_t x, uint16_t y) { - return (((perlin2D_raw((uint32_t)x << 8, (uint32_t)y << 8, true) * 1620) >> 10) + 32771) >> 8; + return (((perlin2D_raw((uint32_t)x << 8, (uint32_t)y << 8, true) * 1620) >> 10) + 32771) >> 8; //scale to 16 bit, offset, then scale to 8bit } uint8_t perlin8(uint16_t x, uint16_t y, uint16_t z) { - return (((perlin3D_raw((uint32_t)x << 8, (uint32_t)y << 8, (uint32_t)z << 8, true) * 2015) >> 10) + 33168) >> 8; //scale to 8bit + return (((perlin3D_raw((uint32_t)x << 8, (uint32_t)y << 8, (uint32_t)z << 8, true) * 2015) >> 10) + 33168) >> 8; //scale to 16 bit, offset, then scale to 8bit } \ No newline at end of file From d2b7e474d6147f33d3ae7e8e0ca04f33936f08dd Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 14 Mar 2025 06:48:18 +0100 Subject: [PATCH 018/153] add legacy defines for compatibility, reverted test changes in rotozoomer --- wled00/FX.cpp | 23 ++++++++++++++--------- wled00/fcn_declare.h | 2 ++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 1bba265dc..756750f27 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -6221,12 +6221,22 @@ uint16_t mode_2Dplasmarotozoom() { const int cols = SEG_W; const int rows = SEG_H; - unsigned dataSize = 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)); unsigned ms = strip.now/15; + // plasma + for (int j = 0; j < rows; j++) { + int index = j*cols; + for (int i = 0; i < cols; i++) { + if (SEGMENT.check1) plasma[index+i] = (i * 4 ^ j * 4) + ms / 6; + else plasma[index+i] = inoise8(i * 40, j * 40, ms); + } + } + // rotozoom float f = (sin_t(*a/2)+((128-SEGMENT.intensity)/128.0f)+1.1f)/1.5f; // scale factor float kosinus = cos_t(*a) * f; @@ -6235,14 +6245,9 @@ uint16_t mode_2Dplasmarotozoom() { float u1 = i * kosinus; float v1 = i * sinus; for (int j = 0; j < rows; j++) { - unsigned u = abs8(u1 - j * sinus) % cols; - unsigned v = abs8(v1 + j * kosinus) % rows; - byte plasma; - if (SEGMENT.check1) plasma = (u * 4 ^ v * 4) + ms / 6; - else plasma = perlin8(u * 40, v * 40, ms); - //else plasma = inoise8(u * SEGMENT.intensity, v * SEGMENT.intensity, ms); - //SEGMENT.setPixelColorXY(i, j, SEGMENT.color_from_palette(plasma[v*cols+u], false, PALETTE_SOLID_WRAP, 255)); - SEGMENT.setPixelColorXY(i, j, SEGMENT.color_from_palette(plasma, false, PALETTE_SOLID_WRAP, 255)); + 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, 255)); } } *a -= 0.03f + float(SEGENV.speed-128)*0.0002f; // rotation speed diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 4f7603f50..bb7114ed5 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -486,6 +486,8 @@ void userLoop(); #include "soc/wdev_reg.h" #define HW_RND_REGISTER REG_READ(WDEV_RND_REG) #endif +#define inoise8 perlin8 // fastled legacy alias +#define inoise16 perlin16 // fastled legacy alias #define hex2int(a) (((a)>='0' && (a)<='9') ? (a)-'0' : ((a)>='A' && (a)<='F') ? (a)-'A'+10 : ((a)>='a' && (a)<='f') ? (a)-'a'+10 : 0) [[gnu::pure]] int getNumVal(const String* req, uint16_t pos); void parseNumber(const char* str, byte* val, byte minv=0, byte maxv=255); From a0d1a8cbc4690ac730e75b1f74494742bdf43a4a Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 22 Mar 2025 11:53:05 -0400 Subject: [PATCH 019/153] Use enum class for json endpoint subtypes Part of the ongoing quest to migrate macro definitions to typed language constructs. This actually yields a small improvement in code size, likely from the byte->int conversion. --- wled00/json.cpp | 52 +++++++++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/wled00/json.cpp b/wled00/json.cpp index 24988be15..0a307594a 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -2,15 +2,6 @@ #include "palettes.h" -#define JSON_PATH_STATE 1 -#define JSON_PATH_INFO 2 -#define JSON_PATH_STATE_INFO 3 -#define JSON_PATH_NODES 4 -#define JSON_PATH_PALETTES 5 -#define JSON_PATH_FXDATA 6 -#define JSON_PATH_NETWORKS 7 -#define JSON_PATH_EFFECTS 8 - /* * JSON API (De)serialization */ @@ -1036,16 +1027,20 @@ class LockedJsonResponse: public AsyncJsonResponse { void serveJson(AsyncWebServerRequest* request) { - byte subJson = 0; + enum class json_target { + all, state, info, state_info, nodes, effects, palettes, fxdata, networks + }; + json_target subJson = json_target::all; + const String& url = request->url(); - if (url.indexOf("state") > 0) subJson = JSON_PATH_STATE; - else if (url.indexOf("info") > 0) subJson = JSON_PATH_INFO; - else if (url.indexOf("si") > 0) subJson = JSON_PATH_STATE_INFO; - else if (url.indexOf(F("nodes")) > 0) subJson = JSON_PATH_NODES; - else if (url.indexOf(F("eff")) > 0) subJson = JSON_PATH_EFFECTS; - else if (url.indexOf(F("palx")) > 0) subJson = JSON_PATH_PALETTES; - else if (url.indexOf(F("fxda")) > 0) subJson = JSON_PATH_FXDATA; - else if (url.indexOf(F("net")) > 0) subJson = JSON_PATH_NETWORKS; + if (url.indexOf("state") > 0) subJson = json_target::state; + else if (url.indexOf("info") > 0) subJson = json_target::info; + else if (url.indexOf("si") > 0) subJson = json_target::state_info; + else if (url.indexOf(F("nodes")) > 0) subJson = json_target::nodes; + else if (url.indexOf(F("eff")) > 0) subJson = json_target::effects; + else if (url.indexOf(F("palx")) > 0) subJson = json_target::palettes; + else if (url.indexOf(F("fxda")) > 0) subJson = json_target::fxdata; + else if (url.indexOf(F("net")) > 0) subJson = json_target::networks; #ifdef WLED_ENABLE_JSONLIVE else if (url.indexOf("live") > 0) { serveLiveLeds(request); @@ -1070,32 +1065,33 @@ void serveJson(AsyncWebServerRequest* request) } // releaseJSONBufferLock() will be called when "response" is destroyed (from AsyncWebServer) // make sure you delete "response" if no "request->send(response);" is made - LockedJsonResponse *response = new LockedJsonResponse(pDoc, subJson==JSON_PATH_FXDATA || subJson==JSON_PATH_EFFECTS); // will clear and convert JsonDocument into JsonArray if necessary + LockedJsonResponse *response = new LockedJsonResponse(pDoc, subJson==json_target::fxdata || subJson==json_target::effects); // will clear and convert JsonDocument into JsonArray if necessary JsonVariant lDoc = response->getRoot(); switch (subJson) { - case JSON_PATH_STATE: + case json_target::state: serializeState(lDoc); break; - case JSON_PATH_INFO: + case json_target::info: serializeInfo(lDoc); break; - case JSON_PATH_NODES: + case json_target::nodes: serializeNodes(lDoc); break; - case JSON_PATH_PALETTES: + case json_target::palettes: serializePalettes(lDoc, request->hasParam(F("page")) ? request->getParam(F("page"))->value().toInt() : 0); break; - case JSON_PATH_EFFECTS: + case json_target::effects: serializeModeNames(lDoc); break; - case JSON_PATH_FXDATA: + case json_target::fxdata: serializeModeData(lDoc); break; - case JSON_PATH_NETWORKS: + case json_target::networks: serializeNetworks(lDoc); break; - default: //all + case json_target::state_info: + case json_target::all: JsonObject state = lDoc.createNestedObject("state"); serializeState(state); JsonObject info = lDoc.createNestedObject("info"); serializeInfo(info); - if (subJson != JSON_PATH_STATE_INFO) + if (subJson == json_target::all) { JsonArray effects = lDoc.createNestedArray(F("effects")); serializeModeNames(effects); // remove WLED-SR extensions from effect names From e21a09cec94ef607eaa286c21b42af6998a66381 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 23 Mar 2025 15:15:52 -0400 Subject: [PATCH 020/153] Separate FS write from serializeConfig Break the actual JSON assembly apart from the file writing code. This permits calling it in other contexts, allowing us to pull the live config data even if the filesystem is out of date. --- wled00/cfg.cpp | 24 ++++++++++++++---------- wled00/fcn_declare.h | 3 ++- wled00/improv.cpp | 2 +- wled00/wled.cpp | 2 +- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 11862f83f..194ef4fbf 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -671,7 +671,7 @@ void deserializeConfigFromFS() { // call readFromConfig() with an empty object so that usermods can initialize to defaults prior to saving JsonObject empty = JsonObject(); UsermodManager::readFromConfig(empty); - serializeConfig(); + serializeConfigToFS(); // init Ethernet (in case default type is set at compile time) #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) initEthernet(); @@ -685,10 +685,10 @@ void deserializeConfigFromFS() { bool needsSave = deserializeConfig(root, true); releaseJSONBufferLock(); - if (needsSave) serializeConfig(); // usermods required new parameters + if (needsSave) serializeConfigToFS(); // usermods required new parameters } -void serializeConfig() { +void serializeConfigToFS() { serializeConfigSec(); DEBUG_PRINTLN(F("Writing settings to /cfg.json...")); @@ -697,6 +697,17 @@ void serializeConfig() { JsonObject root = pDoc->to(); + serializeConfig(root); + + File f = WLED_FS.open(FPSTR(s_cfg_json), "w"); + if (f) serializeJson(root, f); + f.close(); + releaseJSONBufferLock(); + + doSerializeConfig = false; +} + +void serializeConfig(JsonObject root) { JsonArray rev = root.createNestedArray("rev"); rev.add(1); //major settings revision rev.add(0); //minor settings revision @@ -1111,13 +1122,6 @@ void serializeConfig() { JsonObject usermods_settings = root.createNestedObject("um"); UsermodManager::addToConfig(usermods_settings); - - File f = WLED_FS.open(FPSTR(s_cfg_json), "w"); - if (f) serializeJson(root, f); - f.close(); - releaseJSONBufferLock(); - - doSerializeConfig = false; } diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 497a775ee..0f4666b30 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -27,7 +27,8 @@ void IRAM_ATTR touchButtonISR(); bool deserializeConfig(JsonObject doc, bool fromFS = false); void deserializeConfigFromFS(); bool deserializeConfigSec(); -void serializeConfig(); +void serializeConfig(JsonObject doc); +void serializeConfigToFS(); void serializeConfigSec(); template diff --git a/wled00/improv.cpp b/wled00/improv.cpp index 197148b2b..0bc7a6698 100644 --- a/wled00/improv.cpp +++ b/wled00/improv.cpp @@ -272,5 +272,5 @@ void parseWiFiCommand(char* rpcData) { improvActive = 2; forceReconnect = true; - serializeConfig(); + serializeConfigToFS(); } \ No newline at end of file diff --git a/wled00/wled.cpp b/wled00/wled.cpp index 34caeefa3..b2cf26440 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -200,7 +200,7 @@ void WLED::loop() loadLedmap = -1; } yield(); - if (doSerializeConfig) serializeConfig(); + if (doSerializeConfig) serializeConfigToFS(); yield(); handleWs(); From 9c8f8c645ebd1f69f004a8c3716af4504ff394d6 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 23 Mar 2025 15:16:52 -0400 Subject: [PATCH 021/153] Rename 'doSerializeConfig' to 'configNeedsWrite' Clarify the name and usage of this flag, as the function name has changed out from underneath it. --- wled00/cfg.cpp | 2 +- wled00/dmx_input.cpp | 4 ++-- wled00/presets.cpp | 2 +- wled00/set.cpp | 4 ++-- wled00/wled.cpp | 6 +++--- wled00/wled.h | 2 +- wled00/wled_server.cpp | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 194ef4fbf..fa0397fc6 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -704,7 +704,7 @@ void serializeConfigToFS() { f.close(); releaseJSONBufferLock(); - doSerializeConfig = false; + configNeedsWrite = false; } void serializeConfig(JsonObject root) { diff --git a/wled00/dmx_input.cpp b/wled00/dmx_input.cpp index 3197375f1..59467590c 100644 --- a/wled00/dmx_input.cpp +++ b/wled00/dmx_input.cpp @@ -22,7 +22,7 @@ void rdmPersonalityChangedCb(dmx_port_t dmxPort, const rdm_header_t *header, if (header->cc == RDM_CC_SET_COMMAND_RESPONSE) { const uint8_t personality = dmx_get_current_personality(dmx->inputPortNum); DMXMode = std::min(DMX_MODE_PRESET, std::max(DMX_MODE_SINGLE_RGB, int(personality))); - doSerializeConfig = true; + configNeedsWrite = true; DEBUG_PRINTF("DMX personality changed to to: %d\n", DMXMode); } } @@ -40,7 +40,7 @@ void rdmAddressChangedCb(dmx_port_t dmxPort, const rdm_header_t *header, if (header->cc == RDM_CC_SET_COMMAND_RESPONSE) { const uint16_t addr = dmx_get_start_address(dmx->inputPortNum); DMXAddress = std::min(512, int(addr)); - doSerializeConfig = true; + configNeedsWrite = true; DEBUG_PRINTF("DMX start addr changed to: %d\n", DMXAddress); } } diff --git a/wled00/presets.cpp b/wled00/presets.cpp index 54f052637..b749289bd 100644 --- a/wled00/presets.cpp +++ b/wled00/presets.cpp @@ -242,7 +242,7 @@ void savePreset(byte index, const char* pname, JsonObject sObj) if (!sObj[FPSTR(bootPS)].isNull()) { bootPreset = sObj[FPSTR(bootPS)] | bootPreset; sObj.remove(FPSTR(bootPS)); - doSerializeConfig = true; + configNeedsWrite = true; } if (sObj.size()==0 || sObj["o"].isNull()) { // no "o" means not a playlist or custom API call, saving of state is async (not immediately) diff --git a/wled00/set.cpp b/wled00/set.cpp index 00333788d..c817f2553 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -805,8 +805,8 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) lastEditTime = millis(); // do not save if factory reset or LED settings (which are saved after LED re-init) - doSerializeConfig = subPage != SUBPAGE_LEDS && !(subPage == SUBPAGE_SEC && doReboot); - if (subPage == SUBPAGE_UM) doReboot = request->hasArg(F("RBT")); // prevent race condition on dual core system (set reboot here, after doSerializeConfig has been set) + configNeedsWrite = subPage != SUBPAGE_LEDS && !(subPage == SUBPAGE_SEC && doReboot); + if (subPage == SUBPAGE_UM) doReboot = request->hasArg(F("RBT")); // prevent race condition on dual core system (set reboot here, after configNeedsWrite has been set) #ifndef WLED_DISABLE_ALEXA if (subPage == SUBPAGE_SYNC) alexaInit(); #endif diff --git a/wled00/wled.cpp b/wled00/wled.cpp index b2cf26440..9683b432d 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -193,14 +193,14 @@ void WLED::loop() if (aligned) strip.makeAutoSegments(); else strip.fixInvalidSegments(); BusManager::setBrightness(bri); // fix re-initialised bus' brightness - doSerializeConfig = true; + configNeedsWrite = true; } if (loadLedmap >= 0) { strip.deserializeMap(loadLedmap); loadLedmap = -1; } yield(); - if (doSerializeConfig) serializeConfigToFS(); + if (configNeedsWrite) serializeConfigToFS(); yield(); handleWs(); @@ -223,7 +223,7 @@ void WLED::loop() } #endif - if (doReboot && (!doInitBusses || !doSerializeConfig)) // if busses have to be inited & saved, wait until next iteration + if (doReboot && (!doInitBusses || !configNeedsWrite)) // if busses have to be inited & saved, wait until next iteration reset(); // DEBUG serial logging (every 30s) diff --git a/wled00/wled.h b/wled00/wled.h index ea40c5dfe..8926967ff 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -877,7 +877,7 @@ WLED_GLOBAL byte errorFlag _INIT(0); WLED_GLOBAL String messageHead, messageSub; WLED_GLOBAL byte optionType; -WLED_GLOBAL bool doSerializeConfig _INIT(false); // flag to initiate saving of config +WLED_GLOBAL bool configNeedsWrite _INIT(false); // flag to initiate saving of config WLED_GLOBAL bool doReboot _INIT(false); // flag to initiate reboot from async handlers WLED_GLOBAL bool psramSafe _INIT(true); // is it safe to use PSRAM (on ESP32 rev.1; compiler fix used "-mfix-esp32-psram-cache-issue") diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 77f4133c0..06750838f 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -328,7 +328,7 @@ void initServer() interfaceUpdateCallMode = CALL_MODE_WS_SEND; // schedule WS update serveJson(request); return; //if JSON contains "v" } else { - doSerializeConfig = true; //serializeConfig(); //Save new settings to FS + configNeedsWrite = true; //Save new settings to FS } } request->send(200, CONTENT_TYPE_JSON, F("{\"success\":true}")); From 22e2b6f3c517233eaca2e0972790e10d38db20b5 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 23 Mar 2025 15:18:08 -0400 Subject: [PATCH 022/153] Have json/cfg return live config Rather than reading the file off disk, have the json/cfg endpoint return the live config from system state data. This can improve UI behaviour as it can never be out of date or include values that do not apply to the current firmware install. --- wled00/json.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wled00/json.cpp b/wled00/json.cpp index 0a307594a..c09b543f1 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -1028,7 +1028,7 @@ class LockedJsonResponse: public AsyncJsonResponse { void serveJson(AsyncWebServerRequest* request) { enum class json_target { - all, state, info, state_info, nodes, effects, palettes, fxdata, networks + all, state, info, state_info, nodes, effects, palettes, fxdata, networks, config }; json_target subJson = json_target::all; @@ -1041,6 +1041,7 @@ void serveJson(AsyncWebServerRequest* request) else if (url.indexOf(F("palx")) > 0) subJson = json_target::palettes; else if (url.indexOf(F("fxda")) > 0) subJson = json_target::fxdata; else if (url.indexOf(F("net")) > 0) subJson = json_target::networks; + else if (url.indexOf(F("cfg")) > 0) subJson = json_target::config; #ifdef WLED_ENABLE_JSONLIVE else if (url.indexOf("live") > 0) { serveLiveLeds(request); @@ -1051,9 +1052,6 @@ void serveJson(AsyncWebServerRequest* request) request->send_P(200, FPSTR(CONTENT_TYPE_JSON), JSON_palette_names); return; } - else if (url.indexOf(F("cfg")) > 0 && handleFileRead(request, F("/cfg.json"))) { - return; - } else if (url.length() > 6) { //not just /json serveJsonError(request, 501, ERR_NOT_IMPL); return; @@ -1085,6 +1083,8 @@ void serveJson(AsyncWebServerRequest* request) serializeModeData(lDoc); break; case json_target::networks: serializeNetworks(lDoc); break; + case json_target::config: + serializeConfig(lDoc); break; case json_target::state_info: case json_target::all: JsonObject state = lDoc.createNestedObject("state"); From 36cb1cad369cdfe38adf12f08a6b6fc3707b32df Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 23 Mar 2025 15:20:16 -0400 Subject: [PATCH 023/153] settings_um: Use live config Use json/cfg for the usermod settings page. Should fix issues with outdated content when a new firmware is loaded. --- wled00/data/settings_um.htm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/data/settings_um.htm b/wled00/data/settings_um.htm index c2f0ffbf2..b1505cac8 100644 --- a/wled00/data/settings_um.htm +++ b/wled00/data/settings_um.htm @@ -13,7 +13,7 @@ function S() { getLoc(); // load settings and insert values into DOM - fetch(getURL('/cfg.json'), { + fetch(getURL('/json/cfg'), { method: 'get' }) .then(res => { From 7e87891701d35796a773ac6f7fb04648f45a4494 Mon Sep 17 00:00:00 2001 From: Gabriel Sieben Date: Fri, 28 Mar 2025 16:27:26 +0100 Subject: [PATCH 024/153] Update USERMOD BME68X to version 1.0.2 --- usermods/BME68X_v2/BME68X_v2.cpp | 2185 ++++++++--------- usermods/BME68X_v2/README.md | 104 +- .../{library.json.disabled => library.json} | 3 +- 3 files changed, 1158 insertions(+), 1134 deletions(-) rename usermods/BME68X_v2/{library.json.disabled => library.json} (58%) diff --git a/usermods/BME68X_v2/BME68X_v2.cpp b/usermods/BME68X_v2/BME68X_v2.cpp index 081d03ff9..dace35ab6 100644 --- a/usermods/BME68X_v2/BME68X_v2.cpp +++ b/usermods/BME68X_v2/BME68X_v2.cpp @@ -2,1116 +2,1113 @@ * @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 + * @version 1.0.2 + * @date 28 March 2025 */ -#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 + #define UMOD_DEVICE "ESP32" // NOTE - Set your hardware here + #define HARDWARE_VERSION "1.0" // NOTE - Set your hardware version here + #define UMOD_BME680X_SW_VERSION "1.0.2" // NOTE - Version of the User Mod + #define CALIB_FILE_NAME "/BME680X-Calib.hex" // NOTE - Calibration file name + #define UMOD_NAME "BME680X" // NOTE - User module name + #define UMOD_DEBUG_NAME "UM-BME680X: " // NOTE - Debug print module name addon + + #define ESC "\033" + #define ESC_CSI ESC "[" + #define ESC_STYLE_RESET ESC_CSI "0m" + #define ESC_CURSOR_COLUMN(n) ESC_CSI #n "G" + + #define ESC_FGCOLOR_BLACK ESC_CSI "30m" + #define ESC_FGCOLOR_RED ESC_CSI "31m" + #define ESC_FGCOLOR_GREEN ESC_CSI "32m" + #define ESC_FGCOLOR_YELLOW ESC_CSI "33m" + #define ESC_FGCOLOR_BLUE ESC_CSI "34m" + #define ESC_FGCOLOR_MAGENTA ESC_CSI "35m" + #define ESC_FGCOLOR_CYAN ESC_CSI "36m" + #define ESC_FGCOLOR_WHITE ESC_CSI "37m" + #define ESC_FGCOLOR_DEFAULT ESC_CSI "39m" + + /* Debug Print Special Text */ + #define INFO_COLUMN ESC_CURSOR_COLUMN(60) + #define GOGAB_OK INFO_COLUMN "[" ESC_FGCOLOR_GREEN "OK" ESC_STYLE_RESET "]" + #define GOGAB_FAIL INFO_COLUMN "[" ESC_FGCOLOR_RED "FAIL" ESC_STYLE_RESET "]" + #define GOGAB_WARN INFO_COLUMN "[" ESC_FGCOLOR_YELLOW "WARN" ESC_STYLE_RESET "]" + #define GOGAB_DONE INFO_COLUMN "[" ESC_FGCOLOR_CYAN "DONE" ESC_STYLE_RESET "]" + + #include "bsec.h" // Bosch sensor library + #include "wled.h" + #include + + /* UsermodBME68X class definition */ + class UsermodBME68X : public Usermod { + + public: + /* Public: Functions */ + uint16_t getId(); + void loop(); // Loop of the user module called by wled main in loop + void setup(); // Setup of the user module called by wled main + void addToConfig(JsonObject& root); // Extends the settings/user module settings page to include the user module requirements. The settings are written from the wled core to the configuration file. + void appendConfigData(); // Adds extra info to the config page of weld + bool readFromConfig(JsonObject& root); // Reads config values + void addToJsonInfo(JsonObject& root); // Adds user module info to the weld info page + + /* Wled internal functions which can be used by the core or other user mods */ + inline float getTemperature(); // Get Temperature in the selected scale of °C or °F + inline float getHumidity(); // ... + inline float getPressure(); + inline float getGasResistance(); + inline float getAbsoluteHumidity(); + inline float getDewPoint(); + inline float getIaq(); + inline float getStaticIaq(); + inline float getCo2(); + inline float getVoc(); + inline float getGasPerc(); + inline uint8_t getIaqAccuracy(); + inline uint8_t getStaticIaqAccuracy(); + inline uint8_t getCo2Accuracy(); + inline uint8_t getVocAccuracy(); + inline uint8_t getGasPercAccuracy(); + inline bool getStabStatus(); + inline bool getRunInStatus(); + + private: + /* Private: Functions */ + void HomeAssistantDiscovery(); + void MQTT_PublishHASensor(const String& name, const String& deviceClass, const String& unitOfMeasurement, const int8_t& digs, const uint8_t& option = 0); + void MQTT_publish(const char* topic, const float& value, const int8_t& dig); + void onMqttConnect(bool sessionPresent); + void checkIaqSensorStatus(); + void InfoHelper(JsonObject& root, const char* name, const float& sensorvalue, const int8_t& decimals, const char* unit); + void InfoHelper(JsonObject& root, const char* name, const String& sensorvalue, const bool& status); + void loadState(); + void saveState(); + void getValues(); + + /*** V A R I A B L E s & C O N S T A N T s ***/ + /* Private: Settings of Usermod BME68X */ + struct settings_t { + bool enabled; // true if user module is active + byte I2cadress; // Depending on the manufacturer, the BME680 has the address 0x76 or 0x77 + uint8_t Interval; // Interval of reading sensor data in seconds + uint16_t MaxAge; // Force the publication of the value of a sensor after these defined seconds at the latest + bool pubAcc; // Publish the accuracy values + bool publishSensorState; // Publisch the sensor calibration state + bool publishAfterCalibration ; // The IAQ/CO2/VOC/GAS value are only valid after the sensor has been calibrated. If this switch is active, the values are only sent after calibration + bool PublischChange; // Publish values even when they have not changed + bool PublishIAQVerbal; // Publish Index of Air Quality (IAQ) classification Verbal + bool PublishStaticIAQVerbal; // Publish Static Index of Air Quality (Static IAQ) Verbal + byte tempScale; // 0 -> Use Celsius, 1-> Use Fahrenheit + float tempOffset; // Temperature Offset + bool HomeAssistantDiscovery; // Publish Home Assistant Device Information + bool pauseOnActiveWled ; // If this is set to true, the user mod ist not executed while wled is active + + /* Decimal Places (-1 means inactive) */ + struct decimals_t { + int8_t temperature; + int8_t humidity; + int8_t pressure; + int8_t gasResistance; + int8_t absHumidity; + int8_t drewPoint; + int8_t iaq; + int8_t staticIaq; + int8_t co2; + int8_t Voc; + int8_t gasPerc; + } decimals; + } settings; + + /* Private: Flags */ + struct flags_t { + bool InitSuccessful = false; // Initialation was un-/successful + bool MqttInitialized = false; // MQTT Initialation done flag (first MQTT Connect) + bool SaveState = false; // Save the calibration data flag + bool DeleteCaibration = false; // If set the calib file will be deleted on the next round + } flags; + + /* Private: Measurement timers */ + struct timer_t { + long actual; // Actual time stamp + long lastRun; // Last measurement time stamp + } timer; + + /* Private: Various variables */ + String stringbuff; // General string stringbuff buffer + char charbuffer[128]; // General char stringbuff buffer + String InfoPageStatusLine; // Shown on the info page of WLED + String tempScale; // °C or °F + uint8_t bsecState[BSEC_MAX_STATE_BLOB_SIZE]; // Calibration data array + uint16_t stateUpdateCounter; // Save state couter + static const uint8_t bsec_config_iaq[]; // Calibration Buffer + Bsec iaqSensor; // Sensor variable + + /* Private: Sensor values */ + struct values_t { + float temperature; // Temp [°C] (Sensor-compensated) + float humidity; // Relative humidity [%] (Sensor-compensated) + float pressure; // raw pressure [hPa] + float gasResistance; // raw gas restistance [Ohm] + float absHumidity; // UserMod calculated: Absolute Humidity [g/m³] + float drewPoint; // UserMod calculated: drew point [°C/°F] + float iaq; // IAQ (Indoor Air Quallity) + float staticIaq; // Satic IAQ + float co2; // CO2 [PPM] + float Voc; // VOC in [PPM] + float gasPerc; // Gas Percentage in [%] + uint8_t iaqAccuracy; // IAQ accuracy - IAQ Accuracy = 1 means value is inaccurate, IAQ Accuracy = 2 means sensor is being calibrated, IAQ Accuracy = 3 means sensor successfully calibrated. + uint8_t staticIaqAccuracy; // Static IAQ accuracy + uint8_t co2Accuracy; // co2 accuracy + uint8_t VocAccuracy; // voc accuracy + uint8_t gasPercAccuracy; // Gas percentage accuracy + bool stabStatus; // Indicates if the sensor is undergoing initial stabilization during its first use after production + bool runInStatus; // Indicates when the sensor is ready after after switch-on + } valuesA, valuesB, *ValuesPtr, *PrevValuesPtr, *swap; // Data Scructur A, Data Structur B, Pointers to switch between data channel A & B + + struct cvalues_t { + String iaqVerbal; // IAQ verbal + String staticIaqVerbal; // Static IAQ verbal + + } cvalues; + + /* Private: Sensor settings */ + bsec_virtual_sensor_t sensorList[13] = { + BSEC_OUTPUT_IAQ, // Index for Air Quality estimate [0-500] Index for Air Quality (IAQ) gives an indication of the relative change in ambient TVOCs detected by BME680. + BSEC_OUTPUT_STATIC_IAQ, // Unscaled Index for Air Quality estimate + BSEC_OUTPUT_CO2_EQUIVALENT, // CO2 equivalent estimate [ppm] + BSEC_OUTPUT_BREATH_VOC_EQUIVALENT, // Breath VOC concentration estimate [ppm] + BSEC_OUTPUT_RAW_TEMPERATURE, // Temperature sensor signal [degrees Celsius] Temperature directly measured by BME680 in degree Celsius. This value is cross-influenced by the sensor heating and device specific heating. + BSEC_OUTPUT_RAW_PRESSURE, // Pressure sensor signal [Pa] Pressure directly measured by the BME680 in Pa. + BSEC_OUTPUT_RAW_HUMIDITY, // Relative humidity sensor signal [%] Relative humidity directly measured by the BME680 in %. This value is cross-influenced by the sensor heating and device specific heating. + BSEC_OUTPUT_RAW_GAS, // Gas sensor signal [Ohm] Gas resistance measured directly by the BME680 in Ohm.The resistance value changes due to varying VOC concentrations (the higher the concentration of reducing VOCs, the lower the resistance and vice versa). + BSEC_OUTPUT_STABILIZATION_STATUS, // Gas sensor stabilization status [boolean] Indicates initial stabilization status of the gas sensor element: stabilization is ongoing (0) or stabilization is finished (1). + BSEC_OUTPUT_RUN_IN_STATUS, // Gas sensor run-in status [boolean] Indicates power-on stabilization status of the gas sensor element: stabilization is ongoing (0) or stabilization is finished (1) + BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE, // Sensor heat compensated temperature [degrees Celsius] Temperature measured by BME680 which is compensated for the influence of sensor (heater) in degree Celsius. The self heating introduced by the heater is depending on the sensor operation mode and the sensor supply voltage. + BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY, // Sensor heat compensated humidity [%] Relative measured by BME680 which is compensated for the influence of sensor (heater) in %. It converts the ::BSEC_INPUT_HUMIDITY from temperature ::BSEC_INPUT_TEMPERATURE to temperature ::BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE. + BSEC_OUTPUT_GAS_PERCENTAGE // Percentage of min and max filtered gas value [%] + }; + + /*** V A R I A B L E s & C O N S T A N T s ***/ + /* Public: strings to reduce flash memory usage (used more than twice) */ + static const char _enabled[]; + static const char _hadtopic[]; + + /* Public: Settings Strings*/ + static const char _nameI2CAdr[]; + static const char _nameInterval[]; + static const char _nameMaxAge[]; + static const char _namePubAc[]; + static const char _namePubSenState[]; + static const char _namePubAfterCalib[]; + static const char _namePublishChange[]; + static const char _nameTempScale[]; + static const char _nameTempOffset[]; + static const char _nameHADisc[]; + static const char _nameDelCalib[]; + + /* Public: Sensor names / Sensor short names */ + static const char _nameTemp[]; + static const char _nameHum[]; + static const char _namePress[]; + static const char _nameGasRes[]; + static const char _nameAHum[]; + static const char _nameDrewP[]; + static const char _nameIaq[]; + static const char _nameIaqAc[]; + static const char _nameIaqVerb[]; + static const char _nameStaticIaq[]; + static const char _nameStaticIaqVerb[]; + static const char _nameStaticIaqAc[]; + static const char _nameCo2[]; + static const char _nameCo2Ac[]; + static const char _nameVoc[]; + static const char _nameVocAc[]; + static const char _nameComGasAc[]; + static const char _nameGasPer[]; + static const char _nameGasPerAc[]; + static const char _namePauseOnActWL[]; + + static const char _nameStabStatus[]; + static const char _nameRunInStatus[]; + + /* Public: Sensor Units */ + static const char _unitTemp[]; + static const char _unitHum[]; + static const char _unitPress[]; + static const char _unitGasres[]; + static const char _unitAHum[]; + static const char _unitDrewp[]; + static const char _unitIaq[]; + static const char _unitStaticIaq[]; + static const char _unitCo2[]; + static const char _unitVoc[]; + static const char _unitGasPer[]; + static const char _unitNone[]; + + static const char _unitCelsius[]; + static const char _unitFahrenheit[]; + }; // UsermodBME68X class definition End + + /*** Setting C O N S T A N T S ***/ + /* Private: Settings Strings*/ + const char UsermodBME68X::_enabled[] PROGMEM = "Enabled"; + const char UsermodBME68X::_hadtopic[] PROGMEM = "homeassistant/sensor/"; + + const char UsermodBME68X::_nameI2CAdr[] PROGMEM = "i2C Address"; + const char UsermodBME68X::_nameInterval[] PROGMEM = "Interval"; + const char UsermodBME68X::_nameMaxAge[] PROGMEM = "Max Age"; + const char UsermodBME68X::_namePublishChange[] PROGMEM = "Pub changes only"; + const char UsermodBME68X::_namePubAc[] PROGMEM = "Pub Accuracy"; + const char UsermodBME68X::_namePubSenState[] PROGMEM = "Pub Calib State"; + const char UsermodBME68X::_namePubAfterCalib[] PROGMEM = "Pub After Calib"; + const char UsermodBME68X::_nameTempScale[] PROGMEM = "Temp Scale"; + const char UsermodBME68X::_nameTempOffset[] PROGMEM = "Temp Offset"; + const char UsermodBME68X::_nameHADisc[] PROGMEM = "HA Discovery"; + const char UsermodBME68X::_nameDelCalib[] PROGMEM = "Del Calibration Hist"; + const char UsermodBME68X::_namePauseOnActWL[] PROGMEM = "Pause while WLED active"; + + /* Private: Sensor names / Sensor short name */ + const char UsermodBME68X::_nameTemp[] PROGMEM = "Temperature"; + const char UsermodBME68X::_nameHum[] PROGMEM = "Humidity"; + const char UsermodBME68X::_namePress[] PROGMEM = "Pressure"; + const char UsermodBME68X::_nameGasRes[] PROGMEM = "Gas-Resistance"; + const char UsermodBME68X::_nameAHum[] PROGMEM = "Absolute-Humidity"; + const char UsermodBME68X::_nameDrewP[] PROGMEM = "Drew-Point"; + const char UsermodBME68X::_nameIaq[] PROGMEM = "IAQ"; + const char UsermodBME68X::_nameIaqVerb[] PROGMEM = "IAQ-Verbal"; + const char UsermodBME68X::_nameStaticIaq[] PROGMEM = "Static-IAQ"; + const char UsermodBME68X::_nameStaticIaqVerb[] PROGMEM = "Static-IAQ-Verbal"; + const char UsermodBME68X::_nameCo2[] PROGMEM = "CO2"; + const char UsermodBME68X::_nameVoc[] PROGMEM = "VOC"; + const char UsermodBME68X::_nameGasPer[] PROGMEM = "Gas-Percentage"; + const char UsermodBME68X::_nameIaqAc[] PROGMEM = "IAQ-Accuracy"; + const char UsermodBME68X::_nameStaticIaqAc[] PROGMEM = "Static-IAQ-Accuracy"; + const char UsermodBME68X::_nameCo2Ac[] PROGMEM = "CO2-Accuracy"; + const char UsermodBME68X::_nameVocAc[] PROGMEM = "VOC-Accuracy"; + const char UsermodBME68X::_nameGasPerAc[] PROGMEM = "Gas-Percentage-Accuracy"; + const char UsermodBME68X::_nameStabStatus[] PROGMEM = "Stab-Status"; + const char UsermodBME68X::_nameRunInStatus[] PROGMEM = "Run-In-Status"; + + /* Private Units */ + const char UsermodBME68X::_unitTemp[] PROGMEM = " "; // NOTE - Is set with the selectable temperature unit + const char UsermodBME68X::_unitHum[] PROGMEM = "%"; + const char UsermodBME68X::_unitPress[] PROGMEM = "hPa"; + const char UsermodBME68X::_unitGasres[] PROGMEM = "kΩ"; + const char UsermodBME68X::_unitAHum[] PROGMEM = "g/m³"; + const char UsermodBME68X::_unitDrewp[] PROGMEM = " "; // NOTE - Is set with the selectable temperature unit + const char UsermodBME68X::_unitIaq[] PROGMEM = " "; // No unit + const char UsermodBME68X::_unitStaticIaq[] PROGMEM = " "; // No unit + const char UsermodBME68X::_unitCo2[] PROGMEM = "ppm"; + const char UsermodBME68X::_unitVoc[] PROGMEM = "ppm"; + const char UsermodBME68X::_unitGasPer[] PROGMEM = "%"; + const char UsermodBME68X::_unitNone[] PROGMEM = ""; + + const char UsermodBME68X::_unitCelsius[] PROGMEM = "°C"; // Symbol for Celsius + const char UsermodBME68X::_unitFahrenheit[] PROGMEM = "°F"; // Symbol for Fahrenheit + + /* Load Sensor Settings */ + const uint8_t UsermodBME68X::bsec_config_iaq[] = { + #include "config/generic_33v_3s_28d/bsec_iaq.txt" // Allow 28 days for calibration because the WLED module normally stays in the same place anyway + }; + + + /************************************************************************************************************/ + /********************************************* M A I N C O D E *********************************************/ + /************************************************************************************************************/ + + /** + * @brief Called by WLED: Setup of the usermod + */ + void UsermodBME68X::setup() { + DEBUG_PRINTLN(F(UMOD_DEBUG_NAME ESC_FGCOLOR_CYAN "Initialize" ESC_STYLE_RESET)); + + /* Check, if i2c is activated */ + if (i2c_scl < 0 || i2c_sda < 0) { + settings.enabled = false; // Disable usermod once i2c is not running + DEBUG_PRINTLN(F(UMOD_DEBUG_NAME "I2C is not activated. Please activate I2C first." GOGAB_FAIL)); + return; + } + + flags.InitSuccessful = true; // Will be set to false on need + + /* Set data structure pointers */ + ValuesPtr = &valuesA; + PrevValuesPtr = &valuesB; + + /* Init Library*/ + iaqSensor.begin(settings.I2cadress, Wire); // BME68X_I2C_ADDR_LOW + stringbuff = "BSEC library version " + String(iaqSensor.version.major) + "." + String(iaqSensor.version.minor) + "." + String(iaqSensor.version.major_bugfix) + "." + String(iaqSensor.version.minor_bugfix); + DEBUG_PRINT(F(UMOD_NAME)); + DEBUG_PRINTLN(F(stringbuff.c_str())); + + /* Init Sensor*/ + iaqSensor.setConfig(bsec_config_iaq); + iaqSensor.updateSubscription(sensorList, 13, BSEC_SAMPLE_RATE_LP); + iaqSensor.setTPH(BME68X_OS_2X, BME68X_OS_16X, BME68X_OS_1X); // Set the temperature, Pressure and Humidity over-sampling + iaqSensor.setTemperatureOffset(settings.tempOffset); // set the temperature offset in degree Celsius + loadState(); // Load the old calibration data + checkIaqSensorStatus(); // Check the sensor status + // HomeAssistantDiscovery(); + DEBUG_PRINTLN(F(INFO_COLUMN GOGAB_DONE)); + } + + /** + * @brief Called by WLED: Main loop called by WLED + * + */ + void UsermodBME68X::loop() { + if (!settings.enabled || strip.isUpdating() || !flags.InitSuccessful) return; // Leave if not enabled or string is updating or init failed + + if (settings.pauseOnActiveWled && strip.getBrightness()) return; // Workarround Known Issue: handing led update - Leave once pause on activ wled is active and wled is active + + timer.actual = millis(); // Timer to fetch new temperature, humidity and pressure data at intervals + + if (timer.actual - timer.lastRun >= settings.Interval * 1000) { + timer.lastRun = timer.actual; + + /* Get the sonsor measurments and publish them */ + if (iaqSensor.run()) { // iaqSensor.run() + getValues(); // Get the new values + + if (ValuesPtr->temperature != PrevValuesPtr->temperature || !settings.PublischChange) { // NOTE - negative dig means inactive + MQTT_publish(_nameTemp, ValuesPtr->temperature, settings.decimals.temperature); + } + if (ValuesPtr->humidity != PrevValuesPtr->humidity || !settings.PublischChange) { + MQTT_publish(_nameHum, ValuesPtr->humidity, settings.decimals.humidity); + } + if (ValuesPtr->pressure != PrevValuesPtr->pressure || !settings.PublischChange) { + MQTT_publish(_namePress, ValuesPtr->pressure, settings.decimals.humidity); + } + if (ValuesPtr->gasResistance != PrevValuesPtr->gasResistance || !settings.PublischChange) { + MQTT_publish(_nameGasRes, ValuesPtr->gasResistance, settings.decimals.gasResistance); + } + if (ValuesPtr->absHumidity != PrevValuesPtr->absHumidity || !settings.PublischChange) { + MQTT_publish(_nameAHum, PrevValuesPtr->absHumidity, settings.decimals.absHumidity); + } + if (ValuesPtr->drewPoint != PrevValuesPtr->drewPoint || !settings.PublischChange) { + MQTT_publish(_nameDrewP, PrevValuesPtr->drewPoint, settings.decimals.drewPoint); + } + if (ValuesPtr->iaq != PrevValuesPtr->iaq || !settings.PublischChange) { + MQTT_publish(_nameIaq, ValuesPtr->iaq, settings.decimals.iaq); + if (settings.pubAcc) MQTT_publish(_nameIaqAc, ValuesPtr->iaqAccuracy, 0); + if (settings.decimals.iaq>-1) { + if (settings.PublishIAQVerbal) { + if (ValuesPtr->iaq <= 50) cvalues.iaqVerbal = F("Excellent"); + else if (ValuesPtr->iaq <= 100) cvalues.iaqVerbal = F("Good"); + else if (ValuesPtr->iaq <= 150) cvalues.iaqVerbal = F("Lightly polluted"); + else if (ValuesPtr->iaq <= 200) cvalues.iaqVerbal = F("Moderately polluted"); + else if (ValuesPtr->iaq <= 250) cvalues.iaqVerbal = F("Heavily polluted"); + else if (ValuesPtr->iaq <= 350) cvalues.iaqVerbal = F("Severely polluted"); + else cvalues.iaqVerbal = F("Extremely polluted"); + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, _nameIaqVerb); + if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, cvalues.iaqVerbal.c_str()); + } + } + } + if (ValuesPtr->staticIaq != PrevValuesPtr->staticIaq || !settings.PublischChange) { + MQTT_publish(_nameStaticIaq, ValuesPtr->staticIaq, settings.decimals.staticIaq); + if (settings.pubAcc) MQTT_publish(_nameStaticIaqAc, ValuesPtr->staticIaqAccuracy, 0); + if (settings.decimals.staticIaq>-1) { + if (settings.PublishIAQVerbal) { + if (ValuesPtr->staticIaq <= 50) cvalues.staticIaqVerbal = F("Excellent"); + else if (ValuesPtr->staticIaq <= 100) cvalues.staticIaqVerbal = F("Good"); + else if (ValuesPtr->staticIaq <= 150) cvalues.staticIaqVerbal = F("Lightly polluted"); + else if (ValuesPtr->staticIaq <= 200) cvalues.staticIaqVerbal = F("Moderately polluted"); + else if (ValuesPtr->staticIaq <= 250) cvalues.staticIaqVerbal = F("Heavily polluted"); + else if (ValuesPtr->staticIaq <= 350) cvalues.staticIaqVerbal = F("Severely polluted"); + else cvalues.staticIaqVerbal = F("Extremely polluted"); + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, _nameStaticIaqVerb); + if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, cvalues.staticIaqVerbal.c_str()); + } + } + } + if (ValuesPtr->co2 != PrevValuesPtr->co2 || !settings.PublischChange) { + MQTT_publish(_nameCo2, ValuesPtr->co2, settings.decimals.co2); + if (settings.pubAcc) MQTT_publish(_nameCo2Ac, ValuesPtr->co2Accuracy, 0); + } + if (ValuesPtr->Voc != PrevValuesPtr->Voc || !settings.PublischChange) { + MQTT_publish(_nameVoc, ValuesPtr->Voc, settings.decimals.Voc); + if (settings.pubAcc) MQTT_publish(_nameVocAc, ValuesPtr->VocAccuracy, 0); + } + if (ValuesPtr->gasPerc != PrevValuesPtr->gasPerc || !settings.PublischChange) { + MQTT_publish(_nameGasPer, ValuesPtr->gasPerc, settings.decimals.gasPerc); + if (settings.pubAcc) MQTT_publish(_nameGasPerAc, ValuesPtr->gasPercAccuracy, 0); + } + + /**** Publish Sensor State Entrys *****/ + if ((ValuesPtr->stabStatus != PrevValuesPtr->stabStatus || !settings.PublischChange) && settings.publishSensorState) MQTT_publish(_nameStabStatus, ValuesPtr->stabStatus, 0); + if ((ValuesPtr->runInStatus != PrevValuesPtr->runInStatus || !settings.PublischChange) && settings.publishSensorState) MQTT_publish(_nameRunInStatus, ValuesPtr->runInStatus, 0); + + /* Check accuracies - if accurasy level 3 is reached -> save calibration data */ + if ((ValuesPtr->iaqAccuracy != PrevValuesPtr->iaqAccuracy) && ValuesPtr->iaqAccuracy == 3) flags.SaveState = true; // Save after calibration / recalibration + if ((ValuesPtr->staticIaqAccuracy != PrevValuesPtr->staticIaqAccuracy) && ValuesPtr->staticIaqAccuracy == 3) flags.SaveState = true; + if ((ValuesPtr->co2Accuracy != PrevValuesPtr->co2Accuracy) && ValuesPtr->co2Accuracy == 3) flags.SaveState = true; + if ((ValuesPtr->VocAccuracy != PrevValuesPtr->VocAccuracy) && ValuesPtr->VocAccuracy == 3) flags.SaveState = true; + if ((ValuesPtr->gasPercAccuracy != PrevValuesPtr->gasPercAccuracy) && ValuesPtr->gasPercAccuracy == 3) flags.SaveState = true; + + if (flags.SaveState) saveState(); // Save if the save state flag is set + } + } + } + + /** + * @brief Retrieves the sensor data and truncates it to the requested decimal places + * + */ + void UsermodBME68X::getValues() { + /* Swap the point to the data structures */ + swap = PrevValuesPtr; + PrevValuesPtr = ValuesPtr; + ValuesPtr = swap; + + /* Float Values */ + ValuesPtr->temperature = roundf(iaqSensor.temperature * powf(10, settings.decimals.temperature)) / powf(10, settings.decimals.temperature); + ValuesPtr->humidity = roundf(iaqSensor.humidity * powf(10, settings.decimals.humidity)) / powf(10, settings.decimals.humidity); + ValuesPtr->pressure = roundf(iaqSensor.pressure * powf(10, settings.decimals.pressure)) / powf(10, settings.decimals.pressure) /100; // Pa 2 hPa + ValuesPtr->gasResistance = roundf(iaqSensor.gasResistance * powf(10, settings.decimals.gasResistance)) /powf(10, settings.decimals.gasResistance) /1000; // Ohm 2 KOhm + ValuesPtr->iaq = roundf(iaqSensor.iaq * powf(10, settings.decimals.iaq)) / powf(10, settings.decimals.iaq); + ValuesPtr->staticIaq = roundf(iaqSensor.staticIaq * powf(10, settings.decimals.staticIaq)) / powf(10, settings.decimals.staticIaq); + ValuesPtr->co2 = roundf(iaqSensor.co2Equivalent * powf(10, settings.decimals.co2)) / powf(10, settings.decimals.co2); + ValuesPtr->Voc = roundf(iaqSensor.breathVocEquivalent * powf(10, settings.decimals.Voc)) / powf(10, settings.decimals.Voc); + ValuesPtr->gasPerc = roundf(iaqSensor.gasPercentage * powf(10, settings.decimals.gasPerc)) / powf(10, settings.decimals.gasPerc); + + /* Calculate Absolute Humidity [g/m³] */ + if (settings.decimals.absHumidity>-1) { + const float mw = 18.01534; // molar mass of water g/mol + const float r = 8.31447215; // Universal gas constant J/mol/K + ValuesPtr->absHumidity = (6.112 * powf(2.718281828, (17.67 * ValuesPtr->temperature) / (ValuesPtr->temperature + 243.5)) * ValuesPtr->humidity * mw) / ((273.15 + ValuesPtr->temperature) * r); // in ppm + } + /* Calculate Drew Point (C°) */ + if (settings.decimals.drewPoint>-1) { + ValuesPtr->drewPoint = (243.5 * (log( ValuesPtr->humidity / 100) + ((17.67 * ValuesPtr->temperature) / (243.5 + ValuesPtr->temperature))) / (17.67 - log(ValuesPtr->humidity / 100) - ((17.67 * ValuesPtr->temperature) / (243.5 + ValuesPtr->temperature)))); + } + + /* Convert to Fahrenheit when selected */ + if (settings.tempScale) { // settings.tempScale = 0 => Celsius, = 1 => Fahrenheit + ValuesPtr->temperature = ValuesPtr->temperature * 1.8 + 32; // Value stored in Fahrenheit + ValuesPtr->drewPoint = ValuesPtr->drewPoint * 1.8 + 32; + } + + /* Integer Values */ + ValuesPtr->iaqAccuracy = iaqSensor.iaqAccuracy; + ValuesPtr->staticIaqAccuracy = iaqSensor.staticIaqAccuracy; + ValuesPtr->co2Accuracy = iaqSensor.co2Accuracy; + ValuesPtr->VocAccuracy = iaqSensor.breathVocAccuracy; + ValuesPtr->gasPercAccuracy = iaqSensor.gasPercentageAccuracy; + ValuesPtr->stabStatus = iaqSensor.stabStatus; + ValuesPtr->runInStatus = iaqSensor.runInStatus; + } + + + /** + * @brief Sends the current sensor data via MQTT + * @param topic Suptopic of the sensor as const char + * @param value Current sensor value as float + */ + void UsermodBME68X::MQTT_publish(const char* topic, const float& value, const int8_t& dig) { + if (dig<0) return; + if (WLED_MQTT_CONNECTED) { + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, topic); + mqtt->publish(charbuffer, 0, false, String(value, dig).c_str()); + } + } + + /** + * @brief Called by WLED: Initialize the MQTT parts when the connection to the MQTT server is established. + * @param bool Session Present + */ + void UsermodBME68X::onMqttConnect(bool sessionPresent) { + DEBUG_PRINTLN(UMOD_DEBUG_NAME "OnMQTTConnect event fired"); + HomeAssistantDiscovery(); + + if (!flags.MqttInitialized) { + flags.MqttInitialized=true; + DEBUG_PRINTLN(UMOD_DEBUG_NAME "MQTT first connect"); + } + } + + + /** + * @brief MQTT initialization to generate the mqtt topic strings. This initialization also creates the HomeAssistat device configuration (HA Discovery), which home assinstant automatically evaluates to create a device. + */ + void UsermodBME68X::HomeAssistantDiscovery() { + if (!settings.HomeAssistantDiscovery || !flags.InitSuccessful || !settings.enabled) return; // Leave once HomeAssistant Discovery is inactive + + DEBUG_PRINTLN(UMOD_DEBUG_NAME ESC_FGCOLOR_CYAN "Creating HomeAssistant Discovery Mqtt-Entrys" ESC_STYLE_RESET); + + /* Sensor Values */ + MQTT_PublishHASensor(_nameTemp, "TEMPERATURE", tempScale.c_str(), settings.decimals.temperature ); // Temperature + MQTT_PublishHASensor(_namePress, "ATMOSPHERIC_PRESSURE", _unitPress, settings.decimals.pressure ); // Pressure + MQTT_PublishHASensor(_nameHum, "HUMIDITY", _unitHum, settings.decimals.humidity ); // Humidity + MQTT_PublishHASensor(_nameGasRes, "GAS", _unitGasres, settings.decimals.gasResistance ); // There is no device class for resistance in HA yet: https://developers.home-assistant.io/docs/core/entity/sensor/ + MQTT_PublishHASensor(_nameAHum, "HUMIDITY", _unitAHum, settings.decimals.absHumidity ); // Absolute Humidity + MQTT_PublishHASensor(_nameDrewP, "TEMPERATURE", tempScale.c_str(), settings.decimals.drewPoint ); // Drew Point + MQTT_PublishHASensor(_nameIaq, "AQI", _unitIaq, settings.decimals.iaq ); // IAQ + MQTT_PublishHASensor(_nameIaqVerb, "", _unitNone, settings.PublishIAQVerbal, 2); // IAQ Verbal / Set Option 2 (text sensor) + MQTT_PublishHASensor(_nameStaticIaq, "AQI", _unitNone, settings.decimals.staticIaq ); // Static IAQ + MQTT_PublishHASensor(_nameStaticIaqVerb, "", _unitNone, settings.PublishStaticIAQVerbal, 2); // IAQ Verbal / Set Option 2 (text sensor + MQTT_PublishHASensor(_nameCo2, "CO2", _unitCo2, settings.decimals.co2 ); // CO2 + MQTT_PublishHASensor(_nameVoc, "VOLATILE_ORGANIC_COMPOUNDS", _unitVoc, settings.decimals.Voc ); // VOC + MQTT_PublishHASensor(_nameGasPer, "AQI", _unitGasPer, settings.decimals.gasPerc ); // Gas % + + /* Accuracys - switched off once publishAccuracy=0 or the main value is switched of by digs set to a negative number */ + MQTT_PublishHASensor(_nameIaqAc, "AQI", _unitNone, settings.pubAcc - 1 + settings.decimals.iaq * settings.pubAcc, 1); // Option 1: Diagnostics Sektion + MQTT_PublishHASensor(_nameStaticIaqAc, "", _unitNone, settings.pubAcc - 1 + settings.decimals.staticIaq * settings.pubAcc, 1); + MQTT_PublishHASensor(_nameCo2Ac, "", _unitNone, settings.pubAcc - 1 + settings.decimals.co2 * settings.pubAcc, 1); + MQTT_PublishHASensor(_nameVocAc, "", _unitNone, settings.pubAcc - 1 + settings.decimals.Voc * settings.pubAcc, 1); + MQTT_PublishHASensor(_nameGasPerAc, "", _unitNone, settings.pubAcc - 1 + settings.decimals.gasPerc * settings.pubAcc, 1); + + MQTT_PublishHASensor(_nameStabStatus, "", _unitNone, settings.publishSensorState - 1, 1); + MQTT_PublishHASensor(_nameRunInStatus, "", _unitNone, settings.publishSensorState - 1, 1); + + DEBUG_PRINTLN(UMOD_DEBUG_NAME GOGAB_DONE); + } + + /** + * @brief These MQTT entries are responsible for the Home Assistant Discovery of the sensors. HA is shown here where to look for the sensor data. This entry therefore only needs to be sent once. + * Important note: In order to find everything that is sent from this device to Home Assistant via MQTT under the same device name, the "device/identifiers" entry must be the same. + * I use the MQTT device name here. If other user mods also use the HA Discovery, it is recommended to set the identifier the same. Otherwise you would have several devices, + * even though it is one device. I therefore only use the MQTT client name set in WLED here. + * @param name Name of the sensor + * @param topic Topic of the live sensor data + * @param unitOfMeasurement Unit of the measurment + * @param digs Number of decimal places + * @param option Set to true if the sensor is part of diagnostics (dafault 0) + */ + void UsermodBME68X::MQTT_PublishHASensor(const String& name, const String& deviceClass, const String& unitOfMeasurement, const int8_t& digs, const uint8_t& option) { + DEBUG_PRINT(UMOD_DEBUG_NAME "\t" + name); + + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, name.c_str()); // Current values will be posted here + String basetopic = String(_hadtopic) + mqttClientID + F("/") + name + F("/config"); // This is the place where Home Assinstant Discovery will check for new devices + + if (digs < 0) { // if digs are set to -1 -> entry deactivated + /* Delete MQTT Entry */ + if (WLED_MQTT_CONNECTED) { + mqtt->publish(basetopic.c_str(), 0, true, ""); // Send emty entry to delete + DEBUG_PRINTLN(INFO_COLUMN "deleted"); + } + } else { + /* Create all the necessary HAD MQTT entrys - see: https://www.home-assistant.io/integrations/sensor.mqtt/#configuration-variables */ + DynamicJsonDocument jdoc(700); // json document + // See: https://www.home-assistant.io/integrations/mqtt/ + JsonObject avail = jdoc.createNestedObject(F("avty")); // 'avty': 'availability' + avail[F("topic")] = mqttDeviceTopic + String("/status"); // An MQTT topic subscribed to receive availability (online/offline) updates. + avail[F("payload_available")] = "online"; + avail[F("payload_not_available")] = "offline"; + JsonObject device = jdoc.createNestedObject(F("device")); // Information about the device this sensor is a part of to tie it into the device registry. Only works when unique_id is set. At least one of identifiers or connections must be present to identify the device. + device[F("name")] = serverDescription; + device[F("identifiers")] = String(mqttClientID); + device[F("manufacturer")] = F("WLED"); + device[F("model")] = UMOD_DEVICE; + device[F("sw_version")] = versionString; + device[F("hw_version")] = F(HARDWARE_VERSION); + + if (deviceClass != "") jdoc[F("device_class")] = deviceClass; // The type/class of the sensor to set the icon in the frontend. The device_class can be null + if (option == 1) jdoc[F("entity_category")] = "diagnostic"; // Option 1: The category of the entity | When set, the entity category must be diagnostic for sensors. + if (option == 2) jdoc[F("mode")] = "text"; // Option 2: Set text mode | + jdoc[F("expire_after")] = 1800; // If set, it defines the number of seconds after the sensor’s state expires, if it’s not updated. After expiry, the sensor’s state becomes unavailable. Default the sensors state never expires. + jdoc[F("name")] = name; // The name of the MQTT sensor. Without server/module/device name. The device name will be added by HomeAssinstant anyhow + if (unitOfMeasurement != "") jdoc[F("state_class")] = "measurement"; // NOTE: This entry is missing in some other usermods. But it is very important. Because only with this entry, you can use statistics (such as statistical graphs). + jdoc[F("state_topic")] = charbuffer; // The MQTT topic subscribed to receive sensor values. If device_class, state_class, unit_of_measurement or suggested_display_precision is set, and a numeric value is expected, an empty value '' will be ignored and will not update the state, a 'null' value will set the sensor to an unknown state. The device_class can be null. + jdoc[F("unique_id")] = String(mqttClientID) + "-" + name; // An ID that uniquely identifies this sensor. If two sensors have the same unique ID, Home Assistant will raise an exception. + if (unitOfMeasurement != "") jdoc[F("unit_of_measurement")] = unitOfMeasurement; // Defines the units of measurement of the sensor, if any. The unit_of_measurement can be null. + + DEBUG_PRINTF(" (%d bytes)", jdoc.memoryUsage()); + + stringbuff = ""; // clear string buffer + serializeJson(jdoc, stringbuff); // JSON to String + + if (WLED_MQTT_CONNECTED) { // Check if MQTT Connected, otherwise it will crash the 8266 + mqtt->publish(basetopic.c_str(), 0, true, stringbuff.c_str()); // Publish the HA discovery sensor entry + DEBUG_PRINTLN(INFO_COLUMN "published"); + } + } + } + + /** + * @brief Called by WLED: Publish Sensor Information to Info Page + * @param JsonObject Pointer + */ + void UsermodBME68X::addToJsonInfo(JsonObject& root) { + //DEBUG_PRINTLN(F(UMOD_DEBUG_NAME "Add to info event")); + JsonObject user = root[F("u")]; + + if (user.isNull()) + user = root.createNestedObject(F("u")); + + if (!flags.InitSuccessful) { + // Init was not seccessful - let the user know + JsonArray temperature_json = user.createNestedArray(F("BME68X Sensor")); + temperature_json.add(F("not found")); + JsonArray humidity_json = user.createNestedArray(F("BMW68x Reason")); + humidity_json.add(InfoPageStatusLine); + } + else if (!settings.enabled) { + JsonArray temperature_json = user.createNestedArray(F("BME68X Sensor")); + temperature_json.add(F("disabled")); + } + else { + InfoHelper(user, _nameTemp, ValuesPtr->temperature, settings.decimals.temperature, tempScale.c_str()); + InfoHelper(user, _nameHum, ValuesPtr->humidity, settings.decimals.humidity, _unitHum); + InfoHelper(user, _namePress, ValuesPtr->pressure, settings.decimals.pressure, _unitPress); + InfoHelper(user, _nameGasRes, ValuesPtr->gasResistance, settings.decimals.gasResistance, _unitGasres); + InfoHelper(user, _nameAHum, ValuesPtr->absHumidity, settings.decimals.absHumidity, _unitAHum); + InfoHelper(user, _nameDrewP, ValuesPtr->drewPoint, settings.decimals.drewPoint, tempScale.c_str()); + InfoHelper(user, _nameIaq, ValuesPtr->iaq, settings.decimals.iaq, _unitIaq); + InfoHelper(user, _nameIaqVerb, cvalues.iaqVerbal, settings.PublishIAQVerbal); + InfoHelper(user, _nameStaticIaq, ValuesPtr->staticIaq, settings.decimals.staticIaq, _unitStaticIaq); + InfoHelper(user, _nameStaticIaqVerb,cvalues.staticIaqVerbal, settings.PublishStaticIAQVerbal); + InfoHelper(user, _nameCo2, ValuesPtr->co2, settings.decimals.co2, _unitCo2); + InfoHelper(user, _nameVoc, ValuesPtr->Voc, settings.decimals.Voc, _unitVoc); + InfoHelper(user, _nameGasPer, ValuesPtr->gasPerc, settings.decimals.gasPerc, _unitGasPer); + + if (settings.pubAcc) { + if (settings.decimals.iaq >= 0) InfoHelper(user, _nameIaqAc, ValuesPtr->iaqAccuracy, 0, " "); + if (settings.decimals.staticIaq >= 0) InfoHelper(user, _nameStaticIaqAc, ValuesPtr->staticIaqAccuracy, 0, " "); + if (settings.decimals.co2 >= 0) InfoHelper(user, _nameCo2Ac, ValuesPtr->co2Accuracy, 0, " "); + if (settings.decimals.Voc >= 0) InfoHelper(user, _nameVocAc, ValuesPtr->VocAccuracy, 0, " "); + if (settings.decimals.gasPerc >= 0) InfoHelper(user, _nameGasPerAc, ValuesPtr->gasPercAccuracy, 0, " "); + } + + if (settings.publishSensorState) { + InfoHelper(user, _nameStabStatus, ValuesPtr->stabStatus, 0, " "); + InfoHelper(user, _nameRunInStatus, ValuesPtr->runInStatus, 0, " "); + } + } + } + + /** + * @brief Info Page helper function + * @param root JSON object + * @param name Name of the sensor as char + * @param sensorvalue Value of the sensor as float + * @param decimals Decimal places of the value + * @param unit Unit of the sensor + */ + void UsermodBME68X::InfoHelper(JsonObject& root, const char* name, const float& sensorvalue, const int8_t& decimals, const char* unit) { + if (decimals > -1) { + JsonArray sub_json = root.createNestedArray(name); + sub_json.add(roundf(sensorvalue * powf(10, decimals)) / powf(10, decimals)); + sub_json.add(unit); + } + } + + /** + * @brief Info Page helper function (overload) + * @param root JSON object + * @param name Name of the sensor + * @param sensorvalue Value of the sensor as string + * @param status Status of the value (active/inactive) + */ + void UsermodBME68X::InfoHelper(JsonObject& root, const char* name, const String& sensorvalue, const bool& status) { + if (status) { + JsonArray sub_json = root.createNestedArray(name); + sub_json.add(sensorvalue); + } + } + + /** + * @brief Called by WLED: Adds the usermodul neends on the config page for user modules + * @param JsonObject Pointer + * + * @see Usermod::addToConfig() + * @see UsermodManager::addToConfig() + */ + void UsermodBME68X::addToConfig(JsonObject& root) { + DEBUG_PRINT(F(UMOD_DEBUG_NAME "Creating configuration pages content: ")); + + JsonObject top = root.createNestedObject(FPSTR(UMOD_NAME)); + /* general settings */ + top[FPSTR(_enabled)] = settings.enabled; + top[FPSTR(_nameI2CAdr)] = settings.I2cadress; + top[FPSTR(_nameInterval)] = settings.Interval; + top[FPSTR(_namePublishChange)] = settings.PublischChange; + top[FPSTR(_namePubAc)] = settings.pubAcc; + top[FPSTR(_namePubSenState)] = settings.publishSensorState; + top[FPSTR(_nameTempScale)] = settings.tempScale; + top[FPSTR(_nameTempOffset)] = settings.tempOffset; + top[FPSTR(_nameHADisc)] = settings.HomeAssistantDiscovery; + top[FPSTR(_namePauseOnActWL)] = settings.pauseOnActiveWled; + top[FPSTR(_nameDelCalib)] = flags.DeleteCaibration; + + /* Digs */ + JsonObject sensors_json = top.createNestedObject("Sensors"); + sensors_json[FPSTR(_nameTemp)] = settings.decimals.temperature; + sensors_json[FPSTR(_nameHum)] = settings.decimals.humidity; + sensors_json[FPSTR(_namePress)] = settings.decimals.pressure; + sensors_json[FPSTR(_nameGasRes)] = settings.decimals.gasResistance; + sensors_json[FPSTR(_nameAHum)] = settings.decimals.absHumidity; + sensors_json[FPSTR(_nameDrewP)] = settings.decimals.drewPoint; + sensors_json[FPSTR(_nameIaq)] = settings.decimals.iaq; + sensors_json[FPSTR(_nameIaqVerb)] = settings.PublishIAQVerbal; + sensors_json[FPSTR(_nameStaticIaq)] = settings.decimals.staticIaq; + sensors_json[FPSTR(_nameStaticIaqVerb)] = settings.PublishStaticIAQVerbal; + sensors_json[FPSTR(_nameCo2)] = settings.decimals.co2; + sensors_json[FPSTR(_nameVoc)] = settings.decimals.Voc; + sensors_json[FPSTR(_nameGasPer)] = settings.decimals.gasPerc; + + DEBUG_PRINTLN(F(GOGAB_OK)); + } + + /** + * @brief Called by WLED: Add dropdown and additional infos / structure + * @see Usermod::appendConfigData() + * @see UsermodManager::appendConfigData() + */ + void UsermodBME68X::appendConfigData() { + // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'read interval [seconds]');"), UMOD_NAME, _nameInterval); oappend(charbuffer); + // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'only if value changes');"), UMOD_NAME, _namePublishChange); oappend(charbuffer); + // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'maximum age of a message in seconds');"), UMOD_NAME, _nameMaxAge); oappend(charbuffer); + // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'Gas related values are only published after the gas sensor has been calibrated');"), UMOD_NAME, _namePubAfterCalib); oappend(charbuffer); + // snprintf_P(charbuffer, 127, PSTR("addInfo('%s:%s',1,'*) Set to minus to deactivate (all sensors)');"), UMOD_NAME, _nameTemp); oappend(charbuffer); + + /* Dropdown for Celsius/Fahrenheit*/ + oappend(F("dd=addDropdown('")); + oappend(UMOD_NAME); + oappend(F("','")); + oappend(_nameTempScale); + oappend(F("');")); + oappend(F("addOption(dd,'Celsius',0);")); + oappend(F("addOption(dd,'Fahrenheit',1);")); + + /* i²C Address*/ + oappend(F("dd=addDropdown('")); + oappend(UMOD_NAME); + oappend(F("','")); + oappend(_nameI2CAdr); + oappend(F("');")); + oappend(F("addOption(dd,'0x76',0x76);")); + oappend(F("addOption(dd,'0x77',0x77);")); + } + + /** + * @brief Called by WLED: Read Usermod Config Settings default settings values could be set here (or below using the 3-argument getJsonValue()) + * instead of in the class definition or constructor setting them inside readFromConfig() is slightly more robust, handling the rare but + * plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) + * This is called whenever WLED boots and loads cfg.json, or when the UM config + * page is saved. Will properly re-instantiate the SHT class upon type change and + * publish HA discovery after enabling. + * NOTE: Here are the default settings of the user module + * @param JsonObject Pointer + * @return bool + * @see Usermod::readFromConfig() + * @see UsermodManager::readFromConfig() + */ + bool UsermodBME68X::readFromConfig(JsonObject& root) { + DEBUG_PRINT(F(UMOD_DEBUG_NAME "Reading configuration: ")); + + JsonObject top = root[FPSTR(UMOD_NAME)]; + bool configComplete = !top.isNull(); + + /* general settings */ /* DEFAULTS */ + configComplete &= getJsonValue(top[FPSTR(_enabled)], settings.enabled, 1 ); // Usermod enabled per default + configComplete &= getJsonValue(top[FPSTR(_nameI2CAdr)], settings.I2cadress, 0x77 ); // Defalut IC2 adress set to 0x77 (some modules are set to 0x76) + configComplete &= getJsonValue(top[FPSTR(_nameInterval)], settings.Interval, 1 ); // Executed every second + configComplete &= getJsonValue(top[FPSTR(_namePublishChange)], settings.PublischChange, false ); // Publish changed values only + configComplete &= getJsonValue(top[FPSTR(_nameTempScale)], settings.tempScale, 0 ); // Temp sale set to Celsius (1=Fahrenheit) + configComplete &= getJsonValue(top[FPSTR(_nameTempOffset)], settings.tempOffset, 0 ); // Temp offset is set to 0 (Celsius) + configComplete &= getJsonValue(top[FPSTR(_namePubSenState)], settings.publishSensorState, 1 ); // Publish the sensor states + configComplete &= getJsonValue(top[FPSTR(_namePubAc)], settings.pubAcc, 1 ); // Publish accuracy values + configComplete &= getJsonValue(top[FPSTR(_nameHADisc)], settings.HomeAssistantDiscovery, true ); // Activate HomeAssistant Discovery (this Module will be shown as MQTT device in HA) + configComplete &= getJsonValue(top[FPSTR(_namePauseOnActWL)], settings.pauseOnActiveWled, false ); // Pause on active WLED not activated per default + configComplete &= getJsonValue(top[FPSTR(_nameDelCalib)], flags.DeleteCaibration, false ); // IF checked the calibration file will be delete when the save button is pressed + + /* Decimal places */ /* no of digs / -1 means deactivated */ + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameTemp)], settings.decimals.temperature, 1 ); // One decimal places + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameHum)], settings.decimals.humidity, 1 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_namePress)], settings.decimals.pressure, 0 ); // Zero decimal places + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameGasRes)], settings.decimals.gasResistance, -1 ); // deavtivated + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameDrewP)], settings.decimals.drewPoint, 1 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameAHum)], settings.decimals.absHumidity, 1 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameIaq)], settings.decimals.iaq, 0 ); // Index for Air Quality Number is active + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameIaqVerb)], settings.PublishIAQVerbal, -1 ); // deactivated - Index for Air Quality (IAQ) verbal classification + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameStaticIaq)], settings.decimals.staticIaq, 0 ); // activated - Static IAQ is better than IAQ for devices that are not moved + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameStaticIaqVerb)], settings.PublishStaticIAQVerbal, 0 ); // activated + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameCo2)], settings.decimals.co2, 0 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameVoc)], settings.decimals.Voc, 0 ); + configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameGasPer)], settings.decimals.gasPerc, 0 ); + + DEBUG_PRINTLN(F(GOGAB_OK)); + + /* Set the selected temperature unit */ + if (settings.tempScale) { + tempScale = F(_unitFahrenheit); + } + else { + tempScale = F(_unitCelsius); + } + + if (flags.DeleteCaibration) { + DEBUG_PRINT(F(UMOD_DEBUG_NAME "Deleting Calibration File")); + flags.DeleteCaibration = false; + if (WLED_FS.remove(CALIB_FILE_NAME)) { + DEBUG_PRINTLN(F(GOGAB_OK)); + } + else { + DEBUG_PRINTLN(F(GOGAB_FAIL)); + } + } + + if (settings.Interval < 1) settings.Interval = 1; // Correct interval on need (A number less than 1 is not permitted) + iaqSensor.setTemperatureOffset(settings.tempOffset); // Set Temp Offset + + return configComplete; + } + + /** + * @brief Called by WLED: Retunrs the user modul id number + * + * @return uint16_t User module number + */ + uint16_t UsermodBME68X::getId() { + return USERMOD_ID_BME68X; + } + + + /** + * @brief Returns the current temperature in the scale which is choosen in settings + * @return Temperature value (°C or °F as choosen in settings) */ -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 - * + inline float UsermodBME68X::getTemperature() { + return ValuesPtr->temperature; + } + + /** + * @brief Returns the current humidity + * @return Humididty value (%) */ -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 - * + inline float UsermodBME68X::getHumidity() { + return ValuesPtr->humidity; + } + + /** + * @brief Returns the current pressure + * @return Pressure value (hPa) */ -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 + inline float UsermodBME68X::getPressure() { + return ValuesPtr->pressure; + } + + /** + * @brief Returns the current gas resistance + * @return Gas resistance value (kΩ) */ -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 + inline float UsermodBME68X::getGasResistance() { + return ValuesPtr->gasResistance; + } + + /** + * @brief Returns the current absolute humidity + * @return Absolute humidity value (g/m³) */ -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. + inline float UsermodBME68X::getAbsoluteHumidity() { + return ValuesPtr->absHumidity; + } + + /** + * @brief Returns the current dew point + * @return Dew point (°C or °F as choosen in settings) */ -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) + inline float UsermodBME68X::getDewPoint() { + return ValuesPtr->drewPoint; + } + + /** + * @brief Returns the current iaq (Indoor Air Quallity) + * @return Iaq value (0-500) */ -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 + 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) */ -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 + inline float UsermodBME68X::getStaticIaq() { + return ValuesPtr->staticIaq; + } + + /** + * @brief Returns the current co2 + * @return Co2 value (ppm) */ -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) + inline float UsermodBME68X::getCo2() { + return ValuesPtr->co2; + } + + /** + * @brief Returns the current voc (Breath VOC concentration estimate [ppm]) + * @return Voc value (ppm) */ -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() + inline float UsermodBME68X::getVoc() { + return ValuesPtr->Voc; + } + + /** + * @brief Returns the current gas percentage + * @return Gas percentage value (%) */ -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() + 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) */ -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() + 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) */ -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 + 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) */ -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. + 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) */ -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 + 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) */ -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 + 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) */ -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); - } -} - - -static UsermodBME68X bme68x_v2; -REGISTER_USERMOD(bme68x_v2); \ No newline at end of file + inline bool UsermodBME68X::getStabStatus() { + return ValuesPtr->stabStatus; + } + + /** + * @brief Returns the current run in status. + * Indicates if the sensor is undergoing initial stabilization during its first use after production + * @return Tun status accuracy value (0 = switched on first time / 1 = stabilized) + */ + inline bool UsermodBME68X::getRunInStatus() { + return ValuesPtr->runInStatus; + } + + + /** + * @brief Checks whether the library and the sensor are running. + */ + void UsermodBME68X::checkIaqSensorStatus() { + + if (iaqSensor.bsecStatus != BSEC_OK) { + InfoPageStatusLine = "BSEC Library "; + DEBUG_PRINT(UMOD_DEBUG_NAME + InfoPageStatusLine); + flags.InitSuccessful = false; + if (iaqSensor.bsecStatus < BSEC_OK) { + InfoPageStatusLine += " Error Code : " + String(iaqSensor.bsecStatus); + DEBUG_PRINTLN(GOGAB_FAIL); + } + else { + InfoPageStatusLine += " Warning Code : " + String(iaqSensor.bsecStatus); + DEBUG_PRINTLN(GOGAB_WARN); + } + } + else { + InfoPageStatusLine = "Sensor BME68X "; + DEBUG_PRINT(UMOD_DEBUG_NAME + InfoPageStatusLine); + + if (iaqSensor.bme68xStatus != BME68X_OK) { + flags.InitSuccessful = false; + if (iaqSensor.bme68xStatus < BME68X_OK) { + InfoPageStatusLine += "error code: " + String(iaqSensor.bme68xStatus); + DEBUG_PRINTLN(GOGAB_FAIL); + } + else { + InfoPageStatusLine += "warning code: " + String(iaqSensor.bme68xStatus); + DEBUG_PRINTLN(GOGAB_WARN); + } + } + else { + InfoPageStatusLine += F("OK"); + DEBUG_PRINTLN(GOGAB_OK); + } + } + } + + /** + * @brief Loads the calibration data from the file system of the device + */ + void UsermodBME68X::loadState() { + if (WLED_FS.exists(CALIB_FILE_NAME)) { + DEBUG_PRINT(F(UMOD_DEBUG_NAME "Read the calibration file: ")); + File file = WLED_FS.open(CALIB_FILE_NAME, FILE_READ); + if (!file) { + DEBUG_PRINTLN(GOGAB_FAIL); + } + else { + file.read(bsecState, BSEC_MAX_STATE_BLOB_SIZE); + file.close(); + DEBUG_PRINTLN(GOGAB_OK); + iaqSensor.setState(bsecState); + } + } + else { + DEBUG_PRINTLN(F(UMOD_DEBUG_NAME "Calibration file not found.")); + } + } + + /** + * @brief Saves the calibration data from the file system of the device + */ + void UsermodBME68X::saveState() { + DEBUG_PRINT(F(UMOD_DEBUG_NAME "Write the calibration file ")); + File file = WLED_FS.open(CALIB_FILE_NAME, FILE_WRITE); + if (!file) { + DEBUG_PRINTLN(GOGAB_FAIL); + } + else { + iaqSensor.getState(bsecState); + file.write(bsecState, BSEC_MAX_STATE_BLOB_SIZE); + file.close(); + stateUpdateCounter++; + DEBUG_PRINTF("(saved %d times)" GOGAB_OK "\n", stateUpdateCounter); + flags.SaveState = false; // Clear save state flag + + char contbuffer[30]; + + /* Timestamp */ + time_t curr_time; + tm* curr_tm; + time(&curr_time); + curr_tm = localtime(&curr_time); + + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, UMOD_NAME "/Calib Last Run"); + strftime(contbuffer, 30, "%d %B %Y - %T", curr_tm); + if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, contbuffer); + + snprintf(contbuffer, 30, "%d", stateUpdateCounter); + snprintf_P(charbuffer, 127, PSTR("%s/%s"), mqttDeviceTopic, UMOD_NAME "/Calib Count"); + if (WLED_MQTT_CONNECTED) mqtt->publish(charbuffer, 0, false, contbuffer); + } + } + + + static UsermodBME68X bme68x_v2; + REGISTER_USERMOD(bme68x_v2); \ No newline at end of file diff --git a/usermods/BME68X_v2/README.md b/usermods/BME68X_v2/README.md index 7e7a15113..ee2670aa9 100644 --- a/usermods/BME68X_v2/README.md +++ b/usermods/BME68X_v2/README.md @@ -1,65 +1,70 @@ # Usermod BME68X -This usermod was developed for a BME680/BME68X sensor. The BME68X is not compatible with the BME280/BMP280 chip. It has its own library. The original 'BSEC Software Library' from Bosch was used to develop the code. The measured values are displayed on the WLED info page. + +This usermod was developed for a BME680/BME68X sensor. The BME68X is not compatible with the BME280/BMP280 chip. It has its own library. The original 'BSEC Software Library' from Bosch was used to develop the code. The measured values are displayed on the WLED info page.

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

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

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

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

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

The possible settings are - **Enable:** Enables / disables the usermod - **I2C address:** I2C address of the sensor. You can choose between 0X77 & 0X76. The default is 0x77. -- **Interval:** Specifies the interval of seconds at which the usermod should be executed. The default is every second. -- **Pub Chages Only:** If this item is active, the values are only published if they have changed since the last publication. -- **Pub Accuracy:** The Accuracy values associated with the gas values are also published. -- **Pub Calib State:** If this item is active, STABILIZATION_STATUS& RUN_IN_STATUS are also published. +- **Interval:** Specifies the interval of seconds at which the usermod should be executed. The default is every second. +- **Pub Chages Only:** If this item is active, the values are only published if they have changed since the last publication. +- **Pub Accuracy:** The Accuracy values associated with the gas values are also published. +- **Pub Calib State:** If this item is active, STABILIZATION_STATUS& RUN_IN_STATUS are also published. - **Temp Scale:** Here you can choose between °C and °F. -- **Temp Offset:** The temperature offset is always set in °C. It must be converted for Fahrenheit. -- **HA Discovery:** If this item is active, the HomeAssistant sensor tree is created. +- **Temp Offset:** The temperature offset is always set in °C. It must be converted for Fahrenheit. +- **HA Discovery:** If this item is active, the HomeAssistant sensor tree is created. - **Pause While WLED Active:** If WLED has many LEDs to calculate, the computing power may no longer be sufficient to calculate the LEDs and read the sensor data. The LEDs then hang for a few microseconds, which can be seen. If this point is active, no sensor data is fetched as long as WLED is running. -- **Del Calibration Hist:** If a check mark is set here, the calibration file saved in the file system is deleted when the settings are saved. +- **Del Calibration Hist:** If a check mark is set here, the calibration file saved in the file system is deleted when the settings are saved. ### Sensors -Applies to all sensors. The number of decimal places is set here. If the sensor is set to -1, it will no longer be published. In addition, the IAQ values can be activated here in verbal form. + +Applies to all sensors. The number of decimal places is set here. If the sensor is set to -1, it will no longer be published. In addition, the IAQ values can be activated here in verbal form. It is recommended to use the Static IAQ for the IAQ values. This is recommended by Bosch for statically placed devices. @@ -99,8 +105,9 @@ Data is published over MQTT - make sure you've enabled the MQTT sync interface. In addition to outputting via MQTT, you can read the values from the Info Screen on the dashboard page of the device's web interface. Methods also exist to read the read/calculated values from other WLED modules through code. + - getTemperature(); The scale °C/°F is depended to the settings -- getHumidity(); +- getHumidity(); - getPressure(); - getGasResistance(); - getAbsoluteHumidity(); @@ -118,15 +125,36 @@ Methods also exist to read the read/calculated values from other WLED modules th - getStabStatus(); - getRunInStatus(); +## Compilation + +To enable, compile with `BME68X` in `custom_usermods` (e.g. in `platformio_override.ini`) + +Example: + +```[env:esp32_mySpecial] +extends = env:esp32dev +custom_usermods = ${env:esp32dev.custom_usermods} BME68X +``` + ## Revision History + ### Version 1.0.0 + - First version of the BME68X_v user module + ### Version 1.0.1 + - Rebased to WELD Version 0.15 - Reworked some default settings - A problem with the default settings has been fixed +### Version 1.0.2 + +* Rebased to WELD Version 0.16 +* Fixed: Solved compilation problems related to some macro naming interferences. + ## Known problems + - MQTT goes online at device start. Shortly afterwards it goes offline and takes quite a while until it goes online again. The problem does not come from this user module, but from the WLED core. - If you save the settings often, WLED can get stuck. - If many LEDS are connected to WLED, reading the sensor can cause a small but noticeable hang. The "Pause While WLED Active" option was introduced as a workaround. diff --git a/usermods/BME68X_v2/library.json.disabled b/usermods/BME68X_v2/library.json similarity index 58% rename from usermods/BME68X_v2/library.json.disabled rename to usermods/BME68X_v2/library.json index 6bd0bb9b2..0bd4e71b2 100644 --- a/usermods/BME68X_v2/library.json.disabled +++ b/usermods/BME68X_v2/library.json @@ -1,6 +1,5 @@ { - "name:": "BME68X_v2", - "build": { "libArchive": false}, + "name:": "BME68X", "dependencies": { "boschsensortec/BSEC Software Library":"^1.8.1492" } From 1492f1ce897b9aa1237ee70cef607fbf3cfa05ba Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 28 Mar 2025 20:15:36 -0400 Subject: [PATCH 025/153] usermod_v2_HttpPullLightControl: Fix build --- .../{library.json.disabled => library.json} | 0 .../usermod_v2_HttpPullLightControl.cpp | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename usermods/usermod_v2_HttpPullLightControl/{library.json.disabled => library.json} (100%) diff --git a/usermods/usermod_v2_HttpPullLightControl/library.json.disabled b/usermods/usermod_v2_HttpPullLightControl/library.json similarity index 100% rename from usermods/usermod_v2_HttpPullLightControl/library.json.disabled rename to usermods/usermod_v2_HttpPullLightControl/library.json diff --git a/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp b/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp index 854cc2067..b908b057c 100644 --- a/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp +++ b/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp @@ -297,10 +297,10 @@ void HttpPullLightControl::handleResponse(String& responseStr) { // Check for valid JSON, otherwise we brick the program runtime if (jsonStr[0] == '{' || jsonStr[0] == '[') { // Attempt to deserialize the JSON response - DeserializationError error = deserializeJson(doc, jsonStr); + DeserializationError error = deserializeJson(*pDoc, jsonStr); if (error == DeserializationError::Ok) { // Get JSON object from th doc - JsonObject obj = doc.as(); + JsonObject obj = pDoc->as(); // Parse the object throuhg deserializeState (use CALL_MODE_NO_NOTIFY or OR CALL_MODE_DIRECT_CHANGE) deserializeState(obj, CALL_MODE_NO_NOTIFY); } else { From af1a966986713fedef9f3c4ac77cb0bbfbd72b0e Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 28 Mar 2025 20:15:36 -0400 Subject: [PATCH 026/153] usermod/word-clock-matrix: Fix build I've just assigned an arbitrary ID number. The ID system should get removed entirely; we don't want to have to change a base system header to add each module. --- .../{library.json.disabled => library.json} | 0 usermods/word-clock-matrix/word-clock-matrix.cpp | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) rename usermods/word-clock-matrix/{library.json.disabled => library.json} (100%) diff --git a/usermods/word-clock-matrix/library.json.disabled b/usermods/word-clock-matrix/library.json similarity index 100% rename from usermods/word-clock-matrix/library.json.disabled rename to usermods/word-clock-matrix/library.json diff --git a/usermods/word-clock-matrix/word-clock-matrix.cpp b/usermods/word-clock-matrix/word-clock-matrix.cpp index cd8f78c3a..24f69aadb 100644 --- a/usermods/word-clock-matrix/word-clock-matrix.cpp +++ b/usermods/word-clock-matrix/word-clock-matrix.cpp @@ -36,8 +36,8 @@ public: //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))); + Segment &text_seg = strip.getSegment(i); + text_seg.colors[0] = ((0 << 24) | ((0 & 0xFF) << 16) | ((190 & 0xFF) << 8) | ((180 & 0xFF))); strip.getSegment(i).setOption(0, true); strip.setBrightness(64); } @@ -67,7 +67,7 @@ public: //strip.resetSegments(); selectWordSegments(true); colorUpdated(CALL_MODE_FX_CHANGED); - savePreset(13, false); + savePreset(13); selectWordSegments(false); //strip.getSegment(0).setOption(0, true); strip.getSegment(0).setOption(2, true); @@ -329,7 +329,7 @@ public: uint16_t getId() { - return USERMOD_ID_WORD_CLOCK_MATRIX; + return 500; } From 16525c2f3ffe68733ef89549241bcbd954bf5e89 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 28 Mar 2025 20:15:36 -0400 Subject: [PATCH 027/153] usermod/BME68X_v2: Fix macro collision Rename a macro that unfortunately collides with the ESP32 platform. This allows re-enabling this usermod. --- usermods/BME68X_v2/BME68X_v2.cpp | 42 +++++++++---------- .../{library.json.disabled => library.json} | 0 2 files changed, 21 insertions(+), 21 deletions(-) rename usermods/BME68X_v2/{library.json.disabled => library.json} (100%) diff --git a/usermods/BME68X_v2/BME68X_v2.cpp b/usermods/BME68X_v2/BME68X_v2.cpp index 081d03ff9..ab84dd0d3 100644 --- a/usermods/BME68X_v2/BME68X_v2.cpp +++ b/usermods/BME68X_v2/BME68X_v2.cpp @@ -1,5 +1,5 @@ /** - * @file usermod_BMW68X.h + * @file usermod_BMW68X.cpp * @author Gabriel A. Sieben (GeoGab) * @brief Usermod for WLED to implement the BME680/BME688 sensor * @version 1.0.0 @@ -33,10 +33,10 @@ /* 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 "]" +#define OK_MSG INFO_COLUMN "[" ESC_FGCOLOR_GREEN "OK" ESC_STYLE_RESET "]" +#define FAIL_MSG INFO_COLUMN "[" ESC_FGCOLOR_RED "FAIL" ESC_STYLE_RESET "]" +#define WARN_MSG INFO_COLUMN "[" ESC_FGCOLOR_YELLOW "WARN" ESC_STYLE_RESET "]" +#define DONE_MSG INFO_COLUMN "[" ESC_FGCOLOR_CYAN "DONE" ESC_STYLE_RESET "]" #include "bsec.h" // Bosch sensor library #include "wled.h" @@ -328,7 +328,7 @@ void UsermodBME68X::setup() { /* 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)); + DEBUG_PRINTLN(F(UMOD_DEBUG_NAME "I2C is not activated. Please activate I2C first." FAIL_MSG)); return; } @@ -352,7 +352,7 @@ void UsermodBME68X::setup() { loadState(); // Load the old calibration data checkIaqSensorStatus(); // Check the sensor status // HomeAssistantDiscovery(); - DEBUG_PRINTLN(F(INFO_COLUMN DONE)); + DEBUG_PRINTLN(F(INFO_COLUMN DONE_MSG)); } /** @@ -564,7 +564,7 @@ void UsermodBME68X::HomeAssistantDiscovery() { MQTT_PublishHASensor(_nameStabStatus, "", _unitNone, settings.publishSensorState - 1, 1); MQTT_PublishHASensor(_nameRunInStatus, "", _unitNone, settings.publishSensorState - 1, 1); - DEBUG_PRINTLN(UMOD_DEBUG_NAME DONE); + DEBUG_PRINTLN(UMOD_DEBUG_NAME DONE_MSG); } /** @@ -750,7 +750,7 @@ void UsermodBME68X::addToConfig(JsonObject& root) { sensors_json[FPSTR(_nameVoc)] = settings.decimals.Voc; sensors_json[FPSTR(_nameGasPer)] = settings.decimals.gasPerc; - DEBUG_PRINTLN(F(OK)); + DEBUG_PRINTLN(F(OK_MSG)); } /** @@ -831,7 +831,7 @@ bool UsermodBME68X::readFromConfig(JsonObject& root) { configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameVoc)], settings.decimals.Voc, 0 ); configComplete &= getJsonValue(top["Sensors"][FPSTR(_nameGasPer)], settings.decimals.gasPerc, 0 ); - DEBUG_PRINTLN(F(OK)); + DEBUG_PRINTLN(F(OK_MSG)); /* Set the selected temperature unit */ if (settings.tempScale) { @@ -845,10 +845,10 @@ bool UsermodBME68X::readFromConfig(JsonObject& root) { DEBUG_PRINT(F(UMOD_DEBUG_NAME "Deleting Calibration File")); flags.DeleteCaibration = false; if (WLED_FS.remove(CALIB_FILE_NAME)) { - DEBUG_PRINTLN(F(OK)); + DEBUG_PRINTLN(F(OK_MSG)); } else { - DEBUG_PRINTLN(F(FAIL)); + DEBUG_PRINTLN(F(FAIL_MSG)); } } @@ -1026,11 +1026,11 @@ void UsermodBME68X::checkIaqSensorStatus() { flags.InitSuccessful = false; if (iaqSensor.bsecStatus < BSEC_OK) { InfoPageStatusLine += " Error Code : " + String(iaqSensor.bsecStatus); - DEBUG_PRINTLN(FAIL); + DEBUG_PRINTLN(FAIL_MSG); } else { InfoPageStatusLine += " Warning Code : " + String(iaqSensor.bsecStatus); - DEBUG_PRINTLN(WARN); + DEBUG_PRINTLN(WARN_MSG); } } else { @@ -1041,16 +1041,16 @@ void UsermodBME68X::checkIaqSensorStatus() { flags.InitSuccessful = false; if (iaqSensor.bme68xStatus < BME68X_OK) { InfoPageStatusLine += "error code: " + String(iaqSensor.bme68xStatus); - DEBUG_PRINTLN(FAIL); + DEBUG_PRINTLN(FAIL_MSG); } else { InfoPageStatusLine += "warning code: " + String(iaqSensor.bme68xStatus); - DEBUG_PRINTLN(WARN); + DEBUG_PRINTLN(WARN_MSG); } } else { InfoPageStatusLine += F("OK"); - DEBUG_PRINTLN(OK); + DEBUG_PRINTLN(OK_MSG); } } } @@ -1063,12 +1063,12 @@ void UsermodBME68X::loadState() { 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); + DEBUG_PRINTLN(FAIL_MSG); } else { file.read(bsecState, BSEC_MAX_STATE_BLOB_SIZE); file.close(); - DEBUG_PRINTLN(OK); + DEBUG_PRINTLN(OK_MSG); iaqSensor.setState(bsecState); } } @@ -1084,14 +1084,14 @@ 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); + DEBUG_PRINTLN(FAIL_MSG); } else { iaqSensor.getState(bsecState); file.write(bsecState, BSEC_MAX_STATE_BLOB_SIZE); file.close(); stateUpdateCounter++; - DEBUG_PRINTF("(saved %d times)" OK "\n", stateUpdateCounter); + DEBUG_PRINTF("(saved %d times)" OK_MSG "\n", stateUpdateCounter); flags.SaveState = false; // Clear save state flag char contbuffer[30]; diff --git a/usermods/BME68X_v2/library.json.disabled b/usermods/BME68X_v2/library.json similarity index 100% rename from usermods/BME68X_v2/library.json.disabled rename to usermods/BME68X_v2/library.json From 468ef50f75c6337504c68efdf80b95a43f9c01bb Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 28 Mar 2025 20:15:36 -0400 Subject: [PATCH 028/153] usermods/rgb-rotary-encoder: Enable usermod --- .../rgb-rotary-encoder/{library.json.disabled => library.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename usermods/rgb-rotary-encoder/{library.json.disabled => library.json} (100%) diff --git a/usermods/rgb-rotary-encoder/library.json.disabled b/usermods/rgb-rotary-encoder/library.json similarity index 100% rename from usermods/rgb-rotary-encoder/library.json.disabled rename to usermods/rgb-rotary-encoder/library.json From d03604b14af50ca5ead6b000c9eec27d79de4f81 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 28 Mar 2025 20:15:36 -0400 Subject: [PATCH 029/153] usermods/MAX17048_v2: Fix precision variable type The String constructors are ambiguous with uint8_ts. --- usermods/MAX17048_v2/MAX17048_v2.cpp | 4 ++-- .../{library.json.disabled => library.json} | 0 usermods/MAX17048_v2/readme.md | 14 ++------------ 3 files changed, 4 insertions(+), 14 deletions(-) rename usermods/MAX17048_v2/{library.json.disabled => library.json} (100%) diff --git a/usermods/MAX17048_v2/MAX17048_v2.cpp b/usermods/MAX17048_v2/MAX17048_v2.cpp index c284bca7f..520f1a7b3 100644 --- a/usermods/MAX17048_v2/MAX17048_v2.cpp +++ b/usermods/MAX17048_v2/MAX17048_v2.cpp @@ -35,8 +35,8 @@ class Usermod_MAX17048 : public Usermod { unsigned long lastSend = UINT32_MAX - (USERMOD_MAX17048_MAX_MONITOR_INTERVAL - USERMOD_MAX17048_FIRST_MONITOR_AT); - uint8_t VoltageDecimals = 3; // Number of decimal places in published voltage values - uint8_t PercentDecimals = 1; // Number of decimal places in published percent values + unsigned VoltageDecimals = 3; // Number of decimal places in published voltage values + unsigned PercentDecimals = 1; // Number of decimal places in published percent values // string that are used multiple time (this will save some flash memory) static const char _name[]; diff --git a/usermods/MAX17048_v2/library.json.disabled b/usermods/MAX17048_v2/library.json similarity index 100% rename from usermods/MAX17048_v2/library.json.disabled rename to usermods/MAX17048_v2/library.json diff --git a/usermods/MAX17048_v2/readme.md b/usermods/MAX17048_v2/readme.md index 958e6def2..20e4d9618 100644 --- a/usermods/MAX17048_v2/readme.md +++ b/usermods/MAX17048_v2/readme.md @@ -5,26 +5,16 @@ This usermod reads information from an Adafruit MAX17048 and outputs the follow ## Dependencies -Libraries: -- `Adafruit_BusIO@~1.14.5` (by [adafruit](https://github.com/adafruit/Adafruit_BusIO)) -- `Adafruit_MAX1704X@~1.0.2` (by [adafruit](https://github.com/adafruit/Adafruit_MAX1704X)) - -These must be added under `lib_deps` in your `platform.ini` (or `platform_override.ini`). Data is published over MQTT - make sure you've enabled the MQTT sync interface. ## Compilation +Add "MAX17048_v2" to your platformio.ini environment's custom_usermods and build. To enable, compile with `USERMOD_MAX17048` define in the build_flags (e.g. in `platformio.ini` or `platformio_override.ini`) such as in the example below: ```ini [env:usermod_max17048_d1_mini] extends = env:d1_mini -build_flags = - ${common.build_flags_esp8266} - -D USERMOD_MAX17048 -lib_deps = - ${esp8266.lib_deps} - https://github.com/adafruit/Adafruit_BusIO @ 1.14.5 - https://github.com/adafruit/Adafruit_MAX1704X @ 1.0.2 +custom_usermods = ${env:d1_mini.custom_usermods} AHT10 ``` ### Configuration Options From 035d4aed266a21bcf28b23dc16a4349dd96a69d4 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 28 Mar 2025 20:15:36 -0400 Subject: [PATCH 030/153] usermods\MAX17048_v2: Fix readme --- usermods/MAX17048_v2/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/MAX17048_v2/readme.md b/usermods/MAX17048_v2/readme.md index 20e4d9618..df42f989a 100644 --- a/usermods/MAX17048_v2/readme.md +++ b/usermods/MAX17048_v2/readme.md @@ -14,7 +14,7 @@ To enable, compile with `USERMOD_MAX17048` define in the build_flags (e.g. in `p ```ini [env:usermod_max17048_d1_mini] extends = env:d1_mini -custom_usermods = ${env:d1_mini.custom_usermods} AHT10 +custom_usermods = ${env:d1_mini.custom_usermods} MAX17048_v2 ``` ### Configuration Options From e2c919d270baf9ee9fd4997fe2bd69bc2c681e19 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 28 Mar 2025 20:15:36 -0400 Subject: [PATCH 031/153] Fixup usermods_v2_four_line_display_ALT Get this usermod to build, and restore the interconnect with its partner usermod_v2_rotary_encoder_ui_ALT. --- .../{4LD_wled_fonts.c => 4LD_wled_fonts.h} | 4 +- .../library.json | 7 + .../library.json.disabled | 3 - .../platformio_override.sample.ini | 17 +- .../usermod_v2_four_line_display.h | 315 +++++++++++++++++ .../usermod_v2_four_line_display_ALT.cpp | 324 +----------------- .../library.json | 5 +- .../platformio_override.sample.ini | 12 +- .../readme.md | 4 - .../setup_deps.py | 8 + .../usermod_v2_rotary_encoder_ui_ALT.cpp | 4 + 11 files changed, 352 insertions(+), 351 deletions(-) rename usermods/usermod_v2_four_line_display_ALT/{4LD_wled_fonts.c => 4LD_wled_fonts.h} (99%) create mode 100644 usermods/usermod_v2_four_line_display_ALT/library.json delete mode 100644 usermods/usermod_v2_four_line_display_ALT/library.json.disabled create mode 100644 usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display.h create mode 100644 usermods/usermod_v2_rotary_encoder_ui_ALT/setup_deps.py diff --git a/usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.c b/usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.h similarity index 99% rename from usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.c rename to usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.h index 5495f9194..0fb5f3bbf 100644 --- a/usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.c +++ b/usermods/usermod_v2_four_line_display_ALT/4LD_wled_fonts.h @@ -1,7 +1,5 @@ -#pragma once - //WLED custom fonts, curtesy of @Benji (https://github.com/Proto-molecule) - +#pragma once /* Fontname: wled_logo_akemi_4x4 diff --git a/usermods/usermod_v2_four_line_display_ALT/library.json b/usermods/usermod_v2_four_line_display_ALT/library.json new file mode 100644 index 000000000..29cab450f --- /dev/null +++ b/usermods/usermod_v2_four_line_display_ALT/library.json @@ -0,0 +1,7 @@ +{ + "name:": "four_line_display_ALT", + "dependencies": { + "U8g2": "~2.34.4", + "Wire": "" + } +} \ No newline at end of file diff --git a/usermods/usermod_v2_four_line_display_ALT/library.json.disabled b/usermods/usermod_v2_four_line_display_ALT/library.json.disabled deleted file mode 100644 index 56612c96e..000000000 --- a/usermods/usermod_v2_four_line_display_ALT/library.json.disabled +++ /dev/null @@ -1,3 +0,0 @@ -{ - "name:": "usermod_v2_four_line_display_ALT" -} \ No newline at end of file diff --git a/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini b/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini index e59637453..f4fa8c9d8 100644 --- a/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini +++ b/usermods/usermod_v2_four_line_display_ALT/platformio_override.sample.ini @@ -1,18 +1,11 @@ [platformio] -default_envs = esp32dev +default_envs = esp32dev_fld -[env:esp32dev] -board = esp32dev -platform = ${esp32.platform} -build_unflags = ${common.build_unflags} +[env:esp32dev_fld] +extends = env:esp32dev_V4 +custom_usermods = ${env:esp32dev_V4.custom_usermods} four_line_display_ALT build_flags = - ${common.build_flags_esp32} - -D USERMOD_FOUR_LINE_DISPLAY + ${env:esp32dev_V4.build_flags} -D FLD_TYPE=SH1106 -D I2CSCLPIN=27 -D I2CSDAPIN=26 - -lib_deps = - ${esp32.lib_deps} - U8g2@~2.34.4 - Wire diff --git a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display.h b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display.h new file mode 100644 index 000000000..4fc963b9c --- /dev/null +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display.h @@ -0,0 +1,315 @@ +#include "wled.h" +#undef U8X8_NO_HW_I2C // borrowed from WLEDMM: we do want I2C hardware drivers - if possible +#include // from https://github.com/olikraus/u8g2/ + +#pragma once + +#ifndef FLD_ESP32_NO_THREADS + #define FLD_ESP32_USE_THREADS // comment out to use 0.13.x behaviour without parallel update task - slower, but more robust. May delay other tasks like LEDs or audioreactive!! +#endif + +#ifndef FLD_PIN_CS + #define FLD_PIN_CS 15 +#endif + +#ifdef ARDUINO_ARCH_ESP32 + #ifndef FLD_PIN_DC + #define FLD_PIN_DC 19 + #endif + #ifndef FLD_PIN_RESET + #define FLD_PIN_RESET 26 + #endif +#else + #ifndef FLD_PIN_DC + #define FLD_PIN_DC 12 + #endif + #ifndef FLD_PIN_RESET + #define FLD_PIN_RESET 16 + #endif +#endif + +#ifndef FLD_TYPE + #ifndef FLD_SPI_DEFAULT + #define FLD_TYPE SSD1306 + #else + #define FLD_TYPE SSD1306_SPI + #endif +#endif + +// When to time out to the clock or blank the screen +// if SLEEP_MODE_ENABLED. +#define SCREEN_TIMEOUT_MS 60*1000 // 1 min + +// Minimum time between redrawing screen in ms +#define REFRESH_RATE_MS 1000 + +// Extra char (+1) for null +#define LINE_BUFFER_SIZE 16+1 +#define MAX_JSON_CHARS 19+1 +#define MAX_MODE_LINE_SPACE 13+1 + +typedef enum { + NONE = 0, + SSD1306, // U8X8_SSD1306_128X32_UNIVISION_HW_I2C + SH1106, // U8X8_SH1106_128X64_WINSTAR_HW_I2C + SSD1306_64, // U8X8_SSD1306_128X64_NONAME_HW_I2C + SSD1305, // U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C + SSD1305_64, // U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C + SSD1306_SPI, // U8X8_SSD1306_128X32_NONAME_HW_SPI + SSD1306_SPI64, // U8X8_SSD1306_128X64_NONAME_HW_SPI + SSD1309_SPI64, // U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI + SSD1309_64 // U8X8_SSD1309_128X64_NONAME0_HW_I2C +} DisplayType; + +class FourLineDisplayUsermod : public Usermod { + #if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) + public: + FourLineDisplayUsermod() { if (!instance) instance = this; } + static FourLineDisplayUsermod* getInstance(void) { return instance; } + #endif + + private: + + static FourLineDisplayUsermod *instance; + bool initDone = false; + volatile bool drawing = false; + volatile bool lockRedraw = false; + + // HW interface & configuration + U8X8 *u8x8 = nullptr; // pointer to U8X8 display object + + #ifndef FLD_SPI_DEFAULT + int8_t ioPin[3] = {-1, -1, -1}; // I2C pins: SCL, SDA + uint32_t ioFrequency = 400000; // in Hz (minimum is 100000, baseline is 400000 and maximum should be 3400000) + #else + int8_t ioPin[3] = {FLD_PIN_CS, FLD_PIN_DC, FLD_PIN_RESET}; // custom SPI pins: CS, DC, RST + uint32_t ioFrequency = 1000000; // in Hz (minimum is 500kHz, baseline is 1MHz and maximum should be 20MHz) + #endif + + DisplayType type = FLD_TYPE; // display type + bool flip = false; // flip display 180° + uint8_t contrast = 10; // screen contrast + uint8_t lineHeight = 1; // 1 row or 2 rows + uint16_t refreshRate = REFRESH_RATE_MS; // in ms + uint32_t screenTimeout = SCREEN_TIMEOUT_MS; // in ms + bool sleepMode = true; // allow screen sleep? + bool clockMode = false; // display clock + bool showSeconds = true; // display clock with seconds + bool enabled = true; + bool contrastFix = false; + + // Next variables hold the previous known values to determine if redraw is + // required. + String knownSsid = apSSID; + IPAddress knownIp = IPAddress(4, 3, 2, 1); + uint8_t knownBrightness = 0; + uint8_t knownEffectSpeed = 0; + uint8_t knownEffectIntensity = 0; + uint8_t knownMode = 0; + uint8_t knownPalette = 0; + uint8_t knownMinute = 99; + uint8_t knownHour = 99; + byte brightness100; + byte fxspeed100; + byte fxintensity100; + bool knownnightlight = nightlightActive; + bool wificonnected = interfacesInited; + bool powerON = true; + + bool displayTurnedOff = false; + unsigned long nextUpdate = 0; + unsigned long lastRedraw = 0; + unsigned long overlayUntil = 0; + + // Set to 2 or 3 to mark lines 2 or 3. Other values ignored. + byte markLineNum = 255; + byte markColNum = 255; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _contrast[]; + static const char _refreshRate[]; + static const char _screenTimeOut[]; + static const char _flip[]; + static const char _sleepMode[]; + static const char _clockMode[]; + static const char _showSeconds[]; + static const char _busClkFrequency[]; + static const char _contrastFix[]; + + // If display does not work or looks corrupted check the + // constructor reference: + // https://github.com/olikraus/u8g2/wiki/u8x8setupcpp + // or check the gallery: + // https://github.com/olikraus/u8g2/wiki/gallery + + // some displays need this to properly apply contrast + void setVcomh(bool highContrast); + void startDisplay(); + + /** + * Wrappers for screen drawing + */ + void setFlipMode(uint8_t mode); + void setContrast(uint8_t contrast); + void drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH=false); + void draw2x2String(uint8_t col, uint8_t row, const char *string); + void drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH=false); + void draw2x2Glyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font); + void draw2x2GlyphIcons(); + uint8_t getCols(); + void clear(); + void setPowerSave(uint8_t save); + void center(String &line, uint8_t width); + + /** + * Display the current date and time in large characters + * on the middle rows. Based 24 or 12 hour depending on + * the useAMPM configuration. + */ + void showTime(); + + /** + * Enable sleep (turn the display off) or clock mode. + */ + void sleepOrClock(bool enabled); + + public: + + // gets called once at boot. Do all initialization that doesn't depend on + // network here + void setup() override; + + // gets called every time WiFi is (re-)connected. Initialize own network + // interfaces here + void connected() override; + + /** + * Da loop. + */ + void loop() override; + + //function to update lastredraw + inline void updateRedrawTime() { lastRedraw = millis(); } + + /** + * Redraw the screen (but only if things have changed + * or if forceRedraw). + */ + void redraw(bool forceRedraw); + + void updateBrightness(); + void updateSpeed(); + void updateIntensity(); + void drawStatusIcons(); + + /** + * marks the position of the arrow showing + * the current setting being changed + * pass line and colum info + */ + void setMarkLine(byte newMarkLineNum, byte newMarkColNum); + + //Draw the arrow for the current setting being changed + void drawArrow(); + + //Display the current effect or palette (desiredEntry) + // on the appropriate line (row). + void showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row); + + /** + * If there screen is off or in clock is displayed, + * this will return true. This allows us to throw away + * the first input from the rotary encoder but + * to wake up the screen. + */ + bool wakeDisplay(); + + /** + * Allows you to show one line and a glyph as overlay for a period of time. + * Clears the screen and prints. + * Used in Rotary Encoder usermod. + */ + void overlay(const char* line1, long showHowLong, byte glyphType); + + /** + * Allows you to show Akemi WLED logo overlay for a period of time. + * Clears the screen and prints. + */ + void overlayLogo(long showHowLong); + + /** + * Allows you to show two lines as overlay for a period of time. + * Clears the screen and prints. + * Used in Auto Save usermod + */ + void overlay(const char* line1, const char* line2, long showHowLong); + + void networkOverlay(const char* line1, long showHowLong); + + /** + * handleButton() can be used to override default button behaviour. Returning true + * will prevent button working in a default way. + * Replicating button.cpp + */ + bool handleButton(uint8_t b); + + void onUpdateBegin(bool init) override; + + /* + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ + //void addToJsonInfo(JsonObject& root) override; + + /* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + //void addToJsonState(JsonObject& root) override; + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + //void readFromJsonState(JsonObject& root) override; + + void appendConfigData() override; + + /* + * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. + * It will be called by WLED when settings are actually saved (for example, LED settings are saved) + * If you want to force saving the current state, use serializeConfig() in your loop(). + * + * CAUTION: serializeConfig() will initiate a filesystem write operation. + * It might cause the LEDs to stutter and will cause flash wear if called too often. + * Use it sparingly and always in the loop, never in network callbacks! + * + * addToConfig() will also not yet add your setting to one of the settings pages automatically. + * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. + * + * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! + */ + void addToConfig(JsonObject& root) override; + + /* + * readFromConfig() can be used to read back the custom settings you added with addToConfig(). + * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) + * + * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), + * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. + * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + */ + bool readFromConfig(JsonObject& root) override; + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() override { + return USERMOD_ID_FOUR_LINE_DISP; + } + }; + \ No newline at end of file diff --git a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.cpp b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.cpp index 93c4110c3..36a8b029f 100644 --- a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.cpp +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.cpp @@ -1,11 +1,5 @@ -#include "wled.h" -#undef U8X8_NO_HW_I2C // borrowed from WLEDMM: we do want I2C hardware drivers - if possible -#include // from https://github.com/olikraus/u8g2/ -#include "4LD_wled_fonts.c" - -#ifndef FLD_ESP32_NO_THREADS - #define FLD_ESP32_USE_THREADS // comment out to use 0.13.x behaviour without parallel update task - slower, but more robust. May delay other tasks like LEDs or audioreactive!! -#endif +#include "usermod_v2_four_line_display.h" +#include "4LD_wled_fonts.h" // // Inspired by the usermod_v2_four_line_display @@ -20,330 +14,18 @@ // // Make sure to enable NTP and set your time zone in WLED Config | Time. // -// REQUIREMENT: You must add the following requirements to -// REQUIREMENT: "lib_deps" within platformio.ini / platformio_override.ini -// REQUIREMENT: * U8g2 (the version already in platformio.ini is fine) -// REQUIREMENT: * Wire -// // If display does not work or looks corrupted check the // constructor reference: // https://github.com/olikraus/u8g2/wiki/u8x8setupcpp // or check the gallery: // https://github.com/olikraus/u8g2/wiki/gallery -#ifndef FLD_PIN_CS - #define FLD_PIN_CS 15 -#endif - -#ifdef ARDUINO_ARCH_ESP32 - #ifndef FLD_PIN_DC - #define FLD_PIN_DC 19 - #endif - #ifndef FLD_PIN_RESET - #define FLD_PIN_RESET 26 - #endif -#else - #ifndef FLD_PIN_DC - #define FLD_PIN_DC 12 - #endif - #ifndef FLD_PIN_RESET - #define FLD_PIN_RESET 16 - #endif -#endif - -#ifndef FLD_TYPE - #ifndef FLD_SPI_DEFAULT - #define FLD_TYPE SSD1306 - #else - #define FLD_TYPE SSD1306_SPI - #endif -#endif - -// When to time out to the clock or blank the screen -// if SLEEP_MODE_ENABLED. -#define SCREEN_TIMEOUT_MS 60*1000 // 1 min - -// Minimum time between redrawing screen in ms -#define REFRESH_RATE_MS 1000 - -// Extra char (+1) for null -#define LINE_BUFFER_SIZE 16+1 -#define MAX_JSON_CHARS 19+1 -#define MAX_MODE_LINE_SPACE 13+1 - #ifdef ARDUINO_ARCH_ESP32 static TaskHandle_t Display_Task = nullptr; void DisplayTaskCode(void * parameter); #endif - -typedef enum { - NONE = 0, - SSD1306, // U8X8_SSD1306_128X32_UNIVISION_HW_I2C - SH1106, // U8X8_SH1106_128X64_WINSTAR_HW_I2C - SSD1306_64, // U8X8_SSD1306_128X64_NONAME_HW_I2C - SSD1305, // U8X8_SSD1305_128X32_ADAFRUIT_HW_I2C - SSD1305_64, // U8X8_SSD1305_128X64_ADAFRUIT_HW_I2C - SSD1306_SPI, // U8X8_SSD1306_128X32_NONAME_HW_SPI - SSD1306_SPI64, // U8X8_SSD1306_128X64_NONAME_HW_SPI - SSD1309_SPI64, // U8X8_SSD1309_128X64_NONAME0_4W_HW_SPI - SSD1309_64 // U8X8_SSD1309_128X64_NONAME0_HW_I2C -} DisplayType; - - -class FourLineDisplayUsermod : public Usermod { -#if defined(ARDUINO_ARCH_ESP32) && defined(FLD_ESP32_USE_THREADS) - public: - FourLineDisplayUsermod() { if (!instance) instance = this; } - static FourLineDisplayUsermod* getInstance(void) { return instance; } -#endif - - private: - - static FourLineDisplayUsermod *instance; - bool initDone = false; - volatile bool drawing = false; - volatile bool lockRedraw = false; - - // HW interface & configuration - U8X8 *u8x8 = nullptr; // pointer to U8X8 display object - - #ifndef FLD_SPI_DEFAULT - int8_t ioPin[3] = {-1, -1, -1}; // I2C pins: SCL, SDA - uint32_t ioFrequency = 400000; // in Hz (minimum is 100000, baseline is 400000 and maximum should be 3400000) - #else - int8_t ioPin[3] = {FLD_PIN_CS, FLD_PIN_DC, FLD_PIN_RESET}; // custom SPI pins: CS, DC, RST - uint32_t ioFrequency = 1000000; // in Hz (minimum is 500kHz, baseline is 1MHz and maximum should be 20MHz) - #endif - - DisplayType type = FLD_TYPE; // display type - bool flip = false; // flip display 180° - uint8_t contrast = 10; // screen contrast - uint8_t lineHeight = 1; // 1 row or 2 rows - uint16_t refreshRate = REFRESH_RATE_MS; // in ms - uint32_t screenTimeout = SCREEN_TIMEOUT_MS; // in ms - bool sleepMode = true; // allow screen sleep? - bool clockMode = false; // display clock - bool showSeconds = true; // display clock with seconds - bool enabled = true; - bool contrastFix = false; - - // Next variables hold the previous known values to determine if redraw is - // required. - String knownSsid = apSSID; - IPAddress knownIp = IPAddress(4, 3, 2, 1); - uint8_t knownBrightness = 0; - uint8_t knownEffectSpeed = 0; - uint8_t knownEffectIntensity = 0; - uint8_t knownMode = 0; - uint8_t knownPalette = 0; - uint8_t knownMinute = 99; - uint8_t knownHour = 99; - byte brightness100; - byte fxspeed100; - byte fxintensity100; - bool knownnightlight = nightlightActive; - bool wificonnected = interfacesInited; - bool powerON = true; - - bool displayTurnedOff = false; - unsigned long nextUpdate = 0; - unsigned long lastRedraw = 0; - unsigned long overlayUntil = 0; - - // Set to 2 or 3 to mark lines 2 or 3. Other values ignored. - byte markLineNum = 255; - byte markColNum = 255; - - // strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _enabled[]; - static const char _contrast[]; - static const char _refreshRate[]; - static const char _screenTimeOut[]; - static const char _flip[]; - static const char _sleepMode[]; - static const char _clockMode[]; - static const char _showSeconds[]; - static const char _busClkFrequency[]; - static const char _contrastFix[]; - - // If display does not work or looks corrupted check the - // constructor reference: - // https://github.com/olikraus/u8g2/wiki/u8x8setupcpp - // or check the gallery: - // https://github.com/olikraus/u8g2/wiki/gallery - - // some displays need this to properly apply contrast - void setVcomh(bool highContrast); - void startDisplay(); - - /** - * Wrappers for screen drawing - */ - void setFlipMode(uint8_t mode); - void setContrast(uint8_t contrast); - void drawString(uint8_t col, uint8_t row, const char *string, bool ignoreLH=false); - void draw2x2String(uint8_t col, uint8_t row, const char *string); - void drawGlyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font, bool ignoreLH=false); - void draw2x2Glyph(uint8_t col, uint8_t row, char glyph, const uint8_t *font); - void draw2x2GlyphIcons(); - uint8_t getCols(); - void clear(); - void setPowerSave(uint8_t save); - void center(String &line, uint8_t width); - - /** - * Display the current date and time in large characters - * on the middle rows. Based 24 or 12 hour depending on - * the useAMPM configuration. - */ - void showTime(); - - /** - * Enable sleep (turn the display off) or clock mode. - */ - void sleepOrClock(bool enabled); - - public: - - // gets called once at boot. Do all initialization that doesn't depend on - // network here - void setup() override; - - // gets called every time WiFi is (re-)connected. Initialize own network - // interfaces here - void connected() override; - - /** - * Da loop. - */ - void loop() override; - - //function to update lastredraw - inline void updateRedrawTime() { lastRedraw = millis(); } - - /** - * Redraw the screen (but only if things have changed - * or if forceRedraw). - */ - void redraw(bool forceRedraw); - - void updateBrightness(); - void updateSpeed(); - void updateIntensity(); - void drawStatusIcons(); - - /** - * marks the position of the arrow showing - * the current setting being changed - * pass line and colum info - */ - void setMarkLine(byte newMarkLineNum, byte newMarkColNum); - - //Draw the arrow for the current setting being changed - void drawArrow(); - - //Display the current effect or palette (desiredEntry) - // on the appropriate line (row). - void showCurrentEffectOrPalette(int inputEffPal, const char *qstring, uint8_t row); - - /** - * If there screen is off or in clock is displayed, - * this will return true. This allows us to throw away - * the first input from the rotary encoder but - * to wake up the screen. - */ - bool wakeDisplay(); - - /** - * Allows you to show one line and a glyph as overlay for a period of time. - * Clears the screen and prints. - * Used in Rotary Encoder usermod. - */ - void overlay(const char* line1, long showHowLong, byte glyphType); - - /** - * Allows you to show Akemi WLED logo overlay for a period of time. - * Clears the screen and prints. - */ - void overlayLogo(long showHowLong); - - /** - * Allows you to show two lines as overlay for a period of time. - * Clears the screen and prints. - * Used in Auto Save usermod - */ - void overlay(const char* line1, const char* line2, long showHowLong); - - void networkOverlay(const char* line1, long showHowLong); - - /** - * handleButton() can be used to override default button behaviour. Returning true - * will prevent button working in a default way. - * Replicating button.cpp - */ - bool handleButton(uint8_t b); - - void onUpdateBegin(bool init) override; - - /* - * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. - * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. - * Below it is shown how this could be used for e.g. a light sensor - */ - //void addToJsonInfo(JsonObject& root) override; - - /* - * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - //void addToJsonState(JsonObject& root) override; - - /* - * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - //void readFromJsonState(JsonObject& root) override; - - void appendConfigData() override; - - /* - * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. - * It will be called by WLED when settings are actually saved (for example, LED settings are saved) - * If you want to force saving the current state, use serializeConfig() in your loop(). - * - * CAUTION: serializeConfig() will initiate a filesystem write operation. - * It might cause the LEDs to stutter and will cause flash wear if called too often. - * Use it sparingly and always in the loop, never in network callbacks! - * - * addToConfig() will also not yet add your setting to one of the settings pages automatically. - * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. - * - * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! - */ - void addToConfig(JsonObject& root) override; - - /* - * readFromConfig() can be used to read back the custom settings you added with addToConfig(). - * This is called by WLED when settings are loaded (currently this only happens once immediately after boot) - * - * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), - * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. - * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) - */ - bool readFromConfig(JsonObject& root) override; - - /* - * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). - * This could be used in the future for the system to determine whether your usermod is installed. - */ - uint16_t getId() override { - return USERMOD_ID_FOUR_LINE_DISP; - } -}; - // strings to reduce flash memory usage (used more than twice) const char FourLineDisplayUsermod::_name[] PROGMEM = "4LineDisplay"; const char FourLineDisplayUsermod::_enabled[] PROGMEM = "enabled"; @@ -1387,4 +1069,4 @@ bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { static FourLineDisplayUsermod usermod_v2_four_line_display_alt; -REGISTER_USERMOD(usermod_v2_four_line_display_alt); \ No newline at end of file +REGISTER_USERMOD(usermod_v2_four_line_display_alt); diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/library.json b/usermods/usermod_v2_rotary_encoder_ui_ALT/library.json index 5f857218b..ef28d01cf 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/library.json +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/library.json @@ -1,3 +1,6 @@ { - "name:": "usermod_v2_rotary_encoder_ui_ALT" + "name:": "rotary_encoder_ui_ALT", + "build": { + "extraScript": "setup_deps.py" + } } \ No newline at end of file diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini b/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini index 8a88fd6b5..2511d2fa3 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/platformio_override.sample.ini @@ -1,13 +1,11 @@ [platformio] -default_envs = esp32dev +default_envs = esp32dev_re -[env:esp32dev] -board = esp32dev -platform = ${esp32.platform} -build_unflags = ${common.build_unflags} +[env:esp32dev_re] +extends = env:esp32dev_V4 +custom_usermods = ${env:esp32dev_V4.custom_usermods} rotary_encoder_ui_ALT build_flags = - ${common.build_flags_esp32} - -D USERMOD_ROTARY_ENCODER_UI + ${env:esp32dev_V4.build_flags} -D USERMOD_ROTARY_ENCODER_GPIO=INPUT -D ENCODER_DT_PIN=21 -D ENCODER_CLK_PIN=23 diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md index c46e87663..3df6de6ef 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md @@ -25,10 +25,6 @@ Copy the example `platformio_override.sample.ini` to the root directory of your ### Define Your Options -* `USERMOD_ROTARY_ENCODER_UI` - define this to have this user mod included wled00\usermods_list.cpp -* `USERMOD_FOUR_LINE_DISPLAY` - define this to have this the Four Line Display mod included wled00\usermods_list.cpp - also tells this usermod that the display is available - (see the Four Line Display usermod `readme.md` for more details) * `ENCODER_DT_PIN` - defaults to 18 * `ENCODER_CLK_PIN` - defaults to 5 * `ENCODER_SW_PIN` - defaults to 19 diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/setup_deps.py b/usermods/usermod_v2_rotary_encoder_ui_ALT/setup_deps.py new file mode 100644 index 000000000..a6b565951 --- /dev/null +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/setup_deps.py @@ -0,0 +1,8 @@ +Import('env') + + +usermods = env.GetProjectOption("custom_usermods","").split() +# Check for partner usermod +# Allow both "usermod_v2" and unqualified syntax +if any(mod in ("four_line_display_ALT", "usermod_v2_four_line_display_ALT") for mod in usermods): + env.Append(CPPDEFINES=[("USERMOD_FOUR_LINE_DISPLAY")]) diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp index a0cbc532f..891b58321 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp @@ -27,6 +27,10 @@ // * display network (long press buttion) // +#ifdef USERMOD_FOUR_LINE_DISPLAY +#include "usermod_v2_four_line_display.h" +#endif + #ifdef USERMOD_MODE_SORT #error "Usermod Mode Sort is no longer required. Remove -D USERMOD_MODE_SORT from platformio.ini" #endif From ff99c7de7067c10dcdb05cb4a1c1dd19e70b2902 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 28 Mar 2025 20:15:36 -0400 Subject: [PATCH 032/153] Convert usermods/usermod_v2_brightness_follow_sun --- usermods/usermod_v2_brightness_follow_sun/library.json | 3 +++ ...ess_follow_sun.h => usermod_v2_brightness_follow_sun.cpp} | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 usermods/usermod_v2_brightness_follow_sun/library.json rename usermods/usermod_v2_brightness_follow_sun/{usermod_v2_brightness_follow_sun.h => usermod_v2_brightness_follow_sun.cpp} (97%) diff --git a/usermods/usermod_v2_brightness_follow_sun/library.json b/usermods/usermod_v2_brightness_follow_sun/library.json new file mode 100644 index 000000000..53eb3fb59 --- /dev/null +++ b/usermods/usermod_v2_brightness_follow_sun/library.json @@ -0,0 +1,3 @@ +{ + "name": "brightness_follow_sum" +} \ No newline at end of file diff --git a/usermods/usermod_v2_brightness_follow_sun/usermod_v2_brightness_follow_sun.h b/usermods/usermod_v2_brightness_follow_sun/usermod_v2_brightness_follow_sun.cpp similarity index 97% rename from usermods/usermod_v2_brightness_follow_sun/usermod_v2_brightness_follow_sun.h rename to usermods/usermod_v2_brightness_follow_sun/usermod_v2_brightness_follow_sun.cpp index 99f646b21..ff97cba46 100644 --- a/usermods/usermod_v2_brightness_follow_sun/usermod_v2_brightness_follow_sun.h +++ b/usermods/usermod_v2_brightness_follow_sun/usermod_v2_brightness_follow_sun.cpp @@ -1,5 +1,3 @@ -#pragma once - #include "wled.h" //v2 usermod that allows to change brightness and color using a rotary encoder, @@ -128,3 +126,6 @@ const char UsermodBrightnessFollowSun::_update_interval[] PROGMEM = "Update const char UsermodBrightnessFollowSun::_min_bri[] PROGMEM = "Min Brightness"; const char UsermodBrightnessFollowSun::_max_bri[] PROGMEM = "Max Brightness"; const char UsermodBrightnessFollowSun::_relax_hour[] PROGMEM = "Relax Hour"; + +static UsermodBrightnessFollowSun usermod_brightness_follow_sun; +REGISTER_USERMOD(usermod_brightness_follow_sun); From 1cd3a97c51b0d5a76f8042d3e96e3978169e1d03 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 28 Mar 2025 20:15:36 -0400 Subject: [PATCH 033/153] usermods: Fix 7sd_reloaded cross module binding Fix up the cross module binding for usermods/seven_segment_display_reloaded. This requires splitting off headers for BH1750_v2 and SN_Photoresistor. --- usermods/BH1750_v2/BH1750_v2.cpp | 352 +++++++----------- usermods/BH1750_v2/BH1750_v2.h | 92 +++++ .../SN_Photoresistor/SN_Photoresistor.cpp | 288 ++++++-------- usermods/SN_Photoresistor/SN_Photoresistor.h | 90 +++++ usermods/SN_Photoresistor/usermods_list.cpp | 14 - .../library.json | 5 +- .../setup_deps.py | 9 + .../seven_segment_display_reloaded.cpp | 6 + 8 files changed, 453 insertions(+), 403 deletions(-) create mode 100644 usermods/BH1750_v2/BH1750_v2.h create mode 100644 usermods/SN_Photoresistor/SN_Photoresistor.h delete mode 100644 usermods/SN_Photoresistor/usermods_list.cpp create mode 100644 usermods/seven_segment_display_reloaded/setup_deps.py diff --git a/usermods/BH1750_v2/BH1750_v2.cpp b/usermods/BH1750_v2/BH1750_v2.cpp index f033f39ed..3800e915d 100644 --- a/usermods/BH1750_v2/BH1750_v2.cpp +++ b/usermods/BH1750_v2/BH1750_v2.cpp @@ -2,244 +2,176 @@ #warning **** Included USERMOD_BH1750 **** #include "wled.h" -#include +#include "BH1750_v2.h" #ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." #endif -// the max frequency to check photoresistor, 10 seconds -#ifndef USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL -#define USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL 10000 -#endif - -// the min frequency to check photoresistor, 500 ms -#ifndef USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL -#define USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL 500 -#endif - -// how many seconds after boot to take first measurement, 10 seconds -#ifndef USERMOD_BH1750_FIRST_MEASUREMENT_AT -#define USERMOD_BH1750_FIRST_MEASUREMENT_AT 10000 -#endif - -// only report if difference grater than offset value -#ifndef USERMOD_BH1750_OFFSET_VALUE -#define USERMOD_BH1750_OFFSET_VALUE 1 -#endif - -class Usermod_BH1750 : public Usermod +static bool checkBoundSensor(float newValue, float prevValue, float maxDiff) { -private: - int8_t offset = USERMOD_BH1750_OFFSET_VALUE; + return isnan(prevValue) || newValue <= prevValue - maxDiff || newValue >= prevValue + maxDiff || (newValue == 0.0 && prevValue > 0.0); +} - unsigned long maxReadingInterval = USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL; - unsigned long minReadingInterval = USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL; - unsigned long lastMeasurement = UINT32_MAX - (USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - USERMOD_BH1750_FIRST_MEASUREMENT_AT); - unsigned long lastSend = UINT32_MAX - (USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - USERMOD_BH1750_FIRST_MEASUREMENT_AT); - // flag to indicate we have finished the first readLightLevel call - // allows this library to report to the user how long until the first - // measurement - bool getLuminanceComplete = false; +void Usermod_BH1750::_mqttInitialize() +{ + mqttLuminanceTopic = String(mqttDeviceTopic) + F("/brightness"); - // flag set at startup - bool enabled = true; + if (HomeAssistantDiscovery) _createMqttSensor(F("Brightness"), mqttLuminanceTopic, F("Illuminance"), F(" lx")); +} - // strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _enabled[]; - static const char _maxReadInterval[]; - static const char _minReadInterval[]; - static const char _offset[]; - static const char _HomeAssistantDiscovery[]; - - bool initDone = false; - bool sensorFound = false; - - // Home Assistant and MQTT - String mqttLuminanceTopic; - bool mqttInitialized = false; - bool HomeAssistantDiscovery = true; // Publish Home Assistant Discovery messages - - BH1750 lightMeter; - float lastLux = -1000; - - bool checkBoundSensor(float newValue, float prevValue, float maxDiff) - { - return isnan(prevValue) || newValue <= prevValue - maxDiff || newValue >= prevValue + maxDiff || (newValue == 0.0 && prevValue > 0.0); - } +// Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop. +void Usermod_BH1750::_createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement) +{ + String t = String(F("homeassistant/sensor/")) + mqttClientID + F("/") + name + F("/config"); - // set up Home Assistant discovery entries - void _mqttInitialize() - { - mqttLuminanceTopic = String(mqttDeviceTopic) + F("/brightness"); + 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; - if (HomeAssistantDiscovery) _createMqttSensor(F("Brightness"), mqttLuminanceTopic, F("Illuminance"), F(" lx")); + JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device + device[F("name")] = serverDescription; + device[F("identifiers")] = "wled-sensor-" + String(mqttClientID); + device[F("manufacturer")] = F(WLED_BRAND); + device[F("model")] = F(WLED_PRODUCT_NAME); + device[F("sw_version")] = versionString; + + String temp; + serializeJson(doc, temp); + DEBUG_PRINTLN(t); + DEBUG_PRINTLN(temp); + + mqtt->publish(t.c_str(), 0, true, temp.c_str()); +} + +void Usermod_BH1750::setup() +{ + if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; } + sensorFound = lightMeter.begin(); + initDone = true; +} + +void Usermod_BH1750::loop() +{ + if ((!enabled) || strip.isUpdating()) + return; + + unsigned long now = millis(); + + // check to see if we are due for taking a measurement + // lastMeasurement will not be updated until the conversion + // is complete the the reading is finished + if (now - lastMeasurement < minReadingInterval) + { + return; } - // 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) + bool shouldUpdate = now - lastSend > maxReadingInterval; + + float lux = lightMeter.readLightLevel(); + lastMeasurement = millis(); + getLuminanceComplete = true; + + if (shouldUpdate || checkBoundSensor(lux, lastLux, offset)) { - 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; + lastLux = lux; + lastSend = millis(); - 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()); + if (WLED_MQTT_CONNECTED) + { + if (!mqttInitialized) + { + _mqttInitialize(); + mqttInitialized = true; + } + mqtt->publish(mqttLuminanceTopic.c_str(), 0, true, String(lux).c_str()); + DEBUG_PRINTLN(F("Brightness: ") + String(lux) + F("lx")); + } + else + { + DEBUG_PRINTLN(F("Missing MQTT connection. Not publishing data")); + } } +} -public: - void setup() - { - if (i2c_scl<0 || i2c_sda<0) { enabled = false; return; } - sensorFound = lightMeter.begin(); - initDone = true; - } - void loop() - { - if ((!enabled) || strip.isUpdating()) +void Usermod_BH1750::addToJsonInfo(JsonObject &root) +{ + JsonObject user = root[F("u")]; + if (user.isNull()) + user = root.createNestedObject(F("u")); + + JsonArray lux_json = user.createNestedArray(F("Luminance")); + if (!enabled) { + lux_json.add(F("disabled")); + } else if (!sensorFound) { + // if no sensor + lux_json.add(F("BH1750 ")); + lux_json.add(F("Not Found")); + } else if (!getLuminanceComplete) { + // if we haven't read the sensor yet, let the user know + // that we are still waiting for the first measurement + lux_json.add((USERMOD_BH1750_FIRST_MEASUREMENT_AT - millis()) / 1000); + lux_json.add(F(" sec until read")); return; - - unsigned long now = millis(); - - // check to see if we are due for taking a measurement - // lastMeasurement will not be updated until the conversion - // is complete the the reading is finished - if (now - lastMeasurement < minReadingInterval) - { - return; - } - - bool shouldUpdate = now - lastSend > maxReadingInterval; - - float lux = lightMeter.readLightLevel(); - lastMeasurement = millis(); - getLuminanceComplete = true; - - if (shouldUpdate || checkBoundSensor(lux, lastLux, offset)) - { - lastLux = lux; - lastSend = millis(); -#ifndef WLED_DISABLE_MQTT - if (WLED_MQTT_CONNECTED) - { - if (!mqttInitialized) - { - _mqttInitialize(); - mqttInitialized = true; - } - mqtt->publish(mqttLuminanceTopic.c_str(), 0, true, String(lux).c_str()); - DEBUG_PRINTLN(F("Brightness: ") + String(lux) + F("lx")); - } - else - { - DEBUG_PRINTLN(F("Missing MQTT connection. Not publishing data")); - } -#endif - } + } else { + lux_json.add(lastLux); + lux_json.add(F(" lx")); } +} - inline float getIlluminance() { - return (float)lastLux; - } +// (called from set.cpp) stores persistent properties to cfg.json +void Usermod_BH1750::addToConfig(JsonObject &root) +{ + // we add JSON object. + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_maxReadInterval)] = maxReadingInterval; + top[FPSTR(_minReadInterval)] = minReadingInterval; + top[FPSTR(_HomeAssistantDiscovery)] = HomeAssistantDiscovery; + top[FPSTR(_offset)] = offset; - void addToJsonInfo(JsonObject &root) + DEBUG_PRINTLN(F("BH1750 config saved.")); +} + +// called before setup() to populate properties from values stored in cfg.json +bool Usermod_BH1750::readFromConfig(JsonObject &root) +{ + // we look for JSON object. + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { - JsonObject user = root[F("u")]; - if (user.isNull()) - user = root.createNestedObject(F("u")); - - JsonArray lux_json = user.createNestedArray(F("Luminance")); - if (!enabled) { - lux_json.add(F("disabled")); - } else if (!sensorFound) { - // if no sensor - lux_json.add(F("BH1750 ")); - lux_json.add(F("Not Found")); - } else if (!getLuminanceComplete) { - // if we haven't read the sensor yet, let the user know - // that we are still waiting for the first measurement - lux_json.add((USERMOD_BH1750_FIRST_MEASUREMENT_AT - millis()) / 1000); - lux_json.add(F(" sec until read")); - return; - } else { - lux_json.add(lastLux); - lux_json.add(F(" lx")); - } - } - - // (called from set.cpp) stores persistent properties to cfg.json - void addToConfig(JsonObject &root) - { - // we add JSON object. - JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname - top[FPSTR(_enabled)] = enabled; - top[FPSTR(_maxReadInterval)] = maxReadingInterval; - top[FPSTR(_minReadInterval)] = minReadingInterval; - top[FPSTR(_HomeAssistantDiscovery)] = HomeAssistantDiscovery; - top[FPSTR(_offset)] = offset; - - DEBUG_PRINTLN(F("BH1750 config saved.")); - } - - // called before setup() to populate properties from values stored in cfg.json - bool readFromConfig(JsonObject &root) - { - // we look for JSON object. - JsonObject top = root[FPSTR(_name)]; - if (top.isNull()) - { - DEBUG_PRINT(FPSTR(_name)); - DEBUG_PRINT(F("BH1750")); - DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); - return false; - } - bool configComplete = !top.isNull(); - - configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled, false); - configComplete &= getJsonValue(top[FPSTR(_maxReadInterval)], maxReadingInterval, 10000); //ms - configComplete &= getJsonValue(top[FPSTR(_minReadInterval)], minReadingInterval, 500); //ms - configComplete &= getJsonValue(top[FPSTR(_HomeAssistantDiscovery)], HomeAssistantDiscovery, false); - configComplete &= getJsonValue(top[FPSTR(_offset)], offset, 1); - DEBUG_PRINT(FPSTR(_name)); - if (!initDone) { - DEBUG_PRINTLN(F(" config loaded.")); - } else { - DEBUG_PRINTLN(F(" config (re)loaded.")); - } + DEBUG_PRINT(F("BH1750")); + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + bool configComplete = !top.isNull(); - return configComplete; - + configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled, false); + configComplete &= getJsonValue(top[FPSTR(_maxReadInterval)], maxReadingInterval, 10000); //ms + configComplete &= getJsonValue(top[FPSTR(_minReadInterval)], minReadingInterval, 500); //ms + configComplete &= getJsonValue(top[FPSTR(_HomeAssistantDiscovery)], HomeAssistantDiscovery, false); + configComplete &= getJsonValue(top[FPSTR(_offset)], offset, 1); + + DEBUG_PRINT(FPSTR(_name)); + if (!initDone) { + DEBUG_PRINTLN(F(" config loaded.")); + } else { + DEBUG_PRINTLN(F(" config (re)loaded.")); } - uint16_t getId() - { - return USERMOD_ID_BH1750; - } + return configComplete; + +} -}; // strings to reduce flash memory usage (used more than twice) const char Usermod_BH1750::_name[] PROGMEM = "BH1750"; diff --git a/usermods/BH1750_v2/BH1750_v2.h b/usermods/BH1750_v2/BH1750_v2.h new file mode 100644 index 000000000..22f51ce9b --- /dev/null +++ b/usermods/BH1750_v2/BH1750_v2.h @@ -0,0 +1,92 @@ + +#pragma once +#include "wled.h" +#include + +#ifdef WLED_DISABLE_MQTT +#error "This user mod requires MQTT to be enabled." +#endif + +// the max frequency to check photoresistor, 10 seconds +#ifndef USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL +#define USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL 10000 +#endif + +// the min frequency to check photoresistor, 500 ms +#ifndef USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL +#define USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL 500 +#endif + +// how many seconds after boot to take first measurement, 10 seconds +#ifndef USERMOD_BH1750_FIRST_MEASUREMENT_AT +#define USERMOD_BH1750_FIRST_MEASUREMENT_AT 10000 +#endif + +// only report if difference grater than offset value +#ifndef USERMOD_BH1750_OFFSET_VALUE +#define USERMOD_BH1750_OFFSET_VALUE 1 +#endif + +class Usermod_BH1750 : public Usermod +{ +private: + int8_t offset = USERMOD_BH1750_OFFSET_VALUE; + + unsigned long maxReadingInterval = USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL; + unsigned long minReadingInterval = USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL; + unsigned long lastMeasurement = UINT32_MAX - (USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - USERMOD_BH1750_FIRST_MEASUREMENT_AT); + unsigned long lastSend = UINT32_MAX - (USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL - USERMOD_BH1750_FIRST_MEASUREMENT_AT); + // flag to indicate we have finished the first readLightLevel call + // allows this library to report to the user how long until the first + // measurement + bool getLuminanceComplete = false; + + // flag set at startup + bool enabled = true; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _maxReadInterval[]; + static const char _minReadInterval[]; + static const char _offset[]; + static const char _HomeAssistantDiscovery[]; + + bool initDone = false; + bool sensorFound = false; + + // Home Assistant and MQTT + String mqttLuminanceTopic; + bool mqttInitialized = false; + bool HomeAssistantDiscovery = true; // Publish Home Assistant Discovery messages + + BH1750 lightMeter; + float lastLux = -1000; + + // set up Home Assistant discovery entries + void _mqttInitialize(); + + // Create an MQTT Sensor for Home Assistant Discovery purposes, this includes a pointer to the topic that is published to in the Loop. + void _createMqttSensor(const String &name, const String &topic, const String &deviceClass, const String &unitOfMeasurement); + +public: + void setup(); + void loop(); + inline float getIlluminance() { + return (float)lastLux; + } + + void addToJsonInfo(JsonObject &root); + + // (called from set.cpp) stores persistent properties to cfg.json + void addToConfig(JsonObject &root); + + // called before setup() to populate properties from values stored in cfg.json + bool readFromConfig(JsonObject &root); + + inline uint16_t getId() + { + return USERMOD_ID_BH1750; + } + +}; diff --git a/usermods/SN_Photoresistor/SN_Photoresistor.cpp b/usermods/SN_Photoresistor/SN_Photoresistor.cpp index 97f865a97..ffd78c0f6 100644 --- a/usermods/SN_Photoresistor/SN_Photoresistor.cpp +++ b/usermods/SN_Photoresistor/SN_Photoresistor.cpp @@ -1,204 +1,137 @@ #include "wled.h" +#include "SN_Photoresistor.h" //Pin defaults for QuinLed Dig-Uno (A0) #ifndef PHOTORESISTOR_PIN #define PHOTORESISTOR_PIN A0 #endif -// the frequency to check photoresistor, 10 seconds -#ifndef USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL -#define USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL 10000 -#endif - -// how many seconds after boot to take first measurement, 10 seconds -#ifndef USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT -#define USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT 10000 -#endif - -// supplied voltage -#ifndef USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE -#define USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE 5 -#endif - -// 10 bits -#ifndef USERMOD_SN_PHOTORESISTOR_ADC_PRECISION -#define USERMOD_SN_PHOTORESISTOR_ADC_PRECISION 1024.0f -#endif - -// resistor size 10K hms -#ifndef USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE -#define USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE 10000.0f -#endif - -// only report if difference grater than offset value -#ifndef USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE -#define USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE 5 -#endif - -class Usermod_SN_Photoresistor : public Usermod +static bool checkBoundSensor(float newValue, float prevValue, float maxDiff) { -private: - float referenceVoltage = USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE; - float resistorValue = USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE; - float adcPrecision = USERMOD_SN_PHOTORESISTOR_ADC_PRECISION; - int8_t offset = USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE; + return isnan(prevValue) || newValue <= prevValue - maxDiff || newValue >= prevValue + maxDiff; +} - unsigned long readingInterval = USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL; - // set last reading as "40 sec before boot", so first reading is taken after 20 sec - unsigned long lastMeasurement = UINT32_MAX - (USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL - USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT); - // flag to indicate we have finished the first getTemperature call - // allows this library to report to the user how long until the first - // measurement - bool getLuminanceComplete = false; - uint16_t lastLDRValue = -1000; +uint16_t Usermod_SN_Photoresistor::getLuminance() +{ + // http://forum.arduino.cc/index.php?topic=37555.0 + // https://forum.arduino.cc/index.php?topic=185158.0 + float volts = analogRead(PHOTORESISTOR_PIN) * (referenceVoltage / adcPrecision); + float amps = volts / resistorValue; + float lux = amps * 1000000 * 2.0; - // flag set at startup - bool disabled = false; + lastMeasurement = millis(); + getLuminanceComplete = true; + return uint16_t(lux); +} - // strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _enabled[]; - static const char _readInterval[]; - static const char _referenceVoltage[]; - static const char _resistorValue[]; - static const char _adcPrecision[]; - static const char _offset[]; +void Usermod_SN_Photoresistor::setup() +{ + // set pinmode + pinMode(PHOTORESISTOR_PIN, INPUT); +} - bool checkBoundSensor(float newValue, float prevValue, float maxDiff) +void Usermod_SN_Photoresistor::loop() +{ + if (disabled || strip.isUpdating()) + return; + + unsigned long now = millis(); + + // check to see if we are due for taking a measurement + // lastMeasurement will not be updated until the conversion + // is complete the the reading is finished + if (now - lastMeasurement < readingInterval) { - return isnan(prevValue) || newValue <= prevValue - maxDiff || newValue >= prevValue + maxDiff; + return; } - uint16_t getLuminance() + uint16_t currentLDRValue = getLuminance(); + if (checkBoundSensor(currentLDRValue, lastLDRValue, offset)) { - // http://forum.arduino.cc/index.php?topic=37555.0 - // https://forum.arduino.cc/index.php?topic=185158.0 - float volts = analogRead(PHOTORESISTOR_PIN) * (referenceVoltage / adcPrecision); - float amps = volts / resistorValue; - float lux = amps * 1000000 * 2.0; - - lastMeasurement = millis(); - getLuminanceComplete = true; - return uint16_t(lux); - } - -public: - void setup() - { - // set pinmode - pinMode(PHOTORESISTOR_PIN, INPUT); - } - - void loop() - { - if (disabled || strip.isUpdating()) - return; - - unsigned long now = millis(); - - // check to see if we are due for taking a measurement - // lastMeasurement will not be updated until the conversion - // is complete the the reading is finished - if (now - lastMeasurement < readingInterval) - { - return; - } - - uint16_t currentLDRValue = getLuminance(); - if (checkBoundSensor(currentLDRValue, lastLDRValue, offset)) - { - lastLDRValue = currentLDRValue; + lastLDRValue = currentLDRValue; #ifndef WLED_DISABLE_MQTT - if (WLED_MQTT_CONNECTED) - { - char subuf[45]; - strcpy(subuf, mqttDeviceTopic); - strcat_P(subuf, PSTR("/luminance")); - mqtt->publish(subuf, 0, true, String(lastLDRValue).c_str()); - } - else - { - DEBUG_PRINTLN(F("Missing MQTT connection. Not publishing data")); - } - } -#endif - } - - uint16_t getLastLDRValue() - { - return lastLDRValue; - } - - void addToJsonInfo(JsonObject &root) - { - JsonObject user = root[F("u")]; - if (user.isNull()) - user = root.createNestedObject(F("u")); - - JsonArray lux = user.createNestedArray(F("Luminance")); - - if (!getLuminanceComplete) + if (WLED_MQTT_CONNECTED) { - // if we haven't read the sensor yet, let the user know - // that we are still waiting for the first measurement - lux.add((USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT - millis()) / 1000); - lux.add(F(" sec until read")); - return; + char subuf[45]; + strcpy(subuf, mqttDeviceTopic); + strcat_P(subuf, PSTR("/luminance")); + mqtt->publish(subuf, 0, true, String(lastLDRValue).c_str()); } - - lux.add(lastLDRValue); - lux.add(F(" lux")); - } - - uint16_t getId() - { - return USERMOD_ID_SN_PHOTORESISTOR; - } - - /** - * addToConfig() (called from set.cpp) stores persistent properties to cfg.json - */ - void addToConfig(JsonObject &root) - { - // we add JSON object. - JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname - top[FPSTR(_enabled)] = !disabled; - top[FPSTR(_readInterval)] = readingInterval / 1000; - top[FPSTR(_referenceVoltage)] = referenceVoltage; - top[FPSTR(_resistorValue)] = resistorValue; - top[FPSTR(_adcPrecision)] = adcPrecision; - top[FPSTR(_offset)] = offset; - - DEBUG_PRINTLN(F("Photoresistor config saved.")); - } - - /** - * readFromConfig() is called before setup() to populate properties from values stored in cfg.json - */ - bool readFromConfig(JsonObject &root) - { - // we look for JSON object. - JsonObject top = root[FPSTR(_name)]; - if (top.isNull()) { - DEBUG_PRINT(FPSTR(_name)); - DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); - return false; + else + { + DEBUG_PRINTLN(F("Missing MQTT connection. Not publishing data")); } + } +#endif +} - disabled = !(top[FPSTR(_enabled)] | !disabled); - readingInterval = (top[FPSTR(_readInterval)] | readingInterval/1000) * 1000; // convert to ms - referenceVoltage = top[FPSTR(_referenceVoltage)] | referenceVoltage; - resistorValue = top[FPSTR(_resistorValue)] | resistorValue; - adcPrecision = top[FPSTR(_adcPrecision)] | adcPrecision; - offset = top[FPSTR(_offset)] | offset; + +void Usermod_SN_Photoresistor::addToJsonInfo(JsonObject &root) +{ + JsonObject user = root[F("u")]; + if (user.isNull()) + user = root.createNestedObject(F("u")); + + JsonArray lux = user.createNestedArray(F("Luminance")); + + if (!getLuminanceComplete) + { + // if we haven't read the sensor yet, let the user know + // that we are still waiting for the first measurement + lux.add((USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT - millis()) / 1000); + lux.add(F(" sec until read")); + return; + } + + lux.add(lastLDRValue); + lux.add(F(" lux")); +} + + +/** + * addToConfig() (called from set.cpp) stores persistent properties to cfg.json + */ +void Usermod_SN_Photoresistor::addToConfig(JsonObject &root) +{ + // we add JSON object. + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + top[FPSTR(_enabled)] = !disabled; + top[FPSTR(_readInterval)] = readingInterval / 1000; + top[FPSTR(_referenceVoltage)] = referenceVoltage; + top[FPSTR(_resistorValue)] = resistorValue; + top[FPSTR(_adcPrecision)] = adcPrecision; + top[FPSTR(_offset)] = offset; + + DEBUG_PRINTLN(F("Photoresistor config saved.")); +} + +/** +* readFromConfig() is called before setup() to populate properties from values stored in cfg.json +*/ +bool Usermod_SN_Photoresistor::readFromConfig(JsonObject &root) +{ + // we look for JSON object. + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { DEBUG_PRINT(FPSTR(_name)); - DEBUG_PRINTLN(F(" config (re)loaded.")); - - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return true; + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; } -}; + + disabled = !(top[FPSTR(_enabled)] | !disabled); + readingInterval = (top[FPSTR(_readInterval)] | readingInterval/1000) * 1000; // convert to ms + referenceVoltage = top[FPSTR(_referenceVoltage)] | referenceVoltage; + resistorValue = top[FPSTR(_resistorValue)] | resistorValue; + adcPrecision = top[FPSTR(_adcPrecision)] | adcPrecision; + offset = top[FPSTR(_offset)] | offset; + DEBUG_PRINT(FPSTR(_name)); + DEBUG_PRINTLN(F(" config (re)loaded.")); + + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return true; +} + // strings to reduce flash memory usage (used more than twice) const char Usermod_SN_Photoresistor::_name[] PROGMEM = "Photoresistor"; @@ -209,6 +142,5 @@ const char Usermod_SN_Photoresistor::_resistorValue[] PROGMEM = "resistor-value" const char Usermod_SN_Photoresistor::_adcPrecision[] PROGMEM = "adc-precision"; const char Usermod_SN_Photoresistor::_offset[] PROGMEM = "offset"; - static Usermod_SN_Photoresistor sn_photoresistor; REGISTER_USERMOD(sn_photoresistor); \ No newline at end of file diff --git a/usermods/SN_Photoresistor/SN_Photoresistor.h b/usermods/SN_Photoresistor/SN_Photoresistor.h new file mode 100644 index 000000000..3c3dc68e6 --- /dev/null +++ b/usermods/SN_Photoresistor/SN_Photoresistor.h @@ -0,0 +1,90 @@ +#pragma once +#include "wled.h" + +// the frequency to check photoresistor, 10 seconds +#ifndef USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL +#define USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL 10000 +#endif + +// how many seconds after boot to take first measurement, 10 seconds +#ifndef USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT +#define USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT 10000 +#endif + +// supplied voltage +#ifndef USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE +#define USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE 5 +#endif + +// 10 bits +#ifndef USERMOD_SN_PHOTORESISTOR_ADC_PRECISION +#define USERMOD_SN_PHOTORESISTOR_ADC_PRECISION 1024.0f +#endif + +// resistor size 10K hms +#ifndef USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE +#define USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE 10000.0f +#endif + +// only report if difference grater than offset value +#ifndef USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE +#define USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE 5 +#endif + +class Usermod_SN_Photoresistor : public Usermod +{ +private: + float referenceVoltage = USERMOD_SN_PHOTORESISTOR_REFERENCE_VOLTAGE; + float resistorValue = USERMOD_SN_PHOTORESISTOR_RESISTOR_VALUE; + float adcPrecision = USERMOD_SN_PHOTORESISTOR_ADC_PRECISION; + int8_t offset = USERMOD_SN_PHOTORESISTOR_OFFSET_VALUE; + + unsigned long readingInterval = USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL; + // set last reading as "40 sec before boot", so first reading is taken after 20 sec + unsigned long lastMeasurement = UINT32_MAX - (USERMOD_SN_PHOTORESISTOR_MEASUREMENT_INTERVAL - USERMOD_SN_PHOTORESISTOR_FIRST_MEASUREMENT_AT); + // flag to indicate we have finished the first getTemperature call + // allows this library to report to the user how long until the first + // measurement + bool getLuminanceComplete = false; + uint16_t lastLDRValue = -1000; + + // flag set at startup + bool disabled = false; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _readInterval[]; + static const char _referenceVoltage[]; + static const char _resistorValue[]; + static const char _adcPrecision[]; + static const char _offset[]; + + uint16_t getLuminance(); + +public: + void setup(); + void loop(); + + uint16_t getLastLDRValue() + { + return lastLDRValue; + } + + void addToJsonInfo(JsonObject &root); + + uint16_t getId() + { + return USERMOD_ID_SN_PHOTORESISTOR; + } + + /** + * addToConfig() (called from set.cpp) stores persistent properties to cfg.json + */ + void addToConfig(JsonObject &root); + + /** + * readFromConfig() is called before setup() to populate properties from values stored in cfg.json + */ + bool readFromConfig(JsonObject &root); +}; diff --git a/usermods/SN_Photoresistor/usermods_list.cpp b/usermods/SN_Photoresistor/usermods_list.cpp deleted file mode 100644 index a2c6ca165..000000000 --- a/usermods/SN_Photoresistor/usermods_list.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "wled.h" -/* - * Register your v2 usermods here! - */ -#ifdef USERMOD_SN_PHOTORESISTOR -#include "../usermods/SN_Photoresistor/usermod_sn_photoresistor.h" -#endif - -void registerUsermods() -{ -#ifdef USERMOD_SN_PHOTORESISTOR - UsermodManager::add(new Usermod_SN_Photoresistor()); -#endif -} \ No newline at end of file diff --git a/usermods/seven_segment_display_reloaded/library.json b/usermods/seven_segment_display_reloaded/library.json index fdce8b536..4e84e38ed 100644 --- a/usermods/seven_segment_display_reloaded/library.json +++ b/usermods/seven_segment_display_reloaded/library.json @@ -1,3 +1,6 @@ { - "name:": "seven_segment_display_reloaded" + "name": "seven_segment_display_reloaded", + "build": { + "extraScript": "setup_deps.py" + } } \ No newline at end of file diff --git a/usermods/seven_segment_display_reloaded/setup_deps.py b/usermods/seven_segment_display_reloaded/setup_deps.py new file mode 100644 index 000000000..0c663d824 --- /dev/null +++ b/usermods/seven_segment_display_reloaded/setup_deps.py @@ -0,0 +1,9 @@ +Import('env') + + +usermods = env.GetProjectOption("custom_usermods","").split() +# Check for partner usermods +if "SN_Photoresistor" in usermods: + env.Append(CPPDEFINES=[("USERMOD_SN_PHOTORESISTOR")]) +if "BH1750_v2" in usermods: + env.Append(CPPDEFINES=[("USERMOD_BH1750")]) diff --git a/usermods/seven_segment_display_reloaded/seven_segment_display_reloaded.cpp b/usermods/seven_segment_display_reloaded/seven_segment_display_reloaded.cpp index 971a80c8d..893e061bc 100644 --- a/usermods/seven_segment_display_reloaded/seven_segment_display_reloaded.cpp +++ b/usermods/seven_segment_display_reloaded/seven_segment_display_reloaded.cpp @@ -1,4 +1,10 @@ #include "wled.h" +#ifdef USERMOD_SN_PHOTORESISTOR + #include "SN_Photoresistor.h" +#endif +#ifdef USERMOD_BH1750 + #include "BH1750_v2.h" +#endif #ifdef WLED_DISABLE_MQTT #error "This user mod requires MQTT to be enabled." From fab80f4e4eec3a0112eee2c1c282477765482412 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 28 Mar 2025 20:15:36 -0400 Subject: [PATCH 034/153] Fix up usermod dep checking scripts --- usermods/PWM_fan/setup_deps.py | 3 +-- usermods/seven_segment_display_reloaded/setup_deps.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/usermods/PWM_fan/setup_deps.py b/usermods/PWM_fan/setup_deps.py index 2f76ba857..b8f7276c5 100644 --- a/usermods/PWM_fan/setup_deps.py +++ b/usermods/PWM_fan/setup_deps.py @@ -7,6 +7,5 @@ if "Temperature" in usermods: env.Append(CPPDEFINES=[("USERMOD_DALLASTEMPERATURE")]) elif "sht" in usermods: env.Append(CPPDEFINES=[("USERMOD_SHT")]) -else: +elif "PWM_fan" in usermods: # The script can be run if this module was previously selected raise RuntimeError("PWM_fan usermod requires Temperature or sht to be enabled") - diff --git a/usermods/seven_segment_display_reloaded/setup_deps.py b/usermods/seven_segment_display_reloaded/setup_deps.py index 0c663d824..dd28f5fe9 100644 --- a/usermods/seven_segment_display_reloaded/setup_deps.py +++ b/usermods/seven_segment_display_reloaded/setup_deps.py @@ -5,5 +5,5 @@ usermods = env.GetProjectOption("custom_usermods","").split() # Check for partner usermods if "SN_Photoresistor" in usermods: env.Append(CPPDEFINES=[("USERMOD_SN_PHOTORESISTOR")]) -if "BH1750_v2" in usermods: +if any(mod in ("BH1750_v2", "BH1750") for mod in usermods): env.Append(CPPDEFINES=[("USERMOD_BH1750")]) From 62d3e155bde1077bf5784eb481cdc6e0059f993a Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 28 Mar 2025 20:15:36 -0400 Subject: [PATCH 035/153] usermod/SN_Photoresistor: Fix invalid initializer --- usermods/SN_Photoresistor/SN_Photoresistor.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/SN_Photoresistor/SN_Photoresistor.h b/usermods/SN_Photoresistor/SN_Photoresistor.h index 3c3dc68e6..87836c0e4 100644 --- a/usermods/SN_Photoresistor/SN_Photoresistor.h +++ b/usermods/SN_Photoresistor/SN_Photoresistor.h @@ -46,7 +46,7 @@ private: // allows this library to report to the user how long until the first // measurement bool getLuminanceComplete = false; - uint16_t lastLDRValue = -1000; + uint16_t lastLDRValue = 65535; // flag set at startup bool disabled = false; From c3ab562258fc58b39a422134530ed48abdde9c1c Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 28 Mar 2025 21:17:59 -0400 Subject: [PATCH 036/153] Update usermods/usermod_v2_brightness_follow_sun/library.json Fix incorrect module name in library.json Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- usermods/usermod_v2_brightness_follow_sun/library.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/usermod_v2_brightness_follow_sun/library.json b/usermods/usermod_v2_brightness_follow_sun/library.json index 53eb3fb59..6120d873e 100644 --- a/usermods/usermod_v2_brightness_follow_sun/library.json +++ b/usermods/usermod_v2_brightness_follow_sun/library.json @@ -1,3 +1,3 @@ { - "name": "brightness_follow_sum" + "name": "brightness_follow_sun" } \ No newline at end of file From f719ee5b18959802028efbac1938658dccecac87 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 28 Mar 2025 21:18:27 -0400 Subject: [PATCH 037/153] Update usermods/usermod_v2_four_line_display_ALT/library.json Fix incorrect json Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- usermods/usermod_v2_four_line_display_ALT/library.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/usermod_v2_four_line_display_ALT/library.json b/usermods/usermod_v2_four_line_display_ALT/library.json index 29cab450f..87a690f03 100644 --- a/usermods/usermod_v2_four_line_display_ALT/library.json +++ b/usermods/usermod_v2_four_line_display_ALT/library.json @@ -1,5 +1,5 @@ { - "name:": "four_line_display_ALT", + "name": "four_line_display_ALT", "dependencies": { "U8g2": "~2.34.4", "Wire": "" From fbbb369fa4e61dd99e8f97de4a29595683ff8534 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 28 Mar 2025 21:21:26 -0400 Subject: [PATCH 038/153] Update usermods/usermod_v2_rotary_encoder_ui_ALT/library.json Fix typo in json Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- usermods/usermod_v2_rotary_encoder_ui_ALT/library.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/library.json b/usermods/usermod_v2_rotary_encoder_ui_ALT/library.json index ef28d01cf..ddb6334b1 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/library.json +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/library.json @@ -1,5 +1,5 @@ { - "name:": "rotary_encoder_ui_ALT", + "name": "rotary_encoder_ui_ALT", "build": { "extraScript": "setup_deps.py" } From 7db52d794b71239072c8eb784b845045892bdb13 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 28 Mar 2025 21:35:37 -0400 Subject: [PATCH 039/153] Fix incorrect json value in all usermods Must've been a bad search-and-replace on my part somewhere.. --- usermods/ADS1115_v2/library.json | 2 +- usermods/AHT10_v2/library.json | 2 +- usermods/Analog_Clock/library.json | 2 +- usermods/Animated_Staircase/library.json | 2 +- usermods/BH1750_v2/library.json | 2 +- usermods/BME280_v2/library.json | 2 +- usermods/BME68X_v2/library.json | 2 +- usermods/Battery/library.json | 2 +- usermods/Cronixie/library.json | 2 +- usermods/DHT/library.json | 2 +- usermods/EXAMPLE/library.json | 2 +- usermods/Fix_unreachable_netservices_v2/library.json | 2 +- usermods/INA226_v2/library.json | 2 +- usermods/Internal_Temperature_v2/library.json | 2 +- usermods/LD2410_v2/library.json | 2 +- usermods/LDR_Dusk_Dawn_v2/library.json | 2 +- usermods/MAX17048_v2/library.json | 2 +- usermods/MY9291/library.json | 2 +- usermods/PIR_sensor_switch/library.json | 2 +- usermods/PWM_fan/library.json | 2 +- usermods/RTC/library.json | 2 +- usermods/SN_Photoresistor/library.json | 2 +- usermods/Si7021_MQTT_HA/library.json | 2 +- usermods/Temperature/library.json | 2 +- usermods/TetrisAI_v2/library.json | 2 +- usermods/VL53L0X_gestures/library.json | 2 +- usermods/boblight/library.json | 2 +- usermods/buzzer/library.json | 2 +- usermods/deep_sleep/library.json | 2 +- usermods/mpu6050_imu/library.json | 2 +- usermods/multi_relay/library.json | 2 +- usermods/pixels_dice_tray/library.json | 2 +- usermods/pwm_outputs/library.json | 2 +- usermods/quinled-an-penta/library.json | 2 +- usermods/rgb-rotary-encoder/library.json | 2 +- usermods/sd_card/library.json | 2 +- usermods/sensors_to_mqtt/library.json | 2 +- usermods/seven_segment_display/library.json | 2 +- usermods/sht/library.json | 2 +- usermods/smartnest/library.json | 2 +- usermods/stairway_wipe_basic/library.json | 2 +- usermods/usermod_rotary_brightness_color/library.json | 2 +- usermods/usermod_v2_HttpPullLightControl/library.json | 2 +- usermods/usermod_v2_RF433/library.json | 2 +- usermods/usermod_v2_klipper_percentage/library.json | 2 +- usermods/usermod_v2_ping_pong_clock/library.json | 2 +- usermods/usermod_v2_word_clock/library.json | 2 +- usermods/wireguard/library.json | 2 +- usermods/wizlights/library.json | 2 +- usermods/word-clock-matrix/library.json | 2 +- 50 files changed, 50 insertions(+), 50 deletions(-) diff --git a/usermods/ADS1115_v2/library.json b/usermods/ADS1115_v2/library.json index 0b93c9351..9f0c021ce 100644 --- a/usermods/ADS1115_v2/library.json +++ b/usermods/ADS1115_v2/library.json @@ -1,5 +1,5 @@ { - "name:": "ADS1115_v2", + "name": "ADS1115_v2", "dependencies": { "Adafruit BusIO": "https://github.com/adafruit/Adafruit_BusIO#1.13.2", "Adafruit ADS1X15": "https://github.com/adafruit/Adafruit_ADS1X15#2.4.0" diff --git a/usermods/AHT10_v2/library.json b/usermods/AHT10_v2/library.json index 94a206c57..54f8c1715 100644 --- a/usermods/AHT10_v2/library.json +++ b/usermods/AHT10_v2/library.json @@ -1,5 +1,5 @@ { - "name:": "AHT10_v2", + "name": "AHT10_v2", "dependencies": { "enjoyneering/AHT10":"~1.1.0" } diff --git a/usermods/Analog_Clock/library.json b/usermods/Analog_Clock/library.json index 4936950e9..3ed596dc7 100644 --- a/usermods/Analog_Clock/library.json +++ b/usermods/Analog_Clock/library.json @@ -1,3 +1,3 @@ { - "name:": "Analog_Clock" + "name": "Analog_Clock" } \ No newline at end of file diff --git a/usermods/Animated_Staircase/library.json b/usermods/Animated_Staircase/library.json index 626baa494..a2c50ea4c 100644 --- a/usermods/Animated_Staircase/library.json +++ b/usermods/Animated_Staircase/library.json @@ -1,3 +1,3 @@ { - "name:": "Animated_Staircase" + "name": "Animated_Staircase" } \ No newline at end of file diff --git a/usermods/BH1750_v2/library.json b/usermods/BH1750_v2/library.json index b7f006cc2..13740e6c9 100644 --- a/usermods/BH1750_v2/library.json +++ b/usermods/BH1750_v2/library.json @@ -1,5 +1,5 @@ { - "name:": "BH1750_v2", + "name": "BH1750_v2", "dependencies": { "claws/BH1750":"^1.2.0" } diff --git a/usermods/BME280_v2/library.json b/usermods/BME280_v2/library.json index 7ae712583..626fb8b2b 100644 --- a/usermods/BME280_v2/library.json +++ b/usermods/BME280_v2/library.json @@ -1,5 +1,5 @@ { - "name:": "BME280_v2", + "name": "BME280_v2", "dependencies": { "finitespace/BME280":"~3.0.0" } diff --git a/usermods/BME68X_v2/library.json b/usermods/BME68X_v2/library.json index 6bd0bb9b2..2c6e7185d 100644 --- a/usermods/BME68X_v2/library.json +++ b/usermods/BME68X_v2/library.json @@ -1,5 +1,5 @@ { - "name:": "BME68X_v2", + "name": "BME68X_v2", "build": { "libArchive": false}, "dependencies": { "boschsensortec/BSEC Software Library":"^1.8.1492" diff --git a/usermods/Battery/library.json b/usermods/Battery/library.json index 3f4774b87..d6b8ad38a 100644 --- a/usermods/Battery/library.json +++ b/usermods/Battery/library.json @@ -1,3 +1,3 @@ { - "name:": "Battery" + "name": "Battery" } \ No newline at end of file diff --git a/usermods/Cronixie/library.json b/usermods/Cronixie/library.json index d48327649..a1454a79a 100644 --- a/usermods/Cronixie/library.json +++ b/usermods/Cronixie/library.json @@ -1,3 +1,3 @@ { - "name:": "Cronixie" + "name": "Cronixie" } \ No newline at end of file diff --git a/usermods/DHT/library.json b/usermods/DHT/library.json index d61634e9f..7b0dc3618 100644 --- a/usermods/DHT/library.json +++ b/usermods/DHT/library.json @@ -1,5 +1,5 @@ { - "name:": "DHT", + "name": "DHT", "build": { "libArchive": false}, "dependencies": { "DHT_nonblocking":"https://github.com/alwynallan/DHT_nonblocking" diff --git a/usermods/EXAMPLE/library.json b/usermods/EXAMPLE/library.json index 276aba493..dd8a7e5dd 100644 --- a/usermods/EXAMPLE/library.json +++ b/usermods/EXAMPLE/library.json @@ -1,4 +1,4 @@ { - "name:": "EXAMPLE", + "name": "EXAMPLE", "dependencies": {} } diff --git a/usermods/Fix_unreachable_netservices_v2/library.json b/usermods/Fix_unreachable_netservices_v2/library.json index 68b318184..4d1dbfc8e 100644 --- a/usermods/Fix_unreachable_netservices_v2/library.json +++ b/usermods/Fix_unreachable_netservices_v2/library.json @@ -1,4 +1,4 @@ { - "name:": "Fix_unreachable_netservices_v2", + "name": "Fix_unreachable_netservices_v2", "platforms": ["espressif8266"] } diff --git a/usermods/INA226_v2/library.json b/usermods/INA226_v2/library.json index 91a735fe7..ab6c81fbd 100644 --- a/usermods/INA226_v2/library.json +++ b/usermods/INA226_v2/library.json @@ -1,5 +1,5 @@ { - "name:": "INA226_v2", + "name": "INA226_v2", "dependencies": { "wollewald/INA226_WE":"~1.2.9" } diff --git a/usermods/Internal_Temperature_v2/library.json b/usermods/Internal_Temperature_v2/library.json index 6c1652380..571176f45 100644 --- a/usermods/Internal_Temperature_v2/library.json +++ b/usermods/Internal_Temperature_v2/library.json @@ -1,3 +1,3 @@ { - "name:": "Internal_Temperature_v2" + "name": "Internal_Temperature_v2" } \ No newline at end of file diff --git a/usermods/LD2410_v2/library.json b/usermods/LD2410_v2/library.json index 205bb8220..92ad54da7 100644 --- a/usermods/LD2410_v2/library.json +++ b/usermods/LD2410_v2/library.json @@ -1,5 +1,5 @@ { - "name:": "LD2410_v2", + "name": "LD2410_v2", "dependencies": { "ncmreynolds/ld2410":"^0.1.3" } diff --git a/usermods/LDR_Dusk_Dawn_v2/library.json b/usermods/LDR_Dusk_Dawn_v2/library.json index bb57dbd2a..be06c3a3a 100644 --- a/usermods/LDR_Dusk_Dawn_v2/library.json +++ b/usermods/LDR_Dusk_Dawn_v2/library.json @@ -1,3 +1,3 @@ { - "name:": "LDR_Dusk_Dawn_v2" + "name": "LDR_Dusk_Dawn_v2" } \ No newline at end of file diff --git a/usermods/MAX17048_v2/library.json b/usermods/MAX17048_v2/library.json index 03b9acd9f..a9ae1543f 100644 --- a/usermods/MAX17048_v2/library.json +++ b/usermods/MAX17048_v2/library.json @@ -1,5 +1,5 @@ { - "name:": "MAX17048_v2", + "name": "MAX17048_v2", "build": { "libArchive": false}, "dependencies": { "Adafruit_MAX1704X":"https://github.com/adafruit/Adafruit_MAX1704X#1.0.2" diff --git a/usermods/MY9291/library.json b/usermods/MY9291/library.json index 96e0bbf93..e4c63eaf5 100644 --- a/usermods/MY9291/library.json +++ b/usermods/MY9291/library.json @@ -1,4 +1,4 @@ { - "name:": "MY9291", + "name": "MY9291", "platforms": ["espressif8266"] } \ No newline at end of file diff --git a/usermods/PIR_sensor_switch/library.json b/usermods/PIR_sensor_switch/library.json index 0ee7e18b5..d5ebb7689 100644 --- a/usermods/PIR_sensor_switch/library.json +++ b/usermods/PIR_sensor_switch/library.json @@ -1,3 +1,3 @@ { - "name:": "PIR_sensor_switch" + "name": "PIR_sensor_switch" } \ No newline at end of file diff --git a/usermods/PWM_fan/library.json b/usermods/PWM_fan/library.json index a0e53b21f..a8f7a9446 100644 --- a/usermods/PWM_fan/library.json +++ b/usermods/PWM_fan/library.json @@ -1,5 +1,5 @@ { - "name:": "PWM_fan", + "name": "PWM_fan", "build": { "extraScript": "setup_deps.py" } diff --git a/usermods/RTC/library.json b/usermods/RTC/library.json index e0c527d2c..8c103e06d 100644 --- a/usermods/RTC/library.json +++ b/usermods/RTC/library.json @@ -1,3 +1,3 @@ { - "name:": "RTC" + "name": "RTC" } \ No newline at end of file diff --git a/usermods/SN_Photoresistor/library.json b/usermods/SN_Photoresistor/library.json index 7cac93f8d..45519dfa6 100644 --- a/usermods/SN_Photoresistor/library.json +++ b/usermods/SN_Photoresistor/library.json @@ -1,3 +1,3 @@ { - "name:": "SN_Photoresistor" + "name": "SN_Photoresistor" } \ No newline at end of file diff --git a/usermods/Si7021_MQTT_HA/library.json b/usermods/Si7021_MQTT_HA/library.json index 5d7aa300a..cec2edfb1 100644 --- a/usermods/Si7021_MQTT_HA/library.json +++ b/usermods/Si7021_MQTT_HA/library.json @@ -1,5 +1,5 @@ { - "name:": "Si7021_MQTT_HA", + "name": "Si7021_MQTT_HA", "dependencies": { "finitespace/BME280":"3.0.0", "adafruit/Adafruit Si7021 Library" : "1.5.3" diff --git a/usermods/Temperature/library.json b/usermods/Temperature/library.json index 0d9f55ccd..5439bc13e 100644 --- a/usermods/Temperature/library.json +++ b/usermods/Temperature/library.json @@ -1,5 +1,5 @@ { - "name:": "Temperature", + "name": "Temperature", "build": { "libArchive": false}, "dependencies": { "paulstoffregen/OneWire":"~2.3.8" diff --git a/usermods/TetrisAI_v2/library.json b/usermods/TetrisAI_v2/library.json index 7163dadbf..bfff1aa4d 100644 --- a/usermods/TetrisAI_v2/library.json +++ b/usermods/TetrisAI_v2/library.json @@ -1,3 +1,3 @@ { - "name:": "TetrisAI_v2" + "name": "TetrisAI_v2" } \ No newline at end of file diff --git a/usermods/VL53L0X_gestures/library.json b/usermods/VL53L0X_gestures/library.json index db24abd0b..08f5921c7 100644 --- a/usermods/VL53L0X_gestures/library.json +++ b/usermods/VL53L0X_gestures/library.json @@ -1,5 +1,5 @@ { - "name:": "VL53L0X_gestures", + "name": "VL53L0X_gestures", "build": { "libArchive": false}, "dependencies": { "pololu/VL53L0X" : "^1.3.0" diff --git a/usermods/boblight/library.json b/usermods/boblight/library.json index 741d4cb18..12debccf5 100644 --- a/usermods/boblight/library.json +++ b/usermods/boblight/library.json @@ -1,3 +1,3 @@ { - "name:": "boblight" + "name": "boblight" } \ No newline at end of file diff --git a/usermods/buzzer/library.json b/usermods/buzzer/library.json index 6bbcdcc34..c6af3158b 100644 --- a/usermods/buzzer/library.json +++ b/usermods/buzzer/library.json @@ -1,3 +1,3 @@ { - "name:": "buzzer" + "name": "buzzer" } \ No newline at end of file diff --git a/usermods/deep_sleep/library.json b/usermods/deep_sleep/library.json index c8f66de10..8b39b2eed 100644 --- a/usermods/deep_sleep/library.json +++ b/usermods/deep_sleep/library.json @@ -1,3 +1,3 @@ { - "name:": "deep_sleep" + "name": "deep_sleep" } \ No newline at end of file diff --git a/usermods/mpu6050_imu/library.json b/usermods/mpu6050_imu/library.json index 3c39de450..d2a0f70af 100644 --- a/usermods/mpu6050_imu/library.json +++ b/usermods/mpu6050_imu/library.json @@ -1,5 +1,5 @@ { - "name:": "mpu6050_imu", + "name": "mpu6050_imu", "build": { "libArchive": false}, "dependencies": { "electroniccats/MPU6050":"1.0.1" diff --git a/usermods/multi_relay/library.json b/usermods/multi_relay/library.json index 7aa764399..f1caf7d42 100644 --- a/usermods/multi_relay/library.json +++ b/usermods/multi_relay/library.json @@ -1,3 +1,3 @@ { - "name:": "multi_relay" + "name": "multi_relay" } \ No newline at end of file diff --git a/usermods/pixels_dice_tray/library.json b/usermods/pixels_dice_tray/library.json index 5043c0cfd..ac1a7a078 100644 --- a/usermods/pixels_dice_tray/library.json +++ b/usermods/pixels_dice_tray/library.json @@ -1,5 +1,5 @@ { - "name:": "pixels_dice_tray", + "name": "pixels_dice_tray", "build": { "libArchive": false}, "dependencies": { "arduino-pixels-dice":"https://github.com/axlan/arduino-pixels-dice.git", diff --git a/usermods/pwm_outputs/library.json b/usermods/pwm_outputs/library.json index 4bf07777a..bcdb8d5a6 100644 --- a/usermods/pwm_outputs/library.json +++ b/usermods/pwm_outputs/library.json @@ -1,3 +1,3 @@ { - "name:": "pwm_outputs" + "name": "pwm_outputs" } \ No newline at end of file diff --git a/usermods/quinled-an-penta/library.json b/usermods/quinled-an-penta/library.json index 9a1f4e0e5..fca9b0e3a 100644 --- a/usermods/quinled-an-penta/library.json +++ b/usermods/quinled-an-penta/library.json @@ -1,5 +1,5 @@ { - "name:": "quinled-an-penta", + "name": "quinled-an-penta", "build": { "libArchive": false}, "dependencies": { "olikraus/U8g2":"~2.28.8", diff --git a/usermods/rgb-rotary-encoder/library.json b/usermods/rgb-rotary-encoder/library.json index b1394ba25..fa8a65d18 100644 --- a/usermods/rgb-rotary-encoder/library.json +++ b/usermods/rgb-rotary-encoder/library.json @@ -1,5 +1,5 @@ { - "name:": "rgb-rotary-encoder", + "name": "rgb-rotary-encoder", "build": { "libArchive": false}, "dependencies": { "lennarthennigs/ESP Rotary":"^2.1.1" diff --git a/usermods/sd_card/library.json b/usermods/sd_card/library.json index 1f123ead6..44d3e3495 100644 --- a/usermods/sd_card/library.json +++ b/usermods/sd_card/library.json @@ -1,3 +1,3 @@ { - "name:": "sd_card" + "name": "sd_card" } \ No newline at end of file diff --git a/usermods/sensors_to_mqtt/library.json b/usermods/sensors_to_mqtt/library.json index d38c794e4..977053da7 100644 --- a/usermods/sensors_to_mqtt/library.json +++ b/usermods/sensors_to_mqtt/library.json @@ -1,5 +1,5 @@ { - "name:": "sensors_to_mqtt", + "name": "sensors_to_mqtt", "build": { "libArchive": false}, "dependencies": { "adafruit/Adafruit BMP280 Library":"2.6.8", diff --git a/usermods/seven_segment_display/library.json b/usermods/seven_segment_display/library.json index 8764e92b3..653c3d7ff 100644 --- a/usermods/seven_segment_display/library.json +++ b/usermods/seven_segment_display/library.json @@ -1,3 +1,3 @@ { - "name:": "seven_segment_display" + "name": "seven_segment_display" } \ No newline at end of file diff --git a/usermods/sht/library.json b/usermods/sht/library.json index fc62941a3..6849628ca 100644 --- a/usermods/sht/library.json +++ b/usermods/sht/library.json @@ -1,5 +1,5 @@ { - "name:": "sht", + "name": "sht", "dependencies": { "robtillaart/SHT85": "~0.3.3" } diff --git a/usermods/smartnest/library.json b/usermods/smartnest/library.json index e2c6ab351..9b428f6b1 100644 --- a/usermods/smartnest/library.json +++ b/usermods/smartnest/library.json @@ -1,3 +1,3 @@ { - "name:": "smartnest" + "name": "smartnest" } \ No newline at end of file diff --git a/usermods/stairway_wipe_basic/library.json b/usermods/stairway_wipe_basic/library.json index 59cb5da93..b75baef6b 100644 --- a/usermods/stairway_wipe_basic/library.json +++ b/usermods/stairway_wipe_basic/library.json @@ -1,3 +1,3 @@ { - "name:": "stairway_wipe_basic" + "name": "stairway_wipe_basic" } \ No newline at end of file diff --git a/usermods/usermod_rotary_brightness_color/library.json b/usermods/usermod_rotary_brightness_color/library.json index 777ec19c0..ecf73c0f9 100644 --- a/usermods/usermod_rotary_brightness_color/library.json +++ b/usermods/usermod_rotary_brightness_color/library.json @@ -1,3 +1,3 @@ { - "name:": "usermod_rotary_brightness_color" + "name": "usermod_rotary_brightness_color" } \ No newline at end of file diff --git a/usermods/usermod_v2_HttpPullLightControl/library.json b/usermods/usermod_v2_HttpPullLightControl/library.json index 0f66710b3..a9252fc0c 100644 --- a/usermods/usermod_v2_HttpPullLightControl/library.json +++ b/usermods/usermod_v2_HttpPullLightControl/library.json @@ -1,3 +1,3 @@ { - "name:": "usermod_v2_HttpPullLightControl" + "name": "usermod_v2_HttpPullLightControl" } \ No newline at end of file diff --git a/usermods/usermod_v2_RF433/library.json b/usermods/usermod_v2_RF433/library.json index 9ba2bdcf6..d809d3a0d 100644 --- a/usermods/usermod_v2_RF433/library.json +++ b/usermods/usermod_v2_RF433/library.json @@ -1,5 +1,5 @@ { - "name:": "usermod_v2_RF433", + "name": "usermod_v2_RF433", "dependencies": { "sui77/rc-switch":"2.6.4" } diff --git a/usermods/usermod_v2_klipper_percentage/library.json b/usermods/usermod_v2_klipper_percentage/library.json index b31fb1ad1..7a2df6b23 100644 --- a/usermods/usermod_v2_klipper_percentage/library.json +++ b/usermods/usermod_v2_klipper_percentage/library.json @@ -1,3 +1,3 @@ { - "name:": "usermod_v2_klipper_percentage" + "name": "usermod_v2_klipper_percentage" } \ No newline at end of file diff --git a/usermods/usermod_v2_ping_pong_clock/library.json b/usermods/usermod_v2_ping_pong_clock/library.json index fe23cd910..d6c079e58 100644 --- a/usermods/usermod_v2_ping_pong_clock/library.json +++ b/usermods/usermod_v2_ping_pong_clock/library.json @@ -1,3 +1,3 @@ { - "name:": "usermod_v2_ping_pong_clock" + "name": "usermod_v2_ping_pong_clock" } \ No newline at end of file diff --git a/usermods/usermod_v2_word_clock/library.json b/usermods/usermod_v2_word_clock/library.json index 83c14dc7e..b0dcebc6e 100644 --- a/usermods/usermod_v2_word_clock/library.json +++ b/usermods/usermod_v2_word_clock/library.json @@ -1,3 +1,3 @@ { - "name:": "usermod_v2_word_clock" + "name": "usermod_v2_word_clock" } \ No newline at end of file diff --git a/usermods/wireguard/library.json b/usermods/wireguard/library.json index 7c7b17ef8..c1a383c72 100644 --- a/usermods/wireguard/library.json +++ b/usermods/wireguard/library.json @@ -1,5 +1,5 @@ { - "name:": "wireguard", + "name": "wireguard", "build": { "libArchive": false}, "dependencies": { "WireGuard-ESP32-Arduino":"https://github.com/kienvu58/WireGuard-ESP32-Arduino.git" diff --git a/usermods/wizlights/library.json b/usermods/wizlights/library.json index 687fba0f7..114424e5d 100644 --- a/usermods/wizlights/library.json +++ b/usermods/wizlights/library.json @@ -1,3 +1,3 @@ { - "name:": "wizlights" + "name": "wizlights" } \ No newline at end of file diff --git a/usermods/word-clock-matrix/library.json b/usermods/word-clock-matrix/library.json index d971dfff4..afeae5025 100644 --- a/usermods/word-clock-matrix/library.json +++ b/usermods/word-clock-matrix/library.json @@ -1,3 +1,3 @@ { - "name:": "word-clock-matrix" + "name": "word-clock-matrix" } \ No newline at end of file From ff3680813ca34d8e088d19b7a98b31c0f7be0744 Mon Sep 17 00:00:00 2001 From: Michael Bisbjerg Date: Sat, 29 Mar 2025 14:08:23 +0100 Subject: [PATCH 040/153] Create wled-tools.sh Per discussion, add a discover-fueled tool to update/backup wled devices in the local network. --- tools/wled-tools.sh | 226 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 tools/wled-tools.sh diff --git a/tools/wled-tools.sh b/tools/wled-tools.sh new file mode 100644 index 000000000..4ea3282f7 --- /dev/null +++ b/tools/wled-tools.sh @@ -0,0 +1,226 @@ +#!/bin/bash + +# ===================== +# wled-toolbox.sh - A script for managing WLED devices +# ===================== + +# Color Definitions +GREEN="\e[32m" +RED="\e[31m" +BLUE="\e[34m" +YELLOW="\e[33m" +RESET="\e[0m" + +# Path to backup directory +backup_dir="./" + +# Firmware file (if provided) +firmware_file="" + +# Logging function +log() { + local category="$1" + local color="$2" + local text="$3" + + if [ "$quiet" = true ]; then + return + fi + + if [ -t 1 ]; then # Check if output is a terminal + echo -e "${color}[${category}]${RESET} ${text}" + else + echo "[${category}] ${text}" + fi +} + +# Generic curl handler function +curl_handler() { + local command="$1" + local hostname="$2" + + response=$($command -w "%{http_code}" -o /dev/null) + curl_exit_code=$? + + if [ "$response" -ge 200 ] && [ "$response" -lt 300 ]; then + return 0 + elif [ $curl_exit_code -ne 0 ]; then + log "ERROR" "$RED" "Connection error during request to $hostname (curl exit code: $curl_exit_code)." + return 1 + elif [ "$response" -ge 400 ]; then + log "ERROR" "$RED" "Server error during request to $hostname (HTTP status code: $response)." + return 2 + else + log "ERROR" "$RED" "Unexpected response from $hostname (HTTP status code: $response)." + return 3 + fi +} + +# Print help message +show_help() { + cat << EOF +Usage: wled-toolbox.sh [OPTIONS] COMMAND [ARGS...] + +Options: + -h, --help Show this help message and exit. + -v, --verbose Enable verbose output for debugging. + -t, --target Specify a single WLED device by IP address or hostname. + -D, --discover Discover multiple WLED devices using mDNS. + -d, --directory Specify a directory for saving backups (default: working directory). + -f, --firmware Specify the firmware file for updating devices. + -q, --quiet Suppress logging output (also makes discover output hostnames only). + +Commands: + backup Backup the current state of a WLED device or multiple discovered devices. + update Update the firmware of a WLED device or multiple discovered devices. + discover Discover WLED devices using mDNS and list their IP addresses and names. + +EOF +} + +# Discover devices using mDNS +discover_devices() { + if ! command -v avahi-browse &> /dev/null; then + log "ERROR" "$RED" "'avahi-browse' is required but not installed." + exit 1 + fi + + mapfile -t hostnames < <(avahi-browse _wled._tcp --terminate -r -p | awk -F';' '/^=/ {print $7}') + if [ "$quiet" = true ]; then + for hostname in "${hostnames[@]}"; do + echo "$hostname" + done + else + printf "%s\n" "${hostnames[@]}" | sort -u + fi +} + +# Backup one device +backup_one() { + local hostname="$1" + + log "INFO" "$YELLOW" "Backing up device config/presets: $hostname" + + mkdir -p "$backup_dir" + + local cfg_url="http://$hostname/cfg.json" + local presets_url="http://$hostname/presets.json" + local cfg_dest="${backup_dir}/${hostname}.cfg.json" + local presets_dest="${backup_dir}/${hostname}.presets.json" + + # Write to ".tmp" files first, then move when success, to ensure we don't write partial files + local curl_command_cfg="curl -s "$cfg_url" -o "$cfg_dest.tmp"" + local curl_command_presets="curl -s "$presets_url" -o "$presets_dest.tmp"" + + curl_handler "$curl_command_cfg" "$hostname" + curl_handler "$curl_command_presets" "$hostname" + + mv "$cfg_dest.tmp" "$cfg_dest" + mv "$presets_dest.tmp" "$presets_dest" +} + +# Update one device +update_one() { + local hostname="$1" + local firmware="$2" + + log "INFO" "$YELLOW" "Starting firmware update for device: $hostname" + + if [ -z "$firmware" ]; then + log "ERROR" "$RED" "Firmware file not specified." + exit 1 + fi + + local url="http://$hostname/update" + local curl_command="curl -s -X POST -F "file=@$firmware" "$url"" + + curl_handler "$curl_command" "$hostname" +} + +# Command-line arguments processing +command="" +target="" +discover=false +quiet=false + +if [ $# -eq 0 ]; then + show_help + exit 0 +fi + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + show_help + exit 0 + ;; + -v|--verbose) + verbose=true + shift + ;; + -t|--target) + target="$2" + shift 2 + ;; + -D|--discover) + discover=true + shift + ;; + -d|--directory) + backup_dir="$2" + shift 2 + ;; + -f|--firmware) + firmware_file="$2" + shift 2 + ;; + -q|--quiet) + quiet=true + shift + ;; + backup|update|discover) + command="$1" + shift + ;; + *) + log "ERROR" "$RED" "Unknown argument: $1" + exit 1 + ;; + esac + +done + +# Execute the appropriate command +case "$command" in + discover) + discover_devices + ;; + backup) + if [ -n "$target" ]; then + backup_one "$target" + elif [ "$discover" = true ]; then + for hostname in $(discover_devices); do + backup_one "$hostname" + done + else + log "ERROR" "$RED" "No target specified. Use --target or --discover." + exit 1 + fi + ;; + update) + if [ -n "$target" ]; then + update_one "$target" "$firmware_file" + elif [ "$discover" = true ]; then + for hostname in $(discover_devices); do + update_one "$hostname" "$firmware_file" + done + else + log "ERROR" "$RED" "No target specified. Use --target or --discover." + exit 1 + fi + ;; + *) + show_help + exit 1 + ;; +esac From 0139c34ec276d2987156daf7837bb1b8f07ec0c3 Mon Sep 17 00:00:00 2001 From: Michael Bisbjerg Date: Sat, 29 Mar 2025 14:10:33 +0100 Subject: [PATCH 041/153] Rename script in help text --- tools/wled-tools.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/wled-tools.sh b/tools/wled-tools.sh index 4ea3282f7..731841c70 100644 --- a/tools/wled-tools.sh +++ b/tools/wled-tools.sh @@ -59,7 +59,7 @@ curl_handler() { # Print help message show_help() { cat << EOF -Usage: wled-toolbox.sh [OPTIONS] COMMAND [ARGS...] +Usage: wled-tools.sh [OPTIONS] COMMAND [ARGS...] Options: -h, --help Show this help message and exit. From 8a6e3a9898ff8d11d59523740c61e3f50472d4e7 Mon Sep 17 00:00:00 2001 From: Michael Bisbjerg Date: Sat, 29 Mar 2025 18:20:29 +0100 Subject: [PATCH 042/153] Update wled-tools.sh --- tools/wled-tools.sh | 95 ++++++++++++++++++++++++++------------------- 1 file changed, 55 insertions(+), 40 deletions(-) diff --git a/tools/wled-tools.sh b/tools/wled-tools.sh index 731841c70..45f670612 100644 --- a/tools/wled-tools.sh +++ b/tools/wled-tools.sh @@ -1,9 +1,5 @@ #!/bin/bash -# ===================== -# wled-toolbox.sh - A script for managing WLED devices -# ===================== - # Color Definitions GREEN="\e[32m" RED="\e[31m" @@ -11,12 +7,6 @@ BLUE="\e[34m" YELLOW="\e[33m" RESET="\e[0m" -# Path to backup directory -backup_dir="./" - -# Firmware file (if provided) -firmware_file="" - # Logging function log() { local category="$1" @@ -85,26 +75,29 @@ discover_devices() { exit 1 fi - mapfile -t hostnames < <(avahi-browse _wled._tcp --terminate -r -p | awk -F';' '/^=/ {print $7}') - if [ "$quiet" = true ]; then - for hostname in "${hostnames[@]}"; do - echo "$hostname" - done - else - printf "%s\n" "${hostnames[@]}" | sort -u - fi + mapfile -t raw_devices < <(avahi-browse _wled._tcp --terminate -r -p | awk -F';' '/^=/ {print $7, $8, $9}') + + local devices_array=() + for device in "${raw_devices[@]}"; do + read -r hostname address port <<< "$device" + devices_array+=("$hostname" "$address" "$port") + done + + echo "${devices_array[@]}" } # Backup one device backup_one() { local hostname="$1" + local address="$2" + local port="$3" - log "INFO" "$YELLOW" "Backing up device config/presets: $hostname" + log "INFO" "$YELLOW" "Backing up device config/presets: $hostname ($address:$port)" mkdir -p "$backup_dir" - local cfg_url="http://$hostname/cfg.json" - local presets_url="http://$hostname/presets.json" + local cfg_url="http://$address:$port/cfg.json" + local presets_url="http://$address:$port/presets.json" local cfg_dest="${backup_dir}/${hostname}.cfg.json" local presets_dest="${backup_dir}/${hostname}.presets.json" @@ -122,16 +115,13 @@ backup_one() { # Update one device update_one() { local hostname="$1" - local firmware="$2" + local address="$2" + local port="$3" + local firmware="$4" - log "INFO" "$YELLOW" "Starting firmware update for device: $hostname" + log "INFO" "$YELLOW" "Starting firmware update for device: $hostname ($address:$port)" - if [ -z "$firmware" ]; then - log "ERROR" "$RED" "Firmware file not specified." - exit 1 - fi - - local url="http://$hostname/update" + local url="http://$address:$port/update" local curl_command="curl -s -X POST -F "file=@$firmware" "$url"" curl_handler "$curl_command" "$hostname" @@ -142,6 +132,8 @@ command="" target="" discover=false quiet=false +backup_dir="./" +firmware_file="" if [ $# -eq 0 ]; then show_help @@ -154,10 +146,6 @@ while [[ $# -gt 0 ]]; do show_help exit 0 ;; - -v|--verbose) - verbose=true - shift - ;; -t|--target) target="$2" shift 2 @@ -193,14 +181,30 @@ done # Execute the appropriate command case "$command" in discover) - discover_devices + read -ra devices <<< "$(discover_devices)" + for ((i=0; i<${#devices[@]}; i+=3)); do + hostname="${devices[$i]}" + address="${devices[$i+1]}" + port="${devices[$i+2]}" + + if [ "$quiet" = true ]; then + echo "$hostname" + else + log "INFO" "$BLUE" "Discovered device: Hostname=$hostname, Address=$address, Port=$port" + fi + done ;; backup) if [ -n "$target" ]; then - backup_one "$target" + # Assume target is both the hostname and address, with port 80 + backup_one "$target" "$target" "80" elif [ "$discover" = true ]; then - for hostname in $(discover_devices); do - backup_one "$hostname" + read -ra devices <<< "$(discover_devices)" + for ((i=0; i<${#devices[@]}; i+=3)); do + hostname="${devices[$i]}" + address="${devices[$i+1]}" + port="${devices[$i+2]}" + backup_one "$hostname" "$address" "$port" done else log "ERROR" "$RED" "No target specified. Use --target or --discover." @@ -208,11 +212,22 @@ case "$command" in fi ;; update) + # Validate firmware before proceeding + if [ -z "$firmware_file" ] || [ ! -f "$firmware_file" ]; then + log "ERROR" "$RED" "Please provide a file in --firmware that exists" + exit 1 + fi + if [ -n "$target" ]; then - update_one "$target" "$firmware_file" + # Assume target is both the hostname and address, with port 80 + update_one "$target" "$target" "80" "$firmware_file" elif [ "$discover" = true ]; then - for hostname in $(discover_devices); do - update_one "$hostname" "$firmware_file" + read -ra devices <<< "$(discover_devices)" + for ((i=0; i<${#devices[@]}; i+=3)); do + hostname="${devices[$i]}" + address="${devices[$i+1]}" + port="${devices[$i+2]}" + update_one "$hostname" "$address" "$port" "$firmware_file" done else log "ERROR" "$RED" "No target specified. Use --target or --discover." From 6f5482782bb0c747c29a6c6e4cd2e5afcb8b2adc Mon Sep 17 00:00:00 2001 From: Michael Bisbjerg Date: Sat, 29 Mar 2025 21:38:31 +0100 Subject: [PATCH 043/153] Update wled-tools.sh --- tools/wled-tools.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/wled-tools.sh b/tools/wled-tools.sh index 45f670612..3ee574b91 100644 --- a/tools/wled-tools.sh +++ b/tools/wled-tools.sh @@ -53,7 +53,6 @@ Usage: wled-tools.sh [OPTIONS] COMMAND [ARGS...] Options: -h, --help Show this help message and exit. - -v, --verbose Enable verbose output for debugging. -t, --target Specify a single WLED device by IP address or hostname. -D, --discover Discover multiple WLED devices using mDNS. -d, --directory Specify a directory for saving backups (default: working directory). From b941654a687d83bfd3ed801c4ba6d3262e2df513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Sat, 29 Mar 2025 23:22:11 +0100 Subject: [PATCH 044/153] Allow clock overlay to use LED beyond 255 --- wled00/data/settings_time.htm | 4 ++-- wled00/wled.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/wled00/data/settings_time.htm b/wled00/data/settings_time.htm index df054f417..ae29065ea 100644 --- a/wled00/data/settings_time.htm +++ b/wled00/data/settings_time.htm @@ -168,8 +168,8 @@

Clock

Analog Clock overlay:
- First LED: Last LED:
- 12h LED:
+ First LED: Last LED:
+ 12h LED:
Show 5min marks:
Seconds (as trail):
Show clock overlay only if all LEDs are solid black:
diff --git a/wled00/wled.h b/wled00/wled.h index ea40c5dfe..01ab5a2c0 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -558,7 +558,7 @@ WLED_GLOBAL byte currentTimezone _INIT(WLED_TIMEZONE); // Timezone ID. Refer WLED_GLOBAL int utcOffsetSecs _INIT(WLED_UTC_OFFSET); // Seconds to offset from UTC before timzone calculation WLED_GLOBAL byte overlayCurrent _INIT(0); // 0: no overlay 1: analog clock 2: was single-digit clock 3: was cronixie -WLED_GLOBAL byte overlayMin _INIT(0), overlayMax _INIT(DEFAULT_LED_COUNT - 1); // boundaries of overlay mode +WLED_GLOBAL uint16_t overlayMin _INIT(0), overlayMax _INIT(DEFAULT_LED_COUNT - 1); // boundaries of overlay mode WLED_GLOBAL byte analogClock12pixel _INIT(0); // The pixel in your strip where "midnight" would be WLED_GLOBAL bool analogClockSecondsTrail _INIT(false); // Display seconds as trail of LEDs instead of a single pixel From dc5eaf3ae9b3aa4d9cef7914813c2f376df00ba7 Mon Sep 17 00:00:00 2001 From: Michael Bisbjerg Date: Thu, 3 Apr 2025 21:46:58 +0200 Subject: [PATCH 045/153] Rename file --- tools/{wled-tools.sh => wled-tools} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tools/{wled-tools.sh => wled-tools} (100%) diff --git a/tools/wled-tools.sh b/tools/wled-tools similarity index 100% rename from tools/wled-tools.sh rename to tools/wled-tools From 646ec30ae5049a3387069d22cb85cbb61ee273c7 Mon Sep 17 00:00:00 2001 From: Michael Bisbjerg Date: Sun, 6 Apr 2025 01:07:40 +0200 Subject: [PATCH 046/153] Implement error checking for curl; add checks for required param args --- tools/wled-tools | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/tools/wled-tools b/tools/wled-tools index 3ee574b91..d99e9e51a 100644 --- a/tools/wled-tools +++ b/tools/wled-tools @@ -104,11 +104,22 @@ backup_one() { local curl_command_cfg="curl -s "$cfg_url" -o "$cfg_dest.tmp"" local curl_command_presets="curl -s "$presets_url" -o "$presets_dest.tmp"" - curl_handler "$curl_command_cfg" "$hostname" - curl_handler "$curl_command_presets" "$hostname" + if ! curl_handler "$curl_command_cfg" "$hostname"; then + log "ERROR" "$RED" "Failed to backup configuration for $hostname" + rm -f "$cfg_dest.tmp" + return 1 + fi + + if ! curl_handler "$curl_command_presets" "$hostname"; then + log "ERROR" "$RED" "Failed to backup presets for $hostname" + rm -f "$presets_dest.tmp" + return 1 + fi mv "$cfg_dest.tmp" "$cfg_dest" mv "$presets_dest.tmp" "$presets_dest" + log "INFO" "$GREEN" "Successfully backed up config and presets for $hostname" + return 0 } # Update one device @@ -146,6 +157,10 @@ while [[ $# -gt 0 ]]; do exit 0 ;; -t|--target) + if [ -z "$2" ] || [[ "$2" == -* ]]; then + log "ERROR" "$RED" "The --target option requires an argument." + exit 1 + fi target="$2" shift 2 ;; @@ -154,10 +169,18 @@ while [[ $# -gt 0 ]]; do shift ;; -d|--directory) + if [ -z "$2" ] || [[ "$2" == -* ]]; then + log "ERROR" "$RED" "The --directory option requires an argument." + exit 1 + fi backup_dir="$2" shift 2 ;; -f|--firmware) + if [ -z "$2" ] || [[ "$2" == -* ]]; then + log "ERROR" "$RED" "The --firmware option requires an argument." + exit 1 + fi firmware_file="$2" shift 2 ;; @@ -174,7 +197,6 @@ while [[ $# -gt 0 ]]; do exit 1 ;; esac - done # Execute the appropriate command From bc099baeb11fcb4cc43747c3f58d1df24f6e664a Mon Sep 17 00:00:00 2001 From: Michael Bisbjerg Date: Sun, 6 Apr 2025 01:10:27 +0200 Subject: [PATCH 047/153] Guard against hostnames/avahi responses with spaces --- tools/wled-tools | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/tools/wled-tools b/tools/wled-tools index d99e9e51a..345ef1ec1 100644 --- a/tools/wled-tools +++ b/tools/wled-tools @@ -68,22 +68,23 @@ EOF } # Discover devices using mDNS -discover_devices() { - if ! command -v avahi-browse &> /dev/null; then - log "ERROR" "$RED" "'avahi-browse' is required but not installed." - exit 1 - fi +discover_devices() { + if ! command -v avahi-browse &> /dev/null; then + log "ERROR" "$RED" "'avahi-browse' is required but not installed." + exit 1 + fi - mapfile -t raw_devices < <(avahi-browse _wled._tcp --terminate -r -p | awk -F';' '/^=/ {print $7, $8, $9}') + # Map avahi responses to strings seperated by 0x1F (unit separator) + mapfile -t raw_devices < <(avahi-browse _wled._tcp --terminate -r -p | awk -F';' '/^=/ {print $7"\x1F"$8"\x1F"$9}') - local devices_array=() - for device in "${raw_devices[@]}"; do - read -r hostname address port <<< "$device" - devices_array+=("$hostname" "$address" "$port") - done + local devices_array=() + for device in "${raw_devices[@]}"; do + IFS=$'\x1F' read -r hostname address port <<< "$device" + devices_array+=("$hostname" "$address" "$port") + done - echo "${devices_array[@]}" -} + echo "${devices_array[@]}" +} # Backup one device backup_one() { From 5203c3927fbb5e97163704e2435bbeffb81ca0dc Mon Sep 17 00:00:00 2001 From: Michael Bisbjerg Date: Sun, 6 Apr 2025 01:11:20 +0200 Subject: [PATCH 048/153] Update tools/wled-tools Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- tools/wled-tools | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/wled-tools b/tools/wled-tools index 345ef1ec1..40d2df91d 100644 --- a/tools/wled-tools +++ b/tools/wled-tools @@ -135,7 +135,13 @@ update_one() { local url="http://$address:$port/update" local curl_command="curl -s -X POST -F "file=@$firmware" "$url"" - curl_handler "$curl_command" "$hostname" + if ! curl_handler "$curl_command" "$hostname"; then + log "ERROR" "$RED" "Failed to update firmware for $hostname" + return 1 + fi + + log "INFO" "$GREEN" "Successfully initiated firmware update for $hostname" + return 0 } # Command-line arguments processing From 9e96bd6705a08a1f70d24f74f141d805616faa1b Mon Sep 17 00:00:00 2001 From: Michael Bisbjerg Date: Sun, 6 Apr 2025 01:17:44 +0200 Subject: [PATCH 049/153] Add help examples, header --- tools/wled-tools | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tools/wled-tools b/tools/wled-tools index 40d2df91d..1b27b113f 100644 --- a/tools/wled-tools +++ b/tools/wled-tools @@ -1,5 +1,9 @@ #!/bin/bash +# WLED Tools +# A utility for managing WLED devices in a local network +# https://github.com/wled/WLED + # Color Definitions GREEN="\e[32m" RED="\e[31m" @@ -64,6 +68,19 @@ Commands: update Update the firmware of a WLED device or multiple discovered devices. discover Discover WLED devices using mDNS and list their IP addresses and names. +Examples: + # Discover all WLED devices on the network + ./wled-tools discover + + # Backup a specific WLED device + ./wled-tools -t 192.168.1.100 backup + + # Backup all discovered WLED devices to a specific directory + ./wled-tools -D -d /path/to/backups backup + + # Update firmware on all discovered WLED devices + ./wled-tools -D -f /path/to/firmware.bin update + EOF } From 81b74227fad6b2908757da6280abfe9d09a42969 Mon Sep 17 00:00:00 2001 From: Michael Bisbjerg Date: Sun, 6 Apr 2025 01:20:24 +0200 Subject: [PATCH 050/153] Set executable --- tools/wled-tools | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 tools/wled-tools diff --git a/tools/wled-tools b/tools/wled-tools old mode 100644 new mode 100755 From 88aa8e817868e2edd718b26438058d19068deffc Mon Sep 17 00:00:00 2001 From: Michael Bisbjerg Date: Fri, 11 Apr 2025 16:15:52 +0200 Subject: [PATCH 051/153] Add note on avahi-utils --- tools/wled-tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/wled-tools b/tools/wled-tools index 1b27b113f..9d196526f 100755 --- a/tools/wled-tools +++ b/tools/wled-tools @@ -87,7 +87,7 @@ EOF # Discover devices using mDNS discover_devices() { if ! command -v avahi-browse &> /dev/null; then - log "ERROR" "$RED" "'avahi-browse' is required but not installed." + log "ERROR" "$RED" "'avahi-browse' is required but not installed, please install avahi-utils using your preferred package manager." exit 1 fi From 02f14baad44d52f222c27944c372efbc41a404fc Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 15 Apr 2025 19:07:21 +0200 Subject: [PATCH 052/153] Updates to particle system (#4630) * added Sonic Boom AR FX, some tweaks to Sonic Stream * added white color option to Sonic Stream * improvements to collisions (speed look-ahead) * code prettified * added "playful" mode to PS Chase plus some minor speed optimizations * Adding new FX: PS Springy with many config options --- wled00/FX.cpp | 407 +++++++++++++++++++++++++++++++----- wled00/FX.h | 4 +- wled00/FXparticleSystem.cpp | 243 ++++++++++----------- wled00/FXparticleSystem.h | 8 +- 4 files changed, 482 insertions(+), 180 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 832c2e404..f439e4c66 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7818,7 +7818,7 @@ uint16_t mode_particlefireworks(void) { else if (PartSys->sources[j].source.vy < 0) { // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it PartSys->sources[j].source.y = PS_P_RADIUS; // start from bottom PartSys->sources[j].source.x = (PartSys->maxX >> 2) + hw_random(PartSys->maxX >> 1); // centered half - PartSys->sources[j].source.vy = (SEGMENT.custom3) + random16(SEGMENT.custom1 >> 3) + 5; // rocket speed TODO: need to adjust for segment height + PartSys->sources[j].source.vy = (SEGMENT.custom3) + hw_random16(SEGMENT.custom1 >> 3) + 5; // rocket speed TODO: need to adjust for segment height PartSys->sources[j].source.vx = hw_random16(7) - 3; // not perfectly straight up PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white PartSys->sources[j].source.ttl = hw_random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // set fuse time @@ -7888,7 +7888,7 @@ uint16_t mode_particlefireworks(void) { counter = 0; speed += 3 + ((SEGMENT.intensity >> 6)); // increase speed to form a second wave PartSys->sources[j].source.hue += hueincrement; // new color for next circle - PartSys->sources[j].source.sat = min((uint16_t)150, random16()); + PartSys->sources[j].source.sat = 100 + hw_random16(156); } angle += angleincrement; // set angle for next particle } @@ -9514,44 +9514,36 @@ uint16_t mode_particleHourglass(void) { PartSys->updateSystem(); // update system properties (dimensions and data pointers) settingTracker = reinterpret_cast(PartSys->PSdataEnd); //assign data pointer direction = reinterpret_cast(PartSys->PSdataEnd + 4); //assign data pointer - PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, 255)); + PartSys->setUsedParticles(1 + ((SEGMENT.intensity * 255) >> 8)); PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30)); - PartSys->enableParticleCollisions(true, 34); // hardness value found by experimentation on different settings + PartSys->enableParticleCollisions(true, 32); // hardness value found by experimentation on different settings uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7 if ((SEGMENT.intensity | (PartSys->getAvailableParticles() << 8)) != *settingTracker) { // initialize, getAvailableParticles changes while in FX transition *settingTracker = SEGMENT.intensity | (PartSys->getAvailableParticles() << 8); for (uint32_t i = 0; i < PartSys->usedParticles; i++) { - PartSys->particleFlags[i].reversegrav = true; + PartSys->particleFlags[i].reversegrav = true; // resting particles dont fall *direction = 0; // down SEGENV.aux1 = 1; // initialize below } SEGENV.aux0 = PartSys->usedParticles - 1; // initial state, start with highest number particle } + // calculate target position depending on direction + auto calcTargetPos = [&](size_t i) { + return PartSys->particleFlags[i].reversegrav ? + PartSys->maxX - i * PS_P_RADIUS_1D - positionOffset + : (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionOffset; + }; + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // check if particle reached target position after falling - int32_t targetposition; - if (PartSys->particleFlags[i].fixed == false) { // && abs(PartSys->particles[i].vx) < 8) { - // calculate target position depending on direction - bool closeToTarget = false; - bool reachedTarget = false; - if (PartSys->particleFlags[i].reversegrav) { // up - targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D) - positionOffset; // target resting position - if (targetposition - PartSys->particles[i].x <= 5 * PS_P_RADIUS_1D) - closeToTarget = true; - if (PartSys->particles[i].x >= targetposition) // particle has reached target position, pin it. if not pinned, they do not stack well on larger piles - reachedTarget = true; - } - else { // down, highest index particle drops first - targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionOffset; // target resting position note: using -offset instead of -1 + offset - if (PartSys->particles[i].x - targetposition <= 5 * PS_P_RADIUS_1D) - closeToTarget = true; - if (PartSys->particles[i].x <= targetposition) // particle has reached target position, pin it. if not pinned, they do not stack well on larger piles - reachedTarget = true; - } - if (reachedTarget || (closeToTarget && abs(PartSys->particles[i].vx) < 10)) { // reached target or close to target and slow speed + if (PartSys->particleFlags[i].fixed == false && abs(PartSys->particles[i].vx) < 5) { + int32_t targetposition = calcTargetPos(i); + bool closeToTarget = abs(targetposition - PartSys->particles[i].x) < 3 * PS_P_RADIUS_1D; + if (closeToTarget) { // close to target and slow speed PartSys->particles[i].x = targetposition; // set exact position PartSys->particleFlags[i].fixed = true; // pin particle } @@ -9576,19 +9568,20 @@ uint16_t mode_particleHourglass(void) { PartSys->particles[i].hue += 120; } + // re-order particles in case collisions flipped particles (highest number index particle is on the "bottom") + for (int i = 0; i < PartSys->usedParticles - 1; i++) { + if (PartSys->particles[i].x < PartSys->particles[i+1].x && PartSys->particleFlags[i].fixed == false && PartSys->particleFlags[i+1].fixed == false) { + std::swap(PartSys->particles[i].x, PartSys->particles[i+1].x); + } + } + + if (SEGENV.aux1 == 1) { // last countdown call before dropping starts, reset all particles for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particleFlags[i].collide = true; PartSys->particleFlags[i].perpetual = true; PartSys->particles[i].ttl = 260; - uint32_t targetposition; - //calculate target position depending on direction - if (PartSys->particleFlags[i].reversegrav) - targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionOffset); // target resting position - else - targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionOffset; // target resting position -5 - PS_P_RADIUS_1D/2 - - PartSys->particles[i].x = targetposition; + PartSys->particles[i].x = calcTargetPos(i); PartSys->particleFlags[i].fixed = true; } } @@ -9699,7 +9692,7 @@ uint16_t mode_particleBalance(void) { // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) - PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setMotionBlur(SEGMENT.custom2); // enable motion blur PartSys->setBounce(!SEGMENT.check2); PartSys->setWrap(SEGMENT.check2); uint8_t hardness = SEGMENT.custom1 > 0 ? map(SEGMENT.custom1, 0, 255, 50, 250) : 200; // set hardness, make the walls hard if collisions are disabled @@ -9716,6 +9709,17 @@ uint16_t mode_particleBalance(void) { } SEGENV.aux1 = PartSys->usedParticles; + // re-order particles in case collisions flipped particles + for (i = 0; i < PartSys->usedParticles - 1; i++) { + if (PartSys->particles[i].x > PartSys->particles[i+1].x) { + if (SEGMENT.check2) { // check for wrap around + if (PartSys->particles[i].x - PartSys->particles[i+1].x > 3 * PS_P_RADIUS_1D) + continue; + } + std::swap(PartSys->particles[i].x, PartSys->particles[i+1].x); + } + } + if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0) { // how often the force is applied depends on speed setting int32_t xgravity; int32_t increment = (SEGMENT.speed >> 6) + 1; @@ -9756,7 +9760,7 @@ by DedeHai (Damian Schneider) uint16_t mode_particleChase(void) { ParticleSystem1D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization - if (!initParticleSystem1D(PartSys, 1, 255, 3, true)) // init + if (!initParticleSystem1D(PartSys, 1, 255, 2, true)) // init return mode_static(); // allocation failed or is single pixel SEGENV.aux0 = 0xFFFF; // invalidate *PartSys->PSdataEnd = 1; // huedir @@ -9766,39 +9770,43 @@ uint16_t mode_particleChase(void) { PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) return mode_static(); // something went wrong, no data! - // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setColorByPosition(SEGMENT.check3); - PartSys->setMotionBlur(8 + ((SEGMENT.custom3) << 3)); // anable motion blur - // uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer - + PartSys->setMotionBlur(7 + ((SEGMENT.custom3) << 3)); // anable motion blur + uint32_t numParticles = 1 + map(SEGMENT.intensity, 0, 255, 2, 255 / (1 + (SEGMENT.custom1 >> 6))); // depends on intensity and particle size (custom1), minimum 1 + numParticles = min(numParticles, PartSys->usedParticles); // limit to available particles + int32_t huestep = 1 + ((((uint32_t)SEGMENT.custom2 << 19) / numParticles) >> 16); // hue increment uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3 + PartSys->getAvailableParticles(); // note: getAvailableParticles is used to enforce update during transitions if (SEGENV.aux0 != settingssum) { // settings changed changed, update - uint32_t numParticles = map(SEGMENT.intensity, 0, 255, 2, 255 / (1 + (SEGMENT.custom1 >> 6))); // depends on intensity and particle size (custom1) - if (numParticles == 0) numParticles = 1; // minimum 1 particle - PartSys->setUsedParticles(numParticles); - SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 5)) / PartSys->usedParticles; // spacing between particles + if (SEGMENT.check1) + SEGENV.step = PartSys->advPartProps[0].size / 2 + (PartSys->maxX / numParticles); + else + SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 5)) / numParticles; // spacing between particles for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) { PartSys->advPartProps[i].sat = 255; PartSys->particles[i].x = (i - 1) * SEGENV.step; // distribute evenly (starts out of frame for i=0) - PartSys->particles[i].vx = SEGMENT.speed >> 1; + PartSys->particles[i].vx = SEGMENT.speed >> 2; PartSys->advPartProps[i].size = SEGMENT.custom1; if (SEGMENT.custom2 < 255) - PartSys->particles[i].hue = (i * (SEGMENT.custom2 << 3)) / PartSys->usedParticles; // gradient distribution + PartSys->particles[i].hue = i * huestep; // gradient distribution else PartSys->particles[i].hue = hw_random16(); } SEGENV.aux0 = settingssum; } - int32_t huestep = (((uint32_t)SEGMENT.custom2 << 19) / PartSys->usedParticles) >> 16; // hue increment + if(SEGMENT.check1) { + huestep = 1 + (max((int)huestep, 3) * ((int(sin16_t(strip.now * 3) + 32767))) >> 15); // changes gradient spread (scale hue step) + } // wrap around (cannot use particle system wrap if distributing colors manually, it also wraps rendering which does not look good) for (int32_t i = (int32_t)PartSys->usedParticles - 1; i >= 0; i--) { // check from the back, last particle wraps first, multiple particles can overrun per frame if (PartSys->particles[i].x > PartSys->maxX + PS_P_RADIUS_1D + PartSys->advPartProps[i].size) { // wrap it around uint32_t nextindex = (i + 1) % PartSys->usedParticles; - PartSys->particles[i].x = PartSys->particles[nextindex].x - (int)SEGENV.step; + PartSys->particles[i].x = PartSys->particles[nextindex].x - (int)SEGENV.step; + if(SEGMENT.check1) // playful mode, vary size + PartSys->advPartProps[i].size = max(1 + (SEGMENT.custom1 >> 1), ((int(sin16_t(strip.now << 1) + 32767)) >> 8)); // cycle size if (SEGMENT.custom2 < 255) PartSys->particles[i].hue = PartSys->particles[nextindex].hue - huestep; else @@ -9807,11 +9815,37 @@ uint16_t mode_particleChase(void) { PartSys->particles[i].ttl = 300; // reset ttl, cannot use perpetual because memmanager can change pointer at any time } + if (SEGMENT.check1) { // playful mode, changes hue, size, speed, density dynamically + int8_t* huedir = reinterpret_cast(PartSys->PSdataEnd); //assign data pointer + int8_t* stepdir = reinterpret_cast(PartSys->PSdataEnd + 1); + if(*stepdir == 0) *stepdir = 1; // initialize directions + if(*huedir == 0) *huedir = 1; + if (SEGENV.step >= (PartSys->advPartProps[0].size + PS_P_RADIUS_1D * 4) + PartSys->maxX / numParticles) + *stepdir = -1; // increase density (decrease space between particles) + else if (SEGENV.step <= (PartSys->advPartProps[0].size >> 1) + ((PartSys->maxX / numParticles))) + *stepdir = 1; // decrease density + if (SEGENV.aux1 > 512) + *huedir = -1; + else if (SEGENV.aux1 < 50) + *huedir = 1; + if (SEGMENT.call % (1024 / (1 + (SEGMENT.speed >> 2))) == 0) + SEGENV.aux1 += *huedir; + int8_t globalhuestep = 0; // global hue increment + if (SEGMENT.call % (1 + (int(sin16_t(strip.now) + 32767) >> 12)) == 0) + globalhuestep = 2; // global hue change to add some color variation + if ((SEGMENT.call & 0x1F) == 0) + SEGENV.step += *stepdir; // change density + for(int32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].hue -= globalhuestep; // shift global hue (both directions) + PartSys->particles[i].vx = 1 + (SEGMENT.speed >> 2) + ((int32_t(sin16_t(strip.now >> 1) + 32767) * (SEGMENT.speed >> 2)) >> 16); + } + } + PartSys->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets rendering size to one pixel PartSys->update(); // update and render return FRAMETIME; } -static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@!,Density,Size,Hue,Blur,,,Position Color;,!;!;1;pal=11,sx=50,c2=5,c3=0"; +static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@!,Density,Size,Hue,Blur,Playful,,Position Color;,!;!;1;pal=11,sx=50,c2=5,c3=0"; /* Particle Fireworks Starburst replacement (smoother rendering, more settings) @@ -10016,10 +10050,9 @@ static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,Cooling,Bl /* Particle based AR effect, swoop particles along the strip with selected frequency loudness - Uses palette for particle color by DedeHai (Damian Schneider) */ -uint16_t mode_particle1Dsonicstream(void) { +uint16_t mode_particle1DsonicStream(void) { ParticleSystem1D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization @@ -10029,7 +10062,6 @@ uint16_t mode_particle1Dsonicstream(void) { PartSys->sources[0].source.x = 0; // at start //PartSys->sources[1].source.x = PartSys->maxX; // at end PartSys->sources[0].var = 0;//SEGMENT.custom1 >> 3; - PartSys->sources[0].sat = 255; } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -10040,7 +10072,6 @@ uint16_t mode_particle1Dsonicstream(void) { PartSys->updateSystem(); // update system properties (dimensions and data pointers) PartSys->setMotionBlur(20 + (SEGMENT.custom2 >> 1)); // anable motion blur PartSys->setSmearBlur(200); // smooth out the edges - PartSys->sources[0].v = 5 + (SEGMENT.speed >> 2); // FFT processing @@ -10050,11 +10081,10 @@ uint16_t mode_particle1Dsonicstream(void) { uint32_t baseBin = SEGMENT.custom3 >> 1; // 0 - 15 map(SEGMENT.custom3, 0, 31, 0, 14); loudness = fftResult[baseBin];// + fftResult[baseBin + 1]; - int mids = sqrt16((int)fftResult[5] + (int)fftResult[6] + (int)fftResult[7] + (int)fftResult[8] + (int)fftResult[9] + (int)fftResult[10]); // average the mids, bin 5 is ~500Hz, bin 10 is ~2kHz (see audio_reactive.h) if (baseBin > 12) loudness = loudness << 2; // double loudness for high frequencies (better detecion) - uint32_t threshold = 150 - (SEGMENT.intensity >> 1); + uint32_t threshold = 140 - (SEGMENT.intensity >> 1); if (SEGMENT.check2) { // enable low pass filter for dynamic threshold SEGMENT.step = (SEGMENT.step * 31500 + loudness * (32768 - 31500)) >> 15; // low pass filter for simple beat detection: add average to base threshold threshold = 20 + (threshold >> 1) + SEGMENT.step; // add average to threshold @@ -10062,6 +10092,7 @@ uint16_t mode_particle1Dsonicstream(void) { // color uint32_t hueincrement = (SEGMENT.custom1 >> 3); // 0-31 + PartSys->sources[0].sat = SEGMENT.custom1 > 0 ? 255 : 0; // color slider at zero: set to white PartSys->setColorByPosition(SEGMENT.custom1 == 255); // particle manipulation @@ -10072,8 +10103,10 @@ uint16_t mode_particle1Dsonicstream(void) { } else PartSys->particles[i].ttl = 0; } - if (SEGMENT.check1) // modulate colors by mid frequencies + if (SEGMENT.check1) { // modulate colors by mid frequencies + int mids = sqrt32_bw((int)fftResult[5] + (int)fftResult[6] + (int)fftResult[7] + (int)fftResult[8] + (int)fftResult[9] + (int)fftResult[10]); // average the mids, bin 5 is ~500Hz, bin 10 is ~2kHz (see audio_reactive.h) PartSys->particles[i].hue += (mids * perlin8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies + } } if (loudness > threshold) { @@ -10117,6 +10150,266 @@ uint16_t mode_particle1Dsonicstream(void) { return FRAMETIME; } static const char _data_FX_MODE_PS_SONICSTREAM[] PROGMEM = "PS Sonic Stream@!,!,Color,Blur,Bin,Mod,Filter,Push;,!;!;1f;c3=0,o2=1"; + + +/* + Particle based AR effect, creates exploding particles on beats + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particle1DsonicBoom(void) { + ParticleSystem1D *PartSys = nullptr; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 255, 0, true)) // init, no additional data needed + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(180 * SEGMENT.check3); + PartSys->setSmearBlur(64 * SEGMENT.check3); + PartSys->sources[0].var = map(SEGMENT.speed, 0, 255, 10, 127); + + // FFT processing + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + uint32_t loudness; + uint32_t baseBin = SEGMENT.custom3 >> 1; // 0 - 15 map(SEGMENT.custom3, 0, 31, 0, 14); + loudness = fftResult[baseBin];// + fftResult[baseBin + 1]; + + if (baseBin > 12) + loudness = loudness << 2; // double loudness for high frequencies (better detecion) + uint32_t threshold = 150 - (SEGMENT.intensity >> 1); + if (SEGMENT.check2) { // enable low pass filter for dynamic threshold + SEGMENT.step = (SEGMENT.step * 31500 + loudness * (32768 - 31500)) >> 15; // low pass filter for simple beat detection: add average to base threshold + threshold = 20 + (threshold >> 1) + SEGMENT.step; // add average to threshold + } + + // particle manipulation + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (SEGMENT.check1) { // modulate colors by mid frequencies + int mids = sqrt32_bw((int)fftResult[5] + (int)fftResult[6] + (int)fftResult[7] + (int)fftResult[8] + (int)fftResult[9] + (int)fftResult[10]); // average the mids, bin 5 is ~500Hz, bin 10 is ~2kHz (see audio_reactive.h) + PartSys->particles[i].hue += (mids * perlin8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies + } + if (PartSys->particles[i].ttl > 16) { + PartSys->particles[i].ttl -= 16; //ttl is linked to brightness, this allows to use higher brightness but still a (very) short lifespan + } + } + + if (loudness > threshold) { + if (SEGMENT.aux1 == 0) { // edge detected, code only runs once per "beat" + // update position + if (SEGMENT.custom2 < 128) // fixed position + PartSys->sources[0].source.x = map(SEGMENT.custom2, 0, 127, 0, PartSys->maxX); + else if (SEGMENT.custom2 < 255) { // advances on each "beat" + int32_t step = PartSys->maxX / (((270 - SEGMENT.custom2) >> 3)); // step: 2 - 33 steps for full segment width + PartSys->sources[0].source.x = (PartSys->sources[0].source.x + step) % PartSys->maxX; + if (PartSys->sources[0].source.x < step) // align to be symmetrical by making the first position half a step from start + PartSys->sources[0].source.x = step >> 1; + } + else // position set to max, use random postion per beat + PartSys->sources[0].source.x = hw_random(PartSys->maxX); + + // update color + //PartSys->setColorByPosition(SEGMENT.custom1 == 255); // color slider at max: particle color by position + PartSys->sources[0].sat = SEGMENT.custom1 > 0 ? 255 : 0; // color slider at zero: set to white + if (SEGMENT.custom1 == 255) // emit color by position + SEGMENT.aux0 = map(PartSys->sources[0].source.x , 0, PartSys->maxX, 0, 255); + else if (SEGMENT.custom1 > 0) + SEGMENT.aux0 += (SEGMENT.custom1 >> 1); // change emit color per "beat" + } + SEGMENT.aux1 = 1; // track edge detection + + PartSys->sources[0].minLife = 200; + PartSys->sources[0].maxLife = PartSys->sources[0].minLife + (((unsigned)SEGMENT.intensity * loudness * loudness) >> 13); + PartSys->sources[0].source.hue = SEGMENT.aux0; + PartSys->sources[0].size = 1; //SEGMENT.speed>>3; + uint32_t explosionsize = 4 + (PartSys->maxXpixel >> 2); + explosionsize = hw_random16((explosionsize * loudness) >> 10); + for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles + PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + } + } + else + SEGMENT.aux1 = 0; // reset edge detection + + PartSys->update(); // update and render (needs to be done before manipulation for initial particle spacing to be right) + return FRAMETIME; +} +static const char _data_FX_MODE_PS_SONICBOOM[] PROGMEM = "PS Sonic Boom@!,!,Color,Position,Bin,Mod,Filter,Blur;,!;!;1f;c2=63,c3=0,o2=1"; + +/* +Particles bound by springs +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleSpringy(void) { + ParticleSystem1D *PartSys = nullptr; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init + return mode_static(); // allocation failed or is single pixel + SEGENV.aux0 = SEGENV.aux1 = 0xFFFF; // invalidate settings + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(220 * SEGMENT.check1); // anable motion blur + PartSys->setSmearBlur(50); // smear a little + PartSys->setUsedParticles(map(SEGMENT.custom1, 0, 255, 30 >> SEGMENT.check2, 255 >> (SEGMENT.check2*2))); // depends on density and particle size + // PartSys->enableParticleCollisions(true, 140); // enable particle collisions, can not be set too hard or impulses will not strech the springs if soft. + int32_t springlength = PartSys->maxX / (PartSys->usedParticles); // spring length (spacing between particles) + int32_t springK = map(SEGMENT.speed, 0, 255, 5, 35); // spring constant (stiffness) + + uint32_t settingssum = SEGMENT.custom1 + SEGMENT.check2 + PartSys->getAvailableParticles(); // note: getAvailableParticles is used to enforce update during transitions + if (SEGENV.aux0 != settingssum) { // number of particles changed, update distribution + for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) { + PartSys->advPartProps[i].sat = 255; // full saturation + //PartSys->particleFlags[i].collide = true; // enable collision for particles + PartSys->particles[i].x = (i+1) * ((PartSys->maxX) / (PartSys->usedParticles)); // distribute + //PartSys->particles[i].vx = 0; //reset speed + PartSys->advPartProps[i].size = SEGMENT.check2 ? 190 : 2; // set size, small or big + } + SEGENV.aux0 = settingssum; + } + int dxlimit = (2 + ((255 - SEGMENT.speed) >> 5)) * springlength; // limit for spring length to avoid overstretching + + int springforce[PartSys->usedParticles]; // spring forces + memset(springforce, 0, PartSys->usedParticles * sizeof(int32_t)); // reset spring forces + + // calculate spring forces and limit particle positions + if (PartSys->particles[0].x < -springlength) + PartSys->particles[0].x = -springlength; // limit the spring length + else if (PartSys->particles[0].x > dxlimit) + PartSys->particles[0].x = dxlimit; // limit the spring length + springforce[0] += ((springlength >> 1) - (PartSys->particles[0].x)) * springK; // first particle anchors to x=0 + + for (int32_t i = 1; i < PartSys->usedParticles; i++) { + // reorder particles if they are out of order to prevent chaos + if (PartSys->particles[i].x < PartSys->particles[i-1].x) + std::swap(PartSys->particles[i].x, PartSys->particles[i-1].x); // swap particle positions to maintain order + int dx = PartSys->particles[i].x - PartSys->particles[i-1].x; // distance, always positive + if (dx > dxlimit) { // limit the spring length + PartSys->particles[i].x = PartSys->particles[i-1].x + dxlimit; + dx = dxlimit; + } + int dxleft = (springlength - dx); // offset from spring resting position + springforce[i] += dxleft * springK; + springforce[i-1] -= dxleft * springK; + if (i == (PartSys->usedParticles - 1)) { + if (PartSys->particles[i].x >= PartSys->maxX + springlength) + PartSys->particles[i].x = PartSys->maxX + springlength; + int dxright = (springlength >> 1) - (PartSys->maxX - PartSys->particles[i].x); // last particle anchors to x=maxX + springforce[i] -= dxright * springK; + } + } + // apply spring forces to particles + bool dampenoscillations = (SEGMENT.call % (9 - (SEGMENT.speed >> 5))) == 0; // dampen oscillation if particles are slow, more damping on stiffer springs + for (int32_t i = 0; i < PartSys->usedParticles; i++) { + springforce[i] = springforce[i] / 64; // scale spring force (cannot use shifts because of negative values) + int maxforce = 120; // limit spring force + springforce[i] = springforce[i] > maxforce ? maxforce : springforce[i] < -maxforce ? -maxforce : springforce[i]; // limit spring force + PartSys->applyForce(PartSys->particles[i], springforce[i], PartSys->advPartProps[i].forcecounter); + //dampen slow particles to avoid persisting oscillations on higher stiffness + if (dampenoscillations) { + if (abs(PartSys->particles[i].vx) < 3 && abs(springforce[i]) < (springK >> 2)) + PartSys->particles[i].vx = (PartSys->particles[i].vx * 254) / 256; // take out some energy + } + PartSys->particles[i].ttl = 300; // reset ttl, cannot use perpetual + } + + if (SEGMENT.call % ((65 - ((SEGMENT.intensity * (1 + (SEGMENT.speed>>3))) >> 7))) == 0) // more damping for higher stiffness + PartSys->applyFriction((SEGMENT.intensity >> 2)); + + // add a small resetting force so particles return to resting position even under high damping + for (int32_t i = 1; i < PartSys->usedParticles - 1; i++) { + int restposition = (springlength >> 1) + i * springlength; // resting position + int dx = restposition - PartSys->particles[i].x; // distance, always positive + PartSys->applyForce(PartSys->particles[i], dx > 0 ? 1 : (dx < 0 ? -1 : 0), PartSys->advPartProps[i].forcecounter); + } + + // Modes + if (SEGMENT.check3) { // use AR, custom 3 becomes frequency band to use, applies velocity to center particle according to loudness + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + uint32_t baseBin = map(SEGMENT.custom3, 0, 31, 0, 14); + uint32_t loudness = fftResult[baseBin] + fftResult[baseBin+1]; + uint32_t threshold = 80; //150 - (SEGMENT.intensity >> 1); + if (loudness > threshold) { + int offset = (PartSys->maxX >> 1) - PartSys->particles[PartSys->usedParticles>>1].x; // offset from center + if (abs(offset) < PartSys->maxX >> 5) // push particle around in center sector + PartSys->particles[PartSys->usedParticles>>1].vx = ((PartSys->particles[PartSys->usedParticles>>1].vx > 0 ? 1 : -1)) * (loudness >> 3); + } + } + else{ + if (SEGMENT.custom3 <= 10) { // periodic pulse: 0-5 apply at start, 6-10 apply at center + if (strip.now > SEGMENT.step) { + int speed = (SEGMENT.custom3 > 5) ? (SEGMENT.custom3 - 6) : SEGMENT.custom3; + SEGMENT.step = strip.now + 7500 - ((SEGMENT.speed << 3) + (speed << 10)); + int amplitude = 40 + (SEGMENT.custom1 >> 2); + int index = (SEGMENT.custom3 > 5) ? (PartSys->usedParticles / 2) : 0; // center or start particle + PartSys->particles[index].vx += amplitude; + } + } + else if (SEGMENT.custom3 <= 30) { // sinusoidal wave: 11-20 apply at start, 21-30 apply at center + int index = (SEGMENT.custom3 > 20) ? (PartSys->usedParticles / 2) : 0; // center or start particle + int restposition = 0; + if (index > 0) restposition = PartSys->maxX >> 1; // center + //int amplitude = 5 + (SEGMENT.speed >> 3) + (SEGMENT.custom1 >> 2); // amplitude depends on density + int amplitude = 5 + (SEGMENT.custom1 >> 2); // amplitude depends on density + int speed = SEGMENT.custom3 - 10 - (index ? 10 : 0); // map 11-20 and 21-30 to 1-10 + int phase = strip.now * ((1 + (SEGMENT.speed >> 4)) * speed); + if (SEGMENT.check2) amplitude <<= 1; // double amplitude for XL particles + //PartSys->applyForce(PartSys->particles[index], (sin16_t(phase) * amplitude) >> 15, PartSys->advPartProps[index].forcecounter); // apply acceleration + PartSys->particles[index].x = restposition + ((sin16_t(phase) * amplitude) >> 12); // apply position + } + else { + if (hw_random16() < 656) { // ~1% chance to add a pulse + int amplitude = 60; + if (SEGMENT.check2) amplitude <<= 1; // double amplitude for XL particles + PartSys->particles[PartSys->usedParticles >> 1].vx += hw_random16(amplitude << 1) - amplitude; // apply acceleration + } + } + } + + for (int32_t i = 0; i < PartSys->usedParticles; i++) { + if (SEGMENT.custom2 == 255) { // map speed to hue + int speedclr = ((int8_t(abs(PartSys->particles[i].vx))) >> 2) << 4; // scale for greater color variation, dump small values to avoid flickering + //int speed = PartSys->particles[i].vx << 2; // +/- 512 + if (speedclr > 240) speedclr = 240; // limit color to non-wrapping part of palette + PartSys->particles[i].hue = speedclr; + } + else if (SEGMENT.custom2 > 0) + PartSys->particles[i].hue = i * (SEGMENT.custom2 >> 2); // gradient distribution + else { + // map hue to particle density + int deviation; + if (i == 0) // First particle: measure density based on distance to anchor point + deviation = springlength/2 - PartSys->particles[i].x; + else if (i == PartSys->usedParticles - 1) // Last particle: measure density based on distance to right boundary + deviation = springlength/2 - (PartSys->maxX - PartSys->particles[i].x); + else { + // Middle particles: average of compression/expansion from both sides + int leftDx = PartSys->particles[i].x - PartSys->particles[i-1].x; + int rightDx = PartSys->particles[i+1].x - PartSys->particles[i].x; + int avgDistance = (leftDx + rightDx) >> 1; + if (avgDistance < 0) avgDistance = 0; // avoid negative distances (not sure why this happens) + deviation = (springlength - avgDistance); + } + deviation = constrain(deviation, -127, 112); // limit deviation to -127..112 (do not go intwo wrapping part of palette) + PartSys->particles[i].hue = 127 + deviation; // map density to hue + } + } + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PS_SPRINGY[] PROGMEM = "PS Springy@Stiffness,Damping,Density,Hue,Mode,Smear,XL,AR;,!;!;1f;pal=54,c2=0,c3=23"; + #endif // WLED_DISABLE_PARTICLESYSTEM1D ////////////////////////////////////////////////////////////////////////////////////////// @@ -10385,7 +10678,9 @@ addEffect(FX_MODE_PSCHASE, &mode_particleChase, _data_FX_MODE_PS_CHASE); addEffect(FX_MODE_PSSTARBURST, &mode_particleStarburst, _data_FX_MODE_PS_STARBURST); addEffect(FX_MODE_PS1DGEQ, &mode_particle1DGEQ, _data_FX_MODE_PS_1D_GEQ); addEffect(FX_MODE_PSFIRE1D, &mode_particleFire1D, _data_FX_MODE_PS_FIRE1D); -addEffect(FX_MODE_PS1DSONICSTREAM, &mode_particle1Dsonicstream, _data_FX_MODE_PS_SONICSTREAM); +addEffect(FX_MODE_PS1DSONICSTREAM, &mode_particle1DsonicStream, _data_FX_MODE_PS_SONICSTREAM); +addEffect(FX_MODE_PS1DSONICBOOM, &mode_particle1DsonicBoom, _data_FX_MODE_PS_SONICBOOM); +addEffect(FX_MODE_PS1DSPRINGY, &mode_particleSpringy, _data_FX_MODE_PS_SPRINGY); #endif // WLED_DISABLE_PARTICLESYSTEM1D } diff --git a/wled00/FX.h b/wled00/FX.h index 1f8a315a6..6481ff757 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -351,7 +351,9 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_PS1DGEQ 212 #define FX_MODE_PSFIRE1D 213 #define FX_MODE_PS1DSONICSTREAM 214 -#define MODE_COUNT 215 +#define FX_MODE_PS1DSONICBOOM 215 +#define FX_MODE_PS1DSPRINGY 216 +#define MODE_COUNT 217 #define BLEND_STYLE_FADE 0x00 // universal diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index fde07be76..cff534256 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -18,8 +18,8 @@ // local shared functions (used both in 1D and 2D system) static int32_t calcForce_dv(const int8_t force, uint8_t &counter); static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap); // returns false if out of bounds by more than particleradius -static void fast_color_add(CRGB &c1, const CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) -static void fast_color_scale(CRGB &c, const uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 +static void fast_color_add(CRGB &c1, const CRGB &c2, uint8_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) +static void fast_color_scale(CRGB &c, const uint8_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 //static CRGB *allocateCRGBbuffer(uint32_t length); // global variables for memory management @@ -73,7 +73,7 @@ void ParticleSystem2D::update(void) { //update size settings before handling collisions if (advPartSize) { for (uint32_t i = 0; i < usedParticles; i++) { - if(updateSize(&advPartProps[i], &advPartSize[i]) == false) { // if particle shrinks to 0 size + if (updateSize(&advPartProps[i], &advPartSize[i]) == false) { // if particle shrinks to 0 size particles[i].ttl = 0; // kill particle } } @@ -170,7 +170,7 @@ void ParticleSystem2D::setSmearBlur(uint8_t bluramount) { void ParticleSystem2D::setParticleSize(uint8_t size) { particlesize = size; particleHardRadius = PS_P_MINHARDRADIUS; // ~1 pixel - if(particlesize > 1) { + if (particlesize > 1) { particleHardRadius = max(particleHardRadius, (uint32_t)particlesize); // radius used for wall collisions & particle collisions motionBlur = 0; // disable motion blur if particle size is set } @@ -226,7 +226,7 @@ int32_t ParticleSystem2D::sprayEmit(const PSsource &emitter) { // Spray emitter for particles used for flames (particle TTL depends on source TTL) void ParticleSystem2D::flameEmit(const PSsource &emitter) { int emitIndex = sprayEmit(emitter); - if(emitIndex > 0) particles[emitIndex].ttl += emitter.source.ttl; + if (emitIndex > 0) particles[emitIndex].ttl += emitter.source.ttl; } // Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var @@ -268,7 +268,7 @@ void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSparticleFlags &par } } - if(!checkBoundsAndWrap(newY, maxY, renderradius, options->wrapY)) { // check out of bounds note: this must not be skipped. if gravity is enabled, particles will never bounce at the top + if (!checkBoundsAndWrap(newY, maxY, renderradius, options->wrapY)) { // check out of bounds note: this must not be skipped. if gravity is enabled, particles will never bounce at the top partFlags.outofbounds = true; if (options->killoutofbounds) { if (newY < 0) // if gravity is enabled, only kill particles below ground @@ -278,12 +278,12 @@ void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSparticleFlags &par } } - if(part.ttl) { //check x direction only if still alive + if (part.ttl) { //check x direction only if still alive if (options->bounceX) { if ((newX < (int32_t)particleHardRadius) || (newX > (int32_t)(maxX - particleHardRadius))) // reached a wall bounce(part.vx, part.vy, newX, maxX); } - else if(!checkBoundsAndWrap(newX, maxX, renderradius, options->wrapX)) { // check out of bounds + else if (!checkBoundsAndWrap(newX, maxX, renderradius, options->wrapX)) { // check out of bounds partFlags.outofbounds = true; if (options->killoutofbounds) part.ttl = 0; @@ -387,14 +387,14 @@ void ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeCon return; int32_t size = advprops->size; int32_t asymdir = advsize->asymdir; - int32_t deviation = ((uint32_t)size * (uint32_t)advsize->asymmetry) / 255; // deviation from symmetrical size + int32_t deviation = ((uint32_t)size * (uint32_t)advsize->asymmetry + 255) >> 8; // deviation from symmetrical size // Calculate x and y size based on deviation and direction (0 is symmetrical, 64 is x, 128 is symmetrical, 192 is y) if (asymdir < 64) { - deviation = (asymdir * deviation) / 64; + deviation = (asymdir * deviation) >> 6; } else if (asymdir < 192) { - deviation = ((128 - asymdir) * deviation) / 64; + deviation = ((128 - asymdir) * deviation) >> 6; } else { - deviation = ((asymdir - 255) * deviation) / 64; + deviation = ((asymdir - 255) * deviation) >> 6; } // Calculate x and y size based on deviation, limit to 255 (rendering function cannot handle larger sizes) xsize = min((size - deviation), (int32_t)255); @@ -404,7 +404,7 @@ void ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeCon // function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) void ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition) { incomingspeed = -incomingspeed; - incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + incomingspeed = (incomingspeed * wallHardness + 128) >> 8; // reduce speed as energy is lost on non-hard surface if (position < (int32_t)particleHardRadius) position = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better else @@ -491,7 +491,7 @@ void ParticleSystem2D::applyAngleForce(const int8_t force, const uint16_t angle) // note: faster than apply force since direction is always down and counter is fixed for all particles void ParticleSystem2D::applyGravity() { int32_t dv = calcForce_dv(gforce, gforcecounter); - if(dv == 0) return; + if (dv == 0) return; for (uint32_t i = 0; i < usedParticles; i++) { // Note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways particles[i].vy = limitSpeed((int32_t)particles[i].vy - dv); @@ -574,7 +574,7 @@ void ParticleSystem2D::pointAttractor(const uint32_t particleindex, PSparticle & // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds // firemode is only used for PS Fire FX void ParticleSystem2D::ParticleSys_render() { - if(blendingStyle == BLEND_STYLE_FADE && SEGMENT.isInTransition() && lastRender + (strip.getFrameTime() >> 1) > strip.now) // fixes speedup during transitions TODO: find a better solution + if (blendingStyle == BLEND_STYLE_FADE && SEGMENT.isInTransition() && lastRender + (strip.getFrameTime() >> 1) > strip.now) // fixes speedup during transitions TODO: find a better solution return; lastRender = strip.now; CRGB baseRGB; @@ -586,24 +586,24 @@ void ParticleSystem2D::ParticleSys_render() { // update global blur (used for blur transitions) int32_t motionbluramount = motionBlur; int32_t smearamount = smearBlur; - if(pmem->inTransition == effectID && blendingStyle == BLEND_STYLE_FADE) { // FX transition and this is the new FX: fade blur amount but only if using fade style + if (pmem->inTransition == effectID && blendingStyle == BLEND_STYLE_FADE) { // FX transition and this is the new FX: fade blur amount but only if using fade style motionbluramount = globalBlur + (((motionbluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions - smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); + smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); } globalBlur = motionbluramount; globalSmear = smearamount; - if(isOverlay) { + if (isOverlay) { globalSmear = 0; // do not apply smear or blur in overlay or it turns everything into a blurry mess globalBlur = 0; } // handle blurring and framebuffer update if (framebuffer) { - if(!pmem->inTransition) + if (!pmem->inTransition) useAdditiveTransfer = false; // additive transfer is only usd in transitions (or in overlay) // handle buffer blurring or clearing bool bufferNeedsUpdate = !pmem->inTransition || pmem->inTransition == effectID || isNonFadeTransition; // not a transition; or new FX or not fading style: update buffer (blur, or clear) - if(bufferNeedsUpdate) { + if (bufferNeedsUpdate) { bool loadfromSegment = !renderSolo || isNonFadeTransition; if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) for (int32_t y = 0; y <= maxYpixel; y++) { @@ -622,8 +622,8 @@ void ParticleSystem2D::ParticleSys_render() { } } // handle buffer for global large particle size rendering - if(particlesize > 1 && pmem->inTransition) { // if particle size is used by FX we need a clean buffer - if(bufferNeedsUpdate && !globalBlur) { // transfer without adding if buffer was not cleared above (happens if this is the new FX and other FX does not use blurring) + if (particlesize > 1 && pmem->inTransition) { // if particle size is used by FX we need a clean buffer + if (bufferNeedsUpdate && !globalBlur) { // transfer without adding if buffer was not cleared above (happens if this is the new FX and other FX does not use blurring) useAdditiveTransfer = false; // no blurring and big size particle FX is the new FX (rendered first after clearing), can just render normally } else { // this is the old FX (rendering second) or blurring is active: new FX already rendered to the buffer and blurring was applied above; transfer it to segment and clear it @@ -682,7 +682,7 @@ void ParticleSystem2D::ParticleSys_render() { } } // apply 2D blur to rendered frame - if(globalSmear > 0) { + if (globalSmear > 0) { if (framebuffer) blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, globalSmear, globalSmear); else @@ -694,8 +694,8 @@ void ParticleSystem2D::ParticleSys_render() { } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer -void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB& color, const bool wrapX, const bool wrapY) { - if(particlesize == 0) { // single pixel rendering +void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB& color, const bool wrapX, const bool wrapY) { + if (particlesize == 0) { // single pixel rendering uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT; uint32_t y = particles[particleindex].y >> PS_P_RADIUS_SHIFT; if (x <= (uint32_t)maxXpixel && y <= (uint32_t)maxYpixel) { @@ -706,7 +706,7 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32 } return; } - int32_t pxlbrightness[4]; // brightness values for the four pixels representing a particle + uint8_t pxlbrightness[4]; // brightness values for the four pixels representing a particle int32_t pixco[4][2]; // physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs bool pixelvalid[4] = {true, true, true, true}; // is set to false if pixel is out of bounds bool advancedrender = false; // rendering for advanced particles @@ -765,7 +765,7 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32 } maxsize = maxsize/64 + 1; // number of blur passes depends on maxsize, four passes max uint32_t bitshift = 0; - for(uint32_t i = 0; i < maxsize; i++) { + for (uint32_t i = 0; i < maxsize; i++) { if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) bitshift = 1; rendersize += 2; @@ -911,9 +911,9 @@ void ParticleSystem2D::handleCollisions() { collDistSq = (particleHardRadius << 1) + (((uint32_t)advPartProps[idx_i].size + (uint32_t)advPartProps[idx_j].size) >> 1); // collision distance note: not 100% clear why the >> 1 is needed, but it is. collDistSq = collDistSq * collDistSq; // square it for faster comparison } - int32_t dx = particles[idx_j].x - particles[idx_i].x; + int32_t dx = (particles[idx_j].x + particles[idx_j].vx) - (particles[idx_i].x + particles[idx_i].vx); // distance with lookahead if (dx * dx < collDistSq) { // check x direction, if close, check y direction (squaring is faster than abs() or dual compare) - int32_t dy = particles[idx_j].y - particles[idx_i].y; + int32_t dy = (particles[idx_j].y + particles[idx_j].vy) - (particles[idx_i].y + particles[idx_i].vy); // distance with lookahead if (dy * dy < collDistSq) // particles are close collideParticles(particles[idx_i], particles[idx_j], dx, dy, collDistSq); } @@ -927,7 +927,7 @@ void ParticleSystem2D::handleCollisions() { // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const int32_t collDistSq) { int32_t distanceSquared = dx * dx + dy * dy; - // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) + // Calculate relative velocity note: could zero check but that does not improve overall speed but deminish it as that is rarely the case and pushing is still required int32_t relativeVx = (int32_t)particle2.vx - (int32_t)particle1.vx; int32_t relativeVy = (int32_t)particle2.vy - (int32_t)particle1.vy; @@ -992,7 +992,7 @@ void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &parti // tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way // when hard pushing by offsetting position, they sink into each other under gravity // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required - if(distanceSquared < collDistSq && dotProduct > -250) { // too close and also slow, push them apart + if (distanceSquared < collDistSq && dotProduct > -250) { // too close and also slow, push them apart int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are int32_t push = 0; @@ -1100,15 +1100,15 @@ void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, u width = 10; // buffer size is 10x10 } - for(uint32_t y = ystart; y < ystart + ysize; y++) { + for (uint32_t y = ystart; y < ystart + ysize; y++) { carryover = BLACK; uint32_t indexXY = xstart + y * width; - for(uint32_t x = xstart; x < xstart + xsize; x++) { + for (uint32_t x = xstart; x < xstart + xsize; x++) { seeppart = colorbuffer[indexXY]; // create copy of current color fast_color_scale(seeppart, seep); // scale it and seep to neighbours if (x > 0) { fast_color_add(colorbuffer[indexXY - 1], seeppart); - if(carryover) // note: check adds overhead but is faster on average + if (carryover) // note: check adds overhead but is faster on average fast_color_add(colorbuffer[indexXY], carryover); } carryover = seeppart; @@ -1122,15 +1122,15 @@ void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, u } seep = yblur >> 1; - for(uint32_t x = xstart; x < xstart + xsize; x++) { + for (uint32_t x = xstart; x < xstart + xsize; x++) { carryover = BLACK; uint32_t indexXY = x + ystart * width; - for(uint32_t y = ystart; y < ystart + ysize; y++) { + for (uint32_t y = ystart; y < ystart + ysize; y++) { seeppart = colorbuffer[indexXY]; // create copy of current color fast_color_scale(seeppart, seep); // scale it and seep to neighbours if (y > 0) { fast_color_add(colorbuffer[indexXY - width], seeppart); - if(carryover) // note: check adds overhead but is faster on average + if (carryover) // note: check adds overhead but is faster on average fast_color_add(colorbuffer[indexXY], carryover); } carryover = seeppart; @@ -1181,7 +1181,7 @@ bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, PSPRINTLN("PS 2D alloc"); uint32_t requiredmemory = sizeof(ParticleSystem2D); uint32_t dummy; // dummy variable - if((particleMemoryManager(numparticles, sizeof(PSparticle), dummy, dummy, SEGMENT.mode)) == nullptr) // allocate memory for particles + if ((particleMemoryManager(numparticles, sizeof(PSparticle), dummy, dummy, SEGMENT.mode)) == nullptr) // allocate memory for particles return false; // not enough memory, function ensures a minimum of numparticles are available // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) @@ -1199,12 +1199,12 @@ bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t additionalbytes, bool advanced, bool sizecontrol) { PSPRINT("PS 2D init "); - if(!strip.isMatrix) return false; // only for 2D + if (!strip.isMatrix) return false; // only for 2D uint32_t cols = SEGMENT.virtualWidth(); uint32_t rows = SEGMENT.virtualHeight(); uint32_t pixels = cols * rows; - if(advanced) + if (advanced) updateRenderingBuffer(100, false, true); // allocate a 10x10 buffer for rendering advanced particles uint32_t numparticles = calculateNumberOfParticles2D(pixels, advanced, sizecontrol); PSPRINT(" segmentsize:" + String(cols) + " " + String(rows)); @@ -1218,7 +1218,7 @@ bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, PartSys = new (SEGENV.data) ParticleSystem2D(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor updateRenderingBuffer(SEGMENT.vWidth() * SEGMENT.vHeight(), true, true); // update or create rendering buffer note: for fragmentation it might be better to allocate this first, but if memory is scarce, system has a buffer but no particles and will return false - + PSPRINTLN("******init done, pointers:"); #ifdef WLED_DEBUG_PS PSPRINT("framebfr size:"); @@ -1249,7 +1249,7 @@ ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, fractionOfParticlesUsed = 255; // use all particles by default advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared) //advPartSize = nullptr; - updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) + updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) setSize(length); setWallHardness(255); // set default wall hardness to max setGravity(0); //gravity disabled by default @@ -1264,7 +1264,7 @@ ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, sources[i].source.ttl = 1; //set source alive } - if(isadvanced) { + if (isadvanced) { for (uint32_t i = 0; i < numParticles; i++) { advPartProps[i].sat = 255; // set full saturation (for particles that are transferred from non-advanced system) } @@ -1522,7 +1522,6 @@ void ParticleSystem1D::applyFriction(int32_t coefficient) { particles[i].vx = ((int32_t)particles[i].vx * friction) / 255; } #endif - } @@ -1530,7 +1529,7 @@ void ParticleSystem1D::applyFriction(int32_t coefficient) { // if wrap is set, particles half out of bounds are rendered to the other side of the matrix // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds void ParticleSystem1D::ParticleSys_render() { - if(blendingStyle == BLEND_STYLE_FADE && SEGMENT.isInTransition() && lastRender + (strip.getFrameTime() >> 1) > strip.now) // fixes speedup during transitions TODO: find a better solution + if (blendingStyle == BLEND_STYLE_FADE && SEGMENT.isInTransition() && lastRender + (strip.getFrameTime() >> 1) > strip.now) // fixes speedup during transitions TODO: find a better solution return; lastRender = strip.now; CRGB baseRGB; @@ -1542,7 +1541,7 @@ void ParticleSystem1D::ParticleSys_render() { // update global blur (used for blur transitions) int32_t motionbluramount = motionBlur; int32_t smearamount = smearBlur; - if(pmem->inTransition == effectID) { // FX transition and this is the new FX: fade blur amount + if (pmem->inTransition == effectID) { // FX transition and this is the new FX: fade blur amount motionbluramount = globalBlur + (((motionbluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); } @@ -1552,7 +1551,7 @@ void ParticleSystem1D::ParticleSys_render() { if (framebuffer) { // handle buffer blurring or clearing bool bufferNeedsUpdate = !pmem->inTransition || pmem->inTransition == effectID || isNonFadeTransition; // not a transition; or new FX: update buffer (blur, or clear) - if(bufferNeedsUpdate) { + if (bufferNeedsUpdate) { bool loadfromSegment = !renderSolo || isNonFadeTransition; if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) for (int32_t x = 0; x <= maxXpixel; x++) { @@ -1595,7 +1594,7 @@ void ParticleSystem1D::ParticleSys_render() { renderParticle(i, brightness, baseRGB, particlesettings.wrap); } // apply smear-blur to rendered frame - if(globalSmear > 0) { + if (globalSmear > 0) { if (framebuffer) blur1D(framebuffer, maxXpixel + 1, globalSmear, 0); else @@ -1605,7 +1604,7 @@ void ParticleSystem1D::ParticleSys_render() { // add background color uint32_t bg_color = SEGCOLOR(1); if (bg_color > 0) { //if not black - for(int32_t i = 0; i <= maxXpixel; i++) { + for (int32_t i = 0; i <= maxXpixel; i++) { if (framebuffer) fast_color_add(framebuffer[i], bg_color); else @@ -1618,9 +1617,9 @@ void ParticleSystem1D::ParticleSys_render() { } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer -void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB &color, const bool wrap) { +void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB &color, const bool wrap) { uint32_t size = particlesize; - if (advPartProps) {// use advanced size properties + if (advPartProps) { // use advanced size properties size = advPartProps[particleindex].size; } if (size == 0) { //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles (and updating it uses more code) @@ -1629,7 +1628,7 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32 if (framebuffer) fast_color_add(framebuffer[x], color, brightness); else - SEGMENT.addPixelColor(x, color.scale8((uint8_t)brightness), true); + SEGMENT.addPixelColor(x, color.scale8(brightness), true); } return; } @@ -1715,7 +1714,7 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32 else pxlisinframe[1] = false; } - for(uint32_t i = 0; i < 2; i++) { + for (uint32_t i = 0; i < 2; i++) { if (pxlisinframe[i]) { if (framebuffer) fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]); @@ -1736,7 +1735,7 @@ void ParticleSystem1D::handleCollisions() { int32_t overlap = particleHardRadius << 1; // overlap bins to include edge particles to neighbouring bins if (advPartProps) //may be using individual particle size overlap += 256; // add 2 * max radius (approximately) - uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 4); // do not bin small amounts, limit max to 1/2 of particles + uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 4); // do not bin small amounts, limit max to 1/4 of particles uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // calculate number of bins uint16_t binIndices[maxBinParticles]; // array to store indices of particles in a bin uint32_t binParticleCount; // number of particles in the current bin @@ -1767,15 +1766,12 @@ void ParticleSystem1D::handleCollisions() { for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles uint32_t idx_j = binIndices[j]; if (advPartProps) { // use advanced size properties - collisiondistance = (PS_P_MINHARDRADIUS_1D << particlesize) + (((uint32_t)advPartProps[idx_i].size + (uint32_t)advPartProps[idx_j].size) >> 1); + collisiondistance = (PS_P_MINHARDRADIUS_1D << particlesize) + ((advPartProps[idx_i].size + advPartProps[idx_j].size) >> 1); } - int32_t dx = particles[idx_j].x - particles[idx_i].x; - int32_t dv = (int32_t)particles[idx_j].vx - (int32_t)particles[idx_i].vx; - int32_t proximity = collisiondistance; - if (dv >= proximity) // particles would go past each other in next move update - proximity += abs(dv); // add speed difference to catch fast particles - if (dx <= proximity && dx >= -proximity) { // collide if close - collideParticles(particles[idx_i], particleFlags[idx_i], particles[idx_j], particleFlags[idx_j], dx, dv, collisiondistance); + int32_t dx = (particles[idx_j].x + particles[idx_j].vx) - (particles[idx_i].x + particles[idx_i].vx); // distance between particles with lookahead + uint32_t dx_abs = abs(dx); + if (dx_abs <= collisiondistance) { // collide if close + collideParticles(particles[idx_i], particleFlags[idx_i], particles[idx_j], particleFlags[idx_j], dx, dx_abs, collisiondistance); } } } @@ -1784,13 +1780,18 @@ void ParticleSystem1D::handleCollisions() { } // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) -void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, int32_t dx, int32_t relativeVx, const int32_t collisiondistance) { - int32_t dotProduct = (dx * relativeVx); // is always negative if moving towards each other - uint32_t distance = abs(dx); +void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const int32_t collisiondistance) { + int32_t dv = particle2.vx - particle1.vx; + int32_t dotProduct = (dx * dv); // is always negative if moving towards each other + if (dotProduct < 0) { // particles are moving towards each other uint32_t surfacehardness = max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS_1D); // if particles are soft, the impulse must stay above a limit or collisions slip through - // Calculate new velocities after collision - int32_t impulse = relativeVx * surfacehardness / 255; // note: not using dot product like in 2D as impulse is purely speed depnedent + // Calculate new velocities after collision note: not using dot product like in 2D as impulse is purely speed depnedent + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + int32_t impulse = ((dv * surfacehardness) + ((dv >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts + #else // division is faster on ESP32, S2 and S3 + int32_t impulse = (dv * surfacehardness) / 255; + #endif particle1.vx += impulse; particle2.vx -= impulse; @@ -1802,13 +1803,17 @@ void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticl if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D && (SEGMENT.call & 0x07) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction const uint32_t coeff = collisionHardness + (250 - PS_P_MINSURFACEHARDNESS_1D); + #if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(ESP8266) // use bitshifts with rounding instead of division (2x faster) + particle1.vx = ((int32_t)particle1.vx * coeff + (((int32_t)particle1.vx >> 31) & 0xFF)) >> 8; // note: (v>>31) & 0xFF)) extracts the sign and adds 255 if negative for correct rounding using shifts + particle2.vx = ((int32_t)particle2.vx * coeff + (((int32_t)particle2.vx >> 31) & 0xFF)) >> 8; + #else // division is faster on ESP32, S2 and S3 particle1.vx = ((int32_t)particle1.vx * coeff) / 255; particle2.vx = ((int32_t)particle2.vx * coeff) / 255; + #endif } } - if (distance < (collisiondistance - 8) && abs(relativeVx) < 5) // overlapping and moving slowly - { + if (dx_abs < (collisiondistance - 8) && abs(dv) < 5) { // overlapping and moving slowly // particles have volume, push particles apart if they are too close // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle // note: like in 2D, pushing by a distance makes softer piles collapse, giving particles speed prevents that and looks nicer @@ -1818,10 +1823,10 @@ void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticl particle1.vx -= pushamount; particle2.vx += pushamount; - if(distance < collisiondistance >> 1) { // too close, force push particles so they dont collapse - pushamount = 1 + ((collisiondistance - distance) >> 3); // note: push amount found by experimentation + if (dx_abs < collisiondistance >> 1) { // too close, force push particles so they dont collapse + pushamount = 1 + ((collisiondistance - dx_abs) >> 3); // note: push amount found by experimentation - if(particle1.x < (maxX >> 1)) { // lower half, push particle with larger x in positive direction + if (particle1.x < (maxX >> 1)) { // lower half, push particle with larger x in positive direction if (dx < 0 && !particle1flags.fixed) { // particle2.x < particle1.x -> push particle 1 particle1.vx++;// += pushamount; particle1.x += pushamount; @@ -1836,7 +1841,7 @@ void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticl particle2.vx--;// -= pushamount; particle2.x -= pushamount; } - else if (!particle2flags.fixed) { // particle1.x < particle2.x -> push particle 1 + else if (!particle1flags.fixed) { // particle1.x < particle2.x -> push particle 1 particle1.vx--;// -= pushamount; particle1.x -= pushamount; } @@ -1925,7 +1930,7 @@ uint32_t calculateNumberOfSources1D(const uint32_t requestedsources) { bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes) { uint32_t requiredmemory = sizeof(ParticleSystem1D); uint32_t dummy; // dummy variable - if(particleMemoryManager(numparticles, sizeof(PSparticle1D), dummy, dummy, SEGMENT.mode) == nullptr) // allocate memory for particles + if (particleMemoryManager(numparticles, sizeof(PSparticle1D), dummy, dummy, SEGMENT.mode) == nullptr) // allocate memory for particles return false; // not enough memory, function ensures a minimum of numparticles are avialable // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) requiredmemory += sizeof(PSparticleFlags1D) * numparticles; @@ -1940,7 +1945,7 @@ bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t // note: percentofparticles is in uint8_t, for example 191 means 75%, (deafaults to 255 or 100% meaning one particle per pixel), can be more than 100% (but not recommended, can cause out of memory) bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles, const uint32_t additionalbytes, const bool advanced) { if (SEGLEN == 1) return false; // single pixel not supported - if(advanced) + if (advanced) updateRenderingBuffer(10, false, true); // buffer for advanced particles, fixed size uint32_t numparticles = calculateNumberOfParticles1D(fractionofparticles, advanced); uint32_t numsources = calculateNumberOfSources1D(requestedsources); @@ -1961,12 +1966,12 @@ void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start) CRGB seeppart, carryover; uint32_t seep = blur >> 1; carryover = BLACK; - for(uint32_t x = start; x < start + size; x++) { + for (uint32_t x = start; x < start + size; x++) { seeppart = colorbuffer[x]; // create copy of current color fast_color_scale(seeppart, seep); // scale it and seep to neighbours if (x > 0) { fast_color_add(colorbuffer[x-1], seeppart); - if(carryover) // note: check adds overhead but is faster on average + if (carryover) // note: check adds overhead but is faster on average fast_color_add(colorbuffer[x], carryover); // is black on first pass } carryover = seeppart; @@ -2022,7 +2027,7 @@ static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32 // note: result is stored in c1, not using a return value is faster as the CRGB struct does not need to be copied upon return // note2: function is mainly used to add scaled colors, so checking if one color is black is slower // note3: scale is 255 when using blur, checking for that makes blur faster -static void fast_color_add(CRGB &c1, const CRGB &c2, const uint32_t scale) { + __attribute__((optimize("O2"))) static void fast_color_add(CRGB &c1, const CRGB &c2, const uint8_t scale) { uint32_t r, g, b; if (scale < 255) { r = c1.r + ((c2.r * scale) >> 8); @@ -2034,9 +2039,9 @@ static void fast_color_add(CRGB &c1, const CRGB &c2, const uint32_t scale) { b = c1.b + c2.b; } - uint32_t max = std::max(r,g); // check for overflow, using max() is faster as the compiler can optimize - max = std::max(max,b); - if (max < 256) { + // note: this chained comparison is the fastest method for max of 3 values (faster than std:max() or using xor) + uint32_t max = (r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b); + if (max <= 255) { c1.r = r; // save result to c1 c1.g = g; c1.b = b; @@ -2049,7 +2054,7 @@ static void fast_color_add(CRGB &c1, const CRGB &c2, const uint32_t scale) { } // faster than fastled color scaling as it does in place scaling -static void fast_color_scale(CRGB &c, const uint32_t scale) { + __attribute__((optimize("O2"))) static void fast_color_scale(CRGB &c, const uint8_t scale) { c.r = ((c.r * scale) >> 8); c.g = ((c.g * scale) >> 8); c.b = ((c.b * scale) >> 8); @@ -2090,7 +2095,7 @@ void* allocatePSmemory(size_t size, bool overridelimit) { // deallocate memory and update data usage, use with care! void deallocatePSmemory(void* dataptr, uint32_t size) { PSPRINTLN("deallocating PSmemory:" + String(size)); - if(dataptr == nullptr) return; // safety check + if (dataptr == nullptr) return; // safety check free(dataptr); // note: setting pointer null must be done by caller, passing a reference to a cast void pointer is not possible Segment::addUsedSegmentData(size <= Segment::getUsedSegmentData() ? -size : -Segment::getUsedSegmentData()); } @@ -2120,7 +2125,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize } } if (pmem->watchdog == 1) { // if a PS already exists during particle request, it kicked the watchdog in last frame, servicePSmem() adds 1 afterwards -> PS to PS transition - if(pmem->currentFX == effectID) // if the new effect is the same as the current one, do not transition: transferParticles is set above, so this will transfer all particles back if called during transition + if (pmem->currentFX == effectID) // if the new effect is the same as the current one, do not transition: transferParticles is set above, so this will transfer all particles back if called during transition pmem->inTransition = false; // reset transition flag else pmem->inTransition = effectID; // save the ID of the new effect (required to determine blur amount in rendering function) @@ -2148,7 +2153,7 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize } // now we have a valid buffer, if this is a PS to PS FX transition: transfer particles slowly to new FX - if(!SEGMENT.isInTransition()) pmem->inTransition = false; // transition has ended, invoke final transfer + if (!SEGMENT.isInTransition()) pmem->inTransition = false; // transition has ended, invoke final transfer if (pmem->inTransition) { uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer uint16_t progress = SEGMENT.progress(); // transition progress @@ -2156,13 +2161,13 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize if (SEGMENT.mode == effectID) { // new effect ID -> function was called from new FX PSPRINTLN("new effect"); newAvailable = (maxParticles * progress) >> 16; // update total particles available to this PS (newAvailable is guaranteed to be smaller than maxParticles) - if(newAvailable < 2) newAvailable = 2; // give 2 particle minimum (some FX may crash with less as they do i+1 access) - if(newAvailable > numParticlesUsed) newAvailable = numParticlesUsed; // limit to number of particles used, do not move the pointer anymore (will be set to base in final handover) + if (newAvailable < 2) newAvailable = 2; // give 2 particle minimum (some FX may crash with less as they do i+1 access) + if (newAvailable > numParticlesUsed) newAvailable = numParticlesUsed; // limit to number of particles used, do not move the pointer anymore (will be set to base in final handover) uint32_t bufferoffset = (maxParticles - 1) - newAvailable; // offset to new effect particles (in particle structs, not bytes) - if(bufferoffset < maxParticles) // safety check + if (bufferoffset < maxParticles) // safety check buffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // new effect gets the end of the buffer int32_t totransfer = newAvailable - availableToPS; // number of particles to transfer in this transition update - if(totransfer > 0) // safety check + if (totransfer > 0) // safety check particleHandover(buffer, structSize, totransfer); } else { // this was called from the old FX @@ -2170,23 +2175,23 @@ void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize SEGMENT.loadOldPalette(); // load the old palette into segment palette progress = 0xFFFFU - progress; // inverted transition progress newAvailable = ((maxParticles * progress) >> 16); // result is guaranteed to be smaller than maxParticles - if(newAvailable > 0) newAvailable--; // -1 to avoid overlapping memory in 1D<->2D transitions - if(newAvailable < 2) newAvailable = 2; // give 2 particle minimum (some FX may crash with less as they do i+1 access) + if (newAvailable > 0) newAvailable--; // -1 to avoid overlapping memory in 1D<->2D transitions + if (newAvailable < 2) newAvailable = 2; // give 2 particle minimum (some FX may crash with less as they do i+1 access) // note: buffer pointer stays the same, number of available particles is reduced } availableToPS = newAvailable; - } else if(pmem->transferParticles) { // no PS transition, full buffer available + } else if (pmem->transferParticles) { // no PS transition, full buffer available // transition ended (or blending is disabled) -> transfer all remaining particles PSPRINTLN("PS transition ended, final particle handover"); uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer if (maxParticles > availableToPS) { // not all particles transferred yet uint32_t totransfer = maxParticles - availableToPS; // transfer all remaining particles - if(totransfer <= maxParticles) // safety check + if (totransfer <= maxParticles) // safety check particleHandover(buffer, structSize, totransfer); - if(maxParticles > numParticlesUsed) { // FX uses less than max: move the already existing particles to the beginning of the buffer + if (maxParticles > numParticlesUsed) { // FX uses less than max: move the already existing particles to the beginning of the buffer uint32_t usedbytes = availableToPS * structSize; int32_t bufferoffset = (maxParticles - 1) - availableToPS; // offset to existing particles (see above) - if(bufferoffset < (int)maxParticles) { // safety check + if (bufferoffset < (int)maxParticles) { // safety check void* currentBuffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // pointer to current buffer start memmove(buffer, currentBuffer, usedbytes); // move the existing particles to the beginning of the buffer } @@ -2243,7 +2248,7 @@ void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { PSparticle *particles = (PSparticle *)buffer; for (int32_t i = 0; i < numToTransfer; i++) { if (blendingStyle == BLEND_STYLE_FADE) { - if(particles[i].ttl > maxTTL) + if (particles[i].ttl > maxTTL) particles[i].ttl = maxTTL + hw_random16(150); // reduce TTL so it will die soon } else @@ -2258,7 +2263,7 @@ void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { PSparticle1D *particles = (PSparticle1D *)buffer; for (int32_t i = 0; i < numToTransfer; i++) { if (blendingStyle == BLEND_STYLE_FADE) { - if(particles[i].ttl > maxTTL) + if (particles[i].ttl > maxTTL) particles[i].ttl = maxTTL + hw_random16(150); // reduce TTL so it will die soon } else @@ -2285,7 +2290,7 @@ bool segmentIsOverlay(void) { // TODO: this only needs to be checked when segmen // Check for overlap with all previous segments for (unsigned i = 0; i < segID; i++) { - if(strip._segments[i].freeze) continue; // skip inactive segments + if (strip._segments[i].freeze) continue; // skip inactive segments unsigned startX = strip._segments[i].start; unsigned endX = strip._segments[i].stop; unsigned startY = strip._segments[i].startY; @@ -2316,15 +2321,15 @@ void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer, bool ini PSPRINTLN("updateRenderingBuffer"); uint16_t& targetBufferSize = isFramebuffer ? frameBufferSize : renderBufferSize; // corresponding buffer size - // if(isFramebuffer) return; // debug/testing only: disable frame-buffer + // if (isFramebuffer) return; // debug/testing only: disable frame-buffer - if(targetBufferSize < requiredpixels) { // check current buffer size + if (targetBufferSize < requiredpixels) { // check current buffer size CRGB** targetBuffer = isFramebuffer ? &framebuffer : &renderbuffer; // pointer to target buffer - if(*targetBuffer || initialize) { // update only if initilizing or if buffer exists (prevents repeatet allocation attempts if initial alloc failed) - if(*targetBuffer) // buffer exists, free it + if (*targetBuffer || initialize) { // update only if initilizing or if buffer exists (prevents repeatet allocation attempts if initial alloc failed) + if (*targetBuffer) // buffer exists, free it deallocatePSmemory((void*)(*targetBuffer), targetBufferSize * sizeof(CRGB)); *targetBuffer = reinterpret_cast(allocatePSmemory(requiredpixels * sizeof(CRGB), false)); - if(*targetBuffer) + if (*targetBuffer) targetBufferSize = requiredpixels; else targetBufferSize = 0; @@ -2336,10 +2341,10 @@ void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer, bool ini // note: doing it this way makes it independent of the implementation of segment management but is not the most memory efficient way void servicePSmem() { // Increment watchdog for each entry and deallocate if idle too long (i.e. no PS running on that segment) - if(partMemList.size() > 0) { + if (partMemList.size() > 0) { for (size_t i = 0; i < partMemList.size(); i++) { - if(strip.getSegmentsNum() > i) { // segment still exists - if(strip._segments[i].freeze) continue; // skip frozen segments (incrementing watchdog will delete memory, leading to crash) + if (strip.getSegmentsNum() > i) { // segment still exists + if (strip._segments[i].freeze) continue; // skip frozen segments (incrementing watchdog will delete memory, leading to crash) } partMemList[i].watchdog++; // Increment watchdog counter PSPRINT("pmem servic. list size: "); @@ -2356,12 +2361,12 @@ void servicePSmem() { } } else { // no particle system running, release buffer memory - if(framebuffer) { + if (framebuffer) { deallocatePSmemory((void*)framebuffer, frameBufferSize * sizeof(CRGB)); // free the buffers framebuffer = nullptr; frameBufferSize = 0; } - if(renderbuffer) { + if (renderbuffer) { deallocatePSmemory((void*)renderbuffer, renderBufferSize * sizeof(CRGB)); renderbuffer = nullptr; renderBufferSize = 0; @@ -2371,34 +2376,34 @@ void servicePSmem() { // transfer the frame buffer to the segment and handle transitional rendering (both FX render to the same buffer so they mix) void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { - if(!framebuffer) return; // no buffer, nothing to transfer + if (!framebuffer) return; // no buffer, nothing to transfer PSPRINT(" xfer buf "); #ifndef WLED_DISABLE_MODE_BLEND bool tempBlend = SEGMENT.getmodeBlend(); - if(pmem->inTransition && blendingStyle == BLEND_STYLE_FADE) { + if (pmem->inTransition && blendingStyle == BLEND_STYLE_FADE) { SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (using local buffer to do PS blending) } #endif - if(height) { // is 2D, 1D passes height = 0 + if (height) { // is 2D, 1D passes height = 0 for (uint32_t y = 0; y < height; y++) { int index = y * width; // current row index for 1D buffer for (uint32_t x = 0; x < width; x++) { CRGB *c = &framebuffer[index++]; uint32_t clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color - if(useAdditiveTransfer) { + if (useAdditiveTransfer) { uint32_t segmentcolor = SEGMENT.getPixelColorXY((int)x, (int)y); CRGB segmentRGB = CRGB(segmentcolor); - if(clr == 0) // frame buffer is black, just update the framebuffer + if (clr == 0) // frame buffer is black, just update the framebuffer *c = segmentRGB; else { // color to add to segment is not black - if(segmentcolor) { + if (segmentcolor) { fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) and set the segment } SEGMENT.setPixelColorXY((int)x, (int)y, clr); // save back to segment after adding local buffer } } - //if(clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requires proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. + //if (clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requires proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. else SEGMENT.setPixelColorXY((int)x, (int)y, clr); } @@ -2407,20 +2412,20 @@ void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { for (uint32_t x = 0; x < width; x++) { CRGB *c = &framebuffer[x]; uint32_t clr = RGBW32(c->r,c->g,c->b,0); - if(useAdditiveTransfer) { + if (useAdditiveTransfer) { uint32_t segmentcolor = SEGMENT.getPixelColor((int)x);; CRGB segmentRGB = CRGB(segmentcolor); - if(clr == 0) // frame buffer is black, just load the color (for next frame) + if (clr == 0) // frame buffer is black, just load the color (for next frame) *c = segmentRGB; else { // color to add to segment is not black - if(segmentcolor) { + if (segmentcolor) { fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) } SEGMENT.setPixelColor((int)x, clr); // save back to segment after adding local buffer } } - //if(color > 0) // not black + //if (color > 0) // not black else SEGMENT.setPixelColor((int)x, clr); } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index a91ebe25e..099b96a85 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -211,7 +211,7 @@ public: private: //rendering functions void ParticleSys_render(); - [[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB& color, const bool wrapX, const bool wrapY); + [[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB& color, const bool wrapX, const bool wrapY); //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); @@ -343,7 +343,7 @@ public: int32_t sprayEmit(const PSsource1D &emitter); void particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options = NULL, PSadvancedParticle1D *advancedproperties = NULL); // move function //particle physics - [[gnu::hot]] void applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter); //apply a force to a single particle + [[gnu::hot]] void applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter); //apply a force to a single particle void applyForce(const int8_t xforce); // apply a force to all particles void applyGravity(PSparticle1D &part, PSparticleFlags1D &partFlags); // applies gravity to single particle (use this for sources) void applyFriction(const int32_t coefficient); // apply friction to all used particles @@ -378,12 +378,12 @@ public: private: //rendering functions void ParticleSys_render(void); - void renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB &color, const bool wrap); + [[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB &color, const bool wrap); //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); - [[gnu::hot]] void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, int32_t dx, int32_t relativeVx, const int32_t collisiondistance); + [[gnu::hot]] void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const int32_t collisiondistance); //utility functions void updatePSpointers(const bool isadvanced); // update the data pointers to current segment data space From ca7d7d93691cc99047f8002cec97b564e20133ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Sun, 20 Apr 2025 11:01:19 +0200 Subject: [PATCH 053/153] Only disable Arduino OTA when using -D WLED_DISABLE_OTA - since Update object is accessible even with ArduinoOTA disabled it should possible to use HTTP OTA regardless - this saves about 12kB while still allowing OTA updates - HTTP OTA updates can be blocked using PIN or OTA lock --- wled00/wled_server.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 06750838f..3da3a7a99 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -365,7 +365,6 @@ void initServer() createEditHandler(correctPIN); static const char _update[] PROGMEM = "/update"; -#ifndef WLED_DISABLE_OTA //init ota page server.on(_update, HTTP_GET, [](AsyncWebServerRequest *request){ if (otaLock) { @@ -419,12 +418,6 @@ void initServer() } } }); -#else - server.on(_update, HTTP_GET, [](AsyncWebServerRequest *request){ - serveMessage(request, 501, FPSTR(s_notimplemented), F("OTA updating is disabled in this build."), 254); - }); -#endif - #ifdef WLED_ENABLE_DMX server.on(F("/dmxmap"), HTTP_GET, [](AsyncWebServerRequest *request){ From 353868414a2c8336d9c06259517d7f9b636323fa Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 20 Apr 2025 11:38:32 +0200 Subject: [PATCH 054/153] Removed PS memory manager and some minor improvements (#4651) * Removed memory manager from PS - reverted all changes related to memory manager - moved local buffer into effect data memory - some RAM issues may occur on larger setups: tested on S3 it works fine up to 32x32 but runs into memory issues at 64x32 * fixed ifdef, improved readability, add optimize "o2" flags to improve speed - added struct for x and y coordinates, thx to @blazoncek * cleanup and minor improvements - removed local buffer for ESP8266 in 1D system to save on RAM - increased particle brightness in PS Impact - minor tweak in collision binning (might improve speed) - removed comments and some other unused stuff - fixed a few compiler wranings * fixed init sequence bug --- wled00/FX.cpp | 87 ++-- wled00/FX_fcn.cpp | 3 - wled00/FXparticleSystem.cpp | 847 ++++++++---------------------------- wled00/FXparticleSystem.h | 52 +-- 4 files changed, 220 insertions(+), 769 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index f439e4c66..3daf2c0fd 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7998,7 +7998,7 @@ uint16_t mode_particlefire(void) { uint32_t i; // index variable uint32_t numFlames; // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results - if (SEGMENT.call == 0) { // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, SEGMENT.virtualWidth(), 4)) //maximum number of source (PS may limit based on segment size); need 4 additional bytes for time keeping (uint32_t lastcall) return mode_static(); // allocation failed or not 2D SEGENV.aux0 = hw_random16(); // aux0 is wind position (index) in the perlin noise @@ -8090,7 +8090,7 @@ uint16_t mode_particlepit(void) { ParticleSystem2D *PartSys = nullptr; if (SEGMENT.call == 0) { // initialization - if (!initParticleSystem2D(PartSys, 1, 0, true, false)) // init, request one source (actually dont really need one TODO: test if using zero sources also works) + if (!initParticleSystem2D(PartSys, 0, 0, true, false)) // init return mode_static(); // allocation failed or not 2D PartSys->setKillOutOfBounds(true); PartSys->setGravity(); // enable with default gravity @@ -8161,7 +8161,7 @@ uint16_t mode_particlewaterfall(void) { uint8_t numSprays; uint32_t i = 0; - if (SEGMENT.call == 0) { // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 12)) // init, request 12 sources, no additional data needed return mode_static(); // allocation failed or not 2D @@ -8184,7 +8184,7 @@ uint16_t mode_particlewaterfall(void) { else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) - return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + return mode_static(); // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -8313,7 +8313,7 @@ uint16_t mode_particleperlin(void) { ParticleSystem2D *PartSys = nullptr; uint32_t i; - if (SEGMENT.call == 0) { // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, 1, 0, true)) // init with 1 source and advanced properties return mode_static(); // allocation failed or not 2D @@ -8374,20 +8374,19 @@ static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed uint16_t mode_particleimpact(void) { ParticleSystem2D *PartSys = nullptr; uint32_t i = 0; - uint8_t MaxNumMeteors; + uint32_t numMeteors; PSsettings2D meteorsettings; meteorsettings.asByte = 0b00101000; // PS settings for meteors: bounceY and gravity enabled - if (SEGMENT.call == 0) { // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (SEGMENT.call == 0) { // initialization if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed return mode_static(); // allocation failed or not 2D PartSys->setKillOutOfBounds(true); PartSys->setGravity(); // enable default gravity PartSys->setBounceY(true); // always use ground bounce PartSys->setWallRoughness(220); // high roughness - MaxNumMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); - for (i = 0; i < MaxNumMeteors; i++) { - // PartSys->sources[i].source.y = 500; + numMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + for (i = 0; i < numMeteors; i++) { PartSys->sources[i].source.ttl = hw_random16(10 * i); // set initial delay for meteors PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched } @@ -8396,7 +8395,7 @@ uint16_t mode_particleimpact(void) { PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS if (PartSys == nullptr) - return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + return mode_static(); // something went wrong, no data! // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) @@ -8406,29 +8405,18 @@ uint16_t mode_particleimpact(void) { uint8_t hardness = map(SEGMENT.custom2, 0, 255, PS_P_MINSURFACEHARDNESS - 2, 255); PartSys->setWallHardness(hardness); PartSys->enableParticleCollisions(SEGMENT.check3, hardness); // enable collisions and set particle collision hardness - MaxNumMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); - uint8_t numMeteors = MaxNumMeteors; // TODO: clean this up map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation - + numMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); uint32_t emitparticles; // number of particles to emit for each rocket's state for (i = 0; i < numMeteors; i++) { // determine meteor state by its speed: - if ( PartSys->sources[i].source.vy < 0) { // moving down, emit sparks - #ifdef ESP8266 + if ( PartSys->sources[i].source.vy < 0) // moving down, emit sparks emitparticles = 1; - #else - emitparticles = 2; - #endif - } else if ( PartSys->sources[i].source.vy > 0) // moving up means meteor is on 'standby' emitparticles = 0; else { // speed is zero, explode! PartSys->sources[i].source.vy = 10; // set source speed positive so it goes into timeout and launches again - #ifdef ESP8266 - emitparticles = hw_random16(SEGMENT.intensity >> 3) + 5; // defines the size of the explosion - #else - emitparticles = map(SEGMENT.intensity, 0, 255, 10, hw_random16(PartSys->usedParticles>>2)); // defines the size of the explosion !!!TODO: check if this works on ESP8266, drop esp8266 def if it does - #endif + emitparticles = map(SEGMENT.intensity, 0, 255, 10, hw_random16(PartSys->usedParticles>>2)); // defines the size of the explosion } for (int e = emitparticles; e > 0; e--) { PartSys->sprayEmit(PartSys->sources[i]); @@ -8449,13 +8437,13 @@ uint16_t mode_particleimpact(void) { PartSys->sources[i].source.vx = 0; PartSys->sources[i].sourceFlags.collide = true; #ifdef ESP8266 - PartSys->sources[i].maxLife = 180; - PartSys->sources[i].minLife = 20; + PartSys->sources[i].maxLife = 900; + PartSys->sources[i].minLife = 100; #else - PartSys->sources[i].maxLife = 250; - PartSys->sources[i].minLife = 50; + PartSys->sources[i].maxLife = 1250; + PartSys->sources[i].minLife = 250; #endif - PartSys->sources[i].source.ttl = hw_random16((512 - (SEGMENT.speed << 1))) + 40; // standby time til next launch (in frames) + PartSys->sources[i].source.ttl = hw_random16((768 - (SEGMENT.speed << 1))) + 40; // standby time til next launch (in frames) PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y PartSys->sources[i].var = (SEGMENT.custom1 >> 2); // speed variation around vx,vy (+/- var) } @@ -8470,13 +8458,17 @@ uint16_t mode_particleimpact(void) { PartSys->sources[i].source.hue = hw_random16(); // random color PartSys->sources[i].source.ttl = 500; // long life, will explode at bottom PartSys->sources[i].sourceFlags.collide = false; // trail particles will not collide - PartSys->sources[i].maxLife = 60; // spark particle life - PartSys->sources[i].minLife = 20; + PartSys->sources[i].maxLife = 300; // spark particle life + PartSys->sources[i].minLife = 100; PartSys->sources[i].vy = -9; // emitting speed (down) PartSys->sources[i].var = 3; // speed variation around vx,vy (+/- var) } } + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl > 5) PartSys->particles[i].ttl -= 5; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + } + PartSys->update(); // update and render return FRAMETIME; } @@ -8880,7 +8872,7 @@ uint16_t mode_particleghostrider(void) { // emit two particles PartSys->angleEmit(PartSys->sources[0], emitangle, speed); PartSys->angleEmit(PartSys->sources[0], emitangle, speed); - if (SEGMENT.call % (11 - (SEGMENT.custom2 / 25)) == 0) { // every nth frame, cycle color and emit particles //TODO: make this a segment call % SEGMENT.custom2 for better control + if (SEGMENT.call % (11 - (SEGMENT.custom2 / 25)) == 0) { // every nth frame, cycle color and emit particles PartSys->sources[0].source.hue++; } if (SEGMENT.custom2 > 190) //fast color change @@ -8900,7 +8892,7 @@ uint16_t mode_particleblobs(void) { ParticleSystem2D *PartSys = nullptr; if (SEGMENT.call == 0) { - if (!initParticleSystem2D(PartSys, 1, 0, true, true)) //init, request one source, no additional bytes, advanced size & size control (actually dont really need one TODO: test if using zero sources also works) + if (!initParticleSystem2D(PartSys, 0, 0, true, true)) //init, no additional bytes, advanced size & size control return mode_static(); // allocation failed or not 2D PartSys->setBounceX(true); PartSys->setBounceY(true); @@ -9521,8 +9513,8 @@ uint16_t mode_particleHourglass(void) { uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7 - if ((SEGMENT.intensity | (PartSys->getAvailableParticles() << 8)) != *settingTracker) { // initialize, getAvailableParticles changes while in FX transition - *settingTracker = SEGMENT.intensity | (PartSys->getAvailableParticles() << 8); + if (SEGMENT.intensity != *settingTracker) { // initialize + *settingTracker = SEGMENT.intensity; for (uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particleFlags[i].reversegrav = true; // resting particles dont fall *direction = 0; // down @@ -9569,7 +9561,7 @@ uint16_t mode_particleHourglass(void) { } // re-order particles in case collisions flipped particles (highest number index particle is on the "bottom") - for (int i = 0; i < PartSys->usedParticles - 1; i++) { + for (uint32_t i = 0; i < PartSys->usedParticles - 1; i++) { if (PartSys->particles[i].x < PartSys->particles[i+1].x && PartSys->particleFlags[i].fixed == false && PartSys->particleFlags[i+1].fixed == false) { std::swap(PartSys->particles[i].x, PartSys->particles[i+1].x); } @@ -9680,10 +9672,7 @@ uint16_t mode_particleBalance(void) { if (SEGMENT.call == 0) { // initialization if (!initParticleSystem1D(PartSys, 1, 128)) // init, no additional data needed, use half of max particles return mode_static(); // allocation failed or is single pixel - //PartSys->setKillOutOfBounds(true); PartSys->setParticleSize(1); - SEGENV.aux0 = 0; - SEGENV.aux1 = 0; //TODO: really need to set to zero or is it calloc'd? } else PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS @@ -9777,7 +9766,7 @@ uint16_t mode_particleChase(void) { uint32_t numParticles = 1 + map(SEGMENT.intensity, 0, 255, 2, 255 / (1 + (SEGMENT.custom1 >> 6))); // depends on intensity and particle size (custom1), minimum 1 numParticles = min(numParticles, PartSys->usedParticles); // limit to available particles int32_t huestep = 1 + ((((uint32_t)SEGMENT.custom2 << 19) / numParticles) >> 16); // hue increment - uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3 + PartSys->getAvailableParticles(); // note: getAvailableParticles is used to enforce update during transitions + uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3; if (SEGENV.aux0 != settingssum) { // settings changed changed, update if (SEGMENT.check1) SEGENV.step = PartSys->advPartProps[0].size / 2 + (PartSys->maxX / numParticles); @@ -9835,7 +9824,7 @@ uint16_t mode_particleChase(void) { globalhuestep = 2; // global hue change to add some color variation if ((SEGMENT.call & 0x1F) == 0) SEGENV.step += *stepdir; // change density - for(int32_t i = 0; i < PartSys->usedParticles; i++) { + for(uint32_t i = 0; i < PartSys->usedParticles; i++) { PartSys->particles[i].hue -= globalhuestep; // shift global hue (both directions) PartSys->particles[i].vx = 1 + (SEGMENT.speed >> 2) + ((int32_t(sin16_t(strip.now >> 1) + 32767) * (SEGMENT.speed >> 2)) >> 16); } @@ -10007,7 +9996,7 @@ uint16_t mode_particleFire1D(void) { PartSys->setColorByAge(true); uint32_t emitparticles = 1; uint32_t j = hw_random16(); - for (uint i = 0; i < 3; i++) { // 3 base flames TODO: check if this is ok or needs adjustments + for (uint i = 0; i < 3; i++) { // 3 base flames if (PartSys->sources[i].source.ttl > 50) PartSys->sources[i].source.ttl -= 10; // TODO: in 2D making the source fade out slow results in much smoother flames, need to check if it can be done the same else @@ -10028,7 +10017,7 @@ uint16_t mode_particleFire1D(void) { } } else { - PartSys->sources[j].minLife = PartSys->sources[j].source.ttl + SEGMENT.intensity; // TODO: in 2D, emitted particle ttl depends on source TTL, mimic here the same way? OR: change 2D to the same way it is done here and ditch special fire treatment in emit? + PartSys->sources[j].minLife = PartSys->sources[j].source.ttl + SEGMENT.intensity; PartSys->sources[j].maxLife = PartSys->sources[j].minLife + 50; PartSys->sources[j].v = SEGMENT.speed >> 2; if (SEGENV.call & 0x01) // every second frame @@ -10266,7 +10255,7 @@ uint16_t mode_particleSpringy(void) { int32_t springlength = PartSys->maxX / (PartSys->usedParticles); // spring length (spacing between particles) int32_t springK = map(SEGMENT.speed, 0, 255, 5, 35); // spring constant (stiffness) - uint32_t settingssum = SEGMENT.custom1 + SEGMENT.check2 + PartSys->getAvailableParticles(); // note: getAvailableParticles is used to enforce update during transitions + uint32_t settingssum = SEGMENT.custom1 + SEGMENT.check2; if (SEGENV.aux0 != settingssum) { // number of particles changed, update distribution for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) { PartSys->advPartProps[i].sat = 255; // full saturation @@ -10289,7 +10278,7 @@ uint16_t mode_particleSpringy(void) { PartSys->particles[0].x = dxlimit; // limit the spring length springforce[0] += ((springlength >> 1) - (PartSys->particles[0].x)) * springK; // first particle anchors to x=0 - for (int32_t i = 1; i < PartSys->usedParticles; i++) { + for (uint32_t i = 1; i < PartSys->usedParticles; i++) { // reorder particles if they are out of order to prevent chaos if (PartSys->particles[i].x < PartSys->particles[i-1].x) std::swap(PartSys->particles[i].x, PartSys->particles[i-1].x); // swap particle positions to maintain order @@ -10310,7 +10299,7 @@ uint16_t mode_particleSpringy(void) { } // apply spring forces to particles bool dampenoscillations = (SEGMENT.call % (9 - (SEGMENT.speed >> 5))) == 0; // dampen oscillation if particles are slow, more damping on stiffer springs - for (int32_t i = 0; i < PartSys->usedParticles; i++) { + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { springforce[i] = springforce[i] / 64; // scale spring force (cannot use shifts because of negative values) int maxforce = 120; // limit spring force springforce[i] = springforce[i] > maxforce ? maxforce : springforce[i] < -maxforce ? -maxforce : springforce[i]; // limit spring force @@ -10327,7 +10316,7 @@ uint16_t mode_particleSpringy(void) { PartSys->applyFriction((SEGMENT.intensity >> 2)); // add a small resetting force so particles return to resting position even under high damping - for (int32_t i = 1; i < PartSys->usedParticles - 1; i++) { + for (uint32_t i = 1; i < PartSys->usedParticles - 1; i++) { int restposition = (springlength >> 1) + i * springlength; // resting position int dx = restposition - PartSys->particles[i].x; // distance, always positive PartSys->applyForce(PartSys->particles[i], dx > 0 ? 1 : (dx < 0 ? -1 : 0), PartSys->advPartProps[i].forcecounter); @@ -10377,7 +10366,7 @@ uint16_t mode_particleSpringy(void) { } } - for (int32_t i = 0; i < PartSys->usedParticles; i++) { + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { if (SEGMENT.custom2 == 255) { // map speed to hue int speedclr = ((int8_t(abs(PartSys->particles[i].vx))) >> 2) << 4; // scale for greater color variation, dump small values to avoid flickering //int speed = PartSys->particles[i].vx << 2; // +/- 512 diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index db8d5a308..42403fa85 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1665,9 +1665,6 @@ void WS2812FX::service() { _segment_index++; } Segment::setClippingRect(0, 0); // disable clipping for overlays - #if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) - servicePSmem(); // handle segment particle system memory - #endif _isServicing = false; _triggered = false; diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index cff534256..fadc98763 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -14,38 +14,24 @@ #if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled #include "FXparticleSystem.h" - // local shared functions (used both in 1D and 2D system) static int32_t calcForce_dv(const int8_t force, uint8_t &counter); static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap); // returns false if out of bounds by more than particleradius static void fast_color_add(CRGB &c1, const CRGB &c2, uint8_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) static void fast_color_scale(CRGB &c, const uint8_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 //static CRGB *allocateCRGBbuffer(uint32_t length); - -// global variables for memory management -std::vector partMemList; // list of particle memory pointers -partMem *pmem = nullptr; // pointer to particle memory of current segment, updated in particleMemoryManager() -CRGB *framebuffer = nullptr; // local frame buffer for rendering -CRGB *renderbuffer = nullptr; // local particle render buffer for advanced particles -uint16_t frameBufferSize = 0; // size in pixels, used to check if framebuffer is large enough for current segment -uint16_t renderBufferSize = 0; // size in pixels, if allcoated by a 1D system it needs to be updated for 2D -bool renderSolo = false; // is set to true if this is the only particle system using the so it can use the buffer continuously (faster blurring) -int32_t globalBlur = 0; // motion blur to apply if multiple PS are using the buffer -int32_t globalSmear = 0; // smear-blur to apply if multiple PS are using the buffer #endif #ifndef WLED_DISABLE_PARTICLESYSTEM2D ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced, bool sizecontrol) { PSPRINTLN("\n ParticleSystem2D constructor"); - effectID = SEGMENT.mode; // new FX called init, save the effect ID numSources = numberofsources; // number of sources allocated in init numParticles = numberofparticles; // number of particles allocated in init - availableParticles = 0; // let the memory manager assign - fractionOfParticlesUsed = 255; // use all particles by default, usedParticles is updated in updatePSpointers() + usedParticles = numParticles; // use all particles by default advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared) advPartSize = nullptr; - updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles) setMatrixSize(width, height); + updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles) setWallHardness(255); // set default wall hardness to max setWallRoughness(0); // smooth walls by default setGravity(0); //gravity disabled by default @@ -54,9 +40,11 @@ ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t num smearBlur = 0; //no smearing by default emitIndex = 0; collisionStartIdx = 0; - lastRender = 0; //initialize some default non-zero values most FX use + for (uint32_t i = 0; i < numParticles; i++) { + particles[i].sat = 255; // full saturation + } for (uint32_t i = 0; i < numSources; i++) { sources[i].source.sat = 255; //set saturation to max by default sources[i].source.ttl = 1; //set source alive @@ -88,7 +76,7 @@ void ParticleSystem2D::update(void) { particleMoveUpdate(particles[i], particleFlags[i], nullptr, advPartProps ? &advPartProps[i] : nullptr); // note: splitting this into two loops is slower and uses more flash } - ParticleSys_render(); + render(); } // update function for fire animation @@ -96,19 +84,14 @@ void ParticleSystem2D::updateFire(const uint8_t intensity,const bool renderonly) if (!renderonly) fireParticleupdate(); fireIntesity = intensity > 0 ? intensity : 1; // minimum of 1, zero checking is used in render function - ParticleSys_render(); + render(); } // set percentage of used particles as uint8_t i.e 127 means 50% for example void ParticleSystem2D::setUsedParticles(uint8_t percentage) { - fractionOfParticlesUsed = percentage; // note usedParticles is updated in memory manager - updateUsedParticles(numParticles, availableParticles, fractionOfParticlesUsed, usedParticles); + usedParticles = (numParticles * ((int)percentage+1)) >> 8; // number of particles to use (percentage is 0-255, 255 = 100%) PSPRINT(" SetUsedpaticles: allocated particles: "); PSPRINT(numParticles); - PSPRINT(" available particles: "); - PSPRINT(availableParticles); - PSPRINT(" ,used percentage: "); - PSPRINT(fractionOfParticlesUsed); PSPRINT(" ,used particles: "); PSPRINTLN(usedParticles); } @@ -573,71 +556,21 @@ void ParticleSystem2D::pointAttractor(const uint32_t particleindex, PSparticle & // if wrap is set, particles half out of bounds are rendered to the other side of the matrix // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds // firemode is only used for PS Fire FX -void ParticleSystem2D::ParticleSys_render() { - if (blendingStyle == BLEND_STYLE_FADE && SEGMENT.isInTransition() && lastRender + (strip.getFrameTime() >> 1) > strip.now) // fixes speedup during transitions TODO: find a better solution - return; - lastRender = strip.now; +void ParticleSystem2D::render() { CRGB baseRGB; uint32_t brightness; // particle brightness, fades if dying - static bool useAdditiveTransfer = false; // use add instead of set for buffer transferring (must persist between calls) - bool isNonFadeTransition = (pmem->inTransition || pmem->finalTransfer) && blendingStyle != BLEND_STYLE_FADE; - bool isOverlay = segmentIsOverlay(); - // update global blur (used for blur transitions) - int32_t motionbluramount = motionBlur; - int32_t smearamount = smearBlur; - if (pmem->inTransition == effectID && blendingStyle == BLEND_STYLE_FADE) { // FX transition and this is the new FX: fade blur amount but only if using fade style - motionbluramount = globalBlur + (((motionbluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions - smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); - } - globalBlur = motionbluramount; - globalSmear = smearamount; - - if (isOverlay) { - globalSmear = 0; // do not apply smear or blur in overlay or it turns everything into a blurry mess - globalBlur = 0; - } - // handle blurring and framebuffer update - if (framebuffer) { - if (!pmem->inTransition) - useAdditiveTransfer = false; // additive transfer is only usd in transitions (or in overlay) - // handle buffer blurring or clearing - bool bufferNeedsUpdate = !pmem->inTransition || pmem->inTransition == effectID || isNonFadeTransition; // not a transition; or new FX or not fading style: update buffer (blur, or clear) - if (bufferNeedsUpdate) { - bool loadfromSegment = !renderSolo || isNonFadeTransition; - if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) - for (int32_t y = 0; y <= maxYpixel; y++) { - int index = y * (maxXpixel + 1); - for (int32_t x = 0; x <= maxXpixel; x++) { - if (loadfromSegment) { // sharing the framebuffer with another segment or not using fade style blending: update buffer by reading back from segment - framebuffer[index] = SEGMENT.getPixelColorXY(x, y); // read from segment - } - fast_color_scale(framebuffer[index], globalBlur); // note: could skip if only globalsmear is active but usually they are both active and scaling is fast enough - index++; - } - } - } - else { // no blurring: clear buffer - memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); - } - } - // handle buffer for global large particle size rendering - if (particlesize > 1 && pmem->inTransition) { // if particle size is used by FX we need a clean buffer - if (bufferNeedsUpdate && !globalBlur) { // transfer without adding if buffer was not cleared above (happens if this is the new FX and other FX does not use blurring) - useAdditiveTransfer = false; // no blurring and big size particle FX is the new FX (rendered first after clearing), can just render normally - } - else { // this is the old FX (rendering second) or blurring is active: new FX already rendered to the buffer and blurring was applied above; transfer it to segment and clear it - transferBuffer(maxXpixel + 1, maxYpixel + 1, isOverlay); - memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); // clear the buffer after transfer - useAdditiveTransfer = true; // additive transfer reads from segment, adds that to the frame-buffer and writes back to segment, after transfer, segment and buffer are identical + if (motionBlur) { // motion-blurring active + for (int32_t y = 0; y <= maxYpixel; y++) { + int index = y * (maxXpixel + 1); + for (int32_t x = 0; x <= maxXpixel; x++) { + fast_color_scale(framebuffer[index], motionBlur); // note: could skip if only globalsmear is active but usually they are both active and scaling is fast enough + index++; } } } - else { // no local buffer available, apply blur to segment - if (motionBlur > 0) - SEGMENT.fadeToBlackBy(255 - motionBlur); - else - SEGMENT.fill(BLACK); //clear the buffer before rendering next frame + else { // no blurring: clear buffer + memset(framebuffer, 0, (maxXpixel+1) * (maxYpixel+1) * sizeof(CRGB)); } // go over particles and render them to the buffer @@ -672,52 +605,41 @@ void ParticleSystem2D::ParticleSys_render() { for (uint32_t i = 0; i < passes; i++) { if (i == 2) // for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) bitshift = 1; - - if (framebuffer) blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift); - else { - SEGMENT.blur(bluramount << bitshift, true); - } bluramount -= 64; } } + // apply 2D blur to rendered frame - if (globalSmear > 0) { - if (framebuffer) - blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, globalSmear, globalSmear); - else - SEGMENT.blur(globalSmear, true); + if (smearBlur) { + blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, smearBlur, smearBlur); + } + + // transfer the framebuffer to the segment + for (int y = 0; y <= maxYpixel; y++) { + int index = y * (maxXpixel + 1); // current row index for 1D buffer + for (int x = 0; x <= maxXpixel; x++) { + SEGMENT.setPixelColorXY(x, y, framebuffer[index++]); + } } - // transfer framebuffer to segment if available - if (pmem->inTransition != effectID || isNonFadeTransition) // not in transition or is old FX (rendered second) or not fade style - transferBuffer(maxXpixel + 1, maxYpixel + 1, useAdditiveTransfer | isOverlay); } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer -void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB& color, const bool wrapX, const bool wrapY) { +__attribute__((optimize("O2"))) void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB& color, const bool wrapX, const bool wrapY) { if (particlesize == 0) { // single pixel rendering uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT; uint32_t y = particles[particleindex].y >> PS_P_RADIUS_SHIFT; if (x <= (uint32_t)maxXpixel && y <= (uint32_t)maxYpixel) { - if (framebuffer) - fast_color_add(framebuffer[x + (maxYpixel - y) * (maxXpixel + 1)], color, brightness); - else - SEGMENT.addPixelColorXY(x, maxYpixel - y, color.scale8(brightness), true); + fast_color_add(framebuffer[x + (maxYpixel - y) * (maxXpixel + 1)], color, brightness); } return; } uint8_t pxlbrightness[4]; // brightness values for the four pixels representing a particle - int32_t pixco[4][2]; // physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs + struct { + int32_t x,y; + } pixco[4]; // particle pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3] (thx @blazoncek for improved readability struct) bool pixelvalid[4] = {true, true, true, true}; // is set to false if pixel is out of bounds - bool advancedrender = false; // rendering for advanced particles - // check if particle has advanced size properties and buffer is available - if (advPartProps && advPartProps[particleindex].size > 0) { - if (renderbuffer) { - advancedrender = true; - memset(renderbuffer, 0, 100 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10x10 pixels - } - else return; // cannot render without buffers - } + // add half a radius as the rendering algorithm always starts at the bottom left, this leaves things positive, so shifts can be used, then shift coordinate by a full pixel (x--/y-- below) int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS; int32_t yoffset = particles[particleindex].y + PS_P_HALFRADIUS; @@ -726,13 +648,13 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_ int32_t x = (xoffset >> PS_P_RADIUS_SHIFT); // divide by PS_P_RADIUS which is 64, so can bitshift (compiler can not optimize integer) int32_t y = (yoffset >> PS_P_RADIUS_SHIFT); - // set the four raw pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3] - pixco[1][0] = pixco[2][0] = x; // bottom right & top right - pixco[2][1] = pixco[3][1] = y; // top right & top left + // set the four raw pixel coordinates + pixco[1].x = pixco[2].x = x; // bottom right & top right + pixco[2].y = pixco[3].y = y; // top right & top left x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1 y--; - pixco[0][0] = pixco[3][0] = x; // bottom left & top left - pixco[0][1] = pixco[1][1] = y; // bottom left & bottom right + pixco[0].x = pixco[3].x = x; // bottom left & top left + pixco[0].y = pixco[1].y = y; // bottom left & bottom right // calculate brightness values for all four pixels representing a particle using linear interpolation // could check for out of frame pixels here but calculating them is faster (very few are out) @@ -745,11 +667,12 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_ pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightness) >> PS_P_SURFACE pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightness) >> PS_P_SURFACE - if (advancedrender) { - //render particle to a bigger size + if (advPartProps && advPartProps[particleindex].size > 0) { //render particle to a bigger size + CRGB renderbuffer[100]; // 10x10 pixel buffer + memset(renderbuffer, 0, sizeof(renderbuffer)); // clear buffer //particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10 //first, render the pixel to the center of the renderbuffer, then apply 2D blurring - fast_color_add(renderbuffer[4 + (4 * 10)], color, pxlbrightness[0]); // order is: bottom left, bottom right, top right, top left + fast_color_add(renderbuffer[4 + (4 * 10)], color, pxlbrightness[0]); // oCrder is: bottom left, bottom right, top right, top left fast_color_add(renderbuffer[5 + (4 * 10)], color, pxlbrightness[1]); fast_color_add(renderbuffer[5 + (5 * 10)], color, pxlbrightness[2]); fast_color_add(renderbuffer[4 + (5 * 10)], color, pxlbrightness[3]); @@ -809,24 +732,21 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_ else continue; } - if (framebuffer) - fast_color_add(framebuffer[xfb + (maxYpixel - yfb) * (maxXpixel + 1)], renderbuffer[xrb + yrb * 10]); - else - SEGMENT.addPixelColorXY(xfb, maxYpixel - yfb, renderbuffer[xrb + yrb * 10],true); + fast_color_add(framebuffer[xfb + (maxYpixel - yfb) * (maxXpixel + 1)], renderbuffer[xrb + yrb * 10]); } } } else { // standard rendering (2x2 pixels) // check for out of frame pixels and wrap them if required: x,y is bottom left pixel coordinate of the particle if (x < 0) { // left pixels out of frame if (wrapX) { // wrap x to the other side if required - pixco[0][0] = pixco[3][0] = maxXpixel; + pixco[0].x = pixco[3].x = maxXpixel; } else { pixelvalid[0] = pixelvalid[3] = false; // out of bounds } } - else if (pixco[1][0] > (int32_t)maxXpixel) { // right pixels, only has to be checked if left pixel is in frame + else if (pixco[1].x > (int32_t)maxXpixel) { // right pixels, only has to be checked if left pixel is in frame if (wrapX) { // wrap y to the other side if required - pixco[1][0] = pixco[2][0] = 0; + pixco[1].x = pixco[2].x = 0; } else { pixelvalid[1] = pixelvalid[2] = false; // out of bounds } @@ -834,29 +754,21 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_ if (y < 0) { // bottom pixels out of frame if (wrapY) { // wrap y to the other side if required - pixco[0][1] = pixco[1][1] = maxYpixel; + pixco[0].y = pixco[1].y = maxYpixel; } else { pixelvalid[0] = pixelvalid[1] = false; // out of bounds } } - else if (pixco[2][1] > maxYpixel) { // top pixels + else if (pixco[2].y > maxYpixel) { // top pixels if (wrapY) { // wrap y to the other side if required - pixco[2][1] = pixco[3][1] = 0; + pixco[2].y = pixco[3].y = 0; } else { pixelvalid[2] = pixelvalid[3] = false; // out of bounds } } - if (framebuffer) { - for (uint32_t i = 0; i < 4; i++) { - if (pixelvalid[i]) - fast_color_add(framebuffer[pixco[i][0] + (maxYpixel - pixco[i][1]) * (maxXpixel + 1)], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left - } - } - else { - for (uint32_t i = 0; i < 4; i++) { + for (uint32_t i = 0; i < 4; i++) { if (pixelvalid[i]) - SEGMENT.addPixelColorXY(pixco[i][0], maxYpixel - pixco[i][1], color.scale8((uint8_t)pxlbrightness[i]), true); - } + fast_color_add(framebuffer[pixco[i].x + (maxYpixel - pixco[i].y) * (maxXpixel + 1)], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left } } } @@ -866,7 +778,7 @@ void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_ // for code simplicity, no y slicing is done, making very tall matrix configurations less efficient // note: also tested adding y slicing, it gives diminishing returns, some FX even get slower. FX not using gravity would benefit with a 10% FPS improvement void ParticleSystem2D::handleCollisions() { - int32_t collDistSq = particleHardRadius << 1; // distance is double the radius note: particleHardRadius is updated when setting global particle size + uint32_t collDistSq = particleHardRadius << 1; // distance is double the radius note: particleHardRadius is updated when setting global particle size collDistSq = collDistSq * collDistSq; // square it for faster comparison (square is one operation) // note: partices are binned in x-axis, assumption is that no more than half of the particles are in the same bin // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) @@ -889,13 +801,15 @@ void ParticleSystem2D::handleCollisions() { // fill the binIndices array for this bin for (uint32_t i = 0; i < usedParticles; i++) { - if (particles[pidx].ttl > 0 && particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // colliding particle + if (particles[pidx].ttl > 0) { // is alive if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) - if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame - nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) - break; + if(particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // particle is in frame and does collide note: checking flags is quite slow and usually these are set, so faster to check here + if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame + nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) + break; + } + binIndices[binParticleCount++] = pidx; } - binIndices[binParticleCount++] = pidx; } } pidx++; @@ -925,7 +839,7 @@ void ParticleSystem2D::handleCollisions() { // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) -void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const int32_t collDistSq) { +__attribute__((optimize("O2"))) void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const uint32_t collDistSq) { int32_t distanceSquared = dx * dx + dy * dy; // Calculate relative velocity note: could zero check but that does not improve overall speed but deminish it as that is rarely the case and pushing is still required int32_t relativeVx = (int32_t)particle2.vx - (int32_t)particle1.vx; @@ -1039,13 +953,7 @@ void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &parti void ParticleSystem2D::updateSystem(void) { PSPRINTLN("updateSystem2D"); setMatrixSize(SEGMENT.vWidth(), SEGMENT.vHeight()); - updateRenderingBuffer(SEGMENT.vWidth() * SEGMENT.vHeight(), true, false); // update rendering buffer (segment size can change at any time) updatePSpointers(advPartProps != nullptr, advPartSize != nullptr); // update pointers to PS data, also updates availableParticles - setUsedParticles(fractionOfParticlesUsed); // update used particles based on percentage (can change during transitions, execute each frame for code simplicity) - if (partMemList.size() == 1) // if number of vector elements is one, this is the only system - renderSolo = true; - else - renderSolo = false; PSPRINTLN("\n END update System2D, running FX..."); } @@ -1060,18 +968,19 @@ void ParticleSystem2D::updatePSpointers(bool isadvanced, bool sizecontrol) { // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. - - // memory manager needs to know how many particles the FX wants to use so transitions can be handled properly (i.e. pointer will stop changing if enough particles are available during transitions) - uint32_t usedByFX = (numParticles * ((uint32_t)fractionOfParticlesUsed + 1)) >> 8; // final number of particles the FX wants to use (fractionOfParticlesUsed is 0-255) - particles = reinterpret_cast(particleMemoryManager(0, sizeof(PSparticle), availableParticles, usedByFX, effectID)); // get memory, leave buffer size as is (request 0) particleFlags = reinterpret_cast(this + 1); // pointer to particle flags - sources = reinterpret_cast(particleFlags + numParticles); // pointer to source(s) at data+sizeof(ParticleSystem2D) - PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data + particles = reinterpret_cast(particleFlags + numParticles); // pointer to particles + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) at data+sizeof(ParticleSystem2D) + framebuffer = reinterpret_cast(sources + numSources); // pointer to framebuffer + // align pointer after framebuffer + uintptr_t p = reinterpret_cast(framebuffer + (maxXpixel+1)*(maxYpixel+1)); + p = (p + 3) & ~0x03; // align to 4-byte boundary + PSdataEnd = reinterpret_cast(p); // pointer to first available byte after the PS for FX additional data if (isadvanced) { - advPartProps = reinterpret_cast(sources + numSources); + advPartProps = reinterpret_cast(PSdataEnd); PSdataEnd = reinterpret_cast(advPartProps + numParticles); if (sizecontrol) { - advPartSize = reinterpret_cast(advPartProps + numParticles); + advPartSize = reinterpret_cast(PSdataEnd); PSdataEnd = reinterpret_cast(advPartSize + numParticles); } } @@ -1156,42 +1065,42 @@ uint32_t calculateNumberOfParticles2D(uint32_t const pixels, const bool isadvanc numberofParticles /= 8; // if advanced size control is used, much fewer particles are needed note: if changing this number, adjust FX using this accordingly //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) - numberofParticles = ((numberofParticles+3) >> 2) << 2; // note: with a separate particle buffer, this is probably unnecessary + numberofParticles = (numberofParticles+3) & ~0x03; return numberofParticles; } uint32_t calculateNumberOfSources2D(uint32_t pixels, uint32_t requestedsources) { #ifdef ESP8266 int numberofSources = min((pixels) / 8, (uint32_t)requestedsources); - numberofSources = max(1, min(numberofSources, ESP8266_MAXSOURCES)); // limit to 1 - 16 + numberofSources = max(1, min(numberofSources, ESP8266_MAXSOURCES)); // limit #elif ARDUINO_ARCH_ESP32S2 int numberofSources = min((pixels) / 6, (uint32_t)requestedsources); - numberofSources = max(1, min(numberofSources, ESP32S2_MAXSOURCES)); // limit to 1 - 48 + numberofSources = max(1, min(numberofSources, ESP32S2_MAXSOURCES)); // limit #else int numberofSources = min((pixels) / 4, (uint32_t)requestedsources); - numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit to 1 - 64 + numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit #endif // make sure it is a multiple of 4 for proper memory alignment - numberofSources = ((numberofSources+3) >> 2) << 2; + numberofSources = (numberofSources+3) & ~0x03; return numberofSources; } //allocate memory for particle system class, particles, sprays plus additional memory requested by FX //TODO: add percentofparticles like in 1D to reduce memory footprint of some FX? bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, bool isadvanced, bool sizecontrol, uint32_t additionalbytes) { PSPRINTLN("PS 2D alloc"); + PSPRINTLN("numparticles:" + String(numparticles) + " numsources:" + String(numsources) + " additionalbytes:" + String(additionalbytes)); uint32_t requiredmemory = sizeof(ParticleSystem2D); - uint32_t dummy; // dummy variable - if ((particleMemoryManager(numparticles, sizeof(PSparticle), dummy, dummy, SEGMENT.mode)) == nullptr) // allocate memory for particles - return false; // not enough memory, function ensures a minimum of numparticles are available - - // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) + // functions above make sure numparticles is a multiple of 4 bytes (to avoid alignment issues) requiredmemory += sizeof(PSparticleFlags) * numparticles; + requiredmemory += sizeof(PSparticle) * numparticles; if (isadvanced) requiredmemory += sizeof(PSadvancedParticle) * numparticles; if (sizecontrol) requiredmemory += sizeof(PSsizeControl) * numparticles; requiredmemory += sizeof(PSsource) * numsources; - requiredmemory += additionalbytes; + requiredmemory += sizeof(CRGB) * SEGMENT.virtualLength(); // virtualLength is witdh * height + requiredmemory += additionalbytes + 3; // add 3 to ensure there is room for stuffing bytes + //requiredmemory = (requiredmemory + 3) & ~0x03; // align memory block to next 4-byte boundary PSPRINTLN("mem alloc: " + String(requiredmemory)); return(SEGMENT.allocateData(requiredmemory)); } @@ -1204,10 +1113,8 @@ bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t rows = SEGMENT.virtualHeight(); uint32_t pixels = cols * rows; - if (advanced) - updateRenderingBuffer(100, false, true); // allocate a 10x10 buffer for rendering advanced particles uint32_t numparticles = calculateNumberOfParticles2D(pixels, advanced, sizecontrol); - PSPRINT(" segmentsize:" + String(cols) + " " + String(rows)); + PSPRINT(" segmentsize:" + String(cols) + " x " + String(rows)); PSPRINT(" request numparticles:" + String(numparticles)); uint32_t numsources = calculateNumberOfSources2D(pixels, requestedsources); if (!allocateParticleSystemMemory2D(numparticles, numsources, advanced, sizecontrol, additionalbytes)) @@ -1217,19 +1124,8 @@ bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, } PartSys = new (SEGENV.data) ParticleSystem2D(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor - updateRenderingBuffer(SEGMENT.vWidth() * SEGMENT.vHeight(), true, true); // update or create rendering buffer note: for fragmentation it might be better to allocate this first, but if memory is scarce, system has a buffer but no particles and will return false PSPRINTLN("******init done, pointers:"); - #ifdef WLED_DEBUG_PS - PSPRINT("framebfr size:"); - PSPRINT(frameBufferSize); - PSPRINT(" @ addr: 0x"); - Serial.println((uintptr_t)framebuffer, HEX); - PSPRINT("renderbfr size:"); - PSPRINT(renderBufferSize); - PSPRINT(" @ addr: 0x"); - Serial.println((uintptr_t)renderbuffer, HEX); - #endif return true; } @@ -1242,15 +1138,13 @@ bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, #ifndef WLED_DISABLE_PARTICLESYSTEM1D ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced) { - effectID = SEGMENT.mode; numSources = numberofsources; numParticles = numberofparticles; // number of particles allocated in init - availableParticles = 0; // let the memory manager assign - fractionOfParticlesUsed = 255; // use all particles by default + usedParticles = numParticles; // use all particles by default advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared) //advPartSize = nullptr; - updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) setSize(length); + updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) setWallHardness(255); // set default wall hardness to max setGravity(0); //gravity disabled by default setParticleSize(0); // 1 pixel size by default @@ -1258,7 +1152,6 @@ ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, smearBlur = 0; //no smearing by default emitIndex = 0; collisionStartIdx = 0; - lastRender = 0; // initialize some default non-zero values most FX use for (uint32_t i = 0; i < numSources; i++) { sources[i].source.ttl = 1; //set source alive @@ -1293,19 +1186,14 @@ void ParticleSystem1D::update(void) { } } - ParticleSys_render(); + render(); } // set percentage of used particles as uint8_t i.e 127 means 50% for example void ParticleSystem1D::setUsedParticles(const uint8_t percentage) { - fractionOfParticlesUsed = percentage; // note usedParticles is updated in memory manager - updateUsedParticles(numParticles, availableParticles, fractionOfParticlesUsed, usedParticles); + usedParticles = (numParticles * ((int)percentage+1)) >> 8; // number of particles to use (percentage is 0-255, 255 = 100%) PSPRINT(" SetUsedpaticles: allocated particles: "); PSPRINT(numParticles); - PSPRINT(" available particles: "); - PSPRINT(availableParticles); - PSPRINT(" ,used percentage: "); - PSPRINT(fractionOfParticlesUsed); PSPRINT(" ,used particles: "); PSPRINTLN(usedParticles); } @@ -1528,50 +1416,25 @@ void ParticleSystem1D::applyFriction(int32_t coefficient) { // render particles to the LED buffer (uses palette to render the 8bit particle color value) // if wrap is set, particles half out of bounds are rendered to the other side of the matrix // warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds -void ParticleSystem1D::ParticleSys_render() { - if (blendingStyle == BLEND_STYLE_FADE && SEGMENT.isInTransition() && lastRender + (strip.getFrameTime() >> 1) > strip.now) // fixes speedup during transitions TODO: find a better solution - return; - lastRender = strip.now; +void ParticleSystem1D::render() { CRGB baseRGB; uint32_t brightness; // particle brightness, fades if dying - // bool useAdditiveTransfer; // use add instead of set for buffer transferring - bool isNonFadeTransition = (pmem->inTransition || pmem->finalTransfer) && blendingStyle != BLEND_STYLE_FADE; - bool isOverlay = segmentIsOverlay(); - // update global blur (used for blur transitions) - int32_t motionbluramount = motionBlur; - int32_t smearamount = smearBlur; - if (pmem->inTransition == effectID) { // FX transition and this is the new FX: fade blur amount - motionbluramount = globalBlur + (((motionbluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions - smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); - } - globalBlur = motionbluramount; - globalSmear = smearamount; - - if (framebuffer) { - // handle buffer blurring or clearing - bool bufferNeedsUpdate = !pmem->inTransition || pmem->inTransition == effectID || isNonFadeTransition; // not a transition; or new FX: update buffer (blur, or clear) - if (bufferNeedsUpdate) { - bool loadfromSegment = !renderSolo || isNonFadeTransition; - if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) - for (int32_t x = 0; x <= maxXpixel; x++) { - if (loadfromSegment) // sharing the framebuffer with another segment: read buffer back from segment - framebuffer[x] = SEGMENT.getPixelColor(x); // copy to local buffer - fast_color_scale(framebuffer[x], motionBlur); - } - } - else { // no blurring: clear buffer - memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); - } + #ifdef ESP8266 // no local buffer on ESP8266 + if (motionBlur) + SEGMENT.fadeToBlackBy(255 - motionBlur); + else + SEGMENT.fill(BLACK); // clear the buffer before rendering to it + #else + if (motionBlur) { // blurring active + for (int32_t x = 0; x <= maxXpixel; x++) { + fast_color_scale(framebuffer[x], motionBlur); } } - else { // no local buffer available - if (motionBlur > 0) - SEGMENT.fadeToBlackBy(255 - motionBlur); - else - SEGMENT.fill(BLACK); // clear the buffer before rendering to it + else { // no blurring: clear buffer + memset(framebuffer, 0, (maxXpixel+1) * sizeof(CRGB)); } - + #endif // go over particles and render them to the buffer for (uint32_t i = 0; i < usedParticles; i++) { if ( particles[i].ttl == 0 || particleFlags[i].outofbounds) @@ -1594,30 +1457,37 @@ void ParticleSystem1D::ParticleSys_render() { renderParticle(i, brightness, baseRGB, particlesettings.wrap); } // apply smear-blur to rendered frame - if (globalSmear > 0) { - if (framebuffer) - blur1D(framebuffer, maxXpixel + 1, globalSmear, 0); - else - SEGMENT.blur(globalSmear, true); + if (smearBlur) { + #ifdef ESP8266 + SEGMENT.blur(smearBlur, true); // no local buffer on ESP8266 + #else + blur1D(framebuffer, maxXpixel + 1, smearBlur, 0); + #endif } // add background color uint32_t bg_color = SEGCOLOR(1); if (bg_color > 0) { //if not black + CRGB bg_color_crgb = bg_color; // convert to CRGB for (int32_t i = 0; i <= maxXpixel; i++) { - if (framebuffer) - fast_color_add(framebuffer[i], bg_color); - else - SEGMENT.addPixelColor(i, bg_color, true); + #ifdef ESP8266 // no local buffer on ESP8266 + SEGMENT.addPixelColor(i, bg_color, true); + #else + fast_color_add(framebuffer[i], bg_color_crgb); + #endif } } - // transfer local buffer back to segment (if available) - if (pmem->inTransition != effectID || isNonFadeTransition) - transferBuffer(maxXpixel + 1, 0, isOverlay); + + #ifndef ESP8266 + // transfer the frame-buffer to segment + for (int x = 0; x <= maxXpixel; x++) { + SEGMENT.setPixelColor(x, framebuffer[x]); + } + #endif } // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer -void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB &color, const bool wrap) { +__attribute__((optimize("O2"))) void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB &color, const bool wrap) { uint32_t size = particlesize; if (advPartProps) { // use advanced size properties size = advPartProps[particleindex].size; @@ -1625,10 +1495,11 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint8_ if (size == 0) { //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles (and updating it uses more code) uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D; if (x <= (uint32_t)maxXpixel) { //by making x unsigned there is no need to check < 0 as it will overflow - if (framebuffer) - fast_color_add(framebuffer[x], color, brightness); - else - SEGMENT.addPixelColor(x, color.scale8(brightness), true); + #ifdef ESP8266 // no local buffer on ESP8266 + SEGMENT.addPixelColor(x, color.scale8(brightness), true); + #else + fast_color_add(framebuffer[x], color, brightness); + #endif } return; } @@ -1653,12 +1524,8 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint8_ // check if particle has advanced size properties and buffer is available if (advPartProps && advPartProps[particleindex].size > 1) { - if (renderbuffer) { - memset(renderbuffer, 0, 10 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10 pixels - } - else - return; // cannot render advanced particles without buffer - + CRGB renderbuffer[10]; // 10 pixel buffer + memset(renderbuffer, 0, sizeof(renderbuffer)); // clear buffer //render particle to a bigger size //particle size to pixels: 2 - 63 is 4 pixels, < 128 is 6pixels, < 192 is 8 pixels, bigger is 10 pixels //first, render the pixel to the center of the renderbuffer, then apply 1D blurring @@ -1694,10 +1561,11 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint8_ else continue; } - if (framebuffer) - fast_color_add(framebuffer[xfb], renderbuffer[xrb]); - else - SEGMENT.addPixelColor(xfb, renderbuffer[xrb]); + #ifdef ESP8266 // no local buffer on ESP8266 + SEGMENT.addPixelColor(xfb, renderbuffer[xrb], true); + #else + fast_color_add(framebuffer[xfb], renderbuffer[xrb]); + #endif } } else { // standard rendering (2 pixels per particle) @@ -1716,10 +1584,11 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint8_ } for (uint32_t i = 0; i < 2; i++) { if (pxlisinframe[i]) { - if (framebuffer) - fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]); - else - SEGMENT.addPixelColor(pixco[i], color.scale8((uint8_t)pxlbrightness[i]), true); + #ifdef ESP8266 // no local buffer on ESP8266 + SEGMENT.addPixelColor(pixco[i], color.scale8((uint8_t)pxlbrightness[i]), true); + #else + fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]); + #endif } } } @@ -1728,10 +1597,10 @@ void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint8_ // detect collisions in an array of particles and handle them void ParticleSystem1D::handleCollisions() { - int32_t collisiondistance = particleHardRadius << 1; + uint32_t collisiondistance = particleHardRadius << 1; // note: partices are binned by position, assumption is that no more than half of the particles are in the same bin // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) - constexpr int BIN_WIDTH = 32 * PS_P_RADIUS_1D; // width of each bin, a compromise between speed and accuracy (lareger bins are faster but collapse more) + constexpr int BIN_WIDTH = 32 * PS_P_RADIUS_1D; // width of each bin, a compromise between speed and accuracy (larger bins are faster but collapse more) int32_t overlap = particleHardRadius << 1; // overlap bins to include edge particles to neighbouring bins if (advPartProps) //may be using individual particle size overlap += 256; // add 2 * max radius (approximately) @@ -1748,13 +1617,15 @@ void ParticleSystem1D::handleCollisions() { // fill the binIndices array for this bin for (uint32_t i = 0; i < usedParticles; i++) { - if (particles[pidx].ttl > 0 && particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // colliding particle + if (particles[pidx].ttl > 0) { // alivee if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) - if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame - nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) - break; + if(particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // particle is in frame and does collide note: checking flags is quite slow and usually these are set, so faster to check here + if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame + nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) + break; + } + binIndices[binParticleCount++] = pidx; } - binIndices[binParticleCount++] = pidx; } } pidx++; @@ -1780,7 +1651,7 @@ void ParticleSystem1D::handleCollisions() { } // handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS // takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) -void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const int32_t collisiondistance) { +__attribute__((optimize("O2"))) void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const uint32_t collisiondistance) { int32_t dv = particle2.vx - particle1.vx; int32_t dotProduct = (dx * dv); // is always negative if moving towards each other @@ -1853,14 +1724,8 @@ void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticl // update size and pointers (memory location and size can change dynamically) // note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) void ParticleSystem1D::updateSystem(void) { - setSize(SEGMENT.vLength()); // update size - updateRenderingBuffer(SEGMENT.vLength(), true, false); // update rendering buffer (segment size can change at any time) + setSize(SEGMENT.virtualLength()); // update size updatePSpointers(advPartProps != nullptr); - setUsedParticles(fractionOfParticlesUsed); // update used particles based on percentage (can change during transitions, execute each frame for code simplicity) - if (partMemList.size() == 1) // if number of vector elements is one, this is the only system - renderSolo = true; - else - renderSolo = false; } // set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) @@ -1871,15 +1736,20 @@ void ParticleSystem1D::updatePSpointers(bool isadvanced) { // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. - - // memory manager needs to know how many particles the FX wants to use so transitions can be handled properly (i.e. pointer will stop changing if enough particles are available during transitions) - uint32_t usedByFX = (numParticles * ((uint32_t)fractionOfParticlesUsed + 1)) >> 8; // final number of particles the FX wants to use (fractionOfParticlesUsed is 0-255) - particles = reinterpret_cast(particleMemoryManager(0, sizeof(PSparticle1D), availableParticles, usedByFX, effectID)); // get memory, leave buffer size as is (request 0) particleFlags = reinterpret_cast(this + 1); // pointer to particle flags - sources = reinterpret_cast(particleFlags + numParticles); // pointer to source(s) - PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data + particles = reinterpret_cast(particleFlags + numParticles); // pointer to particles + sources = reinterpret_cast(particles + numParticles); // pointer to source(s) + #ifdef ESP8266 // no local buffer on ESP8266 + PSdataEnd = reinterpret_cast(sources + numSources); + #else + framebuffer = reinterpret_cast(sources + numSources); // pointer to framebuffer + // align pointer after framebuffer to 4bytes + uintptr_t p = reinterpret_cast(framebuffer + (maxXpixel+1)); + p = (p + 3) & ~0x03; // align to 4-byte boundary + PSdataEnd = reinterpret_cast(p); // pointer to first available byte after the PS for FX additional data + #endif if (isadvanced) { - advPartProps = reinterpret_cast(sources + numSources); + advPartProps = reinterpret_cast(PSdataEnd); PSdataEnd = reinterpret_cast(advPartProps + numParticles); } #ifdef WLED_DEBUG_PS @@ -1909,33 +1779,34 @@ uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadva numberofParticles = (numberofParticles * (fraction + 1)) >> 8; // calculate fraction of particles numberofParticles = numberofParticles < 20 ? 20 : numberofParticles; // 20 minimum //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) - numberofParticles = ((numberofParticles+3) >> 2) << 2; // note: with a separate particle buffer, this is probably unnecessary + numberofParticles = (numberofParticles+3) & ~0x03; // note: with a separate particle buffer, this is probably unnecessary return numberofParticles; } uint32_t calculateNumberOfSources1D(const uint32_t requestedsources) { #ifdef ESP8266 - int numberofSources = max(1, min((int)requestedsources,ESP8266_MAXSOURCES_1D)); // limit to 1 - 8 + int numberofSources = max(1, min((int)requestedsources,ESP8266_MAXSOURCES_1D)); // limit #elif ARDUINO_ARCH_ESP32S2 - int numberofSources = max(1, min((int)requestedsources, ESP32S2_MAXSOURCES_1D)); // limit to 1 - 16 + int numberofSources = max(1, min((int)requestedsources, ESP32S2_MAXSOURCES_1D)); // limit #else - int numberofSources = max(1, min((int)requestedsources, ESP32_MAXSOURCES_1D)); // limit to 1 - 32 + int numberofSources = max(1, min((int)requestedsources, ESP32_MAXSOURCES_1D)); // limit #endif // make sure it is a multiple of 4 for proper memory alignment (so minimum is acutally 4) - numberofSources = ((numberofSources+3) >> 2) << 2; + numberofSources = (numberofSources+3) & ~0x03; return numberofSources; } //allocate memory for particle system class, particles, sprays plus additional memory requested by FX bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes) { uint32_t requiredmemory = sizeof(ParticleSystem1D); - uint32_t dummy; // dummy variable - if (particleMemoryManager(numparticles, sizeof(PSparticle1D), dummy, dummy, SEGMENT.mode) == nullptr) // allocate memory for particles - return false; // not enough memory, function ensures a minimum of numparticles are avialable // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) requiredmemory += sizeof(PSparticleFlags1D) * numparticles; + requiredmemory += sizeof(PSparticle1D) * numparticles; requiredmemory += sizeof(PSsource1D) * numsources; - requiredmemory += additionalbytes; + #ifndef ESP8266 // no local buffer on ESP8266 + requiredmemory += sizeof(CRGB) * SEGMENT.virtualLength(); + #endif + requiredmemory += additionalbytes + 3; // add 3 to ensure room for stuffing bytes to make it 4 byte aligned if (isadvanced) requiredmemory += sizeof(PSadvancedParticle1D) * numparticles; return(SEGMENT.allocateData(requiredmemory)); @@ -1944,9 +1815,7 @@ bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t // initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) // note: percentofparticles is in uint8_t, for example 191 means 75%, (deafaults to 255 or 100% meaning one particle per pixel), can be more than 100% (but not recommended, can cause out of memory) bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles, const uint32_t additionalbytes, const bool advanced) { - if (SEGLEN == 1) return false; // single pixel not supported - if (advanced) - updateRenderingBuffer(10, false, true); // buffer for advanced particles, fixed size + if (SEGLEN == 1) return false; // single pixel not supported uint32_t numparticles = calculateNumberOfParticles1D(fractionofparticles, advanced); uint32_t numsources = calculateNumberOfSources1D(requestedsources); if (!allocateParticleSystemMemory1D(numparticles, numsources, advanced, additionalbytes)) { @@ -1954,7 +1823,6 @@ bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedso return false; } PartSys = new (SEGENV.data) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources, advanced); // particle system constructor - updateRenderingBuffer(SEGMENT.vLength(), true, true); // update/create frame rendering buffer note: for fragmentation it might be better to allocate this first, but if memory is scarce, system has a buffer but no particles and will return false return true; } @@ -2060,379 +1928,4 @@ static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32 c.b = ((c.b * scale) >> 8); } - -////////////////////////////////////////////////////////// -// memory and transition management for particle system // -////////////////////////////////////////////////////////// -// note: these functions can only be called while strip is servicing - -// allocate memory using the FX data limit, if overridelimit is set, temporarily ignore the limit -void* allocatePSmemory(size_t size, bool overridelimit) { - PSPRINT(" PS mem alloc: "); - PSPRINTLN(size); - // buffer uses effect data, check if there is enough space - if (!overridelimit && Segment::getUsedSegmentData() + size > MAX_SEGMENT_DATA) { - // not enough memory - PSPRINT(F("!!! Effect RAM depleted: ")); - DEBUG_PRINTF_P(PSTR("%d/%d !!!\n"), size, Segment::getUsedSegmentData()); - errorFlag = ERR_NORAM; - return nullptr; - } - void* buffer = calloc(size, sizeof(byte)); - if (buffer == nullptr) { - PSPRINT(F("!!! Memory allocation failed !!!")); - errorFlag = ERR_NORAM; - return nullptr; - } - Segment::addUsedSegmentData(size); - #ifdef WLED_DEBUG_PS - PSPRINT("Pointer address: 0x"); - Serial.println((uintptr_t)buffer, HEX); - #endif - return buffer; -} - -// deallocate memory and update data usage, use with care! -void deallocatePSmemory(void* dataptr, uint32_t size) { - PSPRINTLN("deallocating PSmemory:" + String(size)); - if (dataptr == nullptr) return; // safety check - free(dataptr); // note: setting pointer null must be done by caller, passing a reference to a cast void pointer is not possible - Segment::addUsedSegmentData(size <= Segment::getUsedSegmentData() ? -size : -Segment::getUsedSegmentData()); -} - -// Particle transition manager, creates/extends buffer if needed and handles transition memory-handover -void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint32_t numParticlesUsed, const uint8_t effectID) { - pmem = getPartMem(); - void* buffer = nullptr; - PSPRINTLN("PS MemManager"); - if (pmem) { // segment has a buffer - if (requestedParticles) { // request for a new buffer, this is an init call - PSPRINTLN("Buffer exists, request for particles: " + String(requestedParticles)); - pmem->transferParticles = true; // set flag to transfer particles - uint32_t requestsize = structSize * requestedParticles; // required buffer size - if (requestsize > pmem->buffersize) { // request is larger than buffer, try to extend it - if (Segment::getUsedSegmentData() + requestsize - pmem->buffersize <= MAX_SEGMENT_DATA) { // enough memory available to extend buffer - PSPRINTLN("Extending buffer"); - buffer = allocatePSmemory(requestsize, true); // calloc new memory in FX data, override limit (temporary buffer) - if (buffer) { // allocaction successful, copy old particles to new buffer - memcpy(buffer, pmem->particleMemPointer, pmem->buffersize); // copy old particle buffer note: only required if transition but copy is fast and rarely happens - deallocatePSmemory(pmem->particleMemPointer, pmem->buffersize); // free old memory - pmem->particleMemPointer = buffer; // set new buffer - pmem->buffersize = requestsize; // update buffer size - } - else - return nullptr; // no memory available - } - } - if (pmem->watchdog == 1) { // if a PS already exists during particle request, it kicked the watchdog in last frame, servicePSmem() adds 1 afterwards -> PS to PS transition - if (pmem->currentFX == effectID) // if the new effect is the same as the current one, do not transition: transferParticles is set above, so this will transfer all particles back if called during transition - pmem->inTransition = false; // reset transition flag - else - pmem->inTransition = effectID; // save the ID of the new effect (required to determine blur amount in rendering function) - PSPRINTLN("PS to PS transition"); - } - return pmem->particleMemPointer; // return the available buffer on init call - } - pmem->watchdog = 0; // kick watchdog - buffer = pmem->particleMemPointer; // buffer is already allocated - } - else { // if the id was not found create a buffer and add an element to the list - PSPRINTLN("New particle buffer request: " + String(requestedParticles)); - uint32_t requestsize = structSize * requestedParticles; // required buffer size - buffer = allocatePSmemory(requestsize, false); // allocate new memory - if (buffer) - partMemList.push_back({buffer, requestsize, 0, strip.getCurrSegmentId(), 0, 0, 0, false, true}); // add buffer to list, set flag to transfer/init the particles note: if pushback fails, it may crash - else - return nullptr; // there is no memory available TODO: if localbuffer is allocated, free it and try again, its no use having a buffer but no particles - pmem = getPartMem(); // get the pointer to the new element (check that it was added) - if (!pmem) { // something went wrong - free(buffer); - return nullptr; - } - return buffer; // directly return the buffer on init call - } - - // now we have a valid buffer, if this is a PS to PS FX transition: transfer particles slowly to new FX - if (!SEGMENT.isInTransition()) pmem->inTransition = false; // transition has ended, invoke final transfer - if (pmem->inTransition) { - uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer - uint16_t progress = SEGMENT.progress(); // transition progress - uint32_t newAvailable = 0; - if (SEGMENT.mode == effectID) { // new effect ID -> function was called from new FX - PSPRINTLN("new effect"); - newAvailable = (maxParticles * progress) >> 16; // update total particles available to this PS (newAvailable is guaranteed to be smaller than maxParticles) - if (newAvailable < 2) newAvailable = 2; // give 2 particle minimum (some FX may crash with less as they do i+1 access) - if (newAvailable > numParticlesUsed) newAvailable = numParticlesUsed; // limit to number of particles used, do not move the pointer anymore (will be set to base in final handover) - uint32_t bufferoffset = (maxParticles - 1) - newAvailable; // offset to new effect particles (in particle structs, not bytes) - if (bufferoffset < maxParticles) // safety check - buffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // new effect gets the end of the buffer - int32_t totransfer = newAvailable - availableToPS; // number of particles to transfer in this transition update - if (totransfer > 0) // safety check - particleHandover(buffer, structSize, totransfer); - } - else { // this was called from the old FX - PSPRINTLN("old effect"); - SEGMENT.loadOldPalette(); // load the old palette into segment palette - progress = 0xFFFFU - progress; // inverted transition progress - newAvailable = ((maxParticles * progress) >> 16); // result is guaranteed to be smaller than maxParticles - if (newAvailable > 0) newAvailable--; // -1 to avoid overlapping memory in 1D<->2D transitions - if (newAvailable < 2) newAvailable = 2; // give 2 particle minimum (some FX may crash with less as they do i+1 access) - // note: buffer pointer stays the same, number of available particles is reduced - } - availableToPS = newAvailable; - } else if (pmem->transferParticles) { // no PS transition, full buffer available - // transition ended (or blending is disabled) -> transfer all remaining particles - PSPRINTLN("PS transition ended, final particle handover"); - uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer - if (maxParticles > availableToPS) { // not all particles transferred yet - uint32_t totransfer = maxParticles - availableToPS; // transfer all remaining particles - if (totransfer <= maxParticles) // safety check - particleHandover(buffer, structSize, totransfer); - if (maxParticles > numParticlesUsed) { // FX uses less than max: move the already existing particles to the beginning of the buffer - uint32_t usedbytes = availableToPS * structSize; - int32_t bufferoffset = (maxParticles - 1) - availableToPS; // offset to existing particles (see above) - if (bufferoffset < (int)maxParticles) { // safety check - void* currentBuffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // pointer to current buffer start - memmove(buffer, currentBuffer, usedbytes); // move the existing particles to the beginning of the buffer - } - } - } - // kill unused particles so they do not re-appear when transitioning to next FX - //TODO: should this be done in the handover function? maybe with a "cleanup" parameter? - //TODO2: the memmove above should be done here (or in handover function): it should copy all alive particles to the beginning of the buffer (to TTL=0 particles maybe?) - // -> currently when moving form blobs to ballpit particles disappear - #ifndef WLED_DISABLE_PARTICLESYSTEM2D - if (structSize == sizeof(PSparticle)) { // 2D particle - PSparticle *particles = (PSparticle*)buffer; - for (uint32_t i = availableToPS; i < maxParticles; i++) { - particles[i].ttl = 0; // kill unused particles - } - } - else // 1D particle system - #endif - { - #ifndef WLED_DISABLE_PARTICLESYSTEM1D - PSparticle1D *particles = (PSparticle1D*)buffer; - for (uint32_t i = availableToPS; i < maxParticles; i++) { - particles[i].ttl = 0; // kill unused particles - } - #endif - } - availableToPS = maxParticles; // now all particles are available to new FX - PSPRINTLN("final available particles: " + String(availableToPS)); - pmem->particleType = structSize; // update particle type - pmem->transferParticles = false; - pmem->finalTransfer = true; // let rendering function update its buffer if required - pmem->currentFX = effectID; // FX has now settled in, update the FX ID to track future transitions - } - else // no transition - pmem->finalTransfer = false; - - #ifdef WLED_DEBUG_PS - PSPRINT(" Particle memory Pointer address: 0x"); - Serial.println((uintptr_t)buffer, HEX); - #endif - return buffer; -} - -// (re)initialize particles in the particle buffer for use in the new FX -void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { - if (pmem->particleType != structSize) { // check if we are being handed over from a different system (1D<->2D), clear buffer if so - memset(buffer, 0, numToTransfer * structSize); // clear buffer - } - uint16_t maxTTL = 0; - uint32_t TTLrandom = 0; - maxTTL = ((unsigned)strip.getTransition() << 1) / FRAMETIME_FIXED; // tie TTL to transition time: limit to double the transition time + some randomness - #ifndef WLED_DISABLE_PARTICLESYSTEM2D - if (structSize == sizeof(PSparticle)) { // 2D particle - PSparticle *particles = (PSparticle *)buffer; - for (int32_t i = 0; i < numToTransfer; i++) { - if (blendingStyle == BLEND_STYLE_FADE) { - if (particles[i].ttl > maxTTL) - particles[i].ttl = maxTTL + hw_random16(150); // reduce TTL so it will die soon - } - else - particles[i].ttl = 0; // kill transferred particles if not using fade blending style - particles[i].sat = 255; // full saturation - } - } - else // 1D particle system - #endif - { - #ifndef WLED_DISABLE_PARTICLESYSTEM1D - PSparticle1D *particles = (PSparticle1D *)buffer; - for (int32_t i = 0; i < numToTransfer; i++) { - if (blendingStyle == BLEND_STYLE_FADE) { - if (particles[i].ttl > maxTTL) - particles[i].ttl = maxTTL + hw_random16(150); // reduce TTL so it will die soon - } - else - particles[i].ttl = 0; // kill transferred particles if not using fade blending style - } - #endif - } -} - -// update number of particles to use, limit to allocated (= particles allocated by the calling system) in case more are available in the buffer -void updateUsedParticles(const uint32_t allocated, const uint32_t available, const uint8_t percentage, uint32_t &used) { - uint32_t wantsToUse = 1 + ((allocated * ((uint32_t)percentage + 1)) >> 8); // always give 1 particle minimum - used = max((uint32_t)2, min(available, wantsToUse)); // limit to available particles, use a minimum of 2 -} - -// check if a segment is partially overlapping with an underlying segment (used to enable overlay rendering i.e. adding instead of overwriting pixels) -bool segmentIsOverlay(void) { // TODO: this only needs to be checked when segment is created, could move this to segment class or PS init - unsigned segID = strip.getCurrSegmentId(); - if (segID == 0) return false; // is base segment, no overlay - unsigned newStartX = strip._segments[segID].start; - unsigned newEndX = strip._segments[segID].stop; - unsigned newStartY = strip._segments[segID].startY; - unsigned newEndY = strip._segments[segID].stopY; - - // Check for overlap with all previous segments - for (unsigned i = 0; i < segID; i++) { - if (strip._segments[i].freeze) continue; // skip inactive segments - unsigned startX = strip._segments[i].start; - unsigned endX = strip._segments[i].stop; - unsigned startY = strip._segments[i].startY; - unsigned endY = strip._segments[i].stopY; - - if (newStartX < endX && newEndX > startX && // x-range overlap - newStartY < endY && newEndY > startY) { // y-range overlap - return true; - } - } - - return false; // No overlap detected -} - -// get the pointer to the particle memory for the segment -partMem* getPartMem(void) { - uint8_t segID = strip.getCurrSegmentId(); - for (partMem &pmem : partMemList) { - if (pmem.id == segID) { - return &pmem; - } - } - return nullptr; -} - -// function to update the framebuffer and renderbuffer -void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer, bool initialize) { - PSPRINTLN("updateRenderingBuffer"); - uint16_t& targetBufferSize = isFramebuffer ? frameBufferSize : renderBufferSize; // corresponding buffer size - - // if (isFramebuffer) return; // debug/testing only: disable frame-buffer - - if (targetBufferSize < requiredpixels) { // check current buffer size - CRGB** targetBuffer = isFramebuffer ? &framebuffer : &renderbuffer; // pointer to target buffer - if (*targetBuffer || initialize) { // update only if initilizing or if buffer exists (prevents repeatet allocation attempts if initial alloc failed) - if (*targetBuffer) // buffer exists, free it - deallocatePSmemory((void*)(*targetBuffer), targetBufferSize * sizeof(CRGB)); - *targetBuffer = reinterpret_cast(allocatePSmemory(requiredpixels * sizeof(CRGB), false)); - if (*targetBuffer) - targetBufferSize = requiredpixels; - else - targetBufferSize = 0; - } - } -} - -// service the particle system memory, free memory if idle too long -// note: doing it this way makes it independent of the implementation of segment management but is not the most memory efficient way -void servicePSmem() { - // Increment watchdog for each entry and deallocate if idle too long (i.e. no PS running on that segment) - if (partMemList.size() > 0) { - for (size_t i = 0; i < partMemList.size(); i++) { - if (strip.getSegmentsNum() > i) { // segment still exists - if (strip._segments[i].freeze) continue; // skip frozen segments (incrementing watchdog will delete memory, leading to crash) - } - partMemList[i].watchdog++; // Increment watchdog counter - PSPRINT("pmem servic. list size: "); - PSPRINT(partMemList.size()); - PSPRINT(" element: "); - PSPRINT(i); - PSPRINT(" watchdog: "); - PSPRINTLN(partMemList[i].watchdog); - if (partMemList[i].watchdog > MAX_MEMIDLE) { - deallocatePSmemory(partMemList[i].particleMemPointer, partMemList[i].buffersize); // Free memory - partMemList.erase(partMemList.begin() + i); // Remove entry - //partMemList.shrink_to_fit(); // partMemList is small, memory operations should be unproblematic (this may lead to mem fragmentation, removed for now) - } - } - } - else { // no particle system running, release buffer memory - if (framebuffer) { - deallocatePSmemory((void*)framebuffer, frameBufferSize * sizeof(CRGB)); // free the buffers - framebuffer = nullptr; - frameBufferSize = 0; - } - if (renderbuffer) { - deallocatePSmemory((void*)renderbuffer, renderBufferSize * sizeof(CRGB)); - renderbuffer = nullptr; - renderBufferSize = 0; - } - } -} - -// transfer the frame buffer to the segment and handle transitional rendering (both FX render to the same buffer so they mix) -void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { - if (!framebuffer) return; // no buffer, nothing to transfer - PSPRINT(" xfer buf "); - #ifndef WLED_DISABLE_MODE_BLEND - bool tempBlend = SEGMENT.getmodeBlend(); - if (pmem->inTransition && blendingStyle == BLEND_STYLE_FADE) { - SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (using local buffer to do PS blending) - } - #endif - if (height) { // is 2D, 1D passes height = 0 - for (uint32_t y = 0; y < height; y++) { - int index = y * width; // current row index for 1D buffer - for (uint32_t x = 0; x < width; x++) { - CRGB *c = &framebuffer[index++]; - uint32_t clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color - if (useAdditiveTransfer) { - uint32_t segmentcolor = SEGMENT.getPixelColorXY((int)x, (int)y); - CRGB segmentRGB = CRGB(segmentcolor); - if (clr == 0) // frame buffer is black, just update the framebuffer - *c = segmentRGB; - else { // color to add to segment is not black - if (segmentcolor) { - fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black - clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) and set the segment - } - SEGMENT.setPixelColorXY((int)x, (int)y, clr); // save back to segment after adding local buffer - } - } - //if (clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requires proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. - else - SEGMENT.setPixelColorXY((int)x, (int)y, clr); - } - } - } else { // 1D system - for (uint32_t x = 0; x < width; x++) { - CRGB *c = &framebuffer[x]; - uint32_t clr = RGBW32(c->r,c->g,c->b,0); - if (useAdditiveTransfer) { - uint32_t segmentcolor = SEGMENT.getPixelColor((int)x);; - CRGB segmentRGB = CRGB(segmentcolor); - if (clr == 0) // frame buffer is black, just load the color (for next frame) - *c = segmentRGB; - else { // color to add to segment is not black - if (segmentcolor) { - fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black - clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) - } - SEGMENT.setPixelColor((int)x, clr); // save back to segment after adding local buffer - } - } - //if (color > 0) // not black - else - SEGMENT.setPixelColor((int)x, clr); - } - } - #ifndef WLED_DISABLE_MODE_BLEND - SEGMENT.modeBlend(tempBlend); // restore blending mode - #endif -} - #endif // !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 099b96a85..695a3a028 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -30,28 +30,6 @@ #define PSPRINTLN(x) #endif -// memory and transition manager -struct partMem { - void* particleMemPointer; // pointer to particle memory - uint32_t buffersize; // buffer size in bytes - uint8_t particleType; // type of particles currently in memory: 0 = none, particle struct size otherwise (required for 1D<->2D transitions) - uint8_t id; // ID of segment this memory belongs to - uint8_t watchdog; // counter to handle deallocation - uint8_t inTransition; // to track PS to PS FX transitions (is set to new FX ID during transitions), not set if not both FX are PS FX - uint8_t currentFX; // current FX ID, is set when transition is complete, used to detect back and forth transitions - bool finalTransfer; // used to update buffer in rendering function after transition has ended - bool transferParticles; // if set, particles in buffer are transferred to new FX -}; - -void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint32_t numParticlesUsed, const uint8_t effectID); // update particle memory pointer, handles memory transitions -void particleHandover(void *buffer, size_t structSize, int32_t numParticles); -void updateUsedParticles(const uint32_t allocated, const uint32_t available, const uint8_t percentage, uint32_t &used); -bool segmentIsOverlay(void); // check if segment is fully overlapping with at least one underlying segment -partMem* getPartMem(void); // returns pointer to memory struct for current segment or nullptr -void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer, bool initialize); // allocate CRGB rendering buffer, update size if needed -void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer = false); // transfer the buffer to the segment (supports 1D and 2D) -void servicePSmem(); // increments watchdog, frees memory if idle too long - // limit speed of particles (used in 1D and 2D) static inline int32_t limitSpeed(const int32_t speed) { return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); // note: this is slightly faster than using min/max at the cost of 50bytes of flash @@ -60,7 +38,7 @@ static inline int32_t limitSpeed(const int32_t speed) { #ifndef WLED_DISABLE_PARTICLESYSTEM2D // memory allocation -#define ESP8266_MAXPARTICLES 300 // enough up to 20x20 pixels +#define ESP8266_MAXPARTICLES 256 // enough up to 16x16 pixels #define ESP8266_MAXSOURCES 24 #define ESP32S2_MAXPARTICLES 1024 // enough up to 32x32 pixels #define ESP32S2_MAXSOURCES 64 @@ -178,7 +156,6 @@ public: void pointAttractor(const uint32_t particleindex, PSparticle &attractor, const uint8_t strength, const bool swallow); // set options note: inlining the set function uses more flash so dont optimize void setUsedParticles(const uint8_t percentage); // set the percentage of particles used in the system, 255=100% - inline uint32_t getAvailableParticles(void) { return availableParticles; } // available particles in the buffer, use this to check if buffer changed during FX init void setCollisionHardness(const uint8_t hardness); // hardness for particle collisions (255 means full hard) void setWallHardness(const uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set void setWallRoughness(const uint8_t roughness); // wall roughness randomizes wall collisions @@ -210,12 +187,12 @@ public: private: //rendering functions - void ParticleSys_render(); + void render(); [[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB& color, const bool wrapX, const bool wrapY); //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); - [[gnu::hot]] void collideParticles(PSparticle &particle1, PSparticle &particle2, const int32_t dx, const int32_t dy, const int32_t collDistSq); + [[gnu::hot]] void collideParticles(PSparticle &particle1, PSparticle &particle2, const int32_t dx, const int32_t dy, const uint32_t collDistSq); void fireParticleupdate(); //utility functions void updatePSpointers(const bool isadvanced, const bool sizecontrol); // update the data pointers to current segment data space @@ -223,9 +200,9 @@ private: void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); [[gnu::hot]] void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall // note: variables that are accessed often are 32bit for speed + CRGB *framebuffer; // local frame buffer for rendering PSsettings2D particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above - uint32_t numParticles; // total number of particles allocated by this system note: during transitions, less are available, use availableParticles - uint32_t availableParticles; // number of particles available for use (can be more or less than numParticles, assigned by memory manager) + uint32_t numParticles; // total number of particles allocated by this system uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; uint32_t wallHardness; @@ -233,7 +210,6 @@ private: uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection (32bit for speed) uint16_t collisionStartIdx; // particle array start index for collision detection uint8_t fireIntesity = 0; // fire intensity, used for fire mode (flash use optimization, better than passing an argument to render function) - uint8_t fractionOfParticlesUsed; // percentage of particles used in the system (255=100%), used during transition updates uint8_t forcecounter; // counter for globally applied forces uint8_t gforcecounter; // counter for global gravity int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) @@ -241,8 +217,6 @@ private: uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles) uint8_t motionBlur; // motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 uint8_t smearBlur; // 2D smeared blurring of full frame - uint8_t effectID; // ID of the effect that is using this particle system, used for transitions - uint32_t lastRender; // last time the particles were rendered, intermediate fix for speedup }; void blur2D(CRGB *colorbuffer, const uint32_t xsize, uint32_t ysize, const uint32_t xblur, const uint32_t yblur, const uint32_t xstart = 0, uint32_t ystart = 0, const bool isparticle = false); @@ -258,7 +232,7 @@ bool allocateParticleSystemMemory2D(const uint32_t numparticles, const uint32_t //////////////////////// #ifndef WLED_DISABLE_PARTICLESYSTEM1D // memory allocation -#define ESP8266_MAXPARTICLES_1D 450 +#define ESP8266_MAXPARTICLES_1D 320 #define ESP8266_MAXSOURCES_1D 16 #define ESP32S2_MAXPARTICLES_1D 1300 #define ESP32S2_MAXSOURCES_1D 32 @@ -349,7 +323,6 @@ public: void applyFriction(const int32_t coefficient); // apply friction to all used particles // set options void setUsedParticles(const uint8_t percentage); // set the percentage of particles used in the system, 255=100% - inline uint32_t getAvailableParticles(void) { return availableParticles; } // available particles in the buffer, use this to check if buffer changed during FX init void setWallHardness(const uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set void setSize(const uint32_t x); //set particle system size (= strip length) void setWrap(const bool enable); @@ -377,23 +350,24 @@ public: private: //rendering functions - void ParticleSys_render(void); + void render(void); [[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB &color, const bool wrap); //paricle physics applied by system if flags are set void applyGravity(); // applies gravity to all particles void handleCollisions(); - [[gnu::hot]] void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const int32_t collisiondistance); + [[gnu::hot]] void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, const int32_t dx, const uint32_t dx_abs, const uint32_t collisiondistance); //utility functions void updatePSpointers(const bool isadvanced); // update the data pointers to current segment data space //void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control [[gnu::hot]] void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall // note: variables that are accessed often are 32bit for speed + #ifndef ESP8266 + CRGB *framebuffer; // local frame buffer for rendering + #endif PSsettings1D particlesettings; // settings used when updating particles - uint32_t numParticles; // total number of particles allocated by this system note: never use more than this, even if more are available (only this many advanced particles are allocated) - uint32_t availableParticles; // number of particles available for use (can be more or less than numParticles, assigned by memory manager) - uint8_t fractionOfParticlesUsed; // percentage of particles used in the system (255=100%), used during transition updates + uint32_t numParticles; // total number of particles allocated by this system uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster int32_t collisionHardness; uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection @@ -406,8 +380,6 @@ private: uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations uint8_t smearBlur; // smeared blurring of full frame - uint8_t effectID; // ID of the effect that is using this particle system, used for transitions - uint32_t lastRender; // last time the particles were rendered, intermediate fix for speedup }; bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles = 255, const uint32_t additionalbytes = 0, const bool advanced = false); From 8baf0fdef75a1d7fbc47aba8c71df511661a6713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Sun, 20 Apr 2025 11:44:26 +0200 Subject: [PATCH 055/153] Add UI logic to hide ArduinoOTA checkbox and remove configuration items --- wled00/cfg.cpp | 8 ++++++++ wled00/data/settings_sec.htm | 2 +- wled00/set.cpp | 2 ++ wled00/wled.h | 4 ++++ wled00/xml.cpp | 4 ++++ 5 files changed, 19 insertions(+), 1 deletion(-) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index fa0397fc6..10db56855 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -611,7 +611,9 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { if (pwdCorrect) { //only accept these values from cfg.json if ota is unlocked (else from wsec.json) CJSON(otaLock, ota[F("lock")]); CJSON(wifiLock, ota[F("lock-wifi")]); + #ifndef WLED_DISABLE_OTA CJSON(aOtaEnabled, ota[F("aota")]); + #endif getStringFromJson(otaPass, pwd, 33); //normally not present due to security } @@ -1103,7 +1105,9 @@ void serializeConfig(JsonObject root) { ota[F("lock")] = otaLock; ota[F("lock-wifi")] = wifiLock; ota[F("pskl")] = strlen(otaPass); + #ifndef WLED_DISABLE_OTA ota[F("aota")] = aOtaEnabled; + #endif #ifdef WLED_ENABLE_DMX JsonObject dmx = root.createNestedObject("dmx"); @@ -1174,7 +1178,9 @@ bool deserializeConfigSec() { getStringFromJson(otaPass, ota[F("pwd")], 33); CJSON(otaLock, ota[F("lock")]); CJSON(wifiLock, ota[F("lock-wifi")]); + #ifndef WLED_DISABLE_OTA CJSON(aOtaEnabled, ota[F("aota")]); + #endif releaseJSONBufferLock(); return true; @@ -1214,7 +1220,9 @@ void serializeConfigSec() { ota[F("pwd")] = otaPass; ota[F("lock")] = otaLock; ota[F("lock-wifi")] = wifiLock; + #ifndef WLED_DISABLE_OTA ota[F("aota")] = aOtaEnabled; + #endif File f = WLED_FS.open(FPSTR(s_wsec_json), "w"); if (f) serializeJson(root, f); diff --git a/wled00/data/settings_sec.htm b/wled00/data/settings_sec.htm index 5ac0dd24f..2db798cf4 100644 --- a/wled00/data/settings_sec.htm +++ b/wled00/data/settings_sec.htm @@ -56,7 +56,7 @@

Software Update


- Enable ArduinoOTA: +
Enable ArduinoOTA:

Backup & Restore

⚠ Restoring presets/configuration will OVERWRITE your current presets/configuration.
diff --git a/wled00/set.cpp b/wled00/set.cpp index c817f2553..a40d90bc4 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -593,7 +593,9 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) { otaLock = request->hasArg(F("NO")); wifiLock = request->hasArg(F("OW")); + #ifndef WLED_DISABLE_OTA aOtaEnabled = request->hasArg(F("AO")); + #endif //createEditHandler(correctPIN && !otaLock); } } diff --git a/wled00/wled.h b/wled00/wled.h index 8926967ff..ebeb70912 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -584,7 +584,11 @@ WLED_GLOBAL bool otaLock _INIT(true); // prevents OTA firmware update WLED_GLOBAL bool otaLock _INIT(false); // prevents OTA firmware updates without password. ALWAYS enable if system exposed to any public networks #endif WLED_GLOBAL bool wifiLock _INIT(false); // prevents access to WiFi settings when OTA lock is enabled +#ifndef WLED_DISABLE_OTA WLED_GLOBAL bool aOtaEnabled _INIT(true); // ArduinoOTA allows easy updates directly from the IDE. Careful, it does not auto-disable when OTA lock is on +#else +WLED_GLOBAL bool aOtaEnabled _INIT(false); // ArduinoOTA allows easy updates directly from the IDE. Careful, it does not auto-disable when OTA lock is on +#endif WLED_GLOBAL char settingsPIN[5] _INIT(WLED_PIN); // PIN for settings pages WLED_GLOBAL bool correctPIN _INIT(!strlen(settingsPIN)); WLED_GLOBAL unsigned long lastEditTime _INIT(0); diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 19868d01d..b4c2a3fd4 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -595,6 +595,10 @@ void getSettingsJS(byte subPage, Print& settingsScript) snprintf_P(tmp_buf,sizeof(tmp_buf),PSTR("WLED %s (build %d)"),versionString,VERSION); printSetClassElementHTML(settingsScript,PSTR("sip"),0,tmp_buf); settingsScript.printf_P(PSTR("sd=\"%s\";"), serverDescription); + #ifdef WLED_DISABLE_OTA + //hide settings if not compiled + settingsScript.print(F("toggle('aOTA');")); // hide ArduinoOTA checkbox + #endif } #ifdef WLED_ENABLE_DMX // include only if DMX is enabled From ee9ac947a1f8c780938f2dd102351c20cd7bb86a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Tue, 22 Apr 2025 22:37:18 +0200 Subject: [PATCH 056/153] Segment layering & effect blending improvements - Photoshop-style segment/layer blending - return of strip ABL - remove global LED buffer in favour of segment-local buffer - new effect blending modes/transitions - custom palettes moved out of WS2812FX class - increased limits (matrix size, LED RAM) - added "rainbow"-mode colorwheel - replaced palettes with gamma unmodified ones - move gamma adjustment to last step before sending to LEDs - Segment & WS2812FX class reorganisation (mutable members, reordered members, protected members) --- usermods/Analog_Clock/Analog_Clock.cpp | 6 +- usermods/Cronixie/Cronixie.cpp | 2 +- wled00/FX.cpp | 29 +- wled00/FX.h | 712 ++++----- wled00/FX_2Dfcn.cpp | 240 +-- wled00/FX_fcn.cpp | 1848 +++++++++++------------- wled00/bus_manager.cpp | 293 ++-- wled00/bus_manager.h | 43 +- wled00/cfg.cpp | 229 ++- wled00/colors.cpp | 52 +- wled00/const.h | 137 +- wled00/data/index.css | 4 +- wled00/data/index.htm | 46 +- wled00/data/index.js | 89 +- wled00/data/settings_leds.htm | 10 +- wled00/e131.cpp | 43 +- wled00/fcn_declare.h | 31 +- wled00/file.cpp | 28 +- wled00/ir.cpp | 10 +- wled00/json.cpp | 242 ++-- wled00/led.cpp | 55 +- wled00/mqtt.cpp | 11 +- wled00/overlay.cpp | 3 +- wled00/palettes.h | 1117 ++++++-------- wled00/presets.cpp | 33 +- wled00/set.cpp | 142 +- wled00/udp.cpp | 81 +- wled00/util.cpp | 90 +- wled00/wled.cpp | 3 + wled00/wled.h | 3 + wled00/wled_eeprom.cpp | 2 +- wled00/xml.cpp | 20 +- 32 files changed, 2723 insertions(+), 2931 deletions(-) mode change 100644 => 100755 wled00/FX.h mode change 100644 => 100755 wled00/FX_fcn.cpp mode change 100644 => 100755 wled00/palettes.h diff --git a/usermods/Analog_Clock/Analog_Clock.cpp b/usermods/Analog_Clock/Analog_Clock.cpp index 970ba7224..d3a2b73b8 100644 --- a/usermods/Analog_Clock/Analog_Clock.cpp +++ b/usermods/Analog_Clock/Analog_Clock.cpp @@ -102,9 +102,9 @@ private: void secondsEffectSineFade(int16_t secondLed, Toki::Time const& time) { uint32_t ms = time.ms % 1000; uint8_t b0 = (cos8_t(ms * 64 / 1000) - 128) * 2; - setPixelColor(secondLed, gamma32(scale32(secondColor, b0))); + setPixelColor(secondLed, scale32(secondColor, b0)); uint8_t b1 = (sin8_t(ms * 64 / 1000) - 128) * 2; - setPixelColor(inc(secondLed, 1, secondsSegment), gamma32(scale32(secondColor, b1))); + setPixelColor(inc(secondLed, 1, secondsSegment), scale32(secondColor, b1)); } static inline uint32_t qadd32(uint32_t c1, uint32_t c2) { @@ -191,7 +191,7 @@ public: // for (uint16_t i = 1; i < secondsTrail + 1; ++i) { // uint16_t trailLed = dec(secondLed, i, secondsSegment); // uint8_t trailBright = 255 / (secondsTrail + 1) * (secondsTrail - i + 1); - // setPixelColor(trailLed, gamma32(scale32(secondColor, trailBright))); + // setPixelColor(trailLed, scale32(secondColor, trailBright)); // } } diff --git a/usermods/Cronixie/Cronixie.cpp b/usermods/Cronixie/Cronixie.cpp index 05557de0f..e0a3bbee7 100644 --- a/usermods/Cronixie/Cronixie.cpp +++ b/usermods/Cronixie/Cronixie.cpp @@ -247,7 +247,7 @@ class UsermodCronixie : public Usermod { if (backlight && _digitOut[i] <11) { - uint32_t col = gamma32(strip.getSegment(0).colors[1]); + uint32_t col = strip.getSegment(0).colors[1]; for (uint16_t j=o; j< o+10; j++) { if (j != excl) strip.setPixelColor(j, col); } diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 3daf2c0fd..4a8e8f71a 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -5053,7 +5053,11 @@ uint16_t mode_2Dfirenoise(void) { // firenoise2d. By Andrew Tuline unsigned yscale = SEGMENT.speed*8; unsigned indexx = 0; - CRGBPalette16 pal = SEGMENT.check1 ? SEGPALETTE : SEGMENT.loadPalette(pal, 35); + //CRGBPalette16 pal = SEGMENT.check1 ? SEGPALETTE : SEGMENT.loadPalette(pal, 35); + CRGBPalette16 pal = SEGMENT.check1 ? SEGPALETTE : CRGBPalette16(CRGB::Black, CRGB::Black, CRGB::Black, CRGB::Black, + CRGB::Red, CRGB::Red, CRGB::Red, CRGB::DarkOrange, + CRGB::DarkOrange,CRGB::DarkOrange, CRGB::Orange, CRGB::Orange, + CRGB::Yellow, CRGB::Orange, CRGB::Yellow, CRGB::Yellow); for (int j=0; j < cols; j++) { for (int i=0; i < rows; i++) { indexx = perlin8(j*yscale*rows/255, i*xscale+strip.now/4); // We're moving along our Perlin map. @@ -6088,7 +6092,8 @@ uint16_t mode_2Dscrollingtext(void) { case 5: letterWidth = 5; letterHeight = 12; break; } // letters are rotated - if (((SEGMENT.custom3+1)>>3) % 2) { + const int8_t rotate = map(SEGMENT.custom3, 0, 31, -2, 2); + if (rotate == 1 || rotate == -1) { rotLH = letterWidth; rotLW = letterHeight; } else { @@ -6126,6 +6131,7 @@ uint16_t mode_2Dscrollingtext(void) { else if (!strncmp_P(text,PSTR("#DD"),3)) sprintf (text, zero? ("%02d") : ("%d"), day(localTime)); else if (!strncmp_P(text,PSTR("#DAY"),4)) sprintf (text, ("%s") , dayShortStr(day(localTime))); else if (!strncmp_P(text,PSTR("#DDDD"),5)) sprintf (text, ("%s") , dayStr(day(localTime))); + else if (!strncmp_P(text,PSTR("#DAYL"),5)) sprintf (text, ("%s") , dayStr(day(localTime))); else if (!strncmp_P(text,PSTR("#MO"),3)) sprintf (text, zero? ("%02d") : ("%d"), month(localTime)); else if (!strncmp_P(text,PSTR("#MON"),4)) sprintf (text, ("%s") , monthShortStr(month(localTime))); else if (!strncmp_P(text,PSTR("#MMMM"),5)) sprintf (text, ("%s") , monthStr(month(localTime))); @@ -6159,27 +6165,28 @@ uint16_t mode_2Dscrollingtext(void) { SEGENV.step = strip.now + map(SEGMENT.speed, 0, 255, 250, 50); // shift letters every ~250ms to ~50ms } - if (!SEGMENT.check2) SEGMENT.fade_out(255 - (SEGMENT.custom1>>4)); // trail - bool usePaletteGradient = false; + SEGMENT.fade_out(255 - (SEGMENT.custom1>>4)); // trail uint32_t col1 = SEGMENT.color_from_palette(SEGENV.aux1, false, PALETTE_SOLID_WRAP, 0); uint32_t col2 = BLACK; + // if gradient is selected and palette is default (0) drawCharacter() uses gradient from SEGCOLOR(0) to SEGCOLOR(2) + // otherwise col2 == BLACK means use currently selected palette for gradient + // if gradient is not selected set both colors the same if (SEGMENT.check1) { // use gradient - if(SEGMENT.palette == 0) { // use colors for gradient - col1 = SEGCOLOR(0); - col2 = SEGCOLOR(2); + if (SEGMENT.palette == 0) { // use colors for gradient + col1 = SEGCOLOR(0); + col2 = SEGCOLOR(2); } - else usePaletteGradient = true; - } + } else col2 = col1; // force characters to use single color (from palette) for (int i = 0; i < numberOfLetters; i++) { int xoffset = int(cols) - int(SEGENV.aux0) + rotLW*i; if (xoffset + rotLW < 0) continue; // don't draw characters off-screen - SEGMENT.drawCharacter(text[i], xoffset, yoffset, letterWidth, letterHeight, col1, col2, map(SEGMENT.custom3, 0, 31, -2, 2), usePaletteGradient); + SEGMENT.drawCharacter(text[i], xoffset, yoffset, letterWidth, letterHeight, col1, col2, rotate); } return FRAMETIME; } -static const char _data_FX_MODE_2DSCROLLTEXT[] PROGMEM = "Scrolling Text@!,Y Offset,Trail,Font size,Rotate,Gradient,Overlay,Reverse;!,!,Gradient;!;2;ix=128,c1=0,rev=0,mi=0,rY=0,mY=0"; +static const char _data_FX_MODE_2DSCROLLTEXT[] PROGMEM = "Scrolling Text@!,Y Offset,Trail,Font size,Rotate,Gradient,,Reverse;!,!,Gradient;!;2;ix=128,c1=0,rev=0,mi=0,rY=0,mY=0"; //////////////////////////// diff --git a/wled00/FX.h b/wled00/FX.h old mode 100644 new mode 100755 index 6481ff757..c060c868d --- a/wled00/FX.h +++ b/wled00/FX.h @@ -19,8 +19,24 @@ #include #include "wled.h" -#include "const.h" -#include "bus_manager.h" +#ifdef WLED_DEBUG + // enable additional debug output + #if defined(WLED_DEBUG_HOST) + #include "net_debug.h" + #define DEBUGOUT NetDebug + #else + #define DEBUGOUT Serial + #endif + #define DEBUGFX_PRINT(x) DEBUGOUT.print(x) + #define DEBUGFX_PRINTLN(x) DEBUGOUT.println(x) + #define DEBUGFX_PRINTF(x...) DEBUGOUT.printf(x) + #define DEBUGFX_PRINTF_P(x...) DEBUGOUT.printf_P(x) +#else + #define DEBUGFX_PRINT(x) + #define DEBUGFX_PRINTLN(x) + #define DEBUGFX_PRINTF(x...) + #define DEBUGFX_PRINTF_P(x...) +#endif #define FASTLED_INTERNAL //remove annoying pragma messages #define USE_GET_MILLISECOND_TIMER @@ -88,11 +104,13 @@ extern byte realtimeMode; // used in getMappedPixelIndex() /* How much data bytes each segment should max allocate to leave enough space for other segments, assuming each segment uses the same amount of data. 256 for ESP8266, 640 for ESP32. */ -#define FAIR_DATA_PER_SEG (MAX_SEGMENT_DATA / strip.getMaxSegments()) +#define FAIR_DATA_PER_SEG (MAX_SEGMENT_DATA / WS2812FX::getMaxSegments()) + +#define MIN_SHOW_DELAY (_frametime < 16 ? 8 : 15) #define NUM_COLORS 3 /* number of colors per segment */ -#define SEGMENT strip._segments[strip.getCurrSegmentId()] -#define SEGENV strip._segments[strip.getCurrSegmentId()] +#define SEGMENT (*strip._currentSegment) +#define SEGENV (*strip._currentSegment) #define SEGCOLOR(x) Segment::getCurrentColor(x) #define SEGPALETTE Segment::getCurrentPalette() #define SEGLEN Segment::vLength() @@ -143,10 +161,10 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_RAINBOW 8 #define FX_MODE_RAINBOW_CYCLE 9 #define FX_MODE_SCAN 10 -#define FX_MODE_DUAL_SCAN 11 +#define FX_MODE_DUAL_SCAN 11 // candidate for removal (use Scan) #define FX_MODE_FADE 12 #define FX_MODE_THEATER_CHASE 13 -#define FX_MODE_THEATER_CHASE_RAINBOW 14 +#define FX_MODE_THEATER_CHASE_RAINBOW 14 // candidate for removal (use Theater) #define FX_MODE_RUNNING_LIGHTS 15 #define FX_MODE_SAW 16 #define FX_MODE_TWINKLE 17 @@ -169,7 +187,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_COLORFUL 34 #define FX_MODE_TRAFFIC_LIGHT 35 #define FX_MODE_COLOR_SWEEP_RANDOM 36 -#define FX_MODE_RUNNING_COLOR 37 +#define FX_MODE_RUNNING_COLOR 37 // candidate for removal (use Theater) #define FX_MODE_AURORA 38 #define FX_MODE_RUNNING_RANDOM 39 #define FX_MODE_LARSON_SCANNER 40 @@ -184,7 +202,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_FAIRY 49 //was Police All prior to 0.13.0-b6 (use "Two Dots" with Red/Blue and full intensity) #define FX_MODE_TWO_DOTS 50 #define FX_MODE_FAIRYTWINKLE 51 //was Two Areas prior to 0.13.0-b6 (use "Two Dots" with full intensity) -#define FX_MODE_RUNNING_DUAL 52 +#define FX_MODE_RUNNING_DUAL 52 // candidate for removal (use Running) #define FX_MODE_IMAGE 53 #define FX_MODE_TRICOLOR_CHASE 54 #define FX_MODE_TRICOLOR_WIPE 55 @@ -209,7 +227,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_COLORTWINKLE 74 #define FX_MODE_LAKE 75 #define FX_MODE_METEOR 76 -//#define FX_MODE_METEOR_SMOOTH 77 // merged with meteor +//#define FX_MODE_METEOR_SMOOTH 77 // replaced by Meteor #define FX_MODE_RAILWAY 78 #define FX_MODE_RIPPLE 79 #define FX_MODE_TWINKLEFOX 80 @@ -225,16 +243,16 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_EXPLODING_FIREWORKS 90 #define FX_MODE_BOUNCINGBALLS 91 #define FX_MODE_SINELON 92 -#define FX_MODE_SINELON_DUAL 93 -#define FX_MODE_SINELON_RAINBOW 94 +#define FX_MODE_SINELON_DUAL 93 // candidate for removal (use sinelon) +#define FX_MODE_SINELON_RAINBOW 94 // candidate for removal (use sinelon) #define FX_MODE_POPCORN 95 #define FX_MODE_DRIP 96 #define FX_MODE_PLASMA 97 #define FX_MODE_PERCENT 98 -#define FX_MODE_RIPPLE_RAINBOW 99 +#define FX_MODE_RIPPLE_RAINBOW 99 // candidate for removal (use ripple) #define FX_MODE_HEARTBEAT 100 #define FX_MODE_PACIFICA 101 -#define FX_MODE_CANDLE_MULTI 102 +#define FX_MODE_CANDLE_MULTI 102 // candidate for removal (use candle with multi select) #define FX_MODE_SOLID_GLITTER 103 // candidate for removal (use glitter) #define FX_MODE_SUNRISE 104 #define FX_MODE_PHASED 105 @@ -323,6 +341,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_WAVESINS 184 #define FX_MODE_ROCKTAVES 185 #define FX_MODE_2DAKEMI 186 + #define FX_MODE_PARTICLEVOLCANO 187 #define FX_MODE_PARTICLEFIRE 188 #define FX_MODE_PARTICLEFIREWORKS 189 @@ -360,12 +379,18 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define BLEND_STYLE_FAIRY_DUST 0x01 // universal #define BLEND_STYLE_SWIPE_RIGHT 0x02 // 1D or 2D #define BLEND_STYLE_SWIPE_LEFT 0x03 // 1D or 2D -#define BLEND_STYLE_PINCH_OUT 0x04 // 1D or 2D +#define BLEND_STYLE_OUTSIDE_IN 0x04 // 1D or 2D #define BLEND_STYLE_INSIDE_OUT 0x05 // 1D or 2D #define BLEND_STYLE_SWIPE_UP 0x06 // 2D #define BLEND_STYLE_SWIPE_DOWN 0x07 // 2D #define BLEND_STYLE_OPEN_H 0x08 // 2D #define BLEND_STYLE_OPEN_V 0x09 // 2D +#define BLEND_STYLE_SWIPE_TL 0x0A // 2D +#define BLEND_STYLE_SWIPE_TR 0x0B // 2D +#define BLEND_STYLE_SWIPE_BR 0x0C // 2D +#define BLEND_STYLE_SWIPE_BL 0x0D // 2D +#define BLEND_STYLE_CIRCULAR_OUT 0x0E // 2D +#define BLEND_STYLE_CIRCULAR_IN 0x0F // 2D // as there are many push variants to optimise if statements they are groupped together #define BLEND_STYLE_PUSH_RIGHT 0x10 // 1D or 2D (& 0b00010000) #define BLEND_STYLE_PUSH_LEFT 0x11 // 1D or 2D (& 0b00010000) @@ -387,184 +412,200 @@ typedef enum mapping1D2D { M12_sPinwheel = 4 } mapping1D2D_t; -// segment, 68 bytes -typedef struct Segment { +class WS2812FX; + +// segment, 76 bytes +class Segment { public: - uint16_t start; // start index / start X coordinate 2D (left) - uint16_t stop; // stop index / stop X coordinate 2D (right); segment is invalid if stop == 0 - uint16_t offset; - uint8_t speed; - uint8_t intensity; - uint8_t palette; - uint8_t mode; + uint32_t colors[NUM_COLORS]; + uint16_t start; // start index / start X coordinate 2D (left) + uint16_t stop; // stop index / stop X coordinate 2D (right); segment is invalid if stop == 0 + uint16_t startY; // start Y coodrinate 2D (top); there should be no more than 255 rows + uint16_t stopY; // stop Y coordinate 2D (bottom); there should be no more than 255 rows + uint16_t offset; // offset for 1D effects (effect will wrap around) union { - uint16_t options; //bit pattern: msb first: [transposed mirrorY reverseY] transitional (tbd) paused needspixelstate mirrored on reverse selected + mutable uint16_t options; //bit pattern: msb first: [transposed mirrorY reverseY] transitional (tbd) paused needspixelstate mirrored on reverse selected struct { - bool selected : 1; // 0 : selected - bool reverse : 1; // 1 : reversed - bool on : 1; // 2 : is On - bool mirror : 1; // 3 : mirrored - bool freeze : 1; // 4 : paused/frozen - bool reset : 1; // 5 : indicates that Segment runtime requires reset - bool reverse_y : 1; // 6 : reversed Y (2D) - bool mirror_y : 1; // 7 : mirrored Y (2D) - bool transpose : 1; // 8 : transposed (2D, swapped X & Y) - uint8_t map1D2D : 3; // 9-11 : mapping for 1D effect on 2D (0-use as strip, 1-expand vertically, 2-circular/arc, 3-rectangular/corner, ...) - uint8_t soundSim : 2; // 12-13 : 0-3 sound simulation types ("soft" & "hard" or "on"/"off") - uint8_t set : 2; // 14-15 : 0-3 UI segment sets/groups + mutable bool selected : 1; // 0 : selected + bool reverse : 1; // 1 : reversed + mutable bool on : 1; // 2 : is On + bool mirror : 1; // 3 : mirrored + mutable bool freeze : 1; // 4 : paused/frozen + mutable bool reset : 1; // 5 : indicates that Segment runtime requires reset + bool reverse_y : 1; // 6 : reversed Y (2D) + bool mirror_y : 1; // 7 : mirrored Y (2D) + bool transpose : 1; // 8 : transposed (2D, swapped X & Y) + uint8_t map1D2D : 3; // 9-11 : mapping for 1D effect on 2D (0-use as strip, 1-expand vertically, 2-circular/arc, 3-rectangular/corner, ...) + uint8_t soundSim : 2; // 12-13 : 0-3 sound simulation types ("soft" & "hard" or "on"/"off") + mutable uint8_t set : 2; // 14-15 : 0-3 UI segment sets/groups }; }; uint8_t grouping, spacing; - uint8_t opacity; - uint32_t colors[NUM_COLORS]; - uint8_t cct; //0==1900K, 255==10091K - uint8_t custom1, custom2; // custom FX parameters/sliders + uint8_t opacity, cct; // 0==1900K, 255==10091K + // effect data + uint8_t mode; + uint8_t palette; + uint8_t speed; + uint8_t intensity; + uint8_t custom1, custom2; // custom FX parameters/sliders struct { uint8_t custom3 : 5; // reduced range slider (0-31) bool check1 : 1; // checkmark 1 bool check2 : 1; // checkmark 2 bool check3 : 1; // checkmark 3 + //uint8_t blendMode : 4; // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn }; - uint8_t startY; // start Y coodrinate 2D (top); there should be no more than 255 rows - uint8_t stopY; // stop Y coordinate 2D (bottom); there should be no more than 255 rows - // note: two bytes of padding are added here - char *name; + uint8_t blendMode; // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn + char *name; // segment name // runtime data - unsigned long next_time; // millis() of next update - uint32_t step; // custom "step" var - uint32_t call; // call counter - uint16_t aux0; // custom var - uint16_t aux1; // custom var + mutable unsigned long next_time; // millis() of next update + mutable uint32_t step; // custom "step" var + mutable uint32_t call; // call counter + mutable uint16_t aux0; // custom var + mutable uint16_t aux1; // custom var byte *data; // effect data pointer + static uint16_t maxWidth, maxHeight; // these define matrix width & height (max. segment dimensions) - typedef struct TemporarySegmentData { - uint16_t _optionsT; - uint32_t _colorT[NUM_COLORS]; - uint8_t _speedT; - uint8_t _intensityT; - uint8_t _custom1T, _custom2T; // custom FX parameters/sliders - struct { - uint8_t _custom3T : 5; // reduced range slider (0-31) - bool _check1T : 1; // checkmark 1 - bool _check2T : 1; // checkmark 2 - bool _check3T : 1; // checkmark 3 - }; - uint16_t _aux0T; - uint16_t _aux1T; - uint32_t _stepT; - uint32_t _callT; - uint8_t *_dataT; - unsigned _dataLenT; - TemporarySegmentData() - : _dataT(nullptr) // just in case... - , _dataLenT(0) - {} - } tmpsegd_t; - private: + uint32_t *pixels; // pixel data + unsigned _dataLen; + uint8_t _default_palette; // palette number that gets assigned to pal0 union { - uint8_t _capabilities; + mutable uint8_t _capabilities; // determines segment capabilities in terms of what is available: RGB, W, CCT, manual W, etc. struct { bool _isRGB : 1; bool _hasW : 1; bool _isCCT : 1; bool _manualW : 1; - uint8_t _reserved : 4; }; }; - 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 variables are use to speed up effect calculations by stashing common pre-calculated values + static unsigned _usedSegmentData; // amount of data used by all segments + static unsigned _vLength; // 1D dimension used for current effect + static unsigned _vWidth, _vHeight; // 2D dimensions used for current effect + static uint32_t _currentColors[NUM_COLORS]; // colors used for current effect (faster access from effect functions) static CRGBPalette16 _currentPalette; // palette used for current effect (includes transition, used in color_from_palette()) static CRGBPalette16 _randomPalette; // actual random palette static CRGBPalette16 _newRandomPalette; // target random palette - static uint16_t _lastPaletteChange; // last random palette change time in millis()/1000 - static uint16_t _lastPaletteBlend; // blend palette according to set Transition Delay in millis()%0xFFFF - static uint16_t _transitionprogress; // current transition progress 0 - 0xFFFF - #ifndef WLED_DISABLE_MODE_BLEND + static uint16_t _lastPaletteChange; // last random palette change time (in seconds) + static uint16_t _nextPaletteBlend; // next due time for random palette morph (in millis()) static bool _modeBlend; // mode/effect blending semaphore - // clipping - static uint16_t _clipStart, _clipStop; - static uint8_t _clipStartY, _clipStopY; - #endif + // clipping rectangle used for blending + static uint16_t _clipStart, _clipStop; + static uint8_t _clipStartY, _clipStopY; - // transition data, valid only if transitional==true, holds values during transition (72 bytes) + // transition data, holds values during transition (76 bytes/28 bytes) struct Transition { - #ifndef WLED_DISABLE_MODE_BLEND - tmpsegd_t _segT; // previous segment environment - uint8_t _modeT; // previous mode/effect - #else - uint32_t _colorT[NUM_COLORS]; + Segment *_oldSegment; // previous segment environment (may be nullptr if effect did not change) + unsigned long _start; // must accommodate millis() + uint32_t _colors[NUM_COLORS]; // current colors + #ifndef WLED_SAVE_RAM + CRGBPalette16 _palT; // temporary palette (slowly being morphed from old to new) #endif - uint8_t _palTid; // previous palette - uint8_t _briT; // temporary brightness - uint8_t _cctT; // temporary CCT - CRGBPalette16 _palT; // temporary palette - uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) - unsigned long _start; // must accommodate millis() - uint16_t _dur; - // -> here is one byte of padding + uint16_t _dur; // duration of transition in ms + uint16_t _progress; // transition progress (0-65535); pre-calculated from _start & _dur in updateTransitionProgress() + uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) + uint8_t _palette, _bri, _cct; // palette ID, brightness and CCT at the start of transition (brightness will be 0 if segment was off) Transition(uint16_t dur=750) - : _palT(CRGBPalette16(CRGB::Black)) - , _prevPaletteBlends(0) - , _start(millis()) - , _dur(dur) + : _oldSegment(nullptr) + , _start(millis()) + , _colors{0,0,0} + #ifndef WLED_SAVE_RAM + , _palT(CRGBPalette16(CRGB::Black)) + #endif + , _dur(dur) + , _progress(0) + , _prevPaletteBlends(0) + , _palette(0) + , _bri(0) + , _cct(0) {} + ~Transition() { + //DEBUGFX_PRINTF_P(PSTR("-- Destroying transition: %p\n"), this); + if (_oldSegment) delete _oldSegment; + } } *_t; - [[gnu::hot]] void _setPixelColorXY_raw(const int& x, const int& y, uint32_t& col) const; // set pixel without mapping (internal use only) + protected: + + inline static unsigned getUsedSegmentData() { return Segment::_usedSegmentData; } + inline static void addUsedSegmentData(int len) { Segment::_usedSegmentData += len; } + + inline uint32_t *getPixels() const { return pixels; } + inline void setPixelColorRaw(unsigned i, uint32_t c) const { pixels[i] = c; } + inline uint32_t getPixelColorRaw(unsigned i) const { return pixels[i]; }; + #ifndef WLED_DISABLE_2D + inline void setPixelColorXYRaw(unsigned x, unsigned y, uint32_t c) const { auto XY = [](unsigned X, unsigned Y){ return X + Y*Segment::vWidth(); }; pixels[XY(x,y)] = c; } + inline uint32_t getPixelColorXYRaw(unsigned x, unsigned y) const { auto XY = [](unsigned X, unsigned Y){ return X + Y*Segment::vWidth(); }; return pixels[XY(x,y)]; }; + #endif + void resetIfRequired(); // sets all SEGENV variables to 0 and clears data buffer + CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal); + + // transition functions + void stopTransition(); // ends transition mode by destroying transition structure (does nothing if not in transition) + void updateTransitionProgress() const; // sets transition progress (0-65535) based on time passed since transition start + inline void handleTransition() { + updateTransitionProgress(); + if (isInTransition() && progress() == 0xFFFFU) stopTransition(); + } + inline uint16_t progress() const { return isInTransition() ? _t->_progress : 0xFFFFU; } // relies on handleTransition()/updateTransitionProgress() to update progression variable + inline Segment *getOldSegment() const { return isInTransition() ? _t->_oldSegment : nullptr; } + + inline static void modeBlend(bool blend) { Segment::_modeBlend = blend; } + inline static void setClippingRect(int startX, int stopX, int startY = 0, int stopY = 1) { _clipStart = startX; _clipStop = stopX; _clipStartY = startY; _clipStopY = stopY; }; + inline static bool isPreviousMode() { return Segment::_modeBlend; } // needed for determining CCT/opacity during non-BLEND_STYLE_FADE transition + + static void handleRandomPalette(); public: - Segment(uint16_t sStart=0, uint16_t sStop=30) : - start(sStart), - stop(sStop), - offset(0), - speed(DEFAULT_SPEED), - intensity(DEFAULT_INTENSITY), - palette(0), - mode(DEFAULT_MODE), - options(SELECTED | SEGMENT_ON), - grouping(1), - spacing(0), - opacity(255), - colors{DEFAULT_COLOR,BLACK,BLACK}, - cct(127), - custom1(DEFAULT_C1), - custom2(DEFAULT_C2), - custom3(DEFAULT_C3), - check1(false), - check2(false), - check3(false), - startY(0), - stopY(1), - name(nullptr), - next_time(0), - step(0), - call(0), - aux0(0), - aux1(0), - data(nullptr), - _capabilities(0), - _default_palette(0), - _dataLen(0), - _t(nullptr) + Segment(uint16_t sStart=0, uint16_t sStop=30, uint16_t sStartY = 0, uint16_t sStopY = 1) + : colors{DEFAULT_COLOR,BLACK,BLACK} + , start(sStart) + , stop(sStop > sStart ? sStop : sStart+1) // minimum length is 1 + , startY(sStartY) + , stopY(sStopY > sStartY ? sStopY : sStartY+1) // minimum height is 1 + , offset(0) + , options(SELECTED | SEGMENT_ON) + , grouping(1) + , spacing(0) + , opacity(255) + , cct(127) + , mode(DEFAULT_MODE) + , palette(0) + , speed(DEFAULT_SPEED) + , intensity(DEFAULT_INTENSITY) + , custom1(DEFAULT_C1) + , custom2(DEFAULT_C2) + , custom3(DEFAULT_C3) + , check1(false) + , check2(false) + , check3(false) + , blendMode(0) + , name(nullptr) + , next_time(0) + , step(0) + , call(0) + , aux0(0) + , aux1(0) + , data(nullptr) + , _dataLen(0) + , _default_palette(6) + , _capabilities(0) + , _t(nullptr) { - #ifdef WLED_DEBUG - //Serial.printf("-- Creating segment: %p\n", this); - #endif - } - - Segment(uint16_t sStartX, uint16_t sStopX, uint16_t sStartY, uint16_t sStopY) : Segment(sStartX, sStopX) { - startY = sStartY; - stopY = sStopY; + DEBUGFX_PRINTF_P(PSTR("-- Creating segment: %p [%d,%d:%d,%d]\n"), this, (int)start, (int)stop, (int)startY, (int)stopY); + // allocate render buffer (always entire segment) + pixels = static_cast(d_calloc(sizeof(uint32_t), length())); // error handling is also done in isActive() + if (!pixels) { + DEBUGFX_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!")); + extern byte errorFlag; + errorFlag = ERR_NORAM_PX; + stop = 0; // mark segment as inactive/invalid + } } Segment(const Segment &orig); // copy constructor @@ -572,54 +613,49 @@ typedef struct Segment { ~Segment() { #ifdef WLED_DEBUG - //Serial.printf("-- Destroying segment: %p", this); - //if (name) Serial.printf(" %s (%p)", name, name); - //if (data) Serial.printf(" %d->(%p)", (int)_dataLen, data); - //Serial.println(); + DEBUGFX_PRINTF_P(PSTR("-- Destroying segment: %p [%d,%d:%d,%d]"), this, (int)start, (int)stop, (int)startY, (int)stopY); + if (name) DEBUGFX_PRINTF_P(PSTR(" %s (%p)"), name, name); + if (data) DEBUGFX_PRINTF_P(PSTR(" %u->(%p)"), _dataLen, data); + DEBUGFX_PRINTF_P(PSTR(" T[%p]"), _t); + DEBUGFX_PRINTLN(); #endif - if (name) { free(name); name = nullptr; } - stopTransition(); + clearName(); deallocateData(); + d_free(pixels); } Segment& operator= (const Segment &orig); // copy assignment Segment& operator= (Segment &&orig) noexcept; // move assignment #ifdef WLED_DEBUG - size_t getSize() const { return sizeof(Segment) + (data?_dataLen:0) + (name?strlen(name):0) + (_t?sizeof(Transition):0); } + size_t getSize() const { return sizeof(Segment) + (data?_dataLen:0) + (name?strlen(name):0) + (_t?sizeof(Transition):0) + (pixels?length()*sizeof(uint32_t):0); } #endif - inline bool getOption(uint8_t n) const { return ((options >> n) & 0x01); } - inline bool isSelected() const { return selected; } - inline bool isInTransition() const { return _t != nullptr; } - inline bool isActive() const { return stop > start; } - inline bool hasRGB() const { return _isRGB; } - inline bool hasWhite() const { return _hasW; } - inline bool isCCT() const { return _isCCT; } - inline uint16_t width() const { return isActive() ? (stop - start) : 0; } // segment width in physical pixels (length if 1D) - inline uint16_t height() const { return stopY - startY; } // segment height (if 2D) in physical pixels (it *is* always >=1) - inline uint16_t length() const { return width() * height(); } // segment length (count) in physical pixels - inline uint16_t groupLength() const { return grouping + spacing; } + inline bool getOption(uint8_t n) const { return ((options >> n) & 0x01); } + inline bool isSelected() const { return selected; } + inline bool isInTransition() const { return _t != nullptr; } + inline bool isActive() const { return stop > start && pixels; } + inline bool hasRGB() const { return _isRGB; } + inline bool hasWhite() const { return _hasW; } + inline bool isCCT() const { return _isCCT; } + inline uint16_t width() const { return stop > start ? (stop - start) : 0; }// segment width in physical pixels (length if 1D) + inline uint16_t height() const { return stopY - startY; } // segment height (if 2D) in physical pixels (it *is* always >=1) + inline uint16_t length() const { return width() * height(); } // segment length (count) in physical pixels + inline uint16_t groupLength() const { return grouping + spacing; } inline uint8_t getLightCapabilities() const { return _capabilities; } - inline void deactivate() { setGeometry(0,0); } - inline Segment &clearName() { if (name) free(name); name = nullptr; return *this; } - inline Segment &setName(const String &name) { return setName(name.c_str()); } + inline void deactivate() { setGeometry(0,0); } + inline Segment &clearName() { d_free(name); name = nullptr; return *this; } + inline Segment &setName(const String &name) { return setName(name.c_str()); } - inline static unsigned getUsedSegmentData() { return Segment::_usedSegmentData; } - inline static void addUsedSegmentData(int len) { Segment::_usedSegmentData += len; } - #ifndef WLED_DISABLE_MODE_BLEND - inline static void modeBlend(bool blend) { _modeBlend = blend; } - inline static bool getmodeBlend(void) { return _modeBlend; } - #endif 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 uint32_t getCurrentColor(unsigned i) { return Segment::_currentColors[i= 0 && i < length()) setPixelColorRaw(i,col); } #ifdef WLED_USE_AA_PIXELS void setPixelColor(float i, uint32_t c, bool aa = true) const; inline void setPixelColor(float i, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0, bool aa = true) const { setPixelColor(i, RGBW32(r,g,b,w), aa); } inline void setPixelColor(float i, CRGB c, bool aa = true) const { setPixelColor(i, RGBW32(c.r,c.g,c.b,0), aa); } #endif - #ifndef WLED_DISABLE_MODE_BLEND - static inline void setClippingRect(int startX, int stopX, int startY = 0, int stopY = 1) { _clipStart = startX; _clipStop = stopX; _clipStartY = startY; _clipStopY = stopY; }; - #endif - bool isPixelClipped(int i) const; + [[gnu::hot]] bool isPixelClipped(int i) const; [[gnu::hot]] uint32_t getPixelColor(int i) const; // 1D support functions (some implement 2D as well) - void blur(uint8_t, bool smear = false); - void clear(); - void fill(uint32_t c); - void fade_out(uint8_t r); - void fadeToSecondaryBy(uint8_t fadeBy); - 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 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)); } - [[gnu::hot]] uint32_t color_from_palette(uint16_t, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri = 255) const; + void blur(uint8_t, bool smear = false) const; + void clear() const { fill(BLACK); } // clear segment + void fill(uint32_t c) const; + void fade_out(uint8_t r) const; + void fadeToSecondaryBy(uint8_t fadeBy) const; + void fadeToBlackBy(uint8_t fadeBy) const; + inline void blendPixelColor(int n, uint32_t color, uint8_t blend) const { setPixelColor(n, color_blend(getPixelColor(n), color, blend)); } + inline void blendPixelColor(int n, CRGB c, uint8_t blend) const { blendPixelColor(n, RGBW32(c.r,c.g,c.b,0), blend); } + inline void addPixelColor(int n, uint32_t color, bool preserveCR = true) const { setPixelColor(n, color_add(getPixelColor(n), color, preserveCR)); } + inline void addPixelColor(int n, byte r, byte g, byte b, byte w = 0, bool preserveCR = true) const + { addPixelColor(n, RGBW32(r,g,b,w), preserveCR); } + inline void addPixelColor(int n, CRGB c, bool preserveCR = true) const { addPixelColor(n, RGBW32(c.r,c.g,c.b,0), preserveCR); } + inline void fadePixelColor(uint16_t n, uint8_t fade) const { setPixelColor(n, color_fade(getPixelColor(n), fade, true)); } + [[gnu::hot]] uint32_t color_from_palette(uint16_t, bool mapping, bool moving, uint8_t mcol, uint8_t pbri = 255) const; [[gnu::hot]] uint32_t color_wheel(uint8_t pos) const; - - // 2D Blur: shortcuts for bluring columns or rows only (50% faster than full 2D blur) - inline void blurCols(fract8 blur_amount, bool smear = false) { // blur all columns - blur2D(0, blur_amount, smear); - } - inline void blurRows(fract8 blur_amount, bool smear = false) { // blur all rows - blur2D(blur_amount, 0, smear); - } - // 2D matrix - [[gnu::hot]] unsigned virtualWidth() const; // segment width in virtual pixels (accounts for groupping and spacing) - [[gnu::hot]] unsigned virtualHeight() const; // segment height in virtual pixels (accounts for groupping and spacing) - inline unsigned nrOfVStrips() const { // returns number of virtual vertical strips in 2D matrix (used to expand 1D effects into 2D) + unsigned virtualWidth() const; // segment width in virtual pixels (accounts for groupping and spacing) + unsigned virtualHeight() const; // segment height in virtual pixels (accounts for groupping and spacing) + inline unsigned nrOfVStrips() const { // returns number of virtual vertical strips in 2D matrix (used to expand 1D effects into 2D) #ifndef WLED_DISABLE_2D return (is2D() && map1D2D == M12_pBar) ? virtualWidth() : 1; #else @@ -725,52 +737,54 @@ typedef struct Segment { [[gnu::hot]] bool isPixelXYClipped(int x, int y) const; [[gnu::hot]] uint32_t getPixelColorXY(int x, int y) const; // 2D support functions - inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t blend) { setPixelColorXY(x, y, color_blend(getPixelColorXY(x,y), color, blend)); } - inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), blend); } - inline void addPixelColorXY(int x, int y, uint32_t color, bool 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)); } + inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t blend) const { setPixelColorXY(x, y, color_blend(getPixelColorXY(x,y), color, blend)); } + inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) const { blendPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), blend); } + inline void addPixelColorXY(int x, int y, uint32_t color, bool preserveCR = true) const { setPixelColorXY(x, y, color_add(getPixelColorXY(x,y), color, preserveCR)); } + inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool preserveCR = true) + { addPixelColorXY(x, y, RGBW32(r,g,b,w), preserveCR); } + inline void addPixelColorXY(int x, int y, CRGB c, bool preserveCR = true) const { addPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), preserveCR); } + inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) const { setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), fade, true)); } + inline void blurCols(fract8 blur_amount, bool smear = false) const { blur2D(0, blur_amount, smear); } // blur all columns (50% faster than full 2D blur) + inline void blurRows(fract8 blur_amount, bool smear = false) const { blur2D(blur_amount, 0, smear); } // blur all rows (50% faster than full 2D blur) //void box_blur(unsigned r = 1U, bool smear = false); // 2D box blur - void blur2D(uint8_t blur_x, uint8_t blur_y, bool smear = false); - 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, 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); - inline void fill_solid(CRGB c) { fill(RGBW32(c.r,c.g,c.b,0)); } + void blur2D(uint8_t blur_x, uint8_t blur_y, bool smear = false) const; + void moveX(int delta, bool wrap = false) const; + void moveY(int delta, bool wrap = false) const; + void move(unsigned dir, unsigned delta, bool wrap = false) const; + void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) const; + void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t c, bool soft = false) const; + void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft = false) const; + void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t col2 = 0, int8_t rotate = 0) const; + void wu_pixel(uint32_t x, uint32_t y, CRGB c) const; + inline void drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) const { drawCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } + inline void fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, CRGB c, bool soft = false) const { fillCircle(cx, cy, radius, RGBW32(c.r,c.g,c.b,0), soft); } + inline void drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, CRGB c, bool soft = false) const { drawLine(x0, y0, x1, y1, RGBW32(c.r,c.g,c.b,0), soft); } // automatic inline + inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2 = CRGB::Black, int8_t rotate = 0) const { drawCharacter(chr, x, y, w, h, RGBW32(c.r,c.g,c.b,0), RGBW32(c2.r,c2.g,c2.b,0), rotate); } // automatic inline + inline void fill_solid(CRGB c) const { fill(RGBW32(c.r,c.g,c.b,0)); } #else - inline bool is2D() const { return false; } - inline void setPixelColorXY(int x, int y, uint32_t c) { setPixelColor(x, c); } - inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) { setPixelColor(int(x), c); } - inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColor(x, RGBW32(r,g,b,w)); } - inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0)); } - inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) { setPixelColor(int(x), RGBW32(c.r,c.g,c.b,0)); } + inline constexpr bool is2D() const { return false; } + inline void setPixelColorXY(int x, int y, uint32_t c) const { setPixelColor(x, c); } + inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) const { setPixelColor(int(x), c); } + inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) const { setPixelColor(x, RGBW32(r,g,b,w)); } + inline void setPixelColorXY(int x, int y, CRGB c) const { setPixelColor(x, RGBW32(c.r,c.g,c.b,0)); } + inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) const { setPixelColor(int(x), RGBW32(c.r,c.g,c.b,0)); } #ifdef WLED_USE_AA_PIXELS - inline void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) { setPixelColor(x, c, aa); } + inline void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) const { setPixelColor(x, c, aa); } inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColor(x, RGBW32(r,g,b,w), aa); } - inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0), aa); } + inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) const { setPixelColor(x, RGBW32(c.r,c.g,c.b,0), aa); } #endif - inline bool isPixelXYClipped(int x, int y) { return isPixelClipped(x); } - inline uint32_t getPixelColorXY(int x, int y) { return getPixelColor(x); } - inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) { blendPixelColor(x, c, blend); } - inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); } - 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 bool isPixelXYClipped(int x, int y) const { return isPixelClipped(x); } + inline uint32_t getPixelColorXY(int x, int y) const { return getPixelColor(x); } + inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) const { blendPixelColor(x, c, blend); } + inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) const { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); } + inline void addPixelColorXY(int x, int y, uint32_t color, bool saturate = false) const { addPixelColor(x, color, saturate); } + inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool saturate = false) const { addPixelColor(x, RGBW32(r,g,b,w), saturate); } + inline void addPixelColorXY(int x, int y, CRGB c, bool saturate = false) const { addPixelColor(x, RGBW32(c.r,c.g,c.b,0), saturate); } + inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) const { fadePixelColor(x, fade); } //inline void box_blur(unsigned i, bool vertical, fract8 blur_amount) {} inline void blur2D(uint8_t blur_x, uint8_t blur_y, bool smear = false) {} - inline void blurRow(int row, fract8 blur_amount, bool smear = false) {} - inline void blurCol(int col, fract8 blur_amount, bool smear = false) {} + inline void blurCols(fract8 blur_amount, bool smear = false) { blur(blur_amount, smear); } // blur all columns (50% faster than full 2D blur) + inline void blurRows(fract8 blur_amount, bool smear = false) {} inline void moveX(int delta, bool wrap = false) {} inline void moveY(int delta, bool wrap = false) {} inline void move(uint8_t dir, uint8_t delta, bool wrap = false) {} @@ -780,16 +794,15 @@ typedef struct Segment { 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, bool usePalGrad = false) {} + inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, uint32_t color, uint32_t = 0, int8_t = 0) {} + inline void drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, uint8_t h, CRGB c, CRGB c2, int8_t rotate = 0) {} inline void wu_pixel(uint32_t x, uint32_t y, CRGB c) {} #endif -} segment; -//static int segSize = sizeof(Segment); + friend class WS2812FX; +}; -// main "strip" class -class WS2812FX { // 96 bytes +// main "strip" class (104 bytes) +class WS2812FX { typedef uint16_t (*mode_ptr)(); // pointer to mode function typedef void (*show_callback)(); // pre show callback typedef struct ModeData { @@ -799,8 +812,6 @@ class WS2812FX { // 96 bytes ModeData(uint8_t id, uint16_t (*fcn)(void), const char *data) : _id(id), _fcn(fcn), _data(data) {} } mode_data_t; - static WS2812FX* instance; - public: WS2812FX() : @@ -808,9 +819,6 @@ class WS2812FX { // 96 bytes now(millis()), timebase(0), isMatrix(false), -#ifndef WLED_DISABLE_2D - panels(1), -#endif #ifdef WLED_AUTOSEGMENTS autoSegments(true), #else @@ -819,27 +827,26 @@ class WS2812FX { // 96 bytes correctWB(false), cctFromRgb(false), // true private variables + _pixels(nullptr), _suspend(false), - _length(DEFAULT_LED_COUNT), _brightness(DEFAULT_BRIGHTNESS), + _length(DEFAULT_LED_COUNT), _transitionDur(750), - _targetFps(WLED_FPS), _frametime(FRAMETIME_FIXED), - _cumulativeFps(50 << FPS_CALC_SHIFT), + _targetFps(WLED_FPS), + _cumulativeFps(WLED_FPS), _isServicing(false), _isOffRefreshRequired(false), _hasWhiteChannel(false), _triggered(false), + _segment_index(0), + _mainSegment(0), _modeCount(MODE_COUNT), _callback(nullptr), customMappingTable(nullptr), customMappingSize(0), - _lastShow(0), - _lastServiceShow(0), - _segment_index(0), - _mainSegment(0) + _lastShow(0) { - WS2812FX::instance = this; _mode.reserve(_modeCount); // allocate memory to prevent initial fragmentation (does not increase size()) _modeData.reserve(_modeCount); // allocate memory to prevent initial fragmentation (does not increase size()) if (_mode.capacity() <= 1 || _modeData.capacity() <= 1) _modeCount = 1; // memory allocation failed only show Solid @@ -847,18 +854,16 @@ class WS2812FX { // 96 bytes } ~WS2812FX() { - if (customMappingTable) free(customMappingTable); + d_free(_pixels); + d_free(customMappingTable); _mode.clear(); _modeData.clear(); _segments.clear(); #ifndef WLED_DISABLE_2D panel.clear(); #endif - customPalettes.clear(); } - static WS2812FX* getInstance() { return instance; } - void #ifdef WLED_DEBUG printSize(), // prints memory usage for strip components @@ -873,29 +878,34 @@ class WS2812FX { // 96 bytes resetSegments(), // marks all segments for reset makeAutoSegments(bool forceReset = false), // will create segments based on configured outputs fixInvalidSegments(), // fixes incorrect segment configuration - setPixelColor(unsigned i, uint32_t c) const, // paints absolute strip pixel with index n and color c + blendSegment(const Segment &topSegment) const, // blends topSegment into pixels show(), // initiates LED output setTargetFps(unsigned fps), - setupEffectData(); // add default effects to the list; defined in FX.cpp + setupEffectData(), // add default effects to the list; defined in FX.cpp + waitForIt(); // wait until frame is over (service() has finished or time for 1 frame has passed) - inline void resetTimebase() { timebase = 0UL - millis(); } - inline void restartRuntime() { for (Segment &seg : _segments) { seg.markForReset().resetIfRequired(); } } - inline void setTransitionMode(bool t) { for (Segment &seg : _segments) seg.startTransition(t ? _transitionDur : 0); } - inline void setPixelColor(unsigned n, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) const { setPixelColor(n, RGBW32(r,g,b,w)); } - inline void setPixelColor(unsigned n, CRGB c) const { setPixelColor(n, c.red, c.green, c.blue); } - inline void fill(uint32_t c) const { for (unsigned i = 0; i < getLengthTotal(); i++) setPixelColor(i, c); } // fill whole strip with color (inline) + void setRealtimePixelColor(unsigned i, uint32_t c); + inline void setPixelColor(unsigned n, uint32_t c) const { if (n < getLengthTotal()) _pixels[n] = c; } // paints absolute strip pixel with index n and color c + inline void resetTimebase() { timebase = 0UL - millis(); } + inline void setPixelColor(unsigned n, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) const + { setPixelColor(n, RGBW32(r,g,b,w)); } + inline void setPixelColor(unsigned n, CRGB c) const { setPixelColor(n, c.red, c.green, c.blue); } + inline void fill(uint32_t c) const { for (size_t i = 0; i < getLengthTotal(); i++) setPixelColor(i, c); } // fill whole strip with color (inline) inline void trigger() { _triggered = true; } // Forces the next frame to be computed on all active segments. inline void setShowCallback(show_callback cb) { _callback = cb; } inline void setTransition(uint16_t t) { _transitionDur = t; } // sets transition time (in ms) - inline void appendSegment(const Segment &seg = Segment()) { if (_segments.size() < getMaxSegments()) _segments.push_back(seg); } + inline void appendSegment(uint16_t sStart=0, uint16_t sStop=30, uint16_t sStartY = 0, uint16_t sStopY = 1) + { if (_segments.size() < getMaxSegments()) _segments.emplace_back(sStart,sStop,sStartY,sStopY); } inline void suspend() { _suspend = true; } // will suspend (and canacel) strip.service() execution inline void resume() { _suspend = false; } // will resume strip.service() execution - bool - checkSegmentAlignment() const, - hasRGBWBus() const, - hasCCTBus() const, - deserializeMap(unsigned n = 0); + void restartRuntime(); + void setTransitionMode(bool t); + + bool checkSegmentAlignment() const; + bool hasRGBWBus() const; + bool hasCCTBus() const; + bool deserializeMap(unsigned n = 0); inline bool isUpdating() const { return !BusManager::canAllShow(); } // return true if the strip is being sent pixel updates inline bool isServicing() const { return _isServicing; } // returns true if strip.service() is executing @@ -904,26 +914,23 @@ class WS2812FX { // 96 bytes 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, - getActiveSegmentsNum() const, - getFirstSelectedSegId() const, - getLastActiveSegmentId() const, - getActiveSegsLightCapabilities(bool selectedOnly = false) const, - addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name); // add effect to the list; defined in FX.cpp; + uint8_t paletteBlend; + uint8_t getActiveSegmentsNum() const; + uint8_t getFirstSelectedSegId() const; + uint8_t getLastActiveSegmentId() const; + uint8_t getActiveSegsLightCapabilities(bool selectedOnly = false) const; + uint8_t addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name); // add effect to the list; defined in FX.cpp; inline uint8_t getBrightness() const { return _brightness; } // returns current strip brightness inline static constexpr unsigned getMaxSegments() { return MAX_NUM_SEGMENTS; } // returns maximum number of supported segments (fixed value) inline uint8_t getSegmentsNum() const { return _segments.size(); } // returns currently present segments inline uint8_t getCurrSegmentId() const { return _segment_index; } // returns current segment index (only valid while strip.isServicing()) inline uint8_t getMainSegmentId() const { return _mainSegment; } // returns main segment index - inline uint8_t getPaletteCount() const { return 13 + GRADIENT_PALETTE_COUNT + customPalettes.size(); } inline uint8_t getTargetFps() const { return _targetFps; } // returns rough FPS value for las 2s interval inline uint8_t getModeCount() const { return _modeCount; } // returns number of registered modes/effects - uint16_t - getLengthPhysical() const, - getLengthTotal() const; // will include virtual/nonexistent pixels in matrix + uint16_t getLengthPhysical() const; + uint16_t getLengthTotal() const; // will include virtual/nonexistent pixels in matrix inline uint16_t getFps() const { return (millis() - _lastShow > 2000) ? 0 : (FPS_MULTIPLIER * _cumulativeFps) >> FPS_CALC_SHIFT; } // Returns the refresh rate of the LED strip (_cumulativeFps is stored in fixed point) inline uint16_t getFrameTime() const { return _frametime; } // returns amount of time a frame should take (in ms) @@ -936,12 +943,11 @@ class WS2812FX { // 96 bytes }; unsigned long now, timebase; - uint32_t getPixelColor(unsigned i) const; + inline uint32_t getPixelColor(unsigned n) const { return (n < getLengthTotal()) ? _pixels[n] : 0; } // returns color of pixel n + inline uint32_t getLastShow() const { return _lastShow; } // returns millis() timestamp of last strip.show() call - inline uint32_t getLastShow() const { return _lastShow; } // returns millis() timestamp of last strip.show() call - - const char *getModeData(unsigned id = 0) const { return (id && id < _modeCount) ? _modeData[id] : PSTR("Solid"); } - inline const char **getModeDataSrc() { return &(_modeData[0]); } // vectors use arrays for underlying data + const char *getModeData(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 Segment& getSegment(unsigned id); inline Segment& getFirstSelectedSeg() { return _segments[getFirstSelectedSegId()]; } // returns reference to first segment that is "selected" @@ -949,15 +955,9 @@ class WS2812FX { // 96 bytes inline Segment* getSegments() { return &(_segments[0]); } // returns pointer to segment vector structure (warning: use carefully) // 2D support (panels) - bool - isMatrix; #ifndef WLED_DISABLE_2D - #define WLED_MAX_PANELS 18 - uint8_t - panels; - - typedef struct panel_t { + struct Panel { uint16_t xOffset; // x offset relative to the top left of matrix in LEDs uint16_t yOffset; // y offset relative to the top left of matrix in LEDs uint8_t width; // width of the panel @@ -971,50 +971,48 @@ class WS2812FX { // 96 bytes bool serpentine : 1; // is serpentine? }; }; - panel_t() - : xOffset(0) - , yOffset(0) - , width(8) - , height(8) - , options(0) + Panel() + : xOffset(0) + , yOffset(0) + , width(8) + , height(8) + , options(0) {} - } Panel; + }; std::vector panel; #endif void setUpMatrix(); // sets up automatic matrix ledmap from panel configuration - // outsmart the compiler :) by correctly overloading - inline void setPixelColorXY(int x, int y, uint32_t c) const { setPixelColor((unsigned)(y * Segment::maxWidth + x), c); } - inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) const { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } - inline void setPixelColorXY(int x, int y, CRGB c) const { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } - - inline uint32_t getPixelColorXY(int x, int y) const { return getPixelColor(isMatrix ? y * Segment::maxWidth + x : x); } + inline void setPixelColorXY(unsigned x, unsigned y, uint32_t c) const { setPixelColor(y * Segment::maxWidth + x, c); } + inline void setPixelColorXY(unsigned x, unsigned y, byte r, byte g, byte b, byte w = 0) const { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } + inline void setPixelColorXY(unsigned x, unsigned y, CRGB c) const { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } + inline uint32_t getPixelColorXY(unsigned x, unsigned y) const { return getPixelColor(y * Segment::maxWidth + x); } // end 2D support - void loadCustomPalettes(); // loads custom palettes from JSON - std::vector customPalettes; // TODO: move custom palettes out of WS2812FX class - + bool isMatrix; struct { bool autoSegments : 1; bool correctWB : 1; bool cctFromRgb : 1; }; - std::vector _segments; - friend struct Segment; + Segment *_currentSegment; private: + uint32_t *_pixels; + std::vector _segments; + volatile bool _suspend; - uint16_t _length; uint8_t _brightness; + uint16_t _length; uint16_t _transitionDur; - uint8_t _targetFps; uint16_t _frametime; - uint16_t _cumulativeFps; + uint8_t _targetFps; + uint8_t _cumulativeFps; // will require only 1 byte struct { @@ -1024,6 +1022,9 @@ class WS2812FX { // 96 bytes bool _triggered : 1; }; + uint8_t _segment_index; + uint8_t _mainSegment; + uint8_t _modeCount; std::vector _mode; // SRAM footprint: 4 bytes per element std::vector _modeData; // mode (effect) name and its slider control data array @@ -1036,11 +1037,10 @@ class WS2812FX { // 96 bytes unsigned long _lastShow; unsigned long _lastServiceShow; - uint8_t _segment_index; - uint8_t _mainSegment; + friend class Segment; }; extern const char JSON_mode_names[]; extern const char JSON_palette_names[]; -#endif \ No newline at end of file +#endif diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index 61d14a12b..69c743183 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -8,7 +8,6 @@ Parts of the code adapted from WLED Sound Reactive */ #include "wled.h" -#include "FX.h" #include "palettes.h" // setUpMatrix() - constructs ledmap array from matrix of panels with WxH pixels @@ -26,8 +25,7 @@ void WS2812FX::setUpMatrix() { // calculate width dynamically because it may have gaps Segment::maxWidth = 1; Segment::maxHeight = 1; - for (size_t i = 0; i < panel.size(); i++) { - Panel &p = panel[i]; + for (const Panel &p : panel) { if (p.xOffset + p.width > Segment::maxWidth) { Segment::maxWidth = p.xOffset + p.width; } @@ -37,21 +35,24 @@ void WS2812FX::setUpMatrix() { } // safety check - if (Segment::maxWidth * Segment::maxHeight > MAX_LEDS || Segment::maxWidth <= 1 || Segment::maxHeight <= 1) { + if (Segment::maxWidth * Segment::maxHeight > MAX_LEDS || Segment::maxWidth > 255 || Segment::maxHeight > 255 || Segment::maxWidth <= 1 || Segment::maxHeight <= 1) { DEBUG_PRINTLN(F("2D Bounds error.")); isMatrix = false; Segment::maxWidth = _length; Segment::maxHeight = 1; - panels = 0; panel.clear(); // release memory allocated by panels + panel.shrink_to_fit(); // release memory if allocated resetSegments(); return; } + suspend(); + waitForIt(); + customMappingSize = 0; // prevent use of mapping if anything goes wrong - if (customMappingTable) free(customMappingTable); - customMappingTable = static_cast(malloc(sizeof(uint16_t)*getLengthTotal())); + d_free(customMappingTable); + customMappingTable = static_cast(d_malloc(sizeof(uint16_t)*getLengthTotal())); // prefer to not use SPI RAM if (customMappingTable) { customMappingSize = getLengthTotal(); @@ -85,7 +86,7 @@ void WS2812FX::setUpMatrix() { JsonArray map = pDoc->as(); gapSize = map.size(); if (!map.isNull() && gapSize >= matrixSize) { // not an empty map - gapTable = static_cast(malloc(gapSize)); + gapTable = static_cast(w_malloc(gapSize)); if (gapTable) for (size_t i = 0; i < gapSize; i++) { gapTable[i] = constrain(map[i], -1, 1); } @@ -96,8 +97,7 @@ void WS2812FX::setUpMatrix() { } unsigned x, y, pix=0; //pixel - for (size_t pan = 0; pan < panel.size(); pan++) { - Panel &p = panel[pan]; + for (const Panel &p : panel) { unsigned h = p.vertical ? p.height : p.width; unsigned v = p.vertical ? p.width : p.height; for (size_t j = 0; j < v; j++){ @@ -113,7 +113,8 @@ void WS2812FX::setUpMatrix() { } // delete gap array as we no longer need it - if (gapTable) free(gapTable); + w_free(gapTable); + resume(); #ifdef WLED_DEBUG DEBUG_PRINT(F("Matrix ledmap:")); @@ -126,7 +127,6 @@ void WS2812FX::setUpMatrix() { } else { // memory allocation error DEBUG_PRINTLN(F("ERROR 2D LED map allocation error.")); isMatrix = false; - panels = 0; panel.clear(); Segment::maxWidth = _length; Segment::maxHeight = 1; @@ -144,103 +144,50 @@ void WS2812FX::setUpMatrix() { /////////////////////////////////////////////////////////// #ifndef WLED_DISABLE_2D - -// raw setColor function without checks (checks are done in setPixelColorXY()) -void IRAM_ATTR_YN Segment::_setPixelColorXY_raw(const int& x, const int& y, uint32_t& col) const -{ - const int baseX = start + x; - const int baseY = startY + y; -#ifndef WLED_DISABLE_MODE_BLEND - // if blending modes, blend with underlying pixel - if (_modeBlend && blendingStyle == BLEND_STYLE_FADE) col = color_blend16(strip.getPixelColorXY(baseX, baseY), col, 0xFFFFU - progress()); -#endif - strip.setPixelColorXY(baseX, baseY, col); - - // Apply mirroring - if (mirror || mirror_y) { - const int mirrorX = start + width() - x - 1; - const int mirrorY = startY + height() - y - 1; - if (mirror) strip.setPixelColorXY(transpose ? baseX : mirrorX, transpose ? mirrorY : baseY, col); - if (mirror_y) strip.setPixelColorXY(transpose ? mirrorX : baseX, transpose ? baseY : mirrorY, col); - if (mirror && mirror_y) strip.setPixelColorXY(mirrorX, mirrorY, col); - } -} - -// pixel is clipped if it falls outside clipping range (_modeBlend==true) or is inside clipping range (_modeBlend==false) +// pixel is clipped if it falls outside clipping range // if clipping start > stop the clipping range is inverted -// _modeBlend==true -> old effect during transition -// _modeBlend==false -> new effect during transition bool IRAM_ATTR_YN Segment::isPixelXYClipped(int x, int y) const { -#ifndef WLED_DISABLE_MODE_BLEND - if (_clipStart != _clipStop && blendingStyle != BLEND_STYLE_FADE) { - const bool invertX = _clipStart > _clipStop; + if (blendingStyle != BLEND_STYLE_FADE && isInTransition() && _clipStart != _clipStop) { + const bool invertX = _clipStart > _clipStop; const bool invertY = _clipStartY > _clipStopY; - const int startX = invertX ? _clipStop : _clipStart; - const int stopX = invertX ? _clipStart : _clipStop; - const int startY = invertY ? _clipStopY : _clipStartY; - const int stopY = invertY ? _clipStartY : _clipStopY; + const int cStartX = invertX ? _clipStop : _clipStart; + const int cStopX = invertX ? _clipStart : _clipStop; + const int cStartY = invertY ? _clipStopY : _clipStartY; + const int cStopY = invertY ? _clipStartY : _clipStopY; if (blendingStyle == BLEND_STYLE_FAIRY_DUST) { - const unsigned width = stopX - startX; // assumes full segment width (faster than virtualWidth()) - const unsigned len = width * (stopY - startY); // assumes full segment height (faster than virtualHeight()) + const unsigned width = cStopX - cStartX; // assumes full segment width (faster than virtualWidth()) + const unsigned len = width * (cStopY - cStartY); // assumes full segment height (faster than virtualHeight()) if (len < 2) return false; const unsigned shuffled = hashInt(x + y * width) % len; const unsigned pos = (shuffled * 0xFFFFU) / len; - return progress() > pos; + return progress() <= pos; } - bool xInside = (x >= startX && x < stopX); if (invertX) xInside = !xInside; - bool yInside = (y >= startY && y < stopY); if (invertY) yInside = !yInside; - const bool clip = (invertX && invertY) ? !_modeBlend : _modeBlend; - if (xInside && yInside) return clip; // covers window & corners (inverted) + if (blendingStyle == BLEND_STYLE_CIRCULAR_IN || blendingStyle == BLEND_STYLE_CIRCULAR_OUT) { + const int cx = (cStopX-cStartX+1) / 2; + const int cy = (cStopY-cStartY+1) / 2; + const bool out = (blendingStyle == BLEND_STYLE_CIRCULAR_OUT); + const unsigned prog = out ? progress() : 0xFFFFU - progress(); + int radius2 = max(cx, cy) * prog / 0xFFFF; + radius2 = 2 * radius2 * radius2; + if (radius2 == 0) return out; + const int dx = x - cx; + const int dy = y - cy; + const bool outside = dx * dx + dy * dy > radius2; + return out ? outside : !outside; + } + bool xInside = (x >= cStartX && x < cStopX); if (invertX) xInside = !xInside; + bool yInside = (y >= cStartY && y < cStopY); if (invertY) yInside = !yInside; + const bool clip = blendingStyle == BLEND_STYLE_OUTSIDE_IN ? xInside || yInside : xInside && yInside; return !clip; } -#endif return false; } void IRAM_ATTR_YN Segment::setPixelColorXY(int x, int y, uint32_t col) const { if (!isActive()) return; // not active - - const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) - const int vH = vHeight(); // segment height in logical pixels (is always >= 1) - -#ifndef WLED_DISABLE_MODE_BLEND - unsigned prog = 0xFFFF - progress(); - if (!prog && !_modeBlend && (blendingStyle & BLEND_STYLE_PUSH_MASK)) { - unsigned dX = (blendingStyle == BLEND_STYLE_PUSH_UP || blendingStyle == BLEND_STYLE_PUSH_DOWN) ? 0 : prog * vW / 0xFFFF; - unsigned dY = (blendingStyle == BLEND_STYLE_PUSH_LEFT || blendingStyle == BLEND_STYLE_PUSH_RIGHT) ? 0 : prog * vH / 0xFFFF; - if (blendingStyle == BLEND_STYLE_PUSH_LEFT || blendingStyle == BLEND_STYLE_PUSH_TL || blendingStyle == BLEND_STYLE_PUSH_BL) x += dX; - else x -= dX; - if (blendingStyle == BLEND_STYLE_PUSH_DOWN || blendingStyle == BLEND_STYLE_PUSH_TL || blendingStyle == BLEND_STYLE_PUSH_TR) y -= dY; - else y += dY; - } -#endif - - if (x >= vW || y >= vH || x < 0 || y < 0 || isPixelXYClipped(x,y)) return; // if pixel would fall out of virtual segment just exit - - // if color is unscaled - if (!_colorScaled) col = color_fade(col, _segBri); - - 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(); - - 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); - } + if (x >= (int)vWidth() || y >= (int)vHeight() || x < 0 || y < 0) return; // if pixel would fall out of virtual segment just exit + setPixelColorXYRaw(x, y, col); } #ifdef WLED_USE_AA_PIXELS @@ -289,84 +236,62 @@ void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) const // returns RGBW values of pixel uint32_t IRAM_ATTR_YN Segment::getPixelColorXY(int x, int y) const { if (!isActive()) return 0; // not active - - const int vW = vWidth(); - const int vH = vHeight(); - -#ifndef WLED_DISABLE_MODE_BLEND - unsigned prog = 0xFFFF - progress(); - if (!prog && !_modeBlend && (blendingStyle & BLEND_STYLE_PUSH_MASK)) { - unsigned dX = (blendingStyle == BLEND_STYLE_PUSH_UP || blendingStyle == BLEND_STYLE_PUSH_DOWN) ? 0 : prog * vW / 0xFFFF; - unsigned dY = (blendingStyle == BLEND_STYLE_PUSH_LEFT || blendingStyle == BLEND_STYLE_PUSH_RIGHT) ? 0 : prog * vH / 0xFFFF; - if (blendingStyle == BLEND_STYLE_PUSH_LEFT || blendingStyle == BLEND_STYLE_PUSH_TL || blendingStyle == BLEND_STYLE_PUSH_BL) x -= dX; - else x += dX; - if (blendingStyle == BLEND_STYLE_PUSH_DOWN || blendingStyle == BLEND_STYLE_PUSH_TL || blendingStyle == BLEND_STYLE_PUSH_TR) y -= dY; - else y += dY; - } -#endif - - if (x >= vW || y >= vH || x<0 || y<0 || isPixelXYClipped(x,y)) return 0; // if pixel would fall out of virtual segment just exit - - if (reverse ) x = vW - x - 1; - if (reverse_y) y = vH - y - 1; - if (transpose) { std::swap(x,y); } // swap X & Y if segment transposed - x *= groupLength(); // expand to physical pixels - y *= groupLength(); // expand to physical pixels - if (x >= width() || y >= height()) return 0; - return strip.getPixelColorXY(start + x, startY + y); + if (x >= (int)vWidth() || y >= (int)vHeight() || x<0 || y<0) return 0; // if pixel would fall out of virtual segment just exit + return getPixelColorXYRaw(x,y); } // 2D blurring, can be asymmetrical -void Segment::blur2D(uint8_t blur_x, uint8_t blur_y, bool smear) { +void Segment::blur2D(uint8_t blur_x, uint8_t blur_y, bool smear) const { if (!isActive()) return; // not active const unsigned cols = vWidth(); const unsigned rows = vHeight(); - uint32_t lastnew; // not necessary to initialize lastnew and last, as both will be initialized by the first loop iteration + const auto XY = [&](unsigned x, unsigned y){ return x + y*cols; }; + 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; + const uint8_t seepx = blur_x >> (1 + smear); 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 cur = getPixelColorRaw(XY(x, row)); uint32_t part = color_fade(cur, seepx); curnew = color_fade(cur, keepx); if (x > 0) { if (carryover) curnew = color_add(curnew, carryover); uint32_t prev = color_add(lastnew, part); // optimization: only set pixel if color has changed - if (last != prev) setPixelColorXY(x - 1, row, prev); - } else setPixelColorXY(x, row, curnew); // first pixel + if (last != prev) setPixelColorRaw(XY(x - 1, row), prev); + } else setPixelColorRaw(XY(x, row), curnew); // first pixel lastnew = curnew; last = cur; // save original value for comparison on next iteration carryover = part; } - setPixelColorXY(cols-1, row, curnew); // set last pixel + setPixelColorRaw(XY(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; + const uint8_t seepy = blur_y >> (1 + smear); 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 cur = getPixelColorRaw(XY(col, y)); uint32_t part = color_fade(cur, seepy); curnew = color_fade(cur, keepy); if (y > 0) { if (carryover) curnew = color_add(curnew, carryover); uint32_t prev = color_add(lastnew, part); // optimization: only set pixel if color has changed - if (last != prev) setPixelColorXY(col, y - 1, prev); - } else setPixelColorXY(col, y, curnew); // first pixel + if (last != prev) setPixelColorRaw(XY(col, y - 1), prev); + } else setPixelColorRaw(XY(col, y), curnew); // first pixel lastnew = curnew; last = cur; //save original value for comparison on next iteration carryover = part; } - setPixelColorXY(col, rows - 1, curnew); + setPixelColorRaw(XY(col, rows - 1), curnew); } } } @@ -445,10 +370,11 @@ void Segment::box_blur(unsigned radius, bool smear) { delete[] tmpWSum; } */ -void Segment::moveX(int delta, bool wrap) { +void Segment::moveX(int delta, bool wrap) const { if (!isActive() || !delta) return; // not active const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + const auto XY = [&](unsigned x, unsigned y){ return x + y*vW; }; int absDelta = abs(delta); if (absDelta >= vW) return; uint32_t newPxCol[vW]; @@ -465,16 +391,17 @@ void Segment::moveX(int delta, bool wrap) { 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); + newPxCol[x] = getPixelColorRaw(XY(srcX, y)); } - for (int x = 0; x < stop; x++) setPixelColorXY(x + start, y, newPxCol[x]); + for (int x = 0; x < stop; x++) setPixelColorRaw(XY(x + start, y), newPxCol[x]); } } -void Segment::moveY(int delta, bool wrap) { +void Segment::moveY(int delta, bool wrap) const { if (!isActive() || !delta) return; // not active const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + const auto XY = [&](unsigned x, unsigned y){ return x + y*vW; }; int absDelta = abs(delta); if (absDelta >= vH) return; uint32_t newPxCol[vH]; @@ -491,9 +418,9 @@ void Segment::moveY(int delta, bool wrap) { 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); + newPxCol[y] = getPixelColorRaw(XY(x, srcY)); } - for (int y = 0; y < stop; y++) setPixelColorXY(x, y + start, newPxCol[y]); + for (int y = 0; y < stop; y++) setPixelColorRaw(XY(x, y + start), newPxCol[y]); } } @@ -501,7 +428,7 @@ void Segment::moveY(int 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(unsigned dir, unsigned delta, bool wrap) { +void Segment::move(unsigned dir, unsigned delta, bool wrap) const { if (delta==0) return; switch (dir) { case 0: moveX( delta, wrap); break; @@ -515,7 +442,7 @@ void Segment::move(unsigned dir, unsigned delta, bool wrap) { } } -void Segment::drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) { +void Segment::drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) const { if (!isActive() || radius == 0) return; // not active if (soft) { // Xiaolin Wu’s algorithm @@ -549,9 +476,6 @@ void Segment::drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, 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; @@ -570,20 +494,16 @@ void Segment::drawCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, d += 4 * x + 6; } } - _colorScaled = false; } } // by stepko, taken from https://editor.soulmatelights.com/gallery/573-blobs -void Segment::fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) { +void Segment::fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, bool soft) const { if (!isActive() || radius == 0) return; // not active const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) const int vH = vHeight(); // segment height in logical pixels (is always >= 1) // draw soft bounding circle if (soft) drawCircle(cx, cy, radius, col, soft); - // 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++) { @@ -593,11 +513,10 @@ void Segment::fillCircle(uint16_t cx, uint16_t cy, uint8_t radius, uint32_t col, setPixelColorXY(cx + x, cy + y, col); } } - _colorScaled = false; } //line function -void Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft) { +void Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint32_t c, bool soft) const { if (!isActive()) return; // not active const int 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) @@ -633,15 +552,12 @@ void Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint3 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)); + blendPixelColorXY(x, y, c, seep); + blendPixelColorXY(x+int(steep), y+int(!steep), c, keep); 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 (;;) { @@ -651,7 +567,6 @@ void Segment::drawLine(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint3 if (e2 >-dx) { err -= dy; x0 += sx; } if (e2 < dy) { err += dx; y0 += sy; } } - _colorScaled = false; } } @@ -663,29 +578,25 @@ 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, bool usePalGrad) { +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) const { if (!isActive()) return; // not active if (chr < 32 || chr > 126) return; // only ASCII 32-126 supported chr -= 32; // align with font table entries 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 + CRGBPalette16 grad = col2 ? CRGBPalette16(CRGB(color), CRGB(col2)) : SEGPALETTE; // selected palette as gradient - //if (w<5 || w>6 || h!=8) return; for (int i = 0; i= (int)vWidth() || y0 < 0 || y0 >= (int)vHeight()) continue; // drawing off-screen if (((bits>>(j+(8-w))) & 0x01)) { // bit set - setPixelColorXY(x0, y0, c.color32); + setPixelColorXYRaw(x0, y0, c.color32); } } - _colorScaled = false; } } #define WU_WEIGHT(a,b) ((uint8_t) (((a)*(b)+(a)+(b))>>8)) -void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) { //awesome wu_pixel procedure by reddit u/sutaburosu +void Segment::wu_pixel(uint32_t x, uint32_t y, CRGB c) const { //awesome wu_pixel procedure by reddit u/sutaburosu if (!isActive()) return; // not active // extract the fractional parts and derive their inverses unsigned xx = x & 0xff, yy = y & 0xff, ix = 255 - xx, iy = 255 - yy; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp old mode 100644 new mode 100755 index 42403fa85..57443caba --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -10,7 +10,6 @@ Modified heavily for WLED */ #include "wled.h" -#include "FX.h" #include "FXparticleSystem.h" // TODO: better define the required function (mem service) in FX.h? #include "palettes.h" @@ -30,39 +29,10 @@ 19, 18, 17, 16, 15, 20, 21, 22, 23, 24, 29, 28, 27, 26, 25]} */ -#ifndef PIXEL_COUNTS - #define PIXEL_COUNTS DEFAULT_LED_COUNT -#endif - -#ifndef DATA_PINS - #define DATA_PINS DEFAULT_LED_PIN -#endif - -#ifndef LED_TYPES - #define LED_TYPES DEFAULT_LED_TYPE -#endif - -#ifndef DEFAULT_LED_COLOR_ORDER - #define DEFAULT_LED_COLOR_ORDER COL_ORDER_GRB //default to GRB -#endif - - #if MAX_NUM_SEGMENTS < WLED_MAX_BUSSES #error "Max segments must be at least max number of busses!" #endif -static constexpr unsigned sumPinsRequired(const unsigned* current, size_t count) { - return (count > 0) ? (Bus::getNumberOfPins(*current) + sumPinsRequired(current+1,count-1)) : 0; -} - -static constexpr bool validatePinsAndTypes(const unsigned* types, unsigned numTypes, unsigned numPins ) { - // Pins provided < pins required -> always invalid - // Pins provided = pins required -> always valid - // Pins provided > pins required -> valid if excess pins are a product of last type pins since it will be repeated - return (sumPinsRequired(types, numTypes) > numPins) ? false : - (numPins - sumPinsRequired(types, numTypes)) % Bus::getNumberOfPins(types[numTypes-1]) == 0; -} - /////////////////////////////////////////////////////////////////////////////// // Segment class implementation @@ -73,34 +43,40 @@ 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; +uint16_t Segment::_lastPaletteChange = 0; // in seconds; perhaps it should be per segment +uint16_t Segment::_nextPaletteBlend = 0; // in millis -#ifndef WLED_DISABLE_MODE_BLEND -bool Segment::_modeBlend = false; +bool Segment::_modeBlend = false; uint16_t Segment::_clipStart = 0; uint16_t Segment::_clipStop = 0; uint8_t Segment::_clipStartY = 0; uint8_t Segment::_clipStopY = 1; -#endif // copy constructor Segment::Segment(const Segment &orig) { //DEBUG_PRINTF_P(PSTR("-- Copy segment constructor: %p -> %p\n"), &orig, this); memcpy((void*)this, (void*)&orig, sizeof(Segment)); - _t = nullptr; // copied segment cannot be in transition + _t = nullptr; // copied segment cannot be in transition name = nullptr; data = nullptr; _dataLen = 0; - if (orig.name) { name = static_cast(malloc(strlen(orig.name)+1)); if (name) strcpy(name, orig.name); } + pixels = nullptr; + if (!stop) return; // nothing to do if segment is inactive/invalid + if (orig.name) { name = static_cast(d_malloc(strlen(orig.name)+1)); if (name) strcpy(name, orig.name); } if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); } + if (orig.pixels) { + pixels = static_cast(d_malloc(sizeof(uint32_t) * orig.length())); + if (pixels) memcpy(pixels, orig.pixels, sizeof(uint32_t) * orig.length()); + else { + DEBUG_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!")); + errorFlag = ERR_NORAM_PX; + stop = 0; // mark segment as inactive/invalid + } + } else stop = 0; // mark segment as inactive/invalid } // move constructor @@ -111,6 +87,7 @@ Segment::Segment(Segment &&orig) noexcept { orig.name = nullptr; orig.data = nullptr; orig._dataLen = 0; + orig.pixels = nullptr; } // copy assignment @@ -118,17 +95,29 @@ Segment& Segment::operator= (const Segment &orig) { //DEBUG_PRINTF_P(PSTR("-- Copying segment: %p -> %p\n"), &orig, this); if (this != &orig) { // clean destination - if (name) { free(name); name = nullptr; } - stopTransition(); + if (name) { d_free(name); name = nullptr; } + if (_t) stopTransition(); // also erases _t deallocateData(); + d_free(pixels); // copy source memcpy((void*)this, (void*)&orig, sizeof(Segment)); // erase pointers to allocated data data = nullptr; _dataLen = 0; + pixels = nullptr; + if (!stop) return *this; // nothing to do if segment is inactive/invalid // copy source data - if (orig.name) { name = static_cast(malloc(strlen(orig.name)+1)); if (name) strcpy(name, orig.name); } + if (orig.name) { name = static_cast(d_malloc(strlen(orig.name)+1)); if (name) strcpy(name, orig.name); } if (orig.data) { if (allocateData(orig._dataLen)) memcpy(data, orig.data, orig._dataLen); } + if (orig.pixels) { + pixels = static_cast(d_malloc(sizeof(uint32_t) * orig.length())); + if (pixels) memcpy(pixels, orig.pixels, sizeof(uint32_t) * orig.length()); + else { + DEBUG_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!")); + errorFlag = ERR_NORAM_PX; + stop = 0; // mark segment as inactive/invalid + } + } else stop = 0; // mark segment as inactive/invalid } return *this; } @@ -137,48 +126,60 @@ Segment& Segment::operator= (const Segment &orig) { Segment& Segment::operator= (Segment &&orig) noexcept { //DEBUG_PRINTF_P(PSTR("-- Moving segment: %p -> %p\n"), &orig, this); if (this != &orig) { - if (name) { free(name); name = nullptr; } // free old name - stopTransition(); + if (name) { d_free(name); name = nullptr; } // free old name + if (_t) stopTransition(); // also erases _t deallocateData(); // free old runtime data + d_free(pixels); // free old pixel buffer + // move source data memcpy((void*)this, (void*)&orig, sizeof(Segment)); orig.name = nullptr; orig.data = nullptr; orig._dataLen = 0; - orig._t = nullptr; // old segment cannot be in transition + orig.pixels = nullptr; + orig._t = nullptr; // old segment cannot be in transition } return *this; } // allocates effect data buffer on heap and initialises (erases) it -bool IRAM_ATTR_YN Segment::allocateData(size_t len) { +bool Segment::allocateData(size_t len) { if (len == 0) return false; // nothing to do if (data && _dataLen >= len) { // already allocated enough (reduce fragmentation) - if (call == 0) memset(data, 0, len); // erase buffer if called during effect initialisation + if (call == 0) { + //DEBUG_PRINTF_P(PSTR("-- Clearing data (%d): %p\n"), len, this); + memset(data, 0, len); // erase buffer if called during effect initialisation + } return true; } - //DEBUG_PRINTF_P(PSTR("-- Allocating data (%d): %p\n", len, this); - deallocateData(); // if the old buffer was smaller release it first - if (Segment::getUsedSegmentData() + len > MAX_SEGMENT_DATA) { + //DEBUG_PRINTF_P(PSTR("-- Allocating data (%d): %p\n"), len, this); + if (Segment::getUsedSegmentData() + len - _dataLen > MAX_SEGMENT_DATA) { // not enough memory - DEBUG_PRINT(F("!!! Effect RAM depleted: ")); - DEBUG_PRINTF_P(PSTR("%d/%d !!!\n"), len, Segment::getUsedSegmentData()); + DEBUG_PRINTF_P(PSTR("!!! Not enough RAM: %d/%d !!!\n"), len, Segment::getUsedSegmentData()); errorFlag = ERR_NORAM; return false; } - // do not use SPI RAM on ESP32 since it is slow - data = (byte*)calloc(len, sizeof(byte)); - if (!data) { DEBUG_PRINTLN(F("!!! Allocation failed. !!!")); return false; } // allocation failed - Segment::addUsedSegmentData(len); - //DEBUG_PRINTF_P(PSTR("--- Allocated data (%p): %d/%d -> %p\n"), this, len, Segment::getUsedSegmentData(), data); - _dataLen = len; - return true; + // prefer DRAM over SPI RAM on ESP32 since it is slow + if (data) data = (byte*)d_realloc(data, len); + else data = (byte*)d_malloc(len); + if (data) { + memset(data, 0, len); // erase buffer + Segment::addUsedSegmentData(len - _dataLen); + _dataLen = len; + //DEBUG_PRINTF_P(PSTR("--- Allocated data (%p): %d/%d -> %p\n"), this, len, Segment::getUsedSegmentData(), data); + return true; + } + // allocation failed + DEBUG_PRINTLN(F("!!! Allocation failed. !!!")); + Segment::addUsedSegmentData(-_dataLen); // subtract original buffer size + errorFlag = ERR_NORAM; + return false; } -void IRAM_ATTR_YN Segment::deallocateData() { +void Segment::deallocateData() { if (!data) { _dataLen = 0; return; } - //DEBUG_PRINTF_P(PSTR("--- Released data (%p): %d/%d -> %p\n"), this, _dataLen, Segment::getUsedSegmentData(), data); if ((Segment::getUsedSegmentData() > 0) && (_dataLen > 0)) { // check that we don't have a dangling / inconsistent data pointer - free(data); + //DEBUG_PRINTF_P(PSTR("--- Released data (%p): %d/%d -> %p\n"), this, _dataLen, Segment::getUsedSegmentData(), data); + d_free(data); } else { DEBUG_PRINTF_P(PSTR("---- Released data (%p): inconsistent UsedSegmentData (%d/%d), cowardly refusing to free nothing.\n"), this, _dataLen, Segment::getUsedSegmentData()); } @@ -195,9 +196,10 @@ void IRAM_ATTR_YN Segment::deallocateData() { * may free that data buffer. */ void Segment::resetIfRequired() { - if (!reset) return; + if (!reset || !isActive()) return; //DEBUG_PRINTF_P(PSTR("-- Segment reset: %p\n"), this); if (data && _dataLen > 0) memset(data, 0, _dataLen); // prevent heap fragmentation (just erase buffer instead of deallocateData()) + if (pixels) for (size_t i = 0; i < length(); i++) pixels[i] = BLACK; // clear pixel buffer next_time = 0; step = 0; call = 0; aux0 = 0; aux1 = 0; reset = false; #ifdef WLED_ENABLE_GIF @@ -207,32 +209,36 @@ void Segment::resetIfRequired() { CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; - if (pal > 245 && (strip.customPalettes.size() == 0 || 255U-pal > strip.customPalettes.size()-1)) pal = 0; // TODO remove strip dependency by moving customPalettes out of strip + if (pal > 245 && (customPalettes.size() == 0 || 255U-pal > customPalettes.size()-1)) pal = 0; //default palette. Differs depending on effect - if (pal == 0) pal = _default_palette; //load default palette set in FX _data, party colors as default + if (pal == 0) pal = _default_palette; // _default_palette is set in setMode() switch (pal) { case 0: //default palette. Exceptions for specific effects above - targetPalette = PartyColors_p; break; + targetPalette = PartyColors_p; + break; case 1: //randomly generated palette targetPalette = _randomPalette; //random palette is generated at intervals in handleRandomPalette() break; case 2: {//primary color only - CRGB prim = gamma32(colors[0]); - targetPalette = CRGBPalette16(prim); break;} + CRGB prim = colors[0]; + targetPalette = CRGBPalette16(prim); + break;} case 3: {//primary + secondary - CRGB prim = gamma32(colors[0]); - CRGB sec = gamma32(colors[1]); - targetPalette = CRGBPalette16(prim,prim,sec,sec); break;} + CRGB prim = colors[0]; + CRGB sec = colors[1]; + targetPalette = CRGBPalette16(prim,prim,sec,sec); + break;} case 4: {//primary + secondary + tertiary - CRGB prim = gamma32(colors[0]); - CRGB sec = gamma32(colors[1]); - CRGB ter = gamma32(colors[2]); - targetPalette = CRGBPalette16(ter,sec,prim); break;} + CRGB prim = colors[0]; + CRGB sec = colors[1]; + CRGB ter = colors[2]; + targetPalette = CRGBPalette16(ter,sec,prim); + break;} case 5: {//primary + secondary (+tertiary if not off), more distinct - CRGB prim = gamma32(colors[0]); - CRGB sec = gamma32(colors[1]); + CRGB prim = colors[0]; + CRGB sec = colors[1]; if (colors[2]) { - CRGB ter = gamma32(colors[2]); + CRGB ter = colors[2]; targetPalette = CRGBPalette16(prim,prim,prim,prim,prim,sec,sec,sec,sec,sec,ter,ter,ter,ter,ter,prim); } else { targetPalette = CRGBPalette16(prim,prim,prim,prim,prim,prim,prim,prim,sec,sec,sec,sec,sec,sec,sec,sec); @@ -240,7 +246,7 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { break;} default: //progmem palettes if (pal>245) { - targetPalette = strip.customPalettes[255-pal]; // we checked bounds above + targetPalette = customPalettes[255-pal]; // we checked bounds above } else if (pal < 13) { // palette 6 - 12, fastled palettes targetPalette = *fastledPalettes[pal-6]; } else { @@ -253,251 +259,137 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { return targetPalette; } -void Segment::startTransition(uint16_t dur) { - if (dur == 0) { - if (isInTransition()) _t->_dur = dur; // this will stop transition in next handleTransition() +// starting a transition has to occur before change so we get current values 1st +void Segment::startTransition(uint16_t dur, bool segmentCopy) { + if (dur == 0 || !isActive()) { + if (isInTransition()) _t->_dur = 0; return; } - if (isInTransition()) return; // already in transition no need to store anything - - // starting a transition has to occur before change so we get current values 1st - _t = new(std::nothrow) Transition(dur); // no previous transition running - if (!_t) return; // failed to allocate data - - //DEBUG_PRINTF_P(PSTR("-- Started transition: %p (%p)\n"), this, _t); - loadPalette(_t->_palT, palette); - _t->_palTid = palette; - _t->_briT = on ? opacity : 0; - _t->_cctT = cct; -#ifndef WLED_DISABLE_MODE_BLEND - swapSegenv(_t->_segT); // copy runtime data to temporary - _t->_modeT = mode; - _t->_segT._dataLenT = 0; - _t->_segT._dataT = nullptr; - if (_dataLen > 0 && data) { - _t->_segT._dataT = (byte *)malloc(_dataLen); - if (_t->_segT._dataT) { - //DEBUG_PRINTF_P(PSTR("-- Allocated duplicate data (%d) for %p: %p\n"), _dataLen, this, _t->_segT._dataT); - memcpy(_t->_segT._dataT, data, _dataLen); - _t->_segT._dataLenT = _dataLen; + if (isInTransition()) { + if (segmentCopy && !_t->_oldSegment) { + // already in transition but segment copy requested and not yet created + _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings + _t->_start = millis(); // restart countdown + _t->_dur = dur; + if (_t->_oldSegment) { + _t->_oldSegment->palette = _t->_palette; // restore original palette and colors (from start of transition) + for (unsigned i = 0; i < NUM_COLORS; i++) _t->_oldSegment->colors[i] = _t->_colors[i]; + } + DEBUG_PRINTF_P(PSTR("-- Updated transition with segment copy: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); } + return; } - DEBUG_PRINTF_P(PSTR("-- pal: %d, bri: %d, C:[%08X,%08X,%08X], m: %d\n"), - (int)_t->_palTid, - (int)_t->_briT, - _t->_segT._colorT[0], - _t->_segT._colorT[1], - _t->_segT._colorT[2], - (int)_t->_modeT); -#else - for (size_t i=0; i_colorT[i] = colors[i]; -#endif + + // no previous transition running, start by allocating memory for segment copy + _t = new(std::nothrow) Transition(dur); + if (_t) { + _t->_bri = on ? opacity : 0; + _t->_cct = cct; + _t->_palette = palette; + #ifndef WLED_SAVE_RAM + loadPalette(_t->_palT, palette); + #endif + for (int i=0; i_colors[i] = colors[i]; + if (segmentCopy) _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings + #ifdef WLED_DEBUG + if (_t->_oldSegment) { + DEBUG_PRINTF_P(PSTR("-- Started transition: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); + } else { + DEBUG_PRINTF_P(PSTR("-- Started transition without old segment: S=%p T(%p)\n"), this, _t); + } + #endif + }; } void Segment::stopTransition() { - if (isInTransition()) { - //DEBUG_PRINTF_P(PSTR("-- Stopping transition: %p\n"), this); - #ifndef WLED_DISABLE_MODE_BLEND - if (_t->_segT._dataT && _t->_segT._dataLenT > 0) { - //DEBUG_PRINTF_P(PSTR("-- Released duplicate data (%d) for %p: %p\n"), _t->_segT._dataLenT, this, _t->_segT._dataT); - free(_t->_segT._dataT); - _t->_segT._dataT = nullptr; - _t->_segT._dataLenT = 0; - } - #endif - delete _t; - _t = nullptr; - } - _transitionprogress = 0xFFFFU; // stop means stop - transition has ended + DEBUG_PRINTF_P(PSTR("-- Stopping transition: S=%p T(%p) O[%p]\n"), this, _t, _t->_oldSegment); + delete _t; + _t = nullptr; } -// transition progression between 0-65535 -inline void Segment::updateTransitionProgress() { - _transitionprogress = 0xFFFFU; +// sets transition progress variable (0-65535) based on time passed since transition start +void Segment::updateTransitionProgress() const { if (isInTransition()) { + _t->_progress = 0xFFFF; unsigned diff = millis() - _t->_start; - if (_t->_dur > 0 && diff < _t->_dur) _transitionprogress = diff * 0xFFFFU / _t->_dur; + if (_t->_dur > 0 && diff < _t->_dur) _t->_progress = diff * 0xFFFFU / _t->_dur; } } -#ifndef WLED_DISABLE_MODE_BLEND -void Segment::swapSegenv(tmpsegd_t &tmpSeg) { - //DEBUG_PRINTF_P(PSTR("-- Saving temp seg: %p->(%p) [%d->%p]\n"), this, &tmpSeg, _dataLen, data); - tmpSeg._optionsT = options; - for (size_t i=0; i_segT)) { - // swap SEGENV with transitional data - options = _t->_segT._optionsT; - for (size_t i=0; i_segT._colorT[i]; - speed = _t->_segT._speedT; - intensity = _t->_segT._intensityT; - custom1 = _t->_segT._custom1T; - custom2 = _t->_segT._custom2T; - custom3 = _t->_segT._custom3T; - check1 = _t->_segT._check1T; - check2 = _t->_segT._check2T; - check3 = _t->_segT._check3T; - aux0 = _t->_segT._aux0T; - aux1 = _t->_segT._aux1T; - step = _t->_segT._stepT; - call = _t->_segT._callT; - data = _t->_segT._dataT; - _dataLen = _t->_segT._dataLenT; - } -} - -void Segment::restoreSegenv(const tmpsegd_t &tmpSeg) { - //DEBUG_PRINTF_P(PSTR("-- Restoring temp seg: %p->(%p) [%d->%p]\n"), &tmpSeg, this, _dataLen, data); - if (isInTransition() && &(_t->_segT) != &tmpSeg) { - // update possibly changed variables to keep old effect running correctly - _t->_segT._aux0T = aux0; - _t->_segT._aux1T = aux1; - _t->_segT._stepT = step; - _t->_segT._callT = call; - //if (_t->_segT._dataT != data) DEBUG_PRINTF_P(PSTR("--- data re-allocated: (%p) %p -> %p\n"), this, _t->_segT._dataT, data); - _t->_segT._dataT = data; - _t->_segT._dataLenT = _dataLen; - } - options = tmpSeg._optionsT; - for (size_t i=0; i_cctT : (_t->_segT._optionsT & 0x0004 ? _t->_briT : 0); - // _modeBlend==true -> old effect - if (blendingStyle != BLEND_STYLE_FADE) return _modeBlend ? tmpBri : curBri; // not fade/blend transition, each effect uses its brightness -#else - uint8_t tmpBri = useCct ? _t->_cctT : _t->_briT; -#endif - curBri *= prog; - curBri += tmpBri * (0xFFFFU - prog); - return curBri / 0xFFFFU; + if (blendingStyle == BLEND_STYLE_FADE) return (cct * prog + (_t->_cct * (0xFFFFU - prog))) / 0xFFFFU; + //else return Segment::isPreviousMode() ? _t->_cct : cct; + } + return cct; +} + +// will return segment's opacity during a transition (blending it with old in case of FADE transition) +uint8_t Segment::currentBri() const { + unsigned prog = progress(); + unsigned curBri = on ? opacity : 0; + if (prog < 0xFFFFU) { + // this will blend opacity in new mode if style is FADE (single effect call) + if (blendingStyle == BLEND_STYLE_FADE) curBri = (prog * curBri + _t->_bri * (0xFFFFU - prog)) / 0xFFFFU; + else curBri = Segment::isPreviousMode() ? _t->_bri : curBri; } return curBri; } -uint8_t Segment::currentMode() const { -#ifndef WLED_DISABLE_MODE_BLEND - unsigned prog = isInTransition() ? progress() : 0xFFFFU; - if (prog == 0xFFFFU) return mode; - if (blendingStyle != BLEND_STYLE_FADE) { - // workaround for on/off transition to respect blending style - uint8_t modeT = (bri != briT) && bri ? FX_MODE_STATIC : _t->_modeT; // On/Off transition active (bri!=briT) and final bri>0 : old mode is STATIC - uint8_t modeS = (bri != briT) && !bri ? FX_MODE_STATIC : mode; // On/Off transition active (bri!=briT) and final bri==0 : new mode is STATIC - return _modeBlend ? modeT : modeS; // _modeBlend==true -> old effect - } - return _modeBlend ? _t->_modeT : mode; // _modeBlend==true -> old effect -#else - return mode; -#endif -} - -uint32_t Segment::currentColor(uint8_t slot) const { - if (slot >= NUM_COLORS) slot = 0; - unsigned prog = progress(); - if (prog == 0xFFFFU) return colors[slot]; -#ifndef WLED_DISABLE_MODE_BLEND - if (blendingStyle != BLEND_STYLE_FADE) { - // workaround for on/off transition to respect blending style - uint32_t colT = (bri != briT) && bri ? BLACK : _t->_segT._colorT[slot]; // On/Off transition active (bri!=briT) and final bri>0 : old color is BLACK - uint32_t colS = (bri != briT) && !bri ? BLACK : colors[slot]; // On/Off transition active (bri!=briT) and final bri==0 : new color is BLACK - return _modeBlend ? colT : colS; // _modeBlend==true -> old effect - } - return color_blend16(_t->_segT._colorT[slot], colors[slot], prog); -#else - return color_blend16(_t->_colorT[slot], colors[slot], prog); -#endif -} - // 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(); - unsigned prog = isInTransition() ? progress() : 0xFFFFU; // transition progress; 0xFFFFU = no transition active - // adjust gamma for effects - for (unsigned i = 0; i < NUM_COLORS; i++) { - #ifndef WLED_DISABLE_MODE_BLEND - uint32_t col = isInTransition() ? color_blend16(_t->_segT._colorT[i], colors[i], prog) : colors[i]; - #else - uint32_t col = isInTransition() ? color_blend16(_t->_colorT[i], colors[i], prog) : colors[i]; - #endif - _currentColors[i] = gamma32(col); - } +// and blends colors and palettes if necessary +// prog is the progress of the transition (0-65535) and is passed to the function as it may be called in the context of old segment +// which does not have transition structure +void Segment::beginDraw(uint16_t prog) { + setDrawDimensions(); + // load colors into _currentColors + for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = colors[i]; // load palette into _currentPalette - loadPalette(_currentPalette, palette); - if (prog < 0xFFFFU) { -#ifndef WLED_DISABLE_MODE_BLEND - if (blendingStyle > BLEND_STYLE_FADE) { - //if (_modeBlend) loadPalette(_currentPalette, _t->_palTid); // not fade/blend transition, each effect uses its palette - if (_modeBlend) _currentPalette = _t->_palT; // not fade/blend transition, each effect uses its palette - } else -#endif - { - // blend palettes - // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) - // minimum blend time is 100ms maximum is 65535ms - unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends; - for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, _currentPalette, 48); - _currentPalette = _t->_palT; // copy transitioning/temporary palette - } + loadPalette(Segment::_currentPalette, palette); + if (isInTransition() && prog < 0xFFFFU && blendingStyle == BLEND_STYLE_FADE) { + // blend colors + for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = color_blend16(_t->_colors[i], colors[i], prog); + // blend palettes + // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) + // minimum blend time is 100ms maximum is 65535ms + #ifndef WLED_SAVE_RAM + unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends; + for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, Segment::_currentPalette, 48); + Segment::_currentPalette = _t->_palT; // copy transitioning/temporary palette + #else + unsigned noOfBlends = ((255U * prog) / 0xFFFFU); + CRGBPalette16 tmpPalette; + loadPalette(tmpPalette, _t->_palette); + for (unsigned i = 0; i < noOfBlends; i++) nblendPaletteTowardPalette(tmpPalette, Segment::_currentPalette, 48); + Segment::_currentPalette = tmpPalette; // copy transitioning/temporary palette + #endif } } -// loads palette of the old FX during transitions (used by particle system) -void Segment::loadOldPalette(void) { - if(isInTransition()) - loadPalette(_currentPalette, _t->_palTid); -} - // relies on WS2812FX::service() to call it for each frame void Segment::handleRandomPalette() { + unsigned long now = millis(); + uint16_t now_s = now / 1000; // we only need seconds (and @dedehai hated shift >> 10) + now = (now_s)*1000 + (now % 1000); // ignore days (now is limited to 18 hours as now_s can only store 65535s ~ 18h 12min) + if (now_s < Segment::_lastPaletteChange) Segment::_lastPaletteChange = 0; // handle overflow (will cause 2*randomPaletteChangeTime glitch at most) // is it time to generate a new palette? - if ((uint16_t)(millis()/1000U) - _lastPaletteChange > randomPaletteChangeTime) { - _newRandomPalette = useHarmonicRandomPalette ? generateHarmonicRandomPalette(_randomPalette) : generateRandomPalette(); - _lastPaletteChange = (uint16_t)(millis()/1000U); - _lastPaletteBlend = (uint16_t)(millis())-512; // starts blending immediately + if (now_s > Segment::_lastPaletteChange + randomPaletteChangeTime) { + Segment::_newRandomPalette = useHarmonicRandomPalette ? generateHarmonicRandomPalette(Segment::_randomPalette) : generateRandomPalette(); + Segment::_lastPaletteChange = now_s; + Segment::_nextPaletteBlend = now; // starts blending immediately } - - // assumes that 128 updates are sufficient to blend a palette, so shift by 7 (can be more, can be less) - // in reality there need to be 255 blends to fully blend two entirely different palettes - if ((uint16_t)millis() - _lastPaletteBlend < strip.getTransition() >> 7) return; // not yet time to fade, delay the update - _lastPaletteBlend = (uint16_t)millis(); - nblendPaletteTowardPalette(_randomPalette, _newRandomPalette, 48); + // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in strip.getTransition() time) + // if randomPaletteChangeTime is shorter than strip.getTransition() palette will never fully blend + unsigned frameTime = strip.getFrameTime(); // in ms [8-1000] + unsigned transitionTime = strip.getTransition(); // in ms [100-65535] + if ((uint16_t)now < Segment::_nextPaletteBlend || now > ((Segment::_lastPaletteChange*1000) + transitionTime + 2*frameTime)) return; // not yet time or past transition time, no need to blend + unsigned transitionFrames = frameTime > transitionTime ? 1 : transitionTime / frameTime; // i.e. 700ms/23ms = 30 or 20000ms/8ms = 2500 or 100ms/1000ms = 0 -> 1 + unsigned noOfBlends = transitionFrames > 255 ? 1 : (255 + (transitionFrames>>1)) / transitionFrames; // we do some rounding here + for (unsigned i = 0; i < noOfBlends; i++) nblendPaletteTowardPalette(Segment::_randomPalette, Segment::_newRandomPalette, 48); + Segment::_nextPaletteBlend = now + ((transitionFrames >> 8) * frameTime); // postpone next blend if necessary } // sets Segment geometry (length or width/height and grouping, spacing and offset as well as 2D mapping) @@ -507,19 +399,11 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui // return if neither bounds nor grouping have changed bool boundsUnchanged = (start == i1 && stop == i2); #ifndef WLED_DISABLE_2D - if (Segment::maxHeight>1) boundsUnchanged &= (startY == i1Y && stopY == i2Y); // 2D + boundsUnchanged &= (startY == i1Y && stopY == i2Y); // 2D #endif + boundsUnchanged &= (grouping == grp && spacing == spc); // changing grouping and/or spacing changes virtual segment length (painting dimensions) if (stop && (spc > 0 || m12 != map1D2D)) clear(); -/* - if (boundsUnchanged - && (!grp || (grouping == grp && spacing == spc)) - && (ofs == UINT16_MAX || ofs == offset) - && (m12 == map1D2D) - ) return; -*/ - stateChanged = true; // send UDP/WS broadcast - if (grp) { // prevent assignment of 0 grouping = grp; spacing = spc; @@ -530,30 +414,50 @@ void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, ui if (ofs < UINT16_MAX) offset = ofs; map1D2D = constrain(m12, 0, 7); - DEBUG_PRINTF_P(PSTR("Segment geometry: %d,%d -> %d,%d\n"), (int)i1, (int)i2, (int)i1Y, (int)i2Y); - markForReset(); if (boundsUnchanged) return; + unsigned oldLength = length(); + + DEBUG_PRINTF_P(PSTR("Segment geometry: %d,%d -> %d,%d [%d,%d]\n"), (int)i1, (int)i2, (int)i1Y, (int)i2Y, (int)grp, (int)spc); + markForReset(); + startTransition(strip.getTransition()); // start transition prior to change (if segment is deactivated (start>stop) no transition will happen) + stateChanged = true; // send UDP/WS broadcast + // apply change immediately if (i2 <= i1) { //disable segment + d_free(pixels); + pixels = nullptr; stop = 0; return; } if (i1 < Segment::maxWidth || (i1 >= Segment::maxWidth*Segment::maxHeight && i1 < strip.getLengthTotal())) start = i1; // Segment::maxWidth equals strip.getLengthTotal() for 1D - stop = i2 > Segment::maxWidth*Segment::maxHeight ? MIN(i2,strip.getLengthTotal()) : (i2 > Segment::maxWidth ? Segment::maxWidth : MAX(1,i2)); + stop = i2 > Segment::maxWidth*Segment::maxHeight ? MIN(i2,strip.getLengthTotal()) : constrain(i2, 1, Segment::maxWidth); startY = 0; stopY = 1; #ifndef WLED_DISABLE_2D if (Segment::maxHeight>1) { // 2D if (i1Y < Segment::maxHeight) startY = i1Y; - stopY = i2Y > Segment::maxHeight ? Segment::maxHeight : MAX(1,i2Y); + stopY = constrain(i2Y, 1, Segment::maxHeight); } #endif // safety check if (start >= stop || startY >= stopY) { + d_free(pixels); + pixels = nullptr; stop = 0; return; } + // re-allocate FX render buffer + if (length() != oldLength) { + if (pixels) pixels = static_cast(d_realloc(pixels, sizeof(uint32_t) * length())); + else pixels = static_cast(d_malloc(sizeof(uint32_t) * length())); + if (!pixels) { + DEBUG_PRINTLN(F("!!! Not enough RAM for pixel buffer !!!")); + errorFlag = ERR_NORAM_PX; + stop = 0; + return; + } + } refreshLightCapabilities(); } @@ -565,7 +469,7 @@ Segment &Segment::setColor(uint8_t slot, uint32_t c) { if (slot == 1 && c != BLACK) return *this; // on/off segment cannot have secondary color non black } //DEBUG_PRINTF_P(PSTR("- Starting color transition: %d [0x%X]\n"), slot, c); - startTransition(strip.getTransition()); // start transition prior to change + startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change colors[slot] = c; stateChanged = true; // send UDP/WS broadcast return *this; @@ -579,7 +483,7 @@ Segment &Segment::setCCT(uint16_t k) { } if (cct != k) { //DEBUG_PRINTF_P(PSTR("- Starting CCT transition: %d\n"), k); - startTransition(strip.getTransition()); // start transition prior to change + startTransition(strip.getTransition(), false); // start transition prior to change (no need to copy segment) cct = k; stateChanged = true; // send UDP/WS broadcast } @@ -589,7 +493,7 @@ Segment &Segment::setCCT(uint16_t k) { Segment &Segment::setOpacity(uint8_t o) { if (opacity != o) { //DEBUG_PRINTF_P(PSTR("- Starting opacity transition: %d\n"), o); - startTransition(strip.getTransition()); // start transition prior to change + startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change opacity = o; stateChanged = true; // send UDP/WS broadcast } @@ -597,11 +501,13 @@ Segment &Segment::setOpacity(uint8_t o) { } Segment &Segment::setOption(uint8_t n, bool val) { - bool prevOn = on; - if (n == SEG_OPTION_ON && val != prevOn) startTransition(strip.getTransition()); // start transition prior to change + bool prev = (options >> n) & 0x01; + if (val == prev) return *this; + //DEBUG_PRINTF_P(PSTR("- Starting option transition: %d\n"), n); + if (n == SEG_OPTION_ON) startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change if (val) options |= 0x01 << n; else options &= ~(0x01 << n); - if (!(n == SEG_OPTION_SELECTED || n == SEG_OPTION_RESET)) stateChanged = true; // send UDP/WS broadcast + stateChanged = true; // send UDP/WS broadcast return *this; } @@ -611,10 +517,7 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) { if (fx >= strip.getModeCount()) fx = 0; // set solid mode // if we have a valid mode & is not reserved if (fx != mode) { -#ifndef WLED_DISABLE_MODE_BLEND - //DEBUG_PRINTF_P(PSTR("- Starting effect transition: %d\n"), fx); - startTransition(strip.getTransition()); // set effect transitions -#endif + startTransition(strip.getTransition(), true); // set effect transitions (must create segment copy) mode = fx; int sOpt; // load default values from effect string @@ -633,10 +536,10 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) { sOpt = extractModeDefaults(fx, "mi"); if (sOpt >= 0) mirror = (bool)sOpt; // NOTE: setting this option is a risky business sOpt = extractModeDefaults(fx, "rY"); if (sOpt >= 0) reverse_y = (bool)sOpt; sOpt = extractModeDefaults(fx, "mY"); if (sOpt >= 0) mirror_y = (bool)sOpt; // NOTE: setting this option is a risky business - sOpt = extractModeDefaults(fx, "pal"); if (sOpt >= 0) setPalette(sOpt); //else setPalette(0); } sOpt = extractModeDefaults(fx, "pal"); // always extract 'pal' to set _default_palette - if(sOpt <= 0) sOpt = 6; // partycolors if zero or not set + if (sOpt >= 0 && loadDefaults) setPalette(sOpt); + if (sOpt <= 0) sOpt = 6; // partycolors if zero or not set _default_palette = sOpt; // _deault_palette is loaded into pal0 in loadPalette() (if selected) markForReset(); stateChanged = true; // send UDP/WS broadcast @@ -646,10 +549,10 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) { Segment &Segment::setPalette(uint8_t pal) { if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; // built in palettes - if (pal > 245 && (strip.customPalettes.size() == 0 || 255U-pal > strip.customPalettes.size()-1)) pal = 0; // custom palettes + if (pal > 245 && (customPalettes.size() == 0 || 255U-pal > customPalettes.size()-1)) pal = 0; // custom palettes if (pal != palette) { //DEBUG_PRINTF_P(PSTR("- Starting palette transition: %d\n"), pal); - startTransition(strip.getTransition()); + startTransition(strip.getTransition(), blendingStyle != BLEND_STYLE_FADE); // start transition prior to change (no need to copy segment) palette = pal; stateChanged = true; // send UDP/WS broadcast } @@ -660,8 +563,8 @@ Segment &Segment::setName(const char *newName) { if (newName) { const int newLen = min(strlen(newName), (size_t)WLED_MAX_SEGNAME_LEN); if (newLen) { - if (name) name = static_cast(realloc(name, newLen+1)); - else name = static_cast(malloc(newLen+1)); + if (name) name = static_cast(d_realloc(name, newLen+1)); + else name = static_cast(d_malloc(newLen+1)); if (name) strlcpy(name, newName, newLen+1); name[newLen] = 0; return *this; @@ -690,7 +593,7 @@ unsigned Segment::virtualHeight() const { constexpr int Fixed_Scale = 16384; // fixpoint scaling factor (14bit for fraction) // Pinwheel helper function: matrix dimensions to number of rays static int getPinwheelLength(int vW, int vH) { - // Returns multiple of 8, prevents over drawing + // Returns multiple of 8, prevents over drawing return (max(vW, vH) + 15) & ~7; } static void setPinwheelParameters(int i, int vW, int vH, int& startx, int& starty, int* cosVal, int* sinVal, bool getPixel = false) { @@ -705,7 +608,7 @@ static void setPinwheelParameters(int i, int vW, int vH, int& startx, int& start sinVal[k] = (sin16_t(angle) * Fixed_Scale) >> 15; // using explicit bit shifts as dividing negative numbers is not equivalent (rounding error is acceptable) } startx = (vW * Fixed_Scale) / 2; // + cosVal[0] / 4; // starting position = center + 1/4 pixel (in fixed point) - starty = (vH * Fixed_Scale) / 2; // + sinVal[0] / 4; + starty = (vH * Fixed_Scale) / 2; // + sinVal[0] / 4; } #endif @@ -742,13 +645,10 @@ uint16_t Segment::virtualLength() const { return vLength; } -// pixel is clipped if it falls outside clipping range (_modeBlend==true) or is inside clipping range (_modeBlend==false) +// pixel is clipped if it falls outside clipping range // if clipping start > stop the clipping range is inverted -// _modeBlend==true -> old effect during transition -// _modeBlend==false -> new effect during transition bool IRAM_ATTR_YN Segment::isPixelClipped(int i) const { -#ifndef WLED_DISABLE_MODE_BLEND - if (_clipStart != _clipStop && blendingStyle > BLEND_STYLE_FADE) { + if (blendingStyle != BLEND_STYLE_FADE && isInTransition() && _clipStart != _clipStop) { bool invert = _clipStart > _clipStop; // ineverted start & stop int start = invert ? _clipStop : _clipStart; int stop = invert ? _clipStart : _clipStop; @@ -757,15 +657,11 @@ bool IRAM_ATTR_YN Segment::isPixelClipped(int i) const { if (len < 2) return false; unsigned shuffled = hashInt(i) % len; unsigned pos = (shuffled * 0xFFFFU) / len; - return (progress() <= pos) ^ _modeBlend; + return progress() <= pos; } const bool iInside = (i >= start && i < stop); - //if (!invert && iInside) return _modeBlend; - //if ( invert && !iInside) return _modeBlend; - //return !_modeBlend; - return !iInside ^ invert ^ _modeBlend; // thanks @willmmiles (https://github.com/wled-dev/WLED/pull/3877#discussion_r1554633876) + return !iInside ^ invert; // thanks @willmmiles (https://github.com/wled/WLED/pull/3877#discussion_r1554633876) } -#endif return false; } @@ -775,7 +671,7 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) const #ifndef WLED_DISABLE_2D int vStrip = 0; #endif - int vL = vLength(); + const int vL = vLength(); // if the 1D effect is using virtual strips "i" will have virtual strip id stored in upper 16 bits // in such case "i" will be > virtualLength() if (i >= vL) { @@ -793,23 +689,21 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) const if (is2D()) { 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; + const auto XY = [&](unsigned x, unsigned y){ return x + y*vW;}; switch (map1D2D) { case M12_Pixels: // use all available pixels as a long strip - setPixelColorXY(i % vW, i / vW, col); + setPixelColorRaw(XY(i % vW, i / vW), col); break; case M12_pBar: // expand 1D effect vertically or have it play on virtual strips - if (vStrip > 0) setPixelColorXY(vStrip - 1, vH - i - 1, col); - else for (int x = 0; x < vW; x++) setPixelColorXY(x, vH - i - 1, col); + if (vStrip > 0) setPixelColorRaw(XY(vStrip - 1, vH - i - 1), col); + else for (int x = 0; x < vW; x++) setPixelColorRaw(XY(x, vH - i - 1), col); break; case M12_pArc: // expand in circular fashion from center if (i == 0) - setPixelColorXY(0, 0, col); + setPixelColorRaw(XY(0, 0), col); else { float r = i; float step = HALF_PI / (2.8284f * r + 4); // we only need (PI/4)/(r/sqrt(2)+1) steps @@ -837,106 +731,106 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) const } break; case M12_pCorner: - for (int x = 0; x <= i; x++) setPixelColorXY(x, i, col); - for (int y = 0; y < i; y++) setPixelColorXY(i, y, col); + for (int x = 0; x <= i; x++) setPixelColorRaw(XY(x, i), col); + for (int y = 0; y < i; y++) setPixelColorRaw(XY(i, y), col); break; - case M12_sPinwheel: { - // Uses Bresenham's algorithm to place coordinates of two lines in arrays then draws between them - int startX, startY, cosVal[2], sinVal[2]; // in fixed point scale - setPinwheelParameters(i, vW, vH, startX, startY, cosVal, sinVal); - - unsigned maxLineLength = max(vW, vH) + 2; // pixels drawn is always smaller than dx or dy, +1 pair for rounding errors - uint16_t lineCoords[2][maxLineLength]; // uint16_t to save ram - int lineLength[2] = {0}; - - static int prevRays[2] = {INT_MAX, INT_MAX}; // previous two ray numbers - int closestEdgeIdx = INT_MAX; // index of the closest edge pixel - - for (int lineNr = 0; lineNr < 2; lineNr++) { - int x0 = startX; // x, y coordinates in fixed scale - int y0 = startY; - int x1 = (startX + (cosVal[lineNr] << 9)); // outside of grid - int y1 = (startY + (sinVal[lineNr] << 9)); // outside of grid - const int dx = abs(x1-x0), sx = x0= vW || unsigned(y0) >= vH) { - closestEdgeIdx = min(closestEdgeIdx, idx-2); - break; // stop if outside of grid (exploit unsigned int overflow) - } - coordinates[idx++] = x0; - coordinates[idx++] = y0; - (*length)++; - // note: since endpoint is out of grid, no need to check if endpoint is reached - int e2 = 2 * err; - if (e2 >= dy) { err += dy; x0 += sx; } - if (e2 <= dx) { err += dx; y0 += sy; } + case M12_sPinwheel: { + // Uses Bresenham's algorithm to place coordinates of two lines in arrays then draws between them + int startX, startY, cosVal[2], sinVal[2]; // in fixed point scale + setPinwheelParameters(i, vW, vH, startX, startY, cosVal, sinVal); + + unsigned maxLineLength = max(vW, vH) + 2; // pixels drawn is always smaller than dx or dy, +1 pair for rounding errors + uint16_t lineCoords[2][maxLineLength]; // uint16_t to save ram + int lineLength[2] = {0}; + + static int prevRays[2] = {INT_MAX, INT_MAX}; // previous two ray numbers + int closestEdgeIdx = INT_MAX; // index of the closest edge pixel + + for (int lineNr = 0; lineNr < 2; lineNr++) { + int x0 = startX; // x, y coordinates in fixed scale + int y0 = startY; + int x1 = (startX + (cosVal[lineNr] << 9)); // outside of grid + int y1 = (startY + (sinVal[lineNr] << 9)); // outside of grid + const int dx = abs(x1-x0), sx = x0= (unsigned)vW || (unsigned)y0 >= (unsigned)vH) { + closestEdgeIdx = min(closestEdgeIdx, idx-2); + break; // stop if outside of grid (exploit unsigned int overflow) } + coordinates[idx++] = x0; + coordinates[idx++] = y0; + (*length)++; + // note: since endpoint is out of grid, no need to check if endpoint is reached + int e2 = 2 * err; + if (e2 >= dy) { err += dy; x0 += sx; } + if (e2 <= dx) { err += dx; y0 += sy; } } - - // fill up the shorter line with missing coordinates, so block filling works correctly and efficiently - int diff = lineLength[0] - lineLength[1]; - int longLineIdx = (diff > 0) ? 0 : 1; - int shortLineIdx = longLineIdx ? 0 : 1; - if (diff != 0) { - int idx = (lineLength[shortLineIdx] - 1) * 2; // last valid coordinate index - int lastX = lineCoords[shortLineIdx][idx++]; - int lastY = lineCoords[shortLineIdx][idx++]; - bool keepX = lastX == 0 || lastX == vW - 1; - for (int d = 0; d < abs(diff); d++) { - lineCoords[shortLineIdx][idx] = keepX ? lastX :lineCoords[longLineIdx][idx]; - idx++; - lineCoords[shortLineIdx][idx] = keepX ? lineCoords[longLineIdx][idx] : lastY; - idx++; - } - } - - // draw and block-fill the line coordinates. Note: block filling only efficient if angle between lines is small - closestEdgeIdx += 2; - int max_i = getPinwheelLength(vW, vH) - 1; - bool drawFirst = !(prevRays[0] == i - 1 || (i == 0 && prevRays[0] == max_i)); // draw first line if previous ray was not adjacent including wrap - bool drawLast = !(prevRays[0] == i + 1 || (i == max_i && prevRays[0] == 0)); // same as above for last line - for (int idx = 0; idx < lineLength[longLineIdx] * 2;) { //!! should be long line idx! - int x1 = lineCoords[0][idx]; - int x2 = lineCoords[1][idx++]; - int y1 = lineCoords[0][idx]; - int y2 = lineCoords[1][idx++]; - int minX, maxX, minY, maxY; - (x1 < x2) ? (minX = x1, maxX = x2) : (minX = x2, maxX = x1); - (y1 < y2) ? (minY = y1, maxY = y2) : (minY = y2, maxY = y1); - - // fill the block between the two x,y points - bool alwaysDraw = (drawFirst && drawLast) || // No adjacent rays, draw all pixels - (idx > closestEdgeIdx) || // Edge pixels on uneven lines are always drawn - (i == 0 && idx == 2) || // Center pixel special case - (i == prevRays[1]); // Effect drawing twice in 1 frame - for (int x = minX; x <= maxX; x++) { - for (int y = minY; y <= maxY; y++) { - bool onLine1 = x == x1 && y == y1; - bool onLine2 = x == x2 && y == y2; - if ((alwaysDraw) || - (!onLine1 && (!onLine2 || drawLast)) || // Middle pixels and line2 if drawLast - (!onLine2 && (!onLine1 || drawFirst)) // Middle pixels and line1 if drawFirst - ) { - setPixelColorXY(x, y, col); - } - } - } - } - prevRays[1] = prevRays[0]; - prevRays[0] = i; - break; } + + // fill up the shorter line with missing coordinates, so block filling works correctly and efficiently + int diff = lineLength[0] - lineLength[1]; + int longLineIdx = (diff > 0) ? 0 : 1; + int shortLineIdx = longLineIdx ? 0 : 1; + if (diff != 0) { + int idx = (lineLength[shortLineIdx] - 1) * 2; // last valid coordinate index + int lastX = lineCoords[shortLineIdx][idx++]; + int lastY = lineCoords[shortLineIdx][idx++]; + bool keepX = lastX == 0 || lastX == vW - 1; + for (int d = 0; d < abs(diff); d++) { + lineCoords[shortLineIdx][idx] = keepX ? lastX :lineCoords[longLineIdx][idx]; + idx++; + lineCoords[shortLineIdx][idx] = keepX ? lineCoords[longLineIdx][idx] : lastY; + idx++; + } + } + + // draw and block-fill the line coordinates. Note: block filling only efficient if angle between lines is small + closestEdgeIdx += 2; + int max_i = getPinwheelLength(vW, vH) - 1; + bool drawFirst = !(prevRays[0] == i - 1 || (i == 0 && prevRays[0] == max_i)); // draw first line if previous ray was not adjacent including wrap + bool drawLast = !(prevRays[0] == i + 1 || (i == max_i && prevRays[0] == 0)); // same as above for last line + for (int idx = 0; idx < lineLength[longLineIdx] * 2;) { //!! should be long line idx! + int x1 = lineCoords[0][idx]; + int x2 = lineCoords[1][idx++]; + int y1 = lineCoords[0][idx]; + int y2 = lineCoords[1][idx++]; + int minX, maxX, minY, maxY; + (x1 < x2) ? (minX = x1, maxX = x2) : (minX = x2, maxX = x1); + (y1 < y2) ? (minY = y1, maxY = y2) : (minY = y2, maxY = y1); + + // fill the block between the two x,y points + bool alwaysDraw = (drawFirst && drawLast) || // No adjacent rays, draw all pixels + (idx > closestEdgeIdx) || // Edge pixels on uneven lines are always drawn + (i == 0 && idx == 2) || // Center pixel special case + (i == prevRays[1]); // Effect drawing twice in 1 frame + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + bool onLine1 = x == x1 && y == y1; + bool onLine2 = x == x2 && y == y2; + if ((alwaysDraw) || + (!onLine1 && (!onLine2 || drawLast)) || // Middle pixels and line2 if drawLast + (!onLine2 && (!onLine1 || drawFirst)) // Middle pixels and line1 if drawFirst + ) { + setPixelColorXY(x, y, col); + } + } + } + } + prevRays[1] = prevRays[0]; + prevRays[0] = i; + break; } - return; + } + return; } 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) @@ -948,58 +842,7 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) const } } #endif - -#ifndef WLED_DISABLE_MODE_BLEND - // if we blend using "push" style we need to "shift" new mode to left or right - if (isInTransition() && !_modeBlend && (blendingStyle == BLEND_STYLE_PUSH_RIGHT || blendingStyle == BLEND_STYLE_PUSH_LEFT)) { - unsigned prog = 0xFFFF - progress(); - unsigned dI = prog * vL / 0xFFFF; - if (blendingStyle == BLEND_STYLE_PUSH_RIGHT) i -= dI; - else i += dI; - } -#endif - - if (i >= vL || i < 0 || isPixelClipped(i)) return; // handle clipping on 1D - - unsigned len = length(); - // if color is unscaled - if (!_colorScaled) col = color_fade(col, _segBri); - - // expand pixel (taking into account start, grouping, spacing [and offset]) - i = i * groupLength(); - if (reverse) { // is segment reversed? - if (mirror) { // is segment mirrored? - i = (len - 1) / 2 - i; //only need to index half the pixels - } else { - i = (len - 1) - i; - } - } - i += start; // starting pixel in a group - - uint32_t tmpCol = col; - // set all the pixels in the group - for (int j = 0; j < grouping; j++) { - unsigned indexSet = i + ((reverse) ? -j : j); - if (indexSet >= start && indexSet < stop) { - if (mirror) { //set the corresponding mirrored pixel - unsigned indexMir = stop - indexSet + start - 1; - indexMir += offset; // offset/phase - if (indexMir >= stop) indexMir -= len; // wrap -#ifndef WLED_DISABLE_MODE_BLEND - // _modeBlend==true -> old effect - if (_modeBlend && blendingStyle == BLEND_STYLE_FADE) tmpCol = color_blend16(strip.getPixelColor(indexMir), col, 0xFFFFU - progress()); -#endif - strip.setPixelColor(indexMir, tmpCol); - } - indexSet += offset; // offset/phase - if (indexSet >= stop) indexSet -= len; // wrap -#ifndef WLED_DISABLE_MODE_BLEND - // _modeBlend==true -> old effect - if (_modeBlend && blendingStyle == BLEND_STYLE_FADE) tmpCol = color_blend16(strip.getPixelColor(indexSet), col, 0xFFFFU - progress()); -#endif - strip.setPixelColor(indexSet, tmpCol); - } - } + setPixelColorRaw(i, col); } #ifdef WLED_USE_AA_PIXELS @@ -1039,36 +882,42 @@ void Segment::setPixelColor(float i, uint32_t col, bool aa) const uint32_t IRAM_ATTR_YN Segment::getPixelColor(int i) const { - if (!isActive()) return 0; // not active + if (!isActive() || i < 0) return 0; // not active or invalid index - int vL = vLength(); - if (i >= vL || i < 0) return 0; +#ifndef WLED_DISABLE_2D + int vStrip = i>>16; // virtual strips are only relevant in Bar expansion mode + i &= 0xFFFF; +#endif + if (i >= (int)vLength()) return 0; #ifndef WLED_DISABLE_2D if (is2D()) { const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive) const int vH = vHeight(); // segment height in logical pixels (is always >= 1) + int x = 0, y = 0; switch (map1D2D) { case M12_Pixels: - return getPixelColorXY(i % vW, i / vW); + x = i % vW; + y = i / vW; + break; + case M12_pBar: + if (vStrip > 0) { x = vStrip - 1; y = vH - i - 1; } + else { y = 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 = sqrt32_bw(i*i/2); - return getPixelColorXY(vI,vI); // use diagonal + if (i > vW && i > vH) { + x = y = sqrt32_bw(i*i/2); + break; // use diagonal } + // otherwise fallthrough case M12_pCorner: // use longest dimension - return vW>vH ? getPixelColorXY(i, 0) : getPixelColorXY(0, i); + if (vW > vH) x = i; + else y = i; break; - case M12_sPinwheel: + case M12_sPinwheel: { // not 100% accurate, returns pixel at outer edge - int x, y, cosVal[2], sinVal[2]; + int cosVal[2], sinVal[2]; setPinwheelParameters(i, vW, vH, x, y, cosVal, sinVal, true); int maxX = (vW-1) * Fixed_Scale; int maxY = (vH-1) * Fixed_Scale; @@ -1079,140 +928,56 @@ uint32_t IRAM_ATTR_YN Segment::getPixelColor(int i) const } x /= Fixed_Scale; y /= Fixed_Scale; - return getPixelColorXY(x, y); break; } - return 0; + } + return getPixelColorXY(x, y); } #endif - -#ifndef WLED_DISABLE_MODE_BLEND - if (isInTransition() && !_modeBlend && (blendingStyle == BLEND_STYLE_PUSH_RIGHT || blendingStyle == BLEND_STYLE_PUSH_LEFT)) { - unsigned prog = 0xFFFF - progress(); - unsigned dI = prog * vL / 0xFFFF; - if (blendingStyle == BLEND_STYLE_PUSH_RIGHT) i -= dI; - else i += dI; - } -#endif - - if (i >= vL || i < 0 || isPixelClipped(i)) return 0; // handle clipping on 1D - - if (reverse) i = vL - i - 1; - i *= groupLength(); - i += start; - // offset/phase - i += offset; - if (i >= stop) i -= length(); - return strip.getPixelColor(i); + return getPixelColorRaw(i); } -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; - if (offset != b.offset) d |= SEG_DIFFERS_GSO; - if (grouping != b.grouping) d |= SEG_DIFFERS_GSO; - if (spacing != b.spacing) d |= SEG_DIFFERS_GSO; - if (opacity != b.opacity) d |= SEG_DIFFERS_BRI; - if (mode != b.mode) d |= SEG_DIFFERS_FX; - if (speed != b.speed) d |= SEG_DIFFERS_FX; - if (intensity != b.intensity) d |= SEG_DIFFERS_FX; - if (palette != b.palette) d |= SEG_DIFFERS_FX; - if (custom1 != b.custom1) d |= SEG_DIFFERS_FX; - if (custom2 != b.custom2) d |= SEG_DIFFERS_FX; - if (custom3 != b.custom3) d |= SEG_DIFFERS_FX; - if (startY != b.startY) d |= SEG_DIFFERS_BOUNDS; - if (stopY != b.stopY) d |= SEG_DIFFERS_BOUNDS; - - //bit pattern: (msb first) - // set:2, sound:2, mapping:3, transposed, mirrorY, reverseY, [reset,] paused, mirrored, on, reverse, [selected] - if ((options & 0b1111111111011110U) != (b.options & 0b1111111111011110U)) d |= SEG_DIFFERS_OPT; - if ((options & 0x0001U) != (b.options & 0x0001U)) d |= SEG_DIFFERS_SEL; - for (unsigned i = 0; i < NUM_COLORS; i++) if (colors[i] != b.colors[i]) d |= SEG_DIFFERS_COL; - - return d; -} - -void Segment::refreshLightCapabilities() { +void Segment::refreshLightCapabilities() const { unsigned capabilities = 0; - unsigned segStartIdx = 0xFFFFU; - unsigned segStopIdx = 0; if (!isActive()) { _capabilities = 0; return; } - if (start < Segment::maxWidth * Segment::maxHeight) { - // we are withing 2D matrix (includes 1D segments) - for (int y = startY; y < stopY; y++) for (int x = start; x < stop; x++) { - unsigned index = strip.getMappedPixelIndex(x + Segment::maxWidth * y); // convert logical address to physical - if (index < 0xFFFFU) { - if (segStartIdx > index) segStartIdx = index; - if (segStopIdx < index) segStopIdx = index; + // we must traverse each pixel in segment to determine its capabilities (as pixel may be mapped) + for (unsigned y = startY; y < stopY; y++) for (unsigned x = start; x < stop; x++) { + unsigned index = x + Segment::maxWidth * y; + index = strip.getMappedPixelIndex(index); // convert logical address to physical + if (index == 0xFFFF) continue; // invalid/missing pixel + for (unsigned b = 0; b < BusManager::getNumBusses(); b++) { + const Bus *bus = BusManager::getBus(b); + if (!bus || !bus->isOk()) break; + if (bus->containsPixel(index)) { + if (bus->hasRGB() || (strip.cctFromRgb && bus->hasCCT())) capabilities |= SEG_CAPABILITY_RGB; + if (!strip.cctFromRgb && bus->hasCCT()) capabilities |= SEG_CAPABILITY_CCT; + if (strip.correctWB && (bus->hasRGB() || bus->hasCCT())) capabilities |= SEG_CAPABILITY_CCT; //white balance correction (CCT slider) + if (bus->hasWhite()) { + unsigned aWM = Bus::getGlobalAWMode() == AW_GLOBAL_DISABLED ? bus->getAutoWhiteMode() : Bus::getGlobalAWMode(); + bool whiteSlider = (aWM == RGBW_MODE_DUAL || aWM == RGBW_MODE_MANUAL_ONLY); // white slider allowed + // if auto white calculation from RGB is active (Accurate/Brighter), force RGB controls even if there are no RGB busses + if (!whiteSlider) capabilities |= SEG_CAPABILITY_RGB; + // if auto white calculation from RGB is disabled/optional (None/Dual), allow white channel adjustments + if ( whiteSlider) capabilities |= SEG_CAPABILITY_W; + } + break; } - if (segStartIdx == segStopIdx) segStopIdx++; // we only have 1 pixel segment - } - } else { - // we are on the strip located after the matrix - segStartIdx = start; - segStopIdx = stop; - } - - for (unsigned b = 0; b < BusManager::getNumBusses(); b++) { - const Bus *bus = BusManager::getBus(b); - if (!bus || !bus->isOk()) break; - if (bus->getStart() >= segStopIdx || bus->getStart() + bus->getLength() <= segStartIdx) continue; - if (bus->hasRGB() || (strip.cctFromRgb && bus->hasCCT())) capabilities |= SEG_CAPABILITY_RGB; - if (!strip.cctFromRgb && bus->hasCCT()) capabilities |= SEG_CAPABILITY_CCT; - if (strip.correctWB && (bus->hasRGB() || bus->hasCCT())) capabilities |= SEG_CAPABILITY_CCT; //white balance correction (CCT slider) - if (bus->hasWhite()) { - unsigned aWM = Bus::getGlobalAWMode() == AW_GLOBAL_DISABLED ? bus->getAutoWhiteMode() : Bus::getGlobalAWMode(); - bool whiteSlider = (aWM == RGBW_MODE_DUAL || aWM == RGBW_MODE_MANUAL_ONLY); // white slider allowed - // if auto white calculation from RGB is active (Accurate/Brighter), force RGB controls even if there are no RGB busses - if (!whiteSlider) capabilities |= SEG_CAPABILITY_RGB; - // if auto white calculation from RGB is disabled/optional (None/Dual), allow white channel adjustments - if ( whiteSlider) capabilities |= SEG_CAPABILITY_W; } } _capabilities = capabilities; } -/* - * Fills segment with black - */ -void Segment::clear() { - if (!isActive()) return; // not active - unsigned oldVW = _vWidth; - unsigned oldVH = _vHeight; - unsigned oldVL = _vLength; - unsigned oldSB = _segBri; - _vWidth = virtualWidth(); - _vHeight = virtualHeight(); - _vLength = virtualLength(); - _segBri = currentBri(); - fill(BLACK); - _vWidth = oldVW; - _vHeight = oldVH; - _vLength = oldVL; - _segBri = oldSB; -} - /* * Fills segment with color */ -void Segment::fill(uint32_t c) { +void Segment::fill(uint32_t c) const { if (!isActive()) return; // not active - const int cols = is2D() ? 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; + for (unsigned i = 0; i < length(); i++) setPixelColorRaw(i,c); // always fill all pixels (blending will take care of grouping, spacing and clipping) } /* @@ -1220,16 +985,12 @@ void Segment::fill(uint32_t c) { * fading is highly dependant on frame rate (higher frame rates, faster fading) * each frame will fade at max 9% or as little as 0.8% */ -void Segment::fade_out(uint8_t rate) { +void Segment::fade_out(uint8_t rate) const { if (!isActive()) return; // not active - const int cols = is2D() ? vWidth() : vLength(); - const int rows = vHeight(); // will be 1 for 1D - rate = (256-rate) >> 1; const int mappedRate = 256 / (rate + 1); - - for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { - uint32_t color = is2D() ? getPixelColorXY(x, y) : getPixelColor(x); + for (unsigned j = 0; j < vLength(); j++) { + uint32_t color = getPixelColorRaw(j); if (color == colors[1]) continue; // already at target color for (int i = 0; i < 32; i += 8) { uint8_t c2 = (colors[1]>>i); // get background channel @@ -1242,40 +1003,27 @@ void Segment::fade_out(uint8_t rate) { color &= ~(0xFF< 215 this function does not work properly (creates alternating pattern) */ -void Segment::blur(uint8_t blur_amount, bool smear) { +void Segment::blur(uint8_t blur_amount, bool smear) const { if (!isActive() || blur_amount == 0) return; // optimization: 0 means "don't blur" #ifndef WLED_DISABLE_2D if (is2D()) { @@ -1286,27 +1034,27 @@ void Segment::blur(uint8_t blur_amount, bool smear) { } #endif uint8_t keep = smear ? 255 : 255 - blur_amount; - uint8_t seep = blur_amount >> 1; + uint8_t seep = blur_amount >> (1 + smear); unsigned vlength = vLength(); uint32_t carryover = BLACK; - uint32_t lastnew; // not necessary to initialize lastnew and last, as both will be initialized by the first loop iteration + uint32_t lastnew; uint32_t last; uint32_t curnew = BLACK; for (unsigned i = 0; i < vlength; i++) { - uint32_t cur = getPixelColor(i); + uint32_t cur = getPixelColorRaw(i); uint32_t part = color_fade(cur, seep); curnew = color_fade(cur, keep); if (i > 0) { if (carryover) curnew = color_add(curnew, carryover); 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 + if (last != prev) setPixelColorRaw(i - 1, prev); + } else setPixelColorRaw(i, curnew); // first pixel lastnew = curnew; last = cur; // save original value for comparison on next iteration carryover = part; } - setPixelColor(vlength - 1, curnew); + setPixelColorRaw(vlength - 1, curnew); } /* @@ -1315,17 +1063,23 @@ void Segment::blur(uint8_t blur_amount, bool smear) { * Inspired by the Adafruit examples. */ uint32_t Segment::color_wheel(uint8_t pos) const { - if (palette) return color_from_palette(pos, false, true, 0); // perhaps "strip.paletteBlend < 2" should be better instead of "true" + if (palette) return color_from_palette(pos, false, false, 0); // never wrap palette uint8_t w = W(getCurrentColor(0)); pos = 255 - pos; - if (pos < 85) { - return RGBW32((255 - pos * 3), 0, (pos * 3), w); - } else if (pos < 170) { - pos -= 85; - return RGBW32(0, (pos * 3), (255 - pos * 3), w); + if (useRainbowWheel) { + CRGB rgb; + hsv2rgb_rainbow(CHSV(pos, 255, 255), rgb); + return RGBW32(rgb.r, rgb.g, rgb.b, w); } else { - pos -= 170; - return RGBW32((pos * 3), (255 - pos * 3), 0, w); + if (pos < 85) { + return RGBW32((255 - pos * 3), 0, (pos * 3), w); + } else if (pos < 170) { + pos -= 85; + return RGBW32(0, (pos * 3), (255 - pos * 3), w); + } else { + pos -= 170; + return RGBW32((pos * 3), (255 - pos * 3), 0, w); + } } } @@ -1339,19 +1093,18 @@ uint32_t Segment::color_wheel(uint8_t pos) const { * @returns Single color from palette */ uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool moving, uint8_t mcol, uint8_t pbri) const { - uint32_t color = getCurrentColor(mcol < NUM_COLORS ? mcol : 0); + uint32_t color = getCurrentColor(mcol); // default palette or no RGB support on segment if ((palette == 0 && mcol < NUM_COLORS) || !_isRGB) { return color_fade(color, pbri, true); } - const int vL = vLength(); unsigned paletteIndex = i; - if (mapping && vL > 1) paletteIndex = (i*255)/(vL -1); + if (mapping) paletteIndex = min((i*255)/vLength(), 255U); // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined/no interpolation of palette entries) // ColorFromPalette interpolations are: NOBLEND, LINEARBLEND, LINEARBLEND_NOWRAP TBlendType blend = NOBLEND; - switch (strip.paletteBlend) { // NOTE: paletteBlend should be global + switch (paletteBlend) { case 0: blend = moving ? LINEARBLEND : LINEARBLEND_NOWRAP; break; case 1: blend = LINEARBLEND; break; case 2: blend = LINEARBLEND_NOWRAP; break; @@ -1379,6 +1132,7 @@ void WS2812FX::finalizeInit() { enumerateLedmaps(); _hasWhiteChannel = _isOffRefreshRequired = false; + BusManager::removeAll(); unsigned digitalCount = 0; #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) @@ -1408,94 +1162,8 @@ void WS2812FX::finalizeInit() { busConfigs.clear(); busConfigs.shrink_to_fit(); - //if busses failed to load, add default (fresh install, FS issue, ...) - if (BusManager::getNumBusses() == 0) { - DEBUG_PRINTLN(F("No busses, init default")); - constexpr unsigned defDataTypes[] = {LED_TYPES}; - constexpr unsigned defDataPins[] = {DATA_PINS}; - constexpr unsigned defCounts[] = {PIXEL_COUNTS}; - constexpr unsigned defNumTypes = ((sizeof defDataTypes) / (sizeof defDataTypes[0])); - constexpr unsigned defNumPins = ((sizeof defDataPins) / (sizeof defDataPins[0])); - constexpr unsigned defNumCounts = ((sizeof defCounts) / (sizeof defCounts[0])); - - static_assert(validatePinsAndTypes(defDataTypes, defNumTypes, defNumPins), - "The default pin list defined in DATA_PINS does not match the pin requirements for the default buses defined in LED_TYPES"); - - unsigned prevLen = 0; - unsigned pinsIndex = 0; - digitalCount = 0; - for (unsigned i = 0; i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { - uint8_t defPin[OUTPUT_MAX_PINS]; - // if we have less types than requested outputs and they do not align, use last known type to set current type - unsigned dataType = defDataTypes[(i < defNumTypes) ? i : defNumTypes -1]; - unsigned busPins = Bus::getNumberOfPins(dataType); - - // if we need more pins than available all outputs have been configured - if (pinsIndex + busPins > defNumPins) break; - - // Assign all pins first so we can check for conflicts on this bus - for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) defPin[j] = defDataPins[pinsIndex + j]; - - for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) { - bool validPin = true; - // When booting without config (1st boot) we need to make sure GPIOs defined for LED output don't clash with hardware - // i.e. DEBUG (GPIO1), DMX (2), SPI RAM/FLASH (16&17 on ESP32-WROVER/PICO), read/only pins, etc. - // Pin should not be already allocated, read/only or defined for current bus - while (PinManager::isPinAllocated(defPin[j]) || !PinManager::isPinOk(defPin[j],true)) { - if (validPin) { - DEBUG_PRINTLN(F("Some of the provided pins cannot be used to configure this LED output.")); - defPin[j] = 1; // start with GPIO1 and work upwards - validPin = false; - } else if (defPin[j] < WLED_NUM_PINS) { - defPin[j]++; - } else { - DEBUG_PRINTLN(F("No available pins left! Can't configure output.")); - return; - } - // is the newly assigned pin already defined or used previously? - // try next in line until there are no clashes or we run out of pins - bool clash; - do { - clash = false; - // check for conflicts on current bus - for (const auto &pin : defPin) { - if (&pin != &defPin[j] && pin == defPin[j]) { - clash = true; - break; - } - } - // We already have a clash on current bus, no point checking next buses - if (!clash) { - // check for conflicts in defined pins - for (const auto &pin : defDataPins) { - if (pin == defPin[j]) { - clash = true; - break; - } - } - } - if (clash) defPin[j]++; - if (defPin[j] >= WLED_NUM_PINS) break; - } while (clash); - } - } - pinsIndex += busPins; - - unsigned start = prevLen; - // if we have less counts than pins and they do not align, use last known count to set current count - unsigned count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; - // analog always has length 1 - if (Bus::isPWM(dataType) || Bus::isOnOff(dataType)) count = 1; - prevLen += count; - BusConfig defCfg = BusConfig(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0, useGlobalLedBuffer); - mem += defCfg.memUsage(Bus::isDigital(dataType) && !Bus::is2Pin(dataType) ? digitalCount++ : 0); - if (BusManager::add(defCfg) == -1) break; - } - } - DEBUG_PRINTF_P(PSTR("LED buffer size: %uB/%uB\n"), mem, BusManager::memUsage()); - _length = 0; - for (int i=0; iisOk() || bus->getStart() + bus->getLength() > MAX_LEDS) break; //RGBW mode is enabled if at least one of the strips is RGBW @@ -1504,7 +1172,6 @@ void WS2812FX::finalizeInit() { _isOffRefreshRequired |= bus->isOffRefreshRequired() && !bus->isPWM(); // use refresh bit for phase shift with analog unsigned busEnd = bus->getStart() + bus->getLength(); if (busEnd > _length) _length = busEnd; - // This must be done after all buses have been created, as some kinds (parallel I2S) interact bus->begin(); bus->setBrightness(bri); @@ -1519,28 +1186,28 @@ void WS2812FX::finalizeInit() { loadCustomPalettes(); // (re)load all custom palettes DEBUG_PRINTLN(F("Loading custom ledmaps")); deserializeMap(); // (re)load default ledmap (will also setUpMatrix() if ledmap does not exist) + + // allocate frame buffer after matrix has been set up (gaps!) + if (_pixels) _pixels = static_cast(d_realloc(_pixels, getLengthTotal() * sizeof(uint32_t))); + else _pixels = static_cast(d_malloc(getLengthTotal() * sizeof(uint32_t))); + DEBUG_PRINTF_P(PSTR("strip buffer size: %uB\n"), getLengthTotal() * sizeof(uint32_t)); + + DEBUG_PRINTF_P(PSTR("Heap after strip init: %uB\n"), ESP.getFreeHeap()); } void WS2812FX::service() { unsigned long nowUp = millis(); // Be aware, millis() rolls over every 49 days now = nowUp + timebase; - if (_suspend) return; - unsigned long elapsed = nowUp - _lastServiceShow; - - if (elapsed <= MIN_FRAME_DELAY) return; // keep wifi alive - no matter if triggered or unlimited - if ( !_triggered && (_targetFps != FPS_UNLIMITED)) { // unlimited mode = no frametime - if (elapsed < _frametime) return; // too early for service - } - + if (nowUp - _lastShow < MIN_FRAME_DELAY || _suspend) return; bool doShow = false; _isServicing = true; _segment_index = 0; - for (segment &seg : _segments) { + for (Segment &seg : _segments) { if (_suspend) break; // immediately stop processing segments if suspend requested during service() - // process transition (mode changes in the middle of transition) + // process transition (also pre-calculates progress value) seg.handleTransition(); // reset the segment runtime data if needed seg.resetIfRequired(); @@ -1548,7 +1215,7 @@ void WS2812FX::service() { if (!seg.isActive()) continue; // last condition ensures all solid segments are updated at the same time - if (nowUp >= seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC)) + if (nowUp > seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC)) { doShow = true; unsigned frameDelay = FRAMETIME; @@ -1558,170 +1225,440 @@ void WS2812FX::service() { // when correctWB is true we need to correct/adjust RGB value according to desired CCT value, but it will also affect actual WW/CW ratio // when cctFromRgb is true we implicitly calculate WW and CW from RGB values if (cctFromRgb) BusManager::setSegmentCCT(-1); - else BusManager::setSegmentCCT(seg.currentBri(true), correctWB); + else BusManager::setSegmentCCT(seg.currentCCT(), correctWB); // Effect blending - // When two effects are being blended, each may have different segment data, this - // data needs to be saved first and then restored before running previous mode. - // The blending will largely depend on the effect behaviour since actual output (LEDs) may be - // overwritten by later effect. To enable seamless blending for every effect, additional LED buffer - // would need to be allocated for each effect and then blended together for each pixel. - seg.beginDraw(); // set up parameters for get/setPixelColor() -#ifndef WLED_DISABLE_MODE_BLEND - Segment::setClippingRect(0, 0); // disable clipping (just in case) - if (seg.isInTransition()) { - // a hack to determine if effect has changed - uint8_t m = seg.currentMode(); - Segment::modeBlend(true); // set semaphore - bool sameEffect = (m == seg.currentMode()); - Segment::modeBlend(false); // clear semaphore - // set clipping rectangle - // new mode is run inside clipping area and old mode outside clipping area - unsigned p = seg.progress(); - unsigned w = seg.is2D() ? Segment::vWidth() : Segment::vLength(); - unsigned h = Segment::vHeight(); - unsigned dw = p * w / 0xFFFFU + 1; - unsigned dh = p * h / 0xFFFFU + 1; - unsigned orgBS = blendingStyle; - if (w*h == 1) blendingStyle = BLEND_STYLE_FADE; // disable style for single pixel segments (use fade instead) - else if (sameEffect && (blendingStyle & BLEND_STYLE_PUSH_MASK)) { - // when effect stays the same push will look awful, change it to swipe - switch (blendingStyle) { - case BLEND_STYLE_PUSH_BR: - case BLEND_STYLE_PUSH_TR: - case BLEND_STYLE_PUSH_RIGHT: blendingStyle = BLEND_STYLE_SWIPE_RIGHT; break; - case BLEND_STYLE_PUSH_BL: - case BLEND_STYLE_PUSH_TL: - case BLEND_STYLE_PUSH_LEFT: blendingStyle = BLEND_STYLE_SWIPE_LEFT; break; - case BLEND_STYLE_PUSH_DOWN: blendingStyle = BLEND_STYLE_SWIPE_DOWN; break; - case BLEND_STYLE_PUSH_UP: blendingStyle = BLEND_STYLE_SWIPE_UP; break; - } - } - switch (blendingStyle) { - case BLEND_STYLE_FAIRY_DUST: // fairy dust (must set entire segment, see isPixelXYClipped()) - Segment::setClippingRect(0, w, 0, h); - break; - case BLEND_STYLE_SWIPE_RIGHT: // left-to-right - case BLEND_STYLE_PUSH_RIGHT: // left-to-right - Segment::setClippingRect(0, dw, 0, h); - break; - case BLEND_STYLE_SWIPE_LEFT: // right-to-left - case BLEND_STYLE_PUSH_LEFT: // right-to-left - Segment::setClippingRect(w - dw, w, 0, h); - break; - case BLEND_STYLE_PINCH_OUT: // corners - Segment::setClippingRect((w + dw)/2, (w - dw)/2, (h + dh)/2, (h - dh)/2); // inverted!! - break; - case BLEND_STYLE_INSIDE_OUT: // outward - Segment::setClippingRect((w - dw)/2, (w + dw)/2, (h - dh)/2, (h + dh)/2); - break; - case BLEND_STYLE_SWIPE_DOWN: // top-to-bottom (2D) - case BLEND_STYLE_PUSH_DOWN: // top-to-bottom (2D) - Segment::setClippingRect(0, w, 0, dh); - break; - case BLEND_STYLE_SWIPE_UP: // bottom-to-top (2D) - case BLEND_STYLE_PUSH_UP: // bottom-to-top (2D) - Segment::setClippingRect(0, w, h - dh, h); - break; - case BLEND_STYLE_OPEN_H: // horizontal-outward (2D) same look as INSIDE_OUT on 1D - Segment::setClippingRect((w - dw)/2, (w + dw)/2, 0, h); - break; - case BLEND_STYLE_OPEN_V: // vertical-outward (2D) - Segment::setClippingRect(0, w, (h - dh)/2, (h + dh)/2); - break; - case BLEND_STYLE_PUSH_TL: // TL-to-BR (2D) - Segment::setClippingRect(0, dw, 0, dh); - break; - case BLEND_STYLE_PUSH_TR: // TR-to-BL (2D) - Segment::setClippingRect(w - dw, w, 0, dh); - break; - case BLEND_STYLE_PUSH_BR: // BR-to-TL (2D) - Segment::setClippingRect(w - dw, w, h - dh, h); - break; - case BLEND_STYLE_PUSH_BL: // BL-to-TR (2D) - Segment::setClippingRect(0, dw, h - dh, h); - break; - } - frameDelay = (*_mode[m])(); // run new/current mode - // now run old/previous mode - Segment::tmpsegd_t _tmpSegData; - Segment::modeBlend(true); // set semaphore - seg.swapSegenv(_tmpSegData); // temporarily store new mode state (and swap it with transitional state) - seg.beginDraw(); // set up parameters for get/setPixelColor() - frameDelay = min(frameDelay, (unsigned)(*_mode[seg.currentMode()])()); // run old mode - seg.call++; // increment old mode run counter - seg.restoreSegenv(_tmpSegData); // restore mode state (will also update transitional state) - Segment::modeBlend(false); // unset semaphore - blendingStyle = orgBS; // restore blending style if it was modified for single pixel segment - } else -#endif - frameDelay = (*_mode[seg.mode])(); // run effect mode (not in transition) + uint16_t prog = seg.progress(); + seg.beginDraw(prog); // set up parameters for get/setPixelColor() (will also blend colors and palette if blend style is FADE) + _currentSegment = &seg; // set current segment for effect functions (SEGMENT & SEGENV) + // workaround for on/off transition to respect blending style + frameDelay = (*_mode[seg.mode])(); // run new/current mode (needed for bri workaround) seg.call++; + // if segment is in transition and no old segment exists we don't need to run the old mode + // (blendSegments() takes care of On/Off transitions and clipping) + Segment *segO = seg.getOldSegment(); + if (segO && (seg.mode != segO->mode || blendingStyle != BLEND_STYLE_FADE)) { + Segment::modeBlend(true); // set semaphore for beginDraw() to blend colors and palette + segO->beginDraw(prog); // set up palette & colors (also sets draw dimensions), parent segment has transition progress + _currentSegment = segO; // set current segment + // workaround for on/off transition to respect blending style + frameDelay = min(frameDelay, (unsigned)(*_mode[segO->mode])()); // run old mode (needed for bri workaround; semaphore!!) + segO->call++; // increment old mode run counter + Segment::modeBlend(false); // unset semaphore + } if (seg.isInTransition() && frameDelay > FRAMETIME) frameDelay = FRAMETIME; // force faster updates during transition - BusManager::setSegmentCCT(oldCCT); // restore old CCT for ABL adjustments + BusManager::setSegmentCCT(oldCCT); // restore old CCT for ABL adjustments } seg.next_time = nowUp + frameDelay; } _segment_index++; } - Segment::setClippingRect(0, 0); // disable clipping for overlays - _isServicing = false; - _triggered = false; #ifdef WLED_DEBUG - if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR("Slow effects %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); + if (millis() - nowUp > _frametime) DEBUG_PRINTF_P(PSTR("Slow effects %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); #endif - if (doShow) { + if (doShow && !_suspend) { yield(); Segment::handleRandomPalette(); // slowly transition random palette; move it into for loop when each segment has individual random palette - _lastServiceShow = nowUp; // update timestamp, for precise FPS control - if (!_suspend) show(); + show(); } #ifdef WLED_DEBUG - if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR("Slow strip %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); + if (millis() - nowUp > _frametime) DEBUG_PRINTF_P(PSTR("Slow strip %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); #endif + + _triggered = false; + _isServicing = false; } -void IRAM_ATTR WS2812FX::setPixelColor(unsigned i, uint32_t col) const { - i = getMappedPixelIndex(i); - if (i >= _length) return; - BusManager::setPixelColor(i, col); +// https://en.wikipedia.org/wiki/Blend_modes but using a for top layer & b for bottom layer +static uint8_t _top (uint8_t a, uint8_t b) { return a; } +static uint8_t _bottom (uint8_t a, uint8_t b) { return b; } +static uint8_t _add (uint8_t a, uint8_t b) { unsigned t = a + b; return t > 255 ? 255 : t; } +static uint8_t _subtract (uint8_t a, uint8_t b) { return b > a ? (b - a) : 0; } +static uint8_t _difference(uint8_t a, uint8_t b) { return b > a ? (b - a) : (a - b); } +static uint8_t _average (uint8_t a, uint8_t b) { return (a + b) >> 1; } +#ifdef CONFIG_IDF_TARGET_ESP32C3 +static uint8_t _multiply (uint8_t a, uint8_t b) { return ((a * b) + 255) >> 8; } // faster than division on C3 but slightly less accurate +#else +static uint8_t _multiply (uint8_t a, uint8_t b) { return (a * b) / 255; } // origianl uses a & b in range [0,1] +#endif +static uint8_t _divide (uint8_t a, uint8_t b) { return a > b ? (b * 255) / a : 255; } +static uint8_t _lighten (uint8_t a, uint8_t b) { return a > b ? a : b; } +static uint8_t _darken (uint8_t a, uint8_t b) { return a < b ? a : b; } +static uint8_t _screen (uint8_t a, uint8_t b) { return 255 - _multiply(~a,~b); } // 255 - (255-a)*(255-b)/255 +static uint8_t _overlay (uint8_t a, uint8_t b) { return b < 128 ? 2 * _multiply(a,b) : (255 - 2 * _multiply(~a,~b)); } +static uint8_t _hardlight (uint8_t a, uint8_t b) { return a < 128 ? 2 * _multiply(a,b) : (255 - 2 * _multiply(~a,~b)); } +#ifdef CONFIG_IDF_TARGET_ESP32C3 +static uint8_t _softlight (uint8_t a, uint8_t b) { return (((b * b * (255 - 2 * a) + 255) >> 8) + 2 * a * b + 255) >> 8; } // Pegtop's formula (1 - 2a)b^2 + 2ab +#else +static uint8_t _softlight (uint8_t a, uint8_t b) { return (b * b * (255 - 2 * a) / 255 + 2 * a * b) / 255; } // Pegtop's formula (1 - 2a)b^2 + 2ab +#endif +static uint8_t _dodge (uint8_t a, uint8_t b) { return _divide(~a,b); } +static uint8_t _burn (uint8_t a, uint8_t b) { return ~_divide(a,~b); } + +void WS2812FX::blendSegment(const Segment &topSegment) const { + + typedef uint8_t(*FuncType)(uint8_t, uint8_t); + FuncType funcs[] = { + _top, _bottom, + _add, _subtract, _difference, _average, + _multiply, _divide, _lighten, _darken, _screen, _overlay, + _hardlight, _softlight, _dodge, _burn + }; + + const size_t blendMode = topSegment.blendMode < (sizeof(funcs) / sizeof(FuncType)) ? topSegment.blendMode : 0; + const auto func = funcs[blendMode]; // blendMode % (sizeof(funcs) / sizeof(FuncType)) + const auto blend = [&](uint32_t top, uint32_t bottom){ return RGBW32(func(R(top),R(bottom)), func(G(top),G(bottom)), func(B(top),B(bottom)), func(W(top),W(bottom))); }; + + const int length = topSegment.length(); // physical segment length (counts all pixels in 2D segment) + const int width = topSegment.width(); + const int height = topSegment.height(); + const auto XY = [](int x, int y){ return x + y*Segment::maxWidth; }; + const size_t matrixSize = Segment::maxWidth * Segment::maxHeight; + const size_t startIndx = XY(topSegment.start, topSegment.startY); + const size_t stopIndx = startIndx + length; + const unsigned progress = topSegment.progress(); + const unsigned progInv = 0xFFFFU - progress; + uint8_t opacity = topSegment.currentBri(); // returns transitioned opacity for style FADE + + Segment::setClippingRect(0, 0); // disable clipping by default + + const unsigned dw = (blendingStyle==BLEND_STYLE_OUTSIDE_IN ? progInv : progress) * width / 0xFFFFU + 1; + const unsigned dh = (blendingStyle==BLEND_STYLE_OUTSIDE_IN ? progInv : progress) * height / 0xFFFFU + 1; + const unsigned orgBS = blendingStyle; + if (width*height == 1) blendingStyle = BLEND_STYLE_FADE; // disable style for single pixel segments (use fade instead) + switch (blendingStyle) { + case BLEND_STYLE_CIRCULAR_IN: // (must set entire segment, see isPixelXYClipped()) + case BLEND_STYLE_CIRCULAR_OUT:// (must set entire segment, see isPixelXYClipped()) + case BLEND_STYLE_FAIRY_DUST: // fairy dust (must set entire segment, see isPixelXYClipped()) + Segment::setClippingRect(0, width, 0, height); + break; + case BLEND_STYLE_SWIPE_RIGHT: // left-to-right + case BLEND_STYLE_PUSH_RIGHT: // left-to-right + Segment::setClippingRect(0, dw, 0, height); + break; + case BLEND_STYLE_SWIPE_LEFT: // right-to-left + case BLEND_STYLE_PUSH_LEFT: // right-to-left + Segment::setClippingRect(width - dw, width, 0, height); + break; + case BLEND_STYLE_OUTSIDE_IN: // corners + Segment::setClippingRect((width + dw)/2, (width - dw)/2, (height + dh)/2, (height - dh)/2); // inverted!! + break; + case BLEND_STYLE_INSIDE_OUT: // outward + Segment::setClippingRect((width - dw)/2, (width + dw)/2, (height - dh)/2, (height + dh)/2); + break; + case BLEND_STYLE_SWIPE_DOWN: // top-to-bottom (2D) + case BLEND_STYLE_PUSH_DOWN: // top-to-bottom (2D) + Segment::setClippingRect(0, width, 0, dh); + break; + case BLEND_STYLE_SWIPE_UP: // bottom-to-top (2D) + case BLEND_STYLE_PUSH_UP: // bottom-to-top (2D) + Segment::setClippingRect(0, width, height - dh, height); + break; + case BLEND_STYLE_OPEN_H: // horizontal-outward (2D) same look as INSIDE_OUT on 1D + Segment::setClippingRect((width - dw)/2, (width + dw)/2, 0, height); + break; + case BLEND_STYLE_OPEN_V: // vertical-outward (2D) + Segment::setClippingRect(0, width, (height - dh)/2, (height + dh)/2); + break; + case BLEND_STYLE_SWIPE_TL: // TL-to-BR (2D) + case BLEND_STYLE_PUSH_TL: // TL-to-BR (2D) + Segment::setClippingRect(0, dw, 0, dh); + break; + case BLEND_STYLE_SWIPE_TR: // TR-to-BL (2D) + case BLEND_STYLE_PUSH_TR: // TR-to-BL (2D) + Segment::setClippingRect(width - dw, width, 0, dh); + break; + case BLEND_STYLE_SWIPE_BR: // BR-to-TL (2D) + case BLEND_STYLE_PUSH_BR: // BR-to-TL (2D) + Segment::setClippingRect(width - dw, width, height - dh, height); + break; + case BLEND_STYLE_SWIPE_BL: // BL-to-TR (2D) + case BLEND_STYLE_PUSH_BL: // BL-to-TR (2D) + Segment::setClippingRect(0, dw, height - dh, height); + break; + } + + if (isMatrix && stopIndx <= matrixSize) { +#ifndef WLED_DISABLE_2D + const int nCols = topSegment.virtualWidth(); + const int nRows = topSegment.virtualHeight(); + const Segment *segO = topSegment.getOldSegment(); + const int oCols = segO ? segO->virtualWidth() : nCols; + const int oRows = segO ? segO->virtualHeight() : nRows; + + const auto setMirroredPixel = [&](int x, int y, uint32_t c, uint8_t o) { + const int baseX = topSegment.start + x; + const int baseY = topSegment.startY + y; + size_t indx = XY(baseX, baseY); // absolute address on strip + _pixels[indx] = color_blend(_pixels[indx], blend(c, _pixels[indx]), o); + // Apply mirroring + if (topSegment.mirror || topSegment.mirror_y) { + const int mirrorX = topSegment.start + width - x - 1; + const int mirrorY = topSegment.startY + height - y - 1; + const size_t idxMX = XY(topSegment.transpose ? baseX : mirrorX, topSegment.transpose ? mirrorY : baseY); + const size_t idxMY = XY(topSegment.transpose ? mirrorX : baseX, topSegment.transpose ? baseY : mirrorY); + const size_t idxMM = XY(mirrorX, mirrorY); + if (topSegment.mirror) _pixels[idxMX] = color_blend(_pixels[idxMX], blend(c, _pixels[idxMX]), o); + if (topSegment.mirror_y) _pixels[idxMY] = color_blend(_pixels[idxMY], blend(c, _pixels[idxMY]), o); + if (topSegment.mirror && topSegment.mirror_y) _pixels[idxMM] = color_blend(_pixels[idxMM], blend(c, _pixels[idxMM]), o); + } + }; + + // if we blend using "push" style we need to "shift" canvas to left/right/up/down + unsigned offsetX = (blendingStyle == BLEND_STYLE_PUSH_UP || blendingStyle == BLEND_STYLE_PUSH_DOWN) ? 0 : progInv * nCols / 0xFFFFU; + unsigned offsetY = (blendingStyle == BLEND_STYLE_PUSH_LEFT || blendingStyle == BLEND_STYLE_PUSH_RIGHT) ? 0 : progInv * nRows / 0xFFFFU; + + // we only traverse new segment, not old one + for (int r = 0; r < nRows; r++) for (int c = 0; c < nCols; c++) { + const bool clipped = topSegment.isPixelXYClipped(c, r); + // if segment is in transition and pixel is clipped take old segment's pixel and opacity + const Segment *seg = clipped && segO ? segO : &topSegment; // pixel is never clipped for FADE + int vCols = seg == segO ? oCols : nCols; // old segment may have different dimensions + int vRows = seg == segO ? oRows : nRows; // old segment may have different dimensions + int x = c; + int y = r; + // if we blend using "push" style we need to "shift" canvas to left/right/up/down + switch (blendingStyle) { + case BLEND_STYLE_PUSH_RIGHT: x = (x + offsetX) % nCols; break; + case BLEND_STYLE_PUSH_LEFT: x = (x - offsetX + nCols) % nCols; break; + case BLEND_STYLE_PUSH_DOWN: y = (y + offsetY) % nRows; break; + case BLEND_STYLE_PUSH_UP: y = (y - offsetY + nRows) % nRows; break; + } + uint32_t c_a = BLACK; + if (x < vCols && y < vRows) c_a = seg->getPixelColorRaw(x + y*vCols); // will get clipped pixel from old segment or unclipped pixel from new segment + if (segO && blendingStyle == BLEND_STYLE_FADE && topSegment.mode != segO->mode && x < oCols && y < oRows) { + // we need to blend old segment using fade as pixels ae not clipped + c_a = color_blend16(c_a, segO->getPixelColorRaw(x + y*oCols), progInv); + } else if (blendingStyle != BLEND_STYLE_FADE) { + // workaround for On/Off transition + // (bri != briT) && !bri => from On to Off + // (bri != briT) && bri => from Off to On + if ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri)) c_a = BLACK; + } + // map it into frame buffer + x = c; // restore coordiates if we were PUSHing + y = r; + if (topSegment.reverse ) x = nCols - x - 1; + if (topSegment.reverse_y) y = nRows - y - 1; + if (topSegment.transpose) std::swap(x,y); // swap X & Y if segment transposed + // expand pixel + const unsigned groupLen = topSegment.groupLength(); + if (groupLen == 1) { + setMirroredPixel(x, y, c_a, opacity); + } else { + // handle grouping and spacing + x *= groupLen; // expand to physical pixels + y *= groupLen; // expand to physical pixels + const int maxX = std::min(x + topSegment.grouping, width); + const int maxY = std::min(y + topSegment.grouping, height); + while (y < maxY) { + while (x < maxX) setMirroredPixel(x++, y, c_a, opacity); + y++; + } + } + } +#endif + } else { + const int nLen = topSegment.virtualLength(); + const Segment *segO = topSegment.getOldSegment(); + const int oLen = segO ? segO->virtualLength() : nLen; + + const auto setMirroredPixel = [&](int i, uint32_t c, uint8_t o) { + int indx = topSegment.start + i; + // Apply mirroring + if (topSegment.mirror) { + unsigned indxM = topSegment.stop - i - 1; + indxM += topSegment.offset; // offset/phase + if (indxM >= topSegment.stop) indxM -= length; // wrap + _pixels[indxM] = color_blend(_pixels[indxM], blend(c, _pixels[indxM]), o); + } + indx += topSegment.offset; // offset/phase + if (indx >= topSegment.stop) indx -= length; // wrap + _pixels[indx] = color_blend(_pixels[indx], blend(c, _pixels[indx]), o); + }; + + // if we blend using "push" style we need to "shift" canvas to left/right/ + unsigned offsetI = progInv * nLen / 0xFFFFU; + + for (int k = 0; k < nLen; k++) { + const bool clipped = topSegment.isPixelClipped(k); + // if segment is in transition and pixel is clipped take old segment's pixel and opacity + const Segment *seg = clipped && segO ? segO : &topSegment; // pixel is never clipped for FADE + const int vLen = seg == segO ? oLen : nLen; + int i = k; + // if we blend using "push" style we need to "shift" canvas to left or right + switch (blendingStyle) { + case BLEND_STYLE_PUSH_RIGHT: i = (i + offsetI) % nLen; break; + case BLEND_STYLE_PUSH_LEFT: i = (i - offsetI + nLen) % nLen; break; + } + uint32_t c_a = BLACK; + if (i < vLen) c_a = seg->getPixelColorRaw(i); // will get clipped pixel from old segment or unclipped pixel from new segment + if (segO && blendingStyle == BLEND_STYLE_FADE && topSegment.mode != segO->mode && i < oLen) { + // we need to blend old segment using fade as pixels are not clipped + c_a = color_blend16(c_a, segO->getPixelColorRaw(i), progInv); + } else if (blendingStyle != BLEND_STYLE_FADE) { + // workaround for On/Off transition + // (bri != briT) && !bri => from On to Off + // (bri != briT) && bri => from Off to On + if ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri)) c_a = BLACK; + } + // map into frame buffer + i = k; // restore index if we were PUSHing + if (topSegment.reverse) i = nLen - i - 1; // is segment reversed? + // expand pixel + i *= topSegment.groupLength(); + // set all the pixels in the group + const int maxI = std::min(i + topSegment.grouping, length); // make sure to not go beyond physical length + while (i < maxI) setMirroredPixel(i++, c_a, opacity); + } + } + + blendingStyle = orgBS; + Segment::setClippingRect(0, 0); // disable clipping for overlays } -uint32_t IRAM_ATTR WS2812FX::getPixelColor(unsigned i) const { - i = getMappedPixelIndex(i); - if (i >= _length) return 0; - return BusManager::getPixelColor(i); +// To disable brightness limiter we either set output max current to 0 or single LED current to 0 +static uint8_t estimateCurrentAndLimitBri(uint8_t brightness, uint32_t *pixels) { + unsigned milliAmpsMax = BusManager::ablMilliampsMax(); + if (milliAmpsMax > 0) { + unsigned milliAmpsTotal = 0; + unsigned avgMilliAmpsPerLED = 0; + unsigned lengthDigital = 0; + bool useWackyWS2815PowerModel = false; + + for (size_t i = 0; i < BusManager::getNumBusses(); i++) { + const Bus *bus = BusManager::getBus(i); + if (!(bus && bus->isDigital() && bus->isOk())) continue; + unsigned maPL = bus->getLEDCurrent(); + if (maPL == 0 || bus->getMaxCurrent() > 0) continue; // skip buses with 0 mA per LED or max current per bus defined (PP-ABL) + if (maPL == 255) { + useWackyWS2815PowerModel = true; + maPL = 12; // WS2815 uses 12mA per channel + } + avgMilliAmpsPerLED += maPL * bus->getLength(); + lengthDigital += bus->getLength(); + // sum up the usage of each LED on digital bus + uint32_t busPowerSum = 0; + for (unsigned j = 0; j < bus->getLength(); j++) { + uint32_t c = pixels[j + bus->getStart()]; + byte r = R(c), g = G(c), b = B(c), w = W(c); + if (useWackyWS2815PowerModel) { //ignore white component on WS2815 power calculation + busPowerSum += (max(max(r,g),b)) * 3; + } else { + busPowerSum += (r + g + b + w); + } + } + // RGBW led total output with white LEDs enabled is still 50mA, so each channel uses less + if (bus->hasWhite()) { + busPowerSum *= 3; + busPowerSum >>= 2; //same as /= 4 + } + // powerSum has all the values of channels summed (max would be getLength()*765 as white is excluded) so convert to milliAmps + milliAmpsTotal += (busPowerSum * maPL * brightness) / (765*255); + } + if (lengthDigital > 0) { + avgMilliAmpsPerLED /= lengthDigital; + + if (milliAmpsMax > MA_FOR_ESP && avgMilliAmpsPerLED > 0) { //0 mA per LED and too low numbers turn off calculation + unsigned powerBudget = (milliAmpsMax - MA_FOR_ESP); //80/120mA for ESP power + if (powerBudget > lengthDigital) { //each LED uses about 1mA in standby, exclude that from power budget + powerBudget -= lengthDigital; + } else { + powerBudget = 0; + } + if (milliAmpsTotal > powerBudget) { + //scale brightness down to stay in current limit + unsigned scaleB = powerBudget * 255 / milliAmpsTotal; + brightness = ((brightness * scaleB) >> 8) + 1; + } + } + } + } + return brightness; } void WS2812FX::show() { + unsigned long showNow = millis(); + size_t diff = showNow - _lastShow; + + size_t totalLen = getLengthTotal(); + if (realtimeMode == REALTIME_MODE_INACTIVE || useMainSegmentOnly || realtimeOverride > REALTIME_OVERRIDE_NONE) { + // clear frame buffer + for (size_t i = 0; i < totalLen; i++) _pixels[i] = BLACK; // memset(_pixels, 0, sizeof(uint32_t) * getLengthTotal()); + // blend all segments into (cleared) buffer + for (Segment &seg : _segments) if (seg.isActive() && (seg.on || seg.isInTransition())) { + blendSegment(seg); // blend segment's buffer into frame buffer + } + } + // avoid race condition, capture _callback value show_callback callback = _callback; - if (callback) callback(); - unsigned long showNow = millis(); + if (callback) callback(); // will call setPixelColor or setRealtimePixelColor + + // determine ABL brightness + uint8_t newBri = estimateCurrentAndLimitBri(_brightness, _pixels); + if (newBri != _brightness) BusManager::setBrightness(newBri); + + // paint actuall pixels + for (size_t i = 0; i < totalLen; i++) BusManager::setPixelColor(getMappedPixelIndex(i), realtimeMode && arlsDisableGammaCorrection ? _pixels[i] : gamma32(_pixels[i])); // some buses send asynchronously and this method will return before // all of the data has been sent. // See https://github.com/Makuna/NeoPixelBus/wiki/ESP32-NeoMethods#neoesp32rmt-methods BusManager::show(); - size_t diff = showNow - _lastShow; + // restore brightness for next frame + if (newBri != _brightness) BusManager::setBrightness(_brightness); if (diff > 0) { // skip calculation if no time has passed - size_t fpsCurr = (1000 << FPS_CALC_SHIFT) / diff; // fixed point math - _cumulativeFps = (FPS_CALC_AVG * _cumulativeFps + fpsCurr + FPS_CALC_AVG / 2) / (FPS_CALC_AVG + 1); // "+FPS_CALC_AVG/2" for proper rounding + int fpsCurr = (1000 << FPS_CALC_SHIFT) / diff; // fixed point math (shift left for better precision) + _cumulativeFps += ((fpsCurr - (_cumulativeFps << FPS_CALC_SHIFT)) / FPS_CALC_AVG + ((1<> FPS_CALC_SHIFT; // simple PI controller over FPS_CALC_AVG frames _lastShow = showNow; } } +void WS2812FX::setRealtimePixelColor(unsigned i, uint32_t c) { + if (useMainSegmentOnly) { + const Segment &seg = getMainSegment(); + if (seg.isActive() && i < seg.length()) seg.setPixelColorRaw(i, c); + } else { + setPixelColor(i, c); + } +} + +// reset all segments +void WS2812FX::restartRuntime() { + suspend(); + waitForIt(); + for (Segment &seg : _segments) seg.markForReset().resetIfRequired(); + resume(); +} + +// start or stop transition for all segments +void WS2812FX::setTransitionMode(bool t) { + suspend(); + waitForIt(); + for (Segment &seg : _segments) seg.startTransition(t ? _transitionDur : 0); + resume(); +} + +// wait until frame is over (service() has finished or time for 1 frame has passed; yield() crashes on 8266) +void WS2812FX::waitForIt() { + unsigned long maxWait = millis() + getFrameTime(); + while (isServicing() && maxWait > millis()) delay(1); + #ifdef WLED_DEBUG + if (millis() >= maxWait) DEBUG_PRINTLN(F("Waited for strip to finish servicing.")); + #endif +}; + void WS2812FX::setTargetFps(unsigned fps) { - if (fps <= 250) _targetFps = fps; - if (_targetFps > 0) _frametime = 1000 / _targetFps; - else _frametime = MIN_FRAME_DELAY; // unlimited mode + if (fps > 0 && fps <= 120) _targetFps = fps; + _frametime = 1000 / _targetFps; } void WS2812FX::setCCT(uint16_t k) { - for (segment &seg : _segments) { + for (Segment &seg : _segments) { if (seg.isActive() && seg.isSelected()) { seg.setCCT(k); } @@ -1735,22 +1672,18 @@ void WS2812FX::setBrightness(uint8_t b, bool direct) { if (_brightness == b) return; _brightness = b; if (_brightness == 0) { //unfreeze all segments on power off - for (segment &seg : _segments) { - seg.freeze = false; - } + for (const Segment &seg : _segments) seg.freeze = false; // freeze is mutable } - // setting brightness with NeoPixelBusLg has no effect on already painted pixels, - // so we need to force an update to existing buffer BusManager::setBrightness(b); if (!direct) { unsigned long t = millis(); - if (_segments[0].next_time > t + 22 && t - _lastShow > MIN_FRAME_DELAY) trigger(); //apply brightness change immediately if no refresh soon + if (_segments[0].next_time > t + 22 && t - _lastShow > MIN_SHOW_DELAY) trigger(); //apply brightness change immediately if no refresh soon } } uint8_t WS2812FX::getActiveSegsLightCapabilities(bool selectedOnly) const { uint8_t totalLC = 0; - for (const segment &seg : _segments) { + for (const Segment &seg : _segments) { if (seg.isActive() && (!selectedOnly || seg.isSelected())) totalLC |= seg.getLightCapabilities(); } return totalLC; @@ -1758,7 +1691,7 @@ uint8_t WS2812FX::getActiveSegsLightCapabilities(bool selectedOnly) const { uint8_t WS2812FX::getFirstSelectedSegId() const { size_t i = 0; - for (const segment &seg : _segments) { + for (const Segment &seg : _segments) { if (seg.isActive() && seg.isSelected()) return i; i++; } @@ -1767,8 +1700,8 @@ uint8_t WS2812FX::getFirstSelectedSegId() const { } void WS2812FX::setMainSegmentId(unsigned n) { - _mainSegment = 0; - if (n < _segments.size()) { + _mainSegment = getLastActiveSegmentId(); + if (n < _segments.size() && _segments[n].isActive()) { // only set if segment is active _mainSegment = n; } return; @@ -1782,10 +1715,8 @@ uint8_t WS2812FX::getLastActiveSegmentId() const { } uint8_t WS2812FX::getActiveSegmentsNum() const { - uint8_t c = 0; - for (size_t i = 0; i < _segments.size(); i++) { - if (_segments[i].isActive()) c++; - } + unsigned c = 0; + for (const Segment &seg : _segments) if (seg.isActive()) c++; return c; } @@ -1796,13 +1727,7 @@ uint16_t WS2812FX::getLengthTotal() const { } uint16_t WS2812FX::getLengthPhysical() const { - unsigned len = 0; - for (size_t b = 0; b < BusManager::getNumBusses(); b++) { - Bus *bus = BusManager::getBus(b); - if (bus->isVirtual()) continue; //exclude non-physical network busses - len += bus->getLength(); - } - return len; + return BusManager::getTotalLength(true); } //used for JSON API info.leds.rgbw. Little practical use, deprecate with info.leds.rgbw. @@ -1847,14 +1772,9 @@ Segment& WS2812FX::getSegment(unsigned id) { } void WS2812FX::resetSegments() { - _segments.clear(); // destructs all Segment as part of clearing - #ifndef WLED_DISABLE_2D - segment seg = isMatrix ? Segment(0, Segment::maxWidth, 0, Segment::maxHeight) : Segment(0, _length); - #else - segment seg = Segment(0, _length); - #endif - _segments.push_back(seg); - _segments.shrink_to_fit(); // just in case ... + _segments.clear(); // destructs all Segment as part of clearing + _segments.emplace_back(0, isMatrix ? Segment::maxWidth : _length, 0, isMatrix ? Segment::maxHeight : 1); + _segments.shrink_to_fit(); // just in case ... _mainSegment = 0; } @@ -1902,12 +1822,12 @@ void WS2812FX::makeAutoSegments(bool forceReset) { // there is always at least one segment (but we need to differentiate between 1D and 2D) #ifndef WLED_DISABLE_2D if (isMatrix) - _segments.push_back(Segment(0, Segment::maxWidth, 0, Segment::maxHeight)); + _segments.emplace_back(0, Segment::maxWidth, 0, Segment::maxHeight); else #endif - _segments.push_back(Segment(segStarts[0], segStops[0])); + _segments.emplace_back(segStarts[0], segStops[0]); for (size_t i = 1; i < s; i++) { - _segments.push_back(Segment(segStarts[i], segStops[i])); + _segments.emplace_back(segStarts[i], segStops[i]); } DEBUG_PRINTF_P(PSTR("%d auto segments created.\n"), _segments.size()); @@ -1918,15 +1838,9 @@ void WS2812FX::makeAutoSegments(bool forceReset) { else if (getActiveSegmentsNum() == 1) { size_t i = getLastActiveSegmentId(); #ifndef WLED_DISABLE_2D - _segments[i].start = 0; - _segments[i].stop = Segment::maxWidth; - _segments[i].startY = 0; - _segments[i].stopY = Segment::maxHeight; - _segments[i].grouping = 1; - _segments[i].spacing = 0; + _segments[i].setGeometry(0, Segment::maxWidth, 1, 0, 0xFFFF, 0, Segment::maxHeight); #else - _segments[i].start = 0; - _segments[i].stop = _length; + _segments[i].setGeometry(0, _length); #endif } } @@ -1958,7 +1872,7 @@ void WS2812FX::fixInvalidSegments() { // if any segments were deleted free memory purgeSegments(); // this is always called as the last step after finalizeInit(), update covered bus types - for (segment &seg : _segments) + for (const Segment &seg : _segments) seg.refreshLightCapabilities(); } @@ -1966,7 +1880,7 @@ void WS2812FX::fixInvalidSegments() { //irrelevant in 2D set-up bool WS2812FX::checkSegmentAlignment() const { bool aligned = false; - for (const segment &seg : _segments) { + for (const Segment &seg : _segments) { for (unsigned b = 0; bisOk()) break; @@ -1996,59 +1910,9 @@ void WS2812FX::printSize() { } #endif -void WS2812FX::loadCustomPalettes() { - byte tcp[72]; //support gradient palettes with up to 18 entries - CRGBPalette16 targetPalette; - customPalettes.clear(); // start fresh - for (int index = 0; index<10; index++) { - char fileName[32]; - sprintf_P(fileName, PSTR("/palette%d.json"), index); - - StaticJsonDocument<1536> pDoc; // barely enough to fit 72 numbers - if (WLED_FS.exists(fileName)) { - DEBUG_PRINT(F("Reading palette from ")); - DEBUG_PRINTLN(fileName); - - if (readObjectFromFile(fileName, nullptr, &pDoc)) { - JsonArray pal = pDoc[F("palette")]; - if (!pal.isNull() && pal.size()>3) { // not an empty palette (at least 2 entries) - if (pal[0].is() && pal[1].is()) { - // we have an array of index & hex strings - size_t palSize = MIN(pal.size(), 36); - palSize -= palSize % 2; // make sure size is multiple of 2 - for (size_t i=0, j=0; i()<256; i+=2, j+=4) { - uint8_t rgbw[] = {0,0,0,0}; - tcp[ j ] = (uint8_t) pal[ i ].as(); // index - colorFromHexString(rgbw, pal[i+1].as()); // will catch non-string entires - for (size_t c=0; c<3; c++) tcp[j+1+c] = gamma8(rgbw[c]); // only use RGB component - DEBUG_PRINTF_P(PSTR("%d(%d) : %d %d %d\n"), i, int(tcp[j]), int(tcp[j+1]), int(tcp[j+2]), int(tcp[j+3])); - } - } else { - size_t palSize = MIN(pal.size(), 72); - palSize -= palSize % 4; // make sure size is multiple of 4 - for (size_t i=0; i()<256; i+=4) { - tcp[ i ] = (uint8_t) pal[ i ].as(); // index - tcp[i+1] = gamma8((uint8_t) pal[i+1].as()); // R - tcp[i+2] = gamma8((uint8_t) pal[i+2].as()); // G - tcp[i+3] = gamma8((uint8_t) pal[i+3].as()); // B - DEBUG_PRINTF_P(PSTR("%d(%d) : %d %d %d\n"), i, int(tcp[i]), int(tcp[i+1]), int(tcp[i+2]), int(tcp[i+3])); - } - } - customPalettes.push_back(targetPalette.loadDynamicGradientPalette(tcp)); - } else { - DEBUG_PRINTLN(F("Wrong palette format.")); - } - } - } else { - break; - } - } -} - -//load custom mapping table from JSON file (called from finalizeInit() or deserializeState()) +// load custom mapping table from JSON file (called from finalizeInit() or deserializeState()) +// if this is a matrix set-up and default ledmap.json file does not exist, create mapping table using setUpMatrix() from panel information bool WS2812FX::deserializeMap(unsigned n) { - // 2D support creates its own ledmap (on the fly) if a ledmap.json exists it will overwrite built one. - char fileName[32]; strcpy_P(fileName, PSTR("/ledmap")); if (n) sprintf(fileName +7, "%d", n); @@ -2060,6 +1924,7 @@ bool WS2812FX::deserializeMap(unsigned n) { if (n == 0 || isFile) interfaceUpdateCallMode = CALL_MODE_WS_SEND; // schedule WS update (to inform UI) if (!isFile && n==0 && isMatrix) { + // 2D panel support creates its own ledmap (on the fly) if a ledmap.json does not exist setUpMatrix(); return false; } @@ -2070,25 +1935,28 @@ bool WS2812FX::deserializeMap(unsigned n) { filter[F("width")] = true; filter[F("height")] = true; if (!readObjectFromFile(fileName, nullptr, pDoc, &filter)) { - DEBUG_PRINT(F("ERROR Invalid ledmap in ")); DEBUG_PRINTLN(fileName); + DEBUG_PRINTF_P(PSTR("ERROR Invalid ledmap in %s\n"), fileName); releaseJSONBufferLock(); return false; // if file does not load properly then exit - } + } else + DEBUG_PRINTF_P(PSTR("Reading LED map from %s\n"), fileName); suspend(); + waitForIt(); JsonObject root = pDoc->as(); // if we are loading default ledmap (at boot) set matrix width and height from the ledmap (compatible with WLED MM ledmaps) - if (isMatrix && n == 0 && (!root[F("width")].isNull() || !root[F("height")].isNull())) { - Segment::maxWidth = min(max(root[F("width")].as(), 1), 128); - Segment::maxHeight = min(max(root[F("height")].as(), 1), 128); + if (n == 0 && (!root[F("width")].isNull() || !root[F("height")].isNull())) { + Segment::maxWidth = min(max(root[F("width")].as(), 1), 255); + Segment::maxHeight = min(max(root[F("height")].as(), 1), 255); + isMatrix = true; } - if (customMappingTable) free(customMappingTable); - customMappingTable = static_cast(malloc(sizeof(uint16_t)*getLengthTotal())); + d_free(customMappingTable); + customMappingTable = static_cast(d_malloc(sizeof(uint16_t)*getLengthTotal())); // do not use SPI RAM if (customMappingTable) { - DEBUG_PRINT(F("Reading LED map from ")); DEBUG_PRINTLN(fileName); + DEBUG_PRINTF_P(PSTR("ledmap allocated: %uB\n"), sizeof(uint16_t)*getLengthTotal()); File f = WLED_FS.open(fileName, "r"); f.find("\"map\":["); while (f.available()) { // f.position() < f.size() - 1 @@ -2140,8 +2008,6 @@ bool WS2812FX::deserializeMap(unsigned n) { } -WS2812FX* WS2812FX::instance = nullptr; - const char JSON_mode_names[] PROGMEM = R"=====(["FX names moved"])====="; const char JSON_palette_names[] PROGMEM = R"=====([ "Default","* Random Cycle","* Color 1","* Colors 1&2","* Color Gradient","* Colors Only","Party","Cloud","Lava","Ocean", @@ -2152,4 +2018,4 @@ const char JSON_palette_names[] PROGMEM = R"=====([ "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","Traffic Light" -])====="; \ No newline at end of file +])====="; diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index cee34c2ea..59d6f5435 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -32,8 +32,29 @@ extern bool useParallelI2S; uint32_t colorBalanceFromKelvin(uint16_t kelvin, uint32_t rgb); //udp.cpp -uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const uint8_t* buffer, uint8_t bri=255, bool isRGBW=false); +uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const byte *buffer, uint8_t bri=255, bool isRGBW=false); +//util.cpp +// PSRAM allocation wrappers +#ifndef ESP8266 +void *w_malloc(size_t); // prefer PSRAM over DRAM +void *w_calloc(size_t, size_t); // prefer PSRAM over DRAM +void *w_realloc(void *, size_t); // prefer PSRAM over DRAM +inline void w_free(void *ptr) { heap_caps_free(ptr); } +void *d_malloc(size_t); // prefer DRAM over PSRAM +void *d_calloc(size_t, size_t); // prefer DRAM over PSRAM +void *d_realloc(void *, size_t); // prefer DRAM over PSRAM +inline void d_free(void *ptr) { heap_caps_free(ptr); } +#else +#define w_malloc malloc +#define w_calloc calloc +#define w_realloc realloc +#define w_free free +#define d_malloc malloc +#define d_calloc calloc +#define d_realloc realloc +#define d_free free +#endif //color mangling macros #define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b)))) @@ -72,7 +93,7 @@ void Bus::calculateCCT(uint32_t c, uint8_t &ww, uint8_t &cw) { } 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); @@ -106,7 +127,6 @@ BusDigital::BusDigital(const BusConfig &bc, uint8_t nr) , _colorOrder(bc.colorOrder) , _milliAmpsPerLed(bc.milliAmpsPerLed) , _milliAmpsMax(bc.milliAmpsMax) -, _data(nullptr) { DEBUGBUS_PRINTLN(F("Bus: Creating digital bus.")); if (!isDigital(bc.type) || !bc.count) { DEBUGBUS_PRINTLN(F("Not digial or empty bus!")); return; } @@ -127,10 +147,6 @@ BusDigital::BusDigital(const BusConfig &bc, uint8_t nr) _hasRgb = hasRGB(bc.type); _hasWhite = hasWhite(bc.type); _hasCCT = hasCCT(bc.type); - if (bc.doubleBuffer) { - _data = (uint8_t*)calloc(_len, Bus::getNumberOfChannels(_type)); - if (!_data) DEBUGBUS_PRINTLN(F("Bus: Buffer allocation failed!")); - } 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); @@ -213,43 +229,6 @@ void BusDigital::show() { uint8_t cctWW = 0, cctCW = 0; unsigned newBri = estimateCurrentAndLimitBri(); // will fill _milliAmpsTotal (TODO: could use PolyBus::CalcTotalMilliAmpere()) if (newBri < _bri) PolyBus::setBrightness(_busPtr, _iType, newBri); // limit brightness to stay within current limits - - if (_data) { - size_t channels = getNumberOfChannels(); - int16_t oldCCT = Bus::_cct; // temporarily save bus CCT - for (size_t i=0; i<_len; i++) { - size_t offset = i * channels; - unsigned co = _colorOrderMap.getPixelColorOrder(i+_start, _colorOrder); - uint32_t c; - if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs (_len is always a multiple of 3) - switch (i%3) { - case 0: c = RGBW32(_data[offset] , _data[offset+1], _data[offset+2], 0); break; - case 1: c = RGBW32(_data[offset-1], _data[offset] , _data[offset+1], 0); break; - case 2: c = RGBW32(_data[offset-2], _data[offset-1], _data[offset] , 0); break; - } - } else { - if (hasRGB()) c = RGBW32(_data[offset], _data[offset+1], _data[offset+2], hasWhite() ? _data[offset+3] : 0); - else c = RGBW32(0, 0, 0, _data[offset]); - } - if (hasCCT()) { - // unfortunately as a segment may span multiple buses or a bus may contain multiple segments and each segment may have different CCT - // we need to extract and appy CCT value for each pixel individually even though all buses share the same _cct variable - // TODO: there is an issue if CCT is calculated from RGB value (_cct==-1), we cannot do that with double buffer - Bus::_cct = _data[offset+channels-1]; - Bus::calculateCCT(c, cctWW, cctCW); - if (_type == TYPE_WS2812_WWA) c = RGBW32(cctWW, cctCW, 0, W(c)); // may need swapping - } - unsigned pix = i; - if (_reversed) pix = _len - pix -1; - pix += _skip; - PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, (cctCW<<8) | cctWW); - } - #if !defined(STATUSLED) || STATUSLED>=0 - if (_skip) PolyBus::setPixelColor(_busPtr, _iType, 0, 0, _colorOrderMap.getPixelColorOrder(_start, _colorOrder)); // paint skipped pixels black - #endif - for (int i=1; i<_skip; i++) PolyBus::setPixelColor(_busPtr, _iType, i, 0, _colorOrderMap.getPixelColorOrder(_start, _colorOrder)); // paint skipped pixels black - Bus::_cct = oldCCT; - } else { if (newBri < _bri) { unsigned hwLen = _len; if (_type == TYPE_WS2812_1CH_X3) hwLen = NUM_ICS_WS2812_1CH_3X(_len); // only needs a third of "RGB" LEDs for NeoPixelBus @@ -260,8 +239,7 @@ void BusDigital::show() { PolyBus::setPixelColor(_busPtr, _iType, i, c, 0, (cctCW<<8) | cctWW); // repaint all pixels with new brightness } } - } - PolyBus::show(_busPtr, _iType, !_data); // faster if buffer consistency is not important + PolyBus::show(_busPtr, _iType, false); // faster if buffer consistency is not important // restore bus brightness to its original value // this is done right after show, so this is only OK if LED updates are completed before show() returns // or async show has a separate buffer (ESP32 RMT and I2S are ok) @@ -292,86 +270,61 @@ void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) { if (!_valid) return; 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 (_reversed) pix = _len - pix -1; + pix += _skip; + unsigned co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); + if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs + unsigned pOld = pix; + pix = IC_INDEX_WS2812_1CH_3X(pix); + uint32_t cOld = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, pix, co),_bri); + switch (pOld % 3) { // change only the single channel (TODO: this can cause loss because of get/set) + case 0: c = RGBW32(R(cOld), W(c) , B(cOld), 0); break; + case 1: c = RGBW32(W(c) , G(cOld), B(cOld), 0); break; + case 2: c = RGBW32(R(cOld), G(cOld), W(c) , 0); break; } - if (hasWhite()) *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; - unsigned co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); - if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs - unsigned pOld = pix; - pix = IC_INDEX_WS2812_1CH_3X(pix); - uint32_t cOld = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, pix, co),_bri); - switch (pOld % 3) { // change only the single channel (TODO: this can cause loss because of get/set) - case 0: c = RGBW32(R(cOld), W(c) , B(cOld), 0); break; - case 1: c = RGBW32(W(c) , G(cOld), B(cOld), 0); break; - case 2: c = RGBW32(R(cOld), G(cOld), W(c) , 0); break; - } - } - uint16_t wwcw = 0; - if (hasCCT()) { - uint8_t cctWW = 0, cctCW = 0; - Bus::calculateCCT(c, cctWW, cctCW); - wwcw = (cctCW<<8) | cctWW; - if (_type == TYPE_WS2812_WWA) c = RGBW32(cctWW, cctCW, 0, W(c)); // may need swapping - } - PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, wwcw); } + uint16_t wwcw = 0; + if (hasCCT()) { + uint8_t cctWW = 0, cctCW = 0; + Bus::calculateCCT(c, cctWW, cctCW); + wwcw = (cctCW<<8) | cctWW; + if (_type == TYPE_WS2812_WWA) c = RGBW32(cctWW, cctCW, 0, W(c)); + } + PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, wwcw); } // returns original color if global buffering is enabled, else returns lossly restored color from bus uint32_t IRAM_ATTR BusDigital::getPixelColor(unsigned pix) const { if (!_valid) return 0; - if (_data) { - const size_t offset = pix * getNumberOfChannels(); - uint32_t c; - if (!hasRGB()) { - c = RGBW32(_data[offset], _data[offset], _data[offset], _data[offset]); - } else { - c = RGBW32(_data[offset], _data[offset+1], _data[offset+2], hasWhite() ? _data[offset+3] : 0); + if (_reversed) pix = _len - pix -1; + pix += _skip; + const unsigned co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); + uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, (_type==TYPE_WS2812_1CH_X3) ? IC_INDEX_WS2812_1CH_3X(pix) : pix, co),_bri); + if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs + unsigned r = R(c); + unsigned g = _reversed ? B(c) : G(c); // should G and B be switched if _reversed? + unsigned b = _reversed ? G(c) : B(c); + switch (pix % 3) { // get only the single channel + case 0: c = RGBW32(g, g, g, g); break; + case 1: c = RGBW32(r, r, r, r); break; + case 2: c = RGBW32(b, b, b, b); break; } - return c; - } else { - if (_reversed) pix = _len - pix -1; - pix += _skip; - const unsigned co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); - uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, (_type==TYPE_WS2812_1CH_X3) ? IC_INDEX_WS2812_1CH_3X(pix) : pix, co),_bri); - if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs - unsigned r = R(c); - unsigned g = _reversed ? B(c) : G(c); // should G and B be switched if _reversed? - unsigned b = _reversed ? G(c) : B(c); - switch (pix % 3) { // get only the single channel - case 0: c = RGBW32(g, g, g, g); break; - case 1: c = RGBW32(r, r, r, r); break; - case 2: c = RGBW32(b, b, b, b); break; - } - } - if (_type == TYPE_WS2812_WWA) { - uint8_t w = R(c) | G(c); - c = RGBW32(w, w, 0, w); - } - return c; } + if (_type == TYPE_WS2812_WWA) { + uint8_t w = R(c) | G(c); + c = RGBW32(w, w, 0, w); + } + return c; } -unsigned BusDigital::getPins(uint8_t* pinArray) const { +size_t BusDigital::getPins(uint8_t* pinArray) const { unsigned numPins = is2Pin(_type) + 1; if (pinArray) for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i]; return numPins; } -unsigned BusDigital::getBusSize() const { - return sizeof(BusDigital) + (isOk() ? PolyBus::getDataSize(_busPtr, _iType) + (_data ? _len * getNumberOfChannels() : 0) : 0); +size_t BusDigital::getBusSize() const { + return sizeof(BusDigital) + (isOk() ? PolyBus::getDataSize(_busPtr, _iType) /*+ (_data ? _len * getNumberOfChannels() : 0)*/ : 0); } void BusDigital::setColorOrder(uint8_t colorOrder) { @@ -380,7 +333,7 @@ void BusDigital::setColorOrder(uint8_t colorOrder) { _colorOrder = colorOrder; } -// credit @willmmiles & @netmindz https://github.com/wled-dev/WLED/pull/4056 +// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 std::vector BusDigital::getLEDTypes() { return { {TYPE_WS2812_RGB, "D", PSTR("WS281x")}, @@ -414,8 +367,6 @@ void BusDigital::begin() { void BusDigital::cleanup() { DEBUGBUS_PRINTLN(F("Digital Cleanup.")); PolyBus::cleanup(_busPtr, _iType); - free(_data); - _data = nullptr; _iType = I_NONE; _valid = false; _busPtr = nullptr; @@ -453,7 +404,7 @@ BusPwm::BusPwm(const BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, 1, bc.reversed, bc.refreshReq) // hijack Off refresh flag to indicate usage of dithering { if (!isPWM(bc.type)) return; - unsigned numPins = numPWMPins(bc.type); + const unsigned numPins = numPWMPins(bc.type); [[maybe_unused]] const bool dithering = _needsRefresh; _frequency = bc.frequency ? bc.frequency : WLED_PWM_FREQ; // duty cycle resolution (_depth) can be extracted from this formula: CLOCK_FREQUENCY > _frequency * 2^_depth @@ -461,36 +412,40 @@ BusPwm::BusPwm(const BusConfig &bc) managed_pin_type pins[numPins]; for (unsigned i = 0; i < numPins; i++) pins[i] = {(int8_t)bc.pins[i], true}; - if (!PinManager::allocateMultiplePins(pins, numPins, PinOwner::BusPwm)) return; - -#ifdef ARDUINO_ARCH_ESP32 - // for 2 pin PWM CCT strip pinManager will make sure both LEDC channels are in the same speed group and sharing the same timer - _ledcStart = PinManager::allocateLedc(numPins); - if (_ledcStart == 255) { //no more free LEDC channels - PinManager::deallocateMultiplePins(pins, numPins, PinOwner::BusPwm); - return; - } - // if _needsRefresh is true (UI hack) we are using dithering (credit @dedehai & @zalatnaicsongor) - if (dithering) _depth = 12; // fixed 8 bit depth PWM with 4 bit dithering (ESP8266 has no hardware to support dithering) -#endif - - for (unsigned i = 0; i < numPins; i++) { - _pins[i] = bc.pins[i]; // store only after allocateMultiplePins() succeeded + if (PinManager::allocateMultiplePins(pins, numPins, PinOwner::BusPwm)) { #ifdef ESP8266 - pinMode(_pins[i], OUTPUT); + analogWriteRange((1<<_depth)-1); + analogWriteFreq(_frequency); #else - unsigned channel = _ledcStart + i; - ledcSetup(channel, _frequency, _depth - (dithering*4)); // with dithering _frequency doesn't really matter as resolution is 8 bit - ledcAttachPin(_pins[i], channel); - // LEDC timer reset credit @dedehai - uint8_t group = (channel / 8), timer = ((channel / 2) % 4); // same fromula as in ledcSetup() - ledc_timer_rst((ledc_mode_t)group, (ledc_timer_t)timer); // reset timer so all timers are almost in sync (for phase shift) + // for 2 pin PWM CCT strip pinManager will make sure both LEDC channels are in the same speed group and sharing the same timer + _ledcStart = PinManager::allocateLedc(numPins); + if (_ledcStart == 255) { //no more free LEDC channels + PinManager::deallocateMultiplePins(pins, numPins, PinOwner::BusPwm); + DEBUGBUS_PRINTLN(F("No more free LEDC channels!")); + return; + } + // if _needsRefresh is true (UI hack) we are using dithering (credit @dedehai & @zalatnaicsongor) + if (dithering) _depth = 12; // fixed 8 bit depth PWM with 4 bit dithering (ESP8266 has no hardware to support dithering) #endif + + for (unsigned i = 0; i < numPins; i++) { + _pins[i] = bc.pins[i]; // store only after allocateMultiplePins() succeeded + #ifdef ESP8266 + pinMode(_pins[i], OUTPUT); + #else + unsigned channel = _ledcStart + i; + ledcSetup(channel, _frequency, _depth - (dithering*4)); // with dithering _frequency doesn't really matter as resolution is 8 bit + ledcAttachPin(_pins[i], channel); + // LEDC timer reset credit @dedehai + uint8_t group = (channel / 8), timer = ((channel / 2) % 4); // same fromula as in ledcSetup() + ledc_timer_rst((ledc_mode_t)group, (ledc_timer_t)timer); // reset timer so all timers are almost in sync (for phase shift) + #endif + } + _hasRgb = hasRGB(bc.type); + _hasWhite = hasWhite(bc.type); + _hasCCT = hasCCT(bc.type); + _valid = true; } - _hasRgb = hasRGB(bc.type); - _hasWhite = hasWhite(bc.type); - _hasCCT = hasCCT(bc.type); - _valid = true; DEBUGBUS_PRINTF_P(PSTR("%successfully inited PWM strip with type %u, frequency %u, bit depth %u and pins %u,%u,%u,%u,%u\n"), _valid?"S":"Uns", bc.type, _frequency, _depth, _pins[0], _pins[1], _pins[2], _pins[3], _pins[4]); } @@ -561,7 +516,7 @@ void BusPwm::show() { constexpr unsigned bitShift = 8; // 256 clocks for dead time, ~3us at 80MHz #else // if _needsRefresh is true (UI hack) we are using dithering (credit @dedehai & @zalatnaicsongor) - // https://github.com/wled-dev/WLED/pull/4115 and https://github.com/zalatnaicsongor/WLED/pull/1) + // https://github.com/wled/WLED/pull/4115 and https://github.com/zalatnaicsongor/WLED/pull/1) const bool dithering = _needsRefresh; // avoid working with bitfield const unsigned maxBri = (1<<_depth); // possible values: 16384 (14), 8192 (13), 4096 (12), 2048 (11), 1024 (10), 512 (9) and 256 (8) const unsigned bitShift = dithering * 4; // if dithering, _depth is 12 bit but LEDC channel is set to 8 bit (using 4 fractional bits) @@ -620,14 +575,14 @@ void BusPwm::show() { } } -unsigned BusPwm::getPins(uint8_t* pinArray) const { +size_t BusPwm::getPins(uint8_t* pinArray) const { if (!_valid) return 0; unsigned numPins = numPWMPins(_type); if (pinArray) for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i]; return numPins; } -// credit @willmmiles & @netmindz https://github.com/wled-dev/WLED/pull/4056 +// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 std::vector BusPwm::getLEDTypes() { return { {TYPE_ANALOG_1CH, "A", PSTR("PWM White")}, @@ -695,13 +650,13 @@ void BusOnOff::show() { digitalWrite(_pin, _reversed ? !(bool)_data : (bool)_data); } -unsigned BusOnOff::getPins(uint8_t* pinArray) const { +size_t BusOnOff::getPins(uint8_t* pinArray) const { if (!_valid) return 0; if (pinArray) pinArray[0] = _pin; return 1; } -// credit @willmmiles & @netmindz https://github.com/wled-dev/WLED/pull/4056 +// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 std::vector BusOnOff::getLEDTypes() { return { {TYPE_ONOFF, "", PSTR("On/Off")}, @@ -731,7 +686,7 @@ BusNetwork::BusNetwork(const BusConfig &bc) _hasCCT = false; _UDPchannels = _hasWhite + 3; _client = IPAddress(bc.pins[0],bc.pins[1],bc.pins[2],bc.pins[3]); - _data = (uint8_t*)calloc(_len, _UDPchannels); + _data = (uint8_t*)d_calloc(_len, _UDPchannels); _valid = (_data != nullptr); DEBUGBUS_PRINTF_P(PSTR("%successfully inited virtual strip with type %u and IP %u.%u.%u.%u\n"), _valid?"S":"Uns", bc.type, bc.pins[0], bc.pins[1], bc.pins[2], bc.pins[3]); } @@ -760,12 +715,12 @@ void BusNetwork::show() { _broadcastLock = false; } -unsigned BusNetwork::getPins(uint8_t* pinArray) const { +size_t BusNetwork::getPins(uint8_t* pinArray) const { if (pinArray) for (unsigned i = 0; i < 4; i++) pinArray[i] = _client[i]; return 4; } -// credit @willmmiles & @netmindz https://github.com/wled-dev/WLED/pull/4056 +// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 std::vector BusNetwork::getLEDTypes() { return { {TYPE_NET_DDP_RGB, "N", PSTR("DDP RGB (network)")}, // should be "NNNN" to determine 4 "pin" fields @@ -776,13 +731,13 @@ std::vector BusNetwork::getLEDTypes() { //{TYPE_VIRTUAL_I2C_W, "V", PSTR("I2C White (virtual)")}, // allows setting I2C address in _pin[0] //{TYPE_VIRTUAL_I2C_CCT, "V", PSTR("I2C CCT (virtual)")}, // allows setting I2C address in _pin[0] //{TYPE_VIRTUAL_I2C_RGB, "VVV", PSTR("I2C RGB (virtual)")}, // allows setting I2C address in _pin[0] and 2 additional values in _pin[1] & _pin[2] - //{TYPE_USERMOD, "VVVVV", PSTR("Usermod (virtual)")}, // 5 data fields (see https://github.com/wled-dev/WLED/pull/4123) + //{TYPE_USERMOD, "VVVVV", PSTR("Usermod (virtual)")}, // 5 data fields (see https://github.com/wled/WLED/pull/4123) }; } void BusNetwork::cleanup() { DEBUGBUS_PRINTLN(F("Virtual Cleanup.")); - free(_data); + d_free(_data); _data = nullptr; _type = I_NONE; _valid = false; @@ -790,11 +745,11 @@ void BusNetwork::cleanup() { //utility to get the approx. memory usage of a given BusConfig -unsigned BusConfig::memUsage(unsigned nr) const { +size_t BusConfig::memUsage(unsigned nr) const { if (Bus::isVirtual(type)) { return sizeof(BusNetwork) + (count * Bus::getNumberOfChannels(type)); } else if (Bus::isDigital(type)) { - return sizeof(BusDigital) + PolyBus::memUsage(count + skipAmount, PolyBus::getI(type, pins, nr)) + doubleBuffer * (count + skipAmount) * Bus::getNumberOfChannels(type); + return sizeof(BusDigital) + PolyBus::memUsage(count + skipAmount, PolyBus::getI(type, pins, nr)) /*+ doubleBuffer * (count + skipAmount) * Bus::getNumberOfChannels(type)*/; } else if (Bus::isOnOff(type)) { return sizeof(BusOnOff); } else { @@ -803,7 +758,7 @@ unsigned BusConfig::memUsage(unsigned nr) const { } -unsigned BusManager::memUsage() { +size_t BusManager::memUsage() { // when ESP32, S2 & S3 use parallel I2S only the largest bus determines the total memory requirements for back buffers // front buffers are always allocated per bus unsigned size = 0; @@ -832,22 +787,24 @@ unsigned BusManager::memUsage() { } int BusManager::add(const BusConfig &bc) { - DEBUGBUS_PRINTF_P(PSTR("Bus: Adding bus (%d - %d >= %d)\n"), getNumBusses(), getNumVirtualBusses(), WLED_MAX_BUSSES); - if (getNumBusses() - getNumVirtualBusses() >= WLED_MAX_BUSSES) return -1; - unsigned numDigital = 0; - for (const auto &bus : busses) if (bus->isDigital() && !bus->is2Pin()) numDigital++; + DEBUGBUS_PRINTF_P(PSTR("Bus: Adding bus (p:%d v:%d)\n"), getNumBusses(), getNumVirtualBusses()); + unsigned digital = 0; + unsigned analog = 0; + unsigned twoPin = 0; + for (const auto &bus : busses) { + if (bus->isPWM()) analog += bus->getPins(); // number of analog channels used + if (bus->isDigital() && !bus->is2Pin()) digital++; + if (bus->is2Pin()) twoPin++; + } + if (digital > WLED_MAX_DIGITAL_CHANNELS || analog > WLED_MAX_ANALOG_CHANNELS) return -1; if (Bus::isVirtual(bc.type)) { busses.push_back(make_unique(bc)); - //busses.push_back(new BusNetwork(bc)); } else if (Bus::isDigital(bc.type)) { - busses.push_back(make_unique(bc, numDigital)); - //busses.push_back(new BusDigital(bc, numDigital)); + busses.push_back(make_unique(bc, Bus::is2Pin(bc.type) ? twoPin : digital)); } else if (Bus::isOnOff(bc.type)) { busses.push_back(make_unique(bc)); - //busses.push_back(new BusOnOff(bc)); } else { busses.push_back(make_unique(bc)); - //busses.push_back(new BusPwm(bc)); } return busses.size(); } @@ -865,7 +822,7 @@ static String LEDTypesToJson(const std::vector& types) { return json; } -// credit @willmmiles & @netmindz https://github.com/wled-dev/WLED/pull/4056 +// credit @willmmiles & @netmindz https://github.com/wled/WLED/pull/4056 String BusManager::getLEDTypesJSONString() { String json = "["; json += LEDTypesToJson(BusDigital::getLEDTypes()); @@ -891,7 +848,6 @@ void BusManager::removeAll() { DEBUGBUS_PRINTLN(F("Removing all.")); //prevents crashes due to deleting busses while in use. while (!canAllShow()) yield(); - //for (auto &bus : busses) delete bus; // needed when not using std::unique_ptr C++ >11 busses.clear(); PolyBus::setParallelI2S1Output(false); } @@ -980,9 +936,8 @@ void BusManager::show() { void IRAM_ATTR BusManager::setPixelColor(unsigned pix, uint32_t c) { for (auto &bus : busses) { - unsigned bstart = bus->getStart(); - if (pix < bstart || pix >= bstart + bus->getLength()) continue; - bus->setPixelColor(pix - bstart, c); + if (!bus->containsPixel(pix)) continue; + bus->setPixelColor(pix - bus->getStart(), c); } } @@ -997,9 +952,8 @@ void BusManager::setSegmentCCT(int16_t cct, bool allowWBCorrection) { uint32_t BusManager::getPixelColor(unsigned pix) { for (auto &bus : busses) { - unsigned bstart = bus->getStart(); if (!bus->containsPixel(pix)) continue; - return bus->getPixelColor(pix - bstart); + return bus->getPixelColor(pix - bus->getStart()); } return 0; } @@ -1022,6 +976,5 @@ uint8_t Bus::_gAWM = 255; uint16_t BusDigital::_milliAmpsTotal = 0; std::vector> BusManager::busses; -//std::vector BusManager::busses; uint16_t BusManager::_gMilliAmpsUsed = 0; uint16_t BusManager::_gMilliAmpsMax = ABL_MILLIAMPS_DEFAULT; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 0570cc2d6..064b600a6 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -114,17 +114,17 @@ class Bus { _autoWhiteMode = Bus::hasWhite(type) ? aw : RGBW_MODE_MANUAL_ONLY; }; - virtual ~Bus() {} //throw the bus under the bus (derived class needs to freeData()) + virtual ~Bus() {} //throw the bus under the bus virtual void begin() {}; - virtual void show() = 0; + virtual void show() = 0; virtual bool canShow() const { return true; } virtual void setStatusPixel(uint32_t c) {} - virtual void setPixelColor(unsigned pix, uint32_t c) = 0; + virtual void 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 unsigned getPins(uint8_t* pinArray = nullptr) const { return 0; } + virtual size_t getPins(uint8_t* pinArray = nullptr) const { return 0; } virtual uint16_t getLength() const { return isOk() ? _len : 0; } virtual uint8_t getColorOrder() const { return COL_ORDER_RGB; } virtual unsigned skippedLeds() const { return 0; } @@ -132,7 +132,7 @@ class Bus { virtual uint16_t getLEDCurrent() const { return 0; } virtual uint16_t getUsedCurrent() const { return 0; } virtual uint16_t getMaxCurrent() const { return 0; } - virtual unsigned getBusSize() const { return sizeof(Bus); } + virtual size_t getBusSize() const { return sizeof(Bus); } inline bool hasRGB() const { return _hasRgb; } inline bool hasWhite() const { return _hasWhite; } @@ -148,7 +148,7 @@ class Bus { 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 unsigned getNumberOfChannels() const { return hasWhite() + 3*hasRGB() + hasCCT(); } + inline size_t getNumberOfChannels() const { return hasWhite() + 3*hasRGB() + hasCCT(); } inline uint16_t getStart() const { return _start; } inline uint8_t getType() const { return _type; } inline bool isOk() const { return _valid; } @@ -157,8 +157,8 @@ class Bus { 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 unsigned getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : is2Pin(type) + 1; } // credit @PaoloTK - static constexpr unsigned getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); } + static constexpr size_t getNumberOfPins(uint8_t type) { return isVirtual(type) ? 4 : isPWM(type) ? numPWMPins(type) : is2Pin(type) + 1; } // credit @PaoloTK + static constexpr size_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); } static constexpr bool hasRGB(uint8_t type) { return !((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF); } @@ -243,13 +243,13 @@ class BusDigital : public Bus { void setColorOrder(uint8_t colorOrder) override; [[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override; uint8_t getColorOrder() const override { return _colorOrder; } - unsigned getPins(uint8_t* pinArray = nullptr) const override; + size_t getPins(uint8_t* pinArray = nullptr) const override; unsigned skippedLeds() const override { return _skip; } uint16_t getFrequency() const override { return _frequencykHz; } uint16_t getLEDCurrent() const override { return _milliAmpsPerLed; } uint16_t getUsedCurrent() const override { return _milliAmpsTotal; } uint16_t getMaxCurrent() const override { return _milliAmpsMax; } - unsigned getBusSize() const override; + size_t getBusSize() const override; void begin() override; void cleanup(); @@ -263,7 +263,6 @@ class BusDigital : public Bus { uint16_t _frequencykHz; uint8_t _milliAmpsPerLed; uint16_t _milliAmpsMax; - uint8_t *_data; void *_busPtr; static uint16_t _milliAmpsTotal; // is overwitten/recalculated on each show() @@ -290,9 +289,9 @@ class BusPwm : public Bus { void setPixelColor(unsigned pix, uint32_t c) override; uint32_t getPixelColor(unsigned pix) const override; //does no index check - unsigned getPins(uint8_t* pinArray = nullptr) const override; + size_t getPins(uint8_t* pinArray = nullptr) const override; uint16_t getFrequency() const override { return _frequency; } - unsigned getBusSize() const override { return sizeof(BusPwm); } + size_t getBusSize() const override { return sizeof(BusPwm); } void show() override; inline void cleanup() { deallocatePins(); } @@ -318,8 +317,8 @@ class BusOnOff : public Bus { void setPixelColor(unsigned pix, uint32_t c) override; uint32_t getPixelColor(unsigned pix) const override; - unsigned getPins(uint8_t* pinArray) const override; - unsigned getBusSize() const override { return sizeof(BusOnOff); } + size_t getPins(uint8_t* pinArray) const override; + size_t getBusSize() const override { return sizeof(BusOnOff); } void show() override; inline void cleanup() { PinManager::deallocatePin(_pin, PinOwner::BusOnOff); } @@ -339,10 +338,10 @@ class BusNetwork : public Bus { bool canShow() const override { return !_broadcastLock; } // this should be a return value from UDP routine if it is still sending data out [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c) override; [[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override; - unsigned getPins(uint8_t* pinArray = nullptr) const override; - unsigned getBusSize() const override { return sizeof(BusNetwork) + (isOk() ? _len * _UDPchannels : 0); } - void show() override; - void cleanup(); + size_t getPins(uint8_t* pinArray = nullptr) const override; + size_t getBusSize() const override { return sizeof(BusNetwork) + (isOk() ? _len * _UDPchannels : 0); } + void show() override; + void cleanup(); static std::vector getLEDTypes(); @@ -367,11 +366,10 @@ struct BusConfig { 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) + BusConfig(uint8_t busType, uint8_t* ppins, uint16_t pstart, uint16_t len = 1, uint8_t pcolorOrder = COL_ORDER_GRB, bool rev = false, uint8_t skip = 0, byte aw=RGBW_MODE_MANUAL_ONLY, uint16_t clock_kHz=0U, uint8_t maPerLed=LED_MILLIAMPS_DEFAULT, uint16_t maMax=ABL_MILLIAMPS_DEFAULT) : count(std::max(len,(uint16_t)1)) , start(pstart) , colorOrder(pcolorOrder) @@ -379,7 +377,6 @@ struct BusConfig { , skipAmount(skip) , autoWhite(aw) , frequency(clock_kHz) - , doubleBuffer(dblBfr) , milliAmpsPerLed(maPerLed) , milliAmpsMax(maMax) { @@ -411,7 +408,7 @@ struct BusConfig { return true; } - unsigned memUsage(unsigned nr = 0) const; + size_t memUsage(unsigned nr = 0) const; }; diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index fa0397fc6..c9c110067 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -6,6 +6,35 @@ * The structure of the JSON is not to be considered an official API and may change without notice. */ +#ifndef PIXEL_COUNTS + #define PIXEL_COUNTS DEFAULT_LED_COUNT +#endif + +#ifndef DATA_PINS + #define DATA_PINS DEFAULT_LED_PIN +#endif + +#ifndef LED_TYPES + #define LED_TYPES DEFAULT_LED_TYPE +#endif + +#ifndef DEFAULT_LED_COLOR_ORDER + #define DEFAULT_LED_COLOR_ORDER COL_ORDER_GRB //default to GRB +#endif + +static constexpr unsigned sumPinsRequired(const unsigned* current, size_t count) { + return (count > 0) ? (Bus::getNumberOfPins(*current) + sumPinsRequired(current+1,count-1)) : 0; +} + +static constexpr bool validatePinsAndTypes(const unsigned* types, unsigned numTypes, unsigned numPins ) { + // Pins provided < pins required -> always invalid + // Pins provided = pins required -> always valid + // Pins provided > pins required -> valid if excess pins are a product of last type pins since it will be repeated + return (sumPinsRequired(types, numTypes) > numPins) ? false : + (numPins - sumPinsRequired(types, numTypes)) % Bus::getNumberOfPins(types[numTypes-1]) == 0; +} + + //simple macro for ArduinoJSON's or syntax #define CJSON(a,b) a = b | a @@ -20,7 +49,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { //long vid = doc[F("vid")]; // 2010020 -#if defined(ARDUINO_ARCH_ESP32) && defined(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 @@ -120,7 +149,6 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { uint8_t cctBlending = hw_led[F("cb")] | Bus::getCCTBlend(); Bus::setCCTBlend(cctBlending); strip.setTargetFps(hw_led["fps"]); //NOP if 0, default 42 FPS - CJSON(useGlobalLedBuffer, hw_led[F("ld")]); #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) CJSON(useParallelI2S, hw_led[F("prl")]); #endif @@ -130,12 +158,13 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonObject matrix = hw_led[F("matrix")]; if (!matrix.isNull()) { strip.isMatrix = true; - CJSON(strip.panels, matrix[F("mpc")]); + unsigned numPanels = matrix[F("mpc")] | 1; + numPanels = constrain(numPanels, 1, WLED_MAX_PANELS); strip.panel.clear(); JsonArray panels = matrix[F("panels")]; - int s = 0; + unsigned s = 0; if (!panels.isNull()) { - strip.panel.reserve(max(1U,min((size_t)strip.panels,(size_t)WLED_MAX_PANELS))); // pre-allocate memory for panels + strip.panel.reserve(numPanels); // pre-allocate default 8x8 panels for (JsonObject pnl : panels) { WS2812FX::Panel p; CJSON(p.bottomStart, pnl["b"]); @@ -147,30 +176,21 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(p.height, pnl["h"]); CJSON(p.width, pnl["w"]); strip.panel.push_back(p); - if (++s >= WLED_MAX_PANELS || s >= strip.panels) break; // max panels reached + if (++s >= numPanels) break; // max panels reached } - } else { - // fallback - WS2812FX::Panel p; - strip.panels = 1; - p.height = p.width = 8; - p.xOffset = p.yOffset = 0; - p.options = 0; - strip.panel.push_back(p); } - // cannot call strip.setUpMatrix() here due to already locked JSON buffer + strip.panel.shrink_to_fit(); // release unused memory (just in case) + // cannot call strip.deserializeLedmap()/strip.setUpMatrix() here due to already locked JSON buffer + //if (!fromFS) doInit2D = true; // if called at boot (fromFS==true), WLED::beginStrip() will take care of setting up matrix } #endif + DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), ESP.getFreeHeap()); JsonArray ins = hw_led["ins"]; - - if (fromFS || !ins.isNull()) { - DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), ESP.getFreeHeap()); + if (!ins.isNull()) { int s = 0; // bus iterator - if (fromFS) BusManager::removeAll(); // can't safely manipulate busses directly in network callback - for (JsonObject elm : ins) { - if (s >= WLED_MAX_BUSSES) break; + if (s >= WLED_MAX_BUSSES) break; // only counts physical buses uint8_t pins[5] = {255, 255, 255, 255, 255}; JsonArray pinArr = elm["pin"]; if (pinArr.size() == 0) continue; @@ -199,11 +219,101 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh - //busConfigs.push_back(std::move(BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, useGlobalLedBuffer, maPerLed, maMax))); - busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, useGlobalLedBuffer, maPerLed, maMax); + busConfigs.emplace_back(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, maPerLed, maMax); doInitBusses = true; // finalization done in beginStrip() if (!Bus::isVirtual(ledType)) s++; // have as many virtual buses as you want } + } else if (fromFS) { + //if busses failed to load, add default (fresh install, FS issue, ...) + BusManager::removeAll(); + busConfigs.clear(); + + DEBUG_PRINTLN(F("No busses, init default")); + constexpr unsigned defDataTypes[] = {LED_TYPES}; + constexpr unsigned defDataPins[] = {DATA_PINS}; + constexpr unsigned defCounts[] = {PIXEL_COUNTS}; + constexpr unsigned defNumTypes = (sizeof(defDataTypes) / sizeof(defDataTypes[0])); + constexpr unsigned defNumPins = (sizeof(defDataPins) / sizeof(defDataPins[0])); + constexpr unsigned defNumCounts = (sizeof(defCounts) / sizeof(defCounts[0])); + + static_assert(validatePinsAndTypes(defDataTypes, defNumTypes, defNumPins), + "The default pin list defined in DATA_PINS does not match the pin requirements for the default buses defined in LED_TYPES"); + + unsigned mem = 0; + unsigned pinsIndex = 0; + unsigned digitalCount = 0; + for (unsigned i = 0; i < WLED_MAX_BUSSES; i++) { + uint8_t defPin[OUTPUT_MAX_PINS]; + // if we have less types than requested outputs and they do not align, use last known type to set current type + unsigned dataType = defDataTypes[(i < defNumTypes) ? i : defNumTypes -1]; + unsigned busPins = Bus::getNumberOfPins(dataType); + + // if we need more pins than available all outputs have been configured + if (pinsIndex + busPins > defNumPins) break; + + // Assign all pins first so we can check for conflicts on this bus + for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) defPin[j] = defDataPins[pinsIndex + j]; + + for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) { + bool validPin = true; + // When booting without config (1st boot) we need to make sure GPIOs defined for LED output don't clash with hardware + // i.e. DEBUG (GPIO1), DMX (2), SPI RAM/FLASH (16&17 on ESP32-WROVER/PICO), read/only pins, etc. + // Pin should not be already allocated, read/only or defined for current bus + while (PinManager::isPinAllocated(defPin[j]) || !PinManager::isPinOk(defPin[j],true)) { + if (validPin) { + DEBUG_PRINTLN(F("Some of the provided pins cannot be used to configure this LED output.")); + defPin[j] = 1; // start with GPIO1 and work upwards + validPin = false; + } else if (defPin[j] < WLED_NUM_PINS) { + defPin[j]++; + } else { + DEBUG_PRINTLN(F("No available pins left! Can't configure output.")); + break; + } + // is the newly assigned pin already defined or used previously? + // try next in line until there are no clashes or we run out of pins + bool clash; + do { + clash = false; + // check for conflicts on current bus + for (const auto &pin : defPin) { + if (&pin != &defPin[j] && pin == defPin[j]) { + clash = true; + break; + } + } + // We already have a clash on current bus, no point checking next buses + if (!clash) { + // check for conflicts in defined pins + for (const auto &pin : defDataPins) { + if (pin == defPin[j]) { + clash = true; + break; + } + } + } + if (clash) defPin[j]++; + if (defPin[j] >= WLED_NUM_PINS) break; + } while (clash); + } + } + pinsIndex += busPins; + + // if we have less counts than pins and they do not align, use last known count to set current count + unsigned count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; + unsigned start = 0; + // analog always has length 1 + if (Bus::isPWM(dataType) || Bus::isOnOff(dataType)) count = 1; + BusConfig defCfg = BusConfig(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0); + mem += defCfg.memUsage(Bus::isDigital(dataType) && !Bus::is2Pin(dataType) ? digitalCount++ : 0); + if (mem > MAX_LED_MEMORY) { + DEBUG_PRINTF_P(PSTR("Out of LED memory! Bus %d (%d) #%u not created."), (int)dataType, (int)count, digitalCount); + break; + } + busConfigs.push_back(defCfg); // use push_back for simplification as we needed defCfg to calculate memory usage + doInitBusses = true; // finalization done in beginStrip() + } + DEBUG_PRINTF_P(PSTR("LED buffer size: %uB/%uB\n"), mem, BusManager::memUsage()); } if (hw_led["rev"] && BusManager::getNumBusses()) BusManager::getBus(0)->setReversed(true); //set 0.11 global reversed setting for first bus @@ -292,30 +402,28 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { macroLongPress[s] = 0; macroDoublePress[s] = 0; } - } else { + } else if (fromFS) { // new install/missing configuration (button 0 has defaults) - if (fromFS) { - // relies upon only being called once with fromFS == true, which is currently true. - for (size_t s = 0; s < WLED_MAX_BUTTONS; s++) { - if (buttonType[s] == BTN_TYPE_NONE || btnPin[s] < 0 || !PinManager::allocatePin(btnPin[s], false, PinOwner::Button)) { - btnPin[s] = -1; - buttonType[s] = BTN_TYPE_NONE; - } - if (btnPin[s] >= 0) { - if (disablePullUp) { - pinMode(btnPin[s], INPUT); - } else { - #ifdef ESP32 - pinMode(btnPin[s], buttonType[s]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP); - #else - pinMode(btnPin[s], INPUT_PULLUP); - #endif - } - } - macroButton[s] = 0; - macroLongPress[s] = 0; - macroDoublePress[s] = 0; + // relies upon only being called once with fromFS == true, which is currently true. + for (size_t s = 0; s < WLED_MAX_BUTTONS; s++) { + if (buttonType[s] == BTN_TYPE_NONE || btnPin[s] < 0 || !PinManager::allocatePin(btnPin[s], false, PinOwner::Button)) { + btnPin[s] = -1; + buttonType[s] = BTN_TYPE_NONE; } + if (btnPin[s] >= 0) { + if (disablePullUp) { + pinMode(btnPin[s], INPUT); + } else { + #ifdef ESP32 + pinMode(btnPin[s], buttonType[s]==BTN_TYPE_PUSH_ACT_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP); + #else + pinMode(btnPin[s], INPUT_PULLUP); + #endif + } + } + macroButton[s] = 0; + macroLongPress[s] = 0; + macroDoublePress[s] = 0; } } @@ -392,8 +500,9 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonObject light = doc[F("light")]; CJSON(briMultiplier, light[F("scale-bri")]); - CJSON(strip.paletteBlend, light[F("pal-mode")]); + CJSON(paletteBlend, light[F("pal-mode")]); CJSON(strip.autoSegments, light[F("aseg")]); + CJSON(useRainbowWheel, light[F("rw")]); CJSON(gammaCorrectVal, light["gc"]["val"]); // default 2.8 float light_gc_bri = light["gc"]["bri"]; @@ -648,11 +757,10 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { static const char s_cfg_json[] PROGMEM = "/cfg.json"; void deserializeConfigFromFS() { - bool success = deserializeConfigSec(); + [[maybe_unused]] bool success = deserializeConfigSec(); #ifdef WLED_ADD_EEPROM_SUPPORT if (!success) { //if file does not exist, try reading from EEPROM deEEPSettings(); - return; } #endif @@ -661,23 +769,6 @@ void deserializeConfigFromFS() { DEBUG_PRINTLN(F("Reading settings from /cfg.json...")); success = readObjectFromFile(s_cfg_json, nullptr, pDoc); - if (!success) { // if file does not exist, optionally try reading from EEPROM and then save defaults to FS - releaseJSONBufferLock(); - #ifdef WLED_ADD_EEPROM_SUPPORT - deEEPSettings(); - #endif - - // save default values to /cfg.json - // call readFromConfig() with an empty object so that usermods can initialize to defaults prior to saving - JsonObject empty = JsonObject(); - UsermodManager::readFromConfig(empty); - serializeConfigToFS(); - // init Ethernet (in case default type is set at compile time) - #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) - initEthernet(); - #endif - return; - } // NOTE: This routine deserializes *and* applies the configuration // Therefore, must also initialize ethernet from this function @@ -800,14 +891,13 @@ void serializeConfig(JsonObject root) { JsonObject hw_led = hw.createNestedObject("led"); hw_led[F("total")] = strip.getLengthTotal(); //provided for compatibility on downgrade and per-output ABL hw_led[F("maxpwr")] = BusManager::ablMilliampsMax(); - hw_led[F("ledma")] = 0; // no longer used +// hw_led[F("ledma")] = 0; // no longer used hw_led["cct"] = strip.correctWB; hw_led[F("cr")] = strip.cctFromRgb; hw_led[F("ic")] = cctICused; hw_led[F("cb")] = Bus::getCCTBlend(); hw_led["fps"] = strip.getTargetFps(); hw_led[F("rgbwm")] = Bus::getGlobalAWMode(); // global auto white mode override - hw_led[F("ld")] = useGlobalLedBuffer; #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) hw_led[F("prl")] = BusManager::hasParallelOutput(); #endif @@ -816,7 +906,7 @@ void serializeConfig(JsonObject root) { // 2D Matrix Settings if (strip.isMatrix) { JsonObject matrix = hw_led.createNestedObject(F("matrix")); - matrix[F("mpc")] = strip.panels; + matrix[F("mpc")] = strip.panel.size(); JsonArray panels = matrix.createNestedArray(F("panels")); for (size_t i = 0; i < strip.panel.size(); i++) { JsonObject pnl = panels.createNestedObject(); @@ -926,8 +1016,9 @@ void serializeConfig(JsonObject root) { JsonObject light = root.createNestedObject(F("light")); light[F("scale-bri")] = briMultiplier; - light[F("pal-mode")] = strip.paletteBlend; + light[F("pal-mode")] = paletteBlend; light[F("aseg")] = strip.autoSegments; + light[F("rw")] = useRainbowWheel; JsonObject light_gc = light.createNestedObject("gc"); light_gc["bri"] = (gammaCorrectBri) ? gammaCorrectVal : 1.0f; // keep compatibility diff --git a/wled00/colors.cpp b/wled00/colors.cpp index ba38d5a15..3b3d9ab70 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -208,14 +208,14 @@ CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette) makepastelpalette = true; } - // apply saturation & gamma correction + // apply saturation CRGB RGBpalettecolors[4]; for (int i = 0; i < 4; i++) { if (makepastelpalette && palettecolors[i].saturation > 180) { palettecolors[i].saturation -= 160; //desaturate all four colors } RGBpalettecolors[i] = (CRGB)palettecolors[i]; //convert to RGB - RGBpalettecolors[i] = gamma32(((uint32_t)RGBpalettecolors[i]) & 0x00FFFFFFU); //strip alpha from CRGB + RGBpalettecolors[i] = ((uint32_t)RGBpalettecolors[i]) & 0x00FFFFFFU; //strip alpha from CRGB } return CRGBPalette16(RGBpalettecolors[0], @@ -232,6 +232,54 @@ CRGBPalette16 generateRandomPalette() // generate fully random palette CHSV(hw_random8(), hw_random8(160, 255), hw_random8(128, 255))); } +void loadCustomPalettes() { + byte tcp[72]; //support gradient palettes with up to 18 entries + CRGBPalette16 targetPalette; + customPalettes.clear(); // start fresh + for (int index = 0; index<10; index++) { + char fileName[32]; + sprintf_P(fileName, PSTR("/palette%d.json"), index); + + StaticJsonDocument<1536> pDoc; // barely enough to fit 72 numbers + if (WLED_FS.exists(fileName)) { + DEBUGFX_PRINTF_P(PSTR("Reading palette from %s\n"), fileName); + if (readObjectFromFile(fileName, nullptr, &pDoc)) { + JsonArray pal = pDoc[F("palette")]; + if (!pal.isNull() && pal.size()>3) { // not an empty palette (at least 2 entries) + memset(tcp, 255, sizeof(tcp)); + if (pal[0].is() && pal[1].is()) { + // we have an array of index & hex strings + size_t palSize = MIN(pal.size(), 36); + palSize -= palSize % 2; // make sure size is multiple of 2 + for (size_t i=0, j=0; i()<256; i+=2) { + uint8_t rgbw[] = {0,0,0,0}; + if (colorFromHexString(rgbw, pal[i+1].as())) { // will catch non-string entires + tcp[ j ] = (uint8_t) pal[ i ].as(); // index + for (size_t c=0; c<3; c++) tcp[j+1+c] = rgbw[c]; // only use RGB component + DEBUGFX_PRINTF_P(PSTR("%2u -> %3d [%3d,%3d,%3d]\n"), i, int(tcp[j]), int(tcp[j+1]), int(tcp[j+2]), int(tcp[j+3])); + j += 4; + } + } + } else { + size_t palSize = MIN(pal.size(), 72); + palSize -= palSize % 4; // make sure size is multiple of 4 + for (size_t i=0; i()<256; i+=4) { + tcp[ i ] = (uint8_t) pal[ i ].as(); // index + for (size_t c=0; c<3; c++) tcp[i+1+c] = (uint8_t) pal[i+1+c].as(); + DEBUGFX_PRINTF_P(PSTR("%2u -> %3d [%3d,%3d,%3d]\n"), i, int(tcp[i]), int(tcp[i+1]), int(tcp[i+2]), int(tcp[i+3])); + } + } + customPalettes.push_back(targetPalette.loadDynamicGradientPalette(tcp)); + } else { + DEBUGFX_PRINTLN(F("Wrong palette format.")); + } + } + } else { + break; + } + } +} + void hsv2rgb(const CHSV32& hsv, uint32_t& rgb) // convert HSV (16bit hue) to RGB (32bit with white = 0) { unsigned int remainder, region, p, q, t; diff --git a/wled00/const.h b/wled00/const.h index 2b460f3f1..877e02dc1 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -44,67 +44,48 @@ #endif #endif -#ifndef WLED_MAX_BUSSES - #ifdef ESP8266 - #define WLED_MAX_DIGITAL_CHANNELS 3 - #define WLED_MAX_ANALOG_CHANNELS 5 - #define WLED_MAX_BUSSES 4 // will allow 3 digital & 1 analog RGB - #define WLED_MIN_VIRTUAL_BUSSES 3 // no longer used for bus creation but used to distinguish S2/S3 in UI - #else - #define WLED_MAX_ANALOG_CHANNELS (LEDC_CHANNEL_MAX*LEDC_SPEED_MODE_MAX) - #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM - #define WLED_MAX_BUSSES 6 // will allow 2 digital & 2 analog RGB or 6 PWM white - #define WLED_MAX_DIGITAL_CHANNELS 2 - //#define WLED_MAX_ANALOG_CHANNELS 6 - #define WLED_MIN_VIRTUAL_BUSSES 4 // no longer used for bus creation but used to distinguish S2/S3 in UI - #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB - // the 5th bus (I2S) will prevent Audioreactive usermod from functioning (it is last used though) - #define WLED_MAX_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 4 // no longer used for bus creation but used to distinguish S2/S3 in UI - #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, 8 LEDC, has 2 I2S but NPB supports parallel x8 LCD on I2S1 - #define WLED_MAX_BUSSES 14 // will allow 12 digital & 2 analog RGB - #define WLED_MAX_DIGITAL_CHANNELS 12 // x4 RMT + x8 I2S-LCD - //#define WLED_MAX_ANALOG_CHANNELS 8 - #define WLED_MIN_VIRTUAL_BUSSES 6 // no longer used for bus creation but used to distinguish S2/S3 in UI - #else - // the last digital bus (I2S0) will prevent Audioreactive usermod from functioning - #define WLED_MAX_BUSSES 19 // will allow 16 digital & 3 analog RGB - #define WLED_MAX_DIGITAL_CHANNELS 16 // x1/x8 I2S1 + x8 RMT - //#define WLED_MAX_ANALOG_CHANNELS 16 - #define WLED_MIN_VIRTUAL_BUSSES 6 // no longer used for bus creation but used to distinguish S2/S3 in UI - #endif - #endif +#ifdef ESP8266 + #define WLED_MAX_DIGITAL_CHANNELS 3 + #define WLED_MAX_ANALOG_CHANNELS 5 + #define WLED_MIN_VIRTUAL_BUSSES 3 // no longer used for bus creation but used to distinguish S2/S3 in UI #else - #ifdef ESP8266 - #if WLED_MAX_BUSSES > 5 - #error Maximum number of buses is 5. - #endif - #ifndef WLED_MAX_ANALOG_CHANNELS - #error You must also define WLED_MAX_ANALOG_CHANNELS. - #endif - #ifndef WLED_MAX_DIGITAL_CHANNELS - #error You must also define WLED_MAX_DIGITAL_CHANNELS. - #endif - #define WLED_MIN_VIRTUAL_BUSSES 3 + #define WLED_MAX_ANALOG_CHANNELS (LEDC_CHANNEL_MAX*LEDC_SPEED_MODE_MAX) + #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM + #define WLED_MAX_DIGITAL_CHANNELS 2 + //#define WLED_MAX_ANALOG_CHANNELS 6 + #define WLED_MIN_VIRTUAL_BUSSES 4 // no longer used for bus creation but used to distinguish S2/S3 in UI + #elif defined(CONFIG_IDF_TARGET_ESP32S2) // 4 RMT, 8 LEDC, only has 1 I2S bus, supported in NPB + // the 5th bus (I2S) will prevent Audioreactive usermod from functioning (it is last used though) + #define WLED_MAX_DIGITAL_CHANNELS 12 // x4 RMT + x1/x8 I2S0 + //#define WLED_MAX_ANALOG_CHANNELS 8 + #define WLED_MIN_VIRTUAL_BUSSES 4 // no longer used for bus creation but used to distinguish S2/S3 in UI + #elif defined(CONFIG_IDF_TARGET_ESP32S3) // 4 RMT, 8 LEDC, has 2 I2S but NPB supports parallel x8 LCD on I2S1 + #define WLED_MAX_DIGITAL_CHANNELS 12 // x4 RMT + x8 I2S-LCD + //#define WLED_MAX_ANALOG_CHANNELS 8 + #define WLED_MIN_VIRTUAL_BUSSES 6 // no longer used for bus creation but used to distinguish S2/S3 in UI #else - #if WLED_MAX_BUSSES > 20 - #error Maximum number of buses is 20. - #endif - #ifndef WLED_MAX_ANALOG_CHANNELS - #error You must also define WLED_MAX_ANALOG_CHANNELS. - #endif - #ifndef WLED_MAX_DIGITAL_CHANNELS - #error You must also define WLED_MAX_DIGITAL_CHANNELS. - #endif - #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) - #define WLED_MIN_VIRTUAL_BUSSES 4 - #else - #define WLED_MIN_VIRTUAL_BUSSES 6 - #endif + // the last digital bus (I2S0) will prevent Audioreactive usermod from functioning + #define WLED_MAX_DIGITAL_CHANNELS 16 // x1/x8 I2S1 + x8 RMT + //#define WLED_MAX_ANALOG_CHANNELS 16 + #define WLED_MIN_VIRTUAL_BUSSES 6 // no longer used for bus creation but used to distinguish S2/S3 in UI #endif #endif +// WLED_MAX_BUSSES was used to define the size of busses[] array which is no longer needed +// instead it will help determine max number of buses that can be defined at compile time +#ifdef WLED_MAX_BUSSES + #undef WLED_MAX_BUSSES +#endif +#define WLED_MAX_BUSSES (WLED_MAX_DIGITAL_CHANNELS+WLED_MAX_ANALOG_CHANNELS) + +// Maximum number of pins per output. 5 for RGBCCT analog LEDs. +#define OUTPUT_MAX_PINS 5 + +// for pin manager +#ifdef ESP8266 +#define WLED_NUM_PINS (GPIO_PIN_COUNT+1) // somehow they forgot GPIO 16 (0-16==17) +#else +#define WLED_NUM_PINS (GPIO_PIN_COUNT) +#endif #ifndef WLED_MAX_BUTTONS #ifdef ESP8266 @@ -151,6 +132,8 @@ #endif #endif +#define WLED_MAX_PANELS 18 // must not be more than 32 + //Usermod IDs #define USERMOD_ID_RESERVED 0 //Unused. Might indicate no usermod present #define USERMOD_ID_UNSPECIFIED 1 //Default value for a general user mod that does not specify a custom ID @@ -336,18 +319,6 @@ #define TYPE_NET_ARTNET_RGBW 89 //network ArtNet RGB bus (master broadcast bus, unused) #define TYPE_VIRTUAL_MAX 95 -/* -// old macros that have been moved to Bus class -#define IS_TYPE_VALID(t) ((t) > 15 && (t) < 128) -#define IS_DIGITAL(t) (((t) > 15 && (t) < 40) || ((t) > 47 && (t) < 64)) //digital are 16-39 and 48-63 -#define IS_2PIN(t) ((t) > 47 && (t) < 64) -#define IS_16BIT(t) ((t) == TYPE_UCS8903 || (t) == TYPE_UCS8904) -#define IS_ONOFF(t) ((t) == 40) -#define IS_PWM(t) ((t) > 40 && (t) < 46) //does not include on/Off type -#define NUM_PWM_PINS(t) ((t) - 40) //for analog PWM 41-45 only -#define IS_VIRTUAL(t) ((t) >= 80 && (t) < 96) //this was a poor choice a better would be 96-111 -*/ - //Color orders #define COL_ORDER_GRB 0 //GRB(w),defaut #define COL_ORDER_RGB 1 //common for WS2811 @@ -435,6 +406,7 @@ #define ERR_CONCURRENCY 2 // Conurrency (client active) #define ERR_NOBUF 3 // JSON buffer was not released in time, request cannot be handled at this time #define ERR_NOT_IMPL 4 // Not implemented +#define ERR_NORAM_PX 7 // not enough RAM for pixels #define ERR_NORAM 8 // effect RAM depleted #define ERR_JSON 9 // JSON parsing failed (input too large?) #define ERR_FS_BEGIN 10 // Could not init filesystem (no partition?) @@ -474,30 +446,29 @@ #define NTP_PACKET_SIZE 48 // size of NTP receive buffer #define NTP_MIN_PACKET_SIZE 48 // min expected size - NTP v4 allows for "extended information" appended to the standard fields -// Maximum number of pins per output. 5 for RGBCCT analog LEDs. -#define OUTPUT_MAX_PINS 5 - //maximum number of rendered LEDs - this does not have to match max. physical LEDs, e.g. if there are virtual busses #ifndef MAX_LEDS -#ifdef ESP8266 -#define MAX_LEDS 1664 //can't rely on memory limit to limit this to 1600 LEDs -#elif defined(CONFIG_IDF_TARGET_ESP32S2) -#define MAX_LEDS 2048 //due to memory constraints -#else -#define MAX_LEDS 8192 -#endif + #ifdef ESP8266 + #define MAX_LEDS 1536 //can't rely on memory limit to limit this to 1536 LEDs + #elif defined(CONFIG_IDF_TARGET_ESP32S2) + #define MAX_LEDS 2048 //due to memory constraints S2 + #elif defined(CONFIG_IDF_TARGET_ESP32C3) + #define MAX_LEDS 4096 + #else + #define MAX_LEDS 16384 + #endif #endif #ifndef MAX_LED_MEMORY #ifdef ESP8266 - #define MAX_LED_MEMORY 4000 + #define MAX_LED_MEMORY 4096 #else #if defined(ARDUINO_ARCH_ESP32S2) - #define MAX_LED_MEMORY 16000 + #define MAX_LED_MEMORY 16384 #elif defined(ARDUINO_ARCH_ESP32C3) - #define MAX_LED_MEMORY 32000 + #define MAX_LED_MEMORY 32768 #else - #define MAX_LED_MEMORY 64000 + #define MAX_LED_MEMORY 65536 #endif #endif #endif diff --git a/wled00/data/index.css b/wled00/data/index.css index 31e2daa92..c92c884ab 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -353,12 +353,12 @@ button { padding: 4px 0 0; } -#segutil, #segutil2, #segcont, #putil, #pcont, #pql, #fx, #palw, +#segutil, #segutil2, #segcont, #putil, #pcont, #pql, #fx, #palw, #bsp, .fnd { max-width: 280px; } -#putil, #segutil, #segutil2 { +#putil, #segutil, #segutil2, #bsp { min-height: 42px; margin: 0 auto; } diff --git a/wled00/data/index.htm b/wled00/data/index.htm index c55c98373..3716f7ccd 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -268,28 +268,28 @@

Transition:  s

-

Blend: - -

+

@@ -363,7 +363,7 @@ diff --git a/wled00/data/index.js b/wled00/data/index.js index 147abf389..295a3403b 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -35,9 +35,10 @@ var cfg = { // [year, month (0 -> January, 11 -> December), day, duration in days, image url] var hol = [ [0, 11, 24, 4, "https://aircoookie.github.io/xmas.png"], // christmas - [0, 2, 17, 1, "https://images.alphacoders.com/491/491123.jpg"], // st. Patrick's day - [2025, 3, 20, 2, "https://aircoookie.github.io/easter.png"], // easter 2025 - [2024, 2, 31, 2, "https://aircoookie.github.io/easter.png"], // easter 2024 + [0, 2, 17, 1, "https://images.alphacoders.com/491/491123.jpg"], // st. Patrick's day + [2026, 3, 5, 2, "https://aircoookie.github.io/easter.png"], // easter 2026 + [2027, 2, 28, 2, "https://aircoookie.github.io/easter.png"], // easter 2027 + //[2028, 3, 16, 2, "https://aircoookie.github.io/easter.png"], // easter 2028 [0, 6, 4, 1, "https://images.alphacoders.com/516/516792.jpg"], // 4th of July [0, 0, 1, 1, "https://images.alphacoders.com/119/1198800.jpg"] // new year ]; @@ -57,7 +58,7 @@ function handleVisibilityChange() {if (!d.hidden && new Date () - lastUpdate > 3 function sCol(na, col) {d.documentElement.style.setProperty(na, col);} function gId(c) {return d.getElementById(c);} function gEBCN(c) {return d.getElementsByClassName(c);} -function isEmpty(o) {return Object.keys(o).length === 0;} +function isEmpty(o) {for (const i in o) return false; return true;} function isObj(i) {return (i && typeof i === 'object' && !Array.isArray(i));} function isNumeric(n) {return !isNaN(parseFloat(n)) && isFinite(n);} @@ -805,6 +806,26 @@ function populateSegments(s) ``+ ``+ ``; + let blend = `
Blend mode
`+ + `
`+ + `
`; let sndSim = `
Sound sim
`+ `
d.getElementsByName("MA"+i)[0].value = v.maxpwr; }); d.getElementsByName("PR")[0].checked = l.prl | 0; - d.getElementsByName("LD")[0].checked = l.ld; d.getElementsByName("MA")[0].value = l.maxpwr; d.getElementsByName("ABL")[0].checked = l.maxpwr > 0; } @@ -823,7 +821,6 @@ Swap:
Make a segment for each output:
Custom bus start indices:
- Use global LED buffer:

Color Order Override: @@ -866,7 +863,6 @@ Swap: ms
Random Cycle Palette Time: s
- Use harmonic Random Cycle Palette:

Timed light

Default duration: min
Default target brightness:
@@ -903,8 +899,10 @@ Swap:
+ Use harmonic Random Cycle palette:
+ Use "rainbow" color wheel:
Target refresh rate: FPS - +
diff --git a/wled00/e131.cpp b/wled00/e131.cpp index c16ed9332..98cfe28fb 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -38,8 +38,7 @@ void handleDDPPacket(e131_packet_t* p) { if (realtimeMode != REALTIME_MODE_DDP) ddpSeenPush = false; // just starting, no push yet realtimeLock(realtimeTimeoutMs, REALTIME_MODE_DDP); - if (!realtimeOverride || (realtimeMode && useMainSegmentOnly)) { - if (useMainSegmentOnly) strip.getMainSegment().beginDraw(); + if (!realtimeOverride) { for (unsigned i = start; i < stop; i++, c += ddpChannelsPerLed) { setRealtimePixel(i, data[c], data[c+1], data[c+2], ddpChannelsPerLed >3 ? data[c+3] : 0); } @@ -150,10 +149,9 @@ void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8 realtimeLock(realtimeTimeoutMs, mde); - if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; + if (realtimeOverride) return; wChannel = (availDMXLen > 3) ? e131_data[dataOffset+3] : 0; - 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; @@ -163,7 +161,7 @@ void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8 if (availDMXLen < 4) return; realtimeLock(realtimeTimeoutMs, mde); - if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; + if (realtimeOverride) return; wChannel = (availDMXLen > 4) ? e131_data[dataOffset+4] : 0; if (bri != e131_data[dataOffset+0]) { @@ -171,7 +169,6 @@ void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8 strip.setBrightness(bri, true); } - 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; @@ -228,16 +225,16 @@ void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8 if (e131_data[dataOffset+3] != seg.intensity) seg.intensity = e131_data[dataOffset+3]; if (e131_data[dataOffset+4] != seg.palette) seg.setPalette(e131_data[dataOffset+4]); - if ((e131_data[dataOffset+5] & 0b00000010) != seg.reverse_y) { seg.setOption(SEG_OPTION_REVERSED_Y, e131_data[dataOffset+5] & 0b00000010); } - if ((e131_data[dataOffset+5] & 0b00000100) != seg.mirror_y) { seg.setOption(SEG_OPTION_MIRROR_Y, e131_data[dataOffset+5] & 0b00000100); } - if ((e131_data[dataOffset+5] & 0b00001000) != seg.transpose) { seg.setOption(SEG_OPTION_TRANSPOSED, e131_data[dataOffset+5] & 0b00001000); } - if ((e131_data[dataOffset+5] & 0b00110000) / 8 != seg.map1D2D) { - seg.map1D2D = (e131_data[dataOffset+5] & 0b00110000) / 8; + if (bool(e131_data[dataOffset+5] & 0b00000010) != seg.reverse_y) { seg.reverse_y = bool(e131_data[dataOffset+5] & 0b00000010); } + if (bool(e131_data[dataOffset+5] & 0b00000100) != seg.mirror_y) { seg.mirror_y = bool(e131_data[dataOffset+5] & 0b00000100); } + if (bool(e131_data[dataOffset+5] & 0b00001000) != seg.transpose) { seg.transpose = bool(e131_data[dataOffset+5] & 0b00001000); } + if ((e131_data[dataOffset+5] & 0b00110000) >> 4 != seg.map1D2D) { + seg.map1D2D = (e131_data[dataOffset+5] & 0b00110000) >> 4; } // To maintain backwards compatibility with prior e1.31 values, reverse is fixed to mask 0x01000000 - if ((e131_data[dataOffset+5] & 0b01000000) != seg.reverse) { seg.setOption(SEG_OPTION_REVERSED, e131_data[dataOffset+5] & 0b01000000); } + if ((e131_data[dataOffset+5] & 0b01000000) != seg.reverse) { seg.reverse = bool(e131_data[dataOffset+5] & 0b01000000); } // To maintain backwards compatibility with prior e1.31 values, mirror is fixed to mask 0x10000000 - if ((e131_data[dataOffset+5] & 0b10000000) != seg.mirror) { seg.setOption(SEG_OPTION_MIRROR, e131_data[dataOffset+5] & 0b10000000); } + if ((e131_data[dataOffset+5] & 0b10000000) != seg.mirror) { seg.mirror = bool(e131_data[dataOffset+5] & 0b10000000); } uint32_t colors[3]; byte whites[3] = {0,0,0}; @@ -271,7 +268,7 @@ void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8 case DMX_MODE_MULTIPLE_RGB: case DMX_MODE_MULTIPLE_RGBW: { - bool is4Chan = (DMXMode == DMX_MODE_MULTIPLE_RGBW); + const bool is4Chan = (DMXMode == DMX_MODE_MULTIPLE_RGBW); const unsigned dmxChannelsPerLed = is4Chan ? 4 : 3; const unsigned ledsPerUniverse = is4Chan ? MAX_4_CH_LEDS_PER_UNIVERSE : MAX_3_CH_LEDS_PER_UNIVERSE; uint8_t stripBrightness = bri; @@ -303,7 +300,7 @@ void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8 } realtimeLock(realtimeTimeoutMs, mde); - if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; + if (realtimeOverride) return; if (ledsTotal > totalLen) { ledsTotal = totalLen; @@ -316,17 +313,9 @@ void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8 } } - if (useMainSegmentOnly) strip.getMainSegment().beginDraw(); - if (!is4Chan) { - for (unsigned i = previousLeds; i < ledsTotal; i++) { - setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], 0); - dmxOffset+=3; - } - } else { - for (unsigned i = previousLeds; i < ledsTotal; i++) { - setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], e131_data[dmxOffset+3]); - dmxOffset+=4; - } + for (unsigned i = previousLeds; i < ledsTotal; i++) { + setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], is4Chan ? e131_data[dmxOffset+3] : 0); + dmxOffset += dmxChannelsPerLed; } break; } @@ -529,7 +518,7 @@ void sendArtnetPollReply(ArtPollReply *reply, IPAddress ipAddress, uint16_t port reply->reply_sub_sw = (uint8_t)((portAddress >> 4) & 0x000F); reply->reply_sw_out[0] = (uint8_t)(portAddress & 0x000F); - snprintf_P((char *)reply->reply_node_report, sizeof(reply->reply_node_report)-1, PSTR("#0001 [%04u] OK - WLED v" TOSTRING(WLED_VERSION)), pollReplyCount); + snprintf_P((char *)reply->reply_node_report, sizeof(reply->reply_node_report)-1, PSTR("#0001 [%04u] OK - WLED v%s"), pollReplyCount, versionString); if (pollReplyCount < 9999) { pollReplyCount++; diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index ff2443648..2846c3804 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -172,6 +172,8 @@ inline uint32_t color_blend16(uint32_t c1, uint32_t c2, uint16_t b) { return col [[gnu::hot, gnu::pure]] uint32_t ColorFromPaletteWLED(const CRGBPalette16 &pal, unsigned index, uint8_t brightness = (uint8_t)255U, TBlendType blendType = LINEARBLEND); CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette); CRGBPalette16 generateRandomPalette(); +void loadCustomPalettes(); +#define getPaletteCount() (13 + GRADIENT_PALETTE_COUNT + customPalettes.size()) inline uint32_t colorFromRgbw(byte* rgbw) { return uint32_t((byte(rgbw[3]) << 24) | (byte(rgbw[0]) << 16) | (byte(rgbw[1]) << 8) | (byte(rgbw[2]))); } void hsv2rgb(const CHSV32& hsv, uint32_t& rgb); void colorHStoRGB(uint16_t hue, byte sat, byte* rgb); @@ -490,11 +492,11 @@ void userLoop(); #define inoise8 perlin8 // fastled legacy alias #define inoise16 perlin16 // fastled legacy alias #define hex2int(a) (((a)>='0' && (a)<='9') ? (a)-'0' : ((a)>='A' && (a)<='F') ? (a)-'A'+10 : ((a)>='a' && (a)<='f') ? (a)-'a'+10 : 0) -[[gnu::pure]] int getNumVal(const String* req, uint16_t pos); -void parseNumber(const char* str, byte* val, byte minv=0, byte maxv=255); -bool getVal(JsonVariant elem, byte* val, byte vmin=0, byte vmax=255); // getVal supports inc/decrementing and random ("X~Y(r|[w]~[-][Z])" form) +[[gnu::pure]] int getNumVal(const String &req, uint16_t pos); +void parseNumber(const char* str, byte &val, byte minv=0, byte maxv=255); +bool getVal(JsonVariant elem, byte &val, byte vmin=0, byte vmax=255); // getVal supports inc/decrementing and random ("X~Y(r|[w]~[-][Z])" form) [[gnu::pure]] bool getBoolVal(const JsonVariant &elem, bool dflt); -bool updateVal(const char* req, const char* key, byte* val, byte minv=0, byte maxv=255); +bool updateVal(const char* req, const char* key, byte &val, byte minv=0, byte maxv=255); size_t printSetFormCheckbox(Print& settingsScript, const char* key, int val); size_t printSetFormValue(Print& settingsScript, const char* key, int val); size_t printSetFormValue(Print& settingsScript, const char* key, const char* val); @@ -544,6 +546,27 @@ inline uint8_t hw_random8() { return HW_RND_REGISTER; }; inline uint8_t hw_random8(uint32_t upperlimit) { return (hw_random8() * upperlimit) >> 8; }; // input range 0-255 inline uint8_t hw_random8(uint32_t lowerlimit, uint32_t upperlimit) { uint32_t range = upperlimit - lowerlimit; return lowerlimit + hw_random8(range); }; // input range 0-255 +// PSRAM allocation wrappers +#ifndef ESP8266 +void *w_malloc(size_t); // prefer PSRAM over DRAM +void *w_calloc(size_t, size_t); // prefer PSRAM over DRAM +void *w_realloc(void *, size_t); // prefer PSRAM over DRAM +inline void w_free(void *ptr) { heap_caps_free(ptr); } +void *d_malloc(size_t); // prefer DRAM over PSRAM +void *d_calloc(size_t, size_t); // prefer DRAM over PSRAM +void *d_realloc(void *, size_t); // prefer DRAM over PSRAM +inline void d_free(void *ptr) { heap_caps_free(ptr); } +#else +#define w_malloc malloc +#define w_calloc calloc +#define w_realloc realloc +#define w_free free +#define d_malloc malloc +#define d_calloc calloc +#define d_realloc realloc +#define d_free free +#endif + // RAII guard class for the JSON Buffer lock // Modeled after std::lock_guard class JSONBufferGuard { diff --git a/wled00/file.cpp b/wled00/file.cpp index d390207d4..4df331997 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -39,7 +39,7 @@ void closeFile() { uint32_t s = millis(); #endif f.close(); - DEBUGFS_PRINTF("took %d ms\n", millis() - s); + DEBUGFS_PRINTF("took %lu ms\n", millis() - s); doCloseFile = false; } @@ -69,14 +69,14 @@ static bool bufferedFind(const char *target, bool fromStart = true) { if(buf[count] == target[index]) { if(++index >= targetLen) { // return true if all chars in the target match f.seek((f.position() - bufsize) + count +1); - DEBUGFS_PRINTF("Found at pos %d, took %d ms", f.position(), millis() - s); + DEBUGFS_PRINTF("Found at pos %d, took %lu ms", f.position(), millis() - s); return true; } } count++; } } - DEBUGFS_PRINTF("No match, took %d ms\n", millis() - s); + DEBUGFS_PRINTF("No match, took %lu ms\n", millis() - s); return false; } @@ -111,7 +111,7 @@ static bool bufferedFindSpace(size_t targetLen, bool fromStart = true) { f.seek((f.position() - bufsize) + count +1 - targetLen); knownLargestSpace = MAX_SPACE; //there may be larger spaces after, so we don't know } - DEBUGFS_PRINTF("Found at pos %d, took %d ms", f.position(), millis() - s); + DEBUGFS_PRINTF("Found at pos %d, took %lu ms", f.position(), millis() - s); return true; } } else { @@ -125,7 +125,7 @@ static bool bufferedFindSpace(size_t targetLen, bool fromStart = true) { count++; } } - DEBUGFS_PRINTF("No match, took %d ms\n", millis() - s); + DEBUGFS_PRINTF("No match, took %lu ms\n", millis() - s); return false; } @@ -151,13 +151,13 @@ static bool bufferedFindObjectEnd() { if (buf[count] == '}') objDepth--; if (objDepth == 0) { f.seek((f.position() - bufsize) + count +1); - DEBUGFS_PRINTF("} at pos %d, took %d ms", f.position(), millis() - s); + DEBUGFS_PRINTF("} at pos %d, took %lu ms", f.position(), millis() - s); return true; } count++; } } - DEBUGFS_PRINTF("No match, took %d ms\n", millis() - s); + DEBUGFS_PRINTF("No match, took %lu ms\n", millis() - s); return false; } @@ -203,7 +203,7 @@ static bool appendObjectToFile(const char* key, const JsonDocument* content, uin if (f.position() > 2) f.write(','); //add comma if not first object f.print(key); serializeJson(*content, f); - DEBUGFS_PRINTF("Inserted, took %d ms (total %d)", millis() - s1, millis() - s); + DEBUGFS_PRINTF("Inserted, took %lu ms (total %lu)", millis() - s1, millis() - s); doCloseFile = true; return true; } @@ -251,7 +251,7 @@ static bool appendObjectToFile(const char* key, const JsonDocument* content, uin f.write('}'); doCloseFile = true; - DEBUGFS_PRINTF("Appended, took %d ms (total %d)", millis() - s1, millis() - s); + DEBUGFS_PRINTF("Appended, took %lu ms (total %lu)", millis() - s1, millis() - s); return true; } @@ -321,7 +321,7 @@ bool writeObjectToFile(const char* file, const char* key, const JsonDocument* co } doCloseFile = true; - DEBUGFS_PRINTF("Replaced/deleted, took %d ms\n", millis() - s); + DEBUGFS_PRINTF("Replaced/deleted, took %lu ms\n", millis() - s); return true; } @@ -356,7 +356,7 @@ bool readObjectFromFile(const char* file, const char* key, JsonDocument* dest, c else deserializeJson(*dest, f); f.close(); - DEBUGFS_PRINTF("Read, took %d ms\n", millis() - s); + DEBUGFS_PRINTF("Read, took %lu ms\n", millis() - s); return true; } @@ -392,7 +392,7 @@ static const uint8_t *getPresetCache(size_t &size) { if ((presetsModifiedTime != presetsCachedTime) || (presetsCachedValidate != cacheInvalidate)) { if (presetsCached) { - free(presetsCached); + w_free(presetsCached); presetsCached = nullptr; } } @@ -403,7 +403,7 @@ static const uint8_t *getPresetCache(size_t &size) { presetsCachedTime = presetsModifiedTime; presetsCachedValidate = cacheInvalidate; presetsCachedSize = 0; - presetsCached = (uint8_t*)ps_malloc(file.size() + 1); + presetsCached = (uint8_t*)w_malloc(file.size() + 1); if (presetsCached) { presetsCachedSize = file.size(); file.read(presetsCached, presetsCachedSize); @@ -419,7 +419,7 @@ static const uint8_t *getPresetCache(size_t &size) { #endif bool handleFileRead(AsyncWebServerRequest* request, String path){ - DEBUG_PRINT(F("WS FileRead: ")); DEBUG_PRINTLN(path); + DEBUGFS_PRINT(F("WS FileRead: ")); DEBUGFS_PRINTLN(path); if(path.endsWith("/")) path += "index.htm"; if(path.indexOf(F("sec")) > -1) return false; #ifdef ARDUINO_ARCH_ESP32 diff --git a/wled00/ir.cpp b/wled00/ir.cpp index 9f7950a2f..b2fec76f1 100644 --- a/wled00/ir.cpp +++ b/wled00/ir.cpp @@ -425,8 +425,8 @@ static void decodeIR44(uint32_t code) case IR44_COLDWHITE2 : changeColor(COLOR_COLDWHITE2, 255); changeEffect(FX_MODE_STATIC); break; case IR44_REDPLUS : changeEffect(relativeChange(effectCurrent, 1, 0, strip.getModeCount() -1)); break; case IR44_REDMINUS : changeEffect(relativeChange(effectCurrent, -1, 0, strip.getModeCount() -1)); break; - case IR44_GREENPLUS : changePalette(relativeChange(effectPalette, 1, 0, strip.getPaletteCount() -1)); break; - case IR44_GREENMINUS : changePalette(relativeChange(effectPalette, -1, 0, strip.getPaletteCount() -1)); break; + case IR44_GREENPLUS : changePalette(relativeChange(effectPalette, 1, 0, getPaletteCount() -1)); break; + case IR44_GREENMINUS : changePalette(relativeChange(effectPalette, -1, 0, getPaletteCount() -1)); break; case IR44_BLUEPLUS : changeEffectIntensity( 16); break; case IR44_BLUEMINUS : changeEffectIntensity(-16); break; case IR44_QUICK : changeEffectSpeed( 16); break; @@ -435,7 +435,7 @@ static void decodeIR44(uint32_t code) case IR44_DIY2 : presetFallback(2, FX_MODE_BREATH, 0); break; case IR44_DIY3 : presetFallback(3, FX_MODE_FIRE_FLICKER, 0); break; case IR44_DIY4 : presetFallback(4, FX_MODE_RAINBOW, 0); break; - case IR44_DIY5 : presetFallback(5, FX_MODE_METEOR, 0); break; + case IR44_DIY5 : presetFallback(5, FX_MODE_METEOR, 0); break; case IR44_DIY6 : presetFallback(6, FX_MODE_RAIN, 0); break; case IR44_AUTO : changeEffect(FX_MODE_STATIC); break; case IR44_FLASH : changeEffect(FX_MODE_PALETTE); break; @@ -484,7 +484,7 @@ static void decodeIR6(uint32_t code) case IR6_CHANNEL_UP: incBrightness(); break; case IR6_CHANNEL_DOWN: decBrightness(); break; case IR6_VOLUME_UP: changeEffect(relativeChange(effectCurrent, 1, 0, strip.getModeCount() -1)); break; - case IR6_VOLUME_DOWN: changePalette(relativeChange(effectPalette, 1, 0, strip.getPaletteCount() -1)); + case IR6_VOLUME_DOWN: changePalette(relativeChange(effectPalette, 1, 0, getPaletteCount() -1)); switch(lastIR6ColourIdx) { case 0: changeColor(COLOR_RED); break; case 1: changeColor(COLOR_REDDISH); break; @@ -530,7 +530,7 @@ static void decodeIR9(uint32_t code) /* This allows users to customize IR actions without the need to edit C code and compile. -From the https://github.com/wled-dev/WLED/wiki/Infrared-Control page, download the starter +From the https://github.com/wled/WLED/wiki/Infrared-Control page, download the starter ir.json file that corresponds to the number of buttons on your remote. Many of the remotes with the same number of buttons emit the same codes, but will have different labels or colors. Once you edit the ir.json file, upload it to your controller diff --git a/wled00/json.cpp b/wled00/json.cpp index c09b543f1..03f388256 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -2,14 +2,74 @@ #include "palettes.h" +#define JSON_PATH_STATE 1 +#define JSON_PATH_INFO 2 +#define JSON_PATH_STATE_INFO 3 +#define JSON_PATH_NODES 4 +#define JSON_PATH_PALETTES 5 +#define JSON_PATH_FXDATA 6 +#define JSON_PATH_NETWORKS 7 +#define JSON_PATH_EFFECTS 8 + /* * JSON API (De)serialization */ +namespace { + typedef struct { + uint32_t colors[NUM_COLORS]; + uint16_t start; + uint16_t stop; + uint16_t offset; + uint16_t grouping; + uint16_t spacing; + uint16_t startY; + uint16_t stopY; + uint16_t options; + uint8_t mode; + uint8_t palette; + uint8_t opacity; + uint8_t speed; + uint8_t intensity; + uint8_t custom1; + uint8_t custom2; + uint8_t custom3; + bool check1; + bool check2; + bool check3; + } SegmentCopy; -bool deserializeSegment(JsonObject elem, byte it, byte presetId) + uint8_t differs(const Segment& b, const SegmentCopy& a) { + uint8_t d = 0; + if (a.start != b.start) d |= SEG_DIFFERS_BOUNDS; + if (a.stop != b.stop) d |= SEG_DIFFERS_BOUNDS; + if (a.offset != b.offset) d |= SEG_DIFFERS_GSO; + if (a.grouping != b.grouping) d |= SEG_DIFFERS_GSO; + if (a.spacing != b.spacing) d |= SEG_DIFFERS_GSO; + if (a.opacity != b.opacity) d |= SEG_DIFFERS_BRI; + if (a.mode != b.mode) d |= SEG_DIFFERS_FX; + if (a.speed != b.speed) d |= SEG_DIFFERS_FX; + if (a.intensity != b.intensity) d |= SEG_DIFFERS_FX; + if (a.palette != b.palette) d |= SEG_DIFFERS_FX; + if (a.custom1 != b.custom1) d |= SEG_DIFFERS_FX; + if (a.custom2 != b.custom2) d |= SEG_DIFFERS_FX; + if (a.custom3 != b.custom3) d |= SEG_DIFFERS_FX; + if (a.startY != b.startY) d |= SEG_DIFFERS_BOUNDS; + if (a.stopY != b.stopY) d |= SEG_DIFFERS_BOUNDS; + + //bit pattern: (msb first) + // set:2, sound:2, mapping:3, transposed, mirrorY, reverseY, [reset,] paused, mirrored, on, reverse, [selected] + if ((a.options & 0b1111111111011110U) != (b.options & 0b1111111111011110U)) d |= SEG_DIFFERS_OPT; + if ((a.options & 0x0001U) != (b.options & 0x0001U)) d |= SEG_DIFFERS_SEL; + for (unsigned i = 0; i < NUM_COLORS; i++) if (a.colors[i] != b.colors[i]) d |= SEG_DIFFERS_COL; + + return d; + } +} + +static bool deserializeSegment(JsonObject elem, byte it, byte presetId) { byte id = elem["id"] | it; - if (id >= strip.getMaxSegments()) return false; + if (id >= WS2812FX::getMaxSegments()) return false; bool newSeg = false; int stop = elem["stop"] | -1; @@ -17,16 +77,37 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) // append segment if (id >= strip.getSegmentsNum()) { if (stop <= 0) return false; // ignore empty/inactive segments - strip.appendSegment(Segment(0, strip.getLengthTotal())); + strip.appendSegment(0, strip.getLengthTotal()); id = strip.getSegmentsNum()-1; // segments are added at the end of list newSeg = true; } //DEBUG_PRINTLN(F("-- JSON deserialize segment.")); Segment& seg = strip.getSegment(id); - //DEBUG_PRINTF_P(PSTR("-- Original segment: %p (%p)\n"), &seg, seg.data); - 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); + // we do not want to make segment copy as it may use a lot of RAM (effect data and pixel buffer) + // so we will create a copy of segment options and compare it with original segment when done processing + SegmentCopy prev = { + {seg.colors[0], seg.colors[1], seg.colors[2]}, + seg.start, + seg.stop, + seg.offset, + seg.grouping, + seg.spacing, + seg.startY, + seg.stopY, + seg.options, + seg.mode, + seg.palette, + seg.opacity, + seg.speed, + seg.intensity, + seg.custom1, + seg.custom2, + seg.custom3, + seg.check1, + seg.check2, + seg.check3 + }; int start = elem["start"] | seg.start; if (stop < 0) { @@ -44,7 +125,7 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) elem.remove("rpt"); // remove for recursive call elem.remove("n"); // remove for recursive call unsigned len = stop - start; - for (size_t i=id+1; i= strip.getLengthTotal()) break; //TODO: add support for 2D @@ -58,28 +139,11 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) if (elem["n"]) { // name field exists - if (seg.name) { //clear old name - free(seg.name); - seg.name = nullptr; - } - const char * name = elem["n"].as(); - size_t len = 0; - if (name != nullptr) len = strlen(name); - if (len > 0) { - if (len > WLED_MAX_SEGNAME_LEN) len = WLED_MAX_SEGNAME_LEN; - seg.name = static_cast(malloc(len+1)); - if (seg.name) strlcpy(seg.name, name, WLED_MAX_SEGNAME_LEN+1); - } else { - // but is empty (already deleted above) - elem.remove("n"); - } + seg.setName(name); // will resolve empty and null correctly } else if (start != seg.start || stop != seg.stop) { // clearing or setting segment without name field - if (seg.name) { - free(seg.name); - seg.name = nullptr; - } + seg.clearName(); } uint16_t grp = elem["grp"] | seg.grouping; @@ -97,6 +161,12 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) bool transpose = getBoolVal(elem[F("tp")], seg.transpose); #endif + // if segment's virtual dimensions change we need to restart effect (segment blending and PS rely on dimensions) + if (seg.mirror != mirror) seg.markForReset(); + #ifndef WLED_DISABLE_2D + if (seg.mirror_y != mirror_y || seg.transpose != transpose) seg.markForReset(); + #endif + int len = (stop > start) ? stop - start : 1; int offset = elem[F("of")] | INT32_MAX; if (offset != INT32_MAX) { @@ -118,8 +188,8 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) } byte segbri = seg.opacity; - if (getVal(elem["bri"], &segbri)) { - if (segbri > 0) seg.setOpacity(segbri); + if (getVal(elem["bri"], segbri)) { + if (segbri > 0) seg.setOpacity(segbri); // use transition seg.setOption(SEG_OPTION_ON, segbri); // use transition } @@ -175,13 +245,13 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) if (!colValid) continue; - seg.setColor(i, RGBW32(rgbw[0],rgbw[1],rgbw[2],rgbw[3])); + seg.setColor(i, RGBW32(rgbw[0],rgbw[1],rgbw[2],rgbw[3])); // use transition if (seg.mode == FX_MODE_STATIC) strip.trigger(); //instant refresh } } else { // non RGB & non White segment (usually On/Off bus) - seg.setColor(0, ULTRAWHITE); - seg.setColor(1, BLACK); + seg.setColor(0, ULTRAWHITE); // use transition + seg.setColor(1, BLACK); // use transition } } @@ -197,7 +267,6 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) } #endif - //seg.map1D2D = constrain(map1D2D, 0, 7); // done in setGeometry() seg.set = constrain(set, 0, 3); seg.soundSim = constrain(soundSim, 0, 3); seg.selected = selected; @@ -210,57 +279,58 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) #endif byte fx = seg.mode; - if (getVal(elem["fx"], &fx, 0, strip.getModeCount())) { + if (getVal(elem["fx"], fx, 0, strip.getModeCount())) { if (!presetId && currentPlaylist>=0) unloadPlaylist(); - if (fx != seg.mode) seg.setMode(fx, elem[F("fxdef")]); + if (fx != seg.mode) seg.setMode(fx, elem[F("fxdef")]); // use transition (WARNING: may change map1D2D causing geometry change) } - getVal(elem["sx"], &seg.speed); - getVal(elem["ix"], &seg.intensity); + getVal(elem["sx"], seg.speed); + getVal(elem["ix"], seg.intensity); uint8_t pal = seg.palette; if (seg.getLightCapabilities() & 1) { // ignore palette for White and On/Off segments - if (getVal(elem["pal"], &pal, 0, strip.getPaletteCount())) seg.setPalette(pal); + if (getVal(elem["pal"], pal, 0, getPaletteCount())) seg.setPalette(pal); } - getVal(elem["c1"], &seg.custom1); - getVal(elem["c2"], &seg.custom2); + getVal(elem["c1"], seg.custom1); + getVal(elem["c2"], seg.custom2); uint8_t cust3 = seg.custom3; - getVal(elem["c3"], &cust3, 0, 31); // we can't pass reference to bitfield + getVal(elem["c3"], cust3, 0, 31); // we can't pass reference to bitfield seg.custom3 = constrain(cust3, 0, 31); seg.check1 = getBoolVal(elem["o1"], seg.check1); seg.check2 = getBoolVal(elem["o2"], seg.check2); seg.check3 = getBoolVal(elem["o3"], seg.check3); + uint8_t blend = seg.blendMode; + getVal(elem["bm"], blend, 0, 15); // we can't pass reference to bitfield + seg.blendMode = constrain(blend, 0, 15); + JsonArray iarr = elem[F("i")]; //set individual LEDs if (!iarr.isNull()) { - uint8_t oldMap1D2D = seg.map1D2D; - seg.map1D2D = M12_Pixels; // no mapping - // set brightness immediately and disable transition jsonTransitionOnce = true; - seg.stopTransition(); + if (seg.isInTransition()) seg.startTransition(0); // setting transition time to 0 will stop transition in next frame strip.setTransition(0); strip.setBrightness(scaledBri(bri), true); // freeze and init to black if (!seg.freeze) { seg.freeze = true; - seg.fill(BLACK); + seg.clear(); } - start = 0, stop = 0; - set = 0; //0 nothing set, 1 start set, 2 range set + unsigned iStart = 0, iStop = 0; + unsigned iSet = 0; //0 nothing set, 1 start set, 2 range set for (size_t i = 0; i < iarr.size(); i++) { - if(iarr[i].is()) { - if (!set) { - start = abs(iarr[i].as()); - set++; + if (iarr[i].is()) { + if (!iSet) { + iStart = abs(iarr[i].as()); + iSet++; } else { - stop = abs(iarr[i].as()); - set++; + iStop = abs(iarr[i].as()); + iSet++; } } else { //color uint8_t rgbw[] = {0,0,0,0}; @@ -276,17 +346,16 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) } } - if (set < 2 || stop <= start) stop = start + 1; - uint32_t c = gamma32(RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3])); - while (start < stop) seg.setPixelColor(start++, c); - set = 0; + if (iSet < 2 || iStop <= iStart) iStop = iStart + 1; + uint32_t c = RGBW32(rgbw[0], rgbw[1], rgbw[2], rgbw[3]); + while (iStart < iStop) seg.setRawPixelColor(iStart++, c); // sets pixel color without 1D->2D expansion, grouping or spacing + iSet = 0; } } - seg.map1D2D = oldMap1D2D; // restore mapping strip.trigger(); // force segment update } // send UDP/WS if segment options changed (except selection; will also deselect current preset) - if (seg.differs(prev) & 0x7F) stateChanged = true; + if (differs(seg, prev) & ~SEG_DIFFERS_SEL) stateChanged = true; return true; } @@ -302,7 +371,8 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) #endif bool onBefore = bri; - getVal(root["bri"], &bri); + getVal(root["bri"], bri); + if (bri != briOld) stateChanged = true; bool on = root["on"] | (bri > 0); if (!on != !bri) toggleOnOff(); @@ -329,10 +399,8 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) } } -#ifndef WLED_DISABLE_MODE_BLEND blendingStyle = root[F("bs")] | blendingStyle; blendingStyle &= 0x1F; -#endif // temporary transition (applies only once) tr = root[F("tt")] | -1; @@ -345,6 +413,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) if (tr >= 0) strip.timebase = (unsigned long)tr - millis(); JsonObject nl = root["nl"]; + if (!nl.isNull()) stateChanged = true; nightlightActive = getBoolVal(nl["on"], nightlightActive); nightlightDelayMins = nl["dur"] | nightlightDelayMins; nightlightMode = nl["mode"] | nightlightMode; @@ -371,6 +440,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) if (realtimeOverride > 2) realtimeOverride = REALTIME_OVERRIDE_ALWAYS; if (realtimeMode && useMainSegmentOnly) { strip.getMainSegment().freeze = !realtimeOverride; + realtimeOverride = REALTIME_OVERRIDE_NONE; // ignore request for override if using main segment only } if (root.containsKey("live")) { @@ -388,18 +458,14 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) if (!segVar.isNull()) { // we may be called during strip.service() so we must not modify segments while effects are executing strip.suspend(); - const unsigned long start = millis(); - while (strip.isServicing() && millis() - start < strip.getFrameTime()) yield(); // wait until frame is over - #ifdef WLED_DEBUG - if (millis() - start > 0) DEBUG_PRINTLN(F("JSON: Waited for strip to finish servicing.")); - #endif + strip.waitForIt(); if (segVar.is()) { int id = segVar["id"] | -1; //if "seg" is not an array and ID not specified, apply to all selected/checked segments if (id < 0) { //apply all selected segments for (size_t s = 0; s < strip.getSegmentsNum(); s++) { - Segment &sg = strip.getSegment(s); + const Segment &sg = strip.getSegment(s); if (sg.isActive() && sg.isSelected()) { deserializeSegment(segVar, s, presetId); } @@ -449,7 +515,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) DEBUG_PRINTF_P(PSTR("Preset direct: %d\n"), currentPreset); } else if (!root["ps"].isNull()) { // we have "ps" call (i.e. from button or external API call) or "pd" that includes "ps" (i.e. from UI call) - if (root["win"].isNull() && getVal(root["ps"], &presetCycCurr, 1, 250) && presetCycCurr > 0 && presetCycCurr < 251 && presetCycCurr != currentPreset) { + if (root["win"].isNull() && getVal(root["ps"], presetCycCurr, 1, 250) && presetCycCurr > 0 && presetCycCurr < 251 && presetCycCurr != currentPreset) { DEBUG_PRINTF_P(PSTR("Preset select: %d\n"), presetCycCurr); // b) preset ID only or preset that does not change state (use embedded cycling limits if they exist in getVal()) applyPreset(presetCycCurr, callMode); // async load from file system (only preset ID was specified) @@ -465,11 +531,11 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) } if (root.containsKey(F("rmcpal")) && root[F("rmcpal")].as()) { - if (strip.customPalettes.size()) { + if (customPalettes.size()) { char fileName[32]; - sprintf_P(fileName, PSTR("/palette%d.json"), strip.customPalettes.size()-1); + sprintf_P(fileName, PSTR("/palette%d.json"), customPalettes.size()-1); if (WLED_FS.exists(fileName)) WLED_FS.remove(fileName); - strip.loadCustomPalettes(); + loadCustomPalettes(); } } @@ -488,13 +554,13 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) //if (restart) forceReconnect = true; } - stateUpdated(callMode); + if (stateChanged) stateUpdated(callMode); if (presetToRestore) currentPreset = presetToRestore; return stateResponse; } -void serializeSegment(const JsonObject& root, const Segment& seg, byte id, bool forPreset, bool segmentBounds) +static void serializeSegment(JsonObject& root, const Segment& seg, byte id, bool forPreset, bool segmentBounds) { root["id"] = id; if (segmentBounds) { @@ -517,6 +583,7 @@ void serializeSegment(const JsonObject& root, const Segment& seg, byte id, bool root["bri"] = (segbri) ? segbri : 255; root["cct"] = seg.cct; root[F("set")] = seg.set; + root["lc"] = seg.getLightCapabilities(); if (seg.name != nullptr) root["n"] = reinterpret_cast(seg.name); //not good practice, but decreases required JSON buffer else if (forPreset) root["n"] = ""; @@ -561,6 +628,7 @@ void serializeSegment(const JsonObject& root, const Segment& seg, byte id, bool root["o3"] = seg.check3; root["si"] = seg.soundSim; root["m12"] = seg.map1D2D; + root["bm"] = seg.blendMode; } void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segmentBounds, bool selectedSegmentsOnly) @@ -569,9 +637,7 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme root["on"] = (bri > 0); root["bri"] = briLast; root[F("transition")] = transitionDelay/100; //in 100ms -#ifndef WLED_DISABLE_MODE_BLEND root[F("bs")] = blendingStyle; -#endif } if (!forPreset) { @@ -602,7 +668,7 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme root[F("mainseg")] = strip.getMainSegmentId(); JsonArray seg = root.createNestedArray("seg"); - for (size_t s = 0; s < strip.getMaxSegments(); s++) { + for (size_t s = 0; s < WS2812FX::getMaxSegments(); s++) { if (s >= strip.getSegmentsNum()) { if (forPreset && segmentBounds && !selectedSegmentsOnly) { //disable segments not part of preset JsonObject seg0 = seg.createNestedObject(); @@ -611,7 +677,7 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme } else break; } - Segment &sg = strip.getSegment(s); + const Segment &sg = strip.getSegment(s); if (forPreset && selectedSegmentsOnly && !sg.isSelected()) continue; if (sg.isActive()) { JsonObject seg0 = seg.createNestedObject(); @@ -635,7 +701,7 @@ void serializeInfo(JsonObject root) leds[F("pwr")] = BusManager::currentMilliamps(); leds["fps"] = strip.getFps(); leds[F("maxpwr")] = BusManager::currentMilliamps()>0 ? BusManager::ablMilliampsMax() : 0; - leds[F("maxseg")] = strip.getMaxSegments(); + leds[F("maxseg")] = WS2812FX::getMaxSegments(); //leds[F("actseg")] = strip.getActiveSegmentsNum(); //leds[F("seglock")] = false; //might be used in the future to prevent modifications to segment config leds[F("bootps")] = bootPreset; @@ -649,13 +715,13 @@ void serializeInfo(JsonObject root) #endif unsigned totalLC = 0; - JsonArray lcarr = leds.createNestedArray(F("seglc")); + JsonArray lcarr = leds.createNestedArray(F("seglc")); // deprecated, use state.seg[].lc size_t nSegs = strip.getSegmentsNum(); for (size_t s = 0; s < nSegs; s++) { if (!strip.getSegment(s).isActive()) continue; unsigned lc = strip.getSegment(s).getLightCapabilities(); totalLC |= lc; - lcarr.add(lc); + lcarr.add(lc); // deprecated, use state.seg[].lc } leds["lc"] = totalLC; @@ -703,8 +769,8 @@ void serializeInfo(JsonObject root) #endif root[F("fxcount")] = strip.getModeCount(); - root[F("palcount")] = strip.getPaletteCount(); - root[F("cpalcount")] = strip.customPalettes.size(); //number of custom palettes + root[F("palcount")] = getPaletteCount(); + root[F("cpalcount")] = customPalettes.size(); //number of custom palettes JsonArray ledmaps = root.createNestedArray(F("maps")); for (size_t i=0; i maxPage) page = maxPage; int start = itemPerPage * page; int end = start + itemPerPage; - if (end > palettesCount + customPalettes) end = palettesCount + customPalettes; + if (end > palettesCount + customPalettesCount) end = palettesCount + customPalettesCount; root[F("m")] = maxPage; // inform caller how many pages there are JsonObject palettes = root.createNestedObject("p"); @@ -911,7 +977,7 @@ void serializePalettes(JsonObject root, int page) break; default: if (i >= palettesCount) - setPaletteColors(curPalette, strip.customPalettes[i - palettesCount]); + setPaletteColors(curPalette, customPalettes[i - palettesCount]); else if (i < 13) // palette 6 - 12, fastled palettes setPaletteColors(curPalette, *fastledPalettes[i-6]); else { diff --git a/wled00/led.cpp b/wled00/led.cpp index 2d2f5b6f2..43771f9d5 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -4,11 +4,9 @@ * LED methods */ -void setValuesFromMainSeg() { setValuesFromSegment(strip.getMainSegmentId()); } -void setValuesFromFirstSelectedSeg() { setValuesFromSegment(strip.getFirstSelectedSegId()); } -void setValuesFromSegment(uint8_t s) -{ - Segment& seg = strip.getSegment(s); + // applies chosen setment properties to legacy values +void setValuesFromSegment(uint8_t s) { + const Segment& seg = strip.getSegment(s); colPri[0] = R(seg.colors[0]); colPri[1] = G(seg.colors[0]); colPri[2] = B(seg.colors[0]); @@ -24,25 +22,19 @@ void setValuesFromSegment(uint8_t s) } -// applies global legacy values (col, colSec, effectCurrent...) -// problem: if the first selected segment already has the value to be set, other selected segments are not updated -void applyValuesToSelectedSegs() -{ - // copy of first selected segment to tell if value was updated - unsigned firstSel = strip.getFirstSelectedSegId(); - Segment selsegPrev = strip.getSegment(firstSel); +// applies global legacy values (colPri, colSec, effectCurrent...) to each selected segment +void applyValuesToSelectedSegs() { for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); - if (i != firstSel && (!seg.isActive() || !seg.isSelected())) continue; - - if (effectSpeed != selsegPrev.speed) {seg.speed = effectSpeed; stateChanged = true;} - if (effectIntensity != selsegPrev.intensity) {seg.intensity = effectIntensity; stateChanged = true;} - if (effectPalette != selsegPrev.palette) {seg.setPalette(effectPalette);} - if (effectCurrent != selsegPrev.mode) {seg.setMode(effectCurrent);} + if (!(seg.isActive() && seg.isSelected())) continue; + if (effectSpeed != seg.speed) {seg.speed = effectSpeed; stateChanged = true;} + if (effectIntensity != seg.intensity) {seg.intensity = effectIntensity; stateChanged = true;} + if (effectPalette != seg.palette) {seg.setPalette(effectPalette);} + if (effectCurrent != seg.mode) {seg.setMode(effectCurrent);} uint32_t col0 = RGBW32(colPri[0], colPri[1], colPri[2], colPri[3]); uint32_t col1 = RGBW32(colSec[0], colSec[1], colSec[2], colSec[3]); - if (col0 != selsegPrev.colors[0]) {seg.setColor(0, col0);} - if (col1 != selsegPrev.colors[1]) {seg.setColor(1, col1);} + if (col0 != seg.colors[0]) {seg.setColor(0, col0);} + if (col1 != seg.colors[1]) {seg.setColor(1, col1);} } } @@ -73,7 +65,8 @@ byte scaledBri(byte in) //applies global temporary brightness (briT) to strip void applyBri() { - if (!(realtimeMode && arlsForceMaxBri)) { + if (realtimeOverride || !(realtimeMode && arlsForceMaxBri)) + { //DEBUG_PRINTF_P(PSTR("Applying strip brightness: %d (%d,%d)\n"), (int)briT, (int)bri, (int)briOld); strip.setBrightness(scaledBri(briT)); } @@ -94,7 +87,7 @@ void applyFinalBri() { void stateUpdated(byte callMode) { //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (No notification) // 6: fx changed 7: hue 8: preset cycle 9: blynk 10: alexa 11: ws send only 12: button preset - setValuesFromFirstSelectedSeg(); + setValuesFromFirstSelectedSeg(); // a much better approach would be to use main segment: setValuesFromMainSeg() if (bri != briOld || stateChanged) { if (stateChanged) currentPreset = 0; //something changed, so we are no longer in the preset @@ -104,7 +97,6 @@ void stateUpdated(byte callMode) { //set flag to update ws and mqtt interfaceUpdateCallMode = callMode; - stateChanged = false; } else { if (nightlightActive && !nightlightActiveOld && callMode != CALL_MODE_NOTIFICATION && callMode != CALL_MODE_NO_NOTIFY) { notify(CALL_MODE_NIGHTLIGHT); @@ -134,15 +126,16 @@ void stateUpdated(byte callMode) { jsonTransitionOnce = false; transitionActive = false; applyFinalBri(); - return; + strip.trigger(); + } else { + if (transitionActive) { + briOld = briT; + } else if (bri != briOld || stateChanged) + strip.setTransitionMode(true); // force all segments to transition mode + transitionActive = true; + transitionStartTime = now; } - - if (transitionActive) { - briOld = briT; - } else - strip.setTransitionMode(true); // force all segments to transition mode - transitionActive = true; - transitionStartTime = now; + stateChanged = false; } diff --git a/wled00/mqtt.cpp b/wled00/mqtt.cpp index a462881ec..a1f659510 100644 --- a/wled00/mqtt.cpp +++ b/wled00/mqtt.cpp @@ -68,8 +68,8 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp } if (index == 0) { // start (1st partial packet or the only packet) - if (payloadStr) free(payloadStr); // fail-safe: release buffer - payloadStr = static_cast(malloc(total+1)); // allocate new buffer + w_free(payloadStr); // release buffer if it exists + payloadStr = static_cast(w_malloc(total+1)); // allocate new buffer } if (payloadStr == nullptr) return; // buffer not allocated @@ -94,7 +94,7 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp } else { // Non-Wled Topic used here. Probably a usermod subscribed to this topic. UsermodManager::onMqttMessage(topic, payloadStr); - free(payloadStr); + w_free(payloadStr); payloadStr = nullptr; return; } @@ -124,7 +124,7 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp // topmost topic (just wled/MAC) parseMQTTBriPayload(payloadStr); } - free(payloadStr); + w_free(payloadStr); payloadStr = nullptr; } @@ -196,7 +196,8 @@ bool initMqtt() if (!mqttEnabled || mqttServer[0] == 0 || !WLED_CONNECTED) return false; if (mqtt == nullptr) { - mqtt = new AsyncMqttClient(); + void *ptr = w_malloc(sizeof(AsyncMqttClient)); + mqtt = new (ptr) AsyncMqttClient(); // use placement new (into PSRAM), client will never be deleted if (!mqtt) return false; mqtt->onMessage(onMqttMessage); mqtt->onConnect(onMqttConnect); diff --git a/wled00/overlay.cpp b/wled00/overlay.cpp index fcd0a40c2..3f6e63121 100644 --- a/wled00/overlay.cpp +++ b/wled00/overlay.cpp @@ -90,9 +90,8 @@ void _overlayAnalogCountdown() void handleOverlayDraw() { UsermodManager::handleOverlayDraw(); if (analogClockSolidBlack) { - const Segment* segments = strip.getSegments(); for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { - const Segment& segment = segments[i]; + const Segment& segment = strip.getSegment(i); if (!segment.isActive()) continue; if (segment.mode > 0 || segment.colors[0] > 0) { return; diff --git a/wled00/palettes.h b/wled00/palettes.h old mode 100644 new mode 100755 index c84c1fb97..70cf8418b --- a/wled00/palettes.h +++ b/wled00/palettes.h @@ -1,500 +1,371 @@ +#ifndef PalettesWLED_h +#define PalettesWLED_h + /* * 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 // Unfortunately, these are stored in RAM! // Gradient palette "ib_jul01_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ing/xmas/tn/ib_jul01.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 16 bytes of program space. - -#ifndef PalettesWLED_h -#define PalettesWLED_h - -const byte ib_jul01_gp[] PROGMEM = { - 0, 194, 1, 1, - 94, 1, 29, 18, - 132, 57,131, 28, - 255, 113, 1, 1}; +// http://seaviewsensing.com/pub/cpt-city/ing/xmas/ib_jul01.c3g +const uint8_t ib_jul01_gp[] PROGMEM = { + 0, 230, 6, 17, + 94, 37, 96, 90, + 132, 144, 189, 106, + 255, 187, 3, 13}; // Gradient palette "es_vintage_57_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/vintage/tn/es_vintage_57.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte es_vintage_57_gp[] PROGMEM = { - 0, 2, 1, 1, - 53, 18, 1, 0, - 104, 69, 29, 1, - 153, 167,135, 10, - 255, 46, 56, 4}; - +// http://seaviewsensing.com/pub/cpt-city/es/vintage/es_vintage_57.c3g +const uint8_t es_vintage_57_gp[] PROGMEM = { + 0, 41, 8, 5, + 53, 92, 1, 0, + 104, 155, 96, 36, + 153, 217, 191, 72, + 255, 132, 129, 52}; // Gradient palette "es_vintage_01_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/vintage/tn/es_vintage_01.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 32 bytes of program space. - -const byte es_vintage_01_gp[] PROGMEM = { - 0, 4, 1, 1, - 51, 16, 0, 1, - 76, 97,104, 3, - 101, 255,131, 19, - 127, 67, 9, 4, - 153, 16, 0, 1, - 229, 4, 1, 1, - 255, 4, 1, 1}; - +// http://seaviewsensing.com/pub/cpt-city/es/vintage/es_vintage_01.c3g +const uint8_t es_vintage_01_gp[] PROGMEM = { + 0, 54, 18, 32, + 51, 89, 0, 30, + 76, 176, 170, 48, + 101, 255, 189, 92, + 127, 153, 56, 50, + 153, 89, 0, 30, + 229, 54, 18, 32, + 255, 54, 18, 32}; // Gradient palette "es_rivendell_15_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/rivendell/tn/es_rivendell_15.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte es_rivendell_15_gp[] PROGMEM = { - 0, 1, 14, 5, - 101, 16, 36, 14, - 165, 56, 68, 30, - 242, 150,156, 99, - 255, 150,156, 99}; - +// http://seaviewsensing.com/pub/cpt-city/es/rivendell/es_rivendell_15.c3g +const uint8_t es_rivendell_15_gp[] PROGMEM = { + 0, 35, 69, 54, + 101, 88, 105, 82, + 165, 143, 140, 109, + 242, 208, 204, 175, + 255, 208, 204, 175}; // Gradient palette "rgi_15_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ds/rgi/tn/rgi_15.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 36 bytes of program space. -// Edited to be brighter - -const byte rgi_15_gp[] PROGMEM = { - 0, 4, 1, 70, - 31, 55, 1, 30, - 63, 255, 4, 7, - 95, 59, 2, 29, - 127, 11, 3, 50, - 159, 39, 8, 60, - 191, 112, 19, 40, - 223, 78, 11, 39, - 255, 29, 8, 59}; - +// http://seaviewsensing.com/pub/cpt-city/ds/rgi/rgi_15.c3g +const uint8_t rgi_15_gp[] PROGMEM = { + 0, 54, 14, 111, + 31, 142, 24, 86, + 63, 231, 34, 61, + 95, 146, 31, 88, + 127, 61, 29, 114, + 159, 124, 47, 113, + 191, 186, 66, 112, + 223, 143, 57, 116, + 255, 100, 48, 120}; // Gradient palette "retro2_16_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ma/retro2/tn/retro2_16.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 8 bytes of program space. - -const byte retro2_16_gp[] PROGMEM = { - 0, 188,135, 1, - 255, 46, 7, 1}; - +// http://seaviewsensing.com/pub/cpt-city/ma/retro2/retro2_16.c3g +const uint8_t retro2_16_gp[] PROGMEM = { + 0, 227, 191, 12, + 255, 132, 52, 2}; // Gradient palette "Analogous_1_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/red/tn/Analogous_1.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte Analogous_1_gp[] PROGMEM = { - 0, 3, 0,255, - 63, 23, 0,255, - 127, 67, 0,255, - 191, 142, 0, 45, - 255, 255, 0, 0}; - +// http://seaviewsensing.com/pub/cpt-city/nd/red/Analogous_1.c3g +const uint8_t Analogous_1_gp[] PROGMEM = { + 0, 51, 0, 255, + 63, 102, 0, 255, + 127, 153, 0, 255, + 191, 204, 0, 128, + 255, 255, 0, 0}; // Gradient palette "es_pinksplash_08_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/pink_splash/tn/es_pinksplash_08.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte es_pinksplash_08_gp[] PROGMEM = { - 0, 126, 11,255, - 127, 197, 1, 22, - 175, 210,157,172, - 221, 157, 3,112, - 255, 157, 3,112}; - +// http://seaviewsensing.com/pub/cpt-city/es/pink_splash/es_pinksplash_08.c3g +const uint8_t es_pinksplash_08_gp[] PROGMEM = { + 0, 195, 63, 255, + 127, 231, 9, 97, + 175, 237, 205, 218, + 221, 212, 38, 184, + 255, 212, 38, 184}; // Gradient palette "es_ocean_breeze_036_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/ocean_breeze/tn/es_ocean_breeze_036.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 16 bytes of program space. - -const byte es_ocean_breeze_036_gp[] PROGMEM = { - 0, 1, 6, 7, - 89, 1, 99,111, - 153, 144,209,255, - 255, 0, 73, 82}; - +// http://seaviewsensing.com/pub/cpt-city/es/ocean_breeze/es_ocean_breeze_036.c3g +const uint8_t es_ocean_breeze_036_gp[] PROGMEM = { + 0, 25, 48, 62, + 89, 38, 166, 183, + 153, 205, 233, 255, + 255, 0, 145, 162}; // Gradient palette "departure_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/mjf/tn/departure.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 88 bytes of program space. - -const byte departure_gp[] PROGMEM = { - 0, 8, 3, 0, - 42, 23, 7, 0, - 63, 75, 38, 6, - 84, 169, 99, 38, - 106, 213,169,119, - 116, 255,255,255, - 138, 135,255,138, - 148, 22,255, 24, - 170, 0,255, 0, - 191, 0,136, 0, - 212, 0, 55, 0, - 255, 0, 55, 0}; - +// http://seaviewsensing.com/pub/cpt-city/mjf/departure.c3g +const uint8_t departure_gp[] PROGMEM = { + 0, 68, 34, 0, + 42, 102, 51, 0, + 63, 160, 108, 60, + 84, 218, 166, 120, + 106, 238, 212, 188, + 116, 255, 255, 255, + 138, 200, 255, 200, + 148, 100, 255, 100, + 170, 0, 255, 0, + 191, 0, 192, 0, + 212, 0, 128, 0, + 255, 0, 128, 0}; // Gradient palette "es_landscape_64_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/landscape/tn/es_landscape_64.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 36 bytes of program space. - -const byte es_landscape_64_gp[] PROGMEM = { - 0, 0, 0, 0, - 37, 2, 25, 1, - 76, 15,115, 5, - 127, 79,213, 1, - 128, 126,211, 47, - 130, 188,209,247, - 153, 144,182,205, - 204, 59,117,250, - 255, 1, 37,192}; - +// http://seaviewsensing.com/pub/cpt-city/es/landscape/es_landscape_64.c3g +const uint8_t es_landscape_64_gp[] PROGMEM = { + 0, 0, 0, 0, + 37, 43, 89, 26, + 76, 87, 178, 53, + 127, 163, 235, 8, + 128, 195, 234, 130, + 130, 227, 233, 252, + 153, 205, 219, 234, + 204, 146, 179, 253, + 255, 39, 107, 228}; // Gradient palette "es_landscape_33_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/landscape/tn/es_landscape_33.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 24 bytes of program space. - -const byte es_landscape_33_gp[] PROGMEM = { - 0, 1, 5, 0, - 19, 32, 23, 1, - 38, 161, 55, 1, - 63, 229,144, 1, - 66, 39,142, 74, - 255, 1, 4, 1}; - +// http://seaviewsensing.com/pub/cpt-city/es/landscape/es_landscape_33.c3g +const uint8_t es_landscape_33_gp[] PROGMEM = { + 0, 19, 45, 0, + 19, 116, 86, 3, + 38, 214, 128, 7, + 63, 245, 197, 25, + 66, 124, 196, 156, + 255, 9, 39, 11}; // Gradient palette "rainbowsherbet_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ma/icecream/tn/rainbowsherbet.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 28 bytes of program space. - -const byte rainbowsherbet_gp[] PROGMEM = { - 0, 255, 33, 4, - 43, 255, 68, 25, - 86, 255, 7, 25, - 127, 255, 82,103, - 170, 255,255,242, - 209, 42,255, 22, - 255, 87,255, 65}; - +// http://seaviewsensing.com/pub/cpt-city/ma/icecream/rainbowsherbet.c3g +const uint8_t rainbowsherbet_gp[] PROGMEM = { + 0, 255, 102, 51, + 43, 255, 140, 102, + 86, 255, 51, 102, + 127, 255, 153, 178, + 170, 255, 255, 250, + 209, 128, 255, 97, + 255, 169, 255, 148}; // Gradient palette "gr65_hult_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/hult/tn/gr65_hult.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 24 bytes of program space. - -const byte gr65_hult_gp[] PROGMEM = { - 0, 247,176,247, - 48, 255,136,255, - 89, 220, 29,226, - 160, 7, 82,178, - 216, 1,124,109, - 255, 1,124,109}; - +// http://seaviewsensing.com/pub/cpt-city/hult/gr65_hult.c3g +const uint8_t gr65_hult_gp[] PROGMEM = { + 0, 252, 216, 252, + 48, 255, 192, 255, + 89, 241, 95, 243, + 160, 65, 153, 221, + 216, 34, 184, 182, + 255, 34, 184, 182}; // Gradient palette "gr64_hult_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/hult/tn/gr64_hult.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 32 bytes of program space. - -const byte gr64_hult_gp[] PROGMEM = { - 0, 1,124,109, - 66, 1, 93, 79, - 104, 52, 65, 1, - 130, 115,127, 1, - 150, 52, 65, 1, - 201, 1, 86, 72, - 239, 0, 55, 45, - 255, 0, 55, 45}; - +// http://seaviewsensing.com/pub/cpt-city/hult/gr64_hult.c3g +const uint8_t gr64_hult_gp[] PROGMEM = { + 0, 34, 184, 182, + 66, 14, 162, 160, + 104, 139, 137, 11, + 130, 188, 186, 30, + 150, 139, 137, 11, + 201, 10, 156, 154, + 239, 0, 128, 128, + 255, 0, 128, 128}; // Gradient palette "GMT_drywet_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/gmt/tn/GMT_drywet.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 28 bytes of program space. - -const byte GMT_drywet_gp[] PROGMEM = { - 0, 47, 30, 2, - 42, 213,147, 24, - 84, 103,219, 52, - 127, 3,219,207, - 170, 1, 48,214, - 212, 1, 1,111, - 255, 1, 7, 33}; - +// http://seaviewsensing.com/pub/cpt-city/gmt/GMT_drywet.c3g +const uint8_t GMT_drywet_gp[] PROGMEM = { + 0, 134, 97, 42, + 42, 238, 199, 100, + 84, 180, 238, 135, + 127, 50, 238, 235, + 170, 12, 120, 238, + 212, 38, 1, 183, + 255, 8, 51, 113}; // Gradient palette "ib15_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/ing/general/tn/ib15.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 24 bytes of program space. - -const byte ib15_gp[] PROGMEM = { - 0, 113, 91,147, - 72, 157, 88, 78, - 89, 208, 85, 33, - 107, 255, 29, 11, - 141, 137, 31, 39, - 255, 59, 33, 89}; - +// http://seaviewsensing.com/pub/cpt-city/ing/general/ib15.c3g +const uint8_t ib15_gp[] PROGMEM = { + 0, 187, 160, 205, + 72, 212, 158, 159, + 89, 236, 155, 113, + 107, 255, 95, 74, + 141, 201, 98, 121, + 255, 146, 101, 168}; // Gradient palette "Tertiary_01_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/vermillion/tn/Tertiary_01.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte Tertiary_01_gp[] PROGMEM = { - 0, 0, 1,255, - 63, 3, 68, 45, - 127, 23,255, 0, - 191, 100, 68, 1, - 255, 255, 1, 4}; - +// http://seaviewsensing.com/pub/cpt-city/nd/vermillion/Tertiary_01.c3g +const uint8_t Tertiary_01_gp[] PROGMEM = { + 0, 0, 25, 255, + 63, 51, 140, 128, + 127, 102, 255, 0, + 191, 178, 140, 26, + 255, 255, 25, 51}; // Gradient palette "lava_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/neota/elem/tn/lava.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 52 bytes of program space. - -const byte lava_gp[] PROGMEM = { - 0, 0, 0, 0, - 46, 18, 0, 0, - 96, 113, 0, 0, - 108, 142, 3, 1, - 119, 175, 17, 1, - 146, 213, 44, 2, - 174, 255, 82, 4, - 188, 255,115, 4, - 202, 255,156, 4, - 218, 255,203, 4, - 234, 255,255, 4, - 244, 255,255, 71, - 255, 255,255,255}; - - -// Gradient palette "fierce_ice_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/neota/elem/tn/fierce-ice.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 28 bytes of program space. - -const byte fierce_ice_gp[] PROGMEM = { - 0, 0, 0, 0, - 59, 0, 9, 45, - 119, 0, 38,255, - 149, 3,100,255, - 180, 23,199,255, - 217, 100,235,255, - 255, 255,255,255}; +// http://seaviewsensing.com/pub/cpt-city/neota/elem/lava.c3g +const uint8_t lava_gp[] PROGMEM = { + 0, 0, 0, 0, + 46, 93, 0, 0, + 96, 187, 0, 0, + 108, 204, 38, 13, + 119, 221, 76, 26, + 146, 238, 115, 38, + 174, 255, 153, 51, + 188, 255, 178, 51, + 202, 255, 204, 51, + 218, 255, 230, 51, + 234, 255, 255, 51, + 244, 255, 255, 153, + 255, 255, 255, 255}; +// Gradient palette "fierce-ice_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/neota/elem/fierce-ice.c3g +const uint8_t fierce_ice_gp[] PROGMEM = { + 0, 0, 0, 0, + 59, 0, 51, 128, + 119, 0, 102, 255, + 149, 51, 153, 255, + 180, 102, 204, 255, + 217, 178, 230, 255, + 255, 255, 255, 255}; // Gradient palette "Colorfull_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Colorfull.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 44 bytes of program space. - -const byte Colorfull_gp[] PROGMEM = { - 0, 10, 85, 5, - 25, 29,109, 18, - 60, 59,138, 42, - 93, 83, 99, 52, - 106, 110, 66, 64, - 109, 123, 49, 65, - 113, 139, 35, 66, - 116, 192,117, 98, - 124, 255,255,137, - 168, 100,180,155, - 255, 22,121,174}; - +// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Colorfull.c3g +const uint8_t Colorfull_gp[] PROGMEM = { + 0, 76, 155, 54, + 25, 111, 174, 89, + 60, 146, 193, 125, + 93, 166, 166, 136, + 106, 185, 138, 147, + 109, 193, 121, 148, + 113, 202, 104, 149, + 116, 229, 179, 174, + 124, 255, 255, 199, + 168, 178, 218, 209, + 255, 100, 182, 219}; // Gradient palette "Pink_Purple_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Pink_Purple.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 44 bytes of program space. - -const byte Pink_Purple_gp[] PROGMEM = { - 0, 19, 2, 39, - 25, 26, 4, 45, - 51, 33, 6, 52, - 76, 68, 62,125, - 102, 118,187,240, - 109, 163,215,247, - 114, 217,244,255, - 122, 159,149,221, - 149, 113, 78,188, - 183, 128, 57,155, - 255, 146, 40,123}; - +// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Pink_Purple.c3g +const uint8_t Pink_Purple_gp[] PROGMEM = { + 0, 95, 32, 121, + 25, 106, 40, 128, + 51, 117, 48, 135, + 76, 154, 135, 192, + 102, 190, 222, 249, + 109, 215, 236, 252, + 114, 240, 250, 255, + 122, 213, 200, 241, + 149, 187, 149, 226, + 183, 196, 130, 209, + 255, 206, 111, 191}; // Gradient palette "Sunset_Real_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Sunset_Real.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 28 bytes of program space. - -const byte Sunset_Real_gp[] PROGMEM = { - 0, 120, 0, 0, - 22, 179, 22, 0, - 51, 255,104, 0, - 85, 167, 22, 18, - 135, 100, 0,103, - 198, 16, 0,130, - 255, 0, 0,160}; - +// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Sunset_Real.c3g +const uint8_t Sunset_Real_gp[] PROGMEM = { + 0, 191, 0, 0, + 22, 223, 85, 0, + 51, 255, 170, 0, + 85, 217, 85, 89, + 135, 178, 0, 178, + 198, 89, 0, 195, + 255, 0, 0, 212}; // Gradient palette "Sunset_Yellow_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Sunset_Yellow.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 44 bytes of program space. - -const byte Sunset_Yellow_gp[] PROGMEM = { - 0, 10, 62,123, - 36, 56,130,103, - 87, 153,225, 85, - 100, 199,217, 68, - 107, 255,207, 54, - 115, 247,152, 57, - 120, 239,107, 61, - 128, 247,152, 57, - 180, 255,207, 54, - 223, 255,227, 48, - 255, 255,248, 42}; - +// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Sunset_Yellow.c3g +const uint8_t Sunset_Yellow_gp[] PROGMEM = { + 0, 76, 135, 191, + 36, 143, 188, 178, + 87, 210, 241, 165, + 100, 232, 237, 151, + 107, 255, 232, 138, + 115, 252, 202, 141, + 120, 249, 172, 144, + 128, 252, 202, 141, + 180, 255, 232, 138, + 223, 255, 242, 131, + 255, 255, 252, 125}; // Gradient palette "Beech_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Beech.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 60 bytes of program space. - -const byte Beech_gp[] PROGMEM = { - 0, 255,252,214, - 12, 255,252,214, - 22, 255,252,214, - 26, 190,191,115, - 28, 137,141, 52, - 28, 112,255,205, - 50, 51,246,214, - 71, 17,235,226, - 93, 2,193,199, - 120, 0,156,174, - 133, 1,101,115, - 136, 1, 59, 71, - 136, 7,131,170, - 208, 1, 90,151, - 255, 0, 56,133}; - +// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Beech.c3g +const uint8_t Beech_gp[] PROGMEM = { + 0, 255, 254, 238, + 12, 255, 254, 238, + 22, 255, 254, 238, + 26, 228, 224, 186, + 28, 201, 195, 135, + 28, 186, 255, 234, + 50, 138, 251, 238, + 71, 90, 246, 243, + 93, 45, 225, 231, + 120, 0, 204, 219, + 133, 8, 168, 186, + 136, 16, 132, 153, + 136, 65, 189, 217, + 208, 33, 159, 207, + 255, 0, 129, 197}; // Gradient palette "Another_Sunset_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/atmospheric/tn/Another_Sunset.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 32 bytes of program space. - -const byte Another_Sunset_gp[] PROGMEM = { - 0, 110, 49, 11, - 29, 55, 34, 10, - 68, 22, 22, 9, - 68, 239,124, 8, - 97, 220,156, 27, - 124, 203,193, 61, - 178, 33, 53, 56, - 255, 0, 1, 52}; - - - - +// http://seaviewsensing.com/pub/cpt-city/nd/atmospheric/Another_Sunset.c3g +const uint8_t Another_Sunset_gp[] PROGMEM = { + 0, 185, 121, 73, + 29, 142, 103, 71, + 68, 100, 84, 69, + 68, 249, 184, 66, + 97, 241, 204, 105, + 124, 234, 225, 144, + 178, 117, 125, 140, + 255, 0, 26, 136}; // Gradient palette "es_autumn_19_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/es/autumn/tn/es_autumn_19.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 52 bytes of program space. - -const byte es_autumn_19_gp[] PROGMEM = { - 0, 26, 1, 1, - 51, 67, 4, 1, - 84, 118, 14, 1, - 104, 137,152, 52, - 112, 113, 65, 1, - 122, 133,149, 59, - 124, 137,152, 52, - 135, 113, 65, 1, - 142, 139,154, 46, - 163, 113, 13, 1, - 204, 55, 3, 1, - 249, 17, 1, 1, - 255, 17, 1, 1}; - +// http://seaviewsensing.com/pub/cpt-city/es/autumn/es_autumn_19.c3g +const uint8_t es_autumn_19_gp[] PROGMEM = { + 0, 106, 14, 8, + 51, 153, 41, 19, + 84, 190, 70, 24, + 104, 201, 202, 136, + 112, 187, 137, 5, + 122, 199, 200, 142, + 124, 201, 202, 135, + 135, 187, 137, 5, + 142, 202, 203, 129, + 163, 187, 68, 24, + 204, 142, 35, 17, + 249, 90, 5, 4, + 255, 90, 5, 4}; // Gradient palette "BlacK_Blue_Magenta_White_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/BlacK_Blue_Magenta_White.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 28 bytes of program space. - -const byte BlacK_Blue_Magenta_White_gp[] PROGMEM = { - 0, 0, 0, 0, - 42, 0, 0, 45, - 84, 0, 0,255, - 127, 42, 0,255, - 170, 255, 0,255, - 212, 255, 55,255, - 255, 255,255,255}; - +// http://seaviewsensing.com/pub/cpt-city/nd/basic/BlacK_Blue_Magenta_White.c3g +const uint8_t BlacK_Blue_Magenta_White_gp[] PROGMEM = { + 0, 0, 0, 0, + 42, 0, 0, 128, + 84, 0, 0, 255, + 127, 128, 0, 255, + 170, 255, 0, 255, + 212, 255, 128, 255, + 255, 255, 255, 255}; // Gradient palette "BlacK_Magenta_Red_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/BlacK_Magenta_Red.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte BlacK_Magenta_Red_gp[] PROGMEM = { - 0, 0, 0, 0, - 63, 42, 0, 45, - 127, 255, 0,255, - 191, 255, 0, 45, - 255, 255, 0, 0}; - +// http://seaviewsensing.com/pub/cpt-city/nd/basic/BlacK_Magenta_Red.c3g +const uint8_t BlacK_Magenta_Red_gp[] PROGMEM = { + 0, 0, 0, 0, + 63, 128, 0, 128, + 127, 255, 0, 255, + 191, 255, 0, 128, + 255, 255, 0, 0}; // Gradient palette "BlacK_Red_Magenta_Yellow_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/BlacK_Red_Magenta_Yellow.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 28 bytes of program space. - -const byte BlacK_Red_Magenta_Yellow_gp[] PROGMEM = { - 0, 0, 0, 0, - 42, 42, 0, 0, - 84, 255, 0, 0, - 127, 255, 0, 45, - 170, 255, 0,255, - 212, 255, 55, 45, - 255, 255,255, 0}; - +// http://seaviewsensing.com/pub/cpt-city/nd/basic/BlacK_Red_Magenta_Yellow.c3g +const uint8_t BlacK_Red_Magenta_Yellow_gp[] PROGMEM = { + 0, 0, 0, 0, + 42, 128, 0, 0, + 84, 255, 0, 0, + 127, 255, 0, 128, + 170, 255, 0, 255, + 212, 255, 128, 128, + 255, 255, 255, 0}; // Gradient palette "Blue_Cyan_Yellow_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/nd/basic/tn/Blue_Cyan_Yellow.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte Blue_Cyan_Yellow_gp[] PROGMEM = { - 0, 0, 0,255, - 63, 0, 55,255, - 127, 0,255,255, - 191, 42,255, 45, - 255, 255,255, 0}; - +// http://seaviewsensing.com/pub/cpt-city/nd/basic/Blue_Cyan_Yellow.c3g +const uint8_t Blue_Cyan_Yellow_gp[] PROGMEM = { + 0, 0, 0, 255, + 63, 0, 128, 255, + 127, 0, 255, 255, + 191, 128, 255, 128, + 255, 255, 255, 0}; //Custom palette by Aircoookie - const byte Orange_Teal_gp[] PROGMEM = { 0, 0,150, 92, 55, 0,150, 92, @@ -502,7 +373,6 @@ const byte Orange_Teal_gp[] PROGMEM = { 255, 255, 72, 0}; //Custom palette by Aircoookie - const byte Tiamat_gp[] PROGMEM = { 0, 1, 2, 14, //gc 33, 2, 5, 35, //gc from 47, 61,126 @@ -517,7 +387,6 @@ const byte Tiamat_gp[] PROGMEM = { 255, 255,249,255}; //Custom palette by Aircoookie - const byte April_Night_gp[] PROGMEM = { 0, 1, 5, 45, //deep blue 10, 1, 5, 45, @@ -585,271 +454,215 @@ const byte Atlantica_gp[] PROGMEM = { const byte C9_2_gp[] PROGMEM = { 0, 6, 126, 2, //green 45, 6, 126, 2, - 45, 4, 30, 114, //blue + 46, 4, 30, 114, //blue 90, 4, 30, 114, - 90, 255, 5, 0, //red + 91, 255, 5, 0, //red 135, 255, 5, 0, - 135, 196, 57, 2, //amber + 136, 196, 57, 2, //amber 180, 196, 57, 2, - 180, 137, 85, 2, //yellow + 181, 137, 85, 2, //yellow 255, 137, 85, 2}; //C9, but brighter and with a less purple blue const byte C9_new_gp[] PROGMEM = { 0, 255, 5, 0, //red 60, 255, 5, 0, - 60, 196, 57, 2, //amber (start 61?) + 61, 196, 57, 2, //amber (start 61?) 120, 196, 57, 2, - 120, 6, 126, 2, //green (start 126?) + 121, 6, 126, 2, //green (start 126?) 180, 6, 126, 2, - 180, 4, 30, 114, //blue (start 191?) + 181, 4, 30, 114, //blue (start 191?) 255, 4, 30, 114}; // Gradient palette "temperature_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/arendal/tn/temperature.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 144 bytes of program space. +// http://seaviewsensing.com/pub/cpt-city/arendal/temperature.c3g +const uint8_t temperature_gp[] PROGMEM = { + 0, 30, 92, 179, + 14, 23, 111, 193, + 28, 11, 142, 216, + 42, 4, 161, 230, + 56, 25, 181, 241, + 70, 51, 188, 207, + 84, 102, 204, 206, + 99, 153, 219, 184, + 113, 192, 229, 136, + 127, 204, 230, 75, + 141, 243, 240, 29, + 155, 254, 222, 39, + 170, 252, 199, 7, + 184, 248, 157, 14, + 198, 245, 114, 21, + 226, 219, 30, 38, + 240, 164, 38, 44, + 255, 164, 38, 44}; -const byte temperature_gp[] PROGMEM = { - 0, 1, 27,105, - 14, 1, 40,127, - 28, 1, 70,168, - 42, 1, 92,197, - 56, 1,119,221, - 70, 3,130,151, - 84, 23,156,149, - 99, 67,182,112, - 113, 121,201, 52, - 127, 142,203, 11, - 141, 224,223, 1, - 155, 252,187, 2, - 170, 247,147, 1, - 184, 237, 87, 1, - 198, 229, 43, 1, - 226, 171, 2, 2, - 240, 80, 3, 3, - 255, 80, 3, 3}; - - const byte Aurora2_gp[] PROGMEM = { - 0, 17, 177, 13, //Greenish - 64, 121, 242, 5, //Greenish - 128, 25, 173, 121, //Turquoise - 192, 250, 77, 127, //Pink - 255, 171, 101, 221 //Purple - }; - - // Gradient palette "bhw1_01_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_01.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 12 bytes of program space. - -const byte retro_clown_gp[] PROGMEM = { - 0, 227,101, 3, - 117, 194, 18, 19, - 255, 92, 8,192}; +// Gradient palette "bhw1_01_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_01.c3g +const uint8_t retro_clown_gp[] PROGMEM = { + 0, 244, 168, 48, + 117, 230, 78, 92, + 255, 173, 54, 228}; // Gradient palette "bhw1_04_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_04.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 20 bytes of program space. - -const byte candy_gp[] PROGMEM = { - 0, 229,227, 1, - 15, 227,101, 3, - 142, 40, 1, 80, - 198, 17, 1, 79, - 255, 0, 0, 45}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_04.c3g +const uint8_t candy_gp[] PROGMEM = { + 0, 245, 242, 31, + 15, 244, 168, 48, + 142, 126, 21, 161, + 198, 90, 22, 160, + 255, 0, 0, 128}; // Gradient palette "bhw1_05_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_05.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 8 bytes of program space. - -const byte toxy_reaf_gp[] PROGMEM = { - 0, 1,221, 53, - 255, 73, 3,178}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_05.c3g +const uint8_t toxy_reaf_gp[] PROGMEM = { + 0, 5, 239, 137, + 255, 158, 35, 221}; // Gradient palette "bhw1_06_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_06.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 16 bytes of program space. - -const byte fairy_reaf_gp[] PROGMEM = { - 0, 184, 1,128, - 160, 1,193,182, - 219, 153,227,190, - 255, 255,255,255}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_06.c3g +const uint8_t fairy_reaf_gp[] PROGMEM = { + 0, 225, 19, 194, + 160, 19, 225, 223, + 219, 210, 242, 227, + 255, 255, 255, 255}; // Gradient palette "bhw1_14_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_14.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 36 bytes of program space. - -const byte semi_blue_gp[] PROGMEM = { - 0, 0, 0, 0, - 12, 1, 1, 3, - 53, 8, 1, 22, - 80, 4, 6, 89, - 119, 2, 25,216, - 145, 7, 10, 99, - 186, 15, 2, 31, - 233, 2, 1, 5, - 255, 0, 0, 0}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_14.c3g +const uint8_t semi_blue_gp[] PROGMEM = { + 0, 0, 0, 0, + 12, 35, 4, 48, + 53, 70, 8, 96, + 80, 56, 48, 168, + 119, 43, 89, 239, + 145, 64, 59, 175, + 186, 86, 30, 110, + 233, 43, 15, 55, + 255, 0, 0, 0}; // Gradient palette "bhw1_three_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_three.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 32 bytes of program space. - -const byte pink_candy_gp[] PROGMEM = { - 0, 255,255,255, - 45, 7, 12,255, - 112, 227, 1,127, - 112, 227, 1,127, - 140, 255,255,255, - 155, 227, 1,127, - 196, 45, 1, 99, - 255, 255,255,255}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_three.c3g +const uint8_t pink_candy_gp[] PROGMEM = { + 0, 255, 255, 255, + 45, 64, 64, 255, + 112, 244, 16, 193, + 140, 255, 255, 255, + 155, 244, 16, 193, + 196, 131, 13, 175, + 255, 255, 255, 255}; // Gradient palette "bhw1_w00t_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw1/tn/bhw1_w00t.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 16 bytes of program space. - -const byte red_reaf_gp[] PROGMEM = { - 0, 3, 13, 43, - 104, 78,141,240, - 188, 255, 0, 0, - 255, 28, 1, 1}; - +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw1/bhw1_w00t.c3g +const uint8_t red_reaf_gp[] PROGMEM = { + 0, 49, 68, 126, + 104, 162, 195, 249, + 188, 255, 0, 0, + 255, 110, 14, 14}; // Gradient palette "bhw2_23_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw2/tn/bhw2_23.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Red & Flash in SR -// Size: 28 bytes of program space. - -const byte aqua_flash_gp[] PROGMEM = { - 0, 0, 0, 0, - 66, 57,227,233, - 96, 255,255, 8, - 124, 255,255,255, - 153, 255,255, 8, - 188, 57,227,233, - 255, 0, 0, 0}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_23.c3g +const uint8_t aqua_flash_gp[] PROGMEM = { + 0, 0, 0, 0, + 66, 144, 242, 246, + 96, 255, 255, 64, + 124, 255, 255, 255, + 153, 255, 255, 64, + 188, 144, 242, 246, + 255, 0, 0, 0}; // Gradient palette "bhw2_xc_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw2/tn/bhw2_xc.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// YBlue in SR -// Size: 28 bytes of program space. +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_xc.c3g +const uint8_t yelblu_hot_gp[] PROGMEM = { + 0, 56, 30, 68, + 58, 89, 0, 130, + 122, 103, 0, 86, + 158, 205, 57, 29, + 183, 223, 117, 35, + 219, 241, 177, 41, + 255, 247, 247, 35}; -const byte yelblu_hot_gp[] PROGMEM = { - 0, 4, 2, 9, - 58, 16, 0, 47, - 122, 24, 0, 16, - 158, 144, 9, 1, - 183, 179, 45, 1, - 219, 220,114, 2, - 255, 234,237, 1}; - - // Gradient palette "bhw2_45_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw2/tn/bhw2_45.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 24 bytes of program space. - -const byte lite_light_gp[] PROGMEM = { - 0, 0, 0, 0, - 9, 1, 1, 1, - 40, 5, 5, 6, - 66, 5, 5, 6, - 101, 10, 1, 12, - 255, 0, 0, 0}; +// Gradient palette "bhw2_45_gp", originally from +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_45.c3g +const uint8_t lite_light_gp[] PROGMEM = { + 0, 0, 0, 0, + 9, 30, 21, 30, + 40, 60, 43, 60, + 66, 60, 43, 60, + 101, 76, 16, 77, + 255, 0, 0, 0}; // Gradient palette "bhw2_22_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw2/tn/bhw2_22.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Pink Plasma in SR -// Size: 20 bytes of program space. - -const byte red_flash_gp[] PROGMEM = { - 0, 0, 0, 0, - 99, 227, 1, 1, - 130, 249,199, 95, - 155, 227, 1, 1, - 255, 0, 0, 0}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw2/bhw2_22.c3g +const uint8_t red_flash_gp[] PROGMEM = { + 0, 0, 0, 0, + 99, 244, 12, 12, + 130, 253, 228, 172, + 155, 244, 12, 12, + 255, 0, 0, 0}; // Gradient palette "bhw3_40_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw3/tn/bhw3_40.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 32 bytes of program space. - -const byte blink_red_gp[] PROGMEM = { - 0, 1, 1, 1, - 43, 4, 1, 11, - 76, 10, 1, 3, - 109, 161, 4, 29, - 127, 255, 86,123, - 165, 125, 16,160, - 204, 35, 13,223, - 255, 18, 2, 18}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw3/bhw3_40.c3g +const uint8_t blink_red_gp[] PROGMEM = { + 0, 7, 7, 7, + 43, 53, 25, 73, + 76, 76, 15, 46, + 109, 214, 39, 108, + 127, 255, 156, 191, + 165, 194, 73, 212, + 204, 120, 66, 242, + 255, 93, 29, 90}; // Gradient palette "bhw3_52_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw3/tn/bhw3_52.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Yellow2Blue in SR -// Size: 28 bytes of program space. - -const byte red_shift_gp[] PROGMEM = { - 0, 31, 1, 27, - 45, 34, 1, 16, - 99, 137, 5, 9, - 132, 213,128, 10, - 175, 199, 22, 1, - 201, 199, 9, 6, - 255, 1, 0, 1}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw3/bhw3_52.c3g +const uint8_t red_shift_gp[] PROGMEM = { + 0, 114, 22, 105, + 45, 118, 22, 85, + 99, 201, 45, 67, + 132, 238, 187, 70, + 175, 232, 85, 34, + 201, 232, 56, 59, + 255, 5, 0, 4}; // Gradient palette "bhw4_097_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw4/tn/bhw4_097.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Yellow2Red in SR -// Size: 44 bytes of program space. - -const byte red_tide_gp[] PROGMEM = { - 0, 247, 5, 0, - 28, 255, 67, 1, - 43, 234, 88, 11, - 58, 234,176, 51, - 84, 229, 28, 1, - 114, 113, 12, 1, - 140, 255,225, 44, - 168, 113, 12, 1, - 196, 244,209, 88, - 216, 255, 28, 1, - 255, 53, 1, 1}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw4/bhw4_097.c3g +const uint8_t red_tide_gp[] PROGMEM = { + 0, 252, 46, 0, + 28, 255, 139, 33, + 43, 247, 158, 74, + 58, 247, 216, 134, + 84, 245, 94, 15, + 114, 187, 65, 16, + 140, 255, 241, 127, + 168, 187, 65, 16, + 196, 251, 233, 167, + 216, 255, 94, 9, + 255, 140, 8, 6}; // Gradient palette "bhw4_017_gp", originally from -// http://soliton.vm.bytemark.co.uk/pub/cpt-city/bhw/bhw4/tn/bhw4_017.png.index.html -// converted for FastLED with gammas (2.6, 2.2, 2.5) -// Size: 40 bytes of program space. - -const byte candy2_gp[] PROGMEM = { - 0, 39, 33, 34, - 25, 4, 6, 15, - 48, 49, 29, 22, - 73, 224,173, 1, - 89, 177, 35, 5, - 130, 4, 6, 15, - 163, 255,114, 6, - 186, 224,173, 1, - 211, 39, 33, 34, - 255, 1, 1, 1}; +// http://seaviewsensing.com/pub/cpt-city/bhw/bhw4/bhw4_017.c3g +const uint8_t candy2_gp[] PROGMEM = { + 0, 124, 102, 114, + 25, 55, 49, 83, + 48, 136, 96, 96, + 73, 243, 214, 34, + 89, 222, 104, 54, + 130, 55, 49, 83, + 163, 255, 177, 58, + 186, 243, 214, 34, + 211, 124, 102, 114, + 255, 29, 19, 18}; 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 + 0, 0, 0, 0, //black + 85, 0, 255, 0, //green + 170, 255, 255, 0, //yellow + 255, 255, 0, 0}; //red + +const byte Aurora2_gp[] PROGMEM = { + 0, 17, 177, 13, //Greenish + 64, 121, 242, 5, //Greenish + 128, 25, 173, 121, //Turquoise + 192, 250, 77, 127, //Pink + 255, 171, 101, 221}; //Purple // array of fastled palettes (palette 6 - 12) const TProgmemRGBPalette16 *const fastledPalettes[] PROGMEM = { @@ -866,7 +679,7 @@ const TProgmemRGBPalette16 *const fastledPalettes[] PROGMEM = { // This will let us programmatically choose one based on // a number, rather than having to activate each explicitly // by name every time. -const byte* const gGradientPalettes[] PROGMEM = { +const uint8_t* const gGradientPalettes[] PROGMEM = { Sunset_Real_gp, //13-00 Sunset es_rivendell_15_gp, //14-01 Rivendell es_ocean_breeze_036_gp, //15-02 Breeze diff --git a/wled00/presets.cpp b/wled00/presets.cpp index b749289bd..0a4380f8c 100644 --- a/wled00/presets.cpp +++ b/wled00/presets.cpp @@ -29,8 +29,9 @@ bool presetNeedsSaving() { static void doSaveState() { bool persist = (presetToSave < 251); - unsigned long start = millis(); - while (strip.isUpdating() && millis()-start < (2*FRAMETIME_FIXED)+1) yield(); // wait 2 frames + unsigned long maxWait = millis() + strip.getFrameTime(); + while (strip.isUpdating() && millis() < maxWait) delay(1); // wait for strip to finish updating, accessing FS during sendout causes glitches + if (!requestJSONBufferLock(10)) return; initPresetsFile(); // just in case if someone deleted presets.json using /edit @@ -56,14 +57,10 @@ static void doSaveState() { */ #if defined(ARDUINO_ARCH_ESP32) if (!persist) { - if (tmpRAMbuffer!=nullptr) free(tmpRAMbuffer); + w_free(tmpRAMbuffer); size_t len = measureJson(*pDoc) + 1; - DEBUG_PRINTLN(len); // if possible use SPI RAM on ESP32 - if (psramSafe && psramFound()) - tmpRAMbuffer = (char*) ps_malloc(len); - else - tmpRAMbuffer = (char*) malloc(len); + tmpRAMbuffer = (char*)w_malloc(len); if (tmpRAMbuffer!=nullptr) { serializeJson(*pDoc, tmpRAMbuffer, len); } else { @@ -80,8 +77,8 @@ static void doSaveState() { // clean up saveLedmap = -1; presetToSave = 0; - free(saveName); - free(quickLoad); + w_free(saveName); + w_free(quickLoad); saveName = nullptr; quickLoad = nullptr; playlistSave = false; @@ -168,9 +165,9 @@ void handlePresets() 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 + #if defined(ARDUINO_ARCH_ESP32S2) || defined(ARDUINO_ARCH_ESP32C3) + unsigned long maxWait = millis() + strip.getFrameTime(); + while (strip.isUpdating() && millis() < maxWait) delay(1); // wait for strip to finish updating, accessing FS during sendout causes glitches #endif #ifdef ARDUINO_ARCH_ESP32 @@ -206,7 +203,7 @@ void handlePresets() #if defined(ARDUINO_ARCH_ESP32) //Aircoookie recommended not to delete buffer if (tmpPreset==255 && tmpRAMbuffer!=nullptr) { - free(tmpRAMbuffer); + w_free(tmpRAMbuffer); tmpRAMbuffer = nullptr; } #endif @@ -220,8 +217,8 @@ void handlePresets() //called from handleSet(PS=) [network callback (sObj is empty), IR (irrational), deserializeState, UDP] and deserializeState() [network callback (filedoc!=nullptr)] void savePreset(byte index, const char* pname, JsonObject sObj) { - if (!saveName) saveName = static_cast(malloc(33)); - if (!quickLoad) quickLoad = static_cast(malloc(9)); + if (!saveName) saveName = static_cast(w_malloc(33)); + if (!quickLoad) quickLoad = static_cast(w_malloc(9)); if (!saveName || !quickLoad) return; if (index == 0 || (index > 250 && index < 255)) return; @@ -267,8 +264,8 @@ void savePreset(byte index, const char* pname, JsonObject sObj) presetsModifiedTime = toki.second(); //unix time updateFSInfo(); } - free(saveName); - free(quickLoad); + w_free(saveName); + w_free(quickLoad); saveName = nullptr; quickLoad = nullptr; } else { diff --git a/wled00/set.cpp b/wled00/set.cpp index c817f2553..018d349bc 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -28,7 +28,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) char gw[5] = "GW"; gw[2] = 48+n; gw[4] = 0; //GW address char sn[5] = "SN"; sn[2] = 48+n; sn[4] = 0; //subnet mask if (request->hasArg(cs)) { - if (n >= multiWiFi.size()) multiWiFi.push_back(WiFiConfig()); // expand vector by one + if (n >= multiWiFi.size()) multiWiFi.emplace_back(); // expand vector by one char oldSSID[33]; strcpy(oldSSID, multiWiFi[n].clientSSID); char oldPass[65]; strcpy(oldPass, multiWiFi[n].clientPass); @@ -129,6 +129,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) unsigned length, start, maMax; uint8_t pins[5] = {255, 255, 255, 255, 255}; + // this will set global ABL max current used when per-port ABL is not used unsigned ablMilliampsMax = request->arg(F("MA")).toInt(); BusManager::setMilliampsMax(ablMilliampsMax); @@ -136,10 +137,10 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) strip.correctWB = request->hasArg(F("CCT")); strip.cctFromRgb = request->hasArg(F("CR")); cctICused = request->hasArg(F("IC")); - Bus::setCCTBlend(request->arg(F("CB")).toInt()); + uint8_t cctBlending = request->arg(F("CB")).toInt(); + Bus::setCCTBlend(cctBlending); Bus::setGlobalAWMode(request->arg(F("AW")).toInt()); strip.setTargetFps(request->arg(F("FR")).toInt()); - useGlobalLedBuffer = request->hasArg(F("LD")); #if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32C3) useParallelI2S = request->hasArg(F("PR")); #endif @@ -207,12 +208,12 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) maMax = 0; } else { maPerLed = request->arg(la).toInt(); - maMax = request->arg(ma).toInt(); // if ABL is disabled this will be 0 + maMax = request->arg(ma).toInt() * request->hasArg(F("PPL")); // if PP-ABL is disabled maMax (per bus) must be 0 } type |= request->hasArg(rf) << 7; // off refresh override // actual finalization is done in WLED::loop() (removing old busses and adding new) // this may happen even before this loop is finished so we do "doInitBusses" after the loop - busConfigs.emplace_back(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freq, useGlobalLedBuffer, maPerLed, maMax); + busConfigs.emplace_back(type, pins, start, length, colorOrder | (channelSwap<<4), request->hasArg(cv), skip, awmode, freq, maPerLed, maMax); busesChanged = true; } //doInitBusses = busesChanged; // we will do that below to ensure all input data is processed @@ -334,6 +335,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) t = request->arg(F("TP")).toInt(); randomPaletteChangeTime = MIN(255,MAX(1,t)); useHarmonicRandomPalette = request->hasArg(F("TH")); + useRainbowWheel = request->hasArg(F("RW")); nightlightTargetBri = request->arg(F("TB")).toInt(); t = request->arg(F("TL")).toInt(); @@ -342,7 +344,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) nightlightMode = request->arg(F("TW")).toInt(); t = request->arg(F("PB")).toInt(); - if (t >= 0 && t < 4) strip.paletteBlend = t; + if (t >= 0 && t < 4) paletteBlend = t; t = request->arg(F("BF")).toInt(); if (t > 0) briMultiplier = t; @@ -358,7 +360,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) DEBUG_PRINTLN(F("Enumerating ledmaps")); enumerateLedmaps(); DEBUG_PRINTLN(F("Loading custom palettes")); - strip.loadCustomPalettes(); // (re)load all custom palettes + loadCustomPalettes(); // (re)load all custom palettes } //SYNC @@ -771,14 +773,14 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) if (subPage == SUBPAGE_2D) { strip.isMatrix = request->arg(F("SOMP")).toInt(); - strip.panel.clear(); // release memory if allocated + strip.panel.clear(); if (strip.isMatrix) { - strip.panels = MAX(1,MIN(WLED_MAX_PANELS,request->arg(F("MPC")).toInt())); - strip.panel.reserve(strip.panels); // pre-allocate memory - for (unsigned i=0; iarg(F("MPC")).toInt(), 1, WLED_MAX_PANELS); + strip.panel.reserve(panels); // pre-allocate memory + for (unsigned i=0; iarg(pO).toInt(); strip.panel.push_back(p); } - strip.setUpMatrix(); // will check limits - strip.makeAutoSegments(true); - strip.deserializeMap(); - } else { - Segment::maxWidth = strip.getLengthTotal(); - Segment::maxHeight = 1; } + strip.panel.shrink_to_fit(); // release unused memory + strip.deserializeMap(); // (re)load default ledmap (will also setUpMatrix() if ledmap does not exist) + strip.makeAutoSegments(true); // force re-creation of segments } #endif @@ -824,7 +823,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) //segment select (sets main segment) pos = req.indexOf(F("SM=")); if (pos > 0 && !realtimeMode) { - strip.setMainSegmentId(getNumVal(&req, pos)); + strip.setMainSegmentId(getNumVal(req, pos)); } byte selectedSeg = strip.getFirstSelectedSegId(); @@ -833,7 +832,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("SS=")); if (pos > 0) { - unsigned t = getNumVal(&req, pos); + unsigned t = getNumVal(req, pos); if (t < strip.getSegmentsNum()) { selectedSeg = t; singleSegment = true; @@ -843,7 +842,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) Segment& selseg = strip.getSegment(selectedSeg); pos = req.indexOf(F("SV=")); //segment selected if (pos > 0) { - unsigned t = getNumVal(&req, pos); + unsigned t = getNumVal(req, pos); if (t == 2) for (unsigned i = 0; i < strip.getSegmentsNum(); i++) strip.getSegment(i).selected = false; // unselect other segments selseg.selected = t; } @@ -872,19 +871,19 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) uint16_t spcI = selseg.spacing; pos = req.indexOf(F("&S=")); //segment start if (pos > 0) { - startI = std::abs(getNumVal(&req, pos)); + startI = std::abs(getNumVal(req, pos)); } pos = req.indexOf(F("S2=")); //segment stop if (pos > 0) { - stopI = std::abs(getNumVal(&req, pos)); + stopI = std::abs(getNumVal(req, pos)); } pos = req.indexOf(F("GP=")); //segment grouping if (pos > 0) { - grpI = std::max(1,getNumVal(&req, pos)); + grpI = std::max(1,getNumVal(req, pos)); } pos = req.indexOf(F("SP=")); //segment spacing if (pos > 0) { - spcI = std::max(0,getNumVal(&req, pos)); + spcI = std::max(0,getNumVal(req, pos)); } strip.suspend(); // must suspend strip operations before changing geometry selseg.setGeometry(startI, stopI, grpI, spcI, UINT16_MAX, startY, stopY, selseg.map1D2D); @@ -898,7 +897,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("SB=")); //Segment brightness/opacity if (pos > 0) { - byte segbri = getNumVal(&req, pos); + byte segbri = getNumVal(req, pos); selseg.setOption(SEG_OPTION_ON, segbri); // use transition if (segbri) { selseg.setOpacity(segbri); @@ -907,7 +906,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("SW=")); //segment power if (pos > 0) { - switch (getNumVal(&req, pos)) { + switch (getNumVal(req, pos)) { case 0: selseg.setOption(SEG_OPTION_ON, false); break; // use transition case 1: selseg.setOption(SEG_OPTION_ON, true); break; // use transition default: selseg.setOption(SEG_OPTION_ON, !selseg.on); break; // use transition @@ -915,16 +914,16 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) } pos = req.indexOf(F("PS=")); //saves current in preset - if (pos > 0) savePreset(getNumVal(&req, pos)); + if (pos > 0) savePreset(getNumVal(req, pos)); pos = req.indexOf(F("P1=")); //sets first preset for cycle - if (pos > 0) presetCycMin = getNumVal(&req, pos); + if (pos > 0) presetCycMin = getNumVal(req, pos); pos = req.indexOf(F("P2=")); //sets last preset for cycle - if (pos > 0) presetCycMax = getNumVal(&req, pos); + if (pos > 0) presetCycMax = getNumVal(req, pos); //apply preset - if (updateVal(req.c_str(), "PL=", &presetCycCurr, presetCycMin, presetCycMax)) { + if (updateVal(req.c_str(), "PL=", presetCycCurr, presetCycMin, presetCycMax)) { applyPreset(presetCycCurr); } @@ -932,25 +931,25 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) if (pos > 0) doAdvancePlaylist = true; //set brightness - updateVal(req.c_str(), "&A=", &bri); + updateVal(req.c_str(), "&A=", bri); bool col0Changed = false, col1Changed = false, col2Changed = false; //set colors - col0Changed |= updateVal(req.c_str(), "&R=", &colIn[0]); - col0Changed |= updateVal(req.c_str(), "&G=", &colIn[1]); - col0Changed |= updateVal(req.c_str(), "&B=", &colIn[2]); - col0Changed |= updateVal(req.c_str(), "&W=", &colIn[3]); + col0Changed |= updateVal(req.c_str(), "&R=", colIn[0]); + col0Changed |= updateVal(req.c_str(), "&G=", colIn[1]); + col0Changed |= updateVal(req.c_str(), "&B=", colIn[2]); + col0Changed |= updateVal(req.c_str(), "&W=", colIn[3]); - col1Changed |= updateVal(req.c_str(), "R2=", &colInSec[0]); - col1Changed |= updateVal(req.c_str(), "G2=", &colInSec[1]); - col1Changed |= updateVal(req.c_str(), "B2=", &colInSec[2]); - col1Changed |= updateVal(req.c_str(), "W2=", &colInSec[3]); + col1Changed |= updateVal(req.c_str(), "R2=", colInSec[0]); + col1Changed |= updateVal(req.c_str(), "G2=", colInSec[1]); + col1Changed |= updateVal(req.c_str(), "B2=", colInSec[2]); + col1Changed |= updateVal(req.c_str(), "W2=", colInSec[3]); #ifdef WLED_ENABLE_LOXONE //lox parser pos = req.indexOf(F("LX=")); // Lox primary color if (pos > 0) { - int lxValue = getNumVal(&req, pos); + int lxValue = getNumVal(req, pos); if (parseLx(lxValue, colIn)) { bri = 255; nightlightActive = false; //always disable nightlight when toggling @@ -959,7 +958,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) } pos = req.indexOf(F("LY=")); // Lox secondary color if (pos > 0) { - int lxValue = getNumVal(&req, pos); + int lxValue = getNumVal(req, pos); if(parseLx(lxValue, colInSec)) { bri = 255; nightlightActive = false; //always disable nightlight when toggling @@ -971,11 +970,11 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) //set hue pos = req.indexOf(F("HU=")); if (pos > 0) { - uint16_t temphue = getNumVal(&req, pos); + uint16_t temphue = getNumVal(req, pos); byte tempsat = 255; pos = req.indexOf(F("SA=")); if (pos > 0) { - tempsat = getNumVal(&req, pos); + tempsat = getNumVal(req, pos); } byte sec = req.indexOf(F("H2")); colorHStoRGB(temphue, tempsat, (sec>0) ? colInSec : colIn); @@ -986,25 +985,25 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("&K=")); if (pos > 0) { byte sec = req.indexOf(F("K2")); - colorKtoRGB(getNumVal(&req, pos), (sec>0) ? colInSec : colIn); + colorKtoRGB(getNumVal(req, pos), (sec>0) ? colInSec : colIn); col0Changed |= (!sec); col1Changed |= sec; } //set color from HEX or 32bit DEC pos = req.indexOf(F("CL=")); if (pos > 0) { - colorFromDecOrHexString(colIn, req.substring(pos + 3).c_str()); + colorFromDecOrHexString(colIn, (char*)req.substring(pos + 3).c_str()); col0Changed = true; } pos = req.indexOf(F("C2=")); if (pos > 0) { - colorFromDecOrHexString(colInSec, req.substring(pos + 3).c_str()); + colorFromDecOrHexString(colInSec, (char*)req.substring(pos + 3).c_str()); col1Changed = true; } pos = req.indexOf(F("C3=")); if (pos > 0) { byte tmpCol[4]; - colorFromDecOrHexString(tmpCol, req.substring(pos + 3).c_str()); + colorFromDecOrHexString(tmpCol, (char*)req.substring(pos + 3).c_str()); col2 = RGBW32(tmpCol[0], tmpCol[1], tmpCol[2], tmpCol[3]); selseg.setColor(2, col2); // defined above (SS= or main) col2Changed = true; @@ -1013,7 +1012,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) //set to random hue SR=0->1st SR=1->2nd pos = req.indexOf(F("SR")); if (pos > 0) { - byte sec = getNumVal(&req, pos); + byte sec = getNumVal(req, pos); setRandomColor(sec? colInSec : colIn); col0Changed |= (!sec); col1Changed |= sec; } @@ -1039,19 +1038,19 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) bool fxModeChanged = false, speedChanged = false, intensityChanged = false, paletteChanged = false; bool custom1Changed = false, custom2Changed = false, custom3Changed = false, check1Changed = false, check2Changed = false, check3Changed = false; // set effect parameters - if (updateVal(req.c_str(), "FX=", &effectIn, 0, strip.getModeCount()-1)) { + if (updateVal(req.c_str(), "FX=", effectIn, 0, strip.getModeCount()-1)) { if (request != nullptr) unloadPlaylist(); // unload playlist if changing FX using web request fxModeChanged = true; } - speedChanged = updateVal(req.c_str(), "SX=", &speedIn); - intensityChanged = updateVal(req.c_str(), "IX=", &intensityIn); - paletteChanged = updateVal(req.c_str(), "FP=", &paletteIn, 0, strip.getPaletteCount()-1); - custom1Changed = updateVal(req.c_str(), "X1=", &custom1In); - custom2Changed = updateVal(req.c_str(), "X2=", &custom2In); - custom3Changed = updateVal(req.c_str(), "X3=", &custom3In); - check1Changed = updateVal(req.c_str(), "M1=", &check1In); - check2Changed = updateVal(req.c_str(), "M2=", &check2In); - check3Changed = updateVal(req.c_str(), "M3=", &check3In); + speedChanged = updateVal(req.c_str(), "SX=", speedIn); + intensityChanged = updateVal(req.c_str(), "IX=", intensityIn); + paletteChanged = updateVal(req.c_str(), "FP=", paletteIn, 0, getPaletteCount()-1); + custom1Changed = updateVal(req.c_str(), "X1=", custom1In); + custom2Changed = updateVal(req.c_str(), "X2=", custom2In); + custom3Changed = updateVal(req.c_str(), "X3=", custom3In); + check1Changed = updateVal(req.c_str(), "M1=", check1In); + check2Changed = updateVal(req.c_str(), "M2=", check2In); + check3Changed = updateVal(req.c_str(), "M3=", check3In); stateChanged |= (fxModeChanged || speedChanged || intensityChanged || paletteChanged || custom1Changed || custom2Changed || custom3Changed || check1Changed || check2Changed || check3Changed); @@ -1077,13 +1076,13 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) //set advanced overlay pos = req.indexOf(F("OL=")); if (pos > 0) { - overlayCurrent = getNumVal(&req, pos); + overlayCurrent = getNumVal(req, pos); } //apply macro (deprecated, added for compatibility with pre-0.11 automations) pos = req.indexOf(F("&M=")); if (pos > 0) { - applyPreset(getNumVal(&req, pos) + 16); + applyPreset(getNumVal(req, pos) + 16); } //toggle send UDP direct notifications @@ -1102,7 +1101,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("&T=")); if (pos > 0) { nightlightActive = false; //always disable nightlight when toggling - switch (getNumVal(&req, pos)) + switch (getNumVal(req, pos)) { case 0: if (bri != 0){briLast = bri; bri = 0;} break; //off, only if it was previously on case 1: if (bri == 0) bri = briLast; break; //on, only if it was previously off @@ -1121,7 +1120,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) nightlightActive = false; } else { nightlightActive = true; - if (!aNlDef) nightlightDelayMins = getNumVal(&req, pos); + if (!aNlDef) nightlightDelayMins = getNumVal(req, pos); else nightlightDelayMins = nightlightDelayMinsDefault; nightlightStartTime = millis(); } @@ -1135,7 +1134,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) //set nightlight target brightness pos = req.indexOf(F("NT=")); if (pos > 0) { - nightlightTargetBri = getNumVal(&req, pos); + nightlightTargetBri = getNumVal(req, pos); nightlightActiveOld = false; //re-init } @@ -1143,35 +1142,36 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("NF=")); if (pos > 0) { - nightlightMode = getNumVal(&req, pos); + nightlightMode = getNumVal(req, pos); nightlightActiveOld = false; //re-init } if (nightlightMode > NL_MODE_SUN) nightlightMode = NL_MODE_SUN; pos = req.indexOf(F("TT=")); - if (pos > 0) transitionDelay = getNumVal(&req, pos); + if (pos > 0) transitionDelay = getNumVal(req, pos); strip.setTransition(transitionDelay); //set time (unix timestamp) pos = req.indexOf(F("ST=")); if (pos > 0) { - setTimeFromAPI(getNumVal(&req, pos)); + setTimeFromAPI(getNumVal(req, pos)); } //set countdown goal (unix timestamp) pos = req.indexOf(F("CT=")); if (pos > 0) { - countdownTime = getNumVal(&req, pos); + countdownTime = getNumVal(req, pos); if (countdownTime - toki.second() > 0) countdownOverTriggered = false; } pos = req.indexOf(F("LO=")); if (pos > 0) { - realtimeOverride = getNumVal(&req, pos); + realtimeOverride = getNumVal(req, pos); if (realtimeOverride > 2) realtimeOverride = REALTIME_OVERRIDE_ALWAYS; if (realtimeMode && useMainSegmentOnly) { strip.getMainSegment().freeze = !realtimeOverride; + realtimeOverride = REALTIME_OVERRIDE_NONE; // ignore request for override if using main segment only } } @@ -1184,12 +1184,12 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("U0=")); //user var 0 if (pos > 0) { - userVar0 = getNumVal(&req, pos); + userVar0 = getNumVal(req, pos); } pos = req.indexOf(F("U1=")); //user var 1 if (pos > 0) { - userVar1 = getNumVal(&req, pos); + userVar1 = getNumVal(req, pos); } // you can add more if you need diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 4395b285d..ed608da34 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -6,7 +6,7 @@ #define UDP_SEG_SIZE 36 #define SEG_OFFSET (41) -#define WLEDPACKETSIZE (41+(MAX_NUM_SEGMENTS*UDP_SEG_SIZE)+0) +#define WLEDPACKETSIZE (41+(WS2812FX::getMaxSegments()*UDP_SEG_SIZE)+0) #define UDP_IN_MAXSIZE 1472 #define PRESUMED_NETWORK_DELAY 3 //how many ms could it take on avg to reach the receiver? This will be added to transmitted times @@ -55,7 +55,7 @@ void notify(byte callMode, bool followUp) //0: old 1: supports white 2: supports secondary color //3: supports FX intensity, 24 byte packet 4: supports transitionDelay 5: sup palette //6: supports timebase syncing, 29 byte packet 7: supports tertiary color 8: supports sys time sync, 36 byte packet - //9: supports sync groups, 37 byte packet 10: supports CCT, 39 byte packet 11: per segment options, variable packet length (40+MAX_NUM_SEGMENTS*3) + //9: supports sync groups, 37 byte packet 10: supports CCT, 39 byte packet 11: per segment options, variable packet length (40+WS2812FX::getMaxSegments()*3) //12: enhanced effect sliders, 2D & mapping options udpOut[11] = 12; col = mainseg.colors[1]; @@ -104,7 +104,7 @@ void notify(byte callMode, bool followUp) udpOut[40] = UDP_SEG_SIZE; //size of each loop iteration (one segment) size_t s = 0, nsegs = strip.getSegmentsNum(); for (size_t i = 0; i < nsegs; i++) { - Segment &selseg = strip.getSegment(i); + const Segment &selseg = strip.getSegment(i); if (!selseg.isActive()) continue; unsigned ofs = 41 + s*UDP_SEG_SIZE; //start of segment offset byte udpOut[0 +ofs] = s; @@ -177,7 +177,7 @@ void notify(byte callMode, bool followUp) memcpy(buffer.data + packetSize, &udpOut[41+i*UDP_SEG_SIZE], UDP_SEG_SIZE); packetSize += UDP_SEG_SIZE; if (packetSize + UDP_SEG_SIZE < bufferSize) continue; - DEBUG_PRINTF_P(PSTR("ESP-NOW sending packet: %d (%d)\n"), (int)buffer.packet, packetSize+3); + DEBUG_PRINTF_P(PSTR("ESP-NOW sending packet: %d (%u)\n"), (int)buffer.packet, packetSize+3); err = quickEspNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast(&buffer), packetSize+3); buffer.packet++; packetSize = 0; @@ -266,13 +266,13 @@ static void parseNotifyPacket(const uint8_t *udpIn) { strip.resume(); } size_t inactiveSegs = 0; - for (size_t i = 0; i < numSrcSegs && i < strip.getMaxSegments(); i++) { + for (size_t i = 0; i < numSrcSegs && i < WS2812FX::getMaxSegments(); i++) { unsigned ofs = 41 + i*udpIn[40]; //start of segment offset byte unsigned id = udpIn[0 +ofs]; DEBUG_PRINTF_P(PSTR("UDP segment received: %u\n"), id); if (id > strip.getSegmentsNum()) break; else if (id == strip.getSegmentsNum()) { - if (receiveSegmentBounds && id < strip.getMaxSegments()) strip.appendSegment(); + if (receiveSegmentBounds && id < WS2812FX::getMaxSegments()) strip.appendSegment(); else break; } DEBUG_PRINTF_P(PSTR("UDP segment check: %u\n"), id); @@ -327,7 +327,7 @@ static void parseNotifyPacket(const uint8_t *udpIn) { // freeze, reset should never be synced // LSB to MSB: select, reverse, on, mirror, freeze, reset, reverse_y, mirror_y, transpose, map1d2d (3), ssim (2), set (2) DEBUG_PRINTF_P(PSTR("Apply options: %u\n"), id); - selseg.options = (selseg.options & 0b0000000000110001U) | (udpIn[28+ofs]<<8) | (udpIn[9 +ofs] & 0b11001110U); // ignore selected, freeze, reset + selseg.options = (selseg.options & 0b0000000000110001U) | ((uint16_t)udpIn[28+ofs]<<8) | (udpIn[9 +ofs] & 0b11001110U); // ignore selected, freeze, reset if (applyEffects) { DEBUG_PRINTF_P(PSTR("Apply sliders: %u\n"), id); selseg.custom1 = udpIn[29+ofs]; @@ -406,31 +406,26 @@ static void parseNotifyPacket(const uint8_t *udpIn) { stateUpdated(CALL_MODE_NOTIFICATION); } +// realtimeLock() is called from UDP notifications, JSON API or serial Ada void realtimeLock(uint32_t timeoutMs, byte md) { if (!realtimeMode && !realtimeOverride) { - unsigned stop, start; if (useMainSegmentOnly) { Segment& mainseg = strip.getMainSegment(); - start = mainseg.start; - stop = mainseg.stop; + mainseg.clear(); // clear entire segment (in case sender transmits less pixels) mainseg.freeze = true; // 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; - } + for (size_t s = 0; s < strip.getSegmentsNum(); s++) strip.getSegment(s).freeze = true; } } else { - start = 0; - stop = strip.getLengthTotal(); + // clear entire strip + strip.fill(BLACK); + } + // if strip is off (bri==0) and not already in RTM + if (briT == 0) { + strip.setBrightness(scaledBri(briLast), true); } - // clear strip/segment - for (size_t i = start; i < stop; i++) strip.setPixelColor(i,BLACK); - } - // if strip is off (bri==0) and not already in RTM - if (briT == 0 && !realtimeMode && !realtimeOverride) { - strip.setBrightness(scaledBri(briLast), true); } if (realtimeTimeout != UINT32_MAX) { @@ -452,6 +447,7 @@ void exitRealtime() { realtimeIP[0] = 0; if (useMainSegmentOnly) { // unfreeze live segment again strip.getMainSegment().freeze = false; + strip.trigger(); } else { strip.show(); // possible fix for #3589 } @@ -481,7 +477,8 @@ void handleNotifications() if (e131NewData && millis() - strip.getLastShow() > 15) { e131NewData = false; - strip.show(); + if (useMainSegmentOnly) strip.trigger(); + else strip.show(); } //unlock strip when realtime UDP times out @@ -508,13 +505,13 @@ void handleNotifications() uint8_t lbuf[packetSize]; rgbUdp.read(lbuf, packetSize); realtimeLock(realtimeTimeoutMs, REALTIME_MODE_HYPERION); - if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; + if (realtimeOverride) return; 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); } - if (!(realtimeMode && useMainSegmentOnly)) strip.show(); + if (useMainSegmentOnly) strip.trigger(); + else strip.show(); return; } } @@ -583,7 +580,7 @@ void handleNotifications() realtimeIP = (isSupp) ? notifier2Udp.remoteIP() : notifierUdp.remoteIP(); realtimeLock(realtimeTimeoutMs, REALTIME_MODE_TPM2NET); - if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; + if (realtimeOverride) return; tpmPacketCount++; //increment the packet count if (tpmPacketCount == 1) tpmPayloadFrameSize = (udpIn[2] << 8) + udpIn[3]; //save frame size for the whole payload if this is the first packet @@ -592,13 +589,13 @@ void handleNotifications() 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 tpmPacketCount = 0; - strip.show(); + if (useMainSegmentOnly) strip.trigger(); + else strip.show(); } return; } @@ -610,17 +607,15 @@ void handleNotifications() DEBUG_PRINTLN(realtimeIP); if (packetSize < 2) return; - if (udpIn[1] == 0) - { - realtimeTimeout = 0; + if (udpIn[1] == 0) { + realtimeTimeout = 0; // cancel realtime mode immediately return; } else { realtimeLock(udpIn[1]*1000 +1, REALTIME_MODE_UDP); } - if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; + if (realtimeOverride) return; unsigned totalLen = strip.getLengthTotal(); - if (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) @@ -654,7 +649,8 @@ void handleNotifications() setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]); } } - strip.show(); + if (useMainSegmentOnly) strip.trigger(); + else strip.show(); return; } @@ -679,20 +675,7 @@ void handleNotifications() void setRealtimePixel(uint16_t i, byte r, byte g, byte b, byte w) { unsigned pix = i + arlsOffset; - if (pix < strip.getLengthTotal()) { - if (!arlsDisableGammaCorrection && gammaCorrectCol) { - r = gamma8(r); - g = gamma8(g); - b = gamma8(b); - w = gamma8(w); - } - uint32_t col = RGBW32(r,g,b,w); - if (useMainSegmentOnly) { - strip.getMainSegment().setPixelColor(pix, col); // this expects that strip.getMainSegment().beginDraw() has been called in handleNotification() - } else { - strip.setPixelColor(pix, col); - } - } + strip.setRealtimePixelColor(pix, RGBW32(r,g,b,w)); } /*********************************************************************************************\ @@ -808,7 +791,7 @@ static size_t sequenceNumber = 0; // this needs to be shared across all ou static const size_t ART_NET_HEADER_SIZE = 12; static const byte ART_NET_HEADER[] PROGMEM = {0x41,0x72,0x74,0x2d,0x4e,0x65,0x74,0x00,0x00,0x50,0x00,0x0e}; -uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const uint8_t* buffer, uint8_t bri, bool isRGBW) { +uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const uint8_t *buffer, uint8_t bri, bool isRGBW) { if (!(apActive || interfacesInited) || !client[0] || !length) return 1; // network not initialised or dummy/unset IP address 031522 ajn added check for ap WiFiUDP ddpUdp; diff --git a/wled00/util.cpp b/wled00/util.cpp index ac8a16207..8276c9c87 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -4,17 +4,17 @@ //helper to get int value at a position in string -int getNumVal(const String* req, uint16_t pos) +int getNumVal(const String &req, uint16_t pos) { - return req->substring(pos+3).toInt(); + return req.substring(pos+3).toInt(); } //helper to get int value with in/decrementing support via ~ syntax -void parseNumber(const char* str, byte* val, byte minv, byte maxv) +void parseNumber(const char* str, byte &val, byte minv, byte maxv) { if (str == nullptr || str[0] == '\0') return; - if (str[0] == 'r') {*val = hw_random8(minv,maxv?maxv:255); return;} // maxv for random cannot be 0 + if (str[0] == 'r') {val = hw_random8(minv,maxv?maxv:255); return;} // maxv for random cannot be 0 bool wrap = false; if (str[0] == 'w' && strlen(str) > 1) {str++; wrap = true;} if (str[0] == '~') { @@ -22,19 +22,19 @@ void parseNumber(const char* str, byte* val, byte minv, byte maxv) if (out == 0) { if (str[1] == '0') return; if (str[1] == '-') { - *val = (int)(*val -1) < (int)minv ? maxv : min((int)maxv,(*val -1)); //-1, wrap around + val = (int)(val -1) < (int)minv ? maxv : min((int)maxv,(val -1)); //-1, wrap around } else { - *val = (int)(*val +1) > (int)maxv ? minv : max((int)minv,(*val +1)); //+1, wrap around + val = (int)(val +1) > (int)maxv ? minv : max((int)minv,(val +1)); //+1, wrap around } } else { - if (wrap && *val == maxv && out > 0) out = minv; - else if (wrap && *val == minv && out < 0) out = maxv; + if (wrap && val == maxv && out > 0) out = minv; + else if (wrap && val == minv && out < 0) out = maxv; else { - out += *val; + out += val; if (out > maxv) out = maxv; if (out < minv) out = minv; } - *val = out; + val = out; } return; } else if (minv == maxv && minv == 0) { // limits "unset" i.e. both 0 @@ -49,14 +49,14 @@ void parseNumber(const char* str, byte* val, byte minv, byte maxv) } } } - *val = atoi(str); + val = atoi(str); } //getVal supports inc/decrementing and random ("X~Y(r|~[w][-][Z])" form) -bool getVal(JsonVariant elem, byte* val, byte vmin, byte vmax) { +bool getVal(JsonVariant elem, byte &val, byte vmin, byte vmax) { if (elem.is()) { if (elem < 0) return false; //ignore e.g. {"ps":-1} - *val = elem; + val = elem; return true; } else if (elem.is()) { const char* str = elem; @@ -82,7 +82,7 @@ bool getBoolVal(const JsonVariant &elem, bool dflt) { } -bool updateVal(const char* req, const char* key, byte* val, byte minv, byte maxv) +bool updateVal(const char* req, const char* key, byte &val, byte minv, byte maxv) { const char *v = strstr(req, key); if (v) v += strlen(key); @@ -619,6 +619,68 @@ int32_t hw_random(int32_t lowerlimit, int32_t upperlimit) { return hw_random(diff) + lowerlimit; } +#ifndef ESP8266 +void *w_malloc(size_t size) { + int caps1 = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; + int caps2 = MALLOC_CAP_DEFAULT | MALLOC_CAP_8BIT; + if (psramSafe) { + if (heap_caps_get_free_size(caps2) > 3*MIN_HEAP_SIZE && size < 512) std::swap(caps1, caps2); // use DRAM for small alloactions & when heap is plenty + return heap_caps_malloc_prefer(size, 2, caps1, caps2); // otherwise prefer PSRAM if it exists + } + return heap_caps_malloc(size, caps2); +} + +void *w_realloc(void *ptr, size_t size) { + int caps1 = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; + int caps2 = MALLOC_CAP_DEFAULT | MALLOC_CAP_8BIT; + if (psramSafe) { + if (heap_caps_get_free_size(caps2) > 3*MIN_HEAP_SIZE && size < 512) std::swap(caps1, caps2); // use DRAM for small alloactions & when heap is plenty + return heap_caps_realloc_prefer(ptr, size, 2, caps1, caps2); // otherwise prefer PSRAM if it exists + } + return heap_caps_realloc(ptr, size, caps2); +} + +void *w_calloc(size_t count, size_t size) { + int caps1 = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; + int caps2 = MALLOC_CAP_DEFAULT | MALLOC_CAP_8BIT; + if (psramSafe) { + if (heap_caps_get_free_size(caps2) > 3*MIN_HEAP_SIZE && size < 512) std::swap(caps1, caps2); // use DRAM for small alloactions & when heap is plenty + return heap_caps_calloc_prefer(count, size, 2, caps1, caps2); // otherwise prefer PSRAM if it exists + } + return heap_caps_calloc(count, size, caps2); +} + +void *d_malloc(size_t size) { + int caps1 = MALLOC_CAP_DEFAULT | MALLOC_CAP_8BIT; + int caps2 = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; + if (psramSafe) { + if (size > MIN_HEAP_SIZE) std::swap(caps1, caps2); // prefer PSRAM for large alloactions + return heap_caps_malloc_prefer(size, 2, caps1, caps2); // otherwise prefer DRAM + } + return heap_caps_malloc(size, caps1); +} + +void *d_realloc(void *ptr, size_t size) { + int caps1 = MALLOC_CAP_DEFAULT | MALLOC_CAP_8BIT; + int caps2 = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; + if (psramSafe) { + if (size > MIN_HEAP_SIZE) std::swap(caps1, caps2); // prefer PSRAM for large alloactions + return heap_caps_realloc_prefer(ptr, size, 2, caps1, caps2); // otherwise prefer DRAM + } + return heap_caps_realloc(ptr, size, caps1); +} + +void *d_calloc(size_t count, size_t size) { + int caps1 = MALLOC_CAP_DEFAULT | MALLOC_CAP_8BIT; + int caps2 = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; + if (psramSafe) { + if (size > MIN_HEAP_SIZE) std::swap(caps1, caps2); // prefer PSRAM for large alloactions + return heap_caps_calloc_prefer(count, size, 2, caps1, caps2); // otherwise prefer DRAM + } + return heap_caps_calloc(count, size, caps1); +} +#endif + /* * Fixed point integer based Perlin noise functions by @dedehai * Note: optimized for speed and to mimic fastled inoise functions, not for accuracy or best randomness diff --git a/wled00/wled.cpp b/wled00/wled.cpp index cc338d23f..e22b94c77 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -529,6 +529,7 @@ void WLED::setup() void WLED::beginStrip() { // Initialize NeoPixel Strip and button + strip.setTransition(0); // temporarily prevent transitions to reduce segment copies strip.finalizeInit(); // busses created during deserializeConfig() if config existed strip.makeAutoSegments(); strip.setBrightness(0); @@ -557,6 +558,8 @@ void WLED::beginStrip() applyPreset(bootPreset, CALL_MODE_INIT); } + strip.setTransition(transitionDelayDefault); // restore transitions + // init relay pin if (rlyPin >= 0) { pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); diff --git a/wled00/wled.h b/wled00/wled.h index f8dc1252a..230b9cbcf 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -602,6 +602,8 @@ WLED_GLOBAL bool wasConnected _INIT(false); // color WLED_GLOBAL byte lastRandomIndex _INIT(0); // used to save last random color so the new one is not the same +WLED_GLOBAL std::vector customPalettes; // custom palettes +WLED_GLOBAL uint8_t paletteBlend _INIT(0); // determines bending and wrapping of palette: 0: blend, wrap if moving (SEGMENT.speed>0); 1: blend, always wrap; 2: blend, never wrap; 3: don't blend or wrap // transitions WLED_GLOBAL uint8_t blendingStyle _INIT(0); // effect blending/transitionig style @@ -612,6 +614,7 @@ WLED_GLOBAL unsigned long transitionStartTime; WLED_GLOBAL bool jsonTransitionOnce _INIT(false); // flag to override transitionDelay (playlist, JSON API: "live" & "seg":{"i"} & "tt") WLED_GLOBAL uint8_t randomPaletteChangeTime _INIT(5); // amount of time [s] between random palette changes (min: 1s, max: 255s) WLED_GLOBAL bool useHarmonicRandomPalette _INIT(true); // use *harmonic* random palette generation (nicer looking) or truly random +WLED_GLOBAL bool useRainbowWheel _INIT(false); // use "rainbow" color wheel instead of "spectrum" color wheel // nightlight WLED_GLOBAL bool nightlightActive _INIT(false); diff --git a/wled00/wled_eeprom.cpp b/wled00/wled_eeprom.cpp index d0451d9a9..fb63bc646 100644 --- a/wled00/wled_eeprom.cpp +++ b/wled00/wled_eeprom.cpp @@ -225,7 +225,7 @@ void loadSettingsFromEEPROM() if (lastEEPROMversion > 7) { //strip.paletteFade = EEPROM.read(374); - strip.paletteBlend = EEPROM.read(382); + paletteBlend = EEPROM.read(382); for (int i = 0; i < 8; ++i) { diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 19868d01d..ce0662ca1 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -291,12 +291,11 @@ void getSettingsJS(byte subPage, Print& settingsScript) printSetFormValue(settingsScript,PSTR("CB"),Bus::getCCTBlend()); printSetFormValue(settingsScript,PSTR("FR"),strip.getTargetFps()); printSetFormValue(settingsScript,PSTR("AW"),Bus::getGlobalAWMode()); - printSetFormCheckbox(settingsScript,PSTR("LD"),useGlobalLedBuffer); printSetFormCheckbox(settingsScript,PSTR("PR"),BusManager::hasParallelOutput()); // get it from bus manager not global variable unsigned sumMa = 0; - for (int s = 0; s < BusManager::getNumBusses(); s++) { - const Bus* bus = BusManager::getBus(s); + for (size_t s = 0; s < BusManager::getNumBusses(); s++) { + const Bus *bus = BusManager::getBus(s); if (!bus || !bus->isOk()) break; // should not happen but for safety int offset = s < 10 ? '0' : 'A'; char lp[4] = "L0"; lp[2] = offset+s; lp[3] = 0; //ascii 0-9 //strip data pin @@ -380,7 +379,8 @@ void getSettingsJS(byte subPage, Print& settingsScript) printSetFormValue(settingsScript,PSTR("TB"),nightlightTargetBri); printSetFormValue(settingsScript,PSTR("TL"),nightlightDelayMinsDefault); printSetFormValue(settingsScript,PSTR("TW"),nightlightMode); - printSetFormIndex(settingsScript,PSTR("PB"),strip.paletteBlend); + printSetFormIndex(settingsScript,PSTR("PB"),paletteBlend); + printSetFormCheckbox(settingsScript,PSTR("RW"),useRainbowWheel); printSetFormValue(settingsScript,PSTR("RL"),rlyPin); printSetFormCheckbox(settingsScript,PSTR("RM"),rlyMde); printSetFormCheckbox(settingsScript,PSTR("RO"),rlyOpenDrain); @@ -666,16 +666,14 @@ void getSettingsJS(byte subPage, Print& settingsScript) #ifndef WLED_DISABLE_2D settingsScript.printf_P(PSTR("maxPanels=%d;resetPanels();"),WLED_MAX_PANELS); if (strip.isMatrix) { - if(strip.panels>0){ - printSetFormValue(settingsScript,PSTR("PW"),strip.panel[0].width); //Set generator Width and Height to first panel size for convenience - printSetFormValue(settingsScript,PSTR("PH"),strip.panel[0].height); - } - printSetFormValue(settingsScript,PSTR("MPC"),strip.panels); + printSetFormValue(settingsScript,PSTR("PW"),strip.panel.size()>0?strip.panel[0].width:8); //Set generator Width and Height to first panel size for convenience + printSetFormValue(settingsScript,PSTR("PH"),strip.panel.size()>0?strip.panel[0].height:8); + printSetFormValue(settingsScript,PSTR("MPC"),strip.panel.size()); // panels - for (unsigned i=0; i Date: Wed, 23 Apr 2025 15:06:31 +0200 Subject: [PATCH 057/153] bugfix in enumerating buttons and busses (#4657) char value was changed from "55" to 'A' which is 65. need to deduct 10 so the result is 'A' if index counter is 10. --- wled00/set.cpp | 6 +++--- wled00/xml.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/wled00/set.cpp b/wled00/set.cpp index c817f2553..725875023 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -146,7 +146,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) bool busesChanged = false; for (int s = 0; s < 36; s++) { // theoretical limit is 36 : "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" - int offset = s < 10 ? '0' : 'A'; + int offset = s < 10 ? '0' : 'A' - 10; char lp[4] = "L0"; lp[2] = offset+s; lp[3] = 0; //ascii 0-9 //strip data pin char lc[4] = "LC"; lc[2] = offset+s; lc[3] = 0; //strip length char co[4] = "CO"; co[2] = offset+s; co[3] = 0; //strip color order @@ -220,7 +220,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) // we will not bother with pre-allocating ColorOrderMappings vector BusManager::getColorOrderMap().reset(); for (int s = 0; s < WLED_MAX_COLOR_ORDER_MAPPINGS; s++) { - int offset = s < 10 ? '0' : 'A'; + int offset = s < 10 ? '0' : 'A' - 10; char xs[4] = "XS"; xs[2] = offset+s; xs[3] = 0; //start LED char xc[4] = "XC"; xc[2] = offset+s; xc[3] = 0; //strip length char xo[4] = "XO"; xo[2] = offset+s; xo[3] = 0; //color order @@ -259,7 +259,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) disablePullUp = (bool)request->hasArg(F("IP")); touchThreshold = request->arg(F("TT")).toInt(); for (int i = 0; i < WLED_MAX_BUTTONS; i++) { - int offset = i < 10 ? '0' : 'A'; + int offset = i < 10 ? '0' : 'A' - 10; char bt[4] = "BT"; bt[2] = offset+i; bt[3] = 0; // button pin (use A,B,C,... if WLED_MAX_BUTTONS>10) char be[4] = "BE"; be[2] = offset+i; be[3] = 0; // button type (use A,B,C,... if WLED_MAX_BUTTONS>10) int hw_btn_pin = request->arg(bt).toInt(); diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 19868d01d..de2f5590d 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -298,7 +298,7 @@ void getSettingsJS(byte subPage, Print& settingsScript) for (int s = 0; s < BusManager::getNumBusses(); s++) { const Bus* bus = BusManager::getBus(s); if (!bus || !bus->isOk()) break; // should not happen but for safety - int offset = s < 10 ? '0' : 'A'; + int offset = s < 10 ? '0' : 'A' - 10; char lp[4] = "L0"; lp[2] = offset+s; lp[3] = 0; //ascii 0-9 //strip data pin char lc[4] = "LC"; lc[2] = offset+s; lc[3] = 0; //strip length char co[4] = "CO"; co[2] = offset+s; co[3] = 0; //strip color order From 0f321bfb383a9aed35a0020e7a710937e833dcf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Wed, 23 Apr 2025 18:38:34 +0200 Subject: [PATCH 058/153] Compilation fixes --- usermods/audioreactive/audio_reactive.cpp | 12 ++++++------ .../usermod_v2_rotary_encoder_ui_ALT.cpp | 16 ++++++++-------- wled00/button.cpp | 6 +++--- wled00/fcn_declare.h | 9 +++------ wled00/image_loader.cpp | 2 +- wled00/json.cpp | 2 +- wled00/wled_server.cpp | 2 +- 7 files changed, 23 insertions(+), 26 deletions(-) diff --git a/usermods/audioreactive/audio_reactive.cpp b/usermods/audioreactive/audio_reactive.cpp index 4b3520562..7b0d113f9 100644 --- a/usermods/audioreactive/audio_reactive.cpp +++ b/usermods/audioreactive/audio_reactive.cpp @@ -1736,7 +1736,7 @@ class AudioReactive : public Usermod { } void onStateChange(uint8_t callMode) override { - if (initDone && enabled && addPalettes && palettes==0 && strip.customPalettes.size()<10) { + if (initDone && enabled && addPalettes && palettes==0 && customPalettes.size()<10) { // if palettes were removed during JSON call re-add them createAudioPalettes(); } @@ -1966,7 +1966,7 @@ class AudioReactive : public Usermod { void AudioReactive::removeAudioPalettes(void) { DEBUG_PRINTLN(F("Removing audio palettes.")); while (palettes>0) { - strip.customPalettes.pop_back(); + customPalettes.pop_back(); DEBUG_PRINTLN(palettes); palettes--; } @@ -1978,8 +1978,8 @@ void AudioReactive::createAudioPalettes(void) { if (palettes) return; DEBUG_PRINTLN(F("Adding audio palettes.")); for (int i=0; i= palettes) lastCustPalette -= palettes; for (int pal=0; palupdateRedrawTime(); #endif - effectPaletteIndex = max(min((unsigned)(increase ? effectPaletteIndex+1 : effectPaletteIndex-1), strip.getPaletteCount()+strip.customPalettes.size()-1), 0U); + effectPaletteIndex = max(min((unsigned)(increase ? effectPaletteIndex+1 : effectPaletteIndex-1), getPaletteCount()+strip.customPalettes.size()-1), 0U); effectPalette = palettes_alpha_indexes[effectPaletteIndex]; stateChanged = true; if (applyToAll) { diff --git a/wled00/button.cpp b/wled00/button.cpp index cf8fabe42..1c50200a2 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -74,7 +74,7 @@ void doublePressAction(uint8_t b) if (!macroDoublePress[b]) { switch (b) { //case 0: toggleOnOff(); colorUpdated(CALL_MODE_BUTTON); break; //instant short press on button 0 if no macro set - case 1: ++effectPalette %= strip.getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break; + case 1: ++effectPalette %= getPaletteCount(); colorUpdated(CALL_MODE_BUTTON); break; } } else { applyPreset(macroDoublePress[b], CALL_MODE_BUTTON_PRESET); @@ -226,8 +226,8 @@ void handleAnalog(uint8_t b) effectIntensity = aRead; } else if (macroDoublePress[b] == 247) { // selected palette - effectPalette = map(aRead, 0, 252, 0, strip.getPaletteCount()-1); - effectPalette = constrain(effectPalette, 0, strip.getPaletteCount()-1); // map is allowed to "overshoot", so we need to contrain the result + effectPalette = map(aRead, 0, 252, 0, getPaletteCount()-1); + effectPalette = constrain(effectPalette, 0, getPaletteCount()-1); // map is allowed to "overshoot", so we need to contrain the result } else if (macroDoublePress[b] == 200) { // primary color, hue, full saturation colorHStoRGB(aRead*256,255,colPri); diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 2846c3804..62d05ecd5 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -225,9 +225,8 @@ void onHueConnect(void* arg, AsyncClient* client); void sendHuePoll(); void onHueData(void* arg, AsyncClient* client, void *data, size_t len); -#include "FX.h" // must be below colors.cpp declarations (potentially due to duplicate declarations of e.g. color_blend) - //image_loader.cpp +class Segment; #ifdef WLED_ENABLE_GIF bool fileSeekCallback(unsigned long position); unsigned long filePositionCallback(void); @@ -263,9 +262,7 @@ void handleIR(); #include "ESPAsyncWebServer.h" #include "src/dependencies/json/ArduinoJson-v6.h" #include "src/dependencies/json/AsyncJson-v6.h" -#include "FX.h" -bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0); bool deserializeState(JsonObject root, byte callMode = CALL_MODE_DIRECT_CHANGE, byte presetId = 0); void serializeSegment(const JsonObject& root, const Segment& seg, byte id, bool forPreset = false, bool segmentBounds = true); void serializeState(JsonObject root, bool forPreset = false, bool includeBri = true, bool segmentBounds = true, bool selectedSegmentsOnly = false); @@ -279,8 +276,8 @@ bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient = 0); //led.cpp void setValuesFromSegment(uint8_t s); -void setValuesFromMainSeg(); -void setValuesFromFirstSelectedSeg(); +#define setValuesFromMainSeg() setValuesFromSegment(strip.getMainSegmentId()) +#define setValuesFromFirstSelectedSeg() setValuesFromSegment(strip.getFirstSelectedSegId()) void toggleOnOff(); void applyBri(); void applyFinalBri(); diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp index 966505794..aa4ae2e16 100644 --- a/wled00/image_loader.cpp +++ b/wled00/image_loader.cpp @@ -78,7 +78,7 @@ void drawPixelCallback(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t 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 (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; diff --git a/wled00/json.cpp b/wled00/json.cpp index 03f388256..441468102 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -66,7 +66,7 @@ namespace { } } -static bool deserializeSegment(JsonObject elem, byte it, byte presetId) +static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0) { byte id = elem["id"] | it; if (id >= WS2812FX::getMaxSegments()) return false; diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 06750838f..a41eab835 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -176,7 +176,7 @@ static void handleUpload(AsyncWebServerRequest *request, const String& filename, doReboot = true; request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Configuration restore successful.\nRebooting...")); } else { - if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) strip.loadCustomPalettes(); + if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) loadCustomPalettes(); request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File Uploaded!")); } cacheInvalidate++; From 7f2b6a3f10be5f9d8d5a10dd9b902f38b9d96c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Wed, 23 Apr 2025 18:53:22 +0200 Subject: [PATCH 059/153] More compilation fixes. --- usermods/audioreactive/audio_reactive.cpp | 4 ++-- .../usermod_v2_rotary_encoder_ui_ALT.cpp | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/usermods/audioreactive/audio_reactive.cpp b/usermods/audioreactive/audio_reactive.cpp index 7b0d113f9..2587b8be1 100644 --- a/usermods/audioreactive/audio_reactive.cpp +++ b/usermods/audioreactive/audio_reactive.cpp @@ -1970,11 +1970,11 @@ void AudioReactive::removeAudioPalettes(void) { DEBUG_PRINTLN(palettes); palettes--; } - DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(strip.customPalettes.size()); + DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size()); } void AudioReactive::createAudioPalettes(void) { - DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(strip.customPalettes.size()); + DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(customPalettes.size()); if (palettes) return; DEBUG_PRINTLN(F("Adding audio palettes.")); for (int i=0; iupdateRedrawTime(); #endif - effectPaletteIndex = max(min((unsigned)(increase ? effectPaletteIndex+1 : effectPaletteIndex-1), getPaletteCount()+strip.customPalettes.size()-1), 0U); + effectPaletteIndex = max(min((unsigned)(increase ? effectPaletteIndex+1 : effectPaletteIndex-1), getPaletteCount()+customPalettes.size()-1), 0U); effectPalette = palettes_alpha_indexes[effectPaletteIndex]; stateChanged = true; if (applyToAll) { From f1d52a8ec1e023aea92824cdc829cb99e61e2079 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 16:33:57 +0000 Subject: [PATCH 060/153] Bump h11 from 0.14.0 to 0.16.0 Bumps [h11](https://github.com/python-hyper/h11) from 0.14.0 to 0.16.0. - [Commits](https://github.com/python-hyper/h11/compare/v0.14.0...v0.16.0) --- updated-dependencies: - dependency-name: h11 dependency-version: 0.16.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1737408aa..4d767b057 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ click==8.1.8 # uvicorn colorama==0.4.6 # via platformio -h11==0.14.0 +h11==0.16.0 # via # uvicorn # wsproto From 7852ff558eb9a5ebba13661b049e9317d097ad10 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 26 Apr 2025 14:44:48 +0100 Subject: [PATCH 061/153] Build for each chipset --- .github/workflows/usermods.yml | 2 +- usermods/platformio_override.usermods.ini | 44 +++++++++++++++++++---- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index 02a404ba1..06ce611bf 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -35,7 +35,7 @@ jobs: fail-fast: false matrix: usermod: ${{ fromJSON(needs.get_usermod_envs.outputs.usermods) }} - environment: [usermod_esp32] + environment: [usermod_esp32, usermods_esp32c3, usermods_esp32s2, usermod_esp32s3] steps: - uses: actions/checkout@v4 - name: Set up Node.js diff --git a/usermods/platformio_override.usermods.ini b/usermods/platformio_override.usermods.ini index 611dc0d8b..c738077e9 100644 --- a/usermods/platformio_override.usermods.ini +++ b/usermods/platformio_override.usermods.ini @@ -1,11 +1,41 @@ -[env:usermod_esp32] +[platformio] +default_envs = usermods_esp32, usermods_esp32c3, usermods_esp32s2, usermods_esp32s3 + +[env:usermods_esp32] board = esp32dev platform = ${esp32_idf_V4.platform} build_unflags = ${common.build_unflags} -build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_NAME=\"ESP32_USERMOD\" - -DTOUCH_CS=9 - -DMQTTSWITCHPINS=8 -lib_deps = ${esp32_idf_V4.lib_deps} -monitor_filters = esp32_exception_decoder -board_build.flash_mode = dio +build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32_USERMODS\" +lib_deps = ${esp32.lib_deps} +board_build.partitions = ${esp32.big_partitions} +usermod = ${usermods.custom_usermods} +[env:usermods_esp32c3] +extends = esp32c3 +board = esp32-c3-devkitm-1 +platform = ${esp32_idf_V4.platform} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"C3_USERMODS\" +lib_deps = ${esp32.lib_deps} +board_build.partitions = ${esp32.big_partitions} +usermod = ${usermods.custom_usermods} + +[env:usermods_esp32s2] +extends = esp32s2 +platform = ${esp32_idf_V4.platform} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"S2_USERMODS\" +lib_deps = ${esp32.lib_deps} +board_build.partitions = ${esp32.big_partitions} +usermod = ${usermods.custom_usermods} + +[env:usermods_esp32s3] +extends = esp32s3 +platform = ${esp32_idf_V4.platform} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"S3_USERMODS\" +lib_deps = ${esp32.lib_deps} +board_build.partitions = ${esp32.big_partitions} +usermod = ${usermods.custom_usermods} + +[usermods] \ No newline at end of file From b77881f634885889827643b070fc2cc45e525f9c Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 26 Apr 2025 14:46:55 +0100 Subject: [PATCH 062/153] Build for each chipset --- .github/workflows/usermods.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index 06ce611bf..4de88449a 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -35,7 +35,7 @@ jobs: fail-fast: false matrix: usermod: ${{ fromJSON(needs.get_usermod_envs.outputs.usermods) }} - environment: [usermod_esp32, usermods_esp32c3, usermods_esp32s2, usermod_esp32s3] + environment: [usermods_esp32, usermods_esp32c3, usermods_esp32s2, usermod_esp32s3] steps: - uses: actions/checkout@v4 - name: Set up Node.js From fbb7ef7cfcd52eb46d7ace42869b1a36f566714a Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 26 Apr 2025 14:52:13 +0100 Subject: [PATCH 063/153] fix custom_usermods setting --- usermods/platformio_override.usermods.ini | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/usermods/platformio_override.usermods.ini b/usermods/platformio_override.usermods.ini index c738077e9..5a0d8c3fb 100644 --- a/usermods/platformio_override.usermods.ini +++ b/usermods/platformio_override.usermods.ini @@ -8,7 +8,7 @@ build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32_USERMODS\" lib_deps = ${esp32.lib_deps} board_build.partitions = ${esp32.big_partitions} -usermod = ${usermods.custom_usermods} +custom_usermods = ${usermods.custom_usermods} [env:usermods_esp32c3] extends = esp32c3 @@ -18,7 +18,7 @@ build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"C3_USERMODS\" lib_deps = ${esp32.lib_deps} board_build.partitions = ${esp32.big_partitions} -usermod = ${usermods.custom_usermods} +custom_usermods = ${usermods.custom_usermods} [env:usermods_esp32s2] extends = esp32s2 @@ -27,7 +27,7 @@ build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"S2_USERMODS\" lib_deps = ${esp32.lib_deps} board_build.partitions = ${esp32.big_partitions} -usermod = ${usermods.custom_usermods} +custom_usermods = ${usermods.custom_usermods} [env:usermods_esp32s3] extends = esp32s3 @@ -36,6 +36,7 @@ build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"S3_USERMODS\" lib_deps = ${esp32.lib_deps} board_build.partitions = ${esp32.big_partitions} -usermod = ${usermods.custom_usermods} +custom_usermods = ${usermods.custom_usermods} -[usermods] \ No newline at end of file +[usermods] +# Added in CI \ No newline at end of file From 6c4d049c1acffc490116fa0e9fcce4bcc9739662 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 26 Apr 2025 15:06:07 +0100 Subject: [PATCH 064/153] force new line --- .github/workflows/usermods.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index 4de88449a..9b1dd2ab9 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -63,7 +63,8 @@ jobs: - name: Add usermods environment run: | cp -v usermods/platformio_override.usermods.ini platformio_override.ini - echo -n "custom_usermods = ${{ matrix.usermod }}" >> platformio_override.ini + echo >> platformio_override.ini + echo "custom_usermods = ${{ matrix.usermod }}" >> platformio_override.ini - name: Build firmware run: pio run -e ${{ matrix.environment }} From 19ba25772217db8efe878aed6f1bf10f1d2b426c Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 26 Apr 2025 15:07:31 +0100 Subject: [PATCH 065/153] usermod_esp32s3 --- .github/workflows/usermods.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/usermods.yml b/.github/workflows/usermods.yml index 9b1dd2ab9..a597d2bf5 100644 --- a/.github/workflows/usermods.yml +++ b/.github/workflows/usermods.yml @@ -35,7 +35,7 @@ jobs: fail-fast: false matrix: usermod: ${{ fromJSON(needs.get_usermod_envs.outputs.usermods) }} - environment: [usermods_esp32, usermods_esp32c3, usermods_esp32s2, usermod_esp32s3] + environment: [usermods_esp32, usermods_esp32c3, usermods_esp32s2, usermods_esp32s3] steps: - uses: actions/checkout@v4 - name: Set up Node.js From 92db9e0e002fe1c541005dae2de5d02df13a5ab4 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 26 Apr 2025 15:21:22 +0100 Subject: [PATCH 066/153] fix envs --- usermods/platformio_override.usermods.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/usermods/platformio_override.usermods.ini b/usermods/platformio_override.usermods.ini index 5a0d8c3fb..a2324ba4f 100644 --- a/usermods/platformio_override.usermods.ini +++ b/usermods/platformio_override.usermods.ini @@ -8,10 +8,11 @@ build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"ESP32_USERMODS\" lib_deps = ${esp32.lib_deps} board_build.partitions = ${esp32.big_partitions} +board_build.flash_mode = dio custom_usermods = ${usermods.custom_usermods} [env:usermods_esp32c3] -extends = esp32c3 +extends = esp32c3dev board = esp32-c3-devkitm-1 platform = ${esp32_idf_V4.platform} build_unflags = ${common.build_unflags} From f1c88bc38d5ef94d7c0a36af93ad033e42724a60 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Sat, 26 Apr 2025 15:54:14 +0100 Subject: [PATCH 067/153] fix envs --- usermods/platformio_override.usermods.ini | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/usermods/platformio_override.usermods.ini b/usermods/platformio_override.usermods.ini index a2324ba4f..127b5d33a 100644 --- a/usermods/platformio_override.usermods.ini +++ b/usermods/platformio_override.usermods.ini @@ -19,25 +19,31 @@ build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"C3_USERMODS\" lib_deps = ${esp32.lib_deps} board_build.partitions = ${esp32.big_partitions} +board_build.flash_mode = qio custom_usermods = ${usermods.custom_usermods} [env:usermods_esp32s2] -extends = esp32s2 +extends = esp32s2dev +board = esp32-c3-devkitm-1 platform = ${esp32_idf_V4.platform} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"S2_USERMODS\" lib_deps = ${esp32.lib_deps} board_build.partitions = ${esp32.big_partitions} +board_build.flash_mode = dio custom_usermods = ${usermods.custom_usermods} [env:usermods_esp32s3] extends = esp32s3 +board = esp32-s3-devkitc-1 platform = ${esp32_idf_V4.platform} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=\"S3_USERMODS\" lib_deps = ${esp32.lib_deps} board_build.partitions = ${esp32.big_partitions} custom_usermods = ${usermods.custom_usermods} +board_build.flash_mode = qio + [usermods] # Added in CI \ No newline at end of file From c934776f45b0b6f3372ea01188deaa38393d1822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Sat, 26 Apr 2025 20:08:15 +0200 Subject: [PATCH 068/153] Address issues reported --- wled00/FX.h | 7 ++++--- wled00/FX_2Dfcn.cpp | 11 ++++++----- wled00/FX_fcn.cpp | 25 ++++++++++++++++--------- wled00/bus_manager.cpp | 26 ++++++++++++++------------ wled00/const.h | 5 +++++ wled00/fcn_declare.h | 29 ++++++++++++++++------------- wled00/file.cpp | 4 ++-- wled00/mqtt.cpp | 10 +++++----- wled00/pin_manager.cpp | 2 +- wled00/pin_manager.h | 5 ----- wled00/presets.cpp | 18 +++++++++--------- wled00/util.cpp | 6 +++--- wled00/wled.cpp | 2 ++ wled00/wled.h | 3 +++ 14 files changed, 86 insertions(+), 67 deletions(-) diff --git a/wled00/FX.h b/wled00/FX.h index c060c868d..9c895c0c0 100755 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -833,8 +833,8 @@ class WS2812FX { _length(DEFAULT_LED_COUNT), _transitionDur(750), _frametime(FRAMETIME_FIXED), + _cumulativeFps(WLED_FPS << FPS_CALC_SHIFT), _targetFps(WLED_FPS), - _cumulativeFps(WLED_FPS), _isServicing(false), _isOffRefreshRequired(false), _hasWhiteChannel(false), @@ -845,7 +845,8 @@ class WS2812FX { _callback(nullptr), customMappingTable(nullptr), customMappingSize(0), - _lastShow(0) + _lastShow(0), + _lastServiceShow(0) { _mode.reserve(_modeCount); // allocate memory to prevent initial fragmentation (does not increase size()) _modeData.reserve(_modeCount); // allocate memory to prevent initial fragmentation (does not increase size()) @@ -1011,8 +1012,8 @@ class WS2812FX { uint16_t _transitionDur; uint16_t _frametime; + uint16_t _cumulativeFps; uint8_t _targetFps; - uint8_t _cumulativeFps; // will require only 1 byte struct { diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index 69c743183..9a3c6fbe8 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -86,7 +86,7 @@ void WS2812FX::setUpMatrix() { JsonArray map = pDoc->as(); gapSize = map.size(); if (!map.isNull() && gapSize >= matrixSize) { // not an empty map - gapTable = static_cast(w_malloc(gapSize)); + gapTable = static_cast(p_malloc(gapSize)); if (gapTable) for (size_t i = 0; i < gapSize; i++) { gapTable[i] = constrain(map[i], -1, 1); } @@ -113,7 +113,7 @@ void WS2812FX::setUpMatrix() { } // delete gap array as we no longer need it - w_free(gapTable); + p_free(gapTable); resume(); #ifdef WLED_DEBUG @@ -246,11 +246,11 @@ void Segment::blur2D(uint8_t blur_x, uint8_t blur_y, bool smear) const { const unsigned cols = vWidth(); const unsigned rows = vHeight(); const auto XY = [&](unsigned x, unsigned y){ return x + y*cols; }; - uint32_t lastnew; + uint32_t lastnew; // not necessary to initialize lastnew and last, as both will be initialized by the first loop iteration uint32_t last; if (blur_x) { const uint8_t keepx = smear ? 255 : 255 - blur_x; - const uint8_t seepx = blur_x >> (1 + smear); + 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; @@ -273,7 +273,7 @@ void Segment::blur2D(uint8_t blur_x, uint8_t blur_y, bool smear) const { } if (blur_y) { const uint8_t keepy = smear ? 255 : 255 - blur_y; - const uint8_t seepy = blur_y >> (1 + smear); + const uint8_t seepy = blur_y >> 1; for (unsigned col = 0; col < cols; col++) { uint32_t carryover = BLACK; uint32_t curnew = BLACK; @@ -584,6 +584,7 @@ void Segment::drawCharacter(unsigned char chr, int16_t x, int16_t y, uint8_t w, chr -= 32; // align with font table entries const int font = w*h; + // if col2 == BLACK then use currently selected palette for gradient otherwise create gradient from color and col2 CRGBPalette16 grad = col2 ? CRGBPalette16(CRGB(color), CRGB(col2)) : SEGPALETTE; // selected palette as gradient for (int i = 0; i> (1 + smear); + uint8_t seep = blur_amount >> 1; unsigned vlength = vLength(); uint32_t carryover = BLACK; - uint32_t lastnew; + uint32_t lastnew; // not necessary to initialize lastnew and last, as both will be initialized by the first loop iteration uint32_t last; uint32_t curnew = BLACK; for (unsigned i = 0; i < vlength; i++) { @@ -1198,7 +1198,12 @@ void WS2812FX::finalizeInit() { void WS2812FX::service() { unsigned long nowUp = millis(); // Be aware, millis() rolls over every 49 days now = nowUp + timebase; - if (nowUp - _lastShow < MIN_FRAME_DELAY || _suspend) return; + unsigned long elapsed = nowUp - _lastServiceShow; + if (_suspend || elapsed <= MIN_FRAME_DELAY) return; // keep wifi alive - no matter if triggered or unlimited + if (!_triggered && (_targetFps != FPS_UNLIMITED)) { // unlimited mode = no frametime + if (elapsed < _frametime) return; // too early for service + } + bool doShow = false; _isServicing = true; @@ -1255,15 +1260,16 @@ void WS2812FX::service() { } #ifdef WLED_DEBUG - if (millis() - nowUp > _frametime) DEBUG_PRINTF_P(PSTR("Slow effects %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); + if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR("Slow effects %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); #endif if (doShow && !_suspend) { yield(); Segment::handleRandomPalette(); // slowly transition random palette; move it into for loop when each segment has individual random palette + _lastServiceShow = nowUp; // update timestamp, for precise FPS control show(); } #ifdef WLED_DEBUG - if (millis() - nowUp > _frametime) DEBUG_PRINTF_P(PSTR("Slow strip %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); + if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR("Slow strip %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime); #endif _triggered = false; @@ -1612,8 +1618,8 @@ void WS2812FX::show() { if (newBri != _brightness) BusManager::setBrightness(_brightness); if (diff > 0) { // skip calculation if no time has passed - int fpsCurr = (1000 << FPS_CALC_SHIFT) / diff; // fixed point math (shift left for better precision) - _cumulativeFps += ((fpsCurr - (_cumulativeFps << FPS_CALC_SHIFT)) / FPS_CALC_AVG + ((1<> FPS_CALC_SHIFT; // simple PI controller over FPS_CALC_AVG frames + 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; } } @@ -1653,8 +1659,9 @@ void WS2812FX::waitForIt() { }; void WS2812FX::setTargetFps(unsigned fps) { - if (fps > 0 && fps <= 120) _targetFps = fps; - _frametime = 1000 / _targetFps; + if (fps <= 250) _targetFps = fps; + if (_targetFps > 0) _frametime = 1000 / _targetFps; + else _frametime = MIN_FRAME_DELAY; // unlimited mode } void WS2812FX::setCCT(uint16_t k) { diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 59d6f5435..56e594795 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -37,19 +37,21 @@ uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, const //util.cpp // PSRAM allocation wrappers #ifndef ESP8266 -void *w_malloc(size_t); // prefer PSRAM over DRAM -void *w_calloc(size_t, size_t); // prefer PSRAM over DRAM -void *w_realloc(void *, size_t); // prefer PSRAM over DRAM -inline void w_free(void *ptr) { heap_caps_free(ptr); } -void *d_malloc(size_t); // prefer DRAM over PSRAM -void *d_calloc(size_t, size_t); // prefer DRAM over PSRAM -void *d_realloc(void *, size_t); // prefer DRAM over PSRAM -inline void d_free(void *ptr) { heap_caps_free(ptr); } +extern "C" { + void *p_malloc(size_t); // prefer PSRAM over DRAM + void *p_calloc(size_t, size_t); // prefer PSRAM over DRAM + void *p_realloc(void *, size_t); // prefer PSRAM over DRAM + inline void p_free(void *ptr) { heap_caps_free(ptr); } + void *d_malloc(size_t); // prefer DRAM over PSRAM + void *d_calloc(size_t, size_t); // prefer DRAM over PSRAM + void *d_realloc(void *, size_t); // prefer DRAM over PSRAM + inline void d_free(void *ptr) { heap_caps_free(ptr); } +} #else -#define w_malloc malloc -#define w_calloc calloc -#define w_realloc realloc -#define w_free free +#define p_malloc malloc +#define p_calloc calloc +#define p_realloc realloc +#define p_free free #define d_malloc malloc #define d_calloc calloc #define d_realloc realloc diff --git a/wled00/const.h b/wled00/const.h index 877e02dc1..cfcd0a6b8 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -1,3 +1,4 @@ +#pragma once #ifndef WLED_CONST_H #define WLED_CONST_H @@ -49,6 +50,9 @@ #define WLED_MAX_ANALOG_CHANNELS 5 #define WLED_MIN_VIRTUAL_BUSSES 3 // no longer used for bus creation but used to distinguish S2/S3 in UI #else + #if !defined(LEDC_CHANNEL_MAX) || !defined(LEDC_SPEED_MODE_MAX) + #include "driver/ledc.h" // needed for analog/LEDC channel counts + #endif #define WLED_MAX_ANALOG_CHANNELS (LEDC_CHANNEL_MAX*LEDC_SPEED_MODE_MAX) #if defined(CONFIG_IDF_TARGET_ESP32C3) // 2 RMT, 6 LEDC, only has 1 I2S but NPB does not support it ATM #define WLED_MAX_DIGITAL_CHANNELS 2 @@ -76,6 +80,7 @@ #undef WLED_MAX_BUSSES #endif #define WLED_MAX_BUSSES (WLED_MAX_DIGITAL_CHANNELS+WLED_MAX_ANALOG_CHANNELS) +static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit"); // Maximum number of pins per output. 5 for RGBCCT analog LEDs. #define OUTPUT_MAX_PINS 5 diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 62d05ecd5..486e5c562 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -173,7 +173,8 @@ inline uint32_t color_blend16(uint32_t c1, uint32_t c2, uint16_t b) { return col CRGBPalette16 generateHarmonicRandomPalette(const CRGBPalette16 &basepalette); CRGBPalette16 generateRandomPalette(); void loadCustomPalettes(); -#define getPaletteCount() (13 + GRADIENT_PALETTE_COUNT + customPalettes.size()) +extern std::vector customPalettes; +inline size_t getPaletteCount() { return 13 + GRADIENT_PALETTE_COUNT + customPalettes.size(); } inline uint32_t colorFromRgbw(byte* rgbw) { return uint32_t((byte(rgbw[3]) << 24) | (byte(rgbw[0]) << 16) | (byte(rgbw[1]) << 8) | (byte(rgbw[2]))); } void hsv2rgb(const CHSV32& hsv, uint32_t& rgb); void colorHStoRGB(uint16_t hue, byte sat, byte* rgb); @@ -545,19 +546,21 @@ inline uint8_t hw_random8(uint32_t lowerlimit, uint32_t upperlimit) { uint32_t r // PSRAM allocation wrappers #ifndef ESP8266 -void *w_malloc(size_t); // prefer PSRAM over DRAM -void *w_calloc(size_t, size_t); // prefer PSRAM over DRAM -void *w_realloc(void *, size_t); // prefer PSRAM over DRAM -inline void w_free(void *ptr) { heap_caps_free(ptr); } -void *d_malloc(size_t); // prefer DRAM over PSRAM -void *d_calloc(size_t, size_t); // prefer DRAM over PSRAM -void *d_realloc(void *, size_t); // prefer DRAM over PSRAM -inline void d_free(void *ptr) { heap_caps_free(ptr); } +extern "C" { + void *p_malloc(size_t); // prefer PSRAM over DRAM + void *p_calloc(size_t, size_t); // prefer PSRAM over DRAM + void *p_realloc(void *, size_t); // prefer PSRAM over DRAM + inline void p_free(void *ptr) { heap_caps_free(ptr); } + void *d_malloc(size_t); // prefer DRAM over PSRAM + void *d_calloc(size_t, size_t); // prefer DRAM over PSRAM + void *d_realloc(void *, size_t); // prefer DRAM over PSRAM + inline void d_free(void *ptr) { heap_caps_free(ptr); } +} #else -#define w_malloc malloc -#define w_calloc calloc -#define w_realloc realloc -#define w_free free +#define p_malloc malloc +#define p_calloc calloc +#define p_realloc realloc +#define p_free free #define d_malloc malloc #define d_calloc calloc #define d_realloc realloc diff --git a/wled00/file.cpp b/wled00/file.cpp index 4df331997..c1960e616 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -392,7 +392,7 @@ static const uint8_t *getPresetCache(size_t &size) { if ((presetsModifiedTime != presetsCachedTime) || (presetsCachedValidate != cacheInvalidate)) { if (presetsCached) { - w_free(presetsCached); + p_free(presetsCached); presetsCached = nullptr; } } @@ -403,7 +403,7 @@ static const uint8_t *getPresetCache(size_t &size) { presetsCachedTime = presetsModifiedTime; presetsCachedValidate = cacheInvalidate; presetsCachedSize = 0; - presetsCached = (uint8_t*)w_malloc(file.size() + 1); + presetsCached = (uint8_t*)p_malloc(file.size() + 1); if (presetsCached) { presetsCachedSize = file.size(); file.read(presetsCached, presetsCachedSize); diff --git a/wled00/mqtt.cpp b/wled00/mqtt.cpp index a1f659510..19d4e889c 100644 --- a/wled00/mqtt.cpp +++ b/wled00/mqtt.cpp @@ -68,8 +68,8 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp } if (index == 0) { // start (1st partial packet or the only packet) - w_free(payloadStr); // release buffer if it exists - payloadStr = static_cast(w_malloc(total+1)); // allocate new buffer + p_free(payloadStr); // release buffer if it exists + payloadStr = static_cast(p_malloc(total+1)); // allocate new buffer } if (payloadStr == nullptr) return; // buffer not allocated @@ -94,7 +94,7 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp } else { // Non-Wled Topic used here. Probably a usermod subscribed to this topic. UsermodManager::onMqttMessage(topic, payloadStr); - w_free(payloadStr); + p_free(payloadStr); payloadStr = nullptr; return; } @@ -124,7 +124,7 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp // topmost topic (just wled/MAC) parseMQTTBriPayload(payloadStr); } - w_free(payloadStr); + p_free(payloadStr); payloadStr = nullptr; } @@ -196,7 +196,7 @@ bool initMqtt() if (!mqttEnabled || mqttServer[0] == 0 || !WLED_CONNECTED) return false; if (mqtt == nullptr) { - void *ptr = w_malloc(sizeof(AsyncMqttClient)); + void *ptr = p_malloc(sizeof(AsyncMqttClient)); mqtt = new (ptr) AsyncMqttClient(); // use placement new (into PSRAM), client will never be deleted if (!mqtt) return false; mqtt->onMessage(onMqttMessage); diff --git a/wled00/pin_manager.cpp b/wled00/pin_manager.cpp index 6f1652301..cdbb85267 100644 --- a/wled00/pin_manager.cpp +++ b/wled00/pin_manager.cpp @@ -1,5 +1,5 @@ -#include "pin_manager.h" #include "wled.h" +#include "pin_manager.h" #ifdef ARDUINO_ARCH_ESP32 #ifdef bitRead diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index b285b6ee5..662e499b2 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -3,11 +3,6 @@ /* * 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) diff --git a/wled00/presets.cpp b/wled00/presets.cpp index 0a4380f8c..fed2c1ed9 100644 --- a/wled00/presets.cpp +++ b/wled00/presets.cpp @@ -57,10 +57,10 @@ static void doSaveState() { */ #if defined(ARDUINO_ARCH_ESP32) if (!persist) { - w_free(tmpRAMbuffer); + p_free(tmpRAMbuffer); size_t len = measureJson(*pDoc) + 1; // if possible use SPI RAM on ESP32 - tmpRAMbuffer = (char*)w_malloc(len); + tmpRAMbuffer = (char*)p_malloc(len); if (tmpRAMbuffer!=nullptr) { serializeJson(*pDoc, tmpRAMbuffer, len); } else { @@ -77,8 +77,8 @@ static void doSaveState() { // clean up saveLedmap = -1; presetToSave = 0; - w_free(saveName); - w_free(quickLoad); + p_free(saveName); + p_free(quickLoad); saveName = nullptr; quickLoad = nullptr; playlistSave = false; @@ -203,7 +203,7 @@ void handlePresets() #if defined(ARDUINO_ARCH_ESP32) //Aircoookie recommended not to delete buffer if (tmpPreset==255 && tmpRAMbuffer!=nullptr) { - w_free(tmpRAMbuffer); + p_free(tmpRAMbuffer); tmpRAMbuffer = nullptr; } #endif @@ -217,8 +217,8 @@ void handlePresets() //called from handleSet(PS=) [network callback (sObj is empty), IR (irrational), deserializeState, UDP] and deserializeState() [network callback (filedoc!=nullptr)] void savePreset(byte index, const char* pname, JsonObject sObj) { - if (!saveName) saveName = static_cast(w_malloc(33)); - if (!quickLoad) quickLoad = static_cast(w_malloc(9)); + if (!saveName) saveName = static_cast(p_malloc(33)); + if (!quickLoad) quickLoad = static_cast(p_malloc(9)); if (!saveName || !quickLoad) return; if (index == 0 || (index > 250 && index < 255)) return; @@ -264,8 +264,8 @@ void savePreset(byte index, const char* pname, JsonObject sObj) presetsModifiedTime = toki.second(); //unix time updateFSInfo(); } - w_free(saveName); - w_free(quickLoad); + p_free(saveName); + p_free(quickLoad); saveName = nullptr; quickLoad = nullptr; } else { diff --git a/wled00/util.cpp b/wled00/util.cpp index 8276c9c87..97e1e3b03 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -620,7 +620,7 @@ int32_t hw_random(int32_t lowerlimit, int32_t upperlimit) { } #ifndef ESP8266 -void *w_malloc(size_t size) { +void *p_malloc(size_t size) { int caps1 = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; int caps2 = MALLOC_CAP_DEFAULT | MALLOC_CAP_8BIT; if (psramSafe) { @@ -630,7 +630,7 @@ void *w_malloc(size_t size) { return heap_caps_malloc(size, caps2); } -void *w_realloc(void *ptr, size_t size) { +void *p_realloc(void *ptr, size_t size) { int caps1 = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; int caps2 = MALLOC_CAP_DEFAULT | MALLOC_CAP_8BIT; if (psramSafe) { @@ -640,7 +640,7 @@ void *w_realloc(void *ptr, size_t size) { return heap_caps_realloc(ptr, size, caps2); } -void *w_calloc(size_t count, size_t size) { +void *p_calloc(size_t count, size_t size) { int caps1 = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT; int caps2 = MALLOC_CAP_DEFAULT | MALLOC_CAP_8BIT; if (psramSafe) { diff --git a/wled00/wled.cpp b/wled00/wled.cpp index e22b94c77..111fc12e9 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -753,7 +753,9 @@ void WLED::handleConnection() static bool scanDone = true; static byte stacO = 0; const unsigned long now = millis(); + #ifdef WLED_DEBUG const unsigned long nowS = now/1000; + #endif const bool wifiConfigured = WLED_WIFI_CONFIGURED; // ignore connection handling if WiFi is configured and scan still running diff --git a/wled00/wled.h b/wled00/wled.h index 230b9cbcf..efcbacc11 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -64,6 +64,9 @@ //This is generally a terrible idea, but improves boot success on boards with a 3.3v regulator + cap setup that can't provide 400mA peaks //#define WLED_DISABLE_BROWNOUT_DET +#include +#include + // Library inclusions. #include #ifdef ESP8266 From 125a21da75365883020a58ceba1857abf1ecce5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Sat, 26 Apr 2025 20:15:02 +0200 Subject: [PATCH 069/153] Comment --- wled00/wled.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/wled.h b/wled00/wled.h index efcbacc11..600ed010b 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -606,7 +606,7 @@ WLED_GLOBAL bool wasConnected _INIT(false); // color WLED_GLOBAL byte lastRandomIndex _INIT(0); // used to save last random color so the new one is not the same WLED_GLOBAL std::vector customPalettes; // custom palettes -WLED_GLOBAL uint8_t paletteBlend _INIT(0); // determines bending and wrapping of palette: 0: blend, wrap if moving (SEGMENT.speed>0); 1: blend, always wrap; 2: blend, never wrap; 3: don't blend or wrap +WLED_GLOBAL uint8_t paletteBlend _INIT(0); // determines blending and wrapping of palette: 0: blend, wrap if moving (SEGMENT.speed>0); 1: blend, always wrap; 2: blend, never wrap; 3: don't blend or wrap // transitions WLED_GLOBAL uint8_t blendingStyle _INIT(0); // effect blending/transitionig style From f721efca1e6f57b2b9cfcee022fa9fd4e907341d Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 28 Apr 2025 21:12:27 +0200 Subject: [PATCH 070/153] fixed wrong gravity setting, added option for no trail (#4665) --- wled00/FX.cpp | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 3daf2c0fd..b3d3eeae8 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9308,7 +9308,7 @@ uint16_t mode_particleFireworks1D(void) { uint8_t *forcecounter; if (SEGMENT.call == 0) { // initialization - if (!initParticleSystem1D(PartSys, 4, 150, 4, true)) // init + if (!initParticleSystem1D(PartSys, 4, 150, 4, true)) // init advanced particle system return mode_static(); // allocation failed or is single pixel PartSys->setKillOutOfBounds(true); PartSys->sources[0].sourceFlags.custom1 = 1; // set rocket state to standby @@ -9324,11 +9324,8 @@ uint16_t mode_particleFireworks1D(void) { PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - int32_t gravity = (1 + (SEGMENT.speed >> 3)); - if (!SEGMENT.check1) // gravity enabled for sparks - PartSys->setGravity(0); // disable - else - PartSys->setGravity(gravity); // set gravity + int32_t gravity = (1 + (SEGMENT.speed >> 3)); // gravity value used for rocket speed calculation + PartSys->setGravity(SEGMENT.speed ? gravity : 0); // set gravity if (PartSys->sources[0].sourceFlags.custom1 == 1) { // rocket is on standby PartSys->sources[0].source.ttl--; @@ -9343,8 +9340,8 @@ uint16_t mode_particleFireworks1D(void) { PartSys->sources[0].source.hue = hw_random16(); PartSys->sources[0].var = 10; // emit variation PartSys->sources[0].v = -10; // emit speed - PartSys->sources[0].minLife = 100; - PartSys->sources[0].maxLife = 300; + PartSys->sources[0].minLife = 30; + PartSys->sources[0].maxLife = SEGMENT.check2 ? 400 : 40; PartSys->sources[0].source.x = 0; // start from bottom uint32_t speed = sqrt((gravity * ((PartSys->maxX >> 2) + hw_random16(PartSys->maxX >> 1))) >> 4); // set speed such that rocket explods in frame PartSys->sources[0].source.vx = min(speed, (uint32_t)127); @@ -9383,11 +9380,11 @@ uint16_t mode_particleFireworks1D(void) { PartSys->sources[0].maxLife = 1300; PartSys->sources[0].source.ttl = 100 + hw_random16(64 - (SEGMENT.speed >> 2)); // standby time til next launch PartSys->sources[0].sat = 7 + (SEGMENT.custom3 << 3); //color saturation TODO: replace saturation with something more useful? - PartSys->sources[0].size = hw_random16(64); // random particle size in explosion + PartSys->sources[0].size = hw_random16(SEGMENT.intensity); // random particle size in explosion uint32_t explosionsize = 8 + (PartSys->maxXpixel >> 2) + (PartSys->sources[0].source.x >> (PS_P_RADIUS_SHIFT_1D - 1)); explosionsize += hw_random16((explosionsize * SEGMENT.intensity) >> 8); for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles - if (SEGMENT.check2) + if (SEGMENT.check1) // colorful mode PartSys->sources[0].source.hue = hw_random16(); //random color for each particle PartSys->sprayEmit(PartSys->sources[0]); // emit a particle } @@ -9407,7 +9404,7 @@ uint16_t mode_particleFireworks1D(void) { return FRAMETIME; } -static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Saturation,,Colorful,Smooth;,!;!;1;sx=150,c2=30,c3=31,o2=1"; +static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Saturation,Colorful,Trail,Smooth;,!;!;1;sx=150,c2=30,c3=31,o1=1,o2=1"; /* Particle based Sparkle effect From 7998650e608c6f31dad692b3c267de5e488f32a4 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Fri, 28 Mar 2025 20:12:30 -0400 Subject: [PATCH 071/153] Fix up usermod libArchive settings The ConfigureProjectLibBuilder process will flush and reload the library settings from the on-disk manifests if any new library is installed at that stage. This has the side effect of reverting the libArchive setting applied to usermods which was performed prior to that call. Apply the setting afterwards, instead. Fixes #4597 --- pio-scripts/load_usermods.py | 39 ++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/pio-scripts/load_usermods.py b/pio-scripts/load_usermods.py index ab3c6476a..27a4590d1 100644 --- a/pio-scripts/load_usermods.py +++ b/pio-scripts/load_usermods.py @@ -2,16 +2,19 @@ Import('env') import os.path from collections import deque from pathlib import Path # For OS-agnostic path manipulation +from platformio.builder.tools.piolib import LibBuilderBase from platformio.package.manager.library import LibraryPackageManager usermod_dir = Path(env["PROJECT_DIR"]) / "usermods" -all_usermods = [f for f in usermod_dir.iterdir() if f.is_dir() and f.joinpath('library.json').exists()] +# "usermods" environment: expand list of usermods to everything in the folder if env['PIOENV'] == "usermods": # Add all usermods + all_usermods = [f for f in usermod_dir.iterdir() if f.is_dir() and f.joinpath('library.json').exists()] env.GetProjectConfig().set(f"env:usermods", 'custom_usermods', " ".join([f.name for f in all_usermods])) -def find_usermod(mod: str): +# Utility functions +def find_usermod(mod: str) -> Path: """Locate this library in the usermods folder. We do this to avoid needing to rename a bunch of folders; this could be removed later @@ -28,6 +31,13 @@ def find_usermod(mod: str): return mp raise RuntimeError(f"Couldn't locate module {mod} in usermods directory!") +def is_wled_module(dep: LibBuilderBase) -> bool: + """Returns true if the specified library is a wled module + """ + return usermod_dir in Path(dep.src_dir).parents or str(dep.name).startswith("wled-") + +## Script starts here +# Process usermod option usermods = env.GetProjectOption("custom_usermods","") if usermods: # Inject usermods in to project lib_deps @@ -82,13 +92,6 @@ old_ConfigureProjectLibBuilder = env.ConfigureProjectLibBuilder # Our new wrapper def wrapped_ConfigureProjectLibBuilder(xenv): - # Update usermod properties - # Set libArchive before build actions are added - for um in (um for um in xenv.GetLibBuilders() if usermod_dir in Path(um.src_dir).parents): - build = um._manifest.get("build", {}) - build["libArchive"] = False - um._manifest["build"] = build - # Call the wrapped function result = old_ConfigureProjectLibBuilder.clone(xenv)() @@ -102,12 +105,18 @@ def wrapped_ConfigureProjectLibBuilder(xenv): for dep in result.depbuilders: cached_add_includes(dep, processed_deps, extra_include_dirs) - for um in [dep for dep in result.depbuilders if usermod_dir in Path(dep.src_dir).parents]: - # Add the wled folder to the include path - um.env.PrependUnique(CPPPATH=wled_dir) - # Add WLED's own dependencies - for dir in extra_include_dirs: - um.env.PrependUnique(CPPPATH=dir) + for dep in result.depbuilders: + if is_wled_module(dep): + # Add the wled folder to the include path + dep.env.PrependUnique(CPPPATH=wled_dir) + # Add WLED's own dependencies + for dir in extra_include_dirs: + dep.env.PrependUnique(CPPPATH=dir) + # Enforce that libArchive is not set; we must link them directly to the executable + if dep.lib_archive: + build = dep._manifest.get("build", {}) + build["libArchive"] = False + dep._manifest["build"] = build return result From 6464c620c701bf3ad8297ab81572f23c45404353 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Wed, 30 Apr 2025 21:56:56 -0400 Subject: [PATCH 072/153] load_usermods: Enforce CPPPATH type Ensure that entries put in CPPPATH are always strings so SCons can correctlly deduplicate. --- pio-scripts/load_usermods.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pio-scripts/load_usermods.py b/pio-scripts/load_usermods.py index 27a4590d1..eeebcbf27 100644 --- a/pio-scripts/load_usermods.py +++ b/pio-scripts/load_usermods.py @@ -108,10 +108,10 @@ def wrapped_ConfigureProjectLibBuilder(xenv): for dep in result.depbuilders: if is_wled_module(dep): # Add the wled folder to the include path - dep.env.PrependUnique(CPPPATH=wled_dir) + dep.env.PrependUnique(CPPPATH=str(wled_dir)) # Add WLED's own dependencies for dir in extra_include_dirs: - dep.env.PrependUnique(CPPPATH=dir) + dep.env.PrependUnique(CPPPATH=str(dir)) # Enforce that libArchive is not set; we must link them directly to the executable if dep.lib_archive: build = dep._manifest.get("build", {}) From d9b086cbe9798003bc7db51f01f24cf1b7ef2f0f Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 1 May 2025 13:39:23 +0200 Subject: [PATCH 073/153] Bugfixes in PS, improvements to PS Fireworks 1D (#4673) - fixed inconsitencies in size rendering - fixed palette being wrapped in color by position and color by age modes - Fixed bug in memory layout: for some unknown reason, if flags come before particles, last flag is sometimes overwritten, changing memory laout seems to fix that - New color modes in PS Fireworks 1D: - custom3 slider < 16: lower saturation (check1: single color or multi-color explosions) - custom3 slider <= 23: full saturation (check1: single color or multi-color explosions) - custom3 slider > 23: color by speed (check 1 has not effect here) - custom slider = max: color by age or color by position (depends on check1) --- wled00/FX.cpp | 49 ++++++++++++++++++------------- wled00/FXparticleSystem.cpp | 58 +++++++++++++++++++++++-------------- wled00/FXparticleSystem.h | 12 ++++---- 3 files changed, 72 insertions(+), 47 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index b3d3eeae8..1a7560181 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8127,7 +8127,7 @@ uint16_t mode_particlepit(void) { PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; // set particle size if (SEGMENT.custom1 == 255) { - PartSys->setParticleSize(1); // set global size to 1 for advanced rendering + PartSys->setParticleSize(1); // set global size to 1 for advanced rendering (no single pixel particles) PartSys->advPartProps[i].size = hw_random16(SEGMENT.custom1); // set each particle to random size } else { PartSys->setParticleSize(SEGMENT.custom1); // set global size @@ -9085,7 +9085,6 @@ uint16_t mode_particlePinball(void) { PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom PartSys->setKillOutOfBounds(true); // out of bounds particles dont return - PartSys->setUsedParticles(255); // use all available particles for init SEGENV.aux0 = 1; SEGENV.aux1 = 5000; //set out of range to ensure uptate on first call } @@ -9308,6 +9307,7 @@ uint16_t mode_particleFireworks1D(void) { uint8_t *forcecounter; if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 4, 150, 4, true)) // init advanced particle system if (!initParticleSystem1D(PartSys, 4, 150, 4, true)) // init advanced particle system return mode_static(); // allocation failed or is single pixel PartSys->setKillOutOfBounds(true); @@ -9321,9 +9321,7 @@ uint16_t mode_particleFireworks1D(void) { // Particle System settings PartSys->updateSystem(); // update system properties (dimensions and data pointers) forcecounter = PartSys->PSdataEnd; - PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur - int32_t gravity = (1 + (SEGMENT.speed >> 3)); // gravity value used for rocket speed calculation PartSys->setGravity(SEGMENT.speed ? gravity : 0); // set gravity @@ -9337,17 +9335,17 @@ uint16_t mode_particleFireworks1D(void) { SEGENV.aux0 = 0; PartSys->sources[0].sourceFlags.custom1 = 0; //flag used for rocket state - PartSys->sources[0].source.hue = hw_random16(); + PartSys->sources[0].source.hue = hw_random16(); // different color for each launch PartSys->sources[0].var = 10; // emit variation PartSys->sources[0].v = -10; // emit speed PartSys->sources[0].minLife = 30; - PartSys->sources[0].maxLife = SEGMENT.check2 ? 400 : 40; + PartSys->sources[0].maxLife = SEGMENT.check2 ? 400 : 60; PartSys->sources[0].source.x = 0; // start from bottom uint32_t speed = sqrt((gravity * ((PartSys->maxX >> 2) + hw_random16(PartSys->maxX >> 1))) >> 4); // set speed such that rocket explods in frame PartSys->sources[0].source.vx = min(speed, (uint32_t)127); PartSys->sources[0].source.ttl = 4000; PartSys->sources[0].sat = 30; // low saturation exhaust - PartSys->sources[0].size = 0; // default size + PartSys->sources[0].size = SEGMENT.check3; // single or double pixel rendering PartSys->sources[0].sourceFlags.reversegrav = false ; // normal gravity if (SEGENV.aux0) { // inverted rockets launch from end @@ -9360,17 +9358,17 @@ uint16_t mode_particleFireworks1D(void) { } else { // rocket is launched int32_t rocketgravity = -gravity; - int32_t speed = PartSys->sources[0].source.vx; + int32_t currentspeed = PartSys->sources[0].source.vx; if (SEGENV.aux0) { // negative speed rocket rocketgravity = -rocketgravity; - speed = -speed; + currentspeed = -currentspeed; } PartSys->applyForce(PartSys->sources[0].source, rocketgravity, forcecounter[0]); PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); - PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); // increase speed by calling the move function twice, also ages twice + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); // increase rocket speed by calling the move function twice, also ages twice uint32_t rocketheight = SEGENV.aux0 ? PartSys->maxX - PartSys->sources[0].source.x : PartSys->sources[0].source.x; - if (speed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee + if (currentspeed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee PartSys->sources[0].source.ttl = min((uint32_t)50, rocketheight >> (PS_P_RADIUS_SHIFT_1D + 3)); // alive for a few more frames if (PartSys->sources[0].source.ttl < 2) { // explode @@ -9379,19 +9377,32 @@ uint16_t mode_particleFireworks1D(void) { PartSys->sources[0].minLife = 600; PartSys->sources[0].maxLife = 1300; PartSys->sources[0].source.ttl = 100 + hw_random16(64 - (SEGMENT.speed >> 2)); // standby time til next launch - PartSys->sources[0].sat = 7 + (SEGMENT.custom3 << 3); //color saturation TODO: replace saturation with something more useful? - PartSys->sources[0].size = hw_random16(SEGMENT.intensity); // random particle size in explosion + PartSys->sources[0].sat = SEGMENT.custom3 < 16 ? 10 + (SEGMENT.custom3 << 4) : 255; //color saturation + PartSys->sources[0].size = SEGMENT.check3 ? hw_random16(SEGMENT.intensity) : 0; // random particle size in explosion uint32_t explosionsize = 8 + (PartSys->maxXpixel >> 2) + (PartSys->sources[0].source.x >> (PS_P_RADIUS_SHIFT_1D - 1)); explosionsize += hw_random16((explosionsize * SEGMENT.intensity) >> 8); for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles - if (SEGMENT.check1) // colorful mode - PartSys->sources[0].source.hue = hw_random16(); //random color for each particle - PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + int idx = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + if(SEGMENT.custom3 > 23) { + if(SEGMENT.custom3 == 31) { // highest slider value + PartSys->setColorByAge(SEGMENT.check1); // color by age if colorful mode is enabled + PartSys->setColorByPosition(!SEGMENT.check1); // color by position otherwise + } + else { // if custom3 is set to high value (but not highest), set particle color by initial speed + PartSys->particles[idx].hue = map(abs(PartSys->particles[idx].vx), 0, PartSys->sources[0].var, 0, 16 + hw_random16(200)); // set hue according to speed, use random amount of palette width + PartSys->particles[idx].hue += PartSys->sources[0].source.hue; // add hue offset of the rocket (random starting color) + } + } + else { + if (SEGMENT.check1) // colorful mode + PartSys->sources[0].source.hue = hw_random16(); //random color for each particle + } } } } - if ((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false) // every second frame and not in standby + if ((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false && PartSys->sources[0].source.ttl > 50) // every second frame and not in standby and not about to explode PartSys->sprayEmit(PartSys->sources[0]); // emit exhaust particle + if ((SEGMENT.call & 0x03) == 0) // every fourth frame PartSys->applyFriction(1); // apply friction to all particles @@ -9401,10 +9412,9 @@ uint16_t mode_particleFireworks1D(void) { if (PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan else PartSys->particles[i].ttl = 0; } - return FRAMETIME; } -static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Saturation,Colorful,Trail,Smooth;,!;!;1;sx=150,c2=30,c3=31,o1=1,o2=1"; +static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Color,Colorful,Trail,Smooth;,!;!;1;c2=30,o1=1"; /* Particle based Sparkle effect @@ -9925,7 +9935,6 @@ uint16_t mode_particle1DGEQ(void) { PartSys->sources[i].maxLife = 240 + SEGMENT.intensity; PartSys->sources[i].sat = 255; PartSys->sources[i].size = SEGMENT.custom1; - PartSys->setParticleSize(SEGMENT.custom1); PartSys->sources[i].source.x = (spacing >> 1) + spacing * i; //distribute evenly } diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp index fadc98763..85264b7f1 100644 --- a/wled00/FXparticleSystem.cpp +++ b/wled00/FXparticleSystem.cpp @@ -48,6 +48,7 @@ ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t num for (uint32_t i = 0; i < numSources; i++) { sources[i].source.sat = 255; //set saturation to max by default sources[i].source.ttl = 1; //set source alive + sources[i].sourceFlags.asByte = 0; // all flags disabled } } @@ -559,6 +560,10 @@ void ParticleSystem2D::pointAttractor(const uint32_t particleindex, PSparticle & void ParticleSystem2D::render() { CRGB baseRGB; uint32_t brightness; // particle brightness, fades if dying + TBlendType blend = LINEARBLEND; // default color rendering: wrap palette + if (particlesettings.colorByAge) { + blend = LINEARBLEND_NOWRAP; + } if (motionBlur) { // motion-blurring active for (int32_t y = 0; y <= maxYpixel; y++) { @@ -581,11 +586,11 @@ void ParticleSystem2D::render() { if (fireIntesity) { // fire mode brightness = (uint32_t)particles[i].ttl * (3 + (fireIntesity >> 5)) + 20; brightness = min(brightness, (uint32_t)255); - baseRGB = ColorFromPaletteWLED(SEGPALETTE, brightness, 255); + baseRGB = ColorFromPaletteWLED(SEGPALETTE, brightness, 255, LINEARBLEND_NOWRAP); } else { brightness = min((particles[i].ttl << 1), (int)255); - baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255); + baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255, blend); if (particles[i].sat < 255) { CHSV32 baseHSV; rgb2hsv((uint32_t((byte(baseRGB.r) << 16) | (byte(baseRGB.g) << 8) | (byte(baseRGB.b)))), baseHSV); // convert to HSV @@ -598,6 +603,7 @@ void ParticleSystem2D::render() { renderParticle(i, brightness, baseRGB, particlesettings.wrapX, particlesettings.wrapY); } + // apply global size rendering if (particlesize > 1) { uint32_t passes = particlesize / 64 + 1; // number of blur passes, four passes max uint32_t bluramount = particlesize; @@ -605,7 +611,7 @@ void ParticleSystem2D::render() { for (uint32_t i = 0; i < passes; i++) { if (i == 2) // for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) bitshift = 1; - blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift); + blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift); bluramount -= 64; } } @@ -626,7 +632,11 @@ void ParticleSystem2D::render() { // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer __attribute__((optimize("O2"))) void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB& color, const bool wrapX, const bool wrapY) { - if (particlesize == 0) { // single pixel rendering + uint32_t size = particlesize; + if (advPartProps && advPartProps[particleindex].size > 0) // use advanced size properties (0 means use global size including single pixel rendering) + size = advPartProps[particleindex].size; + + if (size == 0) { // single pixel rendering uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT; uint32_t y = particles[particleindex].y >> PS_P_RADIUS_SHIFT; if (x <= (uint32_t)maxXpixel && y <= (uint32_t)maxYpixel) { @@ -667,7 +677,7 @@ __attribute__((optimize("O2"))) void ParticleSystem2D::renderParticle(const uint pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightness) >> PS_P_SURFACE pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightness) >> PS_P_SURFACE - if (advPartProps && advPartProps[particleindex].size > 0) { //render particle to a bigger size + if (advPartProps && advPartProps[particleindex].size > 1) { //render particle to a bigger size CRGB renderbuffer[100]; // 10x10 pixel buffer memset(renderbuffer, 0, sizeof(renderbuffer)); // clear buffer //particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10 @@ -962,15 +972,13 @@ void ParticleSystem2D::updateSystem(void) { // FX handles the PSsources, need to tell this function how many there are void ParticleSystem2D::updatePSpointers(bool isadvanced, bool sizecontrol) { PSPRINTLN("updatePSpointers"); - // DEBUG_PRINT(F("*** PS pointers ***")); - // DEBUG_PRINTF_P(PSTR("this PS %p "), this); // Note on memory alignment: // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. - particleFlags = reinterpret_cast(this + 1); // pointer to particle flags - particles = reinterpret_cast(particleFlags + numParticles); // pointer to particles - sources = reinterpret_cast(particles + numParticles); // pointer to source(s) at data+sizeof(ParticleSystem2D) + particles = reinterpret_cast(this + 1); // pointer to particles + particleFlags = reinterpret_cast(particles + numParticles); // pointer to particle flags + sources = reinterpret_cast(particleFlags + numParticles); // pointer to source(s) at data+sizeof(ParticleSystem2D) framebuffer = reinterpret_cast(sources + numSources); // pointer to framebuffer // align pointer after framebuffer uintptr_t p = reinterpret_cast(framebuffer + (maxXpixel+1)*(maxYpixel+1)); @@ -1155,6 +1163,7 @@ ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, // initialize some default non-zero values most FX use for (uint32_t i = 0; i < numSources; i++) { sources[i].source.ttl = 1; //set source alive + sources[i].sourceFlags.asByte = 0; // all flags disabled } if (isadvanced) { @@ -1269,7 +1278,7 @@ int32_t ParticleSystem1D::sprayEmit(const PSsource1D &emitter) { particles[emitIndex].x = emitter.source.x; particles[emitIndex].hue = emitter.source.hue; particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife); - particleFlags[emitIndex].collide = emitter.sourceFlags.collide; + particleFlags[emitIndex].collide = emitter.sourceFlags.collide; // TODO: could just set all flags (asByte) but need to check if that breaks any of the FX particleFlags[emitIndex].reversegrav = emitter.sourceFlags.reversegrav; particleFlags[emitIndex].perpetual = emitter.sourceFlags.perpetual; if (advPartProps) { @@ -1419,6 +1428,10 @@ void ParticleSystem1D::applyFriction(int32_t coefficient) { void ParticleSystem1D::render() { CRGB baseRGB; uint32_t brightness; // particle brightness, fades if dying + TBlendType blend = LINEARBLEND; // default color rendering: wrap palette + if (particlesettings.colorByAge || particlesettings.colorByPosition) { + blend = LINEARBLEND_NOWRAP; + } #ifdef ESP8266 // no local buffer on ESP8266 if (motionBlur) @@ -1442,7 +1455,7 @@ void ParticleSystem1D::render() { // generate RGB values for particle brightness = min(particles[i].ttl << 1, (int)255); - baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255); + baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255, blend); if (advPartProps) { //saturation is advanced property in 1D system if (advPartProps[i].sat < 255) { @@ -1489,9 +1502,9 @@ void ParticleSystem1D::render() { // calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer __attribute__((optimize("O2"))) void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB &color, const bool wrap) { uint32_t size = particlesize; - if (advPartProps) { // use advanced size properties + if (advPartProps) // use advanced size properties (1D system has no large size global rendering TODO: add large global rendering?) size = advPartProps[particleindex].size; - } + if (size == 0) { //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles (and updating it uses more code) uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D; if (x <= (uint32_t)maxXpixel) { //by making x unsigned there is no need to check < 0 as it will overflow @@ -1736,30 +1749,32 @@ void ParticleSystem1D::updatePSpointers(bool isadvanced) { // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. - particleFlags = reinterpret_cast(this + 1); // pointer to particle flags - particles = reinterpret_cast(particleFlags + numParticles); // pointer to particles - sources = reinterpret_cast(particles + numParticles); // pointer to source(s) + particles = reinterpret_cast(this + 1); // pointer to particles + particleFlags = reinterpret_cast(particles + numParticles); // pointer to particle flags + sources = reinterpret_cast(particleFlags + numParticles); // pointer to source(s) #ifdef ESP8266 // no local buffer on ESP8266 PSdataEnd = reinterpret_cast(sources + numSources); #else framebuffer = reinterpret_cast(sources + numSources); // pointer to framebuffer // align pointer after framebuffer to 4bytes - uintptr_t p = reinterpret_cast(framebuffer + (maxXpixel+1)); + uintptr_t p = reinterpret_cast(framebuffer + (maxXpixel+1)); // maxXpixel is SEGMENT.virtualLength() - 1 p = (p + 3) & ~0x03; // align to 4-byte boundary PSdataEnd = reinterpret_cast(p); // pointer to first available byte after the PS for FX additional data #endif if (isadvanced) { advPartProps = reinterpret_cast(PSdataEnd); - PSdataEnd = reinterpret_cast(advPartProps + numParticles); + PSdataEnd = reinterpret_cast(advPartProps + numParticles); // since numParticles is a multiple of 4, this is always aligned to 4 bytes. No need to add padding bytes here } #ifdef WLED_DEBUG_PS PSPRINTLN(" PS Pointers: "); PSPRINT(" PS : 0x"); Serial.println((uintptr_t)this, HEX); - PSPRINT(" Sources : 0x"); - Serial.println((uintptr_t)sources, HEX); + PSPRINT(" Particleflags : 0x"); + Serial.println((uintptr_t)particleFlags, HEX); PSPRINT(" Particles : 0x"); Serial.println((uintptr_t)particles, HEX); + PSPRINT(" Sources : 0x"); + Serial.println((uintptr_t)sources, HEX); #endif } @@ -1780,6 +1795,7 @@ uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadva numberofParticles = numberofParticles < 20 ? 20 : numberofParticles; // 20 minimum //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) numberofParticles = (numberofParticles+3) & ~0x03; // note: with a separate particle buffer, this is probably unnecessary + PSPRINTLN(" calc numparticles:" + String(numberofParticles)) return numberofParticles; } diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h index 695a3a028..d188ae23d 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -96,7 +96,7 @@ typedef union { // struct for additional particle settings (option) typedef struct { // 2 bytes - uint8_t size; // particle size, 255 means 10 pixels in diameter + uint8_t size; // particle size, 255 means 10 pixels in diameter, 0 means use global size (including single pixel rendering) uint8_t forcecounter; // counter for applying forces to individual particles } PSadvancedParticle; @@ -127,7 +127,7 @@ typedef struct { int8_t var; // variation of emitted speed (adds random(+/- var) to speed) int8_t vx; // emitting speed int8_t vy; - uint8_t size; // particle size (advanced property) + uint8_t size; // particle size (advanced property), global size is added on top to this size } PSsource; // class uses approximately 60 bytes @@ -214,7 +214,7 @@ private: uint8_t gforcecounter; // counter for global gravity int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) // global particle properties for basic particles - uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles) + uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles, set to 0 or 1 for standard advanced particle rendering) uint8_t motionBlur; // motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 uint8_t smearBlur; // 2D smeared blurring of full frame }; @@ -289,7 +289,7 @@ typedef union { // struct for additional particle settings (optional) typedef struct { uint8_t sat; //color saturation - uint8_t size; // particle size, 255 means 10 pixels in diameter + uint8_t size; // particle size, 255 means 10 pixels in diameter, this overrides global size setting uint8_t forcecounter; } PSadvancedParticle1D; @@ -333,7 +333,7 @@ public: void setColorByPosition(const bool enable); void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero void setSmearBlur(const uint8_t bluramount); // enable 1D smeared blurring of full frame - void setParticleSize(const uint8_t size); //size 0 = 1 pixel, size 1 = 2 pixels, is overruled by advanced particle size + void setParticleSize(const uint8_t size); //size 0 = 1 pixel, size 1 = 2 pixels, is overruled if advanced particle is used void setGravity(int8_t force = 8); void enableParticleCollisions(bool enable, const uint8_t hardness = 255); @@ -377,7 +377,7 @@ private: uint8_t forcecounter; // counter for globally applied forces uint16_t collisionStartIdx; // particle array start index for collision detection //global particle properties for basic particles - uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels + uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, is overruled by advanced particle size uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations uint8_t smearBlur; // smeared blurring of full frame }; From a8dd2435ec82b4f348b8a22f2caae0392f95c9da Mon Sep 17 00:00:00 2001 From: Will Miles Date: Tue, 6 May 2025 22:11:17 -0400 Subject: [PATCH 074/153] Revert "Usermods: Remove libArchive" This reverts commit 0d44e7ec272b130f7e3eccf1d1f20a5405a327fe. --- usermods/ADS1115_v2/library.json | 1 + usermods/AHT10_v2/library.json | 1 + usermods/Analog_Clock/library.json | 3 ++- usermods/Animated_Staircase/library.json | 3 ++- usermods/BH1750_v2/library.json | 1 + usermods/BME280_v2/library.json | 1 + usermods/Battery/library.json | 3 ++- usermods/Cronixie/library.json | 3 ++- usermods/EleksTube_IPS/library.json.disabled | 1 + usermods/Internal_Temperature_v2/library.json | 3 ++- usermods/LD2410_v2/library.json | 1 + usermods/LDR_Dusk_Dawn_v2/library.json | 3 ++- usermods/MY9291/library.json | 1 + usermods/PIR_sensor_switch/library.json | 3 ++- usermods/PWM_fan/library.json | 1 + usermods/RTC/library.json | 3 ++- usermods/SN_Photoresistor/library.json | 3 ++- usermods/ST7789_display/library.json.disabled | 3 ++- usermods/Si7021_MQTT_HA/library.json | 1 + usermods/TetrisAI_v2/library.json | 3 ++- usermods/boblight/library.json | 3 ++- usermods/buzzer/library.json | 3 ++- usermods/deep_sleep/library.json | 3 ++- usermods/multi_relay/library.json | 3 ++- usermods/pwm_outputs/library.json | 3 ++- usermods/sd_card/library.json | 3 ++- usermods/seven_segment_display/library.json | 3 ++- usermods/seven_segment_display_reloaded/library.json | 3 ++- usermods/sht/library.json | 1 + usermods/smartnest/library.json | 3 ++- usermods/stairway_wipe_basic/library.json | 3 ++- usermods/usermod_rotary_brightness_color/library.json | 3 ++- usermods/usermod_v2_HttpPullLightControl/library.json | 3 ++- usermods/usermod_v2_animartrix/library.json | 1 + usermods/usermod_v2_auto_save/library.json | 3 ++- usermods/usermod_v2_four_line_display_ALT/library.json | 1 + usermods/usermod_v2_klipper_percentage/library.json | 3 ++- usermods/usermod_v2_ping_pong_clock/library.json | 3 ++- usermods/usermod_v2_rotary_encoder_ui_ALT/library.json | 1 + usermods/usermod_v2_word_clock/library.json | 3 ++- usermods/wizlights/library.json | 3 ++- usermods/word-clock-matrix/library.json | 3 ++- 42 files changed, 71 insertions(+), 29 deletions(-) diff --git a/usermods/ADS1115_v2/library.json b/usermods/ADS1115_v2/library.json index 9f0c021ce..5e5d7e450 100644 --- a/usermods/ADS1115_v2/library.json +++ b/usermods/ADS1115_v2/library.json @@ -1,5 +1,6 @@ { "name": "ADS1115_v2", + "build": { "libArchive": false }, "dependencies": { "Adafruit BusIO": "https://github.com/adafruit/Adafruit_BusIO#1.13.2", "Adafruit ADS1X15": "https://github.com/adafruit/Adafruit_ADS1X15#2.4.0" diff --git a/usermods/AHT10_v2/library.json b/usermods/AHT10_v2/library.json index 54f8c1715..fa6c2a6fe 100644 --- a/usermods/AHT10_v2/library.json +++ b/usermods/AHT10_v2/library.json @@ -1,5 +1,6 @@ { "name": "AHT10_v2", + "build": { "libArchive": false }, "dependencies": { "enjoyneering/AHT10":"~1.1.0" } diff --git a/usermods/Analog_Clock/library.json b/usermods/Analog_Clock/library.json index 3ed596dc7..f76cf4268 100644 --- a/usermods/Analog_Clock/library.json +++ b/usermods/Analog_Clock/library.json @@ -1,3 +1,4 @@ { - "name": "Analog_Clock" + "name": "Analog_Clock", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/Animated_Staircase/library.json b/usermods/Animated_Staircase/library.json index a2c50ea4c..015b15cef 100644 --- a/usermods/Animated_Staircase/library.json +++ b/usermods/Animated_Staircase/library.json @@ -1,3 +1,4 @@ { - "name": "Animated_Staircase" + "name": "Animated_Staircase", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/BH1750_v2/library.json b/usermods/BH1750_v2/library.json index 13740e6c9..8323d1abb 100644 --- a/usermods/BH1750_v2/library.json +++ b/usermods/BH1750_v2/library.json @@ -1,5 +1,6 @@ { "name": "BH1750_v2", + "build": { "libArchive": false }, "dependencies": { "claws/BH1750":"^1.2.0" } diff --git a/usermods/BME280_v2/library.json b/usermods/BME280_v2/library.json index 626fb8b2b..cfdfe1ba1 100644 --- a/usermods/BME280_v2/library.json +++ b/usermods/BME280_v2/library.json @@ -1,5 +1,6 @@ { "name": "BME280_v2", + "build": { "libArchive": false }, "dependencies": { "finitespace/BME280":"~3.0.0" } diff --git a/usermods/Battery/library.json b/usermods/Battery/library.json index d6b8ad38a..8e71c60a7 100644 --- a/usermods/Battery/library.json +++ b/usermods/Battery/library.json @@ -1,3 +1,4 @@ { - "name": "Battery" + "name": "Battery", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/Cronixie/library.json b/usermods/Cronixie/library.json index a1454a79a..4a1b6988e 100644 --- a/usermods/Cronixie/library.json +++ b/usermods/Cronixie/library.json @@ -1,3 +1,4 @@ { - "name": "Cronixie" + "name": "Cronixie", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/EleksTube_IPS/library.json.disabled b/usermods/EleksTube_IPS/library.json.disabled index eddd12b88..d143638e4 100644 --- a/usermods/EleksTube_IPS/library.json.disabled +++ b/usermods/EleksTube_IPS/library.json.disabled @@ -1,5 +1,6 @@ { "name:": "EleksTube_IPS", + "build": { "libArchive": false }, "dependencies": { "TFT_eSPI" : "2.5.33" } diff --git a/usermods/Internal_Temperature_v2/library.json b/usermods/Internal_Temperature_v2/library.json index 571176f45..b1826ab45 100644 --- a/usermods/Internal_Temperature_v2/library.json +++ b/usermods/Internal_Temperature_v2/library.json @@ -1,3 +1,4 @@ { - "name": "Internal_Temperature_v2" + "name": "Internal_Temperature_v2", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/LD2410_v2/library.json b/usermods/LD2410_v2/library.json index 92ad54da7..757ec4047 100644 --- a/usermods/LD2410_v2/library.json +++ b/usermods/LD2410_v2/library.json @@ -1,5 +1,6 @@ { "name": "LD2410_v2", + "build": { "libArchive": false }, "dependencies": { "ncmreynolds/ld2410":"^0.1.3" } diff --git a/usermods/LDR_Dusk_Dawn_v2/library.json b/usermods/LDR_Dusk_Dawn_v2/library.json index be06c3a3a..709967ea7 100644 --- a/usermods/LDR_Dusk_Dawn_v2/library.json +++ b/usermods/LDR_Dusk_Dawn_v2/library.json @@ -1,3 +1,4 @@ { - "name": "LDR_Dusk_Dawn_v2" + "name": "LDR_Dusk_Dawn_v2", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/MY9291/library.json b/usermods/MY9291/library.json index e4c63eaf5..9c3a33d43 100644 --- a/usermods/MY9291/library.json +++ b/usermods/MY9291/library.json @@ -1,4 +1,5 @@ { "name": "MY9291", + "build": { "libArchive": false }, "platforms": ["espressif8266"] } \ No newline at end of file diff --git a/usermods/PIR_sensor_switch/library.json b/usermods/PIR_sensor_switch/library.json index d5ebb7689..b3cbcbbff 100644 --- a/usermods/PIR_sensor_switch/library.json +++ b/usermods/PIR_sensor_switch/library.json @@ -1,3 +1,4 @@ { - "name": "PIR_sensor_switch" + "name": "PIR_sensor_switch", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/PWM_fan/library.json b/usermods/PWM_fan/library.json index a8f7a9446..8ae3d7fd6 100644 --- a/usermods/PWM_fan/library.json +++ b/usermods/PWM_fan/library.json @@ -1,6 +1,7 @@ { "name": "PWM_fan", "build": { + "libArchive": false, "extraScript": "setup_deps.py" } } \ No newline at end of file diff --git a/usermods/RTC/library.json b/usermods/RTC/library.json index 8c103e06d..688dfc2d0 100644 --- a/usermods/RTC/library.json +++ b/usermods/RTC/library.json @@ -1,3 +1,4 @@ { - "name": "RTC" + "name": "RTC", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/SN_Photoresistor/library.json b/usermods/SN_Photoresistor/library.json index 45519dfa6..c896644f7 100644 --- a/usermods/SN_Photoresistor/library.json +++ b/usermods/SN_Photoresistor/library.json @@ -1,3 +1,4 @@ { - "name": "SN_Photoresistor" + "name": "SN_Photoresistor", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/ST7789_display/library.json.disabled b/usermods/ST7789_display/library.json.disabled index abcd4635c..725e20a65 100644 --- a/usermods/ST7789_display/library.json.disabled +++ b/usermods/ST7789_display/library.json.disabled @@ -1,3 +1,4 @@ { - "name:": "ST7789_display" + "name:": "ST7789_display", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/Si7021_MQTT_HA/library.json b/usermods/Si7021_MQTT_HA/library.json index cec2edfb1..e3d7635e3 100644 --- a/usermods/Si7021_MQTT_HA/library.json +++ b/usermods/Si7021_MQTT_HA/library.json @@ -1,5 +1,6 @@ { "name": "Si7021_MQTT_HA", + "build": { "libArchive": false }, "dependencies": { "finitespace/BME280":"3.0.0", "adafruit/Adafruit Si7021 Library" : "1.5.3" diff --git a/usermods/TetrisAI_v2/library.json b/usermods/TetrisAI_v2/library.json index bfff1aa4d..54aa22d35 100644 --- a/usermods/TetrisAI_v2/library.json +++ b/usermods/TetrisAI_v2/library.json @@ -1,3 +1,4 @@ { - "name": "TetrisAI_v2" + "name": "TetrisAI_v2", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/boblight/library.json b/usermods/boblight/library.json index 12debccf5..b54fb3505 100644 --- a/usermods/boblight/library.json +++ b/usermods/boblight/library.json @@ -1,3 +1,4 @@ { - "name": "boblight" + "name": "boblight", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/buzzer/library.json b/usermods/buzzer/library.json index c6af3158b..0dbb547e3 100644 --- a/usermods/buzzer/library.json +++ b/usermods/buzzer/library.json @@ -1,3 +1,4 @@ { - "name": "buzzer" + "name": "buzzer", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/deep_sleep/library.json b/usermods/deep_sleep/library.json index 8b39b2eed..82e32c994 100644 --- a/usermods/deep_sleep/library.json +++ b/usermods/deep_sleep/library.json @@ -1,3 +1,4 @@ { - "name": "deep_sleep" + "name": "deep_sleep", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/multi_relay/library.json b/usermods/multi_relay/library.json index f1caf7d42..a5e5c6934 100644 --- a/usermods/multi_relay/library.json +++ b/usermods/multi_relay/library.json @@ -1,3 +1,4 @@ { - "name": "multi_relay" + "name": "multi_relay", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/pwm_outputs/library.json b/usermods/pwm_outputs/library.json index bcdb8d5a6..a01068bd4 100644 --- a/usermods/pwm_outputs/library.json +++ b/usermods/pwm_outputs/library.json @@ -1,3 +1,4 @@ { - "name": "pwm_outputs" + "name": "pwm_outputs", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/sd_card/library.json b/usermods/sd_card/library.json index 44d3e3495..33e8f98f2 100644 --- a/usermods/sd_card/library.json +++ b/usermods/sd_card/library.json @@ -1,3 +1,4 @@ { - "name": "sd_card" + "name": "sd_card", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/seven_segment_display/library.json b/usermods/seven_segment_display/library.json index 653c3d7ff..f78aad87b 100644 --- a/usermods/seven_segment_display/library.json +++ b/usermods/seven_segment_display/library.json @@ -1,3 +1,4 @@ { - "name": "seven_segment_display" + "name": "seven_segment_display", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/seven_segment_display_reloaded/library.json b/usermods/seven_segment_display_reloaded/library.json index 4e84e38ed..1b7d0687f 100644 --- a/usermods/seven_segment_display_reloaded/library.json +++ b/usermods/seven_segment_display_reloaded/library.json @@ -1,6 +1,7 @@ { "name": "seven_segment_display_reloaded", "build": { + "libArchive": false, "extraScript": "setup_deps.py" - } + } } \ No newline at end of file diff --git a/usermods/sht/library.json b/usermods/sht/library.json index 6849628ca..0916e9a37 100644 --- a/usermods/sht/library.json +++ b/usermods/sht/library.json @@ -1,5 +1,6 @@ { "name": "sht", + "build": { "libArchive": false }, "dependencies": { "robtillaart/SHT85": "~0.3.3" } diff --git a/usermods/smartnest/library.json b/usermods/smartnest/library.json index 9b428f6b1..3e9ea63a9 100644 --- a/usermods/smartnest/library.json +++ b/usermods/smartnest/library.json @@ -1,3 +1,4 @@ { - "name": "smartnest" + "name": "smartnest", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/stairway_wipe_basic/library.json b/usermods/stairway_wipe_basic/library.json index b75baef6b..f7d353b59 100644 --- a/usermods/stairway_wipe_basic/library.json +++ b/usermods/stairway_wipe_basic/library.json @@ -1,3 +1,4 @@ { - "name": "stairway_wipe_basic" + "name": "stairway_wipe_basic", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/usermod_rotary_brightness_color/library.json b/usermods/usermod_rotary_brightness_color/library.json index ecf73c0f9..4f7a146a0 100644 --- a/usermods/usermod_rotary_brightness_color/library.json +++ b/usermods/usermod_rotary_brightness_color/library.json @@ -1,3 +1,4 @@ { - "name": "usermod_rotary_brightness_color" + "name": "usermod_rotary_brightness_color", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/usermod_v2_HttpPullLightControl/library.json b/usermods/usermod_v2_HttpPullLightControl/library.json index a9252fc0c..870753b99 100644 --- a/usermods/usermod_v2_HttpPullLightControl/library.json +++ b/usermods/usermod_v2_HttpPullLightControl/library.json @@ -1,3 +1,4 @@ { - "name": "usermod_v2_HttpPullLightControl" + "name": "usermod_v2_HttpPullLightControl", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/usermod_v2_animartrix/library.json b/usermods/usermod_v2_animartrix/library.json index 4552be330..667572bad 100644 --- a/usermods/usermod_v2_animartrix/library.json +++ b/usermods/usermod_v2_animartrix/library.json @@ -1,5 +1,6 @@ { "name": "animartrix", + "build": { "libArchive": false }, "dependencies": { "Animartrix": "https://github.com/netmindz/animartrix.git#b172586" } diff --git a/usermods/usermod_v2_auto_save/library.json b/usermods/usermod_v2_auto_save/library.json index d703487a7..127767eb0 100644 --- a/usermods/usermod_v2_auto_save/library.json +++ b/usermods/usermod_v2_auto_save/library.json @@ -1,3 +1,4 @@ { - "name": "auto_save" + "name": "auto_save", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/usermod_v2_four_line_display_ALT/library.json b/usermods/usermod_v2_four_line_display_ALT/library.json index 87a690f03..b16448223 100644 --- a/usermods/usermod_v2_four_line_display_ALT/library.json +++ b/usermods/usermod_v2_four_line_display_ALT/library.json @@ -1,5 +1,6 @@ { "name": "four_line_display_ALT", + "build": { "libArchive": false }, "dependencies": { "U8g2": "~2.34.4", "Wire": "" diff --git a/usermods/usermod_v2_klipper_percentage/library.json b/usermods/usermod_v2_klipper_percentage/library.json index 7a2df6b23..962dda14e 100644 --- a/usermods/usermod_v2_klipper_percentage/library.json +++ b/usermods/usermod_v2_klipper_percentage/library.json @@ -1,3 +1,4 @@ { - "name": "usermod_v2_klipper_percentage" + "name": "usermod_v2_klipper_percentage", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/usermod_v2_ping_pong_clock/library.json b/usermods/usermod_v2_ping_pong_clock/library.json index d6c079e58..4b272eca4 100644 --- a/usermods/usermod_v2_ping_pong_clock/library.json +++ b/usermods/usermod_v2_ping_pong_clock/library.json @@ -1,3 +1,4 @@ { - "name": "usermod_v2_ping_pong_clock" + "name": "usermod_v2_ping_pong_clock", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/library.json b/usermods/usermod_v2_rotary_encoder_ui_ALT/library.json index ddb6334b1..7c828d087 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/library.json +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/library.json @@ -1,6 +1,7 @@ { "name": "rotary_encoder_ui_ALT", "build": { + "libArchive": false, "extraScript": "setup_deps.py" } } \ No newline at end of file diff --git a/usermods/usermod_v2_word_clock/library.json b/usermods/usermod_v2_word_clock/library.json index b0dcebc6e..0ea99d810 100644 --- a/usermods/usermod_v2_word_clock/library.json +++ b/usermods/usermod_v2_word_clock/library.json @@ -1,3 +1,4 @@ { - "name": "usermod_v2_word_clock" + "name": "usermod_v2_word_clock", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/wizlights/library.json b/usermods/wizlights/library.json index 114424e5d..0bfc097c7 100644 --- a/usermods/wizlights/library.json +++ b/usermods/wizlights/library.json @@ -1,3 +1,4 @@ { - "name": "wizlights" + "name": "wizlights", + "build": { "libArchive": false } } \ No newline at end of file diff --git a/usermods/word-clock-matrix/library.json b/usermods/word-clock-matrix/library.json index afeae5025..7bc3919de 100644 --- a/usermods/word-clock-matrix/library.json +++ b/usermods/word-clock-matrix/library.json @@ -1,3 +1,4 @@ { - "name": "word-clock-matrix" + "name": "word-clock-matrix", + "build": { "libArchive": false } } \ No newline at end of file From 849d5e6667c2f5e682281d21f46dd995606e006c Mon Sep 17 00:00:00 2001 From: Will Miles Date: Tue, 6 May 2025 22:37:57 -0400 Subject: [PATCH 075/153] Add libArchive to other usermods Not all of them were removed by the reverted commit; re-do the rest of them. --- usermods/BH1750_v2/library.json | 2 +- usermods/BME68X_v2/library.json | 1 + usermods/EXAMPLE/library.json | 1 + usermods/INA226_v2/library.json | 1 + usermods/usermod_v2_RF433/library.json | 1 + usermods/usermod_v2_brightness_follow_sun/library.json | 3 ++- 6 files changed, 7 insertions(+), 2 deletions(-) diff --git a/usermods/BH1750_v2/library.json b/usermods/BH1750_v2/library.json index 8323d1abb..4e32099b0 100644 --- a/usermods/BH1750_v2/library.json +++ b/usermods/BH1750_v2/library.json @@ -1,6 +1,6 @@ { "name": "BH1750_v2", - "build": { "libArchive": false }, + "build": { "libArchive": false }, "dependencies": { "claws/BH1750":"^1.2.0" } diff --git a/usermods/BME68X_v2/library.json b/usermods/BME68X_v2/library.json index 2f1e1a310..b315aa5d4 100644 --- a/usermods/BME68X_v2/library.json +++ b/usermods/BME68X_v2/library.json @@ -1,5 +1,6 @@ { "name": "BME68X", + "build": { "libArchive": false }, "dependencies": { "boschsensortec/BSEC Software Library":"^1.8.1492" } diff --git a/usermods/EXAMPLE/library.json b/usermods/EXAMPLE/library.json index dd8a7e5dd..d0dc2f88e 100644 --- a/usermods/EXAMPLE/library.json +++ b/usermods/EXAMPLE/library.json @@ -1,4 +1,5 @@ { "name": "EXAMPLE", + "build": { "libArchive": false }, "dependencies": {} } diff --git a/usermods/INA226_v2/library.json b/usermods/INA226_v2/library.json index ab6c81fbd..34fcd3683 100644 --- a/usermods/INA226_v2/library.json +++ b/usermods/INA226_v2/library.json @@ -1,5 +1,6 @@ { "name": "INA226_v2", + "build": { "libArchive": false }, "dependencies": { "wollewald/INA226_WE":"~1.2.9" } diff --git a/usermods/usermod_v2_RF433/library.json b/usermods/usermod_v2_RF433/library.json index d809d3a0d..d8de29b8a 100644 --- a/usermods/usermod_v2_RF433/library.json +++ b/usermods/usermod_v2_RF433/library.json @@ -1,5 +1,6 @@ { "name": "usermod_v2_RF433", + "build": { "libArchive": false }, "dependencies": { "sui77/rc-switch":"2.6.4" } diff --git a/usermods/usermod_v2_brightness_follow_sun/library.json b/usermods/usermod_v2_brightness_follow_sun/library.json index 6120d873e..dec00e55b 100644 --- a/usermods/usermod_v2_brightness_follow_sun/library.json +++ b/usermods/usermod_v2_brightness_follow_sun/library.json @@ -1,3 +1,4 @@ { - "name": "brightness_follow_sun" + "name": "brightness_follow_sun", + "build": { "libArchive": false } } \ No newline at end of file From ee3864175d90fb04030ab592cd73ae9e0b2f8673 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Tue, 6 May 2025 21:55:06 -0400 Subject: [PATCH 076/153] load_usermods: Make missing libArchive an error Rather than try and fail to add this property, abort if it's missing from any requested usermod. --- pio-scripts/load_usermods.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pio-scripts/load_usermods.py b/pio-scripts/load_usermods.py index eeebcbf27..8cf625ff6 100644 --- a/pio-scripts/load_usermods.py +++ b/pio-scripts/load_usermods.py @@ -2,6 +2,8 @@ Import('env') import os.path from collections import deque from pathlib import Path # For OS-agnostic path manipulation +from click import secho +from SCons.Script import Exit from platformio.builder.tools.piolib import LibBuilderBase from platformio.package.manager.library import LibraryPackageManager @@ -105,6 +107,7 @@ def wrapped_ConfigureProjectLibBuilder(xenv): for dep in result.depbuilders: cached_add_includes(dep, processed_deps, extra_include_dirs) + broken_usermods = [] for dep in result.depbuilders: if is_wled_module(dep): # Add the wled folder to the include path @@ -114,9 +117,15 @@ def wrapped_ConfigureProjectLibBuilder(xenv): dep.env.PrependUnique(CPPPATH=str(dir)) # Enforce that libArchive is not set; we must link them directly to the executable if dep.lib_archive: - build = dep._manifest.get("build", {}) - build["libArchive"] = False - dep._manifest["build"] = build + broken_usermods.append(dep) + + if broken_usermods: + broken_usermods = [usermod.name for usermod in broken_usermods] + secho( + f"ERROR: libArchive=false is missing on usermod(s) {' '.join(broken_usermods)} -- modules will not compile in correctly", + fg="red", + err=True) + Exit(1) return result From 0fe722e478eedc013212059d60148f4a1353419c Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 9 May 2025 18:53:16 +0200 Subject: [PATCH 077/153] add new effect: PS Galaxy - parameters tuned to make it look good on most settings --- wled00/FX.cpp | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++ wled00/FX.h | 3 +- 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 1a7560181..b10fc0b74 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8958,6 +8958,104 @@ uint16_t mode_particleblobs(void) { return FRAMETIME; } static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o3=1"; + +/* + Particle Galaxy, particles spiral like in a galaxy + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particlegalaxy(void) { + ParticleSystem2D *PartSys = nullptr; + PSsettings2D sourcesettings; + sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1, 0, true)) // init using 1 source and advanced particle settings + return mode_static(); // allocation failed or not 2D + PartSys->sources[0].source.vx = -4; // will collide with wall and get random bounce direction + PartSys->sources[0].source.x = PartSys->maxX >> 1; // start in the center + PartSys->sources[0].source.y = PartSys->maxY >> 1; + PartSys->sources[0].sourceFlags.perpetual = true; //source does not age + PartSys->sources[0].maxLife = 4000; // lifetime in frames + PartSys->sources[0].minLife = 800; + PartSys->setWallHardness(255); //bounce forever + PartSys->setWallRoughness(200); //randomize wall bounce + } + else { + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + } + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + uint8_t particlesize = SEGMENT.custom1; + if(SEGMENT.check3) + particlesize = SEGMENT.custom1 ? 1 : 0; // set size to 0 (single pixel) or 1 (quad pixel) so motion blur works and adds streaks + PartSys->setParticleSize(particlesize); // set size globally + PartSys->setMotionBlur(250 * SEGMENT.check3); // adds trails to single/quad pixel particles, no effect if size > 1 + + if ((SEGMENT.call % ((33 - SEGMENT.custom3) >> 1)) == 0) // change hue of emitted particles + PartSys->sources[0].source.hue+=2; + + if (hw_random8() < (10 + (SEGMENT.intensity >> 1))) // 5%-55% chance to emit a particle in this frame + PartSys->sprayEmit(PartSys->sources[0]); + + if ((SEGMENT.call & 0x3) == 0) // every 4th frame, move the emitter + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &sourcesettings); + + // move alive particles in a spiral motion (or almost straight in fast starfield mode) + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { //check all particles + if (PartSys->particles[i].ttl == 0) continue; //skip dead particles + + int32_t centerx = PartSys->maxX >> 1; // center of matrix in subpixel coordinates + int32_t centery = PartSys->maxY >> 1; + // (dx/dy): vector pointing from particle to center + int32_t dx = centerx - PartSys->particles[i].x; + int32_t dy = centery - PartSys->particles[i].y; + //speed towards center: + int32_t distance = sqrt32_bw(dx * dx + dy * dy); // absolute distance to center + if (distance < 20) distance = 20; // avoid division by zero, keep a minimum + int32_t speedfactor; + if (SEGMENT.check2) { // starfield mode + PartSys->setKillOutOfBounds(true); + PartSys->sources[0].source.hue = hw_random16(); // start with random color + PartSys->sources[0].var = 7; // emiting variation + PartSys->sources[0].source.x = PartSys->maxX >> 1; // set emitter to center + PartSys->sources[0].source.y = PartSys->maxY >> 1; + speedfactor = 1 + (1 + (SEGMENT.speed >> 1)) * distance; // speed increases towards edge + PartSys->particles[i].x += (-speedfactor * dx) / 400000 - (dy >> 6); + PartSys->particles[i].y += (-speedfactor * dy) / 400000 + (dx >> 6); + } + else { + PartSys->setKillOutOfBounds(false); + PartSys->sources[0].var = 1; // emiting variation + speedfactor = 2 + (((50 + SEGMENT.speed) << 6) / distance); // speed increases towards center + // rotate clockwise + int32_t tempVx = (-speedfactor * dy); // speed is orthogonal to center vector + int32_t tempVy = (speedfactor * dx); + //add speed towards center to make particles spiral in + int vxc = (dx << 9) / (distance - 19); // subtract value from distance to make the pull-in force a bit stronger (helps on faster speeds) + int vyc = (dy << 9) / (distance - 19); + //apply velocity + PartSys->particles[i].x += (tempVx + vxc) / 1024; // note: cannot use bit shift as that causes asymmetric rounding + PartSys->particles[i].y += (tempVy + vyc) / 1024; + + if (distance < 128) { // close to center + if (PartSys->particles[i].ttl > 3) + PartSys->particles[i].ttl -= 4; //age fast + PartSys->particles[i].sat = distance << 1; // turn white towards center + } + } + if(SEGMENT.custom3 == 31) // color by age but mapped to 1024 as particles have a long life, since age is random, this gives more or less random colors + PartSys->particles[i].hue = PartSys->particles[i].ttl >> 2; + else if(SEGMENT.custom3 == 0) // color by distance + PartSys->particles[i].hue = map(distance, 20, (PartSys->maxX + PartSys->maxY) >> 2, 0, 180); // color by distance to center + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEGALAXY[] PROGMEM = "PS Galaxy@!,!,Size,,Color,,Starfield,Trace;;!;2;pal=59,sx=80,c1=2,c3=4"; + #endif //WLED_DISABLE_PARTICLESYSTEM2D #endif // WLED_DISABLE_2D @@ -10657,6 +10755,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECIRCULARGEQ); addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS); + addEffect(FX_MODE_PARTICLEGALAXY, &mode_particlegalaxy, _data_FX_MODE_PARTICLEGALAXY); #endif // WLED_DISABLE_PARTICLESYSTEM2D #endif // WLED_DISABLE_2D diff --git a/wled00/FX.h b/wled00/FX.h index 6481ff757..c56344702 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -353,7 +353,8 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_PS1DSONICSTREAM 214 #define FX_MODE_PS1DSONICBOOM 215 #define FX_MODE_PS1DSPRINGY 216 -#define MODE_COUNT 217 +#define FX_MODE_PARTICLEGALAXY 217 +#define MODE_COUNT 218 #define BLEND_STYLE_FADE 0x00 // universal From 891115eceeba59d4e7f29da12297be9311601873 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 9 May 2025 19:06:06 +0200 Subject: [PATCH 078/153] bugfix --- wled00/FX.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index b10fc0b74..9969d4315 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8977,6 +8977,7 @@ uint16_t mode_particlegalaxy(void) { PartSys->sources[0].sourceFlags.perpetual = true; //source does not age PartSys->sources[0].maxLife = 4000; // lifetime in frames PartSys->sources[0].minLife = 800; + PartSys->sources[0].source.hue = hw_random16(); // start with random color PartSys->setWallHardness(255); //bounce forever PartSys->setWallRoughness(200); //randomize wall bounce } @@ -9017,7 +9018,6 @@ uint16_t mode_particlegalaxy(void) { int32_t speedfactor; if (SEGMENT.check2) { // starfield mode PartSys->setKillOutOfBounds(true); - PartSys->sources[0].source.hue = hw_random16(); // start with random color PartSys->sources[0].var = 7; // emiting variation PartSys->sources[0].source.x = PartSys->maxX >> 1; // set emitter to center PartSys->sources[0].source.y = PartSys->maxY >> 1; @@ -9045,7 +9045,7 @@ uint16_t mode_particlegalaxy(void) { PartSys->particles[i].sat = distance << 1; // turn white towards center } } - if(SEGMENT.custom3 == 31) // color by age but mapped to 1024 as particles have a long life, since age is random, this gives more or less random colors + if(SEGMENT.custom3 == 31) // color by age but mapped to 1024 as particles have a long life, since age is random, this gives more or less random colors but PartSys->particles[i].hue = PartSys->particles[i].ttl >> 2; else if(SEGMENT.custom3 == 0) // color by distance PartSys->particles[i].hue = map(distance, 20, (PartSys->maxX + PartSys->maxY) >> 2, 0, 180); // color by distance to center From 5fe766399b1b2ea8cac8667e8c7fee0bd97cd881 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 9 May 2025 19:06:38 +0200 Subject: [PATCH 079/153] comment --- wled00/FX.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 9969d4315..56cd7669e 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9045,7 +9045,7 @@ uint16_t mode_particlegalaxy(void) { PartSys->particles[i].sat = distance << 1; // turn white towards center } } - if(SEGMENT.custom3 == 31) // color by age but mapped to 1024 as particles have a long life, since age is random, this gives more or less random colors but + if(SEGMENT.custom3 == 31) // color by age but mapped to 1024 as particles have a long life, since age is random, this gives more or less random colors PartSys->particles[i].hue = PartSys->particles[i].ttl >> 2; else if(SEGMENT.custom3 == 0) // color by distance PartSys->particles[i].hue = map(distance, 20, (PartSys->maxX + PartSys->maxY) >> 2, 0, 180); // color by distance to center From 608aff1e17dbbf947e78062adbe2f59e58ce1900 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 10 May 2025 09:11:07 +0200 Subject: [PATCH 080/153] slight speed improvement, fixed indentation --- wled00/FX.cpp | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 56cd7669e..9584ea8e7 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9004,30 +9004,34 @@ uint16_t mode_particlegalaxy(void) { PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &sourcesettings); // move alive particles in a spiral motion (or almost straight in fast starfield mode) + int32_t centerx = PartSys->maxX >> 1; // center of matrix in subpixel coordinates + int32_t centery = PartSys->maxY >> 1; + if (SEGMENT.check2) { // starfield mode + PartSys->setKillOutOfBounds(true); + PartSys->sources[0].var = 7; // emiting variation + PartSys->sources[0].source.x = centerx; // set emitter to center + PartSys->sources[0].source.y = centery; + } + else { + PartSys->setKillOutOfBounds(false); + PartSys->sources[0].var = 1; // emiting variation + } for (uint32_t i = 0; i < PartSys->usedParticles; i++) { //check all particles if (PartSys->particles[i].ttl == 0) continue; //skip dead particles - - int32_t centerx = PartSys->maxX >> 1; // center of matrix in subpixel coordinates - int32_t centery = PartSys->maxY >> 1; // (dx/dy): vector pointing from particle to center - int32_t dx = centerx - PartSys->particles[i].x; - int32_t dy = centery - PartSys->particles[i].y; + int32_t dx = centerx - PartSys->particles[i].x; + int32_t dy = centery - PartSys->particles[i].y; //speed towards center: - int32_t distance = sqrt32_bw(dx * dx + dy * dy); // absolute distance to center + int32_t distance = sqrt32_bw(dx * dx + dy * dy); // absolute distance to center if (distance < 20) distance = 20; // avoid division by zero, keep a minimum - int32_t speedfactor; + int32_t speedfactor; if (SEGMENT.check2) { // starfield mode - PartSys->setKillOutOfBounds(true); - PartSys->sources[0].var = 7; // emiting variation - PartSys->sources[0].source.x = PartSys->maxX >> 1; // set emitter to center - PartSys->sources[0].source.y = PartSys->maxY >> 1; speedfactor = 1 + (1 + (SEGMENT.speed >> 1)) * distance; // speed increases towards edge + //apply velocity PartSys->particles[i].x += (-speedfactor * dx) / 400000 - (dy >> 6); PartSys->particles[i].y += (-speedfactor * dy) / 400000 + (dx >> 6); } else { - PartSys->setKillOutOfBounds(false); - PartSys->sources[0].var = 1; // emiting variation speedfactor = 2 + (((50 + SEGMENT.speed) << 6) / distance); // speed increases towards center // rotate clockwise int32_t tempVx = (-speedfactor * dy); // speed is orthogonal to center vector From b5a710dbe46d1d4ef8884e6a7ac9e2e7cb2a664d Mon Sep 17 00:00:00 2001 From: srg74 <28492985+srg74@users.noreply.github.com> Date: Sun, 11 May 2025 12:19:03 -0400 Subject: [PATCH 081/153] Fixed markdownlint errors --- usermods/Temperature/readme.md | 8 +++++-- usermods/readme.md | 12 +++++------ usermods/sht/readme.md | 19 +++++++++++++---- .../readme.md | 21 ++++++++++--------- .../readme.md | 2 ++ 5 files changed, 40 insertions(+), 22 deletions(-) diff --git a/usermods/Temperature/readme.md b/usermods/Temperature/readme.md index b7697edc3..b09495fea 100644 --- a/usermods/Temperature/readme.md +++ b/usermods/Temperature/readme.md @@ -35,19 +35,23 @@ All parameters can be configured at runtime via the Usermods settings page, incl ## Change Log -2020-09-12 +2020-09-12 + * Changed to use async non-blocking implementation * Do not report erroneous low temperatures to MQTT * Disable plugin if temperature sensor not detected * Report the number of seconds until the first read in the info screen instead of sensor error 2021-04 + * Adaptation for runtime configuration. 2023-05 + * Rewrite to conform to newer recommendations. * Recommended @blazoncek fork of OneWire for ESP32 to avoid Sensor error 2024-09 + * Update OneWire to version 2.3.8, which includes stickbreaker's and garyd9's ESP32 fixes: - blazoncek's fork is no longer needed \ No newline at end of file + blazoncek's fork is no longer needed diff --git a/usermods/readme.md b/usermods/readme.md index 8aa8d6abc..eefb64dbd 100644 --- a/usermods/readme.md +++ b/usermods/readme.md @@ -1,4 +1,4 @@ -### Usermods +# Usermods This folder serves as a repository for usermods (custom `usermod.cpp` files)! @@ -6,11 +6,11 @@ If you have created a usermod you believe is useful (for example to support a pa In order for other people to be able to have fun with your usermod, please keep these points in mind: -- Create a folder in this folder with a descriptive name (for example `usermod_ds18b20_temp_sensor_mqtt`) -- Include your custom files -- If your usermod requires changes to other WLED files, please write a `readme.md` outlining the steps one needs to take -- Create a pull request! -- If your feature is useful for the majority of WLED users, I will consider adding it to the base code! +* Create a folder in this folder with a descriptive name (for example `usermod_ds18b20_temp_sensor_mqtt`) +* Include your custom files +* If your usermod requires changes to other WLED files, please write a `readme.md` outlining the steps one needs to take +* Create a pull request! +* If your feature is useful for the majority of WLED users, I will consider adding it to the base code! While I do my best to not break too much, keep in mind that as WLED is updated, usermods might break. I am not actively maintaining any usermod in this directory, that is your responsibility as the creator of the usermod. diff --git a/usermods/sht/readme.md b/usermods/sht/readme.md index c2cc5a1f8..4470c8836 100644 --- a/usermods/sht/readme.md +++ b/usermods/sht/readme.md @@ -1,35 +1,43 @@ # SHT + Usermod to support various SHT i2c sensors like the SHT30, SHT31, SHT35 and SHT85 ## Requirements -* "SHT85" by Rob Tillaart, v0.2 or higher: https://github.com/RobTillaart/SHT85 + +* "SHT85" by Rob Tillaart, v0.2 or higher: ## Usermod installation Simply copy the below block (build task) to your `platformio_override.ini` and compile WLED using this new build task. Or use an existing one, add the custom_usermod `sht`. ESP32: -``` + +```ini [env:custom_esp32dev_usermod_sht] extends = env:esp32dev custom_usermods = ${env:esp32dev.custom_usermods} sht ``` ESP8266: -``` + +```ini [env:custom_d1_mini_usermod_sht] extends = env:d1_mini custom_usermods = ${env:d1_mini.custom_usermods} sht ``` ## MQTT Discovery for Home Assistant + If you're using Home Assistant and want to have the temperature and humidity available as entities in HA, you can tick the "Add-To-Home-Assistant-MQTT-Discovery" option in the usermod settings. If you have an MQTT broker configured under "Sync Settings" and it is connected, the mod will publish the auto discovery message to your broker and HA will instantly find it and create an entity each for the temperature and humidity. ### Publishing readings via MQTT + Regardless of having MQTT discovery ticked or not, the mod will always report temperature and humidity to the WLED MQTT topic of that instance, if you have a broker configured and it's connected. ## Configuration + Navigate to the "Config" and then to the "Usermods" section. If you compiled WLED with `-D USERMOD_SHT`, you will see the config for it there: + * SHT-Type: * What it does: Select the SHT sensor type you want to use * Possible values: SHT30, SHT31, SHT35, SHT85 @@ -44,8 +52,11 @@ Navigate to the "Config" and then to the "Usermods" section. If you compiled WLE * Default: Disabled ## Change log + 2022-12 + * First implementation. ## Credits -ezcGman | Andy: Find me on the Intermit.Tech (QuinLED) Discord server: https://discord.gg/WdbAauG + +ezcGman | Andy: Find me on the Intermit.Tech (QuinLED) Discord server: diff --git a/usermods/usermod_v2_four_line_display_ALT/readme.md b/usermods/usermod_v2_four_line_display_ALT/readme.md index 39bb5d28e..663c93a4a 100644 --- a/usermods/usermod_v2_four_line_display_ALT/readme.md +++ b/usermods/usermod_v2_four_line_display_ALT/readme.md @@ -5,6 +5,7 @@ This usermod could be used in compination with `usermod_v2_rotary_encoder_ui_ALT ## Functionalities Press the encoder to cycle through the options: + * Brightness * Speed * Intensity @@ -35,15 +36,15 @@ These options are configurable in Config > Usermods * `enabled` - enable/disable usermod * `type` - display type in numeric format - * 1 = I2C SSD1306 128x32 - * 2 = I2C SH1106 128x32 - * 3 = I2C SSD1306 128x64 (4 double-height lines) - * 4 = I2C SSD1305 128x32 - * 5 = I2C SSD1305 128x64 (4 double-height lines) - * 6 = SPI SSD1306 128x32 - * 7 = SPI SSD1306 128x64 (4 double-height lines) - * 8 = SPI SSD1309 128x64 (4 double-height lines) - * 9 = I2C SSD1309 128x64 (4 double-height lines) + * 1 = I2C SSD1306 128x32 + * 2 = I2C SH1106 128x32 + * 3 = I2C SSD1306 128x64 (4 double-height lines) + * 4 = I2C SSD1305 128x32 + * 5 = I2C SSD1305 128x64 (4 double-height lines) + * 6 = SPI SSD1306 128x32 + * 7 = SPI SSD1306 128x64 (4 double-height lines) + * 8 = SPI SSD1309 128x64 (4 double-height lines) + * 9 = I2C SSD1309 128x64 (4 double-height lines) * `pin` - GPIO pins used for display; SPI displays can use SCK, MOSI, CS, DC & RST * `flip` - flip/rotate display 180° * `contrast` - set display contrast (higher contrast may reduce display lifetime) @@ -53,7 +54,6 @@ These options are configurable in Config > Usermods * `showSeconds` - Show seconds on the clock display * `i2c-freq-kHz` - I2C clock frequency in kHz (may help reduce dropped frames, range: 400-3400) - ### PlatformIO requirements Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. @@ -61,4 +61,5 @@ Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. ## Change Log 2021-10 + * First public release diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md index 3df6de6ef..cb6150a42 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/readme.md @@ -5,6 +5,7 @@ This usermod supports the UI of the `usermod_v2_rotary_encoder_ui_ALT`. ## Functionalities Press the encoder to cycle through the options: + * Brightness * Speed * Intensity @@ -39,4 +40,5 @@ No special requirements. ## Change Log 2021-10 + * First public release From 42d9a41cf52f0539351e39461b1326735d2a5e29 Mon Sep 17 00:00:00 2001 From: srg74 <28492985+srg74@users.noreply.github.com> Date: Sun, 11 May 2025 12:49:32 -0400 Subject: [PATCH 082/153] Fixed markdownlint errors #2 --- usermods/Animated_Staircase/README.md | 33 +++++---- usermods/BH1750_v2/readme.md | 16 +++-- usermods/PIR_sensor_switch/readme.md | 18 +++-- usermods/PWM_fan/readme.md | 3 + usermods/audioreactive/readme.md | 37 +++++----- usermods/project_cars_shiftlight/readme.md | 10 +-- .../usermod_v2_HttpPullLightControl/readme.md | 71 ++++++++++--------- usermods/usermod_v2_auto_save/readme.md | 6 +- .../README.md | 6 +- usermods/usermod_v2_word_clock/readme.md | 10 +-- 10 files changed, 125 insertions(+), 85 deletions(-) diff --git a/usermods/Animated_Staircase/README.md b/usermods/Animated_Staircase/README.md index c24a037e1..263ac8065 100644 --- a/usermods/Animated_Staircase/README.md +++ b/usermods/Animated_Staircase/README.md @@ -1,4 +1,5 @@ # Usermod Animated Staircase + This usermod makes your staircase look cool by illuminating it with an animation. It uses PIR or ultrasonic sensors at the top and bottom of your stairs to: @@ -11,11 +12,13 @@ The Animated Staircase can be controlled by the WLED API. Change settings such a speed, on/off time and distance by sending an HTTP request, see below. ## WLED integration + To include this usermod in your WLED setup, you have to be able to [compile WLED from source](https://kno.wled.ge/advanced/compiling-wled/). Before compiling, you have to make the following modifications: Edit your environment in `platformio_override.ini` + 1. Open `platformio_override.ini` 2. add `Animated_Staircase` to the `custom_usermods` line for your environment @@ -25,10 +28,10 @@ If you use PIR sensor enter -1 for echo pin. Maximum distance for ultrasonic sensor can be configured as the time needed for an echo (see below). ## Hardware installation + 1. Attach the LED strip to each step of the stairs. 2. Connect the ESP8266 pin D4 or ESP32 pin D2 to the first LED data pin at the bottom step. -3. Connect the data-out pin at the end of each strip per step to the data-in pin on the - next step, creating one large virtual LED strip. +3. Connect the data-out pin at the end of each strip per step to the data-in pin on the next step, creating one large virtual LED strip. 4. Mount sensors of choice at the bottom and top of the stairs and connect them to the ESP. 5. To make sure all LEDs get enough power and have your staircase lighted evenly, power each step from one side, using at least AWG14 or 2.5mm^2 cable. Don't connect them serial as you @@ -37,24 +40,23 @@ Maximum distance for ultrasonic sensor can be configured as the time needed for You _may_ need to use 10k pull-down resistors on the selected PIR pins, depending on the sensor. ## WLED configuration -1. In the WLED UI, configure a segment for each step. The lowest step of the stairs is the - lowest segment id. -2. Save your segments into a preset. -3. Ideally, add the preset in the config > LED setup menu to the "apply - preset **n** at boot" setting. + +1. In the WLED UI, configure a segment for each step. The lowest step of the stairs is the lowest segment id. +2. Save your segments into a preset. +3. Ideally, add the preset in the config > LED setup menu to the "apply preset **n** at boot" setting. ## Changing behavior through API + The Staircase settings can be changed through the WLED JSON api. **NOTE:** We are using [curl](https://curl.se/) to send HTTP POSTs to the WLED API. If you're using Windows and want to use the curl commands, replace the `\` with a `^` or remove them and put everything on one line. - | Setting | Description | Default | |------------------|---------------------------------------------------------------|---------| | enabled | Enable or disable the usermod | true | -| bottom-sensor | Manually trigger a down to up animation via API | false | +| bottom-sensor | Manually trigger a down to up animation via API | false | | top-sensor | Manually trigger an up to down animation via API | false | @@ -74,6 +76,7 @@ The staircase settings and sensor states are inside the WLED "state" element: ``` ### Enable/disable the usermod + By disabling the usermod you will be able to keep the LED's on, independent from the sensor activity. This enables you to play with the lights without the usermod switching them on or off. @@ -90,6 +93,7 @@ To enable the usermod again, use `"enabled":true`. Alternatively you can use _Usermod_ Settings page where you can change other parameters as well. ### Changing animation parameters and detection range of the ultrasonic HC-SR04 sensor + Using _Usermod_ Settings page you can define different usermod parameters, including sensor pins, delay between segment activation etc. When an ultrasonic sensor is enabled you can enter maximum detection distance in centimeters separately for top and bottom sensors. @@ -99,6 +103,7 @@ distances creates delays in the WLED software, _might_ introduce timing hiccups a less responsive web interface. It is therefore advised to keep the detection distance as short as possible. ### Animation triggering through the API + In addition to activation by one of the stair sensors, you can also trigger the animation manually via the API. To simulate triggering the bottom sensor, use: @@ -115,15 +120,19 @@ curl -X POST -H "Content-Type: application/json" \ -d '{"staircase":{"top-sensor":true}}' \ xxx.xxx.xxx.xxx/json/state ``` + **MQTT** You can publish a message with either `up` or `down` on topic `/swipe` to trigger animation. You can also use `on` or `off` for enabling or disabling the usermod. -Have fun with this usermod.
-www.rolfje.com +Have fun with this usermod + +`www.rolfje.com` Modifications @blazoncek ## Change log + 2021-04 -* Adaptation for runtime configuration. + +- Adaptation for runtime configuration. diff --git a/usermods/BH1750_v2/readme.md b/usermods/BH1750_v2/readme.md index bba4eb712..9f5991076 100644 --- a/usermods/BH1750_v2/readme.md +++ b/usermods/BH1750_v2/readme.md @@ -4,6 +4,7 @@ This usermod will read from an ambient light sensor like the BH1750. The luminance is displayed in both the Info section of the web UI, as well as published to the `/luminance` MQTT topic if enabled. ## Dependencies + - Libraries - `claws/BH1750 @^1.2.0` - Data is published over MQTT - make sure you've enabled the MQTT sync interface. @@ -13,23 +14,30 @@ The luminance is displayed in both the Info section of the web UI, as well as pu To enable, compile with `BH1750` in `custom_usermods` (e.g. in `platformio_override.ini`) ### Configuration Options + The following settings can be set at compile-time but are configurable on the usermod menu (except First Measurement time): -* `USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL` - the max number of milliseconds between measurements, defaults to 10000ms -* `USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL` - the min number of milliseconds between measurements, defaults to 500ms -* `USERMOD_BH1750_OFFSET_VALUE` - the offset value to report on, defaults to 1 -* `USERMOD_BH1750_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 10000 ms + +- `USERMOD_BH1750_MAX_MEASUREMENT_INTERVAL` - the max number of milliseconds between measurements, defaults to 10000ms +- `USERMOD_BH1750_MIN_MEASUREMENT_INTERVAL` - the min number of milliseconds between measurements, defaults to 500ms +- `USERMOD_BH1750_OFFSET_VALUE` - the offset value to report on, defaults to 1 +- `USERMOD_BH1750_FIRST_MEASUREMENT_AT` - the number of milliseconds after boot to take first measurement, defaults to 10000 ms In addition, the Usermod screen allows you to: + - enable/disable the usermod - Enable Home Assistant Discovery of usermod - Configure the SCL/SDA pins ## API + The following method is available to interact with the usermod from other code modules: + - `getIlluminance` read the brightness from the sensor ## Change Log + Jul 2022 + - Added Home Assistant Discovery - Implemented PinManager to register pins - Made pins configurable in usermod menu diff --git a/usermods/PIR_sensor_switch/readme.md b/usermods/PIR_sensor_switch/readme.md index be55406df..2b8897481 100644 --- a/usermods/PIR_sensor_switch/readme.md +++ b/usermods/PIR_sensor_switch/readme.md @@ -25,7 +25,7 @@ You can also use usermod's off timer instead of sensor's. In such case rotate th **NOTE:** Usermod has been included in master branch of WLED so it can be compiled in directly just by defining `-D USERMOD_PIRSWITCH` and optionally `-D PIR_SENSOR_PIN=16` to override default pin. You can also change the default off time by adding `-D PIR_SENSOR_OFF_SEC=30`. -## API to enable/disable the PIR sensor from outside. For example from another usermod: +## API to enable/disable the PIR sensor from outside. For example from another usermod To query or change the PIR sensor state the methods `bool PIRsensorEnabled()` and `void EnablePIRsensor(bool enable)` are available. @@ -33,15 +33,16 @@ When the PIR sensor state changes an MQTT message is broadcasted with topic `wle Usermod can also be configured to send just the MQTT message but not change WLED state using settings page as well as responding to motion only at night (assuming NTP and latitude/longitude are set to determine sunrise/sunset times). -### There are two options to get access to the usermod instance: +### There are two options to get access to the usermod instance -1. Include `usermod_PIR_sensor_switch.h` **before** you include other usermods in `usermods_list.cpp' +_1._ Include `usermod_PIR_sensor_switch.h` **before** you include other usermods in `usermods_list.cpp' or -2. Use `#include "usermod_PIR_sensor_switch.h"` at the top of the `usermod.h` where you need it. +_2._ Use `#include "usermod_PIR_sensor_switch.h"` at the top of the `usermod.h` where you need it. **Example usermod.h :** + ```cpp #include "wled.h" @@ -79,25 +80,30 @@ Usermod can be configured via the Usermods settings page. * `override` - override PIR input when WLED state is changed using UI * `domoticz-idx` - Domoticz virtual switch ID (used with MQTT `domoticz/in`) - Have fun - @gegu & @blazoncek ## Change log + 2021-04 + * Adaptation for runtime configuration. 2021-11 + * Added information about dynamic configuration options * Added option to temporary enable/disable usermod from WLED UI (Info dialog) 2022-11 + * Added compile time option for off timer. * Added Home Assistant autodiscovery MQTT broadcast. * Updated info on compiling. 2023-?? + * Override option * Domoticz virtual switch ID (used with MQTT `domoticz/in`) 2024-02 -* Added compile time option to expand number of PIR sensors (they are logically ORed) `-D PIR_SENSOR_MAX_SENSORS=3` \ No newline at end of file + +* Added compile time option to expand number of PIR sensors (they are logically ORed) `-D PIR_SENSOR_MAX_SENSORS=3` diff --git a/usermods/PWM_fan/readme.md b/usermods/PWM_fan/readme.md index 9fecaabf2..872bbd9b9 100644 --- a/usermods/PWM_fan/readme.md +++ b/usermods/PWM_fan/readme.md @@ -40,6 +40,9 @@ If the fan speed is unlocked, it will revert to temperature controlled speed on ## Change Log 2021-10 + * First public release + 2022-05 + * Added JSON API call to allow changing of speed diff --git a/usermods/audioreactive/readme.md b/usermods/audioreactive/readme.md index 8db5c00bf..bd253c82c 100644 --- a/usermods/audioreactive/readme.md +++ b/usermods/audioreactive/readme.md @@ -8,24 +8,27 @@ Does audio processing and provides data structure that specially written effects **does not** provide effects or draw anything to an LED strip/matrix. ## Additional Documentation + This usermod is an evolution of [SR-WLED](https://github.com/atuline/WLED), and a lot of documentation and information can be found in the [SR-WLED wiki](https://github.com/atuline/WLED/wiki): + * [getting started with audio](https://github.com/atuline/WLED/wiki/First-Time-Setup#sound) * [Sound settings](https://github.com/atuline/WLED/wiki/Sound-Settings) - similar to options on the usemod settings page in WLED. * [Digital Audio](https://github.com/atuline/WLED/wiki/Digital-Microphone-Hookup) * [Analog Audio](https://github.com/atuline/WLED/wiki/Analog-Audio-Input-Options) * [UDP Sound sync](https://github.com/atuline/WLED/wiki/UDP-Sound-Sync) - ## Supported MCUs -This audioreactive usermod works best on "classic ESP32" (dual core), and on ESP32-S3 which also has dual core and hardware floating point support. -It will compile successfully for ESP32-S2 and ESP32-C3, however might not work well, as other WLED functions will become slow. Audio processing requires a lot of computing power, which can be problematic on smaller MCUs like -S2 and -C3. +This audioreactive usermod works best on "classic ESP32" (dual core), and on ESP32-S3 which also has dual core and hardware floating point support. + +It will compile successfully for ESP32-S2 and ESP32-C3, however might not work well, as other WLED functions will become slow. Audio processing requires a lot of computing power, which can be problematic on smaller MCUs like -S2 and -C3. Analog audio is only possible on "classic" ESP32, but not on other MCUs like ESP32-S3. -Currently ESP8266 is not supported, due to low speed and small RAM of this chip. +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 + +## Installation Add 'ADS1115_v2' to `custom_usermods` in your platformio environment. @@ -35,29 +38,31 @@ All parameters are runtime configurable. Some may require a hard reset after cha If you want to define default GPIOs during compile time, use the following (default values in parentheses): -- `-D SR_DMTYPE=x` : defines digital microphone type: 0=analog, 1=generic I2S (default), 2=ES7243 I2S, 3=SPH0645 I2S, 4=generic I2S with master clock, 5=PDM I2S -- `-D AUDIOPIN=x` : GPIO for analog microphone/AUX-in (36) -- `-D I2S_SDPIN=x` : GPIO for SD pin on digital microphone (32) -- `-D I2S_WSPIN=x` : GPIO for WS pin on digital microphone (15) -- `-D I2S_CKPIN=x` : GPIO for SCK pin on digital microphone (14) -- `-D MCLK_PIN=x` : GPIO for master clock pin on digital Line-In boards (-1) -- `-D ES7243_SDAPIN` : GPIO for I2C SDA pin on ES7243 microphone (-1) -- `-D ES7243_SCLPIN` : GPIO for I2C SCL pin on ES7243 microphone (-1) +* `-D SR_DMTYPE=x` : defines digital microphone type: 0=analog, 1=generic I2S (default), 2=ES7243 I2S, 3=SPH0645 I2S, 4=generic I2S with master clock, 5=PDM I2S +* `-D AUDIOPIN=x` : GPIO for analog microphone/AUX-in (36) +* `-D I2S_SDPIN=x` : GPIO for SD pin on digital microphone (32) +* `-D I2S_WSPIN=x` : GPIO for WS pin on digital microphone (15) +* `-D I2S_CKPIN=x` : GPIO for SCK pin on digital microphone (14) +* `-D MCLK_PIN=x` : GPIO for master clock pin on digital Line-In boards (-1) +* `-D ES7243_SDAPIN` : GPIO for I2C SDA pin on ES7243 microphone (-1) +* `-D ES7243_SCLPIN` : GPIO for I2C SCL pin on ES7243 microphone (-1) Other options: -- `-D UM_AUDIOREACTIVE_ENABLE` : makes usermod default enabled (not the same as include into build option!) -- `-D UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF` : disables rise/fall limiter default +* `-D UM_AUDIOREACTIVE_ENABLE` : makes usermod default enabled (not the same as include into build option!) +* `-D UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF` : disables rise/fall limiter default **NOTE** I2S is used for analog audio sampling. Hence, the analog *buttons* (i.e. potentiometers) are disabled when running this usermod with an analog microphone. ### Advanced Compile-Time Options + You can use the following additional flags in your `build_flags` + * `-D SR_SQUELCH=x` : Default "squelch" setting (10) * `-D SR_GAIN=x` : Default "gain" setting (60) * `-D I2S_USE_RIGHT_CHANNEL`: Use RIGHT instead of LEFT channel (not recommended unless you strictly need this). * `-D I2S_USE_16BIT_SAMPLES`: Use 16bit instead of 32bit for internal sample buffers. Reduces sampling quality, but frees some RAM ressources (not recommended unless you absolutely need this). -* `-D I2S_GRAB_ADC1_COMPLETELY`: Experimental: continuously sample analog ADC microphone. Only effective on ESP32. WARNING this _will_ cause conflicts(lock-up) with any analogRead() call. +* `-D I2S_GRAB_ADC1_COMPLETELY`: Experimental: continuously sample analog ADC microphone. Only effective on ESP32. WARNING this *will* cause conflicts(lock-up) with any analogRead() call. * `-D MIC_LOGGER` : (debugging) Logs samples from the microphone to serial USB. Use with serial plotter (Arduino IDE) * `-D SR_DEBUG` : (debugging) Additional error diagnostics and debug info on serial USB. diff --git a/usermods/project_cars_shiftlight/readme.md b/usermods/project_cars_shiftlight/readme.md index 433da4300..338936a80 100644 --- a/usermods/project_cars_shiftlight/readme.md +++ b/usermods/project_cars_shiftlight/readme.md @@ -1,11 +1,11 @@ -### Shift Light for Project Cars +# Shift Light for Project Cars Turn your WLED lights into a rev light and shift indicator for Project Cars. It's easy to use. -1. Make sure your WLED device and your PC/console are on the same network and can talk to each other +_1._ Make sure your WLED device and your PC/console are on the same network and can talk to each other -2. Go to the gameplay settings menu in PCARS and enable UDP. There are 9 numbers you can choose from. This is the refresh rate. The lower the number, the better. However, you might run into problems at faster rates. +_2._ Go to the gameplay settings menu in PCARS and enable UDP. There are 9 numbers you can choose from. This is the refresh rate. The lower the number, the better. However, you might run into problems at faster rates. | Number | Updates/Second | | ------ | -------------- | @@ -19,5 +19,5 @@ It's easy to use. | 8 | 05 | | 9 | 1 | -3. Once you enter a race, WLED should automatically shift to PCARS mode. -4. Done. +_3._ Once you enter a race, WLED should automatically shift to PCARS mode. +_4._ Done. diff --git a/usermods/usermod_v2_HttpPullLightControl/readme.md b/usermods/usermod_v2_HttpPullLightControl/readme.md index eb56d505d..d86ece4d9 100644 --- a/usermods/usermod_v2_HttpPullLightControl/readme.md +++ b/usermods/usermod_v2_HttpPullLightControl/readme.md @@ -5,7 +5,7 @@ The `usermod_v2_HttpPullLightControl` is a custom user module for WLED that enab ## Features * Configure the URL endpoint (only support HTTP for now, no HTTPS) and polling interval via the WLED user interface. -* All options from the JSON API are supported (since v0.0.3). See: https://kno.wled.ge/interfaces/json-api/ +* All options from the JSON API are supported (since v0.0.3). See: [https://kno.wled.ge/interfaces/json-api/](https://kno.wled.ge/interfaces/json-api/) * The ability to control the brightness of all lights and the state (on/off) and color of individual lights remotely. * Start or stop an effect and when you run the same effect when its's already running, it won't restart. * The ability to control all these settings per segment. @@ -13,13 +13,15 @@ The `usermod_v2_HttpPullLightControl` is a custom user module for WLED that enab * Unique ID generation based on the device's MAC address and a configurable salt value, appended to the request URL for identification. ## Configuration + * Enable the `usermod_v2_HttpPullLightControl` via the WLED user interface. * Specify the URL endpoint and polling interval. ## JSON Format and examples + * The module sends a GET request to the configured URL, appending a unique identifier as a query parameter: `https://www.example.com/mycustompage.php?id=xxxxxxxx` where xxxxxxx is a 40 character long SHA1 hash of the MAC address combined with a given salt. -* Response Format (since v0.0.3) it is eactly the same as the WLED JSON API, see: https://kno.wled.ge/interfaces/json-api/ +* Response Format (since v0.0.3) it is eactly the same as the WLED JSON API, see: [https://kno.wled.ge/interfaces/json-api/](https://kno.wled.ge/interfaces/json-api/) After getting the URL (it can be a static file like static.json or a mylogic.php which gives a dynamic response), the response is read and parsed to WLED. * An example of a response to set the individual lights: 0 to RED, 12 to Green and 14 to BLUE. Remember that is will SET lights, you might want to set all the others to black. @@ -58,48 +60,51 @@ After getting the URL (it can be a static file like static.json or a mylogic.php }` * Or use the following example to start an effect, but first we UNFREEZE (frz=false) the segment because it was frozen by individual light control in the previous examples (28=Chase effect, Speed=180m Intensity=128). The three color slots are the slots you see under the color wheel and used by the effect. RED, Black, White in this case. + +```json `{ "seg": { - "frz": false, - "fx": 28, - "sx": 200, - "ix": 128, - "col": [ - "FF0000", - "000000", - "FFFFFF" - ] - } + "frz": false, + "fx": 28, + "sx": 200, + "ix": 128, + "col": [ + "FF0000", + "000000", + "FFFFFF" + ] + } }` - +``` ## Installation 1. Add `usermod_v2_HttpPullLightControl` to your WLED project following the instructions provided in the WLED documentation. 2. Compile by setting the build_flag: -D USERMOD_HTTP_PULL_LIGHT_CONTROL and upload to your ESP32/ESP8266! 3. There are several compile options which you can put in your platformio.ini or platformio_override.ini: -- -DUSERMOD_HTTP_PULL_LIGHT_CONTROL ;To Enable the usermod -- -DHTTP_PULL_LIGHT_CONTROL_URL="\"http://mydomain.com/json-response.php\"" ; The URL which will be requested all the time to set the lights/effects -- -DHTTP_PULL_LIGHT_CONTROL_SALT="\"my_very-S3cret_C0de\"" ; A secret SALT which will help by making the ID more safe -- -DHTTP_PULL_LIGHT_CONTROL_INTERVAL=30 ; The interval at which the URL is requested in seconds -- -DHTTP_PULL_LIGHT_CONTROL_HIDE_SALT ; Do you want to Hide the SALT in the User Interface? If yes, Set this flag. Note that the salt can now only be set via the above -DHTTP_PULL_LIGHT_CONTROL_SALT= setting -- -DWLED_AP_SSID="\"Christmas Card\"" ; These flags are not just for my Usermod but you probably want to set them -- -DWLED_AP_PASS="\"christmas\"" -- -DWLED_OTA_PASS="\"otapw-secret\"" -- -DMDNS_NAME="\"christmascard\"" -- -DSERVERNAME="\"CHRISTMASCARD\"" -- -D ABL_MILLIAMPS_DEFAULT=450 -- -D DEFAULT_LED_COUNT=60 ; For a LED Ring of 60 LEDs -- -D BTNPIN=41 ; The M5Stack Atom S3 Lite has a button on GPIO41 -- -D DATA_PINS=2 ; The M5Stack Atom S3 Lite has a Grove connector on the front, we use this GPIO2 -- -D STATUSLED=35 ; The M5Stack Atom S3 Lite has a Multi-Color LED on GPIO35, although I didnt managed to control it -- -D IRPIN=4 ; The M5Stack Atom S3 Lite has a IR LED on GPIO4 +* -DUSERMOD_HTTP_PULL_LIGHT_CONTROL ;To Enable the usermod +* -DHTTP_PULL_LIGHT_CONTROL_URL="\"`http://mydomain.com/json-response.php`\"" ; The URL which will be requested all the time to set the lights/effects +* -DHTTP_PULL_LIGHT_CONTROL_SALT="\"my_very-S3cret_C0de\"" ; A secret SALT which will help by making the ID more safe +* -DHTTP_PULL_LIGHT_CONTROL_INTERVAL=30 ; The interval at which the URL is requested in seconds +* -DHTTP_PULL_LIGHT_CONTROL_HIDE_SALT ; Do you want to Hide the SALT in the User Interface? If yes, Set this flag. Note that the salt can now only be set via the above -DHTTP_PULL_LIGHT_CONTROL_SALT= setting -- -D DEBUG=1 ; Set these DEBUG flags ONLY if you want to debug and read out Serial (using Visual Studio Code - Serial Monitor) -- -DDEBUG_LEVEL=5 -- -DWLED_DEBUG +* -DWLED_AP_SSID="\"Christmas Card\"" ; These flags are not just for my Usermod but you probably want to set them +* -DWLED_AP_PASS="\"christmas\"" +* -DWLED_OTA_PASS="\"otapw-secret\"" +* -DMDNS_NAME="\"christmascard\"" +* -DSERVERNAME="\"CHRISTMASCARD\"" +* -D ABL_MILLIAMPS_DEFAULT=450 +* -D DEFAULT_LED_COUNT=60 ; For a LED Ring of 60 LEDs +* -D BTNPIN=41 ; The M5Stack Atom S3 Lite has a button on GPIO41 +* -D DATA_PINS=2 ; The M5Stack Atom S3 Lite has a Grove connector on the front, we use this GPIO2 +* -D STATUSLED=35 ; The M5Stack Atom S3 Lite has a Multi-Color LED on GPIO35, although I didnt managed to control it +* -D IRPIN=4 ; The M5Stack Atom S3 Lite has a IR LED on GPIO4 + +* -D DEBUG=1 ; Set these DEBUG flags ONLY if you want to debug and read out Serial (using Visual Studio Code - Serial Monitor) +* -DDEBUG_LEVEL=5 +* -DWLED_DEBUG ## Use Case: Interactive Christmas Cards @@ -107,4 +112,4 @@ Imagine distributing interactive Christmas cards embedded with a tiny ESP32 and Your server keeps track of how many cards are active at any given time. If all 20 cards are active, your server instructs each card to light up all of its LEDs. However, if only 4 cards are active, your server instructs each card to light up only 4 LEDs. This creates a real-time interactive experience, symbolizing the collective spirit of the holiday season. Each lit LED represents a friend who's thinking about the others, and the visual feedback creates a sense of connection among the group, despite the physical distance. -This setup demonstrates a unique way to blend traditional holiday sentiments with modern technology, offering an engaging and memorable experience. \ No newline at end of file +This setup demonstrates a unique way to blend traditional holiday sentiments with modern technology, offering an engaging and memorable experience. diff --git a/usermods/usermod_v2_auto_save/readme.md b/usermods/usermod_v2_auto_save/readme.md index f54d87a76..ce15d8c27 100644 --- a/usermods/usermod_v2_auto_save/readme.md +++ b/usermods/usermod_v2_auto_save/readme.md @@ -2,6 +2,7 @@ v2 Usermod to automatically save settings to preset number AUTOSAVE_PRESET_NUM after a change to any of: + * brightness * effect speed * effect intensity @@ -19,7 +20,7 @@ Note: WLED doesn't respect the brightness of the preset being auto loaded, so th ## Installation -Copy and update the example `platformio_override.ini.sample` +Copy and update the example `platformio_override.ini.sample` from the Rotary Encoder UI usermode folder to the root directory of your particular build. This file should be placed in the same directory as `platformio.ini`. @@ -50,6 +51,9 @@ Note: the Four Line Display usermod requires the libraries `U8g2` and `Wire`. ## Change Log 2021-02 + * First public release + 2021-04 + * Adaptation for runtime configuration. diff --git a/usermods/usermod_v2_brightness_follow_sun/README.md b/usermods/usermod_v2_brightness_follow_sun/README.md index 25daf0ba2..cbd87a55a 100644 --- a/usermods/usermod_v2_brightness_follow_sun/README.md +++ b/usermods/usermod_v2_brightness_follow_sun/README.md @@ -10,8 +10,8 @@ define `USERMOD_BRIGHTNESS_FOLLOW_SUN` e.g. `#define USERMOD_BRIGHTNESS_FOLLOW_S or add `-D USERMOD_BRIGHTNESS_FOLLOW_SUN` to `build_flags` in platformio_override.ini - ### Options + Open Usermod Settings in WLED to change settings: `Enable` - When checked `Enable`, turn on the `Brightness Follow Sun` Usermod, which will automatically turn on the lights, adjust the brightness, and turn off the lights. If you need to completely turn off the lights, please unchecked `Enable`. @@ -24,12 +24,12 @@ Open Usermod Settings in WLED to change settings: `Relax Hour` - The unit is in hours, with an effective range of 0-6. According to the settings, maintain the lowest brightness for 0-6 hours before sunrise and after sunset. - ### PlatformIO requirements No special requirements. -## Change Log +### Change Log 2025-01-02 + * init diff --git a/usermods/usermod_v2_word_clock/readme.md b/usermods/usermod_v2_word_clock/readme.md index c42ee0ee4..b81cebcea 100644 --- a/usermods/usermod_v2_word_clock/readme.md +++ b/usermods/usermod_v2_word_clock/readme.md @@ -1,14 +1,15 @@ # Word Clock Usermod V2 -This usermod drives an 11x10 pixel matrix wordclock with WLED. There are 4 additional dots for the minutes. +This usermod drives an 11x10 pixel matrix wordclock with WLED. There are 4 additional dots for the minutes. The visualisation is described by 4 masks with LED numbers (single dots for minutes, minutes, hours and "clock"). The index of the LEDs in the masks always starts at 0, even if the ledOffset is not 0. There are 3 parameters that control behavior: - + active: enable/disable usermod diplayItIs: enable/disable display of "Es ist" on the clock ledOffset: number of LEDs before the wordclock LEDs -### Update for alternative wiring pattern +## Update for alternative wiring pattern + Based on this fantastic work I added an alternative wiring pattern. The original used a long wire to connect DO to DI, from one line to the next line. @@ -17,10 +18,9 @@ With this method, every other line was inverted and showed the wrong letter. I added a switch in usermod called "meander wiring?" to enable/disable the alternate wiring pattern. - ## Installation -Copy and update the example `platformio_override.ini.sample` +Copy and update the example `platformio_override.ini.sample` from the Rotary Encoder UI usermod folder to the root directory of your particular build. This file should be placed in the same directory as `platformio.ini`. From d381108dc07915a37faf846f495ef1f4f38bc2ca Mon Sep 17 00:00:00 2001 From: Arcitec <38923130+Arcitec@users.noreply.github.com> Date: Wed, 14 May 2025 00:27:29 +0200 Subject: [PATCH 083/153] AR: add compile-time flag for "Automatic Gain Control" option Automatic Gain Control is a very important aspect of the audioreactive plugin, and is vitally important when the external music volume constantly changes. It makes sense to allow users to choose their preferred AGC behavior at compile-time, since they can already set the Gain and Squelch via flags. Adds `SR_AGC` as a flag, which defaults to 0 (off). --- usermods/audioreactive/audio_reactive.cpp | 5 ++++- usermods/audioreactive/readme.md | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/usermods/audioreactive/audio_reactive.cpp b/usermods/audioreactive/audio_reactive.cpp index 4b3520562..e7e79846a 100644 --- a/usermods/audioreactive/audio_reactive.cpp +++ b/usermods/audioreactive/audio_reactive.cpp @@ -65,11 +65,14 @@ static bool udpSyncConnected = false; // UDP connection status -> true i // audioreactive variables #ifdef ARDUINO_ARCH_ESP32 + #ifndef SR_AGC // Automatic gain control mode + #define SR_AGC 0 // default mode = off + #endif static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier static float sampleAvg = 0.0f; // Smoothed Average sample - sampleAvg < 1 means "quiet" (simple noise gate) static float sampleAgc = 0.0f; // Smoothed AGC sample -static uint8_t soundAgc = 0; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) +static uint8_t soundAgc = SR_AGC; // Automatic gain control: 0 - off, 1 - normal, 2 - vivid, 3 - lazy (config value) #endif //static float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency diff --git a/usermods/audioreactive/readme.md b/usermods/audioreactive/readme.md index bd253c82c..5ee575fff 100644 --- a/usermods/audioreactive/readme.md +++ b/usermods/audioreactive/readme.md @@ -60,8 +60,9 @@ You can use the following additional flags in your `build_flags` * `-D SR_SQUELCH=x` : Default "squelch" setting (10) * `-D SR_GAIN=x` : Default "gain" setting (60) +* `-D SR_AGC=x` : (Only ESP32) Default "AGC (Automatic Gain Control)" setting (0): 0=off, 1=normal, 2=vivid, 3=lazy * `-D I2S_USE_RIGHT_CHANNEL`: Use RIGHT instead of LEFT channel (not recommended unless you strictly need this). -* `-D I2S_USE_16BIT_SAMPLES`: Use 16bit instead of 32bit for internal sample buffers. Reduces sampling quality, but frees some RAM ressources (not recommended unless you absolutely need this). +* `-D I2S_USE_16BIT_SAMPLES`: Use 16bit instead of 32bit for internal sample buffers. Reduces sampling quality, but frees some RAM resources (not recommended unless you absolutely need this). * `-D I2S_GRAB_ADC1_COMPLETELY`: Experimental: continuously sample analog ADC microphone. Only effective on ESP32. WARNING this *will* cause conflicts(lock-up) with any analogRead() call. * `-D MIC_LOGGER` : (debugging) Logs samples from the microphone to serial USB. Use with serial plotter (Arduino IDE) * `-D SR_DEBUG` : (debugging) Additional error diagnostics and debug info on serial USB. From d9ad4ec74336e341fc5c543ff3c7f689b9d1ece3 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 19 May 2025 19:50:48 +0200 Subject: [PATCH 084/153] improved & refactored Android FX (#4522) - returns FRAMETIME -> no more flickering in transitions and overlay - no more double-painting of pixels --- wled00/FX.cpp | 74 +++++++++++++++++++++------------------------------ 1 file changed, 31 insertions(+), 43 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 9584ea8e7..8835e71ff 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -802,57 +802,45 @@ static const char _data_FX_MODE_MULTI_STROBE[] PROGMEM = "Strobe Mega@!,!;!,!;!; /* - * Android loading circle + * Android loading circle, refactored by @dedehai */ uint16_t mode_android(void) { - + if (!SEGENV.allocateData(sizeof(uint32_t))) return mode_static(); + uint32_t* counter = reinterpret_cast(SEGENV.data); + unsigned size = SEGENV.aux1 >> 1; // upper 15 bit + unsigned shrinking = SEGENV.aux1 & 0x01; // lowest bit + if(strip.now >= SEGENV.step) { + SEGENV.step = strip.now + 3 + ((8 * (uint32_t)(255 - SEGMENT.speed)) / SEGLEN); + if (size > (SEGMENT.intensity * SEGLEN) / 255) + shrinking = 1; + else if (size < 2) + shrinking = 0; + if (!shrinking) { // growing + if ((*counter % 3) == 1) + SEGENV.aux0++; // advance start position + else + size++; + } else { // shrinking + SEGENV.aux0++; + if ((*counter % 3) != 1) + size--; + } + SEGENV.aux1 = size << 1 | shrinking; // save back + (*counter)++; + if (SEGENV.aux0 >= SEGLEN) SEGENV.aux0 = 0; + } + uint32_t start = SEGENV.aux0; + uint32_t end = (SEGENV.aux0 + size) % SEGLEN; for (unsigned i = 0; i < SEGLEN; i++) { - SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); - } - - if (SEGENV.aux1 > (SEGMENT.intensity*SEGLEN)/255) - { - SEGENV.aux0 = 1; - } else - { - if (SEGENV.aux1 < 2) SEGENV.aux0 = 0; - } - - unsigned a = SEGENV.step & 0xFFFFU; - - if (SEGENV.aux0 == 0) - { - if (SEGENV.call %3 == 1) {a++;} - else {SEGENV.aux1++;} - } else - { - a++; - if (SEGENV.call %3 != 1) SEGENV.aux1--; - } - - if (a >= SEGLEN) a = 0; - - if (a + SEGENV.aux1 < SEGLEN) - { - for (unsigned i = a; i < a+SEGENV.aux1; i++) { + if ((start < end && i >= start && i < end) || (start >= end && (i >= start || i < end))) SEGMENT.setPixelColor(i, SEGCOLOR(0)); - } - } else - { - for (unsigned i = a; i < SEGLEN; i++) { - SEGMENT.setPixelColor(i, SEGCOLOR(0)); - } - for (unsigned i = 0; i < SEGENV.aux1 - (SEGLEN -a); i++) { - SEGMENT.setPixelColor(i, SEGCOLOR(0)); - } + else + SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } - SEGENV.step = a; - - return 3 + ((8 * (uint32_t)(255 - SEGMENT.speed)) / SEGLEN); + return FRAMETIME; } static const char _data_FX_MODE_ANDROID[] PROGMEM = "Android@!,Width;!,!;!;;m12=1"; //vertical - /* * color chase function. * color1 = background color From 66ad27ad3a3da160c082f8371ea90ff526598aab Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 19 May 2025 20:34:27 +0200 Subject: [PATCH 085/153] add support for up to 10 ESPNow remotes (#4654) * add support for up to 10 ESPNow remotes * removed debug line * changed todo comment * fixed some issues, shortened html/java code - reverting name to `linked_remote` - ESPNow remote list is now hidden if unchecked - shortened java script function names and variables to save flash - removed now obsolete settings in xml.cpp - correct checking of valid hex string for remote list in java script * fixed indentation, using emplace_back instead of push_back, using JsonVariant, replaced buttons with +/- * shortened java code * updated java code, fixed bug - element is now properly removed - `+` button is hidden if list is full - user needs to remove a remote, then reload the page to add it (workaround for edge case that needs more code to handle otherwise) * add limit * clearer usage description --- wled00/cfg.cpp | 25 ++++++++++++++-- wled00/data/settings_wifi.htm | 55 +++++++++++++++++++++++++++++++---- wled00/remote.cpp | 8 +---- wled00/set.cpp | 17 +++++++++-- wled00/udp.cpp | 18 ++++++++---- wled00/wled.h | 3 +- wled00/xml.cpp | 10 +++---- 7 files changed, 108 insertions(+), 28 deletions(-) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index fa0397fc6..a342886d0 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -38,8 +38,24 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonObject nw = doc["nw"]; #ifndef WLED_DISABLE_ESPNOW CJSON(enableESPNow, nw[F("espnow")]); - getStringFromJson(linked_remote, nw[F("linked_remote")], 13); - linked_remote[12] = '\0'; + linked_remotes.clear(); + JsonVariant lrem = nw[F("linked_remote")]; + if (!lrem.isNull()) { + if (lrem.is()) { + for (size_t i = 0; i < lrem.size(); i++) { + std::array entry{}; + getStringFromJson(entry.data(), lrem[i], 13); + entry[12] = '\0'; + linked_remotes.emplace_back(entry); + } + } + else { // legacy support for single MAC address in config + std::array entry{}; + getStringFromJson(entry.data(), lrem, 13); + entry[12] = '\0'; + linked_remotes.emplace_back(entry); + } + } #endif size_t n = 0; @@ -725,7 +741,10 @@ void serializeConfig(JsonObject root) { JsonObject nw = root.createNestedObject("nw"); #ifndef WLED_DISABLE_ESPNOW nw[F("espnow")] = enableESPNow; - nw[F("linked_remote")] = linked_remote; + JsonArray lrem = nw.createNestedArray(F("linked_remote")); + for (size_t i = 0; i < linked_remotes.size(); i++) { + lrem.add(linked_remotes[i].data()); + } #endif JsonArray nw_ins = nw.createNestedArray("ins"); diff --git a/wled00/data/settings_wifi.htm b/wled00/data/settings_wifi.htm index 1531d161f..d2d7c66c4 100644 --- a/wled00/data/settings_wifi.htm +++ b/wled00/data/settings_wifi.htm @@ -136,12 +136,52 @@ Static subnet mask:
getLoc(); loadJS(getURL('/settings/s.js?p=1'), false); // If we set async false, file is loaded and executed, then next statement is processed if (loc) d.Sf.action = getURL('/settings/wifi'); + setTimeout(tE, 500); // wait for DOM to load before calling tE() } + + var rC = 0; // remote count + // toggle visibility of ESP-NOW remote list based on checkbox state + function tE() { + // keep the hidden input with MAC addresses, only toggle visibility of the list UI + gId('rlc').style.display = d.Sf.RE.checked ? 'block' : 'none'; + } + // reset remotes: initialize empty list (called from xml.cpp) + function rstR() { + gId('rml').innerHTML = ''; // clear remote list + } + // add remote MAC to the list + function aR(id, mac) { + if (!/^[0-9A-F]{12}$/i.test(mac)) return; // check for valid hex string + let inputs = d.querySelectorAll("#rml input"); + for (let i of (inputs || [])) { + if (i.value === mac) return; + } + let l = gId('rml'), r = cE('div'), i = cE('input'); + i.type = 'text'; + i.name = id; + i.value = mac; + i.maxLength = 12; + i.minLength = 12; + //i.onchange = uR; + r.appendChild(i); + let b = cE('button'); + b.type = 'button'; + b.className = 'sml'; + b.innerText = '-'; + b.onclick = (e) => { + r.remove(); + }; + r.appendChild(b); + l.appendChild(r); + rC++; + gId('+').style.display = gId("rml").childElementCount < 10 ? 'inline' : 'none'; // can't append to list anymore, hide button + } + -
+

@@ -202,11 +242,16 @@ Static subnet mask:
This firmware build does not include ESP-NOW support.
- Enable ESP-NOW:
+ Enable ESP-NOW:
Listen for events over ESP-NOW
- Keep disabled if not using a remote or wireless sync, increases power consumption.
- Paired Remote MAC:
- Last device seen: None
+ Keep disabled if not using a remote or ESP-NOW sync, increases power consumption.
+
+ Last device seen: None +
+ Linked MACs (10 max):
+
+
+
diff --git a/wled00/remote.cpp b/wled00/remote.cpp index 8c060a70c..14c3c0d01 100644 --- a/wled00/remote.cpp +++ b/wled00/remote.cpp @@ -181,16 +181,10 @@ static bool remoteJson(int button) return parsed; } -// Callback function that will be executed when data is received +// Callback function that will be executed when data is received from a linked remote void handleWiZdata(uint8_t *incomingData, size_t len) { message_structure_t *incoming = reinterpret_cast(incomingData); - if (strcmp(last_signal_src, linked_remote) != 0) { - DEBUG_PRINT(F("ESP Now Message Received from Unlinked Sender: ")); - DEBUG_PRINTLN(last_signal_src); - return; - } - if (len != sizeof(message_structure_t)) { DEBUG_PRINTF_P(PSTR("Unknown incoming ESP Now message received of length %u\n"), len); return; diff --git a/wled00/set.cpp b/wled00/set.cpp index 725875023..501202c7a 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -91,8 +91,21 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) bool oldESPNow = enableESPNow; enableESPNow = request->hasArg(F("RE")); if (oldESPNow != enableESPNow) forceReconnect = true; - strlcpy(linked_remote, request->arg(F("RMAC")).c_str(), 13); - strlwr(linked_remote); //Normalize MAC format to lowercase + linked_remotes.clear(); // clear old remotes + for (size_t n = 0; n < 10; n++) { + char rm[4]; + snprintf(rm, sizeof(rm), "RM%d", n); // "RM0" to "RM9" + if (request->hasArg(rm)) { + const String& arg = request->arg(rm); + if (arg.isEmpty()) continue; + std::array mac{}; + strlcpy(mac.data(), request->arg(rm).c_str(), 13); + strlwr(mac.data()); + if (mac[0] != '\0') { + linked_remotes.emplace_back(mac); + } + } + } #endif #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_USE_ETHERNET) diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 4395b285d..c2d450b9e 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -959,14 +959,22 @@ void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rs // usermods hook can override processing if (UsermodManager::onEspNowMessage(address, data, len)) return; - // handle WiZ Mote data - if (data[0] == 0x91 || data[0] == 0x81 || data[0] == 0x80) { - handleWiZdata(data, len); + bool knownRemote = false; + for (const auto& mac : linked_remotes) { + if (strlen(mac.data()) == 12 && strcmp(last_signal_src, mac.data()) == 0) { + knownRemote = true; + break; + } + } + if (!knownRemote) { + DEBUG_PRINT(F("ESP Now Message Received from Unlinked Sender: ")); + DEBUG_PRINTLN(last_signal_src); return; } - if (strlen(linked_remote) == 12 && strcmp(last_signal_src, linked_remote) != 0) { - DEBUG_PRINTLN(F("ESP-NOW unpaired remote sender.")); + // handle WiZ Mote data + if (data[0] == 0x91 || data[0] == 0x81 || data[0] == 0x80) { + handleWiZdata(data, len); return; } diff --git a/wled00/wled.h b/wled00/wled.h index f8dc1252a..a74df05e3 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -538,7 +538,8 @@ WLED_GLOBAL bool serialCanTX _INIT(false); WLED_GLOBAL bool enableESPNow _INIT(false); // global on/off for ESP-NOW WLED_GLOBAL byte statusESPNow _INIT(ESP_NOW_STATE_UNINIT); // state of ESP-NOW stack (0 uninitialised, 1 initialised, 2 error) WLED_GLOBAL bool useESPNowSync _INIT(false); // use ESP-NOW wireless technology for sync -WLED_GLOBAL char linked_remote[13] _INIT(""); // MAC of ESP-NOW remote (Wiz Mote) +//WLED_GLOBAL char linked_remote[13] _INIT(""); // MAC of ESP-NOW remote (Wiz Mote) +WLED_GLOBAL std::vector> linked_remotes; // MAC of ESP-NOW remotes (Wiz Mote) WLED_GLOBAL char last_signal_src[13] _INIT(""); // last seen ESP-NOW sender #endif diff --git a/wled00/xml.cpp b/wled00/xml.cpp index de2f5590d..80e1bf120 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -216,7 +216,11 @@ void getSettingsJS(byte subPage, Print& settingsScript) #ifndef WLED_DISABLE_ESPNOW printSetFormCheckbox(settingsScript,PSTR("RE"),enableESPNow); - printSetFormValue(settingsScript,PSTR("RMAC"),linked_remote); + settingsScript.printf_P(PSTR("rstR();")); // reset remote list + for (size_t i = 0; i < linked_remotes.size(); i++) { + settingsScript.printf_P(PSTR("aR(\"RM%u\",\"%s\");"), i, linked_remotes[i].data()); // add remote to list + } + settingsScript.print(F("tE();")); // fill fields #else //hide remote settings if not compiled settingsScript.print(F("toggle('ESPNOW');")); // hide ESP-NOW setting @@ -258,10 +262,6 @@ void getSettingsJS(byte subPage, Print& settingsScript) #ifndef WLED_DISABLE_ESPNOW if (strlen(last_signal_src) > 0) { //Have seen an ESP-NOW Remote printSetClassElementHTML(settingsScript,PSTR("rlid"),0,last_signal_src); - } else if (!enableESPNow) { - printSetClassElementHTML(settingsScript,PSTR("rlid"),0,(char*)F("(Enable ESP-NOW to listen)")); - } else { - printSetClassElementHTML(settingsScript,PSTR("rlid"),0,(char*)F("None")); } #endif } From 25223c446fde50ac2064db324c4d0780e3141351 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 19 May 2025 20:48:00 +0200 Subject: [PATCH 086/153] fixed bouncing bug (#4694) --- wled00/FX.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 8835e71ff..d50fd4563 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -9540,10 +9540,10 @@ uint16_t mode_particleSparkler(void) { PartSys->sources[i].var = 0; // sparks stationary PartSys->sources[i].minLife = 150 + SEGMENT.intensity; PartSys->sources[i].maxLife = 250 + (SEGMENT.intensity << 1); - uint32_t speed = SEGMENT.speed >> 1; + int32_t speed = SEGMENT.speed >> 1; if (SEGMENT.check1) // sparks move (slide option) PartSys->sources[i].var = SEGMENT.intensity >> 3; - PartSys->sources[i].source.vx = speed; // update speed, do not change direction + PartSys->sources[i].source.vx = PartSys->sources[i].source.vx > 0 ? speed : -speed; // update speed, do not change direction PartSys->sources[i].source.ttl = 400; // replenish its life (setting it perpetual uses more code) PartSys->sources[i].sat = SEGMENT.custom1; // color saturation PartSys->sources[i].size = SEGMENT.check3 ? 120 : 0; From 999637f8adfec4fcef5ecfb41bd141c8ffba57bb Mon Sep 17 00:00:00 2001 From: Will Miles Date: Mon, 19 May 2025 16:37:07 -0400 Subject: [PATCH 087/153] Validate usermods at link time Add additional validation of the linker .map output to confirm that the correct usermods were added. --- pio-scripts/validate_usermods.py | 92 ++++++++++++++++++++++++++++++++ platformio.ini | 1 + 2 files changed, 93 insertions(+) create mode 100644 pio-scripts/validate_usermods.py diff --git a/pio-scripts/validate_usermods.py b/pio-scripts/validate_usermods.py new file mode 100644 index 000000000..50d51f99f --- /dev/null +++ b/pio-scripts/validate_usermods.py @@ -0,0 +1,92 @@ +import re +import sys +from pathlib import Path # For OS-agnostic path manipulation +from click import secho +from SCons.Script import Action, Exit +from platformio import util + +def read_lines(p: Path): + """ Read in the contents of a file for analysis """ + with p.open("r", encoding="utf-8", errors="ignore") as f: + return f.readlines() + +def check_map_file_objects(map_file: list[str], usermod_dirs: list[str]) -> set[str]: + """ Checks that an object file from each usermod_dir appears in the linked output + + Returns the (sub)set of usermod_dirs that are found in the output ELF + """ + # Pattern to match symbols in object directories + # Join directories into alternation + usermod_dir_regex = "|".join([re.escape(dir) for dir in usermod_dirs]) + # Matches nonzero address, any size, and any path in a matching directory + object_path_regex = re.compile(r"0x0*[1-9a-f][0-9a-f]*\s+0x[0-9a-f]+\s+\S+/(" + usermod_dir_regex + r")/\S+\.o") + + found = set() + for line in map_file: + matches = object_path_regex.findall(line) + for m in matches: + found.add(m) + return found + +def count_registered_usermods(map_file: list[str]) -> int: + """ Returns the number of usermod objects in the usermod list """ + # Count the number of entries in the usermods table section + return len([x for x in map_file if ".dtors.tbl.usermods.1" in x]) + + +def validate_map_file(source, target, env): + """ Validate that all usermods appear in the output build """ + build_dir = Path(env.subst("$BUILD_DIR")) + map_file_path = build_dir / env.subst("${PROGNAME}.map") + + if not map_file_path.exists(): + secho(f"ERROR: Map file not found: {map_file_path}", fg="red", err=True) + Exit(1) + + # Load project settings + usermods = env.GetProjectOption("custom_usermods","").split() + libdeps = env.GetProjectOption("lib_deps", []) + lib_builders = env.GetLibBuilders() + + secho(f"INFO: Expecting {len(usermods)} usermods: {', '.join(usermods)}") + + # Map the usermods to libdeps; every usermod should have one + usermod_dirs = [] + for mod in usermods: + modstr = f"{mod} = symlink://" + this_mod_libdeps = [libdep[len(modstr):] for libdep in libdeps if libdep.startswith(modstr)] + if not this_mod_libdeps: + secho( + f"ERROR: Usermod {mod} not found in build libdeps!", + fg="red", + err=True) + Exit(1) + # Save only the final folder name + usermod_dir = Path(this_mod_libdeps[0]).name + # Search lib_builders + this_mod_builders = [builder for builder in lib_builders if Path(builder.src_dir).name == usermod_dir] + if not this_mod_builders: + secho( + f"ERROR: Usermod {mod} not found in library builders!", + fg="red", + err=True) + Exit(1) + usermod_dirs.append(usermod_dir) + + # Now parse the map file + map_file_contents = read_lines(map_file_path) + confirmed_usermods = check_map_file_objects(map_file_contents, usermod_dirs) + usermod_object_count = count_registered_usermods(map_file_contents) + + secho(f"INFO: {len(usermod_dirs)}/{len(usermods)} libraries linked via custom_usermods, producing {usermod_object_count} usermod object entries") + missing_usermods = confirmed_usermods.difference(usermod_dirs) + if missing_usermods: + secho( + f"ERROR: No object files from {missing_usermods} found in linked output!", + fg="red", + err=True) + Exit(1) + return None + +Import("env") +env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking map file...')) diff --git a/platformio.ini b/platformio.ini index a7485244c..b713a2d39 100644 --- a/platformio.ini +++ b/platformio.ini @@ -116,6 +116,7 @@ extra_scripts = pre:pio-scripts/user_config_copy.py pre:pio-scripts/load_usermods.py pre:pio-scripts/build_ui.py + post:pio-scripts/validate_usermods.py ;; double-check the build output usermods ; post:pio-scripts/obj-dump.py ;; convenience script to create a disassembly dump of the firmware (hardcore debugging) # ------------------------------------------------------------------------------ From 24ab2952ee50dc6bd13bf96eafc3584531631cb2 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Mon, 19 May 2025 16:53:08 -0400 Subject: [PATCH 088/153] Add unambiguous usermod list to info Neither the info panel nor the settings dialog can be trusted to accurately report the usermod list: - Not all usermods necessarily add to the info panel - Not all usermods necessarily add to the config page - #4609 is required for the config page to be correct Add a short list to the info object that lists the loaded usermod IDs. This is not displayed via the UI, but can be queried with curl or web debug tools. To be removed when usermod loading is working well. --- wled00/um_manager.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/wled00/um_manager.cpp b/wled00/um_manager.cpp index 9bfb7e737..1a7cc2269 100644 --- a/wled00/um_manager.cpp +++ b/wled00/um_manager.cpp @@ -39,7 +39,13 @@ bool UsermodManager::getUMData(um_data_t **data, uint8_t mod_id) { return false; } void UsermodManager::addToJsonState(JsonObject& obj) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->addToJsonState(obj); } -void UsermodManager::addToJsonInfo(JsonObject& obj) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->addToJsonInfo(obj); } +void UsermodManager::addToJsonInfo(JsonObject& obj) { + auto um_id_list = obj.createNestedArray("um"); + for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) { + um_id_list.add((*mod)->getId()); + (*mod)->addToJsonInfo(obj); + } +} void UsermodManager::readFromJsonState(JsonObject& obj) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->readFromJsonState(obj); } void UsermodManager::addToConfig(JsonObject& obj) { for (auto mod = _usermod_table_begin; mod < _usermod_table_end; ++mod) (*mod)->addToConfig(obj); } bool UsermodManager::readFromConfig(JsonObject& obj) { From ac61eb4b1b2ff260381dad4284ecc734cdf7479d Mon Sep 17 00:00:00 2001 From: Will Miles Date: Mon, 19 May 2025 17:41:11 -0400 Subject: [PATCH 089/153] validate_usermods: Fix inverted check Difference direction was inverted. It's tough to test when it always works correctly on your local machine! H/t @coderabbitai --- pio-scripts/validate_usermods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pio-scripts/validate_usermods.py b/pio-scripts/validate_usermods.py index 50d51f99f..82ea5e07d 100644 --- a/pio-scripts/validate_usermods.py +++ b/pio-scripts/validate_usermods.py @@ -79,7 +79,7 @@ def validate_map_file(source, target, env): usermod_object_count = count_registered_usermods(map_file_contents) secho(f"INFO: {len(usermod_dirs)}/{len(usermods)} libraries linked via custom_usermods, producing {usermod_object_count} usermod object entries") - missing_usermods = confirmed_usermods.difference(usermod_dirs) + missing_usermods = set(usermod_dirs).difference(confirmed_usermods) if missing_usermods: secho( f"ERROR: No object files from {missing_usermods} found in linked output!", From 817157bbc1d7c0c725e8c607ff207e6acb470df7 Mon Sep 17 00:00:00 2001 From: "Christian W. Zuckschwerdt" Date: Thu, 22 May 2025 09:36:41 +0200 Subject: [PATCH 090/153] Change to set LWT only once --- wled00/mqtt.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/wled00/mqtt.cpp b/wled00/mqtt.cpp index a462881ec..dfa336d0d 100644 --- a/wled00/mqtt.cpp +++ b/wled00/mqtt.cpp @@ -27,7 +27,7 @@ static void parseMQTTBriPayload(char* payload) static void onMqttConnect(bool sessionPresent) { //(re)subscribe to required topics - char subuf[MQTT_MAX_TOPIC_LEN + 6]; + char subuf[MQTT_MAX_TOPIC_LEN + 9]; if (mqttDeviceTopic[0] != 0) { strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); @@ -52,6 +52,13 @@ static void onMqttConnect(bool sessionPresent) UsermodManager::onMqttConnect(sessionPresent); DEBUG_PRINTLN(F("MQTT ready")); + +#ifndef USERMOD_SMARTNEST + strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); + strcat_P(subuf, PSTR("/status")); + mqtt->publish(subuf, 0, true, "online"); // retain message for a LWT +#endif + publishMqtt(); } @@ -174,10 +181,6 @@ void publishMqtt() strcat_P(subuf, PSTR("/c")); mqtt->publish(subuf, 0, retainMqttMsg, s); // optionally retain message (#2263) - strlcpy(subuf, mqttDeviceTopic, MQTT_MAX_TOPIC_LEN + 1); - strcat_P(subuf, PSTR("/status")); - mqtt->publish(subuf, 0, true, "online"); // retain message for a LWT - // TODO: use a DynamicBufferList. Requires a list-read-capable MQTT client API. DynamicBuffer buf(1024); bufferPrint pbuf(buf.data(), buf.size()); From c693f63244e9e4bf8732349f86f7392dd0887f86 Mon Sep 17 00:00:00 2001 From: "Christian W. Zuckschwerdt" Date: Thu, 22 May 2025 13:36:55 +0200 Subject: [PATCH 091/153] Fix running initMqtt twice on bootup --- wled00/wled.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/wled00/wled.cpp b/wled00/wled.cpp index cc338d23f..7de25354a 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -738,9 +738,6 @@ void WLED::initInterfaces() e131.begin(e131Multicast, e131Port, e131Universe, E131_MAX_UNIVERSE_COUNT); ddp.begin(false, DDP_DEFAULT_PORT); reconnectHue(); -#ifndef WLED_DISABLE_MQTT - initMqtt(); -#endif interfacesInited = true; wasConnected = true; } From 358e38e0562b6082e26772d7407e8eed357a44a5 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Thu, 22 May 2025 08:41:59 -0400 Subject: [PATCH 092/153] validate_usermods: Ensure map file is created Not all of our platforms create one by default; ensure it's produced. --- pio-scripts/validate_usermods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pio-scripts/validate_usermods.py b/pio-scripts/validate_usermods.py index 82ea5e07d..1291a5613 100644 --- a/pio-scripts/validate_usermods.py +++ b/pio-scripts/validate_usermods.py @@ -89,4 +89,4 @@ def validate_map_file(source, target, env): return None Import("env") -env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking map file...')) +env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")]) From 242df4b04994110dd8351b85715558ccc67f5ab0 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Thu, 22 May 2025 08:43:52 -0400 Subject: [PATCH 093/153] validate_usermods: Fix old ESP32 platform The modern linker used with new platforms (ESP8266, ESP32 >v4) always produces paths in the map file with slash; however the old linker for the old ESP32 platform instead produces paths with backslash when building on Windows. Match both types as a path separator when scanning linked symbols. --- pio-scripts/validate_usermods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pio-scripts/validate_usermods.py b/pio-scripts/validate_usermods.py index 1291a5613..7b3bdf8e1 100644 --- a/pio-scripts/validate_usermods.py +++ b/pio-scripts/validate_usermods.py @@ -19,7 +19,7 @@ def check_map_file_objects(map_file: list[str], usermod_dirs: list[str]) -> set[ # Join directories into alternation usermod_dir_regex = "|".join([re.escape(dir) for dir in usermod_dirs]) # Matches nonzero address, any size, and any path in a matching directory - object_path_regex = re.compile(r"0x0*[1-9a-f][0-9a-f]*\s+0x[0-9a-f]+\s+\S+/(" + usermod_dir_regex + r")/\S+\.o") + object_path_regex = re.compile(r"0x0*[1-9a-f][0-9a-f]*\s+0x[0-9a-f]+\s+\S+[/\\](" + usermod_dir_regex + r")[/\\]\S+\.o") found = set() for line in map_file: From 7ea510e75b666f2b12ecba2b0f2c5d8d3c1722e1 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Thu, 22 May 2025 08:44:05 -0400 Subject: [PATCH 094/153] validate_usermods: Improve message --- pio-scripts/validate_usermods.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pio-scripts/validate_usermods.py b/pio-scripts/validate_usermods.py index 7b3bdf8e1..d3cf5ea8c 100644 --- a/pio-scripts/validate_usermods.py +++ b/pio-scripts/validate_usermods.py @@ -90,3 +90,4 @@ def validate_map_file(source, target, env): Import("env") env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")]) +env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking linked usermods in map file...')) From 792a7aa0815be4fc3a8e7779f5d0d93a49d3cc51 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Thu, 22 May 2025 09:13:54 -0400 Subject: [PATCH 095/153] load_usermods: Resolve folder paths Ensure all paths used in usermod symlinks are fully resolved, including any case correctness issues on Windows. Apparently PlatformIO does not handle symlink files correctly on Windows if there are case differences between cwd and the resolved path. --- pio-scripts/load_usermods.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pio-scripts/load_usermods.py b/pio-scripts/load_usermods.py index 8cf625ff6..4e0457a0c 100644 --- a/pio-scripts/load_usermods.py +++ b/pio-scripts/load_usermods.py @@ -7,7 +7,7 @@ from SCons.Script import Exit from platformio.builder.tools.piolib import LibBuilderBase from platformio.package.manager.library import LibraryPackageManager -usermod_dir = Path(env["PROJECT_DIR"]) / "usermods" +usermod_dir = Path(env["PROJECT_DIR"]).resolve() / "usermods" # "usermods" environment: expand list of usermods to everything in the folder if env['PIOENV'] == "usermods": @@ -48,7 +48,7 @@ if usermods: src_dir = proj.get("platformio", "src_dir") src_dir = src_dir.replace('\\','/') mod_paths = {mod: find_usermod(mod) for mod in usermods.split()} - usermods = [f"{mod} = symlink://{path}" for mod, path in mod_paths.items()] + usermods = [f"{mod} = symlink://{path.resolve()}" for mod, path in mod_paths.items()] proj.set("env:" + env['PIOENV'], 'lib_deps', deps + usermods) # Force usermods to be installed in to the environment build state before the LDF runs # Otherwise we won't be able to see them until it's too late to change their paths for LDF From 75cd411073ad9e6c984b0f9ffc0c4efae8bede33 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 24 May 2025 14:13:08 -0400 Subject: [PATCH 096/153] Improve all-usermod handling Use a magic custom_usermods string instead of a magic environment name; and disable the validation script as it triggers on the non- platform-compatible mods. --- pio-scripts/load_usermods.py | 42 +++++++++----------------------- pio-scripts/validate_usermods.py | 5 ++-- platformio.ini | 2 +- 3 files changed, 16 insertions(+), 33 deletions(-) diff --git a/pio-scripts/load_usermods.py b/pio-scripts/load_usermods.py index 4e0457a0c..4f986d6fe 100644 --- a/pio-scripts/load_usermods.py +++ b/pio-scripts/load_usermods.py @@ -9,12 +9,6 @@ from platformio.package.manager.library import LibraryPackageManager usermod_dir = Path(env["PROJECT_DIR"]).resolve() / "usermods" -# "usermods" environment: expand list of usermods to everything in the folder -if env['PIOENV'] == "usermods": - # Add all usermods - all_usermods = [f for f in usermod_dir.iterdir() if f.is_dir() and f.joinpath('library.json').exists()] - env.GetProjectConfig().set(f"env:usermods", 'custom_usermods', " ".join([f.name for f in all_usermods])) - # Utility functions def find_usermod(mod: str) -> Path: """Locate this library in the usermods folder. @@ -41,38 +35,26 @@ def is_wled_module(dep: LibBuilderBase) -> bool: ## Script starts here # Process usermod option usermods = env.GetProjectOption("custom_usermods","") + +# Handle "all usermods" case +if usermods == '*': + usermods = [f.name for f in usermod_dir.iterdir() if f.is_dir() and f.joinpath('library.json').exists()] + # Update the environment, as many modules use scripts to detect their dependencies + env.GetProjectConfig().set("env:" + env['PIOENV'], 'custom_usermods', " ".join(usermods)) + # Leave a note for the validation script + env.GetProjectConfig().set("env:" + env['PIOENV'], 'custom_all_usermods_enabled', "1") +else: + usermods = usermods.split() + if usermods: # Inject usermods in to project lib_deps proj = env.GetProjectConfig() deps = env.GetProjectOption('lib_deps') src_dir = proj.get("platformio", "src_dir") src_dir = src_dir.replace('\\','/') - mod_paths = {mod: find_usermod(mod) for mod in usermods.split()} + mod_paths = {mod: find_usermod(mod) for mod in usermods} usermods = [f"{mod} = symlink://{path.resolve()}" for mod, path in mod_paths.items()] proj.set("env:" + env['PIOENV'], 'lib_deps', deps + usermods) - # Force usermods to be installed in to the environment build state before the LDF runs - # Otherwise we won't be able to see them until it's too late to change their paths for LDF - # Logic is largely borrowed from PlaformIO internals - not_found_specs = [] - for spec in usermods: - found = False - for storage_dir in env.GetLibSourceDirs(): - #print(f"Checking {storage_dir} for {spec}") - lm = LibraryPackageManager(storage_dir) - if lm.get_package(spec): - #print("Found!") - found = True - break - if not found: - #print("Missing!") - not_found_specs.append(spec) - if not_found_specs: - lm = LibraryPackageManager( - env.subst(os.path.join("$PROJECT_LIBDEPS_DIR", "$PIOENV")) - ) - for spec in not_found_specs: - #print(f"LU: forcing install of {spec}") - lm.install(spec) # Utility function for assembling usermod include paths diff --git a/pio-scripts/validate_usermods.py b/pio-scripts/validate_usermods.py index d3cf5ea8c..a1a1e3c24 100644 --- a/pio-scripts/validate_usermods.py +++ b/pio-scripts/validate_usermods.py @@ -89,5 +89,6 @@ def validate_map_file(source, target, env): return None Import("env") -env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")]) -env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking linked usermods in map file...')) +if not env.GetProjectOption("custom_all_usermods_enabled",""): # TODO: fix handling of platform mismatches + env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")]) + env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking linked usermods in map file...')) diff --git a/platformio.ini b/platformio.ini index b713a2d39..e1a5014b0 100644 --- a/platformio.ini +++ b/platformio.ini @@ -660,5 +660,5 @@ build_flags = ${common.build_flags} ${esp32_idf_V4.build_flags} -D WLED_RELEASE_ lib_deps = ${esp32_idf_V4.lib_deps} monitor_filters = esp32_exception_decoder board_build.flash_mode = dio -; custom_usermods = *every folder with library.json* -- injected by pio-scripts/load_usermods.py +custom_usermods = * ; Expands to all usermods in usermods folder board_build.partitions = ${esp32.extreme_partitions} ; We're gonna need a bigger boat From 0a7d3a9d9b7437d940739c832f615481b708474a Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 24 May 2025 22:16:01 -0400 Subject: [PATCH 097/153] load_usermods: Simplify load code Remove all the unnecessary bits. --- pio-scripts/load_usermods.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pio-scripts/load_usermods.py b/pio-scripts/load_usermods.py index 4f986d6fe..d50bf196b 100644 --- a/pio-scripts/load_usermods.py +++ b/pio-scripts/load_usermods.py @@ -48,14 +48,8 @@ else: if usermods: # Inject usermods in to project lib_deps - proj = env.GetProjectConfig() - deps = env.GetProjectOption('lib_deps') - src_dir = proj.get("platformio", "src_dir") - src_dir = src_dir.replace('\\','/') - mod_paths = {mod: find_usermod(mod) for mod in usermods} - usermods = [f"{mod} = symlink://{path.resolve()}" for mod, path in mod_paths.items()] - proj.set("env:" + env['PIOENV'], 'lib_deps', deps + usermods) - + symlinks = [f"symlink://{find_usermod(mod).resolve()}" for mod in usermods] + env.GetProjectConfig().set("env:" + env['PIOENV'], 'lib_deps', env.GetProjectOption('lib_deps') + symlinks) # Utility function for assembling usermod include paths def cached_add_includes(dep, dep_cache: set, includes: deque): From 75c95d88e25698e09d5806f77249cdde1599cf3b Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 24 May 2025 22:18:22 -0400 Subject: [PATCH 098/153] usermods/*/setup_deps.py: Check lib_deps for deps Check the safest possible location for final information on what components are actually being linked in. This demonstrates a safe approach that works even for out-of-tree modules. --- usermods/PWM_fan/setup_deps.py | 9 +++++---- usermods/seven_segment_display_reloaded/setup_deps.py | 7 ++++--- usermods/usermod_v2_rotary_encoder_ui_ALT/setup_deps.py | 6 +++--- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/usermods/PWM_fan/setup_deps.py b/usermods/PWM_fan/setup_deps.py index b8f7276c5..11879079a 100644 --- a/usermods/PWM_fan/setup_deps.py +++ b/usermods/PWM_fan/setup_deps.py @@ -1,11 +1,12 @@ +from platformio.package.meta import PackageSpec Import('env') -usermods = env.GetProjectOption("custom_usermods","").split() +libs = [PackageSpec(lib).name for lib in env.GetProjectOption("lib_deps",[])] # Check for dependencies -if "Temperature" in usermods: +if "Temperature" in libs: env.Append(CPPDEFINES=[("USERMOD_DALLASTEMPERATURE")]) -elif "sht" in usermods: +elif "sht" in libs: env.Append(CPPDEFINES=[("USERMOD_SHT")]) -elif "PWM_fan" in usermods: # The script can be run if this module was previously selected +elif "PWM_fan" in libs: # The script can be run if this module was previously selected raise RuntimeError("PWM_fan usermod requires Temperature or sht to be enabled") diff --git a/usermods/seven_segment_display_reloaded/setup_deps.py b/usermods/seven_segment_display_reloaded/setup_deps.py index dd28f5fe9..1c51accce 100644 --- a/usermods/seven_segment_display_reloaded/setup_deps.py +++ b/usermods/seven_segment_display_reloaded/setup_deps.py @@ -1,9 +1,10 @@ +from platformio.package.meta import PackageSpec Import('env') -usermods = env.GetProjectOption("custom_usermods","").split() +libs = [PackageSpec(lib).name for lib in env.GetProjectOption("lib_deps",[])] # Check for partner usermods -if "SN_Photoresistor" in usermods: +if "SN_Photoresistor" in libs: env.Append(CPPDEFINES=[("USERMOD_SN_PHOTORESISTOR")]) -if any(mod in ("BH1750_v2", "BH1750") for mod in usermods): +if any(mod in ("BH1750_v2", "BH1750") for mod in libs): env.Append(CPPDEFINES=[("USERMOD_BH1750")]) diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/setup_deps.py b/usermods/usermod_v2_rotary_encoder_ui_ALT/setup_deps.py index a6b565951..ed579bc12 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/setup_deps.py +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/setup_deps.py @@ -1,8 +1,8 @@ +from platformio.package.meta import PackageSpec Import('env') - -usermods = env.GetProjectOption("custom_usermods","").split() +libs = [PackageSpec(lib).name for lib in env.GetProjectOption("lib_deps",[])] # Check for partner usermod # Allow both "usermod_v2" and unqualified syntax -if any(mod in ("four_line_display_ALT", "usermod_v2_four_line_display_ALT") for mod in usermods): +if any(mod in ("four_line_display_ALT", "usermod_v2_four_line_display_ALT") for mod in libs): env.Append(CPPDEFINES=[("USERMOD_FOUR_LINE_DISPLAY")]) From 309c8d67f3e66298ceb2fcc74267865ecbd3b5a6 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 24 May 2025 23:14:22 -0400 Subject: [PATCH 099/153] Generalize module link validation Perform validation for external modules, too. --- pio-scripts/load_usermods.py | 4 -- pio-scripts/validate_modules.py | 95 ++++++++++++++++++++++++++++++++ pio-scripts/validate_usermods.py | 94 ------------------------------- platformio.ini | 2 +- 4 files changed, 96 insertions(+), 99 deletions(-) create mode 100644 pio-scripts/validate_modules.py delete mode 100644 pio-scripts/validate_usermods.py diff --git a/pio-scripts/load_usermods.py b/pio-scripts/load_usermods.py index d50bf196b..31e211fc0 100644 --- a/pio-scripts/load_usermods.py +++ b/pio-scripts/load_usermods.py @@ -39,10 +39,6 @@ usermods = env.GetProjectOption("custom_usermods","") # Handle "all usermods" case if usermods == '*': usermods = [f.name for f in usermod_dir.iterdir() if f.is_dir() and f.joinpath('library.json').exists()] - # Update the environment, as many modules use scripts to detect their dependencies - env.GetProjectConfig().set("env:" + env['PIOENV'], 'custom_usermods', " ".join(usermods)) - # Leave a note for the validation script - env.GetProjectConfig().set("env:" + env['PIOENV'], 'custom_all_usermods_enabled', "1") else: usermods = usermods.split() diff --git a/pio-scripts/validate_modules.py b/pio-scripts/validate_modules.py new file mode 100644 index 000000000..eb6ebb446 --- /dev/null +++ b/pio-scripts/validate_modules.py @@ -0,0 +1,95 @@ +import re +import sys +from pathlib import Path # For OS-agnostic path manipulation +from typing import Iterable +from click import secho +from SCons.Script import Action, Exit +from platformio import util +from platformio.builder.tools.piolib import LibBuilderBase + + +def is_wled_module(env, dep: LibBuilderBase) -> bool: + """Returns true if the specified library is a wled module + """ + usermod_dir = Path(env["PROJECT_DIR"]).resolve() / "usermods" + return usermod_dir in Path(dep.src_dir).parents or str(dep.name).startswith("wled-") + + +def read_lines(p: Path): + """ Read in the contents of a file for analysis """ + with p.open("r", encoding="utf-8", errors="ignore") as f: + return f.readlines() + + +def check_map_file_objects(map_file: list[str], dirs: Iterable[str]) -> set[str]: + """ Identify which dirs contributed to the final build + + Returns the (sub)set of dirs that are found in the output ELF + """ + # Pattern to match symbols in object directories + # Join directories into alternation + usermod_dir_regex = "|".join([re.escape(dir) for dir in dirs]) + # Matches nonzero address, any size, and any path in a matching directory + object_path_regex = re.compile(r"0x0*[1-9a-f][0-9a-f]*\s+0x[0-9a-f]+\s+\S+[/\\](" + usermod_dir_regex + r")[/\\]\S+\.o") + + found = set() + for line in map_file: + matches = object_path_regex.findall(line) + for m in matches: + found.add(m) + return found + + +def count_usermod_objects(map_file: list[str]) -> int: + """ Returns the number of usermod objects in the usermod list """ + # Count the number of entries in the usermods table section + return len([x for x in map_file if ".dtors.tbl.usermods.1" in x]) + + +def validate_map_file(source, target, env): + """ Validate that all modules appear in the output build """ + build_dir = Path(env.subst("$BUILD_DIR")) + map_file_path = build_dir / env.subst("${PROGNAME}.map") + + if not map_file_path.exists(): + secho(f"ERROR: Map file not found: {map_file_path}", fg="red", err=True) + Exit(1) + + # Identify the WLED module source directories + module_lib_builders = [builder for builder in env.GetLibBuilders() if is_wled_module(env, builder)] + + if env.GetProjectOption("custom_usermods","") == "*": + # All usermods build; filter non-platform-OK modules + module_lib_builders = [builder for builder in module_lib_builders if env.IsCompatibleLibBuilder(builder)] + else: + incompatible_builders = [builder for builder in module_lib_builders if not env.IsCompatibleLibBuilder(builder)] + if incompatible_builders: + secho( + f"ERROR: Modules {[b.name for b in incompatible_builders]} are not compatible with this platform!", + fg="red", + err=True) + Exit(1) + pass + + # Extract the values we care about + modules = {Path(builder.build_dir).name: builder.name for builder in module_lib_builders} + secho(f"INFO: {len(modules)} libraries linked as WLED optional/user modules") + + # Now parse the map file + map_file_contents = read_lines(map_file_path) + usermod_object_count = count_usermod_objects(map_file_contents) + secho(f"INFO: {usermod_object_count} usermod object entries") + + confirmed_modules = check_map_file_objects(map_file_contents, modules.keys()) + missing_modules = [modname for mdir, modname in modules.items() if mdir not in confirmed_modules] + if missing_modules: + secho( + f"ERROR: No object files from {missing_modules} found in linked output!", + fg="red", + err=True) + Exit(1) + return None + +Import("env") +env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")]) +env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking linked optional modules (usermods) in map file')) diff --git a/pio-scripts/validate_usermods.py b/pio-scripts/validate_usermods.py deleted file mode 100644 index a1a1e3c24..000000000 --- a/pio-scripts/validate_usermods.py +++ /dev/null @@ -1,94 +0,0 @@ -import re -import sys -from pathlib import Path # For OS-agnostic path manipulation -from click import secho -from SCons.Script import Action, Exit -from platformio import util - -def read_lines(p: Path): - """ Read in the contents of a file for analysis """ - with p.open("r", encoding="utf-8", errors="ignore") as f: - return f.readlines() - -def check_map_file_objects(map_file: list[str], usermod_dirs: list[str]) -> set[str]: - """ Checks that an object file from each usermod_dir appears in the linked output - - Returns the (sub)set of usermod_dirs that are found in the output ELF - """ - # Pattern to match symbols in object directories - # Join directories into alternation - usermod_dir_regex = "|".join([re.escape(dir) for dir in usermod_dirs]) - # Matches nonzero address, any size, and any path in a matching directory - object_path_regex = re.compile(r"0x0*[1-9a-f][0-9a-f]*\s+0x[0-9a-f]+\s+\S+[/\\](" + usermod_dir_regex + r")[/\\]\S+\.o") - - found = set() - for line in map_file: - matches = object_path_regex.findall(line) - for m in matches: - found.add(m) - return found - -def count_registered_usermods(map_file: list[str]) -> int: - """ Returns the number of usermod objects in the usermod list """ - # Count the number of entries in the usermods table section - return len([x for x in map_file if ".dtors.tbl.usermods.1" in x]) - - -def validate_map_file(source, target, env): - """ Validate that all usermods appear in the output build """ - build_dir = Path(env.subst("$BUILD_DIR")) - map_file_path = build_dir / env.subst("${PROGNAME}.map") - - if not map_file_path.exists(): - secho(f"ERROR: Map file not found: {map_file_path}", fg="red", err=True) - Exit(1) - - # Load project settings - usermods = env.GetProjectOption("custom_usermods","").split() - libdeps = env.GetProjectOption("lib_deps", []) - lib_builders = env.GetLibBuilders() - - secho(f"INFO: Expecting {len(usermods)} usermods: {', '.join(usermods)}") - - # Map the usermods to libdeps; every usermod should have one - usermod_dirs = [] - for mod in usermods: - modstr = f"{mod} = symlink://" - this_mod_libdeps = [libdep[len(modstr):] for libdep in libdeps if libdep.startswith(modstr)] - if not this_mod_libdeps: - secho( - f"ERROR: Usermod {mod} not found in build libdeps!", - fg="red", - err=True) - Exit(1) - # Save only the final folder name - usermod_dir = Path(this_mod_libdeps[0]).name - # Search lib_builders - this_mod_builders = [builder for builder in lib_builders if Path(builder.src_dir).name == usermod_dir] - if not this_mod_builders: - secho( - f"ERROR: Usermod {mod} not found in library builders!", - fg="red", - err=True) - Exit(1) - usermod_dirs.append(usermod_dir) - - # Now parse the map file - map_file_contents = read_lines(map_file_path) - confirmed_usermods = check_map_file_objects(map_file_contents, usermod_dirs) - usermod_object_count = count_registered_usermods(map_file_contents) - - secho(f"INFO: {len(usermod_dirs)}/{len(usermods)} libraries linked via custom_usermods, producing {usermod_object_count} usermod object entries") - missing_usermods = set(usermod_dirs).difference(confirmed_usermods) - if missing_usermods: - secho( - f"ERROR: No object files from {missing_usermods} found in linked output!", - fg="red", - err=True) - Exit(1) - return None - -Import("env") -if not env.GetProjectOption("custom_all_usermods_enabled",""): # TODO: fix handling of platform mismatches - env.Append(LINKFLAGS=[env.subst("-Wl,--Map=${BUILD_DIR}/${PROGNAME}.map")]) - env.AddPostAction("$BUILD_DIR/${PROGNAME}.elf", Action(validate_map_file, cmdstr='Checking linked usermods in map file...')) diff --git a/platformio.ini b/platformio.ini index e1a5014b0..9bdf58d34 100644 --- a/platformio.ini +++ b/platformio.ini @@ -116,7 +116,7 @@ extra_scripts = pre:pio-scripts/user_config_copy.py pre:pio-scripts/load_usermods.py pre:pio-scripts/build_ui.py - post:pio-scripts/validate_usermods.py ;; double-check the build output usermods + post:pio-scripts/validate_modules.py ;; double-check the build output usermods ; post:pio-scripts/obj-dump.py ;; convenience script to create a disassembly dump of the firmware (hardcore debugging) # ------------------------------------------------------------------------------ From e80a7c6b757a6c17b58a224c6442ce010fb41993 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 24 May 2025 23:15:36 -0400 Subject: [PATCH 100/153] usermod_v2_HttpPullLightControl: Add usermod object The module instance was missing. --- .../usermod_v2_HttpPullLightControl.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp b/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp index b908b057c..44a2726ed 100644 --- a/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp +++ b/usermods/usermod_v2_HttpPullLightControl/usermod_v2_HttpPullLightControl.cpp @@ -4,6 +4,9 @@ const char HttpPullLightControl::_name[] PROGMEM = "HttpPullLightControl"; const char HttpPullLightControl::_enabled[] PROGMEM = "Enable"; +static HttpPullLightControl http_pull_usermod; +REGISTER_USERMOD(http_pull_usermod); + void HttpPullLightControl::setup() { //Serial.begin(115200); From f3623158d7051ef792bf3f3243ebee90fba794e9 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 25 May 2025 08:33:27 -0400 Subject: [PATCH 101/153] Usermod script cleanup Fix whitespace and remove unused imports --- pio-scripts/load_usermods.py | 8 +++----- pio-scripts/validate_modules.py | 11 ++++------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/pio-scripts/load_usermods.py b/pio-scripts/load_usermods.py index 31e211fc0..146cb1f87 100644 --- a/pio-scripts/load_usermods.py +++ b/pio-scripts/load_usermods.py @@ -1,11 +1,9 @@ Import('env') -import os.path from collections import deque from pathlib import Path # For OS-agnostic path manipulation from click import secho from SCons.Script import Exit from platformio.builder.tools.piolib import LibBuilderBase -from platformio.package.manager.library import LibraryPackageManager usermod_dir = Path(env["PROJECT_DIR"]).resolve() / "usermods" @@ -21,7 +19,7 @@ def find_usermod(mod: str) -> Path: return mp mp = usermod_dir / f"{mod}_v2" if mp.exists(): - return mp + return mp mp = usermod_dir / f"usermod_v2_{mod}" if mp.exists(): return mp @@ -50,7 +48,7 @@ if usermods: # Utility function for assembling usermod include paths def cached_add_includes(dep, dep_cache: set, includes: deque): """ Add dep's include paths to includes if it's not in the cache """ - if dep not in dep_cache: + if dep not in dep_cache: dep_cache.add(dep) for include in dep.get_include_dirs(): if include not in includes: @@ -96,7 +94,7 @@ def wrapped_ConfigureProjectLibBuilder(xenv): secho( f"ERROR: libArchive=false is missing on usermod(s) {' '.join(broken_usermods)} -- modules will not compile in correctly", fg="red", - err=True) + err=True) Exit(1) return result diff --git a/pio-scripts/validate_modules.py b/pio-scripts/validate_modules.py index eb6ebb446..f8c9a599d 100644 --- a/pio-scripts/validate_modules.py +++ b/pio-scripts/validate_modules.py @@ -1,10 +1,8 @@ import re -import sys from pathlib import Path # For OS-agnostic path manipulation from typing import Iterable from click import secho from SCons.Script import Action, Exit -from platformio import util from platformio.builder.tools.piolib import LibBuilderBase @@ -56,8 +54,8 @@ def validate_map_file(source, target, env): Exit(1) # Identify the WLED module source directories - module_lib_builders = [builder for builder in env.GetLibBuilders() if is_wled_module(env, builder)] - + module_lib_builders = [builder for builder in env.GetLibBuilders() if is_wled_module(env, builder)] + if env.GetProjectOption("custom_usermods","") == "*": # All usermods build; filter non-platform-OK modules module_lib_builders = [builder for builder in module_lib_builders if env.IsCompatibleLibBuilder(builder)] @@ -68,8 +66,7 @@ def validate_map_file(source, target, env): f"ERROR: Modules {[b.name for b in incompatible_builders]} are not compatible with this platform!", fg="red", err=True) - Exit(1) - pass + Exit(1) # Extract the values we care about modules = {Path(builder.build_dir).name: builder.name for builder in module_lib_builders} @@ -77,7 +74,7 @@ def validate_map_file(source, target, env): # Now parse the map file map_file_contents = read_lines(map_file_path) - usermod_object_count = count_usermod_objects(map_file_contents) + usermod_object_count = count_usermod_objects(map_file_contents) secho(f"INFO: {usermod_object_count} usermod object entries") confirmed_modules = check_map_file_objects(map_file_contents, modules.keys()) From dcd3e072739c47f1a4201b2df9f95209b4ee800d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Mon, 26 May 2025 18:00:45 +0200 Subject: [PATCH 102/153] Securing OTA update - prevent settings change if not using private IP address or same subnet - prevent OTA from differnet subnet if PIN is not set - ability to revert firmware --- wled00/cfg.cpp | 2 ++ wled00/data/settings_sec.htm | 3 +++ wled00/data/update.htm | 19 ++++++++++++--- wled00/set.cpp | 1 + wled00/wled.h | 1 + wled00/wled_server.cpp | 46 ++++++++++++++++++++++++++++++++---- wled00/xml.cpp | 1 + 7 files changed, 66 insertions(+), 7 deletions(-) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index d3415efd6..bb918f30d 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -631,6 +631,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(aOtaEnabled, ota[F("aota")]); #endif getStringFromJson(otaPass, pwd, 33); //normally not present due to security + CJSON(otaSameSubnet, ota[F("same-subnet")]); } #ifdef WLED_ENABLE_DMX @@ -1127,6 +1128,7 @@ void serializeConfig(JsonObject root) { #ifndef WLED_DISABLE_OTA ota[F("aota")] = aOtaEnabled; #endif + ota[F("same-subnet")] = otaSameSubnet; #ifdef WLED_ENABLE_DMX JsonObject dmx = root.createNestedObject("dmx"); diff --git a/wled00/data/settings_sec.htm b/wled00/data/settings_sec.htm index 2db798cf4..7f4627049 100644 --- a/wled00/data/settings_sec.htm +++ b/wled00/data/settings_sec.htm @@ -57,6 +57,9 @@

Software Update


Enable ArduinoOTA:
+ Only allow update from same network/WiFi:
+ ⚠ If you are using multiple VLANs (i.e. IoT or guest network) either set PIN or disable this option.
+ Disabling this option will make your device less secure.


Backup & Restore

⚠ Restoring presets/configuration will OVERWRITE your current presets/configuration.
diff --git a/wled00/data/update.htm b/wled00/data/update.htm index 96ba821e8..8b39b1cce 100644 --- a/wled00/data/update.htm +++ b/wled00/data/update.htm @@ -3,9 +3,20 @@ WLED Update +