Skip to content

Fix/gc9a01 spiffs shim - #5795

Closed
srg74 wants to merge 176 commits into
wled:mainfrom
srg74:fix/gc9a01-spiffs-shim
Closed

Fix/gc9a01 spiffs shim#5795
srg74 wants to merge 176 commits into
wled:mainfrom
srg74:fix/gc9a01-spiffs-shim

Conversation

@srg74

@srg74 srg74 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added support for GC9A01 TFT displays with status information, brightness and color indicators, clock, Wi-Fi status, and network details.
    • Added configurable sleep, wake, orientation, clock format, timeout, and backlight settings.
    • Added rotary encoder integration for display overlays and direct brightness control.
    • Added navigation for effects, palettes, presets, and other lighting settings through the display interface.
    • Added compatibility support for applications using SPIFFS while using LittleFS storage.
  • Documentation

    • Added setup, wiring, configuration, and troubleshooting documentation for the GC9A01 display.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 965ca764-c75e-49a5-8a31-eaf6394f4814

📥 Commits

Reviewing files that changed from the base of the PR and between a9738aa and 1f81644.

📒 Files selected for processing (2)
  • usermods/platformio_override.usermods.ini
  • usermods/usermod_v2_gc9a01_display/readme.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • usermods/usermod_v2_gc9a01_display/readme.md
  • usermods/platformio_override.usermods.ini

Walkthrough

Adds a GC9A01 TFT display usermod with rendering, sleep, clock, configuration, and rotary encoder support. Adds SPIFFS compatibility shims, PlatformIO include paths, TFT dependency metadata, and setup documentation.

Changes

GC9A01 display integration

Layer / File(s) Summary
Build compatibility and setup
.gitignore, include/SPIFFS.h, lib/compat_spiffs/..., usermods/platformio_override.usermods.ini, usermods/usermod_v2_gc9a01_display/library.json, usermods/usermod_v2_gc9a01_display/readme.md
Adds SPIFFS-to-LittleFS compatibility, include paths for four ESP32 environments, the TFT dependency, ignore rules, and GC9A01 setup documentation.
Display contract and rendering
usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.h, usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp
Adds the GC9A01 usermod API, display initialization, status screens, overlays, WiFi indicators, logo rendering, brightness controls, and clock rendering.
Display lifecycle and configuration
usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp
Adds sleep and wake handling, rotary overlays, loop scheduling, JSON state, configuration persistence, display queries, and usermod registration.
Rotary encoder display integration
usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp
Connects rotary encoder state changes to GC9A01 redraws and overlays for brightness, effects, palettes, colors, presets, and CCT values.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RotaryEncoderUI
  participant WLED
  participant UsermodGC9A01Display
  participant TFT_eSPI
  RotaryEncoderUI->>WLED: Change display control state
  RotaryEncoderUI->>UsermodGC9A01Display: Wake or show overlay
  UsermodGC9A01Display->>WLED: Read current state
  UsermodGC9A01Display->>TFT_eSPI: Render updated display
Loading

Possibly related PRs

  • wled/WLED#4592: Updates shared usermod build configuration and usermod environment coverage.

Suggested labels: enhancement, hardware

Suggested reviewers: willmmiles

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% 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 accurately identifies the GC9A01 and SPIFFS compatibility changes, although it does not describe the broader display usermod implementation.
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.

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 12

🧹 Nitpick comments (9)
usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp (5)

693-694: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use snprintf instead of sprintf.

The current values cannot overflow timeStr. The bound is still not expressed in the code. Static analysis flags the call.

♻️ Proposed change
   char timeStr[10];
-  sprintf(timeStr, "%02d:%02d", hrs, mins);
+  snprintf(timeStr, sizeof(timeStr), "%02d:%02d", hrs, mins);
🤖 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/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp` around
lines 693 - 694, Replace the sprintf call formatting timeStr in the displayed
time construction with snprintf, passing the timeStr buffer size to express the
bound while preserving the existing "%02d:%02d" output.

Source: Linters/SAST tools


503-503: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The knownMode >= 0 check is always true.

knownMode is uint8_t. The comparison is redundant and produces a compiler warning with -Wtype-limits. Line 514 also compares a signed int with the unsigned effectName.length().

♻️ Proposed change
-  } else if (knownMode < strip.getModeCount() && knownMode >= 0) {
+  } else if (knownMode < strip.getModeCount()) {
-    for (int i = 0; i < effectName.length(); i++) {
+    for (unsigned i = 0; i < effectName.length(); i++) {
🤖 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/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp` at line
503, Update the conditional containing knownMode in the display mode handling to
remove the redundant knownMode >= 0 check, since knownMode is uint8_t, while
preserving the strip.getModeCount() upper-bound validation. Also adjust the
comparison near the effectName.length() check to use compatible signed or
unsigned types and eliminate the -Wtype-limits warning.

182-182: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Reduce full-screen clears and String allocations in the redraw path.

drawMainInterface() runs up to once per second. It starts with tft.fillScreen(TFT_BLACK) and then redraws every element. The user sees flicker on each update. The function also builds several String objects (overlayTitle, effectName, cleanName, brightStr) on every call. Repeated heap allocation and growth fragments the heap on ESP32.

Redraw only the regions that changed. Build text into fixed char buffers instead of String.

As per path instructions (docs/cpp.instructions.md): "For display redraw loops, precompute invariants, avoid repeated allocations/String growth, and keep hot-path data in appropriate memory."

Also applies to: 190-198, 500-532

🤖 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/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp` at line
182, Update drawMainInterface() to remove the per-frame
tft.fillScreen(TFT_BLACK) full-screen clear and redraw only UI regions whose
values changed, preserving unchanged display content to prevent flicker. Replace
the per-call String variables overlayTitle, effectName, cleanName, and brightStr
with appropriately sized fixed char buffers, formatting text without heap
allocation or growth. Precompute invariant display data outside the redraw hot
path where practical.

Source: Path instructions


627-640: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use an RGB565 bitmap and a bulk display write for startup/logo flash.

epd_bitmap_ is a 120x120 unsigned long PROGMEM array, using 14,400 × 4 bytes despite the display using color565 output. Convert it to a static const uint16_t ... PROGMEM array and update drawWLEDLogo() so it needs only LOGO_SIZE * LOGO_SIZE elements and transmits one packed color sequence for tft.setAddrWindow().

🤖 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/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp` around
lines 627 - 640, Update epd_bitmap_ to a static const uint16_t PROGMEM RGB565
array, preserving the 120×120 logo dimensions and packed color values. In
drawWLEDLogo(), iterate over LOGO_SIZE * LOGO_SIZE elements, read each 16-bit
value from PROGMEM, and send the contiguous RGB565 buffer through one
setAddrWindow() bulk display write instead of converting and calling pushColor()
per pixel.

943-956: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the unused GC9A01 members and stubs.

getEncoderModeName(), drawCurrentModeIndicator(), drawModeOverlay(), needsRedraw, knownMinute, knownHour, knownPowerState, and setBrightness(uint8_t) are no longer read. Remove them from the header and implementation to avoid dead code.

🤖 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/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp` around
lines 943 - 956, Remove the unused getEncoderModeName(),
drawCurrentModeIndicator(), and drawModeOverlay() definitions from the GC9A01
display implementation, and remove their declarations plus needsRedraw,
knownMinute, knownHour, knownPowerState, and setBrightness(uint8_t) from the
corresponding class header. Update any related initialization or references so
no dead declarations or definitions remain.

Source: Path instructions

usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp (2)

1040-1042: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace the remaining sprintf calls with snprintf.

These five calls write into 64-byte buffers. The current values cannot overflow them. The bound is still not expressed in the code, and static analysis reports each call.

Apply the same change in each location, for example:

snprintf(lineBuffer, sizeof(lineBuffer), "Custom%d: %d", par, val);

Also applies to: 1134-1136, 1181-1183, 1230-1232, 1277-1279

🤖 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/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp`
around lines 1040 - 1042, Replace all five sprintf calls writing to lineBuffer
in the relevant display-update code paths with snprintf, passing
sizeof(lineBuffer) as the buffer size while preserving each existing format
string and arguments.

Source: Linters/SAST tools


825-832: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated GC9A01 wake block into one helper.

The identical five-line pattern appears in ten functions:

if (gc9a01Display && gc9a01Display->wakeDisplay()) {
  gc9a01Display->redraw(true);
  return;
}
if (gc9a01Display) gc9a01Display->updateRedrawTime();

Each copy must stay in sync. A single private helper that returns a bool removes the duplication for both display types.

♻️ Proposed helper
// returns true if the caller must discard the input (a display was woken)
bool RotaryEncoderUIUsermod::prepareDisplays() {
`#ifdef` USERMOD_FOUR_LINE_DISPLAY
  if (display && display->wakeDisplay()) { display->redraw(true); return true; }
  if (display) display->updateRedrawTime();
`#endif`
`#ifdef` USERMOD_GC9A01_DISPLAY
  if (gc9a01Display && gc9a01Display->wakeDisplay()) { gc9a01Display->redraw(true); return true; }
  if (gc9a01Display) gc9a01Display->updateRedrawTime();
`#endif`
  return false;
}

Each change function then starts with if (prepareDisplays()) return;.

Also applies to: 869-875, 907-914, 951-958, 996-1003, 1058-1065, 1103-1110, 1151-1158, 1198-1205, 1248-1255

🤖 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/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp`
around lines 825 - 832, Extract the duplicated display-wake handling into a
private RotaryEncoderUIUsermod::prepareDisplays() helper that processes both
display types, redraws any woken display, updates redraw timing otherwise, and
returns whether input should be discarded. Replace the repeated blocks in all
listed change functions with an early `if (prepareDisplays()) return;` while
preserving the existing conditional compilation behavior.
usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.h (1)

13-13: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused GC9A01 pin aliases.

GC9A01_CS_PIN, GC9A01_DC_PIN, and GC9A01_RST_PIN are defined but never referenced by the usermod. TFT_eSPI uses TFT_CS, TFT_DC, and TFT_RST for display pins, and the #ifndef fallbacks do not configure the driver after #include <TFT_eSPI.h>.

🤖 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/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.h` at line 13,
Remove the unused GC9A01 pin alias definitions, including GC9A01_CS_PIN,
GC9A01_DC_PIN, and GC9A01_RST_PIN, from the display usermod; retain TFT_eSPI’s
TFT_CS, TFT_DC, and TFT_RST configuration unchanged.
usermods/usermod_v2_gc9a01_display/readme.md (1)

74-78: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Move vendor-specific identity overrides out of the generic setup.

If this README targets general WLED users, remove these hardcoded values or move them to a clearly marked board-specific section. The block sets WLED_BRAND, WLED_PRODUCT_NAME, WLED_REPO, SERVERNAME, and WLED_RELEASE_NAME to the author's product and fork.

Custom brand and product values can change API and MQTT identity and may affect client recognition. (github.com)

As per path instructions, AI-generated configuration changes that are unrelated to the PR objective must be identified.

🤖 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/usermod_v2_gc9a01_display/readme.md` around lines 74 - 78, Remove
the vendor-specific identity overrides from the generic setup instructions,
including WLED_BRAND, WLED_PRODUCT_NAME, WLED_REPO, SERVERNAME, and
WLED_RELEASE_NAME, or relocate them into a clearly labeled board-specific
configuration section.

Sources: Path instructions, MCP tools

🤖 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 `@include/SPIFFS.h`:
- Around line 5-12: Update the SPIFFS definitions in include/SPIFFS.h lines 5-12
and lib/compat_spiffs/include/SPIFFS.h lines 5-12 so SPIFFS aliases or
references the mounted global LittleFS instance used by begin(), rather than
creating a separate fs::LittleFSFS object; alternatively, ensure this exact shim
object is mounted before compatibility-library file operations.

In `@usermods/usermod_v2_gc9a01_display/readme.md`:
- Line 3: Update the opening description to replace “fully-featured” with “fully
featured,” preserving the rest of the sentence unchanged.
- Line 73: Remove WLED_DISABLE_BROWNOUT_DET from the generic setup instructions
in the readme. Document it only in the board- or environment-specific setup
section for known power-supply issues, including a warning that it disables the
ESP32 brownout detector.

In `@usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp`:
- Around line 895-903: Replace FPSTR() uses on the GC9A01 configuration key
literals with shared static const char PROGMEM arrays, declared once in the
usermod and reused by readFromConfig(), addToConfig(), and appendConfigData().
Update the affected enabled, sleepMode, clockMode, and clock12hour key accesses
consistently while preserving existing configuration behavior.
- Line 70: Resolve the mismatch between the lastRedraw sentinel behavior and
setup initialization: choose whether the display sleeps after boot or waits for
first interaction. For sleep-after-boot, remove the ULONG_MAX sentinel usage and
corresponding lastRedraw != ULONG_MAX guard; for wait-for-interaction, remove
the lastRedraw = millis() assignment in setup(). Update the related comments to
describe the selected behavior accurately.
- Around line 62-63: Replace absolute timestamp comparisons in the overlay
timeout and update scheduling logic, including the blocks around
activeOverlayMode and the referenced redraw paths, with unsigned elapsed-time
subtraction comparisons. Update checks involving now, overlayUntil, nextUpdate,
and millis() so behavior remains correct across millis() rollover, preserving
the existing expiration and redraw conditions.
- Around line 912-918: Update readFromConfig() so it stores the backlight value
without calling setBacklight() before setup, and return the presence check for
the newest configuration key instead of true. Add the requested initDone member,
set it at the end of setup(), and have initDisplay() apply the stored backlight
only after TFT_BL has been configured as an output.
- Around line 341-343: Fix the note-symbol detection in the mode-name handling
by checking the raw byte value as unsigned rather than comparing the signed
result of String::charAt() to 127. In the condition around modeName, require
length greater than 1 before reading index 1, while preserving the existing
substring(5) removal behavior.

In `@usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.h`:
- Around line 26-28: Remove the fallback TFT_BL definition and guard all
backlight pin operations in initDisplay() and related code so they compile and
run only when TFT_eSPI provides TFT_BL. In setup(), register the defined
backlight pin with PinManager using allocatePin(TFT_BL, true,
PinOwner::UM_Unspecified) before configuring or driving it.
- Around line 30-32: Add USERMOD_ID_GC9A01_DISPLAY with value 59 to the shared
usermod ID definitions in const.h, then remove the local `#ifndef/`#define
fallback from the usermod_v2_gc9a01_display header so it uses the centralized
definition.

In
`@usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp`:
- Around line 678-686: The GC9A01-specific reset block around select_state
currently treats the 750 ms overlay expiry as selection expiry, so an untouched
Effect selection falls back to brightness. Replace this dependency with a
dedicated rotary-user-mod inactivity timeout that starts or refreshes when
changeState() selects a state and only resets select_state after the intended
timeout; alternatively, update changeState() so the overlay duration matches
that selection timeout. Preserve the existing asleep-display reset behavior.
- Around line 850-852: Replace sprintf in the brightness display code with
snprintf, passing the actual brightnessStr capacity (sizeof(brightnessStr)) as
the bound while preserving the existing format and overlay behavior.

---

Nitpick comments:
In `@usermods/usermod_v2_gc9a01_display/readme.md`:
- Around line 74-78: Remove the vendor-specific identity overrides from the
generic setup instructions, including WLED_BRAND, WLED_PRODUCT_NAME, WLED_REPO,
SERVERNAME, and WLED_RELEASE_NAME, or relocate them into a clearly labeled
board-specific configuration section.

In `@usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp`:
- Around line 693-694: Replace the sprintf call formatting timeStr in the
displayed time construction with snprintf, passing the timeStr buffer size to
express the bound while preserving the existing "%02d:%02d" output.
- Line 503: Update the conditional containing knownMode in the display mode
handling to remove the redundant knownMode >= 0 check, since knownMode is
uint8_t, while preserving the strip.getModeCount() upper-bound validation. Also
adjust the comparison near the effectName.length() check to use compatible
signed or unsigned types and eliminate the -Wtype-limits warning.
- Line 182: Update drawMainInterface() to remove the per-frame
tft.fillScreen(TFT_BLACK) full-screen clear and redraw only UI regions whose
values changed, preserving unchanged display content to prevent flicker. Replace
the per-call String variables overlayTitle, effectName, cleanName, and brightStr
with appropriately sized fixed char buffers, formatting text without heap
allocation or growth. Precompute invariant display data outside the redraw hot
path where practical.
- Around line 627-640: Update epd_bitmap_ to a static const uint16_t PROGMEM
RGB565 array, preserving the 120×120 logo dimensions and packed color values. In
drawWLEDLogo(), iterate over LOGO_SIZE * LOGO_SIZE elements, read each 16-bit
value from PROGMEM, and send the contiguous RGB565 buffer through one
setAddrWindow() bulk display write instead of converting and calling pushColor()
per pixel.
- Around line 943-956: Remove the unused getEncoderModeName(),
drawCurrentModeIndicator(), and drawModeOverlay() definitions from the GC9A01
display implementation, and remove their declarations plus needsRedraw,
knownMinute, knownHour, knownPowerState, and setBrightness(uint8_t) from the
corresponding class header. Update any related initialization or references so
no dead declarations or definitions remain.

In `@usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.h`:
- Line 13: Remove the unused GC9A01 pin alias definitions, including
GC9A01_CS_PIN, GC9A01_DC_PIN, and GC9A01_RST_PIN, from the display usermod;
retain TFT_eSPI’s TFT_CS, TFT_DC, and TFT_RST configuration unchanged.

In
`@usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp`:
- Around line 1040-1042: Replace all five sprintf calls writing to lineBuffer in
the relevant display-update code paths with snprintf, passing sizeof(lineBuffer)
as the buffer size while preserving each existing format string and arguments.
- Around line 825-832: Extract the duplicated display-wake handling into a
private RotaryEncoderUIUsermod::prepareDisplays() helper that processes both
display types, redraws any woken display, updates redraw timing otherwise, and
returns whether input should be discarded. Replace the repeated blocks in all
listed change functions with an early `if (prepareDisplays()) return;` while
preserving the existing conditional compilation behavior.
🪄 Autofix

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 Plus

Run ID: ee3f313a-dad1-4705-83e5-9ede182e133d

📥 Commits

Reviewing files that changed from the base of the PR and between d9b9a84 and ed975af.

📒 Files selected for processing (10)
  • .gitignore
  • include/SPIFFS.h
  • lib/compat_spiffs/include/SPIFFS.h
  • usermods/platformio_override.usermods.ini
  • usermods/usermod_v2_gc9a01_display/library.json
  • usermods/usermod_v2_gc9a01_display/logo_data.h
  • usermods/usermod_v2_gc9a01_display/readme.md
  • usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp
  • usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.h
  • usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp

Comment thread include/SPIFFS.h
Comment on lines +5 to +12
// Compatibility shim for libraries that still include SPIFFS.h.
// WLED uses LittleFS for filesystem access, so we map the legacy SPIFFS
// symbol to LittleFS to keep TFT_eSPI and similar libraries buildable.
namespace fs {
using SPIFFSFS = LittleFSFS;
}

inline fs::LittleFSFS SPIFFS;

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repository files matching SPIFFS.h and LittleFS references =="
git ls-files | rg '(^|/)(SPIFFS\.h|.*LittleFS.*)|platform\.txt|platform\.properties|_fs.*' || true

echo
echo "== include/SPIFFS.h =="
if [ -f include/SPIFFS.h ]; then cat -n include/SPIFFS.h; fi

echo
echo "== lib/compat_spiffs/include/SPIFFS.h =="
if [ -f lib/compat_spiffs/include/SPIFFS.h ]; then cat -n lib/compat_spiffs/include/SPIFFS.h; fi

echo
echo "== references to SPIFFS outside compatibility headers =="
rg -n '\bSPIFFS\b|SPIFFSFS|LittleFSFS|LittleFS\.(begin|format|mkdir|open|read|write|remove|rename|listDir)|LittleFSFS\s+LittleFS|inline\s+fs::LittleFSFS\s+LittleFS' .

Repository: wled/WLED

Length of output: 2431


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect likely LittleFS declarations/usages =="
rg -n "fs::LittleFSFS|LittleFSFS\s+(LittleFS|SPIFFS)|LittleFSFS LittleFS|LittleFSFS\s*LittleFS|LittleFSFS\\* LittleFS|LittleFSFS\\*SPIFFS|\\bLittleFS\\.begin\\(" -g '!lib/compat_spiffs' .

Repository: wled/WLED

Length of output: 249


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
for p in [Path("include/SPIFFS.h"), Path("lib/compat_spiffs/include/SPIFFS.h")]:
    if p.exists():
        txt = p.read_text()
        print(f"{p}: bytes={len(txt)} lines={len(txt.splitlines())} contains inline fs::LittleFSFS SPIFFS: {bool('inline fs::LittleFSFS SPIFFS;' in txt)} exists variable SPIFFS: {bool('\\bSPIFFS\\b' in txt)}")
        for i, line in enumerate(txt.splitlines(), 1):
            if "LittleFS" in line or "SPIFFS" in line:
                print(f"  {i}: {line}")
PY

Repository: wled/WLED

Length of output: 649


🌐 Web query:

Arduino ESP32 LittleFS.h class LittleFSFS begin method inline global LittleFS instance

💡 Result:

In the ESP32 Arduino LittleFS library, LittleFSFS::begin(...) is declared as a bool member function and the global Arduino instance is named LittleFS (of type fs::LittleFSFS). [1][2] 1) LittleFSFS class and begin signature (Arduino-ESP32) - The class is declared as class LittleFSFS: public FS, with begin(...) as: - bool begin(bool formatOnFail = false, const char * basePath = "/littlefs", uint8_t maxOpenFiles = 10, const char * partitionLabel = "spiffs"); This is shown in the header. [2] 2) Implementation details of LittleFSFS::begin - The begin implementation: - Stores partitionLabel_ (allocating a copy when partitionLabel is provided). [3] - Checks esp_littlefs_mounted(partitionLabel_) and returns true if already mounted. [3] - Builds an esp_vfs_littlefs_conf_t with.base_path and.partition_label and calls esp_vfs_littlefs_register(&conf). [3] - If esp_vfs_littlefs_register fails with ESP_FAIL and formatOnFail is true, it calls format and then retries esp_vfs_littlefs_register. [3] - On success it sets the mountpoint to basePath via _impl->mountpoint(basePath) and returns true. [3] - Returns false on mount/register failure. [3] 3) Global LittleFS instance (what you call in sketches) - The header declares an extern global instance: - extern fs::LittleFSFS LittleFS; so sketches use LittleFS.begin(...). [2] 4) How begin is called in practice (example sketch) - In the LittleFS_test.ino example, setup calls begin on the global LittleFS instance, e.g.: - if (!LittleFS.begin(FORMAT_LITTLEFS_IF_FAILED)) {... } and in the TWOPART case it calls begin(...) with arguments including basePath and partition label. [4] Example usage pattern (Arduino sketch) - Include the header and call the global instance: - #include <LittleFS.h> - if (!LittleFS.begin(/formatOnFail/ true)) { /handle error/ } This matches the example’s use of the global LittleFS instance. [4]

Citations:


Route the SPIFFS compatibility shim to the mounted WLED LittleFS instance.

begin() mounts the LittleFS global, but these headers define a separate SPIFFS object. The extra object stays unmounted, so compatibility libraries can fail on file operations even after the mounted LittleFS filesystem appears ready. Keep both compatibility headers’ SPIFFS definition as an alias or external reference to the same mounted LittleFS instance, or mount this exact shim object before use.

📍 Affects 2 files
  • include/SPIFFS.h#L5-L12 (this comment)
  • lib/compat_spiffs/include/SPIFFS.h#L5-L12
🤖 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 `@include/SPIFFS.h` around lines 5 - 12, Update the SPIFFS definitions in
include/SPIFFS.h lines 5-12 and lib/compat_spiffs/include/SPIFFS.h lines 5-12 so
SPIFFS aliases or references the mounted global LittleFS instance used by
begin(), rather than creating a separate fs::LittleFSFS object; alternatively,
ensure this exact shim object is mounted before compatibility-library file
operations.

Source: MCP tools

@@ -0,0 +1,120 @@
# GC9A01 Display Usermod

A fully-featured WLED usermod providing comprehensive visual interface on GC9A01 240x240 round TFT displays with complete rotary encoder integration.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use “fully featured”.

Replace fully-featured with fully featured in the opening sentence.

🧰 Tools
🪛 LanguageTool

[grammar] ~3-~3: Ensure spelling is correct
Context: ... Display Usermod A fully-featured WLED usermod providing comprehensive visual interface on GC9A0...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 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/usermod_v2_gc9a01_display/readme.md` at line 3, Update the opening
description to replace “fully-featured” with “fully featured,” preserving the
rest of the sentence unchanged.

Source: Linters/SAST tools

build_flags = ${common.build_flags} ${esp32_idf_V5.build_flags}
-Iinclude
-D WLED_DEBUG
-D WLED_DISABLE_BROWNOUT_DET

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg 'usermods/.*/readme\.md|usermods/.*/.*\.md|usermods/usermod_v2_gc9a01_display/readme.md|CHANGELOG.md|platformio|wled\.h|Brownout|BROWNOUT|DISABLE_BROWNOUT|brownout' || true

echo
echo "Target README context:"
if [ -f usermods/usermod_v2_gc9a01_display/readme.md ]; then
  nl -ba usermods/usermod_v2_gc9a01_display/readme.md | sed -n '1,130p'
fi

echo
echo "Repository usages and docs for brownout flag:"
rg -n "WLED_DISABLE_BROWNOUT_DET|DISABLE_BROWNOUT|brownout|BROWNOUT" . -S || true

Repository: wled/WLED

Length of output: 3322


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Target README around setup block:"
awk 'NR>=1 && NR<=130 { printf "%5d\t%s\n", NR, $0 }' usermods/usermod_v2_gc9a01_display/readme.md

echo
echo "Definition/usages of brownout flag:"
rg -n "WLED_DISABLE_BROWNOUT_DET|DEFAULT_BROWNOUT|DISABLE_BROWN" -S . || true

echo
echo "Relevant wled.h snippets:"
rg -n -C 3 "WLED_DISABLE_BROWNOUT_DET|DEFAULT_BROWNOUT|brownout|BROWNOUT" wled00/wled.h platformio*.ini platformio_override.sample.ini usermods/platformio_override.usermods.ini CHANGELOG.md -S || true

Repository: wled/WLED

Length of output: 11970


Do not include WLED_DISABLE_BROWNOUT_DET in the generic setup.

This flag disables the ESP32 brownout detector and is off by default. Add it only for environments with known power-supply issues, and include a board-specific warning.

Proposed fix
-  -D WLED_DISABLE_BROWNOUT_DET
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-D WLED_DISABLE_BROWNOUT_DET
🤖 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/usermod_v2_gc9a01_display/readme.md` at line 73, Remove
WLED_DISABLE_BROWNOUT_DET from the generic setup instructions in the readme.
Document it only in the board- or environment-specific setup section for known
power-supply issues, including a warning that it disables the ESP32 brownout
detector.

Sources: Path instructions, MCP tools

Comment on lines +62 to +63
if (activeOverlayMode >= 0) {
if (now >= overlayUntil) {

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle millis() rollover in the timeout comparisons.

The code compares absolute timestamps: now >= overlayUntil, now < nextUpdate, and millis() < overlayUntil. millis() wraps to 0 after about 49.7 days. After a wrap, overlayUntil and nextUpdate stay large. The overlay then never expires and loop() stops calling redraw().

Compare the difference instead. The subtraction of unsigned values is rollover-safe.

🐛 Proposed fix pattern
-    if (now >= overlayUntil) {
+    if ((long)(now - overlayUntil) >= 0) {
-  if (now < nextUpdate) return;
+  if ((long)(now - nextUpdate) < 0) return;
 bool UsermodGC9A01Display::isOverlayActive() {
-  return (activeOverlayMode >= 0 && millis() < overlayUntil);
+  return (activeOverlayMode >= 0 && (long)(millis() - overlayUntil) < 0);
 }
 
 int UsermodGC9A01Display::getActiveOverlayMode() {
-  if (millis() >= overlayUntil) {
+  if ((long)(millis() - overlayUntil) >= 0) {
     return -1; // Overlay has expired
   }
   return activeOverlayMode;
 }

Also applies to: 857-859, 958-967

🤖 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/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp` around
lines 62 - 63, Replace absolute timestamp comparisons in the overlay timeout and
update scheduling logic, including the blocks around activeOverlayMode and the
referenced redraw paths, with unsigned elapsed-time subtraction comparisons.
Update checks involving now, overlayUntil, nextUpdate, and millis() so behavior
remains correct across millis() rollover, preserving the existing expiration and
redraw conditions.

overlayUntil = 0;
forceRedraw = true;
needRedraw = true; // Force redraw to show main screen
lastRedraw = now; // Reset sleep timer when returning to main screen

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The lastRedraw sentinel comment does not match the behavior.

The header states that lastRedraw = ULONG_MAX makes "the sleep timer start after first interaction". setup() overwrites lastRedraw with millis() at line 845. The sentinel is therefore never active at boot, and the display sleeps after displayTimeout without any interaction.

Decide on one behavior. If the display must sleep after boot, remove the sentinel and the lastRedraw != ULONG_MAX guard. If it must wait for an interaction, remove the assignment in setup().

As per path instructions: "VERIFY comments match code behavior - AI frequently generates plausible but incorrect comments."

Also applies to: 150-150, 845-845

🤖 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/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp` at line 70,
Resolve the mismatch between the lastRedraw sentinel behavior and setup
initialization: choose whether the display sleeps after boot or waits for first
interaction. For sleep-after-boot, remove the ULONG_MAX sentinel usage and
corresponding lastRedraw != ULONG_MAX guard; for wait-for-interaction, remove
the lastRedraw = millis() assignment in setup(). Update the related comments to
describe the selected behavior accurately.

Source: Path instructions

Comment on lines +912 to +918
// Load backlight setting (0-100% range)
uint8_t newBacklight = top[FPSTR("backlight")] | backlight;
if (newBacklight != backlight) {
setBacklight(newBacklight);
}

return 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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not drive the backlight pin from readFromConfig(), and report missing keys.

Two problems exist here.

First, WLED calls readFromConfig() before setup(). setBacklight() calls analogWrite(TFT_BL, pwm) at that time. initDisplay() has not yet run pinMode(TFT_BL, OUTPUT). Store the value and apply it in initDisplay().

Second, the function returns true unconditionally. WLED uses the return value to decide whether the stored configuration is complete. If a key is missing after an upgrade, the config is not re-saved with defaults. Return the presence check of the newest key.

🐛 Proposed fix
   // Load backlight setting (0-100% range)
-  uint8_t newBacklight = top[FPSTR("backlight")] | backlight;
-  if (newBacklight != backlight) {
-    setBacklight(newBacklight);
-  }
-  
-  return true;
+  uint8_t newBacklight = top[FPSTR(_gcBacklight)] | backlight;
+  if (initDone && newBacklight != backlight) setBacklight(newBacklight);
+  else backlight = min(newBacklight, (uint8_t)100);
+
+  return !top[FPSTR(_gcBacklight)].isNull();

initDone needs to be added as a member and set at the end of setup().

🤖 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/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.cpp` around
lines 912 - 918, Update readFromConfig() so it stores the backlight value
without calling setBacklight() before setup, and return the presence check for
the newest configuration key instead of true. Add the requested initDone member,
set it at the end of setup(), and have initDisplay() apply the stored backlight
only after TFT_BL has been configured as an output.

Comment on lines +26 to +28
#ifndef TFT_BL
#define TFT_BL 4 // Default fallback if not defined by TFT_eSPI
#endif

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not force a default backlight pin.

If TFT_eSPI does not define TFT_BL, the code still defines it as GPIO4. initDisplay() then calls pinMode(TFT_BL, OUTPUT) and digitalWrite(TFT_BL, HIGH) on a pin that the user did not assign. GPIO4 may be used by LED output or another usermod.

The usermod also never registers the pin with PinManager. WLED cannot detect the conflict.

Remove the fallback and guard all backlight code with the real TFT_BL definition. Allocate the pin with PinManager::allocatePin(TFT_BL, true, PinOwner::UM_Unspecified) in setup().

🐛 Proposed fix
-// Use TFT_eSPI's backlight pin definition
-#ifndef TFT_BL
-  `#define` TFT_BL 4  // Default fallback if not defined by TFT_eSPI
-#endif
+// Backlight pin comes from the TFT_eSPI configuration only.
+// If TFT_BL is not defined, backlight control is disabled.
🤖 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/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.h` around lines
26 - 28, Remove the fallback TFT_BL definition and guard all backlight pin
operations in initDisplay() and related code so they compile and run only when
TFT_eSPI provides TFT_BL. In setup(), register the defined backlight pin with
PinManager using allocatePin(TFT_BL, true, PinOwner::UM_Unspecified) before
configuring or driving it.

Comment on lines +30 to +32
#ifndef USERMOD_ID_GC9A01_DISPLAY
#define USERMOD_ID_GC9A01_DISPLAY 59 // Use the official ID from const.h
#endif

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether the usermod ID is declared in const.h and whether 59 is already used.
fd -t f 'const.h' wled00 --exec rg -n 'USERMOD_ID_' {} \; | rg -n '\b59\b|GC9A01'

Repository: wled/WLED

Length of output: 147


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate relevant files =="
fd -t f 'const.h|usermod_v2_gc9a01_display.h|UsermodManager|rotary encoder' . | sed -n '1,120p'

echo
echo "== const.h usermod IDs relevant section =="
if [ -f wled00/const.h ]; then
  rg -n "USERMOD_ID_|USERMOD_COUNT|MAX_USERMODS|UsermodManager|usermod" wled00/const.h | sed -n '1,220p'
else
  echo "wled00/const.h not found"
fi

echo
echo "== all repository references to USERMOD_ID_GC9A01_DISPLAY and ID 59 =="
rg -n 'USERMOD_ID_GC9A01_DISPLAY|\bUSERMOD_ID_[A-Za-z0-9_]*\s+\{\s*59\s*\}|\bUSERMOD_ID_[A-Za-z0-9_]*\s+(=|:|,)\s*59\b|\b59\b' . \
  --glob '!build/**' --glob '!bin/**' --glob '!docs/**' | sed -n '1,240p'

echo
echo "== usermod files =="
git ls-files | rg 'usermods/.*usermod_v2_gc9a01_display|wled00/const.h|usermod.*\.h|UsermodManager' | sed -n '1,200p'

Repository: wled/WLED

Length of output: 38066


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== UsermodManager declarations/definitions =="
rg -n "class UsermodManager|lookup\\(|find\\(|getId\\(|_usermods|UMData|getUMData" wled00 | sed -n '1,260p'

echo
echo "== IDs around USERMOD_ID_USER_FX and next available IDs =="
sed -n '248,258p' wled00/const.h

echo
echo "== usermod getId returns and const.h mapping =="
python3 - <<'PY'
import pathlib, re
const = pathlib.Path('wled00/const.h').read_text()
m = re.findall(r'^`#define`\s+(USERMOD_ID_[A-Za-z0-9_]+)\s+(\d+)\s*//\s*Usermod "(.*?)"',$ const, re.M)
ids = {macro:int(value) for macro, value, _ in m}
rev = {value:macro for macro, value in m}
print("USERMOD_ID_GC9A01_DISPLAY in const.h:", "USERMOD_ID_GC9A01_DISPLAY" in ids)
print("ID 59 used by const.h usermod:", rev.get(59))
for p in pathlib.Path('usermods').rglob('*.h'):
    text = p.read_text(errors='ignore')
    if 'getId()' in text and re.search(r'return\s+USERMOD_ID_', text):
        for macro in re.findall(r'USERMOD_ID_[A-Za-z0-9_]+', text):
            print(f"{p}: {macro}", "const.h value=", ids.get(macro, "missing"))
PY

Repository: wled/WLED

Length of output: 14512


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== UsermodManager relevant source =="
sed -n '24,92p' wled00/um_manager.cpp
sed -n '368,390p' wled00/fcn_declare.h
sed -n '398,428p' wled00/fcn_declare.h

echo
echo "== const.h IDs section =="
sed -n '246,257p' wled00/const.h

echo
echo "== parse USERMOD IDs and usermod references =="
python3 - <<'PY'
import pathlib, re

usermod_re = re.compile(r'^`#define`\s+(USERMOD_ID_[A-Za-z0-9_]+)\s+(\d+)\s*//\s*Usermod "(.*?)"')
ids = {}
for line in pathlib.Path('wled00/const.h').read_text().splitlines():
    m = usermod_re.match(line)
    if m:
        base = int(m.group(2))
        mac = m.group(1)
        ids[mac] = base
        ids[base] = mac

for name in ['USERMOD_ID_GC9A01_DISPLAY', 59]:
    print("USERMOD_ID_GC9A01_DISPLAY defined?", "USERMOD_ID_GC9A01_DISPLAY" in ids)
    print("ID 59 base const macro", ids.get(59))

roots = ['usermods', 'wled00']
for root in roots:
    print(f"\nReferences to USERMOD_ID_GC9A01_DISPLAY under {root}:")
    for p in pathlib.Path(root).rglob('*'):
        if p.is_file() and p.suffix in {'.h','.hpp','.cpp','.ino'}:
            txt = p.read_text(errors='ignore')
            if 'USERMOD_ID_GC9A01_DISPLAY' in txt:
                for i, line in enumerate(txt.splitlines(), 1):
                    if 'USERMOD_ID_GC9A01_DISPLAY' in line:
                        print(f"{p}:{i}: {line.strip()}")
PY

Repository: wled/WLED

Length of output: 8972


Add USERMOD_ID_GC9A01_DISPLAY to wled00/const.h and remove the local fallback.

It is only defined under usermods/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.h, while other usermods register their official IDs in wled00/const.h. ID 59 is not currently used by any registered usermod, but the local fallback does not create a shared official entry.

🤖 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/usermod_v2_gc9a01_display/usermod_v2_gc9a01_display.h` around lines
30 - 32, Add USERMOD_ID_GC9A01_DISPLAY with value 59 to the shared usermod ID
definitions in const.h, then remove the local `#ifndef/`#define fallback from the
usermod_v2_gc9a01_display header so it uses the centralized definition.

Source: Coding guidelines

Comment on lines +678 to +686
// Check if GC9A01 overlay has expired or display is asleep and reset to brightness mode (state 0)
#ifdef USERMOD_GC9A01_DISPLAY
if (gc9a01Display != nullptr && select_state > 0) {
if (!gc9a01Display->isOverlayActive() || gc9a01Display->isDisplayAsleep()) {
// Overlay has expired or display is asleep, return to brightness mode
select_state = 0;
}
}
#endif

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The state reset can discard a state selection after 750 ms.

changeState() calls gc9a01Display->overlay(stateName, 750, glyph). isOverlayActive() returns true only while millis() < overlayUntil. This block runs every 2 ms.

If the user presses the button to select "Effect" and does not rotate the encoder within 750 ms, select_state resets to 0. The next rotation changes brightness instead of the effect. Users of the Four Line Display do not experience this reset.

updateRedrawTime() extends overlayUntil by 3000 ms, but only after the first rotation.

Use a dedicated inactivity timer in the rotary usermod, or align the overlay duration in changeState() with the intended selection timeout.

🤖 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/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp`
around lines 678 - 686, The GC9A01-specific reset block around select_state
currently treats the 750 ms overlay expiry as selection expiry, so an untouched
Effect selection falls back to brightness. Replace this dependency with a
dedicated rotary-user-mod inactivity timeout that starts or refreshes when
changeState() selects a state and only resets select_state after the intended
timeout; alternatively, update changeState() so the overlay duration matches
that selection timeout. Preserve the existing asleep-display reset behavior.

Comment on lines +850 to +852
char brightnessStr[16];
sprintf(brightnessStr, "Brightness %d%%", (bri * 100) / 255);
gc9a01Display->overlay(brightnessStr, 500, 10);

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

brightnessStr has no spare capacity.

"Brightness 100%" is 15 characters. With the terminator it needs exactly 16 bytes. The buffer is 16 bytes. Any change to the format string overflows the buffer.

Use snprintf and state the bound.

🐛 Proposed fix
-      char brightnessStr[16];
-      sprintf(brightnessStr, "Brightness %d%%", (bri * 100) / 255);
+      char brightnessStr[20];
+      snprintf(brightnessStr, sizeof(brightnessStr), "Brightness %d%%", (bri * 100) / 255);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
char brightnessStr[16];
sprintf(brightnessStr, "Brightness %d%%", (bri * 100) / 255);
gc9a01Display->overlay(brightnessStr, 500, 10);
char brightnessStr[20];
snprintf(brightnessStr, sizeof(brightnessStr), "Brightness %d%%", (bri * 100) / 255);
gc9a01Display->overlay(brightnessStr, 500, 10);
🧰 Tools
🪛 ast-grep (0.45.0)

[error] 850-850: Use of an unbounded buffer function that can overflow the destination; use a size-bounded equivalent (fgets, strncpy/strlcpy, strncat/strlcat, snprintf).
Context: sprintf(brightnessStr, "Brightness %d%%", (bri * 100) / 255)
Note: [CWE-120] Buffer Copy without Checking Size of Input ('Classic Buffer Overflow').

(dangerous-buffer-functions-cpp)

🤖 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/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.cpp`
around lines 850 - 852, Replace sprintf in the brightness display code with
snprintf, passing the actual brightnessStr capacity (sizeof(brightnessStr)) as
the bound while preserving the existing format and overlay behavior.

Source: Linters/SAST tools

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 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 `@lib/compat_spiffs/src/SPIFFS.h`:
- Line 9: Indent the SPIFFSFS alias declaration by two spaces to match the
required C++ header namespace style.
- Line 12: Update the SPIFFS compatibility alias to reuse the existing LittleFS
singleton instead of constructing a new LittleFSFS instance; preserve the legacy
SPIFFS symbol while ensuring filesystem lifecycle calls operate on the same
LittleFS object.
🪄 Autofix

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 Plus

Run ID: 88bed582-2911-4652-84b2-dfb1c324dac3

📥 Commits

Reviewing files that changed from the base of the PR and between ed975af and a9738aa.

📒 Files selected for processing (1)
  • lib/compat_spiffs/src/SPIFFS.h

// Placed in lib/compat_spiffs/src so PlatformIO adds it to the include path
// automatically when scanning libraries.
namespace fs {
using SPIFFSFS = LittleFSFS;

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Apply the required two-space indentation.

Line 9 places the namespace member at column 1.

-using SPIFFSFS = LittleFSFS;
+  using SPIFFSFS = LittleFSFS;

As per path instructions, C++ headers must use 2-space indentation and match the existing C++ style.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
using SPIFFSFS = LittleFSFS;
using SPIFFSFS = LittleFSFS;
🤖 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 `@lib/compat_spiffs/src/SPIFFS.h` at line 9, Indent the SPIFFSFS alias
declaration by two spaces to match the required C++ header namespace style.

Source: Path instructions

using SPIFFSFS = LittleFSFS;
}

inline fs::LittleFSFS SPIFFS;

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --glob '*.{cpp,h,hpp,ino}' \
  '\b(SPIFFS|LittleFS)[[:space:]]*\.[[:space:]]*(begin|end|open|format|exists|remove|rename|mkdir|rmdir)[[:space:]]*\(' .

Repository: wled/WLED

Length of output: 147


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)SPIFFS\.h$|LittleFS|WLED\.ino$' || true

echo "== SPIFFS.h if present =="
if [ -f lib/compat_spiffs/src/SPIFFS.h ]; then
  cat -n lib/compat_spiffs/src/SPIFFS.h
fi

echo "== references to SPIFFS/LittleFS symbols =="
rg -n '\b(SPIFFS|LittleFS)\b' -g '*.{cpp,h,hpp,ino,c}' . || true

echo "== filesystem lifecycle references =="
rg -n '\b(SPIFFS|LittleFS)[[:space:]]*\.[[:space:]]*(begin|end|open|format|exists|remove|rename|mkdir|rmdir|fs|size|free)\s*\(' . || true

Repository: wled/WLED

Length of output: 2499


🌐 Web query:

Arduino ESP32 LittleFS LittleFSFS begin mount partition returns true mounted without assigning mount point

💡 Result:

In the Arduino ESP32 environment, the LittleFS begin() method provides default values for its parameters, which explains why it can return true even when you do not explicitly assign a mount point [1][2]. The signature of the begin method is typically: bool begin(bool formatOnFail = false, const char *basePath = "/littlefs", uint8_t maxOpenFiles = 10, const char *partitionLabel = "littlefs") When you call LittleFS.begin() without arguments: 1. It uses the default base path (mount point) of "/littlefs" [1]. 2. It attempts to mount the partition labeled "littlefs" [1]. If your call returns true, the filesystem has been successfully mounted at the default location (/littlefs). If you were expecting it to mount elsewhere or if you have a custom partition setup, you must explicitly provide the basePath and partitionLabel parameters [1][3][4]. Key considerations: - Default Behavior: If you do not specify parameters, the library defaults to "/littlefs" as the mount point [1]. This is why code often works "without" visible assignment; it is assigned automatically by the library's default arguments [2]. - Partition Table: LittleFS requires a partition of the correct type (often defined as spiffs or littlefs in your partitions.csv file) [5][6][7]. If the library cannot find a partition with the specified label (defaulting to "littlefs"), mounting will fail and return false [1][4]. - Legacy/Third-Party Libraries: Note that if you are using the older lorol/LITTLEFS library instead of the built-in core library, some behaviors regarding default parameters and labels may vary, though the core espressif/arduino-esp32 implementation follows the standard described above [1][8][9]. To ensure explicit control, always define your parameters: if (!LittleFS.begin(true, "/your_mount_point", 10, "your_partition_label")) { ... } [4]

Citations:


🌐 Web query:

espressif arduino-esp32 LittleFS.cpp LittleFSFS begin _mount return true

💡 Result:

In the espressif/arduino-esp32 framework, the LittleFSFS::begin method returns true when the LittleFS partition is successfully mounted or if it is already mounted [1][2]. The specific implementation logic in LittleFS.cpp functions as follows: 1. Check for existing mount: The method first calls esp_littlefs_mounted(partitionLabel_) [1][2]. If this returns true, the filesystem is already accessible, and the function immediately returns true [1][2]. 2. Registration: If not already mounted, it prepares an esp_vfs_littlefs_conf_t configuration structure and attempts to register the filesystem using esp_vfs_littlefs_register(&conf) [1][2]. 3. Optional Formatting: If the registration fails (ESP_FAIL) and formatOnFail is set to true, it attempts to format the partition [1][2]. If formatting succeeds, it tries to register the filesystem again [1][2]. 4. Success Criteria: If registration is successful (err == ESP_OK), the method sets the internal mount point path via _impl->mountpoint(basePath) and returns true [1][2]. If the registration fails and cannot be recovered by formatting, it logs an error and returns false [1][2]. The return value indicates that the VFS (Virtual File System) layer has successfully associated the LittleFS partition with the specified base path [1][2].

Citations:


🌐 Web query:

espressif arduino-esp32 LittleFS.cpp LittleFSFS end esp_littlefs_unmount unregister partition _impl mountpoint

💡 Result:

In the arduino-esp32 framework, the LittleFS.end method is responsible for properly unmounting and unregistering the LittleFS filesystem from the Virtual File System (VFS) layer [1]. When LittleFS.end is called, the following process occurs: 1. Verification: It checks if the filesystem is currently mounted by calling esp_littlefs_mounted(partitionLabel_) [1][2]. 2. Unregistration: If mounted, it calls esp_vfs_littlefs_unregister(partitionLabel_) to unregister the partition [1][2]. 3. VFS Update: Internally, esp_vfs_littlefs_unregister handles unmounting the underlying filesystem and freeing the associated resources [3][4]. 4. Cleanup: After successfully unregistering, it calls _impl->mountpoint(NULL) to clear the mountpoint reference in the filesystem implementation wrapper [1][2]. This ensures that the partition is no longer associated with a VFS path, allowing it to be re-mounted later if needed [1][4].

Citations:


🌐 Web query:

espressif arduino-esp32 LittleFS.cpp LittleFSFS end existing mount another begin false

💡 Result:

If you’re referring to the Arduino-ESP32 LittleFS implementation, the behavior is: 1) LittleFSFS::begin will do nothing harmful if the same partition is already mounted. - In LittleFSFS::begin, arduino-esp32 checks esp_littlefs_mounted(partitionLabel_) and, if already mounted, logs “LittleFS Already Mounted!” and returns true immediately [1]. - That means you do NOT need (and should not do) an explicit “end; begin;” just to re-mount the same partition; begin is already idempotent for an already-mounted partition [1]. 2) LittleFSFS::end unmounts only if the partition is currently mounted. - end checks esp_littlefs_mounted(partitionLabel_) and, if mounted, calls esp_vfs_littlefs_unregister(partitionLabel_) and then clears the stored mountpoint [1]. 3) “end existing mount another begin false” / “begin false” in practice: - The “begin” parameter “formatOnFail” controls only whether formatting is attempted if mount fails; it is set via LittleFSFS::begin(bool formatOnFail,...) and the code passes.format_if_mount_failed = false in the esp_vfs_littlefs_conf_t, but then conditionally retries with format only when formatOnFail is true [1]. - Therefore, calling LittleFS.begin(false) (i.e., formatOnFail=false) will NOT format on mount failure; mount will fail and return false instead [1]. 4) Mounting a different LittleFS partition (different partitionLabel) can coexist with “another begin”. - The implementation supports specifying a partitionLabel; begin stores/updates partitionLabel_ and uses it for the mounted-check and registration [1]. - The official LittleFS_test example shows mounting a second partition by calling LittleFS.begin(..., basePath, maxOpenFiles, "part2") and then LittleFS.end when done, before using the first partition again [2]. What to do in your situation - If you just want to ensure a single mount: call LittleFS.begin(false) once at startup. If it’s already mounted, begin will just return true [1]. - If you need to switch to a different partition/basePath: call LittleFS.end first (so the previous partition is unregistered), then call LittleFS.begin(false, newBasePath, newMaxOpenFiles, newPartitionLabel) [1][2]. - If begin(false) fails: because formatOnFail is false, it will not format; you must either repair/format externally or call begin(true) / format intentionally [1]. Code sketch (switch partitions) - end previous mount, then mount another without formatting on fail: LittleFS.end; bool ok = LittleFS.begin(false, "/lfs2", 5, "part2"); If you tell me the exact call sequence you’re using (the basePath + partitionLabel values and whether you expect re-mounting same vs different partition), I can map it precisely to the code paths above.

Citations:


Reuse the existing LittleFS singleton.

This compatibility shim currently constructs a duplicate LittleFSFS wrapper with default mount parameters. A legacy SPIFFS shim is useful, but avoid exposing a second instance that can interfere with the same LittleFS partition when filesystem lifecycle calls are used.

Suggested fix
-inline fs::LittleFSFS SPIFFS;
+#define SPIFFS LittleFS
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
inline fs::LittleFSFS SPIFFS;
`#define` SPIFFS LittleFS
🤖 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 `@lib/compat_spiffs/src/SPIFFS.h` at line 12, Update the SPIFFS compatibility
alias to reuse the existing LittleFS singleton instead of constructing a new
LittleFSFS instance; preserve the legacy SPIFFS symbol while ensuring filesystem
lifecycle calls operate on the same LittleFS object.

@srg74 srg74 closed this Aug 9, 2026
@srg74
srg74 deleted the fix/gc9a01-spiffs-shim branch August 9, 2026 21:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant