From f2f25f2a7dee64a1eb5609d6c61b0a335a812cb3 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Mon, 12 Jan 2026 18:10:14 -0700 Subject: [PATCH 01/14] Added the Spinning Wheel effect into the user_fx usermod --- usermods/user_fx/user_fx.cpp | 190 +++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index da6937c87d..9f0963b97d 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -2,12 +2,18 @@ // for information how FX metadata strings work see https://kno.wled.ge/interfaces/json-api/#effect-metadata +// paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) +#define PALETTE_SOLID_WRAP (strip.paletteBlend == 1 || strip.paletteBlend == 3) + +#define indexToVStrip(index, stripNr) ((index) | (int((stripNr)+1)<<16)) + // static effect, used if an effect fails to initialize static uint16_t mode_static(void) { SEGMENT.fill(SEGCOLOR(0)); return strip.isOffRefreshRequired() ? FRAMETIME : 350; } + ///////////////////////// // User FX functions // ///////////////////////// @@ -89,6 +95,189 @@ unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vW static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spark rate,Diffusion Speed,Turbulence,,Use palette;;Color;;2;pal=35"; +/* + * Spinning Wheel effect - LED animates around 1D strip (or each column in a 2D matrix), slows down and stops at random position + * Created by Bob Loeffler and claude.ai + * First slider (Spin speed) is for the speed of the moving/spinning LED (random number within a narrow speed range) + * Second slider (Spin time) is for how long before the slowdown phase starts (random number within a narrow time range) + * Third slider (Spin delay) is for how long it takes for the LED to start spinning again after the previous spin + * The first checkbox sets the color mode (color wheel or palette) + * The second checkbox sets the spin speed to a random number (within the full speed range) + * The third checkbox sets the spin time to a random number (within the full time range) + * aux0 stores the settings checksum to detect changes + * aux1 stores the color scale for performance + */ + +uint16_t mode_spinning_wheel(void) { + if (SEGLEN < 1) return mode_static(); + + unsigned strips = SEGMENT.nrOfVStrips(); + const unsigned stateVarsPerStrip = 8; + unsigned dataSize = sizeof(uint32_t) * stateVarsPerStrip; + if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); + uint32_t* state = reinterpret_cast(SEGENV.data); + // state[0] = current position (fixed point: upper 16 bits = position, lower 16 bits = fraction) + // state[1] = velocity (fixed point: pixels per frame * 65536) + // state[2] = phase (0=fast spin, 1=slowing, 2=wobble, 3=stopped) + // state[3] = stop time (when phase 3 was entered) + // state[4] = wobble step (0=at stop pos, 1=moved back, 2=returned to stop) + // state[5] = slowdown start time (when to transition from phase 0 to phase 1) + // state[6] = wobble timing (for 200ms / 400ms / 300ms delays) + // state[7] = store the stop position per strip + + // state[] index values for easier readability + constexpr unsigned CUR_POS_IDX = 0; + constexpr unsigned VELOCITY_IDX = 1; + constexpr unsigned PHASE_IDX = 2; + constexpr unsigned STOP_TIME_IDX = 3; + constexpr unsigned WOBBLE_STEP_IDX = 4; + constexpr unsigned SLOWDOWN_TIME_IDX = 5; + constexpr unsigned WOBBLE_TIME_IDX = 6; + constexpr unsigned STOP_POS_IDX = 7; + + SEGMENT.fill(SEGCOLOR(1)); + + // Handle random seeding globally (outside the virtual strip) + if (SEGENV.call == 0) { + random16_set_seed(analogRead(0)); + SEGENV.aux1 = (255 << 16) / SEGLEN; // Cache the color scaling + } + + // Check if settings changed (do this once, not per virtual strip) + uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom3 + SEGMENT.check2 + SEGMENT.check3; + bool settingsChanged = (SEGENV.aux0 != settingssum); + if (settingsChanged) { + random16_add_entropy(analogRead(0)); + SEGENV.aux0 = settingssum; + } + + struct virtualStrip { + + static void runStrip(uint16_t stripNr, uint32_t* state, bool settingsChanged) { + + uint8_t phase = state[PHASE_IDX]; + uint32_t now = strip.now; + + // Check for restart conditions + bool needsReset = false; + if (SEGENV.call == 0) { + needsReset = true; + } else if (settingsChanged) { + needsReset = true; + } else if (phase == 3 && state[STOP_TIME_IDX] != 0) { + uint16_t spin_delay = map(SEGMENT.custom3, 0, 31, 2000, 15000); // delay between spins + if (now >= state[STOP_TIME_IDX] + spin_delay) + needsReset = true; + } + + // Initialize or restart + if (needsReset) { + state[CUR_POS_IDX] = 0; + + // Set velocity + if (SEGMENT.check2) { // random speed + state[VELOCITY_IDX] = random16(200, 900) * 655; + } else { + uint16_t speed = map(SEGMENT.speed, 0, 255, 300, 800); + state[VELOCITY_IDX] = random16(speed - 100, speed + 100) * 655; + } + + // Set slowdown start time + if (SEGMENT.check3) { // random slowdown + state[SLOWDOWN_TIME_IDX] = now + random16(2000, 6000); + } else { + uint16_t slowdown = map(SEGMENT.intensity, 0, 255, 3000, 5000); + state[SLOWDOWN_TIME_IDX] = now + random16(slowdown - 1000, slowdown + 1000); + } + + state[PHASE_IDX] = 0; + state[STOP_TIME_IDX] = 0; + state[WOBBLE_STEP_IDX] = 0; + state[WOBBLE_TIME_IDX] = 0; + state[STOP_POS_IDX] = 0; // Initialize stop position + phase = 0; + } + + uint32_t pos_fixed = state[CUR_POS_IDX]; + uint32_t velocity = state[VELOCITY_IDX]; + + // Phase management + if (phase == 0) { + // Fast spinning phase + if (now >= state[SLOWDOWN_TIME_IDX]) { + phase = 1; + state[PHASE_IDX] = 1; + } + } else if (phase == 1) { + // Slowing phase - apply deceleration + uint32_t decel = velocity / 80; + if (decel < 100) decel = 100; + + velocity = (velocity > decel) ? velocity - decel : 0; + state[VELOCITY_IDX] = velocity; + + // Check if stopped + if (velocity < 2000) { + velocity = 0; + state[VELOCITY_IDX] = 0; + phase = 2; + state[PHASE_IDX] = 2; + state[WOBBLE_STEP_IDX] = 0; + uint16_t stop_pos = (pos_fixed >> 16) % SEGLEN; + state[STOP_POS_IDX] = stop_pos; + state[WOBBLE_TIME_IDX] = now; + } + } else if (phase == 2) { + // Wobble phase (moves the LED back one and then forward one) + uint32_t wobble_step = state[WOBBLE_STEP_IDX]; + uint16_t stop_pos = state[STOP_POS_IDX]; + uint32_t elapsed = now - state[WOBBLE_TIME_IDX]; + + if (wobble_step == 0 && elapsed >= 200) { + // Move back one LED from stop position + uint16_t back_pos = (stop_pos == 0) ? SEGLEN - 1 : stop_pos - 1; + pos_fixed = ((uint32_t)back_pos) << 16; + state[CUR_POS_IDX] = pos_fixed; + state[WOBBLE_STEP_IDX] = 1; + state[WOBBLE_TIME_IDX] = now; + } else if (wobble_step == 1 && elapsed >= 400) { + // Move forward to the stop position + pos_fixed = ((uint32_t)stop_pos) << 16; + state[CUR_POS_IDX] = pos_fixed; + state[WOBBLE_STEP_IDX] = 2; + state[WOBBLE_TIME_IDX] = now; + } else if (wobble_step == 2 && elapsed >= 300) { + // Wobble complete, enter stopped phase + phase = 3; + state[PHASE_IDX] = 3; + state[STOP_TIME_IDX] = now; + } + } + + // Update position (phases 0 and 1 only) + if (phase == 0 || phase == 1) { + pos_fixed += velocity; + state[CUR_POS_IDX] = pos_fixed; + } + + // Draw LED for all phases using indexToVStrip + uint16_t pos = (pos_fixed >> 16) % SEGLEN; + uint8_t hue = (SEGENV.aux1 * pos) >> 16; // Use cached color scaling + uint32_t color = SEGMENT.check1 ? SEGMENT.color_wheel(hue) : SEGMENT.color_from_palette(hue, true, PALETTE_SOLID_WRAP, 0); + SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color); + } + }; + + for (unsigned stripNr=0; stripNr Date: Mon, 12 Jan 2026 20:24:52 -0700 Subject: [PATCH 02/14] Fixed a few issues that coderabbit mentioned --- usermods/user_fx/user_fx.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 9f0963b97d..047a7026ec 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -108,7 +108,7 @@ static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spar * aux1 stores the color scale for performance */ -uint16_t mode_spinning_wheel(void) { +static uint16_t mode_spinning_wheel(void) { if (SEGLEN < 1) return mode_static(); unsigned strips = SEGMENT.nrOfVStrips(); @@ -166,7 +166,7 @@ uint16_t mode_spinning_wheel(void) { needsReset = true; } else if (phase == 3 && state[STOP_TIME_IDX] != 0) { uint16_t spin_delay = map(SEGMENT.custom3, 0, 31, 2000, 15000); // delay between spins - if (now >= state[STOP_TIME_IDX] + spin_delay) + if ((now - state[STOP_TIME_IDX]) >= spin_delay) needsReset = true; } @@ -204,7 +204,7 @@ uint16_t mode_spinning_wheel(void) { // Phase management if (phase == 0) { // Fast spinning phase - if (now >= state[SLOWDOWN_TIME_IDX]) { + if ((int32_t)(now - state[SLOWDOWN_TIME_IDX]) >= 0) { phase = 1; state[PHASE_IDX] = 1; } From 6376750dddac73795317f709da3c7c288b19bdda Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Mon, 12 Jan 2026 22:18:01 -0700 Subject: [PATCH 03/14] Fixed integer overflow when storing color scale in aux1. And added a comment about the velocity scaling. --- usermods/user_fx/user_fx.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 047a7026ec..51a11f4c53 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -140,7 +140,7 @@ static uint16_t mode_spinning_wheel(void) { // Handle random seeding globally (outside the virtual strip) if (SEGENV.call == 0) { random16_set_seed(analogRead(0)); - SEGENV.aux1 = (255 << 16) / SEGLEN; // Cache the color scaling + SEGENV.aux1 = (255 << 8) / SEGLEN; // Cache the color scaling } // Check if settings changed (do this once, not per virtual strip) @@ -176,7 +176,7 @@ static uint16_t mode_spinning_wheel(void) { // Set velocity if (SEGMENT.check2) { // random speed - state[VELOCITY_IDX] = random16(200, 900) * 655; + state[VELOCITY_IDX] = random16(200, 900) * 655; // fixed-point velocity scaling (approx. 65536/100) } else { uint16_t speed = map(SEGMENT.speed, 0, 255, 300, 800); state[VELOCITY_IDX] = random16(speed - 100, speed + 100) * 655; @@ -262,7 +262,7 @@ static uint16_t mode_spinning_wheel(void) { // Draw LED for all phases using indexToVStrip uint16_t pos = (pos_fixed >> 16) % SEGLEN; - uint8_t hue = (SEGENV.aux1 * pos) >> 16; // Use cached color scaling + uint8_t hue = (SEGENV.aux1 * pos) >> 8; // Use cached color scaling uint32_t color = SEGMENT.check1 ? SEGMENT.color_wheel(hue) : SEGMENT.color_from_palette(hue, true, PALETTE_SOLID_WRAP, 0); SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color); } From a707b397da5ebc3ca46aa3d69835676cf1607836 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Tue, 20 Jan 2026 17:44:55 -0700 Subject: [PATCH 04/14] Additions/changes: * Added Color Per Block checkbox. Enabled will set the spinner LEDs to the same color (instead of changing colors depending on the palette and LED position). * Added Sync Restart checkbox. Enabled means that all spinners will restart together (instead of individually) after they have all stopped spinning. * Added resizing the spinner slider (between 1 and 10 LEDs). * Changed how we do random speed and slowdown start time (sliders set to 0 = random). * tweaks here and there --- usermods/user_fx/user_fx.cpp | 105 ++++++++++++++++++++++++++++------- 1 file changed, 84 insertions(+), 21 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 51a11f4c53..71f7c165a6 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -98,12 +98,15 @@ static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spar /* * Spinning Wheel effect - LED animates around 1D strip (or each column in a 2D matrix), slows down and stops at random position * Created by Bob Loeffler and claude.ai - * First slider (Spin speed) is for the speed of the moving/spinning LED (random number within a narrow speed range) - * Second slider (Spin time) is for how long before the slowdown phase starts (random number within a narrow time range) - * Third slider (Spin delay) is for how long it takes for the LED to start spinning again after the previous spin - * The first checkbox sets the color mode (color wheel or palette) - * The second checkbox sets the spin speed to a random number (within the full speed range) - * The third checkbox sets the spin time to a random number (within the full time range) + * First slider (Spin speed) is for the speed of the moving/spinning LED (random number within a narrow speed range). + * If value is 0, a random speed will be selected from the full range of values. + * Second slider (Spin slowdown start time) is for how long before the slowdown phase starts (random number within a narrow time range). + * If value is 0, a random time will be selected from the full range of values. + * Third slider (Spinner size) is for the number of pixels that make up the spinner. + * Fourth slider (Spin delay) is for how long it takes for the LED to start spinning again after the previous spin. + * The first checkbox sets the color mode (color wheel or palette). + * The second checkbox sets "color per block" mode. Enabled means that each spinner block will be the same color no matter what its LED position is. + * The third checkbox enables synchronized restart (all spinners restart together instead of individually). * aux0 stores the settings checksum to detect changes * aux1 stores the color scale for performance */ @@ -126,7 +129,7 @@ static uint16_t mode_spinning_wheel(void) { // state[7] = store the stop position per strip // state[] index values for easier readability - constexpr unsigned CUR_POS_IDX = 0; + constexpr unsigned CUR_POS_IDX = 0; // state[0] constexpr unsigned VELOCITY_IDX = 1; constexpr unsigned PHASE_IDX = 2; constexpr unsigned STOP_TIME_IDX = 3; @@ -144,16 +147,38 @@ static uint16_t mode_spinning_wheel(void) { } // Check if settings changed (do this once, not per virtual strip) - uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom3 + SEGMENT.check2 + SEGMENT.check3; + uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom3 + SEGMENT.check3; bool settingsChanged = (SEGENV.aux0 != settingssum); if (settingsChanged) { random16_add_entropy(analogRead(0)); SEGENV.aux0 = settingssum; } + // Check if all spinners are stopped and ready to restart (for synchronized restart) + bool allReadyToRestart = true; + if (SEGMENT.check3) { + uint8_t spinnerSize = map(SEGMENT.custom1, 0, 255, 1, 10); + uint16_t spin_delay = map(SEGMENT.custom3, 0, 31, 2000, 15000); + uint32_t now = strip.now; + + for (unsigned stripNr = 0; stripNr < strips; stripNr += spinnerSize) { + uint32_t* stripState = &state[stripNr * stateVarsPerStrip]; + // Check if this spinner is stopped AND has waited its delay + if (stripState[PHASE_IDX] != 3 || stripState[STOP_TIME_IDX] == 0) { + allReadyToRestart = false; + break; + } + // Check if delay has elapsed + if ((now - stripState[STOP_TIME_IDX]) < spin_delay) { + allReadyToRestart = false; + break; + } + } + } + struct virtualStrip { - static void runStrip(uint16_t stripNr, uint32_t* state, bool settingsChanged) { + static void runStrip(uint16_t stripNr, uint32_t* state, bool settingsChanged, bool allReadyToRestart) { uint8_t phase = state[PHASE_IDX]; uint32_t now = strip.now; @@ -165,9 +190,18 @@ static uint16_t mode_spinning_wheel(void) { } else if (settingsChanged) { needsReset = true; } else if (phase == 3 && state[STOP_TIME_IDX] != 0) { - uint16_t spin_delay = map(SEGMENT.custom3, 0, 31, 2000, 15000); // delay between spins - if ((now - state[STOP_TIME_IDX]) >= spin_delay) - needsReset = true; + // If synchronized restart is enabled, only restart when all strips are ready + if (SEGMENT.check3) { + if (allReadyToRestart) { + needsReset = true; + } + } else { + // Normal mode: restart after individual strip delay + uint16_t spin_delay = map(SEGMENT.custom3, 0, 31, 2000, 15000); + if ((now - state[STOP_TIME_IDX]) >= spin_delay) { + needsReset = true; + } + } } // Initialize or restart @@ -175,18 +209,18 @@ static uint16_t mode_spinning_wheel(void) { state[CUR_POS_IDX] = 0; // Set velocity - if (SEGMENT.check2) { // random speed + uint16_t speed = map(SEGMENT.speed, 0, 255, 300, 800); + if (speed == 300) { // random speed (user selected 0 on speed slider) state[VELOCITY_IDX] = random16(200, 900) * 655; // fixed-point velocity scaling (approx. 65536/100) } else { - uint16_t speed = map(SEGMENT.speed, 0, 255, 300, 800); state[VELOCITY_IDX] = random16(speed - 100, speed + 100) * 655; } // Set slowdown start time - if (SEGMENT.check3) { // random slowdown + uint16_t slowdown = map(SEGMENT.intensity, 0, 255, 3000, 5000); + if (slowdown == 3000) { // random slowdown start time (user selected 0 on intensity slider) state[SLOWDOWN_TIME_IDX] = now + random16(2000, 6000); } else { - uint16_t slowdown = map(SEGMENT.intensity, 0, 255, 3000, 5000); state[SLOWDOWN_TIME_IDX] = now + random16(slowdown - 1000, slowdown + 1000); } @@ -260,21 +294,50 @@ static uint16_t mode_spinning_wheel(void) { state[CUR_POS_IDX] = pos_fixed; } - // Draw LED for all phases using indexToVStrip + // Draw LED for all phases uint16_t pos = (pos_fixed >> 16) % SEGLEN; - uint8_t hue = (SEGENV.aux1 * pos) >> 8; // Use cached color scaling + + uint8_t spinnerSize = map(SEGMENT.custom1, 0, 255, 1, 10); + + // Calculate color once per spinner block (based on strip number, not position) + uint8_t hue; + if (SEGMENT.check2) { + // Each spinner block gets its own color based on strip number + uint16_t numSpinners = max(1, (int)(SEGMENT.nrOfVStrips() / spinnerSize)); + hue = (255 * (stripNr / spinnerSize)) / numSpinners; + } else { + // Color changes with position + hue = (SEGENV.aux1 * pos) >> 8; + } + uint32_t color = SEGMENT.check1 ? SEGMENT.color_wheel(hue) : SEGMENT.color_from_palette(hue, true, PALETTE_SOLID_WRAP, 0); - SEGMENT.setPixelColor(indexToVStrip(pos, stripNr), color); + + // Draw the spinner with configurable size (1-10 LEDs) + for (int8_t x = 0; x < spinnerSize; x++) { + for (uint8_t y = 0; y < spinnerSize; y++) { + uint16_t drawPos = (pos + y) % SEGLEN; + int16_t drawStrip = stripNr + x; + + // Wrap horizontally if needed, or skip if out of bounds + if (drawStrip >= 0 && drawStrip < (int16_t)SEGMENT.nrOfVStrips()) { + SEGMENT.setPixelColor(indexToVStrip(drawPos, drawStrip), color); + } + } + } } }; for (unsigned stripNr=0; stripNr Date: Tue, 20 Jan 2026 22:41:13 -0700 Subject: [PATCH 05/14] One minor fix for the spinner colors --- usermods/user_fx/user_fx.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 71f7c165a6..b91e8764c5 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -303,7 +303,7 @@ static uint16_t mode_spinning_wheel(void) { uint8_t hue; if (SEGMENT.check2) { // Each spinner block gets its own color based on strip number - uint16_t numSpinners = max(1, (int)(SEGMENT.nrOfVStrips() / spinnerSize)); + uint16_t numSpinners = max(1U, (SEGMENT.nrOfVStrips() + spinnerSize - 1) / spinnerSize); hue = (255 * (stripNr / spinnerSize)) / numSpinners; } else { // Color changes with position From ccff1b5b82dc4fea0622193eccac72479d22d212 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Thu, 22 Jan 2026 23:54:54 -0700 Subject: [PATCH 06/14] Changed the two analogRead() to hw_random16() --- usermods/user_fx/user_fx.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index b91e8764c5..5fdc16a4f8 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -115,7 +115,7 @@ static uint16_t mode_spinning_wheel(void) { if (SEGLEN < 1) return mode_static(); unsigned strips = SEGMENT.nrOfVStrips(); - const unsigned stateVarsPerStrip = 8; + constexpr unsigned stateVarsPerStrip = 8; unsigned dataSize = sizeof(uint32_t) * stateVarsPerStrip; if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); uint32_t* state = reinterpret_cast(SEGENV.data); @@ -142,7 +142,7 @@ static uint16_t mode_spinning_wheel(void) { // Handle random seeding globally (outside the virtual strip) if (SEGENV.call == 0) { - random16_set_seed(analogRead(0)); + random16_set_seed(hw_random16()); SEGENV.aux1 = (255 << 8) / SEGLEN; // Cache the color scaling } @@ -150,7 +150,7 @@ static uint16_t mode_spinning_wheel(void) { uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom3 + SEGMENT.check3; bool settingsChanged = (SEGENV.aux0 != settingssum); if (settingsChanged) { - random16_add_entropy(analogRead(0)); + random16_add_entropy(hw_random16()); SEGENV.aux0 = settingssum; } From 0f5469a4341e228067763acd9eb19ba457f1c21b Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Fri, 23 Jan 2026 23:57:48 -0700 Subject: [PATCH 07/14] Changes from SEGLEN to vstripLen suggested by coderabbitai, but it's not working correctly now. Committing and pushing so coderabbitai can check the latest code. --- usermods/user_fx/user_fx.cpp | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 5fdc16a4f8..789c2c46ab 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -3,7 +3,7 @@ // for information how FX metadata strings work see https://kno.wled.ge/interfaces/json-api/#effect-metadata // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) -#define PALETTE_SOLID_WRAP (strip.paletteBlend == 1 || strip.paletteBlend == 3) +#define PALETTE_SOLID_WRAP (paletteBlend == 1 || paletteBlend == 3) #define indexToVStrip(index, stripNr) ((index) | (int((stripNr)+1)<<16)) @@ -115,6 +115,10 @@ static uint16_t mode_spinning_wheel(void) { if (SEGLEN < 1) return mode_static(); unsigned strips = SEGMENT.nrOfVStrips(); + if (strips == 0) return mode_static(); + + const uint16_t vstripLen = SEGLEN / strips; + constexpr unsigned stateVarsPerStrip = 8; unsigned dataSize = sizeof(uint32_t) * stateVarsPerStrip; if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); @@ -143,7 +147,7 @@ static uint16_t mode_spinning_wheel(void) { // Handle random seeding globally (outside the virtual strip) if (SEGENV.call == 0) { random16_set_seed(hw_random16()); - SEGENV.aux1 = (255 << 8) / SEGLEN; // Cache the color scaling + SEGENV.aux1 = (255 << 8) / vstripLen; // Cache the color scaling } // Check if settings changed (do this once, not per virtual strip) @@ -175,10 +179,9 @@ static uint16_t mode_spinning_wheel(void) { } } } - + struct virtualStrip { - - static void runStrip(uint16_t stripNr, uint32_t* state, bool settingsChanged, bool allReadyToRestart) { + static void runStrip(uint16_t stripNr, uint32_t* state, bool settingsChanged, bool allReadyToRestart, uint16_t vstripLen) { uint8_t phase = state[PHASE_IDX]; uint32_t now = strip.now; @@ -257,7 +260,7 @@ static uint16_t mode_spinning_wheel(void) { phase = 2; state[PHASE_IDX] = 2; state[WOBBLE_STEP_IDX] = 0; - uint16_t stop_pos = (pos_fixed >> 16) % SEGLEN; + uint16_t stop_pos = (pos_fixed >> 16) % vstripLen; state[STOP_POS_IDX] = stop_pos; state[WOBBLE_TIME_IDX] = now; } @@ -269,7 +272,7 @@ static uint16_t mode_spinning_wheel(void) { if (wobble_step == 0 && elapsed >= 200) { // Move back one LED from stop position - uint16_t back_pos = (stop_pos == 0) ? SEGLEN - 1 : stop_pos - 1; + uint16_t back_pos = (stop_pos == 0) ? vstripLen - 1 : stop_pos - 1; pos_fixed = ((uint32_t)back_pos) << 16; state[CUR_POS_IDX] = pos_fixed; state[WOBBLE_STEP_IDX] = 1; @@ -295,7 +298,7 @@ static uint16_t mode_spinning_wheel(void) { } // Draw LED for all phases - uint16_t pos = (pos_fixed >> 16) % SEGLEN; + uint16_t pos = (pos_fixed >> 16) % vstripLen; uint8_t spinnerSize = map(SEGMENT.custom1, 0, 255, 1, 10); @@ -315,7 +318,7 @@ static uint16_t mode_spinning_wheel(void) { // Draw the spinner with configurable size (1-10 LEDs) for (int8_t x = 0; x < spinnerSize; x++) { for (uint8_t y = 0; y < spinnerSize; y++) { - uint16_t drawPos = (pos + y) % SEGLEN; + uint16_t drawPos = (pos + y) % vstripLen; int16_t drawStrip = stripNr + x; // Wrap horizontally if needed, or skip if out of bounds @@ -331,7 +334,7 @@ static uint16_t mode_spinning_wheel(void) { // Only run on strips that are multiples of spinnerSize to avoid overlap uint8_t spinnerSize = map(SEGMENT.custom1, 0, 255, 1, 10); if (stripNr % spinnerSize == 0) { - virtualStrip::runStrip(stripNr, &state[stripNr * stateVarsPerStrip], settingsChanged, allReadyToRestart); + virtualStrip::runStrip(stripNr, &state[stripNr * stateVarsPerStrip], settingsChanged, allReadyToRestart, vstripLen); } } From 0833dacfe876a4cc42b56220c594bd13a73a31b0 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sat, 24 Jan 2026 12:04:40 -0700 Subject: [PATCH 08/14] Rolled back changes from vstripLen to SEGLEN as that is what works correctly. Also changed to the global paletteBlend. --- usermods/user_fx/user_fx.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 789c2c46ab..bb5e9bd1b5 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -117,8 +117,6 @@ static uint16_t mode_spinning_wheel(void) { unsigned strips = SEGMENT.nrOfVStrips(); if (strips == 0) return mode_static(); - const uint16_t vstripLen = SEGLEN / strips; - constexpr unsigned stateVarsPerStrip = 8; unsigned dataSize = sizeof(uint32_t) * stateVarsPerStrip; if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); @@ -147,7 +145,7 @@ static uint16_t mode_spinning_wheel(void) { // Handle random seeding globally (outside the virtual strip) if (SEGENV.call == 0) { random16_set_seed(hw_random16()); - SEGENV.aux1 = (255 << 8) / vstripLen; // Cache the color scaling + SEGENV.aux1 = (255 << 8) / SEGLEN; // Cache the color scaling } // Check if settings changed (do this once, not per virtual strip) @@ -181,7 +179,7 @@ static uint16_t mode_spinning_wheel(void) { } struct virtualStrip { - static void runStrip(uint16_t stripNr, uint32_t* state, bool settingsChanged, bool allReadyToRestart, uint16_t vstripLen) { + static void runStrip(uint16_t stripNr, uint32_t* state, bool settingsChanged, bool allReadyToRestart) { uint8_t phase = state[PHASE_IDX]; uint32_t now = strip.now; @@ -260,7 +258,7 @@ static uint16_t mode_spinning_wheel(void) { phase = 2; state[PHASE_IDX] = 2; state[WOBBLE_STEP_IDX] = 0; - uint16_t stop_pos = (pos_fixed >> 16) % vstripLen; + uint16_t stop_pos = (pos_fixed >> 16) % SEGLEN; state[STOP_POS_IDX] = stop_pos; state[WOBBLE_TIME_IDX] = now; } @@ -272,7 +270,7 @@ static uint16_t mode_spinning_wheel(void) { if (wobble_step == 0 && elapsed >= 200) { // Move back one LED from stop position - uint16_t back_pos = (stop_pos == 0) ? vstripLen - 1 : stop_pos - 1; + uint16_t back_pos = (stop_pos == 0) ? SEGLEN - 1 : stop_pos - 1; pos_fixed = ((uint32_t)back_pos) << 16; state[CUR_POS_IDX] = pos_fixed; state[WOBBLE_STEP_IDX] = 1; @@ -298,7 +296,7 @@ static uint16_t mode_spinning_wheel(void) { } // Draw LED for all phases - uint16_t pos = (pos_fixed >> 16) % vstripLen; + uint16_t pos = (pos_fixed >> 16) % SEGLEN; uint8_t spinnerSize = map(SEGMENT.custom1, 0, 255, 1, 10); @@ -318,7 +316,7 @@ static uint16_t mode_spinning_wheel(void) { // Draw the spinner with configurable size (1-10 LEDs) for (int8_t x = 0; x < spinnerSize; x++) { for (uint8_t y = 0; y < spinnerSize; y++) { - uint16_t drawPos = (pos + y) % vstripLen; + uint16_t drawPos = (pos + y) % SEGLEN; int16_t drawStrip = stripNr + x; // Wrap horizontally if needed, or skip if out of bounds @@ -334,7 +332,7 @@ static uint16_t mode_spinning_wheel(void) { // Only run on strips that are multiples of spinnerSize to avoid overlap uint8_t spinnerSize = map(SEGMENT.custom1, 0, 255, 1, 10); if (stripNr % spinnerSize == 0) { - virtualStrip::runStrip(stripNr, &state[stripNr * stateVarsPerStrip], settingsChanged, allReadyToRestart, vstripLen); + virtualStrip::runStrip(stripNr, &state[stripNr * stateVarsPerStrip], settingsChanged, allReadyToRestart); } } From 2cf0be3ed7f43c2191aabad82ba2dbbf0540721b Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Tue, 10 Feb 2026 22:50:55 -0700 Subject: [PATCH 09/14] Removed return FRAMETIME --- usermods/user_fx/user_fx.cpp | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index bb5e9bd1b5..74d927a5da 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -8,20 +8,20 @@ #define indexToVStrip(index, stripNr) ((index) | (int((stripNr)+1)<<16)) // static effect, used if an effect fails to initialize -static uint16_t mode_static(void) { +static void mode_static(void) { SEGMENT.fill(SEGCOLOR(0)); - return strip.isOffRefreshRequired() ? FRAMETIME : 350; } +#define FX_FALLBACK_STATIC { mode_static(); return; } ///////////////////////// // User FX functions // ///////////////////////// // Diffusion Fire: fire effect intended for 2D setups smaller than 16x16 -static uint16_t mode_diffusionfire(void) { +static void mode_diffusionfire(void) { if (!strip.isMatrix || !SEGMENT.is2D()) - return mode_static(); // not a 2D set-up + FX_FALLBACK_STATIC; // not a 2D set-up const int cols = SEG_W; const int rows = SEG_H; @@ -35,7 +35,7 @@ static uint16_t mode_diffusionfire(void) { unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vWidth()*vHeight() for 2D if (!SEGENV.allocateData(dataSize)) - return mode_static(); // allocation failed + FX_FALLBACK_STATIC; // allocation failed if (SEGENV.call == 0) { SEGMENT.fill(BLACK); @@ -90,7 +90,6 @@ unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vW } } } - return FRAMETIME; } static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spark rate,Diffusion Speed,Turbulence,,Use palette;;Color;;2;pal=35"; @@ -111,15 +110,15 @@ static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spar * aux1 stores the color scale for performance */ -static uint16_t mode_spinning_wheel(void) { - if (SEGLEN < 1) return mode_static(); +static void mode_spinning_wheel(void) { + if (SEGLEN < 1) FX_FALLBACK_STATIC; unsigned strips = SEGMENT.nrOfVStrips(); - if (strips == 0) return mode_static(); + if (strips == 0) FX_FALLBACK_STATIC; constexpr unsigned stateVarsPerStrip = 8; unsigned dataSize = sizeof(uint32_t) * stateVarsPerStrip; - if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); + if (!SEGENV.allocateData(dataSize * strips)) FX_FALLBACK_STATIC; uint32_t* state = reinterpret_cast(SEGENV.data); // state[0] = current position (fixed point: upper 16 bits = position, lower 16 bits = fraction) // state[1] = velocity (fixed point: pixels per frame * 65536) @@ -335,8 +334,6 @@ static uint16_t mode_spinning_wheel(void) { virtualStrip::runStrip(stripNr, &state[stripNr * stateVarsPerStrip], settingsChanged, allReadyToRestart); } } - - return FRAMETIME; } static const char _data_FX_MODE_SPINNINGWHEEL[] PROGMEM = "Spinning Wheel@Speed (0=random),Slowdown (0=random),Spinner size,,Spin delay,Color mode,Color per block,Sync restart;!,!;!;;m12=1,c1=1,c3=8"; From b55156a8fa9e6eb1aaf87ac2b65d3bcb262b1bda Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Fri, 6 Mar 2026 20:37:55 -0700 Subject: [PATCH 10/14] Fixed a color issue by using ColorFromPaletteWLED() instead of color_from_palette(). Also removed color_wheel() and the Color Mode option as it's very similar to the new color function. And now using strips variable instead of SEGMENT.nrOfVStrips() after the initial assignment at the top of the code. --- usermods/user_fx/user_fx.cpp | 44 +++++++++++++++++------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 29bb33069c..1020e14c0d 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -99,20 +99,19 @@ static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spar /* -/ Spinning Wheel effect - LED animates around 1D strip (or each column in a 2D matrix), slows down and stops at random position -* Created by Bob Loeffler and claude.ai -* First slider (Spin speed) is for the speed of the moving/spinning LED (random number within a narrow speed range). -* If value is 0, a random speed will be selected from the full range of values. -* Second slider (Spin slowdown start time) is for how long before the slowdown phase starts (random number within a narrow time range). -* If value is 0, a random time will be selected from the full range of values. -* Third slider (Spinner size) is for the number of pixels that make up the spinner. -* Fourth slider (Spin delay) is for how long it takes for the LED to start spinning again after the previous spin. -* The first checkbox sets the color mode (color wheel or palette). -* The second checkbox sets "color per block" mode. Enabled means that each spinner block will be the same color no matter what its LED position is. -* The third checkbox enables synchronized restart (all spinners restart together instead of individually). -* aux0 stores the settings checksum to detect changes -* aux1 stores the color scale for performance -*/ + * Spinning Wheel effect - LED animates around 1D strip (or each column in a 2D matrix), slows down and stops at random position + * Created by Bob Loeffler and claude.ai + * First slider (Spin speed) is for the speed of the moving/spinning LED (random number within a narrow speed range). + * If value is 0, a random speed will be selected from the full range of values. + * Second slider (Spin slowdown start time) is for how long before the slowdown phase starts (random number within a narrow time range). + * If value is 0, a random time will be selected from the full range of values. + * Third slider (Spinner size) is for the number of pixels that make up the spinner. + * Fourth slider (Spin delay) is for how long it takes for the LED to start spinning again after the previous spin. + * The first checkbox sets "color per block" mode. Enabled means that each spinner block will be the same color no matter what its LED position is. + * The second checkbox enables synchronized restart (all spinners restart together instead of individually). + * aux0 stores the settings checksum to detect changes + * aux1 stores the color scale for performance + */ static void mode_spinning_wheel(void) { if (SEGLEN < 1) FX_FALLBACK_STATIC; @@ -182,8 +181,7 @@ static void mode_spinning_wheel(void) { } struct virtualStrip { - static void runStrip(uint16_t stripNr, uint32_t* state, bool settingsChanged, bool allReadyToRestart) { - + static void runStrip(uint16_t stripNr, uint32_t* state, bool settingsChanged, bool allReadyToRestart, unsigned strips) { uint8_t phase = state[PHASE_IDX]; uint32_t now = strip.now; @@ -307,14 +305,14 @@ static void mode_spinning_wheel(void) { uint8_t hue; if (SEGMENT.check2) { // Each spinner block gets its own color based on strip number - uint16_t numSpinners = max(1U, (SEGMENT.nrOfVStrips() + spinnerSize - 1) / spinnerSize); - hue = (255 * (stripNr / spinnerSize)) / numSpinners; + uint16_t numSpinners = max(1U, (strips + spinnerSize - 1) / spinnerSize); + hue = (uint32_t)(255) * (stripNr / spinnerSize) / numSpinners; } else { // Color changes with position hue = (SEGENV.aux1 * pos) >> 8; } - uint32_t color = SEGMENT.check1 ? SEGMENT.color_wheel(hue) : SEGMENT.color_from_palette(hue, true, PALETTE_SOLID_WRAP, 0); + uint32_t color = ColorFromPaletteWLED(SEGPALETTE, hue, 255, LINEARBLEND); // Draw the spinner with configurable size (1-10 LEDs) for (int8_t x = 0; x < spinnerSize; x++) { @@ -323,7 +321,7 @@ static void mode_spinning_wheel(void) { int16_t drawStrip = stripNr + x; // Wrap horizontally if needed, or skip if out of bounds - if (drawStrip >= 0 && drawStrip < (int16_t)SEGMENT.nrOfVStrips()) { + if (drawStrip >= 0 && drawStrip < strips) { SEGMENT.setPixelColor(indexToVStrip(drawPos, drawStrip), color); } } @@ -331,15 +329,15 @@ static void mode_spinning_wheel(void) { } }; - for (unsigned stripNr=0; stripNr Date: Sun, 8 Mar 2026 02:35:52 -0700 Subject: [PATCH 11/14] Added the ability to spin the wheel(s)/spinner(s) with a push button or the new Spin Me checkbox. --- usermods/user_fx/user_fx.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 1020e14c0d..375afafead 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -107,8 +107,10 @@ static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spar * If value is 0, a random time will be selected from the full range of values. * Third slider (Spinner size) is for the number of pixels that make up the spinner. * Fourth slider (Spin delay) is for how long it takes for the LED to start spinning again after the previous spin. - * The first checkbox sets "color per block" mode. Enabled means that each spinner block will be the same color no matter what its LED position is. - * The second checkbox enables synchronized restart (all spinners restart together instead of individually). + * The first checkbox allows the spinner to spin. If it's enabled, the spinner will do its thing. If it's not enabled, it will wait for the user to enable + * it either by clicking the checkbox or by pressing a physical button (e.g. using a playlist to run a couple presets that have JSON API codes). + * The second checkbox sets "color per block" mode. Enabled means that each spinner block will be the same color no matter what its LED position is. + * The third checkbox enables synchronized restart (all spinners restart together instead of individually). * aux0 stores the settings checksum to detect changes * aux1 stores the color scale for performance */ @@ -151,7 +153,7 @@ static void mode_spinning_wheel(void) { } // Check if settings changed (do this once, not per virtual strip) - uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom3 + SEGMENT.check3; + uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom3 + SEGMENT.check1 + SEGMENT.check3; bool settingsChanged = (SEGENV.aux0 != settingssum); if (settingsChanged) { random16_add_entropy(hw_random16()); @@ -189,7 +191,7 @@ static void mode_spinning_wheel(void) { bool needsReset = false; if (SEGENV.call == 0) { needsReset = true; - } else if (settingsChanged) { + } else if (settingsChanged && SEGMENT.check1) { needsReset = true; } else if (phase == 3 && state[STOP_TIME_IDX] != 0) { // If synchronized restart is enabled, only restart when all strips are ready @@ -207,7 +209,7 @@ static void mode_spinning_wheel(void) { } // Initialize or restart - if (needsReset) { + if (needsReset && SEGMENT.check1) { // spin the spinner(s) only if the "Spin me!" checkbox is enabled state[CUR_POS_IDX] = 0; // Set velocity @@ -233,7 +235,7 @@ static void mode_spinning_wheel(void) { state[STOP_POS_IDX] = 0; // Initialize stop position phase = 0; } - + uint32_t pos_fixed = state[CUR_POS_IDX]; uint32_t velocity = state[VELOCITY_IDX]; @@ -337,7 +339,7 @@ static void mode_spinning_wheel(void) { } } } -static const char _data_FX_MODE_SPINNINGWHEEL[] PROGMEM = "Spinning Wheel@Speed (0=random),Slowdown (0=random),Spinner size,,Spin delay,,Color per block,Sync restart;!,!;!;;m12=1,c1=1,c3=8"; +static const char _data_FX_MODE_SPINNINGWHEEL[] PROGMEM = "Spinning Wheel@Speed (0=random),Slowdown (0=random),Spinner size,,Spin delay,Spin me!,Color per block,Sync restart;!,!;!;;m12=1,c1=1,c3=8,o3=1"; /* From 5c7f727d2282dd4ff273336e6c4dc456983151fa Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Tue, 10 Mar 2026 09:20:46 -0700 Subject: [PATCH 12/14] Hopefully fixing a bug in pio-scripts\validate_modules.py --- pio-scripts/validate_modules.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pio-scripts/validate_modules.py b/pio-scripts/validate_modules.py index ae02e1f80d..8456305517 100644 --- a/pio-scripts/validate_modules.py +++ b/pio-scripts/validate_modules.py @@ -66,7 +66,8 @@ def check_elf_modules(elf_path: Path, env, module_lib_builders) -> set[str]: src_dir = str(builder.src_dir).rstrip("/\\") # Guard against prefix collisions (e.g. /path/to/mod vs /path/to/mod-extra) # by requiring a path separator immediately after the directory name. - if re.search(re.escape(src_dir) + r'[/\\]', placed_output): + src_dir_pattern = re.escape(src_dir).replace(r'\\', r'[/\\]') + if re.search(src_dir_pattern + r'[/\\]', placed_output): found.add(Path(builder.build_dir).name) return found From d064822a3b39df1a4412a75bc8fffbed4b200cd8 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Tue, 10 Mar 2026 12:41:27 -0700 Subject: [PATCH 13/14] Set default of check1 to 1 so it will automatically spin. --- usermods/user_fx/user_fx.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 375afafead..e01c319fd8 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -339,7 +339,7 @@ static void mode_spinning_wheel(void) { } } } -static const char _data_FX_MODE_SPINNINGWHEEL[] PROGMEM = "Spinning Wheel@Speed (0=random),Slowdown (0=random),Spinner size,,Spin delay,Spin me!,Color per block,Sync restart;!,!;!;;m12=1,c1=1,c3=8,o3=1"; +static const char _data_FX_MODE_SPINNINGWHEEL[] PROGMEM = "Spinning Wheel@Speed (0=random),Slowdown (0=random),Spinner size,,Spin delay,Spin me!,Color per block,Sync restart;!,!;!;;m12=1,c1=1,c3=8,o1=1,o3=1"; /* From dd9ee84acab9c98363acbf8cce5ec81237ee39a9 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Tue, 10 Mar 2026 14:19:07 -0700 Subject: [PATCH 14/14] There were more changes in the pio script that I didn't see. --- pio-scripts/validate_modules.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/pio-scripts/validate_modules.py b/pio-scripts/validate_modules.py index eeb26f2892..ae098f43cc 100644 --- a/pio-scripts/validate_modules.py +++ b/pio-scripts/validate_modules.py @@ -58,14 +58,25 @@ def check_elf_modules(elf_path: Path, env, module_lib_builders) -> set[str]: # nm -l appends a tab-separated "file:lineno" location to each symbol line. remaining = {Path(str(b.src_dir)): Path(b.build_dir).name for b in module_lib_builders} found = set() - for builder in module_lib_builders: - # builder.src_dir is the library source directory (used by is_wled_module() too) - src_dir = str(builder.src_dir).rstrip("/\\") - # Guard against prefix collisions (e.g. /path/to/mod vs /path/to/mod-extra) - # by requiring a path separator immediately after the directory name. - src_dir_pattern = re.escape(src_dir).replace(r'\\', r'[/\\]') - if re.search(src_dir_pattern + r'[/\\]', placed_output): - found.add(Path(builder.build_dir).name) + + for line in nm_output.splitlines(): + if not remaining: + break # all builders matched + addr, _, _ = line.partition(' ') + if not addr.lstrip('0'): + continue # zero address — skip debug-section marker + if '\t' not in line: + continue + loc = line.rsplit('\t', 1)[1] + # Strip trailing :lineno (e.g. "/path/to/foo.cpp:42" → "/path/to/foo.cpp") + src_path = Path(loc.rsplit(':', 1)[0]) + # Path.is_relative_to() handles OS-specific separators correctly without + # any regex, avoiding Windows path escaping issues. + for src_dir in list(remaining): + if src_path.is_relative_to(src_dir): + found.add(remaining.pop(src_dir)) + break + return found