From 27d3420ad7e81fb78e30f3833579b02d37a12e2b Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Wed, 22 Jan 2025 09:31:51 +0000 Subject: [PATCH 01/40] 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 0000000000..58973e956b --- /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 02/40] 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 58973e956b..0b51a39351 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 03/40] 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 0b51a39351..124367224d 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 04/40] 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 124367224d..efd5296dfd 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 05/40] 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 efd5296dfd..ec5f9876a8 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 06/40] 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 ec5f9876a8..21fef24856 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 07/40] 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 21fef24856..9a81d464ca 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 0000000000..611dc0d8bd --- /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 08/40] 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 9a81d464ca..fed79f5744 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 09/40] 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 fed79f5744..7467d9915e 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 10/40] 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 7467d9915e..02a404ba1c 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 e7e0eb0f3203d3979d803338faa3cc5ce8c52a24 Mon Sep 17 00:00:00 2001 From: Brandon502 <105077712+Brandon502@users.noreply.github.com> Date: Thu, 13 Feb 2025 19:01:10 -0500 Subject: [PATCH 11/40] Pinwheel Rework Optimized pinwheel algorithm. Math and memory optimizations by @DedeHai --- wled00/FX_fcn.cpp | 222 +++++++++++++++++++++++++--------------------- 1 file changed, 122 insertions(+), 100 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 420460240d..9c40113d9b 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -680,37 +680,25 @@ unsigned Segment::virtualHeight() const { // Constants for mapping mode "Pinwheel" #ifndef WLED_DISABLE_2D -constexpr int Pinwheel_Steps_Small = 72; // no holes up to 16x16 -constexpr int Pinwheel_Size_Small = 16; // larger than this -> use "Medium" -constexpr int Pinwheel_Steps_Medium = 192; // no holes up to 32x32 -constexpr int Pinwheel_Size_Medium = 32; // larger than this -> use "Big" -constexpr int Pinwheel_Steps_Big = 304; // no holes up to 50x50 -constexpr int Pinwheel_Size_Big = 50; // larger than this -> use "XL" -constexpr int Pinwheel_Steps_XL = 368; -constexpr float Int_to_Rad_Small = (DEG_TO_RAD * 360) / Pinwheel_Steps_Small; // conversion: from 0...72 to Radians -constexpr float Int_to_Rad_Med = (DEG_TO_RAD * 360) / Pinwheel_Steps_Medium; // conversion: from 0...192 to Radians -constexpr float Int_to_Rad_Big = (DEG_TO_RAD * 360) / Pinwheel_Steps_Big; // conversion: from 0...304 to Radians -constexpr float Int_to_Rad_XL = (DEG_TO_RAD * 360) / Pinwheel_Steps_XL; // conversion: from 0...368 to Radians - -constexpr int Fixed_Scale = 512; // fixpoint scaling factor (9bit for fraction) - -// Pinwheel helper function: pixel index to radians -static float getPinwheelAngle(int i, int vW, int vH) { - int maxXY = max(vW, vH); - if (maxXY <= Pinwheel_Size_Small) return float(i) * Int_to_Rad_Small; - if (maxXY <= Pinwheel_Size_Medium) return float(i) * Int_to_Rad_Med; - if (maxXY <= Pinwheel_Size_Big) return float(i) * Int_to_Rad_Big; - // else - return float(i) * Int_to_Rad_XL; -} +constexpr int Fixed_Scale = 16384; // fixpoint scaling factor (14bit for fraction) // Pinwheel helper function: matrix dimensions to number of rays static int getPinwheelLength(int vW, int vH) { - int maxXY = max(vW, vH); - if (maxXY <= Pinwheel_Size_Small) return Pinwheel_Steps_Small; - if (maxXY <= Pinwheel_Size_Medium) return Pinwheel_Steps_Medium; - if (maxXY <= Pinwheel_Size_Big) return Pinwheel_Steps_Big; - // else - return Pinwheel_Steps_XL; + // Returns multiple of 8, prevents over drawing + return (max(vW, vH) + 15) & ~7; +} +static void setPinwheelParameters(int i, int vW, int vH, int& startx, int& starty, int* cosVal, int* sinVal, bool getPixel = false) { + int steps = getPinwheelLength(vW, vH); + int baseAngle = ((0xFFFF + steps / 2) / steps); // 360° / steps, in 16 bit scale round to nearest integer + int rotate = 0; + if (getPixel) rotate = baseAngle / 2; // rotate by half a ray width when reading pixel color + for (int k = 0; k < 2; k++) // angular steps for two consecutive rays + { + int angle = (i + k) * baseAngle + rotate; + cosVal[k] = (cos16(angle) * Fixed_Scale) >> 15; // step per pixel in fixed point, cos16 output is -0x7FFF to +0x7FFF + sinVal[k] = (sin16(angle) * Fixed_Scale) >> 15; // using explicit bit shifts as dividing negative numbers is not equivalent (rounding error is acceptable) + } + startx = (vW * Fixed_Scale) / 2; // + cosVal[0] / 4; // starting position = center + 1/4 pixel (in fixed point) + starty = (vH * Fixed_Scale) / 2; // + sinVal[0] / 4; } #endif @@ -845,55 +833,103 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) const for (int x = 0; x <= i; x++) setPixelColorXY(x, i, col); for (int y = 0; y < i; y++) setPixelColorXY(i, y, col); break; - case M12_sPinwheel: { - // i = angle --> 0 - 296 (Big), 0 - 192 (Medium), 0 - 72 (Small) - float centerX = roundf((vW-1) / 2.0f); - float centerY = roundf((vH-1) / 2.0f); - float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians - float cosVal = cos_t(angleRad); - float sinVal = sin_t(angleRad); - - // avoid re-painting the same pixel - int lastX = INT_MIN; // impossible position - int lastY = INT_MIN; // impossible position - // draw line at angle, starting at center and ending at the segment edge - // we use fixed point math for better speed. Starting distance is 0.5 for better rounding - // int_fast16_t and int_fast32_t types changed to int, minimum bits commented - int posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point 18 bit - int posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point 18 bit - int inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) 10 bit - int inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) 10 bit - - int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint - int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint - - // Odd rays start further from center if prevRay started at center. - static int prevRay = INT_MIN; // previous ray number - if ((i % 2 == 1) && (i - 1 == prevRay || i + 1 == prevRay)) { - int jump = min(vW/3, vH/3); // can add 2 if using medium pinwheel - posx += inc_x * jump; - posy += inc_y * jump; - } - prevRay = i; - - // draw ray until we hit any edge - while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) { - // scale down to integer (compiler will replace division with appropriate bitshift) - int x = posx / Fixed_Scale; - int y = posy / Fixed_Scale; - // set pixel - if (x != lastX || y != lastY) setPixelColorXY(x, y, col); // only paint if pixel position is different - lastX = x; - lastY = y; - // advance to next position - posx += inc_x; - posy += inc_y; + 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; } + } + } + + // 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; } - break; } - } - _colorScaled = false; - 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) @@ -1025,31 +1061,17 @@ uint32_t IRAM_ATTR_YN Segment::getPixelColor(int i) const break; case M12_sPinwheel: // not 100% accurate, returns pixel at outer edge - // i = angle --> 0 - 296 (Big), 0 - 192 (Medium), 0 - 72 (Small) - float centerX = roundf((vW-1) / 2.0f); - float centerY = roundf((vH-1) / 2.0f); - float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians - float cosVal = cos_t(angleRad); - float sinVal = sin_t(angleRad); - - int posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point 18 bit - int posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point 18 bit - int inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) 10 bit - int inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) 10 bit - int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint - int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint - - // trace ray from center until we hit any edge - to avoid rounding problems, we use the same method as in setPixelColor - int x = INT_MIN; - int y = INT_MIN; - while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) { - // scale down to integer (compiler will replace division with appropriate bitshift) - x = posx / Fixed_Scale; - y = posy / Fixed_Scale; - // advance to next position - posx += inc_x; - posy += inc_y; + int x, y, cosVal[2], sinVal[2]; + setPinwheelParameters(i, vW, vH, x, y, cosVal, sinVal, true); + int maxX = (vW-1) * Fixed_Scale; + int maxY = (vH-1) * Fixed_Scale; + // trace ray from center until we hit any edge - to avoid rounding problems, we use fixed point coordinates + while ((x < maxX) && (y < maxY) && (x > Fixed_Scale) && (y > Fixed_Scale)) { + x += cosVal[0]; // advance to next position + y += sinVal[0]; } + x /= Fixed_Scale; + y /= Fixed_Scale; return getPixelColorXY(x, y); break; } From 80061e8d76b1681632e999afe66876319eb8909f Mon Sep 17 00:00:00 2001 From: Brandon502 <105077712+Brandon502@users.noreply.github.com> Date: Mon, 24 Feb 2025 21:51:49 -0500 Subject: [PATCH 12/40] Pinwheel: Use sin/cos16_t --- wled00/FX_fcn.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 9c40113d9b..58fcff5db7 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -694,8 +694,8 @@ static void setPinwheelParameters(int i, int vW, int vH, int& startx, int& start for (int k = 0; k < 2; k++) // angular steps for two consecutive rays { int angle = (i + k) * baseAngle + rotate; - cosVal[k] = (cos16(angle) * Fixed_Scale) >> 15; // step per pixel in fixed point, cos16 output is -0x7FFF to +0x7FFF - sinVal[k] = (sin16(angle) * Fixed_Scale) >> 15; // using explicit bit shifts as dividing negative numbers is not equivalent (rounding error is acceptable) + cosVal[k] = (cos16_t(angle) * Fixed_Scale) >> 15; // step per pixel in fixed point, cos16 output is -0x7FFF to +0x7FFF + sinVal[k] = (sin16_t(angle) * Fixed_Scale) >> 15; // using explicit bit shifts as dividing negative numbers is not equivalent (rounding error is acceptable) } startx = (vW * Fixed_Scale) / 2; // + cosVal[0] / 4; // starting position = center + 1/4 pixel (in fixed point) starty = (vH * Fixed_Scale) / 2; // + sinVal[0] / 4; From 2012317bc9bc43184da9e1a81897410df701ec00 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 3 Mar 2025 06:57:16 +0100 Subject: [PATCH 13/40] 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 e6b620098a..d4b4b2d597 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 e5132da57b..a17d486037 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 6584d524e9..4d5557fdaf 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 16af85e712..d5341a96e3 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 bafdf58a93..e242d68f5e 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 14/40] 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 | 115 ++++++++++++++++++++---------------------------- wled00/wled.cpp | 56 ++++++++++++++++------- 2 files changed, 86 insertions(+), 85 deletions(-) diff --git a/wled00/util.cpp b/wled00/util.cpp index d5341a96e3..e7dbbca236 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); - - int32_t gradx = cornergradient(hashx); - int32_t grady = cornergradient(hashy); - int32_t gradz = cornergradient(hashz); - return (gradx * dx + grady * dy + gradz * dz) >> 4; + // 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 = (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 e242d68f5e..de36908ca2 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 15/40] 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 e7dbbca236..c04915031d 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 de36908ca2..639b7fe0c4 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 16/40] updated scaling, improved hashing, updated rotozoomer to not use a buffer --- wled00/FX.cpp | 23 ++++------ wled00/fcn_declare.h | 6 +-- wled00/util.cpp | 106 ++++++++++++++++++++++++------------------- wled00/wled.cpp | 60 ++++++++++++++++-------- 4 files changed, 112 insertions(+), 83 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a17d486037..1bba265dc9 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 4d5557fdaf..7ee87eaeeb 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 c04915031d..33973303be 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; + +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) } -uint8_t perlin8(uint16_t x, uint16_t y) { - return uint8_t((perlin2D_raw(uint32_t(x)<<8, uint32_t(y)<<8) >> 8) + 0x7F); +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) } -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, 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) } -uint16_t perlin16(uint32_t x) { - return perlin1D_raw(x) + 0x7FFF; +uint8_t perlin8(uint16_t x) { + return (((perlin1D_raw((uint32_t)x << 8, true) * 1168) >> 10) + 0x7FFF) >> 8; } -uint16_t perlin16(uint32_t x, uint32_t y) { - return perlin2D_raw(x, y) + 0x7FFF; +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; } -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 +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 639b7fe0c4..ea615f3cbf 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(); - - 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 - } + 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) { @@ -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 17/40] 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 7ee87eaeeb..9bc323d7f0 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 33973303be..07c0db121e 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 ea615f3cbf..1d42e8c119 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 18/40] 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 07c0db121e..ac8a162073 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 19/40] 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 1bba265dc9..756750f273 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 4f7603f50b..bb7114ed5f 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 a70bfa0c8974e8446e78a6df504cbf1cc19d80e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Sat, 15 Mar 2025 14:37:46 +0100 Subject: [PATCH 20/40] Merge pull request #4596 from Dschogo/patch-1 Fix wipe effect smoothness --- wled00/FX.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index e5132da57b..4d17b81f51 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -229,8 +229,7 @@ uint16_t color_wipe(bool rev, bool useRandomColors) { } unsigned ledIndex = (prog * SEGLEN) >> 15; - unsigned rem = 0; - rem = (prog * SEGLEN) * 2; //mod 0xFFFF + uint16_t rem = (prog * SEGLEN) * 2; //mod 0xFFFF by truncating rem /= (SEGMENT.intensity +1); if (rem > 255) rem = 255; From 630315180d68b10cdf6a742fa02e5bb551712e53 Mon Sep 17 00:00:00 2001 From: marcone <48169102+marcone@users.noreply.github.com> Date: Sat, 22 Mar 2025 10:28:38 -0700 Subject: [PATCH 21/40] Fix typo in build.yml --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3de08ff7b5..6f2c373add 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: build: - name: Build Enviornments + name: Build Environments runs-on: ubuntu-latest needs: get_default_envs strategy: From f3287137108cf7b5e439d1b5f29ca057d9caef30 Mon Sep 17 00:00:00 2001 From: marcone <48169102+marcone@users.noreply.github.com> Date: Sun, 23 Mar 2025 11:11:34 -0700 Subject: [PATCH 22/40] Fix typo --- platformio_override.sample.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini index f3b4e1c654..ee2b177143 100644 --- a/platformio_override.sample.ini +++ b/platformio_override.sample.ini @@ -34,7 +34,7 @@ lib_deps = ${esp8266.lib_deps} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} ; -; *** To use the below defines/overrides, copy and paste each onto it's own line just below build_flags in the section above. +; *** To use the below defines/overrides, copy and paste each onto its own line just below build_flags in the section above. ; ; Set a release name that may be used to distinguish required binary for flashing ; -D WLED_RELEASE_NAME=\"ESP32_MULTI_USREMODS\" From a0d1a8cbc4690ac730e75b1f74494742bdf43a4a Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sat, 22 Mar 2025 11:53:05 -0400 Subject: [PATCH 23/40] 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 24988be153..0a307594a1 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 24/40] 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 11862f83f5..194ef4fbf6 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 497a775ee0..0f4666b306 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 197148b2bf..0bc7a6698f 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 34caeefa3f..b2cf264402 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 25/40] 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 194ef4fbf6..fa0397fc65 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 3197375f13..59467590c4 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 54f052637b..b749289bd8 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 00333788d4..c817f2553c 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 b2cf264402..9683b432d7 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 ea40c5dfe2..8926967ff9 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 77f4133c0e..06750838f3 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 26/40] 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 0a307594a1..c09b543f1c 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 27/40] 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 c2f0ffbf2e..b1505cac8b 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 28/40] Update USERMOD BME68X to version 1.0.2 --- usermods/BME68X_v2/BME68X_v2.cpp | 2185 ++++++++--------- usermods/BME68X_v2/README.md | 102 +- .../{library.json.disabled => library.json} | 3 +- 3 files changed, 1157 insertions(+), 1133 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 081d03ff9d..dace35ab61 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 7e7a151136..ee2670aa90 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. -## Settings -The settings of the usermods are set in the usermod section of wled.

The possible settings are - **Enable:** Enables / disables the usermod - **I2C address:** I2C address of the sensor. You can choose between 0X77 & 0X76. The default is 0x77. -- **Interval:** Specifies the interval of seconds at which the usermod should be executed. The default is every second. -- **Pub Chages Only:** If this item is active, the values are only published if they have changed since the last publication. -- **Pub Accuracy:** The Accuracy values associated with the gas values are also published. -- **Pub Calib State:** If this item is active, STABILIZATION_STATUS& RUN_IN_STATUS are also published. +- **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 6bd0bb9b2d..0bd4e71b27 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 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 29/40] 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 df054f4174..ae29065ead 100644 --- a/wled00/data/settings_time.htm +++ b/wled00/data/settings_time.htm @@ -168,8 +168,8 @@

Time setup

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 ea40c5dfe2..01ab5a2c0d 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 02f14baad44d52f222c27944c372efbc41a404fc Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 15 Apr 2025 19:07:21 +0200 Subject: [PATCH 30/40] 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 832c2e404d..f439e4c664 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 1f8a315a6e..6481ff7572 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 fde07be766..cff5342565 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 a91ebe25e4..099b96a859 100644 --- a/wled00/FXparticleSystem.h +++ b/wled00/FXparticleSystem.h @@ -211,7 +211,7 @@ class ParticleSystem2D { 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 @@ class ParticleSystem1D 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 @@ class ParticleSystem1D 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 353868414a2c8336d9c06259517d7f9b636323fa Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 20 Apr 2025 11:38:32 +0200 Subject: [PATCH 31/40] 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 | 851 ++++++++---------------------------- wled00/FXparticleSystem.h | 52 +-- 4 files changed, 222 insertions(+), 771 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index f439e4c664..3daf2c0fdf 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 db8d5a308f..42403fa852 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 cff5342565..fadc987633 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 099b96a859..695a3a0280 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 @@ class ParticleSystem2D { 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 @@ class ParticleSystem2D { 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 @@ class ParticleSystem2D { 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 @@ class ParticleSystem2D { 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 @@ class ParticleSystem2D { 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 @@ class ParticleSystem1D 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 @@ class ParticleSystem1D 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 @@ class ParticleSystem1D 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 10b925acb7ec0a440491940bb3cde537da50c1d5 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 23 Apr 2025 15:06:31 +0200 Subject: [PATCH 32/40] 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 c817f2553c..725875023e 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 19868d01d9..de2f5590df 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 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 33/40] 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 1737408aa9..4d767b0574 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 34/40] 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 02a404ba1c..06ce611bfb 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 611dc0d8bd..c738077e92 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 35/40] 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 06ce611bfb..4de88449aa 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 36/40] 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 c738077e92..5a0d8c3fbc 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 37/40] 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 4de88449aa..9b1dd2ab9a 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 38/40] 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 9b1dd2ab9a..a597d2bf56 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 39/40] 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 5a0d8c3fbc..a2324ba4fa 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 40/40] 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 a2324ba4fa..127b5d33aa 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