Skip to content

Add FSEQ + FPP usermods for local playback#5641

Open
Danit2 wants to merge 2 commits into
wled:mainfrom
Danit2:FSEQ_Player_with_ZIP
Open

Add FSEQ + FPP usermods for local playback#5641
Danit2 wants to merge 2 commits into
wled:mainfrom
Danit2:FSEQ_Player_with_ZIP

Conversation

@Danit2
Copy link
Copy Markdown

@Danit2 Danit2 commented May 22, 2026

Introduce a new FSEQ usermod package: adds a local .fseq playback effect and an FPP-compatible "FPP Connect" usermod. Files added include the FSEQ player implementation (fseq_player.{h,cpp}), effect glue (fseq_effect.h), web UI manager, XLZ unzip support, SD adapter compatibility shim, registration, and library.json (depends on bitbank2/unzipLIB). Features: per-segment local playback, optional outgoing FPP sync, incoming FPP discovery/control (START/SYNC/PING/BLANK/STOP), FPP-style HTTP endpoints, deferred XLZ unpacking on uploads, and an SD management web UI. Also includes small updates to existing SD/WLED integration files to hook the new usermods into the build/runtime.

Summary by CodeRabbit

  • New Features

    • FSEQ sequence playback from SD card with per-segment playback, loop and realtime sync controls
    • FPP compatibility for network discovery, UDP realtime control and override playback
    • Web-based SD manager UI for listing, uploading and deleting sequence files
    • Automatic extraction of compressed .xlz archives on SD
    • New HTTP endpoints exposing sequence and device status
  • Documentation

    • Added comprehensive README covering usage, upload behavior, limitations and UI/API paths

Review Change Stack

Introduce a new FSEQ usermod package: adds a local .fseq playback effect and an FPP-compatible "FPP Connect" usermod. Files added include the FSEQ player implementation (fseq_player.{h,cpp}), effect glue (fseq_effect.h), web UI manager, XLZ unzip support, SD adapter compatibility shim, registration, and library.json (depends on bitbank2/unzipLIB). Features: per-segment local playback, optional outgoing FPP sync, incoming FPP discovery/control (START/SYNC/PING/BLANK/STOP), FPP-style HTTP endpoints, deferred XLZ unpacking on uploads, and an SD management web UI. Also includes small updates to existing SD/WLED integration files to hook the new usermods into the build/runtime.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

Walkthrough

Adds an SD-backed FSEQ playback engine, an FPP-compatible network usermod with UDP/HTTP endpoints and upload handling, a browser SD manager UI, XLZ archive unpacking, and effect integration into WLED (new usermod IDs and realtime mode).

Changes

FSEQ Playback System

Layer / File(s) Summary
Constants and SD Adapter Infrastructure
wled00/const.h, usermods/FSEQ/sd_adapter_compat.h, usermods/sd_card/sd_card.cpp
New usermod IDs and realtime-mode constant; SD backend compatibility wrapper for SPI/MMC with safe defaults; SD-SPI defaults changed to -1 and old-pin types updated.
File Indexing and Cache
usermods/FSEQ/fseq_player.cpp
SD-root filename index cache (sorted, case-insensitive), refresh/invalidate helpers, and FPP override activity timeout handling.
Binary I/O and Playback Helpers
usermods/FSEQ/fseq_player.cpp
Little-endian readers, path existence checks, header debug, and playback-state navigation/timing helpers.
Frame Rendering and Sync
usermods/FSEQ/fseq_player.cpp
Segment and realtime frame rendering, late-join alignment, loop/stop handling, and sync adjustments (hard/soft) with filtering and slew.
Recording Load and State APIs
usermods/FSEQ/fseq_player.cpp
Recording header parsing/validation, state initialization/clear, per-segment control APIs, looping/repeat behavior, and scheduling.
Public Types and Header API
usermods/FSEQ/fseq_player.h
FSEQPlayer class, FileHeader/PlaybackState types, and C-style helpers for file index and FPP override management.
Effect Mode Integration
usermods/FSEQ/fseq_effect.h
mode_fseq_player() effect with per-segment playback controls, FPP override suppression, and sync message generation plus mode descriptor.
XLZ Archive Unpacking
usermods/FSEQ/xlz_unzip.h, usermods/FSEQ/xlz_unzip.cpp
SD-adapted ZIP callbacks, entry sanitization, single-entry extraction with heap buffering, free-space checks, batch processing, and index invalidation.
Web UI and File Management
usermods/FSEQ/web_ui_manager.h, usermods/FSEQ/web_ui_manager.cpp
Embedded SD manager page and REST APIs: /fsequi, /api/sd/list, /api/fseq/list, streaming /api/sd/upload, and /api/sd/delete with path safety and cache invalidation.
Usermod Registration
usermods/FSEQ/usermod_fseq.h, usermods/FSEQ/register_usermod.cpp
UsermodFseq registers the effect and UI endpoints; usermodFpp/usermodFseq instances created and registered at build.
Documentation and Configuration
usermods/FSEQ/README.md, usermods/FSEQ/library.json, wled00/json.cpp
README describing FSEQ Player and FPP Connect features, library manifest declaring unzipLIB@1.0.0, and JSON serialization updated to emit lm: "FSEQ" for the new realtime mode.

FPP Network Integration

Layer / File(s) Summary
FPP Connect Usermod
usermods/FSEQ/usermod_fpp.h
UsermodFPP with UDP listeners (unicast/multicast/broadcast), FPP START/SYNC/STOP/BLANK/PING handling, queued command processing, WiFi-aware listener lifecycle, HTTP endpoints (/api/system/info, /api/system/status, /api/fppd/multiSyncSystems, /fpp upload, /fseqfilelist), discovery/ping bursts, and buffered SD upload handling with deferred XLZ processing.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

usermod

Suggested reviewers

  • softhack007
  • willmmiles
  • netmindz
  • DedeHai
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: introducing FSEQ and FPP usermods for local playback functionality.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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


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.

@coderabbitai coderabbitai Bot added enhancement connectivity Issue regarding protocols, WiFi connection or availability of interfaces labels May 22, 2026
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.

Actionable comments posted: 8

🧹 Nitpick comments (5)
usermods/FSEQ/README.md (2)

199-199: 💤 Low value

Minor style improvement: avoid adverb repetition.

The phrase "scans the SD root only - extracts only the first file" repeats "only" twice in close succession. Consider rephrasing for smoother reading, e.g., "scans the SD root exclusively" or "extracts just the first file".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/FSEQ/README.md` at line 199, Edit the README phrase "scans the SD
root only - extracts only the first file" to avoid repeating "only"; change one
occurrence to a synonym such as "scans the SD root exclusively - extracts just
the first file" or "scans the SD root only - extracts just the first file" so
the sentence reads smoothly (update the line containing "scans the SD root only
- extracts only the first file").

299-299: 💤 Low value

Simplify wording for clarity.

"do not behave exactly the same" could be simplified to "behave differently" for more concise documentation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/FSEQ/README.md` at line 299, Replace the sentence "Browser upload
and FPP upload do **not** behave exactly the same for `.xlz` files." with a more
concise wording such as "Browser upload and FPP upload behave differently for
`.xlz` files." — locate the exact string in the README and update it in place,
preserving the `.xlz` code formatting and surrounding punctuation.
usermods/FSEQ/fseq_player.cpp (1)

1-5: ⚡ Quick win

Make wled.h the first include in this .cpp file.

This keeps translation-unit setup consistent with the project convention and avoids subtle include-order regressions.

♻️ Proposed change
-#include "fseq_player.h"
-#include "usermod_fseq.h"
-#include "wled.h"
-#include "sd_adapter_compat.h"
+#include "wled.h"
+#include "fseq_player.h"
+#include "usermod_fseq.h"
+#include "sd_adapter_compat.h"
 `#include` <Arduino.h>

As per coding guidelines Include "wled.h" as the primary project header in C++.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/FSEQ/fseq_player.cpp` around lines 1 - 5, Move the project primary
header include so "wled.h" is the first include in this translation unit: open
fseq_player.cpp and reorder the include directives so that `#include` "wled.h"
appears before "fseq_player.h", "usermod_fseq.h", "sd_adapter_compat.h" and
<Arduino.h>, preserving all existing includes and keeping their relative order
otherwise; this aligns with the project convention and prevents include-order
regressions.
usermods/FSEQ/fseq_player.h (1)

4-9: ⚡ Quick win

Replace these header macros with constexpr constants.

These values live in a public header, so #define leaks global names into every includer and drops type safety. A typed constexpr keeps the same behavior without polluting the preprocessor namespace.

As per coding guidelines: "Prefer constexpr over #define for compile-time constants in C++."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/FSEQ/fseq_player.h` around lines 4 - 9, Replace the preprocessor
macros with typed constexpr variables: remove the `#define` entries for
RECORDING_REPEAT_LOOP and RECORDING_REPEAT_DEFAULT in fseq_player.h and declare
them as constexpr (preferably inline constexpr) int constants with the same
values (e.g. -1 and 0) so they retain compile-time semantics without polluting
the preprocessor namespace; update any includes/usages as needed to use the new
constexpr names (RECORDING_REPEAT_LOOP, RECORDING_REPEAT_DEFAULT).
usermods/FSEQ/xlz_unzip.cpp (1)

141-141: ⚡ Quick win

Prefer WLED allocators for the unzip buffer.

Line 141 uses malloc, but this code should use WLED allocators to keep memory placement consistent and reduce heap-fragmentation risk on embedded targets.

♻️ Suggested change
-  uint8_t* buffer = static_cast<uint8_t*>(malloc(XLZ_BUFFER_SIZE));
+  uint8_t* buffer = static_cast<uint8_t*>(d_malloc(XLZ_BUFFER_SIZE));
   if (!buffer) {
     DEBUG_PRINTLN(F("[XLZ] Failed to allocate unzip buffer"));
     out.close();
     SD_ADAPTER.remove(outputPath.c_str());
     zip.closeCurrentFile();
     return false;
   }
@@
-  free(buffer);
+  free(buffer);

As per coding guidelines: “Use d_malloc() (DRAM-preferred) / p_malloc() (PSRAM-preferred) for allocation in C++.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/FSEQ/xlz_unzip.cpp` at line 141, Replace the direct malloc call that
allocates the unzip buffer with the WLED allocator recommended for this target:
instead of uint8_t* buffer = static_cast<uint8_t*>(malloc(XLZ_BUFFER_SIZE)); use
the appropriate WLED allocator (d_malloc for DRAM-preferred or p_malloc for
PSRAM-preferred) to allocate XLZ_BUFFER_SIZE for the variable buffer; ensure you
keep the uint8_t* buffer variable and free it with the matching deallocator used
in the codebase.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@usermods/FSEQ/fseq_effect.h`:
- Around line 67-79: The code currently uses fileCount as the playback reload
key which causes a reload whenever any file is added/removed; instead persist
and compare the last-resolved filename/path for the segment and only reload when
that filename actually changes. Modify the logic around
fileCount/selectedIndex/currentFileName (where
FSEQ_getFileNameByIndex(selectedIndex, currentFileName) is called and
SEGMENT.custom3 is used) to read a per-segment storedLastFileName, update that
stored value when FSEQ_getFileNameByIndex succeeds, and trigger reload only if
storedLastFileName != currentFileName (do the same replacement in the other
occurrence referenced around the second block). Ensure the storedLastFileName
survives between calls for the same segment (e.g., attach it to the segment
state) and remove reliance on fileCount for reload decisions.
- Around line 134-152: The code only sends a START when transports change but
never sends a STOP for transports that were just disabled; update the branch
around needsStart in the FSEQ effect sync logic to detect when
_fseq_lastSendMulticast[segId] != sendSyncMulticast or
_fseq_lastSendBroadcast[segId] != sendSyncBroadcast and for each transport that
was previously true but is now false call FPP_sendEffectSyncMessage with the
STOP opcode (same params as the START/periodic send) before sending the START
for the newly enabled transport; ensure you update
_fseq_lastSendMulticast[segId] and _fseq_lastSendBroadcast[segId] only after
emitting STOP(s) and the subsequent START so receivers on the disabled transport
get the STOP.

In `@usermods/FSEQ/usermod_fpp.h`:
- Around line 789-805: Add an inactivity timeout cleanup so stale upload state
(uploadStream, currentUploadFile, uploadSessionActive, currentUploadFileName) is
cleared when a client disconnected mid-transfer: before returning the 409 in the
index==0 branch (and the other upload branches referenced), check if
uploadSessionActive && (millis() - uploadStartTime) > UPLOAD_INACTIVITY_MS; if
true, close and delete the WriteBufferingStream (uploadStream), close and remove
the partial file via SD_ADAPTER.remove(currentUploadFileName.c_str()), set
currentUploadFile to nullptr, delete uploadStream, set uploadSessionActive=false
and reset currentUploadFileName/uploadStartTime, then continue to open a fresh
file; apply the same timeout/cleanup logic to the other upload handling blocks
(the ones around the other referenced ranges) to prevent persistent 409s.
- Around line 161-168: The current rememberSyncProgress() (called from
processUdpPacket()) directly mutates shared playback/status fields
(lastFppSyncSeconds, lastFppSyncMillis, lastSequenceName via
cacheSequenceName()) without synchronization; change this by introducing a
single loop-consumed queue or command snapshot mechanism so all mutations to FPP
sync/status state happen in loop() under a single serialized context (use the
existing fppMux or a new queue). Update rememberSyncProgress(),
buildSystemStatusJSON(), getStableStatusElapsedSeconds(), and
clearPlaybackStatusCache() callers to either enqueue update commands (for
UDP/HTTP handlers) or read from an immutable snapshot object provided by loop(),
and have loop() dequeue/apply updates to lastFppSyncSeconds, lastFppSyncMillis,
lastStatusElapsed*, and lastSequenceName (via cacheSequenceName()) to ensure
consistent, serialized state changes.

In `@usermods/FSEQ/web_ui_manager.cpp`:
- Around line 389-390: The code prepends a slash to the incoming filename but
does not block directory traversal or backslash variants; update the validation
where the variable filename is normalized (the site that currently does if
(!filename.startsWith("/")) filename = "/" + filename;) and the other place
handling delete/upload paths to first URL-decode and canonicalize the path, then
reject any inputs containing "..", "\" or "/"-backslash mixes (and
percent-encoded traversal like "%2e%2e"), or that resolve outside the intended
upload directory; return/emit an error response on invalid input instead of
using the unsafe filename.
- Around line 8-11: The upload code currently ignores the return value of
File::write which allows short/partial writes to be treated as success; modify
the write calls that use UploadContext::file (the occurrences around the blocks
noted at lines ~374-381 and ~409) to check the returned byte count against the
expected bytes written, and if it is less than expected or write() returns an
error set UploadContext::error = true, abort the upload, close the FILE handle,
delete/unlink the partial file, and ensure the request returns a proper failure
response; also ensure any subsequent logic checks UploadContext::error before
marking the upload as successful.
- Around line 128-133: The code uses title.innerHTML = '<span
class="code">#'+f.index+'</span> '+f.name in the data.fseqFiles loop which risks
XSS because f.name is user-controlled; change this to build DOM nodes and set
textContent for the filename (e.g., create the span for '#'+f.index via a
textNode or span.textContent and append a separate text node or element for
f.name using textContent) so no untrusted string is assigned to innerHTML;
update the loop that handles data.fseqFiles and remove direct innerHTML usage
(reference: data.fseqFiles.forEach, title.innerHTML).

In `@usermods/sd_card/sd_card.cpp`:
- Around line 32-35: The defaults for configPinSourceSelect/Clock/Poci/Pico were
changed to -1 but the snapshot variables (uint8_t oldPin...) used for change
detection convert -1 to 255, causing false change detection; fix by making the
snapshot variables use a signed type matching the config pins (e.g., change the
uint8_t oldPin... variables to int8_t oldPin...) or otherwise ensure the
comparison uses a signed cast so comparisons between
configPinSourceSelect/configPinSourceClock/configPinPoci/configPinPico and
oldPin... are done as signed values; update the variables used in the
change-detection comparisons (the oldPin... identifiers and any comparisons
around where reinit is triggered) so -1 is handled correctly.

---

Nitpick comments:
In `@usermods/FSEQ/fseq_player.cpp`:
- Around line 1-5: Move the project primary header include so "wled.h" is the
first include in this translation unit: open fseq_player.cpp and reorder the
include directives so that `#include` "wled.h" appears before "fseq_player.h",
"usermod_fseq.h", "sd_adapter_compat.h" and <Arduino.h>, preserving all existing
includes and keeping their relative order otherwise; this aligns with the
project convention and prevents include-order regressions.

In `@usermods/FSEQ/fseq_player.h`:
- Around line 4-9: Replace the preprocessor macros with typed constexpr
variables: remove the `#define` entries for RECORDING_REPEAT_LOOP and
RECORDING_REPEAT_DEFAULT in fseq_player.h and declare them as constexpr
(preferably inline constexpr) int constants with the same values (e.g. -1 and 0)
so they retain compile-time semantics without polluting the preprocessor
namespace; update any includes/usages as needed to use the new constexpr names
(RECORDING_REPEAT_LOOP, RECORDING_REPEAT_DEFAULT).

In `@usermods/FSEQ/README.md`:
- Line 199: Edit the README phrase "scans the SD root only - extracts only the
first file" to avoid repeating "only"; change one occurrence to a synonym such
as "scans the SD root exclusively - extracts just the first file" or "scans the
SD root only - extracts just the first file" so the sentence reads smoothly
(update the line containing "scans the SD root only - extracts only the first
file").
- Line 299: Replace the sentence "Browser upload and FPP upload do **not**
behave exactly the same for `.xlz` files." with a more concise wording such as
"Browser upload and FPP upload behave differently for `.xlz` files." — locate
the exact string in the README and update it in place, preserving the `.xlz`
code formatting and surrounding punctuation.

In `@usermods/FSEQ/xlz_unzip.cpp`:
- Line 141: Replace the direct malloc call that allocates the unzip buffer with
the WLED allocator recommended for this target: instead of uint8_t* buffer =
static_cast<uint8_t*>(malloc(XLZ_BUFFER_SIZE)); use the appropriate WLED
allocator (d_malloc for DRAM-preferred or p_malloc for PSRAM-preferred) to
allocate XLZ_BUFFER_SIZE for the variable buffer; ensure you keep the uint8_t*
buffer variable and free it with the matching deallocator used in the codebase.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fe641aa8-23d1-4a5b-b071-dc98bba3811a

📥 Commits

Reviewing files that changed from the base of the PR and between 6ae22c9 and d3ca843.

📒 Files selected for processing (16)
  • usermods/FSEQ/README.md
  • usermods/FSEQ/fseq_effect.h
  • usermods/FSEQ/fseq_player.cpp
  • usermods/FSEQ/fseq_player.h
  • usermods/FSEQ/library.json
  • usermods/FSEQ/register_usermod.cpp
  • usermods/FSEQ/sd_adapter_compat.h
  • usermods/FSEQ/usermod_fpp.h
  • usermods/FSEQ/usermod_fseq.h
  • usermods/FSEQ/web_ui_manager.cpp
  • usermods/FSEQ/web_ui_manager.h
  • usermods/FSEQ/xlz_unzip.cpp
  • usermods/FSEQ/xlz_unzip.h
  • usermods/sd_card/sd_card.cpp
  • wled00/const.h
  • wled00/json.cpp

Comment thread usermods/FSEQ/fseq_effect.h
Comment thread usermods/FSEQ/fseq_effect.h Outdated
Comment thread usermods/FSEQ/usermod_fpp.h Outdated
Comment thread usermods/FSEQ/usermod_fpp.h
Comment thread usermods/FSEQ/web_ui_manager.cpp
Comment thread usermods/FSEQ/web_ui_manager.cpp
Comment thread usermods/FSEQ/web_ui_manager.cpp Outdated
Comment thread usermods/sd_card/sd_card.cpp
@coderabbitai coderabbitai Bot added the usermod usermod related label May 22, 2026
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.

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (5)
usermods/FSEQ/fseq_player.cpp (1)

38-40: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle mixed-case .fseq suffixes.

This only accepts two exact spellings, so valid files like /show.Fseq never enter the index and cannot be selected by index/name lookup. Please switch to a real case-insensitive suffix check and reuse it in the SD UI filter so both paths stay aligned.

🐛 Proposed fix
+static bool endsWithIgnoreCase(const String &value, const char *suffix) {
+  const size_t n = strlen(suffix);
+  if (value.length() < n) return false;
+  return value.substring(value.length() - n).equalsIgnoreCase(suffix);
+}
+
 bool isFseqFileName(const String &name) {
-  return name.endsWith(".fseq") || name.endsWith(".FSEQ");
+  return endsWithIgnoreCase(name, ".fseq");
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/FSEQ/fseq_player.cpp` around lines 38 - 40, The isFseqFileName
function currently only matches two exact spellings; change it to perform a
case-insensitive suffix check by normalizing the input (e.g., toLowerCase) and
checking endsWith(".fseq") inside isFseqFileName, and update the SD UI
file-filter code to call and reuse isFseqFileName instead of duplicating its
logic so mixed-case variants like "Fseq" or "fSeQ" are accepted consistently
across indexing and the UI.
usermods/FSEQ/web_ui_manager.cpp (2)

336-339: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the same case-insensitive FSEQ check here.

This filter repeats the same exact-case suffix test as the indexer. Mixed-case names like /show.Fseq will still land in otherFiles even though they are valid FSEQ recordings. Share one ignore-case helper between the cache and this endpoint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/FSEQ/web_ui_manager.cpp` around lines 336 - 339, The filtering logic
in web_ui_manager.cpp builds a filename String called name and uses
name.endsWith(".fseq") || name.endsWith(".FSEQ"), which is case-sensitive and
inconsistent with the indexer; replace this with a single case-insensitive check
(e.g., compare name.toLowerCase().endsWith(".fseq") or call the shared helper
used by the indexer such as isFseqFile/IsFseqSuffix) so mixed-case filenames
like "/show.Fseq" are recognized as FSEQ; update the branch around the name
variable (the block that creates otherFiles.createNestedObject()) to use that
shared helper or toLowerCase() check to avoid duplicating logic.

235-242: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Don’t show “Upload complete” when /api/sd/upload returns a 4xx/5xx

xhr.onload runs when the request finishes even for HTTP error statuses, but the UI currently always reports success. Gate the success path on xhr.status (and show xhr.responseText on failure).

🐛 Proposed fix
   xhr.onload=function(){
-    statusText.innerText='Upload complete';
-    loadSDList();
+    if (xhr.status >= 200 && xhr.status < 300) {
+      statusText.innerText='Upload complete';
+      loadSDList();
+    } else {
+      statusText.innerText=xhr.responseText||('Upload failed ('+xhr.status+')');
+    }
     setTimeout(()=>{
       progressBar.style.width='0%';
       statusText.innerText='';
     },2000);
   };
+
+  xhr.onerror=function(){
+    statusText.innerText='Upload failed';
+  };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/FSEQ/web_ui_manager.cpp` around lines 235 - 242, xhr.onload
currently treats any finished request as success; update xhr.onload to check
xhr.status (e.g., success if 200–299) before setting statusText to 'Upload
complete' and calling loadSDList(), otherwise set statusText to the error text
(xhr.responseText or a short fallback) to indicate failure and avoid showing
success; also ensure progressBar reset behavior still runs (on success or after
showing the error) so the UI clears consistently.
usermods/FSEQ/xlz_unzip.cpp (1)

243-251: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Account for the existing destination file in the free-space check.

unpackCurrentFile() deletes finalOutputPath before writing, but this precheck ignores the bytes that would be reclaimed. Replacing an existing .fseq can therefore fail spuriously on a nearly full card even when the overwrite would fit.

🐛 Proposed fix
+  uint64_t existingOutputSize = 0;
+  if (SD_ADAPTER.exists(finalOutputPath.c_str())) {
+    File existing = SD_ADAPTER.open(finalOutputPath.c_str(), FILE_READ);
+    if (existing) {
+      existingOutputSize = existing.size();
+      existing.close();
+    }
+  }
+
   const uint64_t totalBytes = SD_ADAPTER.totalBytes();
   const uint64_t usedBytes = SD_ADAPTER.usedBytes();
-  const uint64_t freeBytes = (totalBytes >= usedBytes) ? (totalBytes - usedBytes) : 0;
+  const uint64_t freeBytes =
+      (totalBytes >= usedBytes) ? (totalBytes - usedBytes + existingOutputSize)
+                                : existingOutputSize;
   if (fileInfo.uncompressed_size > freeBytes) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/FSEQ/xlz_unzip.cpp` around lines 243 - 251, The free-space precheck
in unpackCurrentFile() uses fileInfo.uncompressed_size vs freeBytes from
SD_ADAPTER but ignores that finalOutputPath is deleted before writing, so an
existing file's bytes would be reclaimed; update the check to account for the
current destination file size by querying the filesystem for finalOutputPath
(e.g., get its size if it exists) and treat effectiveFree = freeBytes +
existingFileSize (or reduce required bytes by existingFileSize) before comparing
to fileInfo.uncompressed_size; keep the existing DEBUG_PRINTF, call
g_xlzZip.closeZIP() on failure as before.
usermods/FSEQ/fseq_effect.h (1)

96-118: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't cache the filename until the segment actually starts playing.

If loadRecordingForSegment() fails once, Line 108 still records currentFileName, so fileChanged stays false forever and this segment never retries the load until the user changes selection.

🔧 Localized fix
   _fseq_lastIndex[segId] = selectedIndex;
   _fseq_lastLoop[segId] = loop;
-  _fseq_lastFileName[segId] = currentFileName;
 
   FSEQPlayer::setSegmentLooping(segId, loop);
 
   if (!FSEQPlayer::isSegmentPlaying(segId)) {
+    _fseq_lastFileName[segId] = String();
     stopFseqSyncIfNeeded(segId, currentFileName);
     _fseq_wasPlaying[segId] = false;
     _fseq_syncStartMs[segId] = 0;
     _fseq_lastSyncMs[segId] = 0;
     SEGMENT.fill(BLACK);
     return;
   }
+
+  _fseq_lastFileName[segId] = currentFileName;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/FSEQ/fseq_effect.h` around lines 96 - 118, The code caches
currentFileName into _fseq_lastFileName[segId] unconditionally even when
loadRecordingForSegment() fails, preventing future retry; fix by moving the
assignment of _fseq_lastFileName[segId] = currentFileName (and any state that
should reflect a successful load) so it only happens after the segment actually
starts playing (e.g., after calling FSEQPlayer::loadRecordingForSegment(...) and
verifying FSEQPlayer::isSegmentPlaying(segId) or another definitive success
signal), leaving _fseq_lastFileName unchanged on load failure so fileChanged
will be true and the load will be retried later.
🧹 Nitpick comments (1)
usermods/FSEQ/xlz_unzip.cpp (1)

153-170: ⚡ Quick win

Use delay(1) in these long extraction loops.

These loops can run for large archives, and yield() is not the watchdog-safe pattern this repo expects on ESP32. Replacing both call sites with delay(1) keeps extraction cooperative during long runs.

♻️ Proposed fix
-    yield();
+    delay(1);
@@
-    yield();
+    delay(1);
As per coding guidelines, “ESP32 timing: don’t assume `yield()` feeds watchdog; if using custom loops/tasks, use `delay(1)` rather than `yield()`.”

Also applies to: 325-326

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/FSEQ/xlz_unzip.cpp` around lines 153 - 170, The long extraction loop
using yield() (see zip.readCurrentFile, out.write and the written variable in
xlz_unzip.cpp) must use delay(1) to be watchdog-safe on ESP32; replace the
yield() call inside the loop with delay(1) and also change the other yield()
call mentioned around lines 325-326 to delay(1) so long-running decompression
stays cooperative with the ESP32 watchdog.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@usermods/FSEQ/fseq_player.h`:
- Around line 4-5: The constants static constexpr int16_t RECORDING_REPEAT_LOOP
and RECORDING_REPEAT_DEFAULT are defined before any includes so int16_t may be
undefined; move these two definitions below the header includes (e.g., after
`#include` "wled.h" or other existing includes) so the fixed-width integer type is
available, keeping the same names and values.

In `@usermods/FSEQ/usermod_fpp.h`:
- Around line 716-733: The bug is that uploadSessionActive is being used for two
different meanings (XLZ-pending vs upload-in-progress) causing XLZ uploads to be
left pending and never unpacked; introduce a separate boolean (e.g.,
uploadInProgress or uploadAcceptingUploads) to represent whether new uploads are
allowed/ongoing and keep uploadSessionActive reserved for "xlzPendingScan" or
rename uploadSessionActive to xlzPendingScan usage; update
finishSuccessfulFppUpload (and the code paths that set/clear uploadSessionActive
like the non-.xlz branch) to set xlzPendingScan = true for ".xlz" and only touch
the new uploadInProgress flag for upload lifecycle, and change the upload
acceptance check (the code that currently inspects open handles /
uploadSessionActive) to consult the new uploadInProgress flag so an XLZ pending
state does not cancel/disable the pending unpack.
- Around line 682-714: cleanupFppUploadState and cleanupStaleFppUploadIfNeeded
race with the AsyncWebServer POST handler because uploadStream,
currentUploadFile, currentUploadFileName, uploadSessionActive, xlzPendingScan
and lastUploadActivity are accessed from multiple FreeRTOS tasks; protect all
accesses and mutations of these shared members by introducing a single mutex (or
single-owner queue/event handoff) and lock it in cleanupFppUploadState,
cleanupStaleFppUploadIfNeeded, the /fpp POST handler (where
uploadStream/currentUploadFile are written/flushed), and any loop-side checks
that read uploadSessionActive/xlzPendingScan/lastUploadActivity so reads are
consistent. Also fix the XLZ lifecycle in finishSuccessfulFppUpload by
explicitly clearing xlzPendingScan when the uploaded file is not an .xlz (or by
setting/clearing uploadSessionActive/xlzPendingScan under the same lock) so a
non-.xlz upload cannot leave xlzPendingScan true and prevent the loop scan
condition (uploadSessionActive && xlzPendingScan) from ever running.

---

Outside diff comments:
In `@usermods/FSEQ/fseq_effect.h`:
- Around line 96-118: The code caches currentFileName into
_fseq_lastFileName[segId] unconditionally even when loadRecordingForSegment()
fails, preventing future retry; fix by moving the assignment of
_fseq_lastFileName[segId] = currentFileName (and any state that should reflect a
successful load) so it only happens after the segment actually starts playing
(e.g., after calling FSEQPlayer::loadRecordingForSegment(...) and verifying
FSEQPlayer::isSegmentPlaying(segId) or another definitive success signal),
leaving _fseq_lastFileName unchanged on load failure so fileChanged will be true
and the load will be retried later.

In `@usermods/FSEQ/fseq_player.cpp`:
- Around line 38-40: The isFseqFileName function currently only matches two
exact spellings; change it to perform a case-insensitive suffix check by
normalizing the input (e.g., toLowerCase) and checking endsWith(".fseq") inside
isFseqFileName, and update the SD UI file-filter code to call and reuse
isFseqFileName instead of duplicating its logic so mixed-case variants like
"Fseq" or "fSeQ" are accepted consistently across indexing and the UI.

In `@usermods/FSEQ/web_ui_manager.cpp`:
- Around line 336-339: The filtering logic in web_ui_manager.cpp builds a
filename String called name and uses name.endsWith(".fseq") ||
name.endsWith(".FSEQ"), which is case-sensitive and inconsistent with the
indexer; replace this with a single case-insensitive check (e.g., compare
name.toLowerCase().endsWith(".fseq") or call the shared helper used by the
indexer such as isFseqFile/IsFseqSuffix) so mixed-case filenames like
"/show.Fseq" are recognized as FSEQ; update the branch around the name variable
(the block that creates otherFiles.createNestedObject()) to use that shared
helper or toLowerCase() check to avoid duplicating logic.
- Around line 235-242: xhr.onload currently treats any finished request as
success; update xhr.onload to check xhr.status (e.g., success if 200–299) before
setting statusText to 'Upload complete' and calling loadSDList(), otherwise set
statusText to the error text (xhr.responseText or a short fallback) to indicate
failure and avoid showing success; also ensure progressBar reset behavior still
runs (on success or after showing the error) so the UI clears consistently.

In `@usermods/FSEQ/xlz_unzip.cpp`:
- Around line 243-251: The free-space precheck in unpackCurrentFile() uses
fileInfo.uncompressed_size vs freeBytes from SD_ADAPTER but ignores that
finalOutputPath is deleted before writing, so an existing file's bytes would be
reclaimed; update the check to account for the current destination file size by
querying the filesystem for finalOutputPath (e.g., get its size if it exists)
and treat effectiveFree = freeBytes + existingFileSize (or reduce required bytes
by existingFileSize) before comparing to fileInfo.uncompressed_size; keep the
existing DEBUG_PRINTF, call g_xlzZip.closeZIP() on failure as before.

---

Nitpick comments:
In `@usermods/FSEQ/xlz_unzip.cpp`:
- Around line 153-170: The long extraction loop using yield() (see
zip.readCurrentFile, out.write and the written variable in xlz_unzip.cpp) must
use delay(1) to be watchdog-safe on ESP32; replace the yield() call inside the
loop with delay(1) and also change the other yield() call mentioned around lines
325-326 to delay(1) so long-running decompression stays cooperative with the
ESP32 watchdog.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f6d4dbf1-a4cb-429e-834c-cd709eb7ee38

📥 Commits

Reviewing files that changed from the base of the PR and between d3ca843 and d994163.

📒 Files selected for processing (8)
  • usermods/FSEQ/README.md
  • usermods/FSEQ/fseq_effect.h
  • usermods/FSEQ/fseq_player.cpp
  • usermods/FSEQ/fseq_player.h
  • usermods/FSEQ/usermod_fpp.h
  • usermods/FSEQ/web_ui_manager.cpp
  • usermods/FSEQ/xlz_unzip.cpp
  • usermods/sd_card/sd_card.cpp
✅ Files skipped from review due to trivial changes (1)
  • usermods/FSEQ/README.md

Comment on lines +4 to +5
static constexpr int16_t RECORDING_REPEAT_LOOP = -1;
static constexpr int16_t RECORDING_REPEAT_DEFAULT = 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
nl -ba usermods/FSEQ/fseq_player.h | sed -n '1,12p'

Repository: wled/WLED

Length of output: 96


🏁 Script executed:

#!/bin/bash
set -euo pipefail
awk '{printf "%4d\t%s\n", NR, $0}' usermods/FSEQ/fseq_player.h | sed -n '1,25p'

Repository: wled/WLED

Length of output: 822


Move RECORDING_REPEAT_* below the includes in usermods/FSEQ/fseq_player.h

int16_t is used at lines 4–5 before any header (including #include "wled.h") is included, so the header isn’t self-contained and can break when included first in a translation unit.

🔧 Proposed fix
-static constexpr int16_t RECORDING_REPEAT_LOOP = -1;
-static constexpr int16_t RECORDING_REPEAT_DEFAULT = 0;
-
 `#include` "wled.h"
 `#include` "sd_adapter_compat.h"
+
+static constexpr int16_t RECORDING_REPEAT_LOOP = -1;
+static constexpr int16_t RECORDING_REPEAT_DEFAULT = 0;
🧰 Tools
🪛 Clang (14.0.6)

[error] 4-4: unknown type name 'constexpr'

(clang-diagnostic-error)


[error] 4-4: expected ';' after top level declarator

(clang-diagnostic-error)


[error] 5-5: unknown type name 'constexpr'

(clang-diagnostic-error)


[error] 5-5: expected ';' after top level declarator

(clang-diagnostic-error)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/FSEQ/fseq_player.h` around lines 4 - 5, The constants static
constexpr int16_t RECORDING_REPEAT_LOOP and RECORDING_REPEAT_DEFAULT are defined
before any includes so int16_t may be undefined; move these two definitions
below the header includes (e.g., after `#include` "wled.h" or other existing
includes) so the fixed-width integer type is available, keeping the same names
and values.

Comment on lines +682 to +714
void cleanupFppUploadState(bool removePartialFile) {
const String partialFile = currentUploadFileName;

if (uploadStream) {
delete uploadStream;
uploadStream = nullptr;
}

if (currentUploadFile) {
currentUploadFile.close();
}

if (removePartialFile && partialFile.length() > 0) {
SD_ADAPTER.remove(partialFile.c_str());
DEBUG_PRINTF("[FPP] Removed partial upload: %s\n", partialFile.c_str());
}

currentUploadFile = File();
currentUploadFileName = "";
uploadStartTime = 0;
lastUploadActivity = 0;
uploadSessionActive = false;
xlzPendingScan = false;
}

void cleanupStaleFppUploadIfNeeded() {
if (!uploadStream && !currentUploadFile) return;
if (lastUploadActivity == 0) return;
if (millis() - lastUploadActivity < uploadInactivityTimeout) return;

DEBUG_PRINTLN(F("[FPP] Upload timed out; cleaning partial state"));
cleanupFppUploadState(true);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

For ESPAsyncWebServer on ESP32, does the HTTP upload body callback run outside the Arduino loop() context, and should state shared between that callback and loop() be protected with a mutex or serialized queue?

💡 Result:

Yes: the HTTP upload/body callbacks are not run in the Arduino sketch’s loop context on ESP32; they execute from the AsyncTCP/AsyncWebServer callback task (i.e., a separate FreeRTOS task), so they can run concurrently with loop. Evidence: AsyncTCP is documented/configured as a separate task that runs the network event queue and “executes server callbacks” (CONFIG_ASYNC_TCP_RUNNING_CORE / async_tcp task), not in loop [1][2]. A GitHub issue discussing callback context on ESP32 explicitly notes “loop is running in a task … and the ESPAsyncWebserver is running in a separate task” [3]. Should shared state between that callback and loop be protected (mutex or serialized queue)? - Treat it as concurrent access and protect any non-atomic shared variables you read/write from both contexts. The GitHub issue confirms you “have to consider concurrency access” and only safe cases are atomic/primitive types with only simple reads/writes (no read-modify-write), and same-core assumptions [3]. - If you need to move structured data or do read-modify-write, use a synchronization mechanism (mutex, critical section) or a serialized queue/event-transfer pattern so loop is the sole consumer. The general safe pattern is to have the callback quickly store incoming data into a queue and return, then have loop process it. Practical guidance: 1) Keep the upload/body callback short; accumulate data carefully. The library states handleBody/handleUpload callbacks are called multiple times while parsing [4][5]. 2) For communication to loop, prefer a queue or “set flags + copy plain primitive data” approach; only mutex-guard complex structures or read-modify-write operations. 3) You do not need a mutex for values that are truly atomic and are only ever written or read (no RMW) from both contexts [3]. Bottom line: The callback runs outside loop, so assume concurrency and synchronize shared state unless you can prove it’s atomic/plain and only accessed with simple read/write operations [3][1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="usermods/FSEQ/usermod_fpp.h"

# Show the referenced functions/snippets with line numbers
sed -n '640,760p' "$FILE" | cat -n
echo "-----"
sed -n '880,1045p' "$FILE" | cat -n
echo "-----"
sed -n '1045,1185p' "$FILE" | cat -n
echo "-----"

# Find all uses of the lifecycle flags
rg -n "uploadSessionActive|xlzPendingScan|cleanupStaleFppUploadIfNeeded|cleanupFppUploadState|finishSuccessfulFppUpload" "$FILE"

echo "-----"

# Identify the /fpp upload callback and whether it touches shared upload state
rg -n "(/fpp|fpp).*upload|on\\(.*upload|handle\\w*Upload|upload\\w*\\(" "$FILE" || true

# Print nearby server route setup if found
rg -n "server\\.on\\(|AsyncWebServer|AsyncWebServerRequest|AsyncWebServer|AsyncWebHandler|server" "$FILE" | head -n 80

Repository: wled/WLED

Length of output: 17479


Fix concurrent upload-state access and XLZ pending lifecycle in FSEQ usermod.

  • cleanupStaleFppUploadIfNeeded() / cleanupFppUploadState() (loop-side, lines 682-714) can delete/close uploadStream and currentUploadFile while the /fpp AsyncWebServer POST handler (lines 931-1009) is simultaneously writing/flushing them—ESPAsyncWebServer callbacks run outside Arduino loop() in a separate FreeRTOS task. Add proper serialization (e.g., mutex or a single-owner queue/event handoff) for the shared upload members (uploadStream, currentUploadFile, currentUploadFileName, uploadSessionActive, xlzPendingScan, lastUploadActivity).
  • finishSuccessfulFppUpload() (lines 716-733) sets xlzPendingScan = true only for .xlz, but the non-.xlz path never clears xlzPendingScan. Since the /fpp handler only rejects when uploadStream || currentUploadFile is set (not when an XLZ is pending), starting a new non-.xlz upload can set uploadSessionActive = false and leave xlzPendingScan = true, so the pending XLZ never runs the loop condition (uploadSessionActive && xlzPendingScan, lines 1128-1136).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/FSEQ/usermod_fpp.h` around lines 682 - 714, cleanupFppUploadState
and cleanupStaleFppUploadIfNeeded race with the AsyncWebServer POST handler
because uploadStream, currentUploadFile, currentUploadFileName,
uploadSessionActive, xlzPendingScan and lastUploadActivity are accessed from
multiple FreeRTOS tasks; protect all accesses and mutations of these shared
members by introducing a single mutex (or single-owner queue/event handoff) and
lock it in cleanupFppUploadState, cleanupStaleFppUploadIfNeeded, the /fpp POST
handler (where uploadStream/currentUploadFile are written/flushed), and any
loop-side checks that read uploadSessionActive/xlzPendingScan/lastUploadActivity
so reads are consistent. Also fix the XLZ lifecycle in finishSuccessfulFppUpload
by explicitly clearing xlzPendingScan when the uploaded file is not an .xlz (or
by setting/clearing uploadSessionActive/xlzPendingScan under the same lock) so a
non-.xlz upload cannot leave xlzPendingScan true and prevent the loop scan
condition (uploadSessionActive && xlzPendingScan) from ever running.

Comment on lines +716 to +733
void finishSuccessfulFppUpload(const String &uploadedFile) {
String lowerName = uploadedFile;
lowerName.toLowerCase();

lastUploadFinished = millis();
lastUploadActivity = lastUploadFinished;
currentUploadFileName = "";
currentUploadFile = File();
uploadStartTime = 0;

if (lowerName.endsWith(".xlz")) {
xlzPendingScan = true;
uploadSessionActive = true;
} else {
FSEQ_invalidateFileIndexCache();
uploadSessionActive = false;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

uploadSessionActive is now doing two incompatible jobs.

After a successful .xlz upload, uploadSessionActive means “waiting for unzip”, but Line 939 still accepts a new upload because it only checks open handles. If that later upload finishes as a normal file, Line 731 clears uploadSessionActive while leaving xlzPendingScan set, so the earlier XLZ can no longer satisfy the Line 1128 processing condition and never gets unpacked.

🔧 Minimal containment fix
-          if (uploadStream || currentUploadFile) {
+          if (uploadStream || currentUploadFile || xlzPendingScan) {
             request->send(409, "text/plain", "Upload already in progress");
             return;
           }

Also applies to: 936-974, 1128-1136

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@usermods/FSEQ/usermod_fpp.h` around lines 716 - 733, The bug is that
uploadSessionActive is being used for two different meanings (XLZ-pending vs
upload-in-progress) causing XLZ uploads to be left pending and never unpacked;
introduce a separate boolean (e.g., uploadInProgress or uploadAcceptingUploads)
to represent whether new uploads are allowed/ongoing and keep
uploadSessionActive reserved for "xlzPendingScan" or rename uploadSessionActive
to xlzPendingScan usage; update finishSuccessfulFppUpload (and the code paths
that set/clear uploadSessionActive like the non-.xlz branch) to set
xlzPendingScan = true for ".xlz" and only touch the new uploadInProgress flag
for upload lifecycle, and change the upload acceptance check (the code that
currently inspects open handles / uploadSessionActive) to consult the new
uploadInProgress flag so an XLZ pending state does not cancel/disable the
pending unpack.

@softhack007
Copy link
Copy Markdown
Member

softhack007 commented May 23, 2026

Comment thread usermods/FSEQ/xlz_unzip.h
@Danit2
Copy link
Copy Markdown
Author

Danit2 commented May 23, 2026

What's the difference to the existing FSEQ PRs ? Like

This is a user module designed to meet all expectations.

The FSEQ player is now available as an effect and supports multi-segment playback.
It uses the standard SD card user module.
Nevertheless, a higher-level system can control the players synchronously and set the ESP to real-time mode.

For me, it’s the best solution that meets all needs.

Hope this is okay now.

@softhack007
Copy link
Copy Markdown
Member

softhack007 commented May 23, 2026

@coderabbitai This PR is an evolution of #5395 (by the same user).

In order to optimize the review

  • please collect all pending changes requested for Add FSEQ usermod (SD playback + FPP) #5395
  • check which requests are either obsolete or already resolved in the current PR
  • create a list of requests and possible improvements that still apply

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 23, 2026

Tip

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch PR `#5395` details, reviews, and comments
echo "=== PR `#5395` Basic Info ==="
gh pr view 5395 --repo wled/WLED --json title,state,body,labels,author

echo ""
echo "=== PR `#5395` Reviews ==="
gh pr reviews 5395 --repo wled/WLED

echo ""
echo "=== PR `#5395` Review Comments (inline) ==="
gh api repos/wled/WLED/pulls/5395/comments --paginate | jq '.[] | {path: .path, line: .line, user: .user.login, body: .body, created_at: .created_at}'

echo ""
echo "=== PR `#5395` Issue Comments (general) ==="
gh api repos/wled/WLED/issues/5395/comments --paginate | jq '.[] | {user: .user.login, body: .body, created_at: .created_at}'

Length of output: 154773


🏁 Script executed:

#!/bin/bash
# Check which unresolved issues from PR `#5395` are present in the new PR `#5641` code

echo "=== Check for .bak files ==="
find usermods/FSEQ -name "*.bak" 2>/dev/null

echo ""
echo "=== Check channel_count handling in fseq_player.cpp (truncation bug) ==="
rg -n "channel_count|packetLength|bytes_remaining" usermods/FSEQ/fseq_player.cpp -A2 -B1 | head -60

echo ""
echo "=== Check for VLA (variable-length array) in fseq_player.cpp ==="
rg -n "frame_data\[" usermods/FSEQ/fseq_player.cpp -A2 -B2 | head -30

echo ""
echo "=== Check clearPlaybackState / clearLastPlayback guard ==="
rg -n "clearPlaybackState\|clearLastPlayback\|clearSegmentPlayback\|playbackLedStop\|playback_start" usermods/FSEQ/fseq_player.cpp -A5 -B2 | head -80

echo ""
echo "=== Check now = millis() seeding before first frame ==="
rg -n "millis\(\)\|loadRecording\|scheduleNextFrame\|now =" usermods/FSEQ/fseq_player.cpp -A2 -B1 | head -60

echo ""
echo "=== Check loop button state in web_ui_manager.cpp ==="
rg -n "loopBtn\|playBtn\|dataset.state\|btn-stop\|startloop" usermods/FSEQ/web_ui_manager.cpp -A3 -B2 | head -60

echo ""
echo "=== Check README endpoint descriptions ==="
rg -n "api/sd/list\|api/fseq/list\|JSON\|json\|HTML\|html" usermods/FSEQ/README.md -B1 -A2 | head -40

Length of output: 3118


🏁 Script executed:

#!/bin/bash
echo "=== clearPlaybackState full implementation ==="
rg -n "clearPlaybackState" usermods/FSEQ/fseq_player.cpp -A 20 | head -50

echo ""
echo "=== PlaybackState initialization / default values ==="
rg -n "PlaybackState\|frame_count\|frame_data_offset\|loop\b" usermods/FSEQ/fseq_player.h -A2 -B1 | head -60

echo ""
echo "=== scheduleNextFrame and timing initialization ==="
rg -n "scheduleNextFrame\|next_frame_time\|millis\(\)\|now\b" usermods/FSEQ/fseq_player.cpp -A3 -B1 | head -80

echo ""
echo "=== loadRecordingIntoState - check millis seeding before first playNextFrame call ==="
rg -n "loadRecordingIntoState\|playNextRecordingFrame\|loadRecordingForSegment\|loadRecording\b" usermods/FSEQ/fseq_player.cpp -A3 -B1 | head -80

echo ""
echo "=== Loop button in embedded HTML (search for loopBtn/startloop in web_ui_manager.cpp) ==="
rg -n "loopBtn\|startloop\|loop.*btn\|btn.*loop\|dataset" usermods/FSEQ/web_ui_manager.cpp | head -30

echo ""
echo "=== Check README for list endpoint documentation ==="
grep -n "list\|fseq\|sd\|JSON\|endpoint\|API" usermods/FSEQ/README.md | head -40

echo ""
echo "=== Check if `#5641` still has double-playback concern (handlePlayRecording / renderSegmentFrame called from both usermods) ==="
rg -n "renderSegmentFrame\|renderRealtimeFrame\|loop\(\)" usermods/FSEQ/usermod_fseq.h usermods/FSEQ/usermod_fpp.h -A5 | head -60

echo ""
echo "=== Check processFrameDataForSegment off-by-one (>= vs >) ==="
rg -n "index.*lastLed\|lastLed\|>=\|> lastLed\|>= lastLed" usermods/FSEQ/fseq_player.cpp -B2 -A2 | head -40

Length of output: 4399


@softhack007 — here is a full cross-reference of the pending items from PR #5395 against the current PR #5641.


Issues from PR #5395 → ✅ Resolved / superseded in #5641

The architectural rewrite eliminates or supersedes most of the bugs fixed in #5395's late commits. The following were verified directly against the new code:

#5395 finding Status in #5641
web_ui_manager.bak backup file committed Removed — no .bak files present
channel_count (uint32_t) silently truncated to uint16_t packetLength Fixedconst uint32_t packetLength = state.file_header.channel_count throughout
VLA char frame_data[buffer_size] (non-standard C++) Gone — replaced by gFseqFrameBuffer shared static buffer
Duplicate usermod IDs (both returning USERMOD_ID_SD_CARD) FixedUSERMOD_ID_FSEQ = 59, USERMOD_ID_FPP = 60
Double-playback: both UsermodFseq::loop() and UsermodFPP::loop() calling frame render FixedUsermodFseq::loop() is intentionally empty; rendering is driven by the effect engine
Off-by-one ++index > lastLed in pixel write loop Fixed — new code uses if (++index >= maxLeds) break
clearLastPlayback() iterating 65 535 LEDs when playbackLedStop == uint16_t(-1) Obsolete — new per-segment clearPlaybackState(state) resets struct fields, no LED iteration
README /api/sd/list and /api/fseq/list incorrectly described as HTML pages Fixed — new README explicitly documents both as JSON endpoints
Loop button updating playBtn state instead of loopBtn Obsolete — completely new Web UI implementation

Issues from PR #5395⚠️ Still applicable in #5641

1. Dead parameter in skipRemainingFrameData (minor)

fseq_player.cpp lines 198–208:

bool FSEQPlayer::skipRemainingFrameData(PlaybackState &state, uint32_t bytesToSkip) {
  if (bytesToSkip == 0) return true;
  (void)bytesToSkip;   // ← parameter is discarded immediately

  const uint32_t newOffset = state.frame_data_offset + state.file_header.channel_count;
  ...

The bytesToSkip parameter is accepted but never used — the seek always jumps one full channel_count forward from frame_data_offset. This is functionally correct in today's callers (they always want to skip to the next frame boundary), but the misleading signature is dead code worth cleaning up.

2. Playback start-time seeding before the first frame (from April 17 review of #5395)

scheduleNextFrame computes state.next_time = state.playback_start_time + ((state.frame + 1U) * step). This is a cleaner design than the old now-based approach, but it still requires playback_start_time to be seeded with a fresh millis() value inside loadRecordingIntoState before the first scheduleNextFrame call. This should be verified; if the elapsed-seconds anchor path (secondsElapsed > 0) seeds it correctly but the zero-start path does not call millis(), the first frame will have stale timing.

3. Architectural recommendations from @willmmiles (PR #5395 discussion)

The discussion between @willmmiles, @DedeHai, and @netmindz reached a consensus direction:

  • FPP override should honour useMainSegmentOnly and realtimeRespectLedMaps
  • A future "live-mode preset slot" hook would let users customise entry/exit behaviour (brightness, transitions, segment assignments)
  • Today's implementation overrides segments directly without consulting those settings

This is not a blocker for this PR but is worth calling out as a known gap for a follow-up.


New issues visible only in #5641 (not present in #5395)

One new concern introduced by the rewrite is worth flagging:

gFseqFrameBuffer is a shared static buffer

processFrameDataForSegment reads file data into gFseqFrameBuffer and then copies it into the segment. If WLED ever calls the FSEQ effect for two different segments in the same render pass (which is allowed when multiple segments share the same effect mode), both calls will use the same buffer. Segment A's tail end could be overwritten by Segment B's first read before the pixel writes complete. This is a potential data-corruption issue for multi-segment FSEQ setups, which is one of the headline features of this PR.


Summary for the reviewer: the most actionable items still pending are the skipRemainingFrameData dead-parameter cleanup, the playback_start_time seeding verification, and the gFseqFrameBuffer shared-buffer safety for multi-segment use. Everything critical from the #5395 review has been resolved or superseded by the rewrite.

@ElToberino
Copy link
Copy Markdown

What's the difference to the existing FSEQ PRs ? Like

This is a user module designed to meet all expectations.

tpm2 still missing as I see.
Integrating this would be nice, as it is still widely used and working well.
Recording tpm2 files via Jinx! is very simple and it would be a great step forward if WLED was able to play these files. There are even tools available for editing/cutting these recorderd files.

The former attempts of integrating tpm2 playback did not work with larger numbers of leds (which is the standard case for a matrix use case).

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

Labels

connectivity Issue regarding protocols, WiFi connection or availability of interfaces enhancement usermod usermod related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants