Skip to content

playback of .tpm2#2876

Draft
constant-flow wants to merge 2 commits intowled:mainfrom
constant-flow:pr/tpm2-playback
Draft

playback of .tpm2#2876
constant-flow wants to merge 2 commits intowled:mainfrom
constant-flow:pr/tpm2-playback

Conversation

@constant-flow
Copy link
Copy Markdown
Contributor

@constant-flow constant-flow commented Nov 9, 2022

Enable playback of animation recordings via presets (tpm2 right now, extensible to other formats).
Animations can be stored in flash /edit or on SD card (see other upcoming PR)

Summary by CodeRabbit

  • New Features

    • Playback Recordings usermod enables playback of previously recorded LED animation sequences stored on LittleFS or optional SD card storage
    • Configure playback behavior including repeat options and frame rate overrides via preset API commands with flexible segment targeting
  • Documentation

    • Added comprehensive setup and configuration guide for the Playback Recordings usermod

@ElToberino
Copy link
Copy Markdown

ElToberino commented Nov 11, 2022

@constant-flow:
Thanks for your work. Everything is running as intendend!

I have a question/suggestion concerning the 2D mode coming up with V0.14:
If I define a matrix (let's say WxH -> 30x5 eg.) in the settings, the other realtime modes (tpm2.net, artnet etc.) seem to handle the segment handling in another way: Entering the realtime mode, the whole matrix plays the data as it is shown and sent out in Jinx!.
Running your usermod in WLED 2D-mode, the recorded data is only played in the first row of the matrix (30 Leds) and so the animation is not played correctly. Redefining the WLED settings to 1D (150 Leds) everything works fine.

So my question:
Would it make sense to make your usermod handle the tpm2 data as the other realtime modes do?
I read your intention was to make the file playable on a certain segment, which is also a nice feature.
Perhaps it would be possible to make these two modes optional with via the json setting?

@blazoncek
Copy link
Copy Markdown
Contributor

It uses setRealtimePixel() so everything should be ok.
setRealtimePixel() is not aware of 2D setup or segments (beyond main segment).

@ElToberino
Copy link
Copy Markdown

I just can tell you what I noticed:
With a 2D-Setup (30x5) the file is only played in the first row. Changing the setup to 1D with 150 Leds, everything works as it should. All other realtime modes don't show this behaviour and work fine with 2D-Setup.

@martenbiehl
Copy link
Copy Markdown

Is the 2d setup something that prohibits this user mod from being merged? I would really like to use it. Especially the segments feature is important for my use case.

@ElToberino
Copy link
Copy Markdown

ElToberino commented Nov 17, 2022

I've just tested your mod with my matrix and faced an issue:
My matrix has a size of 39x22 Leds; these 858 Leds are driven on pin 16 and pin 4 of my esp32. (Each pin drives 429 Leds).
Playing the tpm2 files (from flash and from SD) I noticed that the animation is played much slower than it should be. The files are recorded with 25 fps. Playing with the default setting of 25 fps they are played far too slow. Increasing the fps rate via Json seems to help, but I can never reach the proper speed. I tried many different fps rates, but the maximum playback speed I could reach was about 68% of the intended speed.

So I wonder if this a limitation of the ESP32 or WLED or if there is a code issue. (My tpm2 records are fine; I also habe tested it with another tpm2 player where everything is running properly.)

I tested it with 0.13.3 and 0.14.

@constant-flow
Copy link
Copy Markdown
Contributor Author

So far the implementation is rather naïve and doesn't do any kind of double/triple buffering. Loading data for so many LEDs is something I didn't test and cannot test (no hardware at hand). All data is read just-in-time from the file. Did you load the data from SD or from flash, I bet SD is slower than the Flash as well. I think if required the performance can be boosted, but at some point there will be definitely a limit by the hardware itself to playback that fast, latest when the animation doesn't fit into memory or when it's too many LEDs to get the info for.

@constant-flow
Copy link
Copy Markdown
Contributor Author

@ElToberino concerning the 2D setup. I located the origin of the issue. My code relies on the stop attribute of a segment, which in fact is stopX. So I try to find out if there is a clever way (maybe already implemented way) to support 2D and segments at the same time.

@ElToberino
Copy link
Copy Markdown

@constant-flow: Thanks for investigating the 2D setup!
Concerning the slow playback speed I also think this is caused by the large number of 858 leds though - in principle - the ESP32 should be able to handle this number easily without any problems. This issue also occurs when reading the file directly from ESP's flash memory, so it is not sd card related. As you said, it really could be caused by missing buffering but, unfortunately, I don't have sufficient skills yet to implement and test a buffering.
Thanks for your work and your feedback!

@blazoncek
Copy link
Copy Markdown
Contributor

Try adjusting WLED FPS in LED settings (advanced part). Start with 25FPS.

@ElToberino
Copy link
Copy Markdown

@blazoncek:
Thanks, but that's what I've already done.

@constant-flow
Copy link
Copy Markdown
Contributor Author

@ElToberino you may want to try the development branch of this PR I just commited a change that hopefully fixes the 2D Segments issue. More than welcome to get feedback if it works for you. I will start to look into buffering and timing today.

@constant-flow constant-flow marked this pull request as draft November 22, 2022 19:37
@ElToberino
Copy link
Copy Markdown

@constant-flow:
Thank you - that sounds great. I'll test the 2D-fix this evening and will report.

@ElToberino
Copy link
Copy Markdown

@constant-flow:
I've just tested your changed code - it really works fine with 2D setup now. Thanks a lot for your work!

@softhack007 softhack007 added the usermod usermod related label Jan 7, 2023
@ElToberino
Copy link
Copy Markdown

ElToberino commented Jan 26, 2023

Hi, @constant-flow!
I don't know if you want/need this feedback, but I tried the current version (development branch - I know it's marked as WIP) of your usermode with the current 0.14 beta but didn't get it running.

The code of this draft in here also works fine with the current 0.14 beta (of course showing the issue of far too slow speed with my 858 leds.)

Best regards!

@stale
Copy link
Copy Markdown

stale Bot commented Jun 18, 2023

Hey! This issue has been open for quite some time without any new comments now. It will be closed automatically in a week if no further activity occurs.
Thank you for using WLED!

@stale stale Bot added the stale This issue will be closed soon because of prolonged inactivity label Jun 18, 2023
@ElToberino
Copy link
Copy Markdown

It's not stale.
I still hope it's going on in here...

@stale stale Bot removed the stale This issue will be closed soon because of prolonged inactivity label Jun 21, 2023
@constant-flow
Copy link
Copy Markdown
Contributor Author

@ElToberino do you remember what you meant with your post.

From what i read it is (referencing this commit graph), you mean commit 0df38e67 on pr/tpm2-playback was working and you tested 7ab6cc37 (added buffering (WIP, not tested)) on feature/tpm2-playback and it didn't work?

Can you recall what specifically didn't work? Not compiling, No playback, Wrong speed, Wrong display, or simply still too slow?

image

Looking at the graph, it seems like I branched off to far off from the working state. Merging the buffering part into the functional version creates way to many conflicts. So probably a rewrite is required. Hooray 😞

@constant-flow
Copy link
Copy Markdown
Contributor Author

constant-flow commented Jul 29, 2023

@blazoncek @Aircoookie @ElToberino

OK, I was able to manually merge the changes of my buffering code (7ab6cc3) and the code of this PR (0df38e6).
You can find it in my fork pr/tpm2-playback-dev.

I literally didn't do any changes to the code other than the changes from the branches.
✅ Playing back TPM2 from flash memory does work for me.

🫵 How to test (please do 🙇 )

Get my code from here and make sure you are on the tpm2-playback-dev branch

Build and Deploy

Select and build the [env:esp32dev_playback] build env and adjust your upload mode.

You have three option (you probably know 🤓 but I add it for completeness)

  1. OTA: Adjust the IP if you use OTA
  2. No-OTA: uncomment the upload_protocol & upload_port lines in this env and flash via USB.
  3. DIY: Add the buildflag -D USERMOD_PLAYBACK_RECORDINGS to your env. (You don't need to switch to env:esp32dev_playback here)

Store playback file

So please test this first you need to upload a TPM2 (e.g. record.tpm2) file via the edit route, for me http://192.168.178.42/edit

... after uploading it should look similar to this:

image

Create preset

Then setup a preset, according to your filename:

Example preset:

{"playback":{"file":"/record.tpm2","fps":10}}

Hit Save

image

Play recording

Click the preset, to play it

Please reply

if this works for you, only then, i'd check how much work FSEQ would be to implement.
I guess I'd then also create a new PR.

@blazoncek
Copy link
Copy Markdown
Contributor

you need to upload a TPM2 (e.g. record.tpm2)

@constant-flow can you provide a sample TPM2 file? I have none.

@constant-flow
Copy link
Copy Markdown
Contributor Author

constant-flow commented Jul 30, 2023

Here is a simple file living on the discord.

Please try with a segment defined, as it relies on the segments

@blazoncek
Copy link
Copy Markdown
Contributor

@constant-flow I've checked your fork, tpm2 branch and although the code is clean and well written I would recommend different approach.

Please exercise a new possibility in 0.14 to create custom effects and use that to:

  • display recording (it will automatically use segment)
  • use segment name for recording file (user can easily switch recordings, no special fiddling with presets)
  • use SEGENV.call==0 as a trigger to load recording

I would also like to see in the code:

  • add enable/disable config item

I do not know anything about TPM2 but was wondering if it does not have FPS field? Does it?

@blazoncek
Copy link
Copy Markdown
Contributor

Also tested the usermod and more often than not I get a crash. Sometimes it will play something. :)
playback_crash.txt

@ElToberino
Copy link
Copy Markdown

ElToberino commented Aug 6, 2023

Hi, @constant-flow,
I've just seen you're back - this is really nice.
I'll try to find some time this week to test your fork and I will report after that.

@ElToberino
Copy link
Copy Markdown

Having compiled and tested your code I have to report some problems unfortunately:
With my 768 leds matrix (96x8) I always got a crash when calling the playback-preset. Reducing the number of leds to 512 (64x8) I faced the same malfunction.
So I reduced again to only 256 leds (32x8) - and the playback worked as intended.

Additional setup information: Tested with two different files recorded with Jinx! (size: 508 kb and 86 kb). WLED configured as a 1-D-strip, matrix patching was done in Jinx!).

As it was before in your first PR (before buffering was added) there seem to be problems with a larger number of leds, though 768 leds should be no problem for an esp32 (with tpm2.net via Wifi everything works fine.)

@blazoncek blazoncek added the keep This issue will never become stale/closed automatically label Apr 4, 2024
@softhack007
Copy link
Copy Markdown
Member

@coderabbitai please review.

Additionally we need a few summary lists:

  • noteworthy good practices in this PR
  • a prioritized summary of suggested improvements
  • list of related PR (open or closed) that also tried to add fseq playback or SDCard support
  • major changes to make this PR compatible with the current code in main

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 13, 2026

🧩 Analysis chain

I'll gather the PR code, review the implementation, and search for related PRs before generating my full response.


🏁 Script executed:

#!/bin/bash
# Get PR details
gh pr view 2876 --repo wled/WLED --json title,body,files,baseRefName,headRefName,state,createdAt,updatedAt

# List files changed
gh pr diff 2876 --repo wled/WLED --name-only

Length of output: 894


🏁 Script executed:

#!/bin/bash
# Search for related PRs about fseq, SD card, tpm2 playback
gh pr list --repo wled/WLED --state all --search "fseq" --json number,title,state,createdAt,url | head -50
gh pr list --repo wled/WLED --state all --search "tpm2" --json number,title,state,createdAt,url | head -50
gh pr list --repo wled/WLED --state all --search "sdcard playback" --json number,title,state,createdAt,url | head -20
gh pr list --repo wled/WLED --state all --search "SD card animation" --json number,title,state,createdAt,url | head -20

Length of output: 6317


🏁 Script executed:

#!/bin/bash
# Get the full diff of the PR
gh pr diff 2876 --repo wled/WLED

Length of output: 19950


🏁 Script executed:

#!/bin/bash
# Check current API in main for relevant functions
rg -n "setRealtimePixel" --type=cpp --type=h -C2
echo "---"
rg -n "getSegment" --type=cpp --type=h -C2 | head -80
echo "---"
rg -n "realtimeLock\|exitRealtime\|REALTIME_MODE_GENERIC\|REALTIME_MODE_PLAYBACK" --type=cpp --type=h | head -40
echo "---"
# Check ArduinoJson version / JsonInteger
rg -n "JsonInteger\|JsonFloat\b" --type=cpp --type=h | head -20
echo "---"
# Check Segment.start / sg.stop usage
rg -n "\.stop\b\|\.start\b" wled00/FX.h | head -30

Length of output: 11768


🏁 Script executed:

#!/bin/bash
rg -n "realtimeLock\|exitRealtime\|REALTIME_MODE_GENERIC\|REALTIME_MODE_PLAYBACK\|realtimeMode\b" --type=h --type=cpp | head -50
echo "==="
rg -n "JsonInteger\|JsonFloat\b\|is<float>\|is<int>" --type=h --type=cpp | head -20
echo "==="
# Check Segment members start/stop
rg -nP "uint16_t start|uint16_t stop|\.start\b|\.stop\b" wled00/FX.h | head -30
echo "==="
# ArduinoJson version
rg -rn "ARDUINOJSON_VERSION\|ArduinoJson" --include="*.json" --include="*.txt" --include="*.ini" | head -10
echo "==="
# check strip.getSegment signature
rg -n "getSegment" wled00/FX.h wled00/FX_fcn.cpp | head -20

Length of output: 1548


🏁 Script executed:

#!/bin/bash
# Check for realtimeLock, exitRealtime, realtimeMode - broader search
rg -n "realtimeLock\|exitRealtime\|realtimeMode\|realtimeOverride\|realtimeTimeoutMs\|REALTIME_MODE" | head -60
echo "==="
# Check getSegment with -1/out of range behavior
rg -n -A10 "Segment& WS2812FX::getSegment" wled00/FX_fcn.cpp
echo "==="
# check strip.show() call validity from usermod
rg -n "strip\.show()" | head -20
echo "==="
# Check ArduinoJson version in library files
rg -rn "ARDUINOJSON_VERSION" | head -10

Length of output: 1752


🏁 Script executed:

#!/bin/bash
rg -n -e "realtimeLock" -e "exitRealtime" -e "realtimeTimeoutMs" | head -30
echo "==="
rg -n -e "REALTIME_MODE_GENERIC" -e "REALTIME_MODE_PLAYBACK" | head -20
echo "==="
# Check if realtimeMode and realtimeOverride are global variables
rg -n -e "realtimeMode" -e "realtimeOverride" wled00/ | head -30
echo "==="
# Check JsonFloat/JsonInteger in v6 header
rg -n "JsonFloat\|JsonInteger" wled00/src/dependencies/json/ArduinoJson-v6.h | head -10
echo "==="
# Check Segment stopY member for 2D support
rg -n "stopY\|startY" wled00/FX.h | head -20

Length of output: 5467


🏁 Script executed:

#!/bin/bash
# Check Segment struct for startY/stopY members
rg -n "startY\|stopY" wled00/FX.h | head -20
echo "==="
# Check JsonInteger/JsonFloat availability in ArduinoJson v6.18.1
rg -n "JsonInteger\|JsonFloat\|ARDUINOJSON_INTEGER\|ARDUINOJSON_FLOAT" wled00/src/dependencies/json/ArduinoJson-v6.h | head -20
echo "==="
# Verify const.h REALTIME_MODE constants
rg -n "REALTIME_MODE\|REALTIME_OVERRIDE" wled00/const.h | head -20

Length of output: 7


🏁 Script executed:

#!/bin/bash
# Check Segment struct members around lines 320-380
sed -n '310,400p' wled00/FX.h
echo "==="
# Check REALTIME constants in const.h
sed -n '125,150p' wled00/const.h

Length of output: 5042


@softhack007 — full review below.


PR #2876playback of .tpm2 Review

✅ Noteworthy Good Practices

  1. Clean layer separation — format-agnostic file_* layer cleanly decouples from the TPM2-specific tpm2_* layer. Adding FSEQ or other formats only requires adding a case to the switch.
  2. Extensible format enumCOUNT_PLAYBACK_FORMATS sentinel makes the format array bounds self-maintaining.
  3. Overflow-safe frame timingmillis() - lastFrame < msFrameDelay is correctly overflow-safe.
  4. Full TPM2 packet-type dispatch — command / response / data / unknown packet types are all handled per the TPM2 spec, with graceful skipping for unimplemented types.
  5. Sub-1 FPS slideshow mode — accepting fps as a float (e.g. 0.5) is a thoughtful touch.
  6. Flexible repeat control — accepting both bool (loop forever) and int (repeat count) in JSON is user-friendly.
  7. SD-over-Flash priority#ifdef SD_ADAPTER guard prefers SD when available, clean and correct.
  8. PROGMEM class nameconst char PlaybackRecordings::_name[] PROGMEM is correct.
  9. Good README — clear setup instructions, worked examples for all options, SD-card notes.
  10. Forward declarations — all free functions are declared before the class, avoiding circular dependency issues.

🔴 Prioritized Suggested Improvements

Priority 1 — Will not compile / runtime crash

is<JsonInteger>() / is<JsonFloat>() don't exist in ArduinoJson v6

The bundled ArduinoJson is v6.18.1 (wled00/src/dependencies/json/ArduinoJson-v6.h). These type aliases were removed in v6; the correct calls are is<int>() and is<double>():

// BEFORE (won't compile with ArduinoJson v6):
if (jsonPlaybackRepeats.is<JsonInteger>()) { ... }
if (jsonPlaybackFps.is<JsonInteger>() || jsonPlaybackFps.is<JsonFloat>()) { ... }

// AFTER:
if (jsonPlaybackRepeats.is<int>()) { ... }
if (jsonPlaybackFps.is<int>() || jsonPlaybackFps.is<double>()) { ... }

Priority 2 — Global mutable state at file scope

Every non-constant variable (currentPlaybackFormat, playbackRepeats, msFrameDelay, playbackFile, playbackLedStart, playbackLedStop, colorData, colorChannels, lastFrame) is declared at file scope outside the class. These should be private members of PlaybackRecordings. Currently, if the header is ever included from multiple translation units (or a second usermod instantiated), all state is shared/duplicated unpredictably.


Priority 3 — playbackFile.available() misused as "is file open?" check

File::available() returns the number of bytes remaining — it returns 0 at EOF even when the file is still open. This means file_loadPlayback() will fail to close a file that has just finished playing, and may open multiple file handles:

// BEFORE (broken at EOF):
if (playbackFile.available()) { playbackFile.close(); }

// AFTER: use a dedicated flag
bool _isPlaying = false;
// ...
if (_isPlaying) { playbackFile.close(); _isPlaying = false; }

Priority 4 — Segment copied by value; incorrect uint8_t narrowing

strip.getSegment() returns Segment&. The PR copies it by value (expensive — Segment is 72 bytes plus heap pointers) and passes id = -1 (an int) to a uint8_t parameter, where -1 silently wraps to 255, then falls back to the main segment. The intent (main segment fallback) is correct but relies on undefined narrowing:

// BEFORE:
Segment sg = strip.getSegment(id);   // value copy, int→uint8_t narrowing

// AFTER:
uint8_t segId = (id < 0) ? strip.getMainSegmentId() : (uint8_t)id;
Segment& sg = strip.getSegment(segId);

Priority 5 — No 2D support (confirmed root cause)

sg.stop is the X-axis stop only (// stop X coordinate 2D (right)). The Segment struct also has uint8_t startY / uint8_t stopY. Playing a recording linearly from sg.start to sg.stop skips all rows beyond row 0. Full 2D support requires mapping the packet's linear pixel index to (x, y) using the segment's 2D geometry. This is the confirmed bug reported by @ElToberino.


Priority 6 — Byte-by-byte file reads (performance bottleneck)

tpm2_GetNextColorData() calls playbackFile.read() three times per pixel. For 858 LEDs that's ~2574 individual FS reads per frame — each a syscall with seek tracking overhead. This is the likely cause of the ~68% speed limit reported. A small read buffer (e.g. uint8_t buf[64*3] per read(buf, n) call) would reduce syscall count dramatically.


Priority 7 — file_checkRealtimeOverride() incorrectly manipulates realtimeOverride

The function transitions REALTIME_OVERRIDE_ALWAYS → REALTIME_OVERRIDE_ONCE, then on the next loop call uses OVERRIDE_ONCE as the exit trigger. This conflicts with WLED's own use of REALTIME_OVERRIDE_ONCE (which exitRealtime() already clears to NONE). If a user has set OVERRIDE_ONCE for any other reason the playback will incorrectly stop. The correct approach is to detect that realtimeMode has been set to REALTIME_MODE_INACTIVE externally (meaning someone else called exitRealtime()), and stop playback at that point.


Priority 8 — String class members (heap fragmentation)

String jsonKeyPlayback = "playback";
String jsonKeyFilePath = "file";
// ... 6 more String members

Each String on ESP8266/ESP32 is a heap allocation. Use static constexpr char arrays (ideally PROGMEM) instead:

static constexpr char _jsonKeyPlayback[]   PROGMEM = "playback";
static constexpr char _jsonKeyFilePath[]   PROGMEM = "file";

Priority 9 — playback_formats[] second entry is spaces (latent bug)

static const String playback_formats[] = {"tpm2",/*, "fseq"*/"   "};

The " " (three spaces) is left as a placeholder for the commented-out FSEQ entry. Any filename ending in three spaces would incorrectly be detected as FSEQ format. Replace with "" or remove the entry entirely until FSEQ is implemented.


Priority 10 — Dead variable and dead function

  • colorChannels is set to 3 and never read or modified anywhere.
  • tpm2_printWholePlayback() is a debug utility that is never called from the class and would consume the entire file on first use. Gate it with #ifdef WLED_DEBUG or remove it.

Priority 11 — Missing addToConfig() / addToJsonInfo()

There is no enable/disable toggle and no status visible in the WLED UI (e.g. "currently playing: record.tpm2, frame 42/120"). As @blazoncek noted, these are expected for a mainline-ready usermod.


🔗 Related PRs (FSEQ / SD-card playback / TPM2)

# Title State Notes
#2292 TPM2 Playback from LittleFS and SD CLOSED Predecessor to this PR
#2413 FSEQ Playback from LittleFS and SD OPEN Direct FSEQ counterpart, open since 2021
#3551 FSEQ Playback from LittleFS and SD CLOSED Rework of #2413, closed 2023
#4570 added support for fseq files OPEN Another FSEQ attempt, open since 2025
#5395 Add FSEQ usermod (SD playback + FPP) OPEN Most recent FSEQ usermod, includes FPP support
#5417 FSEQ Usermod: integrate sd_card usermod + compressed xLights CLOSED Extension of #5395, closed 2026
#3835 Image effect (GIF support) MERGED Solved a closely analogous problem (file → LEDs) via the custom effects API
#5040 Improved 1D support for GIF images MERGED Follow-up to #3835

The merged GIF effect PRs (#3835, #5040) are architecturally the most relevant reference — they solved the same "file → per-frame LED data" problem using the custom effects API (with SEGENV.call == 0 init and segment name as filename), which @blazoncek also recommended for this PR.


🔧 Major Changes Required for Compatibility with Current main

  1. Fix is<JsonInteger>()/is<JsonFloat>()is<int>()/is<double>()compile blocker.

  2. Move all file-scope globals into PlaybackRecordings private members — the class currently has no state of its own; everything is file-scope globals which will cause ODR violations and make the usermod non-reusable.

  3. Fix playbackFile.available() as open-check — replace with a bool _isPlaying member.

  4. Fix Segment copy + intuint8_t narrowing — use Segment& sg = strip.getSegment(segId) with explicit main-segment fallback.

  5. 2D support — iterate sg.startY..sg.stopY rows and sg.start..sg.stop columns, mapping the linear TPM2 pixel index to (x, y) via the segment geometry. The XY() helper in FX.h can be used for this.

  6. Buffered file reads — replace per-pixel read() calls with playbackFile.read(buf, count) to reduce I/O overhead for large setups.

  7. Consider implementing as a custom effect (as @blazoncek suggested) — this would automatically give segment support, UI integration via addToConfig/addToJsonInfo, and correct integration with the effects pipeline, following the pattern established by the GIF effect (#3835). The realtime API approach means playback fights with the normal effects pipeline and requires manual strip.show() calls.


🧠 Learnings used
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: 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: 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: 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: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-12-28T14:06:48.772Z
Learning: In WLED post-commit ee9ac947, the rendering pipeline uses per-segment buffers and per-pixel bus updates. Unmapped (0xFFFF) mapping entries are now skipped in WS2812FX::show() (no “clear to black”), which can leave physical gap LEDs with stale/random colors unless they are explicitly cleared. This is a behavior change from pre-0.16 where a full physical buffer was effectively refreshed each frame.

Learnt from: softhack007
Repo: wled/WLED PR: 5443
File: wled00/FX_fcn.cpp:1277-1277
Timestamp: 2026-03-24T12:13:21.670Z
Learning: In WLED's `WS2812FX::service()` (wled00/FX_fcn.cpp), `seg.freeze` means "do not run the effect function (_mode[seg.mode]())" — it does NOT mean "skip show()". A frozen segment's pixel buffer can still be updated externally (e.g., realtime control or single-pixel JSON API). `strip.trigger()` is the primary mechanism to flush those externally written pixels to the LEDs on the next service tick. Therefore, frozen segments must remain part of the `doShow`/`show()` path, and it is architecturally wrong to exclude frozen segments from `doShow`. Do not suggest skipping frozen segments from the show path in future reviews.

Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2026-03-25T12:25:11.610Z
Learning: In WLED's `Segment::startTransition()` (wled00/FX_fcn.cpp:284), when `isInTransition()` is true and `_t->_oldSegment` already exists, the function silently returns without updating `_t->_start` or `_t->_bri`. This causes a bug where rapid successive on/off toggles during a non-FADE blending transition (e.g., fairy dust) leave the transition clock stale: by the time of the second re-trigger, elapsed time may already exceed `_dur`, so `updateTransitionProgress()` sets `_progress = 0xFFFF` immediately on the next service tick and `stopTransition()` fires — the blending effect never plays. The fix is to always reset `_t->_start = millis()`, `_t->_dur = dur`, and `_t->_bri = currentBri()` (current visible brightness) in the `isInTransition()` branch, regardless of whether `_oldSegment` exists.

Learnt from: DedeHai
Repo: wled/WLED PR: 5464
File: wled00/FX_fcn.cpp:1699-1701
Timestamp: 2026-04-09T07:26:14.510Z
Learning: In WLED (wled00/util.cpp), `allocate_buffer()` processes `BFRALLOC_NOBYTEACCESS` in an `if` with an `else` that covers all other flags. On classic ESP32, this means `BFRALLOC_ENFORCE_PSRAM` and `BFRALLOC_PREFER_PSRAM` are silently ignored when `BFRALLOC_NOBYTEACCESS` is also set — IRAM (MALLOC_CAP_32BIT) is always tried first. Combining `BFRALLOC_CLEAR | BFRALLOC_NOBYTEACCESS` is unsafe: the allocator's internal `memset` at util.cpp:901 will perform byte-level access on a potentially IRAM-resident buffer. This affects `Segment::pixels` in `setGeometry()` (FX.h) and `_pixels` in `finalizeInit()` (FX_fcn.cpp). Additionally, `memcpy` on `Segment::pixels` in the copy constructor and copy assignment (FX_fcn.cpp lines ~73 and ~116) is also a byte-level violation when the buffer is in IRAM. The safe pattern is element-wise 32-bit access (for loops over `uint32_t*`) rather than `memset`/`memcpy` for all `BFRALLOC_NOBYTEACCESS` buffers.

Learnt from: softhack007
Repo: wled/WLED PR: 4838
File: platformio.ini:149-150
Timestamp: 2026-03-29T16:47:56.452Z
Learning: In WLED PR `#4838` (ESP-IDF V5 branch, platformio.ini): The original FastLED library has been replaced with a custom `fastled-slim` fork. As a result, the `[v5_pioarduino_workaround]` section (containing `lib_archive = yes`) and the commented-out `post:pio-scripts/fastled_cxx_workaround.py` extra script are intentional dead code pending removal after back-to-back testing. Do NOT flag `lib_archive = yes` in `[v5_pioarduino_workaround]` as unreferenced or suggest moving it to active env blocks — the entire section is slated for removal.
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 13, 2026

Walkthrough

This pull request introduces a new "Playback recordings" usermod for WLED that enables playback of LED animation recordings stored on LittleFS and optionally SD cards. The feature includes documentation, the usermod implementation with file handling and frame decoding logic, and build system integration.

Changes

Cohort / File(s) Summary
Documentation
usermods/playback_recordings/readme.md
Added comprehensive setup guide covering build flag configuration, optional SD-card support, preset creation, JSON configuration structure (with playback.file, repeat, fps, seg parameters), and SD/Flash file preference behavior.
Usermod Implementation
usermods/playback_recordings/usermod_playback_recordings.h
Implemented PlaybackRecordings usermod with JSON-driven configuration loading, TPM2 frame format parsing, file I/O for LittleFS and SD sources, frame-by-frame playback with configurable timing and repeat logic, and realtime pixel rendering via setRealtimePixel().
Build System Integration
wled00/const.h, wled00/usermods_list.cpp
Added USERMOD_ID_PLAYBACK_RECORDINGS identifier macro and conditional compilation directives to register the usermod when enabled.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client/API
    participant Preset as Preset System
    participant Usermod as PlaybackRecordings<br/>Usermod
    participant FileSystem as LittleFS/SD
    participant Strip as LED Strip

    Client->>Preset: Set preset with playback config
    Preset->>Usermod: readFromJsonState()
    Usermod->>Usermod: Parse JSON (file, repeat, fps, seg)
    Usermod->>FileSystem: Open recording file
    loop Playback Loop (realtimeMode = PLAYBACK)
        Usermod->>FileSystem: Read TPM2 frame packet
        FileSystem-->>Usermod: Frame data (RGB bytes)
        Usermod->>Usermod: Decode frame & write pixels
        Usermod->>Strip: setRealtimePixel() + show()
        alt Frame delay elapsed
            Usermod->>Usermod: Advance to next frame
        end
        alt End of file
            alt Repeat enabled
                Usermod->>FileSystem: seek(0)
            else Repeat exhausted/disabled
                Usermod->>FileSystem: Close file
                Usermod->>Usermod: Exit realtime mode
                Usermod->>Strip: Clear segment
            end
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Suggested labels

documentation, feature, usermod

Suggested reviewers

  • blazoncek
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'playback of .tpm2' is directly related to the main change: adding playback support for TPM2 format recordings via a new usermod.

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

✨ Finishing Touches
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch pr/tpm2-playback

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 the documentation documentation should be updated to explain behaviour label Apr 13, 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: 5

🧹 Nitpick comments (1)
usermods/playback_recordings/usermod_playback_recordings.h (1)

77-98: Move constant JSON keys and format names out of heap Strings.

These are fixed literals, but each instance allocates RAM for them anyway. In a usermod, this is easy to avoid with static const char[] PROGMEM, and it matches the project convention.

As per coding guidelines, usermods/**/*.{cpp,h,hpp}: Store repeated strings as static const char[] PROGMEM in usermods.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/playback_recordings/usermod_playback_recordings.h` around lines 77 -
98, Replace the heap-allocated String members (jsonKeyPlayback, jsonKeyFilePath,
jsonKeyPlaybackSegment, jsonKeyPlaybackSegmentId, jsonKeyPlaybackRepeats,
jsonKeyFramesPerSecond, formatTpm2) and the playback_formats array with
project-convention flash-stored literals: declare them as static const char[]
PROGMEM (and for the array use static const char* PROGMEM playback_formats[]).
Update all usages in PlaybackRecordings methods to read from PROGMEM (e.g., use
FPSTR/strcmp_P or construct a runtime String only when needed) so no RAM is
consumed by these constant keys/format names. Ensure names remain the same for
easy locating (jsonKeyPlayback, jsonKeyFilePath, jsonKeyPlaybackSegment,
jsonKeyPlaybackSegmentId, jsonKeyPlaybackRepeats, jsonKeyFramesPerSecond,
formatTpm2, playback_formats).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@usermods/playback_recordings/readme.md`:
- Around line 6-10: Change the README instructions to tell contributors to
update platformio_override.ini (not platformio.ini) and to add the USERMOD flag
to the configuration's build_flags there; update the example fenced snippet to
be labeled as ini and show adding -D USERMOD_PLAYBACK_RECORDINGS under
build_flags so readers and markdownlint are satisfied and WLED's rule about not
editing the main platformio.ini is followed.

In `@usermods/playback_recordings/usermod_playback_recordings.h`:
- Around line 66-77: Reset currentPlaybackFormat to
PLAYBACK_FORMAT::FORMAT_UNKNOWN before scanning a new filename and ensure the
format-lookup loop doesn't read past playback_formats; either make
COUNT_PLAYBACK_FORMATS match the actual number of elements in playback_formats
or, better, iterate using the array's real size (e.g.,
sizeof(playback_formats)/sizeof(playback_formats[0])) when checking entries.
Update the code paths that detect formats (references: currentPlaybackFormat,
playback_formats, PLAYBACK_FORMAT, COUNT_PLAYBACK_FORMATS) so detection always
starts from FORMAT_UNKNOWN and loop bounds are derived from the actual array
length to avoid out-of-bounds access.
- Around line 63-64: Define a distinct realtime mode constant for playback
instead of aliasing REALTIME_MODE_PLAYBACK to REALTIME_MODE_GENERIC; add a new
unique enum/define (e.g., REALTIME_MODE_PLAYBACK) in the same place where
realtime modes are declared and replace all usages that currently rely on the
alias—specifically update the definition (remove the `#define`
REALTIME_MODE_PLAYBACK REALTIME_MODE_GENERIC) and change callers like
file_handlePlayPlayback() and any realtimeLock(..., REALTIME_MODE_PLAYBACK)
invocations so they reference the new dedicated mode constant so playback
sessions are isolated from GENERIC realtime sessions.
- Around line 171-179: The code reads fps from jsonPlaybackFps and computes
newMsDelay without validating its value, which allows fps==0 or negative values
to cause divide-by-zero or huge wrap-around; update the jsonPlaybackFps handling
in the block that sets msFrameDelay (using symbols jsonPlaybackEntry,
jsonKeyFramesPerSecond, jsonPlaybackFps, fps, newMsDelay, PLAYBACK_FRAME_DELAY,
msFrameDelay) to reject non-positive fps (e.g. if fps <= 0) before computing
round(1000.f / fps), log a warning/debug message about the invalid fps, and
leave msFrameDelay at PLAYBACK_FRAME_DELAY (or previously set value) when the
value is invalid.
- Around line 405-416: The code treats packetLength as a pixel count but TPM2
packetLength is bytes; change tpm2_processFrameData to compute pixels =
packetLength / channels (where channels = 3 for RGB TPM2 frames), validate that
packetLength % channels == 0 (handle/log/ignore trailing bytes if not
divisible), then set lastLed = min(playbackLedStop, uint16_t(playbackLedStart +
pixels)) and loop pixels reading one pixel's worth of bytes per LED via
tpm2_GetNextColorData and calling setRealtimePixel; keep the final
tpm2_SkipUntilEndOfPacket() call. Ensure you reference packetLength, channels
(use a constant or literal 3), tpm2_GetNextColorData, setRealtimePixel,
playbackLedStart and playbackLedStop when making the change.

---

Nitpick comments:
In `@usermods/playback_recordings/usermod_playback_recordings.h`:
- Around line 77-98: Replace the heap-allocated String members (jsonKeyPlayback,
jsonKeyFilePath, jsonKeyPlaybackSegment, jsonKeyPlaybackSegmentId,
jsonKeyPlaybackRepeats, jsonKeyFramesPerSecond, formatTpm2) and the
playback_formats array with project-convention flash-stored literals: declare
them as static const char[] PROGMEM (and for the array use static const char*
PROGMEM playback_formats[]). Update all usages in PlaybackRecordings methods to
read from PROGMEM (e.g., use FPSTR/strcmp_P or construct a runtime String only
when needed) so no RAM is consumed by these constant keys/format names. Ensure
names remain the same for easy locating (jsonKeyPlayback, jsonKeyFilePath,
jsonKeyPlaybackSegment, jsonKeyPlaybackSegmentId, jsonKeyPlaybackRepeats,
jsonKeyFramesPerSecond, formatTpm2, playback_formats).
🪄 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: e7cdbf5c-ce74-432a-9c23-bdc417eb083e

📥 Commits

Reviewing files that changed from the base of the PR and between f104fb0 and 0df38e6.

📒 Files selected for processing (4)
  • usermods/playback_recordings/readme.md
  • usermods/playback_recordings/usermod_playback_recordings.h
  • wled00/const.h
  • wled00/usermods_list.cpp

Comment on lines +6 to +10
- modify `platformio.ini` and add to the `build_flags` of your configuration the following

```
-D USERMOD_PLAYBACK_RECORDINGS
```
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 | 🟡 Minor

Point builders to platformio_override.ini and label the snippet as ini.

For a regular usermod, documenting edits to the main platformio.ini sends contributors down a path WLED generally does not want, and the unlabeled fence here is already tripping markdownlint. Please switch the instructions to platformio_override.ini (or an equivalent override file) and mark the block as ini.

Based on learnings, in WLED, the general rule is that usermod PRs must NOT modify the main platformio.ini and should use platformio_override.ini instead.

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 8-8: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/playback_recordings/readme.md` around lines 6 - 10, Change the
README instructions to tell contributors to update platformio_override.ini (not
platformio.ini) and to add the USERMOD flag to the configuration's build_flags
there; update the example fenced snippet to be labeled as ini and show adding -D
USERMOD_PLAYBACK_RECORDINGS under build_flags so readers and markdownlint are
satisfied and WLED's rule about not editing the main platformio.ini is followed.

Comment on lines +63 to +64
//TODO: maybe add as custom RT_MODE in `const.h` and `json.cpp`
#define REALTIME_MODE_PLAYBACK REALTIME_MODE_GENERIC
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

Give playback its own realtime mode.

Aliasing playback to REALTIME_MODE_GENERIC means file_handlePlayPlayback() will also run during unrelated generic realtime sessions, and realtimeLock(..., REALTIME_MODE_PLAYBACK) cannot distinguish playback from those sources. That makes cross-feature interference unavoidable here.

Also applies to: 335-342, 418-420

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/playback_recordings/usermod_playback_recordings.h` around lines 63 -
64, Define a distinct realtime mode constant for playback instead of aliasing
REALTIME_MODE_PLAYBACK to REALTIME_MODE_GENERIC; add a new unique enum/define
(e.g., REALTIME_MODE_PLAYBACK) in the same place where realtime modes are
declared and replace all usages that currently rely on the alias—specifically
update the definition (remove the `#define` REALTIME_MODE_PLAYBACK
REALTIME_MODE_GENERIC) and change callers like file_handlePlayPlayback() and any
realtimeLock(..., REALTIME_MODE_PLAYBACK) invocations so they reference the new
dedicated mode constant so playback sessions are isolated from GENERIC realtime
sessions.

Comment on lines +66 to +77
enum PLAYBACK_FORMAT {
TPM2=0,
FSEQ,
FORMAT_UNKNOWN,
COUNT_PLAYBACK_FORMATS
};

enum PLAYBACK_FORMAT currentPlaybackFormat = PLAYBACK_FORMAT::FORMAT_UNKNOWN;

int32_t playbackRepeats = PLAYBACK_REPEAT_LOOP;
uint32_t msFrameDelay = PLAYBACK_FRAME_DELAY; // time between frames
static const String playback_formats[] = {"tpm2",/*, "fseq"*/" "};
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

Reset format detection and stop iterating past playback_formats.

currentPlaybackFormat is never reset before scanning a new file name, so an unsupported extension after a previous TPM2 playback will keep the old format. On top of that, COUNT_PLAYBACK_FORMATS is 3 while playback_formats only has 2 elements, so the last iteration reads past the array.

🐛 Proposed fix
-    for(int i=0; i<PLAYBACK_FORMAT::COUNT_PLAYBACK_FORMATS; i++){
-      if(pathToPlayback.endsWith(playback_formats[i])) {
-        currentPlaybackFormat = (PLAYBACK_FORMAT) i;
+    currentPlaybackFormat = PLAYBACK_FORMAT::FORMAT_UNKNOWN;
+    for (uint8_t i = 0; i < (sizeof(playback_formats) / sizeof(playback_formats[0])); i++) {
+      if (pathToPlayback.endsWith(playback_formats[i])) {
+        currentPlaybackFormat = static_cast<PLAYBACK_FORMAT>(i);
         break;
       }
     }

Also applies to: 146-152

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/playback_recordings/usermod_playback_recordings.h` around lines 66 -
77, Reset currentPlaybackFormat to PLAYBACK_FORMAT::FORMAT_UNKNOWN before
scanning a new filename and ensure the format-lookup loop doesn't read past
playback_formats; either make COUNT_PLAYBACK_FORMATS match the actual number of
elements in playback_formats or, better, iterate using the array's real size
(e.g., sizeof(playback_formats)/sizeof(playback_formats[0])) when checking
entries. Update the code paths that detect formats (references:
currentPlaybackFormat, playback_formats, PLAYBACK_FORMAT,
COUNT_PLAYBACK_FORMATS) so detection always starts from FORMAT_UNKNOWN and loop
bounds are derived from the actual array length to avoid out-of-bounds access.

Comment on lines +171 to +179
//adjust the framerate if defined
msFrameDelay = PLAYBACK_FRAME_DELAY;
JsonVariant jsonPlaybackFps = jsonPlaybackEntry[jsonKeyFramesPerSecond];
if (jsonPlaybackFps) {
if(jsonPlaybackFps.is<JsonInteger>() || jsonPlaybackFps.is<JsonFloat>()) {
float fps = jsonPlaybackFps;
uint32_t newMsDelay = round(1000.f / fps);
DEBUG_PRINTF("[%s] framerate %d -> delay between frames: %d\n", _name, fps, newMsDelay);
msFrameDelay = newMsDelay;
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

Reject non-positive fps values before computing the delay.

fps = 0 divides by zero here, and negative values wrap into a huge uint32_t delay after the cast. A malformed preset can wedge playback timing instead of falling back to the default.

🛡️ Proposed fix
     msFrameDelay = PLAYBACK_FRAME_DELAY;
     JsonVariant jsonPlaybackFps = jsonPlaybackEntry[jsonKeyFramesPerSecond];
     if (jsonPlaybackFps) {
-      if(jsonPlaybackFps.is<JsonInteger>() || jsonPlaybackFps.is<JsonFloat>()) {
-        float fps = jsonPlaybackFps;
-        uint32_t newMsDelay = round(1000.f / fps);
-        DEBUG_PRINTF("[%s] framerate %d -> delay between frames: %d\n", _name, fps, newMsDelay);
-        msFrameDelay = newMsDelay;
+      if (jsonPlaybackFps.is<JsonInteger>() || jsonPlaybackFps.is<JsonFloat>()) {
+        float fps = jsonPlaybackFps;
+        if (fps > 0.0f) {
+          uint32_t newMsDelay = round(1000.0f / fps);
+          msFrameDelay = newMsDelay;
+        } else {
+          DEBUG_PRINTF("[%s] '%s' must be > 0\n", _name, jsonKeyFramesPerSecond.c_str());
+        }
       } else {
         DEBUG_PRINTF("[%s] %s either as integer or float, though delay will be in ms and integer always\n", _name, jsonKeyFramesPerSecond.c_str());
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/playback_recordings/usermod_playback_recordings.h` around lines 171
- 179, The code reads fps from jsonPlaybackFps and computes newMsDelay without
validating its value, which allows fps==0 or negative values to cause
divide-by-zero or huge wrap-around; update the jsonPlaybackFps handling in the
block that sets msFrameDelay (using symbols jsonPlaybackEntry,
jsonKeyFramesPerSecond, jsonPlaybackFps, fps, newMsDelay, PLAYBACK_FRAME_DELAY,
msFrameDelay) to reject non-positive fps (e.g. if fps <= 0) before computing
round(1000.f / fps), log a warning/debug message about the invalid fps, and
leave msFrameDelay at PLAYBACK_FRAME_DELAY (or previously set value) when the
value is invalid.

Comment on lines +405 to +416
void tpm2_processFrameData()
{
uint16_t packetLength = tpm2_getNextPacketLength(); // opt-TODO maybe stretch recording to available leds
uint16_t lastLed = min(playbackLedStop, uint16_t(playbackLedStart + packetLength));

for (uint16_t i = playbackLedStart; i < lastLed; i++)
{
tpm2_GetNextColorData(colorData);
setRealtimePixel(i, colorData[0], colorData[1], colorData[2], colorData[3]);
}

tpm2_SkipUntilEndOfPacket();
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

Treat the TPM2 length field as bytes, not LED count.

The TPM2 frame size is the number of payload bytes, and RGB TPM2 frames use 3 data bytes per LED. This loop uses packetLength directly as a pixel count, so a 150-byte frame gets decoded as 150 LEDs / 450 color bytes and desynchronizes the stream. Decode packetLength / colorChannels pixels after validating divisibility. (gist.github.com)

🐛 Proposed fix
 void tpm2_processFrameData()
 {
-  uint16_t packetLength = tpm2_getNextPacketLength(); // opt-TODO maybe stretch recording to available leds
-  uint16_t lastLed = min(playbackLedStop, uint16_t(playbackLedStart + packetLength));
+  uint16_t packetLength = tpm2_getNextPacketLength();
+  if ((packetLength % colorChannels) != 0) {
+    DEBUG_PRINTF("[%s] Invalid TPM2 payload length: %u\n", PlaybackRecordings::_name, packetLength);
+    tpm2_SkipUntilEndOfPacket();
+    return;
+  }
+
+  uint16_t pixelCount = packetLength / colorChannels;
+  uint16_t lastLed = min(playbackLedStop, uint16_t(playbackLedStart + pixelCount));
 
   for (uint16_t i = playbackLedStart; i < lastLed; i++)
   {
     tpm2_GetNextColorData(colorData);
     setRealtimePixel(i, colorData[0], colorData[1], colorData[2], colorData[3]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@usermods/playback_recordings/usermod_playback_recordings.h` around lines 405
- 416, The code treats packetLength as a pixel count but TPM2 packetLength is
bytes; change tpm2_processFrameData to compute pixels = packetLength / channels
(where channels = 3 for RGB TPM2 frames), validate that packetLength % channels
== 0 (handle/log/ignore trailing bytes if not divisible), then set lastLed =
min(playbackLedStop, uint16_t(playbackLedStart + pixels)) and loop pixels
reading one pixel's worth of bytes per LED via tpm2_GetNextColorData and calling
setRealtimePixel; keep the final tpm2_SkipUntilEndOfPacket() call. Ensure you
reference packetLength, channels (use a constant or literal 3),
tpm2_GetNextColorData, setRealtimePixel, playbackLedStart and playbackLedStop
when making the change.

@softhack007 softhack007 added the needs_rework PR needs improvements before merging (RED FLAG) label Apr 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation documentation should be updated to explain behaviour keep This issue will never become stale/closed automatically needs_rework PR needs improvements before merging (RED FLAG) usermod usermod related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants