Skip to content

Implement lastPreset tracking for auto-save usermod#5389

Open
ampledashes wants to merge 1 commit intowled:mainfrom
ampledashes:patch-1
Open

Implement lastPreset tracking for auto-save usermod#5389
ampledashes wants to merge 1 commit intowled:mainfrom
ampledashes:patch-1

Conversation

@ampledashes
Copy link
Copy Markdown

@ampledashes ampledashes commented Feb 20, 2026

Added lastPreset variable to track the last preset used for auto-saving functionality so that the last preset active will be called upon reboot if the user has those settings configured

Added lastPreset variable to track the last preset used for auto-saving functionality so that the last preset active will be called upon reboot
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 20, 2026

Walkthrough

Adds preset-recall detection to the auto-save usermod by tracking the last recalled preset and extending the autosave settle window when a new preset is recalled, preventing premature autosave triggers during preset switches.

Changes

Cohort / File(s) Summary
Auto-save Preset Tracking
usermods/usermod_v2_auto_save/usermod_v2_auto_save.cpp
Added lastPreset private state variable to detect preset recalls in loop() and extend autosave settle window when preset changes, coexisting with existing brightness/speed/intensity/mode/palette change detection.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested reviewers

  • willmmiles
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing lastPreset tracking for the auto-save usermod, which is the primary feature addition in this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ampledashes ampledashes marked this pull request as draft February 20, 2026 22:41
@ampledashes ampledashes marked this pull request as ready for review February 20, 2026 22:42
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
usermods/usermod_v2_auto_save/usermod_v2_auto_save.cpp (1)

134-144: ⚠️ Potential issue | 🟠 Major

Preset-recall tracking is unreachable with default autoSaveIgnorePresets=true

The early-return guard at line 134 fires for any currentPreset > 0 when autoSaveIgnorePresets is true (the default since line 56). The new block at lines 141–144 is therefore dead code in the default configuration — it is only reached when currentPreset == 0, i.e. no preset is active. The feature only works when users explicitly set autoSaveIgnorePresets = false, which is the non-default, undocumented path.

A secondary consequence: because lastPreset is never updated during the early-return path, it goes stale. When a user recalls preset N (early return fires, lastPreset unchanged) and then makes a manual change that clears currentPreset to 0, the comparison lastPreset (N) != currentPreset (0) fires a spurious "preset change" event. The practical impact is limited — the brightness/speed/etc. change already sets autoSaveAfter independently — but lastPreset should still be kept consistent.

Suggested fix: move the lastPreset synchronisation (and conditionally autoSaveAfter extension) before the early-return guard, or restructure the guard to allow preset-recall tracking to proceed regardless of autoSaveIgnorePresets:

🐛 Proposed fix
  void loop() {
    static unsigned long lastRun = 0;
    unsigned long now = millis();
    if (!autoSaveAfterSec || !enabled || (strip.isUpdating() && now - lastRun < 240)) return;
    lastRun = now;
    uint8_t currentMode = strip.getMainSegment().mode;
    uint8_t currentPalette = strip.getMainSegment().palette;
    unsigned long wouldAutoSaveAfter = now + autoSaveAfterSec*1000;

-   if (!autoSaveAfterSec || !enabled || (autoSaveIgnorePresets && currentPreset>0) || (strip.isUpdating() && now - lastRun < 240)) return;  // setting 0 as autosave seconds disables autosave
-   lastRun = now;
-   uint8_t currentMode = strip.getMainSegment().mode;
-   uint8_t currentPalette = strip.getMainSegment().palette;
-   unsigned long wouldAutoSaveAfter = now + autoSaveAfterSec*1000;
-
-   // Trigger on preset recall
-   if (lastPreset != currentPreset) {
-     lastPreset = currentPreset;
-     autoSaveAfter = wouldAutoSaveAfter;
-   }
-
-   if (knownBrightness != bri) {
+   // Track preset recalls before the autoSaveIgnorePresets guard so lastPreset
+   // is always consistent and the settle window is extended on any preset switch.
+   if (lastPreset != currentPreset) {
+     lastPreset = currentPreset;
+     autoSaveAfter = wouldAutoSaveAfter;
+   }
+
+   if (autoSaveIgnorePresets && currentPreset > 0) return;  // preset active, skip save
+
+   if (knownBrightness != bri) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/usermod_v2_auto_save/usermod_v2_auto_save.cpp` around lines 134 -
144, The preset-recall update is unreachable when autoSaveIgnorePresets is true
because the early-return uses (autoSaveIgnorePresets && currentPreset>0); fix by
moving the preset-change detection/synchronization before that guard: compute
wouldAutoSaveAfter, check if lastPreset != currentPreset, update lastPreset and
(only if you want to extend autosave regardless of ignore flag) set
autoSaveAfter = wouldAutoSaveAfter as appropriate, then evaluate the existing
early-return (which can still skip extending autosave when autoSaveIgnorePresets
is true). Ensure you reference and update the same variables: lastPreset,
currentPreset, autoSaveAfter, wouldAutoSaveAfter, and keep the
strip.isUpdating()/now-lastRun check intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@usermods/usermod_v2_auto_save/usermod_v2_auto_save.cpp`:
- Around line 134-144: The preset-recall update is unreachable when
autoSaveIgnorePresets is true because the early-return uses
(autoSaveIgnorePresets && currentPreset>0); fix by moving the preset-change
detection/synchronization before that guard: compute wouldAutoSaveAfter, check
if lastPreset != currentPreset, update lastPreset and (only if you want to
extend autosave regardless of ignore flag) set autoSaveAfter =
wouldAutoSaveAfter as appropriate, then evaluate the existing early-return
(which can still skip extending autosave when autoSaveIgnorePresets is true).
Ensure you reference and update the same variables: lastPreset, currentPreset,
autoSaveAfter, wouldAutoSaveAfter, and keep the strip.isUpdating()/now-lastRun
check intact.

@softhack007 softhack007 added the usermod usermod related label Feb 20, 2026
@softhack007
Copy link
Copy Markdown
Member

@ampledashes I'm still struggling to understand the use case for this PR. Can you explain a bit more in which scenarios this will be helpful?

The usermod already tracks the current effect, palette, effect sliders, colors, brightness. Afaik the purpose is not to create a copy of the last active preset, but to help users who tinker with effects and want the same effect to be active on next start.

the last preset active will be called upon reboot

This is exactly something that will not happen: If you activate preset 37, then switch WLED off after a minute, WLED will not re-activate preset 37. It will boot into the autosave preset, let's say preset 250.

Maybe I missed something?

@softhack007 softhack007 added the waiting for feedback addition information needed to better understand the issue label Feb 20, 2026
@ampledashes
Copy link
Copy Markdown
Author

ampledashes commented Feb 21, 2026

The specific gap we’re addressing is that recalling a preset and then powering off before making any value changes results in nothing being saved — the autosave never triggered because no tracked values changed. On reboot, the device loads stale autosave state rather than the recently recalled preset’s state. This PR ensures preset recalls also schedule a save, so the autosave snapshot stays current.

@DedeHai
Copy link
Copy Markdown
Collaborator

DedeHai commented Feb 21, 2026

// By default it will not save the state if an unmodified preset
// is selected (to not duplicate it). You can change this behaviour
// by setting autoSaveIgnorePresets=false

does that not work?

@softhack007
Copy link
Copy Markdown
Member

softhack007 commented Feb 21, 2026

recalling a preset and then powering off before making any value changes results in nothing being saved

Ah OK, so technically the issue is that this UM is not multi-segment aware. It tracks changes to the global "currenteffect", "currentPalette" etc. and reacts on changes in the main segment, but changing the effect in the second, third, etc segment will not be recognised.

The same happens when changing to a preset that doesn't modify the first segment, but only affects other segments.

https://github.com/ampledashes/WLED/blob/5d1e8e921f5cef4f2857b3ac2291b029719b6ca0/usermods/usermod_v2_auto_save/usermod_v2_auto_save.cpp#L116-L121

Adding "currentPreset" to the watched state information will be an improvement, but it's still not a solution when users change the effect in the second segment without using a preset.

Plus it will add flash writes (wearing out flash) that are just copying the current preset state into the dedicated autosave preset.

@softhack007 softhack007 changed the title Implement lastPreset tracking for auto-save feature Implement lastPreset tracking for auto-save usermod Feb 21, 2026
@softhack007
Copy link
Copy Markdown
Member

  1. I think we need a new UM settings checkbox, like "autosave when activating existing presets".
  2. do we also want to update the autosave preset slot when any kind of JSON preset - for example a button or timer controlled preset with LO=2 to end realtime mode?
  3. what happens when I run a playlist that changes to another preset every 10 seconds? Should autosave update on each playlist step?

@softhack007 softhack007 added this to the 16.1 milestone Apr 9, 2026
@softhack007
Copy link
Copy Markdown
Member

@coderabbitai please re-review this PR.
Summarize good practice, changes requested by reviewers, and improvements (prioritized).

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 9, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the full content of the changed file
cat usermods/usermod_v2_auto_save/usermod_v2_auto_save.cpp

Length of output: 11775


@softhack007 — here is a structured re-review of PR #5389.


Re-review Summary — PR #5389: lastPreset tracking for auto-save

What the PR does

Adds uint8_t lastPreset to track the last-seen currentPreset. When a preset recall is detected in loop(), it reschedules the auto-save settle window, so that switching to a preset without making any other changes still triggers an eventual auto-save.


🔴 Critical Issue — New code is dead in the default configuration

The early-return guard at the top of loop() reads:

if (!autoSaveAfterSec || !enabled ||
    (autoSaveIgnorePresets && currentPreset > 0) ||   // ← fires in default config
    (strip.isUpdating() && now - lastRun < 240)) return;

autoSaveIgnorePresets defaults to true. Whenever a preset is active (currentPreset > 0), the function returns before reaching the new preset-recall block. The feature is a complete no-op in the default configuration.

Fix options:

  • Move the preset-recall detection block above the early-return guard (and update lastPreset unconditionally), or
  • Introduce a separate opt-in flag (see below) and include its condition in the guard logic.

Additionally, after a preset recall the knownMode, knownPalette, etc. are not updated to match the recalled preset's state. This means after the settle window the existing change-detection could immediately schedule a second save.


🟡 Design Issues Raised by Reviewers (prioritized)

Priority Issue Reviewer
1 No new opt-in setting — behavior silently conflicts with existing autoSaveIgnorePresets softhack007
2 Playlist scenario: if a playlist cycles presets every few seconds, each step reschedules the settle window, potentially causing continuous back-to-back saves softhack007
3 JSON-controlled presets (e.g. button/timer with LO=2) would also trigger saves unintentionally softhack007
4 autoSaveIgnorePresets=false already partially addresses the stated use case — the PR adds complexity without clearly superseding the existing mechanism DedeHai
5 Multi-segment awareness gap (pre-existing, not introduced here): only the main segment is watched softhack007

✅ Suggested Improvements (prioritized)

  1. Add a dedicated opt-in setting, e.g. autoSaveOnPresetRecall (bool, default false), exposed in the config. Guard the preset-recall block with it. This keeps existing behaviour intact and gives users explicit control.

  2. Restructure the guard so lastPreset is always updated (to avoid drift), even when a save is not scheduled:

// Always sync lastPreset to avoid drift, regardless of other guards
if (lastPreset != currentPreset) {
    lastPreset = currentPreset;
    if (autoSaveOnPresetRecall && enabled && autoSaveAfterSec)
        autoSaveAfter = now + autoSaveAfterSec * 1000UL;
}
if (!autoSaveAfterSec || !enabled || (autoSaveIgnorePresets && currentPreset > 0) || ...) return;
  1. Sync known* values after a preset recall to prevent a redundant second save:
if (lastPreset != currentPreset) {
    lastPreset = currentPreset;
    // Sync known values so change-detection doesn't double-fire
    knownBrightness       = bri;
    knownEffectSpeed      = effectSpeed;
    knownEffectIntensity  = effectIntensity;
    knownMode             = strip.getMainSegment().mode;
    knownPalette          = strip.getMainSegment().palette;
    if (autoSaveOnPresetRecall) autoSaveAfter = wouldAutoSaveAfter;
}
  1. Throttle or debounce playlist saves — if currentPreset changes more than once within the settle window, only the last change should matter (which the settle-window approach already handles, but only if the save is not prematurely fired between playlist steps).

  2. Document the feature in the file-header comment block, alongside the existing bullet list of tracked state.


🧠 Learnings used
Learnt from: BobLoeffler68
Repo: wled/WLED PR: 5440
File: usermods/user_fx/user_fx.cpp:1307-1327
Timestamp: 2026-03-24T07:59:28.661Z
Learning: In WLED's Dissolve Plus effect (usermods/user_fx/user_fx.cpp, mode_dissolveplus), when the "Last one" checkbox (check3/lastOneMode) is turned off while the effect is in PHASE_PAUSE_SURVIVOR, the phase is intentionally NOT immediately reset. The pause expires naturally via the normal delay timing (a few hundred ticks), after which the filling phase begins. PHASE_DISSOLVE_SURVIVOR is reset to PHASE_DISSOLVE and PHASE_FILL_SURVIVOR is reset to PHASE_FILL on toggle-off, but PHASE_PAUSE_SURVIVOR is left to expire on its own. Do not flag this as a bug in future reviews.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-03-08T00:57:36.134Z
Learning: In WLED (wled00/cfg.cpp), `deserializeConfigFromFS()` only sets `needsSave=true` (triggering a write of cfg.json) when usermod settings are present in the JSON (lines 758-761). On a fresh install with no usermods, `needsSave=false` so `serializeConfigToFS()` is never called at first boot. Correct defaults are never written to cfg.json until the user manually saves. Structural fix: set `needsSave = needsSave || !cfgExists` where `cfgExists = WLED_FS.exists(s_cfg_json)` checked before reading the file.

Learnt from: DedeHai
Repo: wled/WLED PR: 4798
File: wled00/FX.cpp:7531-7533
Timestamp: 2025-08-26T11:51:21.817Z
Learning: In WLED PR `#4798`, DedeHai confirmed that certain gamma-related calls in FX.cpp/FX_fcn.cpp/particle systems are intentional for effect-level shaping (e.g., brightness curves, TV sim, Pride 2015 pre-mix), distinct from final output gamma. Do not flag or remove these in future reviews; add comments when feasible to clarify intent.

Learnt from: KrX3D
Repo: wled/WLED PR: 4585
File: usermods/seven_segment_display_reloaded_v2/seven_segment_display_reloaded_v2.cpp:612-617
Timestamp: 2026-02-18T00:41:15.185Z
Learning: In the seven_segment_display_reloaded_v2 usermod (PR `#4585`), the `addToConfig()` method intentionally calls `_updateMQTT()` to publish MQTT state on every config save. This behavior is by design and should not be flagged as an issue.

Learnt from: softhack007
Repo: wled/WLED PR: 5443
File: wled00/FX_fcn.cpp:1277-1277
Timestamp: 2026-03-24T12:10:32.630Z
Learning: In WLED's `WS2812FX::service()` (wled00/FX_fcn.cpp), the old condition `|| (doShow && seg.mode == FX_MODE_STATIC)` was an **inclusion** guard — it caused FX_MODE_STATIC to render only when another segment had already set doShow=true. It did NOT skip or protect FX_MODE_STATIC from rendering. The PR `#5443` simplification removes this condition, meaning FX_MODE_STATIC now renders on every `timeToShow` tick uniformly. This is intentional and not a regression. Do not flag FX_MODE_STATIC special-casing as missing in future reviews of this function.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-02-22T17:17:24.214Z
Learning: In WLED's deserializeState() (wled00/json.cpp), when a playlist is loaded via `loadPlaylist()`, setting `stateChanged = false` immediately after prevents premature LED updates with stale state. This allows the first preset in the playlist to handle the state update with correct values, while still preserving other JSON processing (WiFi config, palette removal, playlist advance flags) that follows in the function.

Learnt from: BobLoeffler68
Repo: wled/WLED PR: 5440
File: usermods/user_fx/user_fx.cpp:1304-1313
Timestamp: 2026-03-25T07:03:35.475Z
Learning: In WLED `mode_dissolveplus` (usermods/user_fx/user_fx.cpp), using `hw_random16(SEGLEN)` to select the survivor pixel index is correct and safe for this 1D-only effect. The 0xFFFF unmapped-entry concern from the physical bus mapping does not apply to 1D segments because virtual indices 0..SEGLEN-1 always map to valid physical LEDs without gaps. Do not flag this as a bug in future reviews of 1D effects.

Learnt from: blazoncek
Repo: wled/WLED PR: 5140
File: wled00/data/settings_time.htm:66-76
Timestamp: 2025-12-01T07:01:16.949Z
Learning: In WLED PR `#5134`, the fix for macros being initialized with the enable bit set only handles new configurations, not existing ones. If there is a bug in timer/macro handling code (e.g., in settings_time.htm), it must be fixed to work correctly for existing configurations as well.

Learnt from: willmmiles
Repo: wled/WLED PR: 5462
File: wled00/json.cpp:1189-1198
Timestamp: 2026-03-30T15:32:08.847Z
Learning: In WLED's `respondModeData()` (wled00/json.cpp), the 256-character `lineBuffer` limit for effect descriptor strings (getModeData) is an intentional constraint that matches the same 256-char limit used ~6 other places in the codebase. It is not new to this PR and should not be flagged as a regression. Lifting it requires a future refactor (better type system or dynamic buffer).

Learnt from: KrX3D
Repo: wled/WLED PR: 4585
File: usermods/seven_segment_display_reloaded_v2/seven_segment_display_reloaded_v2.cpp:3-5
Timestamp: 2026-02-18T00:11:02.403Z
Learning: In PR `#4585`, the seven_segment_display_reloaded_v2 usermod intentionally uses `#error` to require MQTT (`#ifdef WLED_DISABLE_MQTT `#error` "This user mod requires MQTT to be enabled." `#endif``) because the integration flow has not been tested or validated without MQTT, and failing fast at compile time is preferred over allowing potentially broken builds until a full MQTT-optional refactor is completed.

Learnt from: CR
Repo: wled/WLED PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-04-08T19:07:13.970Z
Learning: Every pull request should include a clear description of what changed and why, with screenshots for new features and links to related issues

Copy link
Copy Markdown
Member

@softhack007 softhack007 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ampledashes please address/ clarify the points raised by reviewers.

Summary is here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

usermod usermod related waiting for feedback addition information needed to better understand the issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants