From f2f25f2a7dee64a1eb5609d6c61b0a335a812cb3 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Mon, 12 Jan 2026 18:10:14 -0700 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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); } }