Skip to content

Xsn/activity render refactor (for AI review)#1

Open
ngxson wants to merge 21 commits intomasterfrom
xsn/activity_render_refactor
Open

Xsn/activity render refactor (for AI review)#1
ngxson wants to merge 21 commits intomasterfrom
xsn/activity_render_refactor

Conversation

@ngxson
Copy link
Owner

@ngxson ngxson commented Feb 9, 2026

Mirror upstream crosspoint-reader#774

Summary by CodeRabbit

  • Refactor
    • Rendering consolidated to an on-demand requestUpdate flow, removing background render threads for steadier, synchronous updates.
    • Unified render entry across activities and scoped locking to avoid redraws during transitions, improving visual stability and responsiveness.
  • New Features
    • Server setup screens now render QR codes and richer connection info.
  • Behavior
    • OTA wait timing adjusted (short fixed delay), affecting update pacing.

@ngxson
Copy link
Owner Author

ngxson commented Feb 9, 2026

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 9, 2026

✅ 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

coderabbitai bot commented Feb 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a centralized FreeRTOS render task and RAII RenderLock in Activity; subclasses remove per-activity render tasks/mutexes, expose public render(Activity::RenderLock&&) overrides, and switch from per-activity update flags to a unified requestUpdate() notification flow (subactivities forward requests).

Changes

Cohort / File(s) Summary
Core Activity Infrastructure
src/activities/Activity.h, src/activities/Activity.cpp
Introduce a single render task lifecycle (renderTaskTrampoline, renderTaskLoop), requestUpdate(), renderTaskHandle, renderingMutex, and nested RAII RenderLock; make onEnter/onExit virtual and declare virtual render(RenderLock&&).
Activity composition
src/activities/ActivityWithSubactivity.*
Forward requestUpdate() to active subActivity; add RenderLock scope around activity transitions to avoid concurrent rendering during swaps.
UI Activities (broad)
src/activities/... (browser, home, home/, reader/, network/, settings/, util/*)
Remove per-activity FreeRTOS render tasks, semaphores/mutexes, and update flags; expose render(Activity::RenderLock&&) publicly; replace updateRequired wake-ups with requestUpdate() across state transitions and call sites.
Readers & selection menus
src/activities/reader/*, src/activities/reader/*Selection*.{h,cpp}
Rename render entrypoints (e.g., renderScreen()render(Activity::RenderLock&&)), delete task trampolines/display loops, update call sites to use requestUpdate() and scoped RenderLock.
Network & server activities
src/activities/network/*
Remove display task loops and per-activity semaphores; add render(Activity::RenderLock&&) overrides; switch state transitions and server/UI updates to requestUpdate(); add QR/host rendering helpers where applicable.
Settings & utilities
src/activities/settings/*, src/activities/util/*
Eliminate background render tasks and mutex teardown; move render implementations to render(Activity::RenderLock&&) and adapt loops/handlers to call requestUpdate() on state changes.
OTA wait change
src/network/OtaUpdater.cpp
Replace vTaskDelay(...) with delay(100) in OTA wait loop (temporary FIXME noted).

Sequence Diagram(s)

sequenceDiagram
  participant App as Application
  participant Activity as Activity (base)
  participant RTOS as FreeRTOS
  participant RenderTask as Render Task
  participant Sub as SubActivity / Activity::render()

  App->>Activity: onEnter()
  Activity->>RTOS: xTaskCreate(renderTaskTrampoline, this)
  RTOS->>RenderTask: start
  loop runtime
    App->>Activity: requestUpdate()
    Activity->>RTOS: xTaskNotify(renderTaskHandle, 1, eIncrement)
    RTOS->>RenderTask: deliver notification
    RenderTask->>Activity: acquire RenderLock (take mutex)
    RenderTask->>Sub: call render(Activity::RenderLock&&)
    Sub-->>RenderTask: render completes
    RenderTask->>Activity: release RenderLock (mutex)
  end
  App->>Activity: onExit()
  Activity->>RTOS: vTaskDelete(renderTaskHandle)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰
I hopped through tasks and mutex vines,
Tuned render beats to single lines,
One notify, one cozy lock,
No scattered tasks that chase the clock,
I thump — redraws now align.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Xsn/activity render refactor (for AI review)' is vague and contains a branch name prefix and meta-annotation ('for AI review') that obscure the actual change. While 'activity render refactor' alludes to a real aspect of the PR, it does not clearly convey the primary technical achievement (centralizing rendering via RenderLock RAII) or its scope. Replace the branch-centric and vague title with a clear, specific description of the main change, e.g., 'Centralize activity rendering with RenderLock RAII' or 'Replace per-activity render tasks with centralized rendering.' Remove branch names and meta-tags like 'for AI review'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch xsn/activity_render_refactor

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

Copy link

@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: 2

Caution

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

⚠️ Outside diff range comments (3)
src/activities/home/RecentBooksActivity.cpp (1)

70-85: ⚠️ Potential issue | 🟠 Major

Guard against empty list before modulo operations.

Lines 75 and 82 perform (selectorIndex ± 1) % listSize without checking if listSize is zero. When recentBooks is empty, listSize equals 0, resulting in undefined behavior. Add a guard at the beginning of the modulo block.

🛠️ Proposed fix
  int listSize = static_cast<int>(recentBooks.size());
+ if (listSize == 0) {
+   return;
+ }
  if (upReleased) {
    if (skipPage) {
      selectorIndex = std::max(static_cast<int>((selectorIndex / pageItems - 1) * pageItems), 0);
    } else {
      selectorIndex = (selectorIndex + listSize - 1) % listSize;
src/activities/settings/KOReaderSettingsActivity.cpp (1)

121-125: ⚠️ Potential issue | 🔴 Critical

Remove unowned semaphore give call.

Line 123 calls xSemaphoreGive(renderingMutex) without owning the mutex. On FreeRTOS, giving an unowned mutex causes assertion failures or lock state corruption. Replace with requestUpdate(), which correctly signals the render task without requiring mutex ownership. All other code paths in this method already use requestUpdate().

🔧 Proposed fix
    if (!KOREADER_STORE.hasCredentials()) {
      // Can't authenticate without credentials - just show message briefly
-     xSemaphoreGive(renderingMutex);
+     requestUpdate();
      return;
    }
src/activities/reader/EpubReaderChapterSelectionActivity.cpp (1)

50-88: ⚠️ Potential issue | 🟠 Major

Guard against zero TOC items before modulo.

If getTotalItems() returns 0 and the user presses Up/Down, the modulo operations will divide by zero.

🔧 Proposed fix
   const int pageItems = getPageItems();
   const int totalItems = getTotalItems();
@@
   } else if (prevReleased) {
+    if (totalItems == 0) {
+      return;
+    }
     if (skipPage) {
       selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + totalItems) % totalItems;
     } else {
       selectorIndex = (selectorIndex + totalItems - 1) % totalItems;
     }
     requestUpdate();
   } else if (nextReleased) {
+    if (totalItems == 0) {
+      return;
+    }
     if (skipPage) {
       selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % totalItems;
     } else {
       selectorIndex = (selectorIndex + 1) % totalItems;
     }
     requestUpdate();
   }
🤖 Fix all issues with AI agents
In `@src/activities/Activity.cpp`:
- Around line 18-35: The onEnter() call to xTaskCreate currently ignores its
return value and relies on an assert, so if task creation fails renderTaskHandle
stays null and onExit()'s vTaskDelete(renderTaskHandle) can delete the current
task; update onEnter() to capture xTaskCreate's BaseType_t result (from calling
renderTaskTrampoline) and only assign/use renderTaskHandle if xTaskCreate
returns pdPASS, logging or handling the failure, and change onExit() to check
renderTaskHandle != nullptr before calling vTaskDelete (and still use RenderLock
to protect rendering); reference functions/vars: onEnter, onExit, xTaskCreate,
renderTaskTrampoline, renderTaskHandle, vTaskDelete, RenderLock.

In `@src/activities/settings/KOReaderAuthActivity.cpp`:
- Around line 56-59: Lock the same mutex used elsewhere before updating state
and statusMessage to avoid a data race: wrap the assignments to state =
AUTHENTICATING and statusMessage = "Authenticating..." (and the subsequent
requestUpdate() call if other paths do) in a scoped lock (e.g.,
std::lock_guard<std::mutex> guard(yourMutex)) so writes are synchronized with
render reads; update the block that sets state, statusMessage, and calls
requestUpdate() to acquire that mutex consistently with other code paths.
🧹 Nitpick comments (5)
src/activities/reader/EpubReaderPercentSelectionActivity.h (1)

25-30: Remove stale RTOS-related comments.

The comments at Line 27 and Line 29 refer to fields that no longer exist and are now misleading.

🧹 Proposed cleanup
-  // Render dirty flag for the task loop.
-
-  // FreeRTOS task and mutex for rendering.
-
src/activities/settings/ClearCacheActivity.cpp (1)

117-124: Use RenderLock instead of manual mutex operations for consistency.

The renderingMutex still exists and the manual xSemaphoreTake/xSemaphoreGive calls will compile and work correctly. However, other parts of the codebase use the RenderLock RAII wrapper (e.g., Activity.cpp:12, Activity.cpp:30, ActivityWithSubactivity.cpp:14). Consider refactoring to use RenderLock lock(*this); at line 121 instead of manual mutex operations for consistency with the established pattern.

src/activities/Activity.h (1)

20-21: Verify [[noreturn]] on the trampoline function.

The [[noreturn]] attribute on renderTaskTrampoline is technically correct since it calls renderTaskLoop() which never returns. However, from the implementation in Activity.cpp, the trampoline returns void and calls the loop:

void Activity::renderTaskTrampoline(void* param) {
  auto* self = static_cast<Activity*>(param);
  return self->renderTaskLoop();  // returns void, then calls noreturn
}

The attribute works here because control never returns from renderTaskLoop(), but marking a void-returning function as [[noreturn]] when it delegates to another [[noreturn]] function is unconventional. Consider removing [[noreturn]] from the trampoline or changing the return statement to just self->renderTaskLoop(); without return.

src/activities/reader/EpubReaderActivity.cpp (1)

600-603: Potential unbounded recursion on persistent load failures.

The recursive return render() call could cause stack overflow if the page file remains corrupt after cache clearing. Consider adding a retry counter or returning an error state instead.

🔧 Suggested safeguard
   {
     auto p = section->loadPageFromSectionFile();
     if (!p) {
       Serial.printf("[%lu] [ERS] Failed to load page from SD - clearing section cache\n", millis());
       section->clearCache();
       section.reset();
-      return render();
+      // Request another render attempt rather than recursing
+      requestUpdate();
+      return;
     }
src/activities/home/MyLibraryActivity.cpp (1)

86-88: Minor: Double semicolon.

Line 88 has a stray semicolon after the statement.

🧹 Fix
   const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Left) ||
                           mappedInput.wasReleased(MappedInputManager::Button::Up);
-  ;

Comment on lines +18 to +35
void Activity::onEnter() {
xTaskCreate(&renderTaskTrampoline, name.c_str(),
8192, // Stack size
this, // Parameters
1, // Priority
&renderTaskHandle // Task handle
);
assert(renderTaskHandle != nullptr && "Failed to create render task");
Serial.printf("[%lu] [ACT] Entering activity: %s\n", millis(), name.c_str());
}

void Activity::onExit() {
RenderLock lock(*this); // Ensure we don't delete the task while it's rendering
vTaskDelete(renderTaskHandle);
renderTaskHandle = nullptr;

Serial.printf("[%lu] [ACT] Exiting activity: %s\n", millis(), name.c_str());
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find and examine the Activity.cpp file
find . -name "Activity.cpp" -type f | head -5

Repository: ngxson/crosspoint-reader

Length of output: 97


🌐 Web query:

FreeRTOS vTaskDelete nullptr current task behavior

💡 Result:

In FreeRTOS, calling:

vTaskDelete(NULL);

means “delete the calling (current) task”. Passing NULL is explicitly defined to delete the task that calls vTaskDelete(). [1][2]

What happens next:

  • The task is removed from the scheduler’s lists (ready/blocked/suspended/event lists), so it will no longer be scheduled. [1]
  • The task’s kernel-allocated resources (TCB + stack) are not necessarily freed immediately. They are freed later by the Idle task, so the Idle task must get CPU time or cleanup will be delayed. [1][2]
  • Anything your task allocated itself (e.g., heap blocks via pvPortMalloc(), queues, semaphores, buffers, file/library resources, etc.) is not automatically freed by vTaskDelete()—you must free/close those yourself before deleting the task. [1][3]
  • If you stored the task handle in a variable somewhere, FreeRTOS will not set your variable to NULL; if you need that, another task must do it (the deleting task can’t run code “after” deleting itself). [4]

Practical implication: after vTaskDelete(NULL), your task should be considered gone; don’t write code that expects it to continue running, and ensure the Idle task isn’t starved so the TCB/stack cleanup can occur. [1][2]

Sources:
[1] FreeRTOS vTaskDelete() API documentation (task.h / Doxygen mirror)
[2] FreeRTOS forum archive: “Query about task deletion” (Richard Barry)
[3] FreeRTOS forum: “vTaskDelete memory free”
[4] FreeRTOS forum: “Deleting a task and erasing it’s handler”


🏁 Script executed:

# Also check the header file to understand the context and class definition
find . -name "Activity.h" -o -name "Activity.hpp" | head -5

Repository: ngxson/crosspoint-reader

Length of output: 95


🏁 Script executed:

# Examine the Activity.cpp file at lines 18-35
sed -n '1,50p' ./src/activities/Activity.cpp

Repository: ngxson/crosspoint-reader

Length of output: 1462


🏁 Script executed:

# Check the header file to understand the class structure
cat ./src/activities/Activity.h

Repository: ngxson/crosspoint-reader

Length of output: 1603


🏁 Script executed:

# Search for callers of onEnter() to understand if early return is acceptable
rg "\.onEnter\(\)" --max-count 20

Repository: ngxson/crosspoint-reader

Length of output: 50


🏁 Script executed:

# Search for places where Activity is instantiated or where onEnter/onExit are called
rg "onEnter|onExit" --type cpp --type h -B 2 -A 2

Repository: ngxson/crosspoint-reader

Length of output: 39186


🏁 Script executed:

# Check if there are derived classes that override these methods
rg "class.*:.*Activity" --type h

Repository: ngxson/crosspoint-reader

Length of output: 3229


Guard task deletion to prevent deleting the current task if render-task creation fails.

Line 19 ignores xTaskCreate's return value; only an assert guards against null handle, which is compiled out in release builds. If creation fails, renderTaskHandle remains null. In onExit() (line 31), calling vTaskDelete(nullptr) deletes the current calling task per FreeRTOS semantics, which is catastrophic—you'd delete the task executing onExit() (the main/UI loop) instead of the render task.

Check the return value of xTaskCreate and guard the deletion:

Suggested fix
 void Activity::onEnter() {
-  xTaskCreate(&renderTaskTrampoline, name.c_str(),
-              8192,              // Stack size
-              this,              // Parameters
-              1,                 // Priority
-              &renderTaskHandle  // Task handle
-  );
-  assert(renderTaskHandle != nullptr && "Failed to create render task");
+  const BaseType_t ok = xTaskCreate(&renderTaskTrampoline, name.c_str(),
+                                   8192,              // Stack size
+                                   this,              // Parameters
+                                   1,                 // Priority
+                                   &renderTaskHandle  // Task handle
+  );
+  if (ok != pdPASS || renderTaskHandle == nullptr) {
+    renderTaskHandle = nullptr;
+    Serial.printf("[%lu] [ACT] ERROR: Failed to create render task for %s\n", millis(), name.c_str());
+    return;
+  }
   Serial.printf("[%lu] [ACT] Entering activity: %s\n", millis(), name.c_str());
 }
 
 void Activity::onExit() {
-  RenderLock lock(*this);  // Ensure we don't delete the task while it's rendering
-  vTaskDelete(renderTaskHandle);
-  renderTaskHandle = nullptr;
+  if (renderTaskHandle) {
+    RenderLock lock(*this);  // Ensure we don't delete the task while it's rendering
+    vTaskDelete(renderTaskHandle);
+    renderTaskHandle = nullptr;
+  }
 
   Serial.printf("[%lu] [ACT] Exiting activity: %s\n", millis(), name.c_str());
 }
🤖 Prompt for AI Agents
In `@src/activities/Activity.cpp` around lines 18 - 35, The onEnter() call to
xTaskCreate currently ignores its return value and relies on an assert, so if
task creation fails renderTaskHandle stays null and onExit()'s
vTaskDelete(renderTaskHandle) can delete the current task; update onEnter() to
capture xTaskCreate's BaseType_t result (from calling renderTaskTrampoline) and
only assign/use renderTaskHandle if xTaskCreate returns pdPASS, logging or
handling the failure, and change onExit() to check renderTaskHandle != nullptr
before calling vTaskDelete (and still use RenderLock to protect rendering);
reference functions/vars: onEnter, onExit, xTaskCreate, renderTaskTrampoline,
renderTaskHandle, vTaskDelete, RenderLock.

@ngxson
Copy link
Owner Author

ngxson commented Feb 9, 2026

@coderabbitai scan for migration code that I may forgot to copy or to reimplement (in other words, I may delete something by accident). skip reviewing bugs that are already existed there

@coderabbitai
Copy link

coderabbitai bot commented Feb 9, 2026

Tip

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

🧩 Analysis chain

@ngxson, I'll scan the codebase for any accidentally deleted or missing migration code. Let me examine the changes systematically to identify any logic that may have been removed beyond the threading infrastructure.


🏁 Script executed:

#!/bin/bash
# Check for any TODO or FIXME comments that might indicate incomplete migration
rg -n "TODO|FIXME" --type cpp --type-add 'h:*.h' --type h src/activities/

# Look for any commented-out code that might indicate incomplete migration
rg -n "^[[:space:]]*//" src/activities/ -A 2 -B 2 | head -100

Length of output: 7017


🏁 Script executed:

#!/bin/bash
# Check Activity.cpp implementation for completeness
cat src/activities/Activity.cpp

Length of output: 1475


🏁 Script executed:

#!/bin/bash
# Examine a few complex activities to verify logic preservation
# Let's check OpdsBookBrowserActivity for any missing logic
cat src/activities/browser/OpdsBookBrowserActivity.cpp | head -200

Length of output: 6816


🏁 Script executed:

#!/bin/bash
# Check WifiSelectionActivity as it had significant changes according to the summary
cat src/activities/network/WifiSelectionActivity.cpp | grep -A 5 -B 5 "RenderLock"

Length of output: 1285


🏁 Script executed:

#!/bin/bash
# Look for any orphaned function calls or references to removed functions
rg -n "displayTaskLoop|taskTrampoline|updateRequired" src/activities/ --type cpp --type-add 'h:*.h' --type h

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check ButtonRemapActivity which had complex logic changes
cat src/activities/settings/ButtonRemapActivity.cpp | grep -A 10 -B 5 "errorMessage\|errorUntil"

Length of output: 2234


🏁 Script executed:

#!/bin/bash
# Check if any vTaskDelay calls were removed that might affect timing
git diff HEAD~3 HEAD -- src/activities/ | grep -C 3 "vTaskDelay"

Length of output: 4735


🏁 Script executed:

#!/bin/bash
# Look for any state initialization that might have been in onEnter
# Focus on activities that had complex setup
cat src/activities/reader/EpubReaderActivity.cpp | grep -A 20 "void EpubReaderActivity::onEnter"

Length of output: 664


🏁 Script executed:

#!/bin/bash
# Check if any cleanup logic was in the old onExit functions
# that might have been lost
git diff HEAD~3 HEAD -- src/activities/home/HomeActivity.cpp | grep -C 5 "onExit"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check for any conditional logic that was in displayTaskLoop functions
# that might have been business logic rather than just rendering
git diff HEAD~3 HEAD -- src/activities/settings/ButtonRemapActivity.cpp | grep -A 10 "displayTaskLoop"

Length of output: 805


🏁 Script executed:

#!/bin/bash
# Look for any state checks or validations that were removed
git diff HEAD~3 HEAD -- src/activities/ | grep -C 3 "if.*state\|if.*status\|if.*error" | head -100

Length of output: 3386


🏁 Script executed:

#!/bin/bash
# Check WifiSelectionActivity render for the vTaskDelay issue
cat src/activities/network/WifiSelectionActivity.cpp | grep -A 20 "void WifiSelectionActivity::render"

Length of output: 5468


🏁 Script executed:

#!/bin/bash
# Check KOReaderSyncActivity for any lingering vTaskDelay with mutex usage
cat src/activities/reader/KOReaderSyncActivity.cpp | grep -B 5 -A 10 "vTaskDelay\|renderingMutex"

Length of output: 4974


🏁 Script executed:

#!/bin/bash
# Check ClearCacheActivity for vTaskDelay with mutex
cat src/activities/settings/ClearCacheActivity.cpp | grep -B 5 -A 10 "vTaskDelay\|renderingMutex"

Length of output: 610


🏁 Script executed:

#!/bin/bash
# Check OtaUpdateActivity for vTaskDelay with mutex
cat src/activities/settings/OtaUpdateActivity.cpp | grep -B 5 -A 10 "vTaskDelay\|renderingMutex"

Length of output: 2125


🏁 Script executed:

#!/bin/bash
# Also check if ActivityWithSubactivity was properly updated
cat src/activities/ActivityWithSubactivity.cpp

Length of output: 928


🏁 Script executed:

#!/bin/bash
# Check for any direct xSemaphoreTake calls that don't use RenderLock
rg -n "xSemaphoreTake\(renderingMutex" src/activities/ --type cpp

Length of output: 2720


🏁 Script executed:

#!/bin/bash
# Get a complete picture - check how many xSemaphoreGive calls match the xSemaphoreTake
rg -n "xSemaphoreGive\(renderingMutex" src/activities/ --type cpp

Length of output: 2411


🏁 Script executed:

#!/bin/bash
# Check EpubReaderActivity for context around mutex usage
cat src/activities/reader/EpubReaderActivity.cpp | grep -B 3 -A 3 "xSemaphoreTake\(renderingMutex" | head -60

Length of output: 23


🏁 Script executed:

#!/bin/bash
# Verify Activity.h has renderingMutex as protected/public
cat src/activities/Activity.h | grep -A 5 -B 5 "renderingMutex"

Length of output: 810


🏁 Script executed:

#!/bin/bash
# Check for the pattern - escaping the parenthesis properly
rg -n "xSemaphoreTake.*renderingMutex" src/activities/reader/EpubReaderActivity.cpp -A 3

Length of output: 1041


🏁 Script executed:

#!/bin/bash
# Check KOReaderSettingsActivity for imbalanced mutex usage
cat src/activities/settings/KOReaderSettingsActivity.cpp | grep -B 5 -A 5 "xSemaphoreGive.*renderingMutex"

Length of output: 373


🏁 Script executed:

#!/bin/bash
# Check if there are any early returns between take/give that could cause leaks
rg -n "xSemaphoreTake.*renderingMutex" src/activities/ --type cpp -A 10 | grep -B 5 "return"

Length of output: 5458


🏁 Script executed:

#!/bin/bash
# Final check - verify all activities properly inherit from Activity
rg "class.*Activity.*:.*public" src/activities/ --type h | head -20

Length of output: 2286


@ngxson, I found several incomplete migrations and actual bugs that need to be fixed. Here's a comprehensive report:

🔴 Critical Issues Found

1. Imbalanced Mutex Usage (Will Cause Crashes/Deadlocks)

OtaUpdateActivity.cpp - Mutex Leak:

  • 8 xSemaphoreTake(renderingMutex, ...) calls
  • Only 7 xSemaphoreGive(renderingMutex) calls
  • Impact: This will cause a mutex leak and eventual deadlock.

KOReaderSettingsActivity.cpp - Line 123:

  • Has xSemaphoreGive(renderingMutex) without a matching xSemaphoreTake
  • Impact: Will cause assertion failure or undefined behavior when trying to release a mutex that wasn't acquired.

2. vTaskDelay() in render() Method

WifiSelectionActivity.cpp - Lines 4-7:

void WifiSelectionActivity::render() {
  if (state == WifiSelectionState::PASSWORD_ENTRY) {
    vTaskDelay(10 / portTICK_PERIOD_MS);  // ❌ WRONG - render() is now called from centralized task
    return;
  }
  • Impact: The render() method is now called by the centralized rendering task in the base Activity class. Adding delays here will block all rendering unnecessarily. This line should simply return; without the delay.

3. Inconsistent Use of RenderLock RAII Pattern

The migration introduced Activity::RenderLock as a safer RAII wrapper for mutex operations, but several activities still use manual xSemaphoreTake/xSemaphoreGive:

Files with manual mutex usage:

  • EpubReaderActivity.cpp: 6 occurrences (lines 224, 244, 256, 328, 406, 461)
  • KOReaderSyncActivity.cpp: 21 occurrences
  • KOReaderAuthActivity.cpp: 3 occurrences (lines 17, 25, 37)
  • ClearCacheActivity.cpp: 1 occurrence (line 121)
  • OtaUpdateActivity.cpp: 8 occurrences

Why this matters: Manual mutex operations are error-prone (as evidenced by the imbalanced usage above). The RenderLock RAII pattern ensures mutexes are always released even during early returns or exceptions.

Example of proper pattern:

// ❌ Old pattern (error-prone)
xSemaphoreTake(renderingMutex, portMAX_DELAY);
state = SYNCING;
xSemaphoreGive(renderingMutex);

// ✅ New pattern (safe)
{
  RenderLock lock(*this);
  state = SYNCING;
} // Mutex automatically released

4. vTaskDelay with Mutex (Minor Issue)

KOReaderSyncActivity.cpp, OtaUpdateActivity.cpp, ClearCacheActivity.cpp use vTaskDelay after mutex operations. While the mutex is properly released before the delay, this pattern suggests these are background tasks that might need review.

📋 Recommended Fixes

  1. Fix imbalanced mutex usage immediately in OtaUpdateActivity.cpp and KOReaderSettingsActivity.cpp
  2. Remove vTaskDelay from WifiSelectionActivity::render()
  3. Refactor all manual xSemaphoreTake/xSemaphoreGive pairs to use RenderLock RAII wrapper
  4. Review background task patterns in KOReaderSyncActivity, OtaUpdateActivity, and ClearCacheActivity

All other aspects of the migration appear correct - the centralized rendering task infrastructure, requestUpdate() mechanism, and removal of per-activity rendering tasks are properly implemented.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (4)
src/activities/util/KeyboardEntryActivity.cpp (1)

217-232: ⚠️ Potential issue | 🟡 Minor

Potential infinite loop if text cannot fit.

The while(true) loop decrements lineEndIdx when text is too wide. If even a single character exceeds pageWidth - 40, the loop will never terminate because an empty substring still advances lineStartIdx while the remaining text never fits.

Consider adding a safeguard to break when no progress can be made:

🛡️ Proposed fix to prevent infinite loop
   while (true) {
     std::string lineText = displayText.substr(lineStartIdx, lineEndIdx - lineStartIdx);
     const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, lineText.c_str());
     if (textWidth <= pageWidth - 40) {
       renderer.drawText(UI_10_FONT_ID, 20, inputEndY, lineText.c_str());
       if (lineEndIdx == displayText.length()) {
         break;
       }

       inputEndY += renderer.getLineHeight(UI_10_FONT_ID);
       lineStartIdx = lineEndIdx;
       lineEndIdx = displayText.length();
     } else {
+      if (lineEndIdx <= lineStartIdx + 1) {
+        // Can't fit even one character; force draw and advance to prevent infinite loop
+        renderer.drawText(UI_10_FONT_ID, 20, inputEndY, lineText.c_str());
+        inputEndY += renderer.getLineHeight(UI_10_FONT_ID);
+        lineStartIdx = lineEndIdx;
+        lineEndIdx = displayText.length();
+        if (lineStartIdx >= displayText.length()) break;
+        continue;
+      }
       lineEndIdx -= 1;
     }
   }
src/activities/settings/KOReaderSettingsActivity.cpp (1)

120-126: ⚠️ Potential issue | 🔴 Critical

Critical: Orphaned xSemaphoreGive without matching xSemaphoreTake.

Line 124 calls xSemaphoreGive(renderingMutex) but there's no corresponding xSemaphoreTake in this code path. This is leftover code from the old rendering model that was not fully migrated. Since the mutex-based rendering was replaced with requestUpdate(), this line should be removed entirely.

This causes mutex imbalance which can lead to undefined behavior or corruption of the semaphore state.

🐛 Proposed fix
   } else if (selectedIndex == 4) {
     // Authenticate
     if (!KOREADER_STORE.hasCredentials()) {
       // Can't authenticate without credentials - just show message briefly
-      xSemaphoreGive(renderingMutex);
       return;
     }
src/activities/home/MyLibraryActivity.cpp (2)

75-83: ⚠️ Potential issue | 🟡 Minor

Request an update after long‑press “go home”.
The basepath and file list change, but the function returns without notifying the render task, so the UI may not refresh until the next input.

✅ Suggested fix
   if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS &&
       basepath != "/") {
     basepath = "/";
     loadFiles();
     selectorIndex = 0;
+    requestUpdate();
     return;
   }

62-68: ⚠️ Potential issue | 🟠 Major

Add requestUpdate() call after loading files in onEnter().

The render task blocks on ulTaskNotifyTake() waiting for a notification. Without calling requestUpdate() after loading data, the render task never executes and the screen remains blank until an update is triggered elsewhere.

✅ Suggested fix
 void MyLibraryActivity::onEnter() {
   Activity::onEnter();

   loadFiles();

   selectorIndex = 0;
+  requestUpdate();
 }
🤖 Fix all issues with AI agents
In `@src/activities/network/WifiSelectionActivity.cpp`:
- Around line 416-422: The render() method in WifiSelectionActivity is blocking
the centralized render task by calling vTaskDelay when state ==
WifiSelectionState::PASSWORD_ENTRY; remove the vTaskDelay call so render()
simply returns early in that case (keep the state check and return) to avoid
blocking the global render loop and ensure no other blocking calls are
introduced in WifiSelectionActivity::render.

In `@src/activities/network/WifiSelectionActivity.h`:
- Around line 95-99: The render() method in WifiSelectionActivity currently
calls vTaskDelay(10 / portTICK_PERIOD_MS), which blocks centralized rendering
during PASSWORD_ENTRY transitions; remove that vTaskDelay call from
WifiSelectionActivity::render() and replace it with an immediate return so
render() doesn't stall the renderer, and move any timing or wait logic into the
activity state machine (e.g., handle delays in loop() or onEnter()/onExit(), or
trigger redraws via requestUpdate() when the PASSWORD_ENTRY state changes) so
that render() only draws and never blocks.

In `@src/activities/reader/EpubReaderPercentSelectionActivity.h`:
- Around line 23-29: The comment above the percent field in
EpubReaderPercentSelectionActivity is stale—remove or update the "Render dirty
flag for the task loop" text since there is no render-dirty flag; edit the
comment near the int percent = 0; declaration in
EpubReaderPercentSelectionActivity (and any nearby comments related to
render/task loop) to either delete the stale line or replace it with a short,
accurate description (e.g., "Current slider percent (0-100)") so comments match
the current state of render() and the percent member.

Comment on lines 416 to 422
void WifiSelectionActivity::render() {
// Don't render if we're in PASSWORD_ENTRY state - we're just transitioning
// from the keyboard subactivity back to the main activity
if (state == WifiSelectionState::PASSWORD_ENTRY) {
vTaskDelay(10 / portTICK_PERIOD_MS);
return;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove vTaskDelay from render() — blocking is inappropriate in the centralized rendering model.

With the centralized render task calling render(), inserting a delay here blocks the render task unnecessarily. For a state where no rendering should occur, simply return early.

Proposed fix
 void WifiSelectionActivity::render() {
   // Don't render if we're in PASSWORD_ENTRY state - we're just transitioning
   // from the keyboard subactivity back to the main activity
   if (state == WifiSelectionState::PASSWORD_ENTRY) {
-    vTaskDelay(10 / portTICK_PERIOD_MS);
     return;
   }
📝 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
void WifiSelectionActivity::render() {
// Don't render if we're in PASSWORD_ENTRY state - we're just transitioning
// from the keyboard subactivity back to the main activity
if (state == WifiSelectionState::PASSWORD_ENTRY) {
vTaskDelay(10 / portTICK_PERIOD_MS);
return;
}
void WifiSelectionActivity::render() {
// Don't render if we're in PASSWORD_ENTRY state - we're just transitioning
// from the keyboard subactivity back to the main activity
if (state == WifiSelectionState::PASSWORD_ENTRY) {
return;
}
🤖 Prompt for AI Agents
In `@src/activities/network/WifiSelectionActivity.cpp` around lines 416 - 422, The
render() method in WifiSelectionActivity is blocking the centralized render task
by calling vTaskDelay when state == WifiSelectionState::PASSWORD_ENTRY; remove
the vTaskDelay call so render() simply returns early in that case (keep the
state check and return) to avoid blocking the global render loop and ensure no
other blocking calls are introduced in WifiSelectionActivity::render.

Comment on lines 95 to 99
void onEnter() override;
void onExit() override;
void loop() override;
void render() override;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if the file exists and examine vTaskDelay usage
if [ -f "src/activities/network/WifiSelectionActivity.cpp" ]; then
  echo "=== vTaskDelay usage in WifiSelectionActivity.cpp ==="
  rg -n "vTaskDelay" src/activities/network/WifiSelectionActivity.cpp -A 2 -B 2
else
  echo "File not found at expected path"
  # Try to find it
  find . -name "WifiSelectionActivity.cpp" -type f 2>/dev/null | head -5
fi

Repository: ngxson/crosspoint-reader

Length of output: 312


🏁 Script executed:

#!/bin/bash
# Also check the header file to verify the method signatures
if [ -f "src/activities/network/WifiSelectionActivity.h" ]; then
  echo "=== Header file context (lines 90-105) ==="
  sed -n '90,105p' src/activities/network/WifiSelectionActivity.h
else
  find . -name "WifiSelectionActivity.h" -type f 2>/dev/null | head -5
fi

Repository: ngxson/crosspoint-reader

Length of output: 623


🏁 Script executed:

#!/bin/bash
# Check the render method implementation entirely
if [ -f "src/activities/network/WifiSelectionActivity.cpp" ]; then
  echo "=== render() method ==="
  rg -n "void WifiSelectionActivity::render" src/activities/network/WifiSelectionActivity.cpp -A 50
fi

Repository: ngxson/crosspoint-reader

Length of output: 9500


Remove vTaskDelay from the render path to prevent stalling centralized rendering.

The vTaskDelay(10 / portTICK_PERIOD_MS) call in render() blocks rendering during PASSWORD_ENTRY state transitions. Since render() is now centralized, this delay affects the entire activity rendering pipeline. Replace it with a plain return and handle timing via state transitions or requestUpdate() calls.

Change in WifiSelectionActivity.cpp (lines 419-421)
  if (state == WifiSelectionState::PASSWORD_ENTRY) {
-   vTaskDelay(10 / portTICK_PERIOD_MS);
    return;
  }
🤖 Prompt for AI Agents
In `@src/activities/network/WifiSelectionActivity.h` around lines 95 - 99, The
render() method in WifiSelectionActivity currently calls vTaskDelay(10 /
portTICK_PERIOD_MS), which blocks centralized rendering during PASSWORD_ENTRY
transitions; remove that vTaskDelay call from WifiSelectionActivity::render()
and replace it with an immediate return so render() doesn't stall the renderer,
and move any timing or wait logic into the activity state machine (e.g., handle
delays in loop() or onEnter()/onExit(), or trigger redraws via requestUpdate()
when the PASSWORD_ENTRY state changes) so that render() only draws and never
blocks.

Comment on lines +23 to +29
void render() override;

private:
// Current percent value (0-100) shown on the slider.
int percent = 0;
// Render dirty flag for the task loop.
bool updateRequired = false;
// FreeRTOS task and mutex for rendering.
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove the stale render-task comment.
The render-dirty flag is gone, but the comment still references the old task loop, which is misleading.

🧹 Suggested cleanup
-  // Render dirty flag for the task loop.
-
📝 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
void render() override;
private:
// Current percent value (0-100) shown on the slider.
int percent = 0;
// Render dirty flag for the task loop.
bool updateRequired = false;
// FreeRTOS task and mutex for rendering.
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
void render() override;
private:
// Current percent value (0-100) shown on the slider.
int percent = 0;
🤖 Prompt for AI Agents
In `@src/activities/reader/EpubReaderPercentSelectionActivity.h` around lines 23 -
29, The comment above the percent field in EpubReaderPercentSelectionActivity is
stale—remove or update the "Render dirty flag for the task loop" text since
there is no render-dirty flag; edit the comment near the int percent = 0;
declaration in EpubReaderPercentSelectionActivity (and any nearby comments
related to render/task loop) to either delete the stale line or replace it with
a short, accurate description (e.g., "Current slider percent (0-100)") so
comments match the current state of render() and the percent member.

Co-authored-by: znelson <[email protected]>
Copy link

@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: 1

Caution

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

⚠️ Outside diff range comments (2)
src/activities/settings/ButtonRemapActivity.cpp (1)

35-93: ⚠️ Potential issue | 🔴 Critical

Avoid calling onBack() while holding RenderLock (deadlock risk).
If onBack triggers Activity::onExit, it will attempt to take the same render mutex and can deadlock. Release the lock before navigation.

Suggested fix
-  {
-    RenderLock lock(*this);
+  bool shouldExit = false;
+  {
+    RenderLock lock(*this);
     // Wait for a front button press to assign to the current role.
     const int pressedButton = mappedInput.getPressedFrontButton();
     if (pressedButton < 0) {
       return;
     }
@@
     tempMapping[currentStep] = static_cast<uint8_t>(pressedButton);
     currentStep++;
 
     if (currentStep >= kRoleCount) {
       // All roles assigned; save to settings and exit.
       applyTempMapping();
       SETTINGS.saveToFile();
-      onBack();
-      return;
+      shouldExit = true;
+    } else {
+      requestUpdate();
     }
-
-    requestUpdate();
   }
+  if (shouldExit) {
+    onBack();
+    return;
+  }
src/activities/settings/KOReaderSettingsActivity.cpp (1)

120-131: ⚠️ Potential issue | 🔴 Critical

Remove the stray xSemaphoreGive without ownership.

Line 124 releases the rendering mutex without any matching acquire in this code path. The handleSelection() method is not protected by a lock when called from loop(), so this unconditional give will cause FreeRTOS mutex ownership violation and trigger assertions. Remove it.

🐛 Proposed fix
-      xSemaphoreGive(renderingMutex);
       return;
🤖 Fix all issues with AI agents
In `@src/activities/settings/ClearCacheActivity.cpp`:
- Around line 121-125: Remove the inconsistent manual semaphore usage in loop():
instead of taking/releasing renderingMutex around the state assignment, directly
assign state = CLEARING (same approach used in onEnter() and clearCache()), keep
the subsequent requestUpdate() and vTaskDelay unchanged, and rely on RenderLock
in the render task for actual render protection; update any references to
renderingMutex in loop() accordingly so there is no
xSemaphoreTake/xSemaphoreGive pair surrounding the state change.
🧹 Nitpick comments (4)
src/activities/settings/OtaUpdateActivity.cpp (1)

23-50: Replace raw semaphore calls with RenderLock for state updates throughout this activity.

This keeps mutex ownership scoped and reduces the risk of future imbalance. The pattern is established across the codebase and is the idiomatic way to protect critical sections.

Apply to both occurrences: lines 23-50 and 159-178.

♻️ Example refactor for one transition (apply similarly to other state changes)
-  xSemaphoreTake(renderingMutex, portMAX_DELAY);
-  state = CHECKING_FOR_UPDATE;
-  xSemaphoreGive(renderingMutex);
-  requestUpdate();
+  {
+    Activity::RenderLock lock(*this);
+    state = CHECKING_FOR_UPDATE;
+  }
+  requestUpdate();
src/activities/reader/KOReaderSyncActivity.cpp (1)

51-58: Migrate manual mutex pairs to Activity::RenderLock RAII pattern.
RenderLock is now the standard approach across the codebase for handling rendering synchronization. This file has multiple manual xSemaphoreTake/xSemaphoreGive pairs that should be wrapped in RenderLock to reduce the risk of missed releases and maintain consistency with the render sync model.

Example refactor for lines 53–56
-  xSemaphoreTake(renderingMutex, portMAX_DELAY);
-  state = SYNCING;
-  statusMessage = "Syncing time...";
-  xSemaphoreGive(renderingMutex);
+  {
+    Activity::RenderLock lock(*this);
+    state = SYNCING;
+    statusMessage = "Syncing time...";
+  }

Apply this pattern to all remaining manual mutex pairs in this file (at least 13 other occurrences).

src/activities/Activity.h (1)

45-53: RenderLock is missing move semantics despite being passed by rvalue reference.

The render(RenderLock&&) signature suggests move semantics, but RenderLock only has deleted copy operations without explicit move constructor/assignment. While this works because the compiler won't implicitly generate move operations when copy operations are deleted, it may cause confusion and prevent storing the lock in certain scenarios.

Consider explicitly defaulting or defining move operations for clarity:

♻️ Suggested improvement
   public:
     explicit RenderLock(Activity& activity);
     RenderLock(const RenderLock&) = delete;
     RenderLock& operator=(const RenderLock&) = delete;
+    RenderLock(RenderLock&&) = default;
+    RenderLock& operator=(RenderLock&&) = delete;
     ~RenderLock();
src/activities/reader/EpubReaderActivity.cpp (1)

222-231: Consider wrapping manual mutex operations with a scoped RAII helper for consistency.

This file contains 6 paired xSemaphoreTake/xSemaphoreGive calls (at lines 224-228, 244-248, 256-260, 328-333, 406-423, 461-477) that protect inter-task synchronization in the loop() function. While these operations are correctly paired and necessary to prevent concurrent access to the section object during render, the manual mutex handling is inconsistent with the RAII RenderLock pattern used elsewhere.

Notably, other reader activities (ReaderActivity, TxtReaderActivity, XtcReaderActivity) do not employ this pattern, making EpubReaderActivity an outlier. A scoped mutex wrapper (e.g., a simple RAII guard) around these critical sections would improve code consistency and reduce error-proneness without requiring architectural changes.

Comment on lines 121 to 125
xSemaphoreTake(renderingMutex, portMAX_DELAY);
state = CLEARING;
xSemaphoreGive(renderingMutex);
updateRequired = true;
requestUpdate();
vTaskDelay(10 / portTICK_PERIOD_MS);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "renderingMutex" -S src/activities

Repository: ngxson/crosspoint-reader

Length of output: 6069


🏁 Script executed:

rg -n "RenderLock" -S src/activities

Repository: ngxson/crosspoint-reader

Length of output: 6559


🏁 Script executed:

# Find the ClearCacheActivity.cpp file and examine the onEnter method
fd "ClearCacheActivity.cpp" src/

Repository: ngxson/crosspoint-reader

Length of output: 114


🏁 Script executed:

sed -n '1,150p' src/activities/settings/ClearCacheActivity.cpp | cat -n

Repository: ngxson/crosspoint-reader

Length of output: 5828


🏁 Script executed:

# Get more context around the area
sed -n '100,140p' src/activities/settings/ClearCacheActivity.cpp | cat -n

Repository: ngxson/crosspoint-reader

Length of output: 1485


🏁 Script executed:

# Check the full file to understand the method structure
wc -l src/activities/settings/ClearCacheActivity.cpp

Repository: ngxson/crosspoint-reader

Length of output: 118


🏁 Script executed:

# Search for state variable usage in ClearCacheActivity
rg -n "state" src/activities/settings/ClearCacheActivity.cpp

# Check Activity base class for requestUpdate implementation
rg -n "requestUpdate" src/activities/Activity.h src/activities/Activity.cpp

Repository: ngxson/crosspoint-reader

Length of output: 483


🏁 Script executed:

# Get the requestUpdate implementation
sed -n '37,50p' src/activities/Activity.cpp | cat -n

Repository: ngxson/crosspoint-reader

Length of output: 561


🏁 Script executed:

# Check the header definition
sed -n '35,45p' src/activities/Activity.h | cat -n

Repository: ngxson/crosspoint-reader

Length of output: 542


🏁 Script executed:

# Check the full Activity.cpp to understand the rendering flow
head -60 src/activities/Activity.cpp | cat -n

Repository: ngxson/crosspoint-reader

Length of output: 1908


Remove inconsistent semaphore usage in loop() method.

The code at lines 121-125 uses manual xSemaphoreTake/xSemaphoreGive around the state assignment, while onEnter() (line 14) and clearCache() (lines 78, 113) modify state without any semaphore protection. Since requestUpdate() only sends a task notification and the actual render protection comes from RenderLock acquired when the render task runs, this manual semaphore pattern is unnecessary and creates inconsistency.

♻️ Proposed simplification
-      xSemaphoreTake(renderingMutex, portMAX_DELAY);
       state = CLEARING;
-      xSemaphoreGive(renderingMutex);
       requestUpdate();
📝 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
xSemaphoreTake(renderingMutex, portMAX_DELAY);
state = CLEARING;
xSemaphoreGive(renderingMutex);
updateRequired = true;
requestUpdate();
vTaskDelay(10 / portTICK_PERIOD_MS);
state = CLEARING;
requestUpdate();
vTaskDelay(10 / portTICK_PERIOD_MS);
🤖 Prompt for AI Agents
In `@src/activities/settings/ClearCacheActivity.cpp` around lines 121 - 125,
Remove the inconsistent manual semaphore usage in loop(): instead of
taking/releasing renderingMutex around the state assignment, directly assign
state = CLEARING (same approach used in onEnter() and clearCache()), keep the
subsequent requestUpdate() and vTaskDelay unchanged, and rely on RenderLock in
the render task for actual render protection; update any references to
renderingMutex in loop() accordingly so there is no
xSemaphoreTake/xSemaphoreGive pair surrounding the state change.

Copy link

@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: 2

Caution

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

⚠️ Outside diff range comments (2)
src/network/OtaUpdater.cpp (1)

240-246: ⚠️ Potential issue | 🟡 Minor

Blocking the render task with 100 ms delays during OTA progress will freeze UI updates and input handling.

delay(100) at line 245 blocks the render task for each OTA chunk; since installUpdate() is called from OtaUpdateActivity::loop() (which runs on the centralized render task), this will pause rendering and input during downloads. Consider reducing the delay value—10 ms would be more responsive—or switch to a non-blocking approach once requestUpdateAndWait() is available.

src/activities/reader/KOReaderSyncActivity.cpp (1)

53-66: 🛠️ Refactor suggestion | 🟠 Major

Switch state/statusMessage updates to use RenderLock RAII for consistency and safety.

This file still uses manual xSemaphoreTake/xSemaphoreGive pairs for state updates (11+ instances), while the render() signature already accepts RenderLock&&. Migrating to the RAII pattern prevents accidental semaphore imbalance if code paths are refactored in the future, and keeps the locking strategy consistent across the activity.

Suggested pattern (apply throughout this file)
-  xSemaphoreTake(renderingMutex, portMAX_DELAY);
-  state = SYNCING;
-  statusMessage = "Syncing time...";
-  xSemaphoreGive(renderingMutex);
+  {
+    RenderLock lock(*this);
+    state = SYNCING;
+    statusMessage = "Syncing time...";
+  }
🤖 Fix all issues with AI agents
In `@src/activities/ActivityWithSubactivity.cpp`:
- Around line 3-9: exitActivity() currently calls subActivity->onExit() and
resets subActivity without holding the RenderLock, risking concurrent render
access; modify ActivityWithSubactivity::exitActivity to acquire the RenderLock
(e.g., create a RenderLock guard) before calling subActivity->onExit() and
before resetting subActivity, or alternatively call Activity::onExit() (which
stops rendering) before destroying subActivity—ensure the render task is stopped
or the RenderLock is held while mutating or resetting the subActivity pointer to
avoid use-after-free.
- Around line 25-33: ActivityWithSubactivity::requestUpdate() currently reads
and calls subActivity without synchronization and can race with
enterNewActivity()/exitActivity() which reset subActivity; fix by using the same
RenderLock (or a dedicated mutex used by enterNewActivity()/exitActivity()) to
protect access: acquire the lock, copy the subActivity shared pointer (or check
non-null), release the lock, and then call requestUpdate() on the copied pointer
(or call under the lock if that is safe with your lock ordering) so the pointer
cannot be reset concurrently; ensure you reference the same lock used by
enterNewActivity()/exitActivity() to avoid races or deadlocks.
🧹 Nitpick comments (4)
src/activities/settings/ButtonRemapActivity.cpp (1)

64-93: Remove unnecessary block braces; fix typo in FIXME.

The block {} at lines 64-93 no longer serves a scoping purpose (no RAII object or early-exit requiring it) and can be removed to reduce indentation. Also, minor typo: "one it's" → "once it's".

♻️ Suggested cleanup
-  {
-    // Wait for the UI to refresh before accepting another assignment.
-    // This avoids rapid double-presses that can advance the step without a visible redraw.
-    delay(100);  // FIXME `@ngxson` : use requestUpdateAndWait() one it's implemented
+  // Wait for the UI to refresh before accepting another assignment.
+  // This avoids rapid double-presses that can advance the step without a visible redraw.
+  delay(100);  // FIXME `@ngxson` : use requestUpdateAndWait() once it's implemented

-    // Wait for a front button press to assign to the current role.
-    const int pressedButton = mappedInput.getPressedFrontButton();
-    if (pressedButton < 0) {
-      return;
-    }
+  // Wait for a front button press to assign to the current role.
+  const int pressedButton = mappedInput.getPressedFrontButton();
+  if (pressedButton < 0) {
+    return;
+  }

-    // Update temporary mapping and advance the remap step.
-    // Only accept the press if this hardware button isn't already assigned elsewhere.
-    if (!validateUnassigned(static_cast<uint8_t>(pressedButton))) {
-      requestUpdate();
-      return;
-    }
-    tempMapping[currentStep] = static_cast<uint8_t>(pressedButton);
-    currentStep++;
-
-    if (currentStep >= kRoleCount) {
-      // All roles assigned; save to settings and exit.
-      applyTempMapping();
-      SETTINGS.saveToFile();
-      onBack();
-      return;
-    }
+  // Update temporary mapping and advance the remap step.
+  // Only accept the press if this hardware button isn't already assigned elsewhere.
+  if (!validateUnassigned(static_cast<uint8_t>(pressedButton))) {
+    requestUpdate();
+    return;
+  }
+  tempMapping[currentStep] = static_cast<uint8_t>(pressedButton);
+  currentStep++;

-    requestUpdate();
+  if (currentStep >= kRoleCount) {
+    // All roles assigned; save to settings and exit.
+    applyTempMapping();
+    SETTINGS.saveToFile();
+    onBack();
+    return;
   }
+
+  requestUpdate();
src/activities/settings/OtaUpdateActivity.cpp (1)

23-50: Use RenderLock RAII for these state transitions to complete the migration.

The onWifiSelectionComplete() method still uses manual semaphore pairs while the rest of this file has migrated to Activity::RenderLock. For consistency and to protect against semaphore imbalance on early returns, replace the manual pairs with RenderLock here as well.

♻️ Suggested pattern (apply similarly to all state updates in this method)
-  xSemaphoreTake(renderingMutex, portMAX_DELAY);
-  state = CHECKING_FOR_UPDATE;
-  xSemaphoreGive(renderingMutex);
+  {
+    RenderLock lock(*this);
+    state = CHECKING_FOR_UPDATE;
+  }
src/activities/settings/ClearCacheActivity.cpp (1)

121-126: Remove the 100ms delay after requestUpdate() or replace with proper synchronization.

The event-driven render task is signaled via xTaskNotify(), which wakes it immediately; an artificial delay provides no benefit and unnecessarily blocks the calling thread. Once requestUpdateAndWait() is implemented as planned, use that instead to wait for render completion without wasting CPU cycles.

src/activities/reader/KOReaderSyncActivity.cpp (1)

88-93: Remove blocking delays after requestUpdate() or replace with requestUpdateAndWait().

The delay(100) calls at lines 92 and 138 block the sync task unnecessarily. Since rendering runs in a dedicated task, waiting is not needed. Either drop the delay entirely or implement requestUpdateAndWait() to wait for the render to complete. This pattern appears across multiple activities (see WifiSelectionActivity, ButtonRemapActivity, ClearCacheActivity, OtaUpdateActivity).

Comment on lines 3 to 9
void ActivityWithSubactivity::exitActivity() {
// No need to lock, since onExit() already acquires its own lock
if (subActivity) {
Serial.printf("[%lu] [ACT] Exiting subactivity...\n", millis());
subActivity->onExit();
subActivity.reset();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Lock sub-activity teardown to avoid render/data races.

exitActivity() mutates and destroys subActivity without a RenderLock, and Activity::onExit() acquires the lock only after this call. The render task can still run and dereference subActivity concurrently, risking use-after-free. Guard the teardown with RenderLock (or move the base Activity::onExit() earlier, but ensure the render task is stopped before subActivity destruction).

🔧 Suggested fix
 void ActivityWithSubactivity::exitActivity() {
-  // No need to lock, since onExit() already acquires its own lock
+  RenderLock lock(*this);
   if (subActivity) {
     Serial.printf("[%lu] [ACT] Exiting subactivity...\n", millis());
     subActivity->onExit();
     subActivity.reset();
   }
 }
📝 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
void ActivityWithSubactivity::exitActivity() {
// No need to lock, since onExit() already acquires its own lock
if (subActivity) {
Serial.printf("[%lu] [ACT] Exiting subactivity...\n", millis());
subActivity->onExit();
subActivity.reset();
}
void ActivityWithSubactivity::exitActivity() {
RenderLock lock(*this);
if (subActivity) {
Serial.printf("[%lu] [ACT] Exiting subactivity...\n", millis());
subActivity->onExit();
subActivity.reset();
}
🤖 Prompt for AI Agents
In `@src/activities/ActivityWithSubactivity.cpp` around lines 3 - 9,
exitActivity() currently calls subActivity->onExit() and resets subActivity
without holding the RenderLock, risking concurrent render access; modify
ActivityWithSubactivity::exitActivity to acquire the RenderLock (e.g., create a
RenderLock guard) before calling subActivity->onExit() and before resetting
subActivity, or alternatively call Activity::onExit() (which stops rendering)
before destroying subActivity—ensure the render task is stopped or the
RenderLock is held while mutating or resetting the subActivity pointer to avoid
use-after-free.

Comment on lines +25 to +33
void ActivityWithSubactivity::requestUpdate() {
if (subActivity) {
// Note: no need lock here. If we reach this branch, that mean enterNewActivity() must have been called, acquired
// the lock and make sure there is no pending render requests before passing the control to subActivity
subActivity->requestUpdate();
} else {
Activity::requestUpdate();
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make requestUpdate() safe during sub-activity swaps.

requestUpdate() reads subActivity and calls into it without synchronization. If enterNewActivity()/exitActivity() runs concurrently, this can race with subActivity.reset() and crash. Protect the read/call with the same RenderLock (or a dedicated mutex for sub-activity ownership).

🔧 Suggested fix
 void ActivityWithSubactivity::requestUpdate() {
-  if (subActivity) {
+  RenderLock lock(*this);
+  if (subActivity) {
     // Note: no need lock here. If we reach this branch, that mean enterNewActivity() must have been called, acquired
     // the lock and make sure there is no pending render requests before passing the control to subActivity
     subActivity->requestUpdate();
   } else {
     Activity::requestUpdate();
   }
 }
📝 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
void ActivityWithSubactivity::requestUpdate() {
if (subActivity) {
// Note: no need lock here. If we reach this branch, that mean enterNewActivity() must have been called, acquired
// the lock and make sure there is no pending render requests before passing the control to subActivity
subActivity->requestUpdate();
} else {
Activity::requestUpdate();
}
}
void ActivityWithSubactivity::requestUpdate() {
RenderLock lock(*this);
if (subActivity) {
// Note: no need lock here. If we reach this branch, that mean enterNewActivity() must have been called, acquired
// the lock and make sure there is no pending render requests before passing the control to subActivity
subActivity->requestUpdate();
} else {
Activity::requestUpdate();
}
}
🤖 Prompt for AI Agents
In `@src/activities/ActivityWithSubactivity.cpp` around lines 25 - 33,
ActivityWithSubactivity::requestUpdate() currently reads and calls subActivity
without synchronization and can race with enterNewActivity()/exitActivity()
which reset subActivity; fix by using the same RenderLock (or a dedicated mutex
used by enterNewActivity()/exitActivity()) to protect access: acquire the lock,
copy the subActivity shared pointer (or check non-null), release the lock, and
then call requestUpdate() on the copied pointer (or call under the lock if that
is safe with your lock ordering) so the pointer cannot be reset concurrently;
ensure you reference the same lock used by enterNewActivity()/exitActivity() to
avoid races or deadlocks.

Copy link

@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: 2

Caution

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

⚠️ Outside diff range comments (2)
src/activities/home/MyLibraryActivity.cpp (1)

118-126: ⚠️ Potential issue | 🟠 Major

Add requestUpdate() after long-press BACK navigation.

This path mutates basepath/files but returns without signaling the centralized renderer, so the UI can remain stale after a long-press BACK.

🐛 Proposed fix
   if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= GO_HOME_MS &&
       basepath != "/") {
     basepath = "/";
     loadFiles();
     selectorIndex = 0;
+    requestUpdate();
     return;
   }
src/activities/network/WifiSelectionActivity.cpp (1)

247-249: ⚠️ Potential issue | 🟠 Major

Incomplete migration: replace manual mutex with RAII RenderLock.

This is the only location in this file still using manual xSemaphoreTake/xSemaphoreGive. Other SD card operations (lines 20, 329, 357) correctly use RenderLock. Manual mutex handling is error-prone and inconsistent with the refactor's goal.

Proposed fix
-    xSemaphoreTake(renderingMutex, portMAX_DELAY);
-    WIFI_STORE.setLastConnectedSsid(selectedSSID);
-    xSemaphoreGive(renderingMutex);
+    {
+      RenderLock lock(*this);
+      WIFI_STORE.setLastConnectedSsid(selectedSSID);
+    }
🤖 Fix all issues with AI agents
In `@src/activities/settings/OtaUpdateActivity.cpp`:
- Around line 150-155: The render flag in OtaUpdater (observed via
OtaUpdater::getRender()) is never cleared after OtaUpdater::installUpdate()
finishes, causing OtaUpdateActivity::loop() to repeatedly call requestUpdate();
fix by adding a public mutator (e.g., OtaUpdater::clearRender() or
OtaUpdater::setRender(false)) or by updating OtaUpdater::installUpdate() to
reset the internal render flag to false after the download/install completes,
and then call that new method from OtaUpdateActivity::loop() (or let
installUpdate() clear it) so getRender() returns false after completion.

In `@src/activities/settings/SettingsActivity.cpp`:
- Around line 162-166: The lambda enterSubActivity currently creates a
RenderLock then calls exitActivity() and enterNewActivity(), but
enterNewActivity() already acquires the same RenderLock which risks deadlock;
remove the redundant RenderLock acquisition from the enterSubActivity lambda so
it simply calls exitActivity() and enterNewActivity(), relying on
enterNewActivity() (and exitActivity() if needed) to provide the required mutual
exclusion around rendering.
🧹 Nitpick comments (2)
src/activities/settings/OtaUpdateActivity.cpp (2)

23-50: Complete RenderLock migration for state transitions.

Manual xSemaphoreTake/xSemaphoreGive usage around Line 23 (and the other state updates in this method) is a leftover from the old render model. Prefer Activity::RenderLock to keep locking consistent with the centralized renderer and prevent leaks on future early returns. Apply the same pattern to the NO_UPDATE branch as well.

♻️ Suggested pattern for each state update
-  xSemaphoreTake(renderingMutex, portMAX_DELAY);
-  state = CHECKING_FOR_UPDATE;
-  xSemaphoreGive(renderingMutex);
+  {
+    RenderLock lock(*this);
+    state = CHECKING_FOR_UPDATE;
+  }
-    xSemaphoreTake(renderingMutex, portMAX_DELAY);
-    state = FAILED;
-    xSemaphoreGive(renderingMutex);
+    {
+      RenderLock lock(*this);
+      state = FAILED;
+    }
-  xSemaphoreTake(renderingMutex, portMAX_DELAY);
-  state = WAITING_CONFIRMATION;
-  xSemaphoreGive(renderingMutex);
+  {
+    RenderLock lock(*this);
+    state = WAITING_CONFIRMATION;
+  }

169-170: Avoid fixed delays after requestUpdate in the activity thread.

Line 169 blocks the activity loop for 100 ms; with centralized rendering this can stall input and background work. Same pattern at Line 27. Consider a non-blocking state step or requestUpdateAndWait once available.

Comment on lines 150 to +155
void OtaUpdateActivity::loop() {
// TODO @ngxson : refactor this logic later
if (updater.getRender()) {
requestUpdate();
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.h" -o -name "*.hpp" -o -name "*.cpp" -o -name "*.cc" | xargs grep -l "OtaUpdater" | head -20

Repository: ngxson/crosspoint-reader

Length of output: 217


🏁 Script executed:

rg -l "class OtaUpdater"

Repository: ngxson/crosspoint-reader

Length of output: 92


🏁 Script executed:

rg "getRender\s*\(" -C 5

Repository: ngxson/crosspoint-reader

Length of output: 1337


🏁 Script executed:

rg "setRender|clearRender|renderFlag|renderRequest|resetRender" -C 3

Repository: ngxson/crosspoint-reader

Length of output: 4672


🏁 Script executed:

cat -n src/network/OtaUpdater.h

Repository: ngxson/crosspoint-reader

Length of output: 1151


🏁 Script executed:

cat -n src/network/OtaUpdater.cpp

Repository: ngxson/crosspoint-reader

Length of output: 11175


🏁 Script executed:

rg "requestUpdate" src/activities/settings/OtaUpdateActivity.cpp -C 10

Repository: ngxson/crosspoint-reader

Length of output: 2550


The render flag remains sticky after OTA download completes, causing requestUpdate() to be called repeatedly every loop iteration.

In OtaUpdater::installUpdate(), the render flag is set to true (line 244) during the download loop but is never reset to false after the loop exits (line 246). Since OtaUpdater has no public method to clear this flag (only a const getter), the flag stays true indefinitely, triggering requestUpdate() on every loop() tick in OtaUpdateActivity (lines 152-154).

Add a method to clear the flag or explicitly reset it after installUpdate() completes.

🤖 Prompt for AI Agents
In `@src/activities/settings/OtaUpdateActivity.cpp` around lines 150 - 155, The
render flag in OtaUpdater (observed via OtaUpdater::getRender()) is never
cleared after OtaUpdater::installUpdate() finishes, causing
OtaUpdateActivity::loop() to repeatedly call requestUpdate(); fix by adding a
public mutator (e.g., OtaUpdater::clearRender() or OtaUpdater::setRender(false))
or by updating OtaUpdater::installUpdate() to reset the internal render flag to
false after the download/install completes, and then call that new method from
OtaUpdateActivity::loop() (or let installUpdate() clear it) so getRender()
returns false after completion.

Comment on lines 162 to 166
auto enterSubActivity = [this](Activity* activity) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
RenderLock lock(*this);
exitActivity();
enterNewActivity(activity);
xSemaphoreGive(renderingMutex);
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Avoid double-locking the render mutex (deadlock risk).
enterNewActivity() already acquires RenderLock; taking another lock here can deadlock if the mutex is non-recursive. Remove the redundant lock and rely on enterNewActivity() for mutual exclusion.

🛠️ Proposed fix
-    auto enterSubActivity = [this](Activity* activity) {
-      RenderLock lock(*this);
-      exitActivity();
-      enterNewActivity(activity);
-    };
+    auto enterSubActivity = [this](Activity* activity) {
+      exitActivity();
+      enterNewActivity(activity);
+    };
📝 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
auto enterSubActivity = [this](Activity* activity) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
RenderLock lock(*this);
exitActivity();
enterNewActivity(activity);
xSemaphoreGive(renderingMutex);
};
auto enterSubActivity = [this](Activity* activity) {
exitActivity();
enterNewActivity(activity);
};
🤖 Prompt for AI Agents
In `@src/activities/settings/SettingsActivity.cpp` around lines 162 - 166, The
lambda enterSubActivity currently creates a RenderLock then calls exitActivity()
and enterNewActivity(), but enterNewActivity() already acquires the same
RenderLock which risks deadlock; remove the redundant RenderLock acquisition
from the enterSubActivity lambda so it simply calls exitActivity() and
enterNewActivity(), relying on enterNewActivity() (and exitActivity() if needed)
to provide the required mutual exclusion around rendering.

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