refactor: move render() to Activity super class, use freeRTOS notification (#774)

## Summary

Currently, each activity has to manage their own `displayTaskLoop` which
adds redundant boilerplate code. The loop is a wait loop which is also
not the best practice, as the `updateRequested` boolean is not protected
by a mutex.

In this PR:
- Move `displayTaskLoop` to the super `Activity` class
- Replace `updateRequested` with freeRTOS's [direct to task
notification](https://www.freertos.org/Documentation/02-Kernel/02-Kernel-features/03-Direct-to-task-notifications/01-Task-notifications)
- For `ActivityWithSubactivity`, whenever a sub-activity is present, the
parent's `render()` automatically goes inactive

With this change, activities now only need to expose `render()`
function, and anywhere in the code base can call `requestUpdate()` to
request a new rendering pass.

## Additional Context

In theory, this change may also make the battery life a bit better,
since one wait loop is removed. Although the equipment in my home lab
wasn't been able to verify it (the electric current is too noisy and
small). Would appreciate if anyone has any insights on this subject.

Update: I managed to hack [a small piece of
code](https://github.com/ngxson/crosspoint-reader/tree/xsn/measure_cpu_usage)
that allow tracking CPU idle time.

The CPU load does decrease a bit (1.47% down to 1.39%), which make
sense, because the display task is now sleeping most of the time unless
notified. This should translate to a slightly increase in battery life
in the long run.

```
PR:
[40012] [MEM] Free: 185856 bytes, Total: 231004 bytes, Min Free: 123316 bytes
[40012] [IDLE] Idle time: 98.61% (CPU load: 1.39%)
[50017] [MEM] Free: 185856 bytes, Total: 231004 bytes, Min Free: 123316 bytes
[50017] [IDLE] Idle time: 98.61% (CPU load: 1.39%)
[60022] [MEM] Free: 185856 bytes, Total: 231004 bytes, Min Free: 123316 bytes
[60022] [IDLE] Idle time: 98.61% (CPU load: 1.39%)

master:
[20012] [MEM] Free: 195016 bytes, Total: 231532 bytes, Min Free: 132460 bytes
[20012] [IDLE] Idle time: 98.53% (CPU load: 1.47%)
[30017] [MEM] Free: 195016 bytes, Total: 231532 bytes, Min Free: 132460 bytes
[30017] [IDLE] Idle time: 98.53% (CPU load: 1.47%)
[40022] [MEM] Free: 195016 bytes, Total: 231532 bytes, Min Free: 132460 bytes
[40022] [IDLE] Idle time: 98.53% (CPU load: 1.47%)
```

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **NO**


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Streamlined rendering architecture by consolidating update mechanisms
across all activities, improving efficiency and consistency.
* Modernized synchronization patterns for display updates to ensure
reliable, conflict-free rendering.

* **Bug Fixes**
* Enhanced rendering stability through improved locking mechanisms and
explicit update requests.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: znelson <znelson@users.noreply.github.com>
This commit is contained in:
Xuan-Son Nguyen
2026-02-16 11:11:15 +01:00
committed by GitHub
parent 0508bfc1f7
commit a616f42cb4
53 changed files with 458 additions and 1409 deletions

View File

@@ -57,11 +57,6 @@ void applyReaderOrientation(GfxRenderer& renderer, const uint8_t orientation) {
} // namespace
void EpubReaderActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderActivity*>(param);
self->displayTaskLoop();
}
void EpubReaderActivity::onEnter() {
ActivityWithSubactivity::onEnter();
@@ -73,8 +68,6 @@ void EpubReaderActivity::onEnter() {
// NOTE: This affects layout math and must be applied before any render calls.
applyReaderOrientation(renderer, SETTINGS.orientation);
renderingMutex = xSemaphoreCreateMutex();
epub->setupCacheDir();
FsFile f;
@@ -108,14 +101,7 @@ void EpubReaderActivity::onEnter() {
RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), epub->getThumbBmpPath());
// Trigger first update
updateRequired = true;
xTaskCreate(&EpubReaderActivity::taskTrampoline, "EpubReaderActivityTask",
8192, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
requestUpdate();
}
void EpubReaderActivity::onExit() {
@@ -124,14 +110,6 @@ void EpubReaderActivity::onExit() {
// Reset orientation back to portrait for the rest of the UI
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
APP_STATE.readerActivityLoadCount = 0;
APP_STATE.saveToFile();
section.reset();
@@ -146,7 +124,7 @@ void EpubReaderActivity::loop() {
if (pendingSubactivityExit) {
pendingSubactivityExit = false;
exitActivity();
updateRequired = true;
requestUpdate();
skipNextButtonCheck = true; // Skip button processing to ignore stale events
}
// Deferred go home: process after subActivity->loop() returns to avoid race condition
@@ -186,8 +164,6 @@ void EpubReaderActivity::loop() {
// Enter reader menu activity.
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
// Don't start activity transition while rendering
xSemaphoreTake(renderingMutex, portMAX_DELAY);
const int currentPage = section ? section->currentPage + 1 : 0;
const int totalPages = section ? section->pageCount : 0;
float bookProgress = 0.0f;
@@ -201,7 +177,6 @@ void EpubReaderActivity::loop() {
this->renderer, this->mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent,
SETTINGS.orientation, [this](const uint8_t orientation) { onReaderMenuBack(orientation); },
[this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); }));
xSemaphoreGive(renderingMutex);
}
// Long press BACK (1s+) goes to file selection
@@ -238,7 +213,7 @@ void EpubReaderActivity::loop() {
if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) {
currentSpineIndex = epub->getSpineItemsCount() - 1;
nextPageNumber = UINT16_MAX;
updateRequired = true;
requestUpdate();
return;
}
@@ -251,13 +226,13 @@ void EpubReaderActivity::loop() {
currentSpineIndex = nextTriggered ? currentSpineIndex + 1 : currentSpineIndex - 1;
section.reset();
xSemaphoreGive(renderingMutex);
updateRequired = true;
requestUpdate();
return;
}
// No current section, attempt to rerender the book
if (!section) {
updateRequired = true;
requestUpdate();
return;
}
@@ -272,7 +247,7 @@ void EpubReaderActivity::loop() {
section.reset();
xSemaphoreGive(renderingMutex);
}
updateRequired = true;
requestUpdate();
} else {
if (section->currentPage < section->pageCount - 1) {
section->currentPage++;
@@ -284,7 +259,7 @@ void EpubReaderActivity::loop() {
section.reset();
xSemaphoreGive(renderingMutex);
}
updateRequired = true;
requestUpdate();
}
}
@@ -293,7 +268,7 @@ void EpubReaderActivity::onReaderMenuBack(const uint8_t orientation) {
// Apply the user-selected orientation when the menu is dismissed.
// This ensures the menu can be navigated without immediately rotating the screen.
applyOrientation(orientation);
updateRequired = true;
requestUpdate();
}
// Translate an absolute percent into a spine index plus a normalized position
@@ -349,7 +324,7 @@ void EpubReaderActivity::jumpToPercent(int percent) {
pendingSpineProgress = 1.0f;
}
// Reset state so renderScreen() reloads and repositions on the target spine.
// Reset state so render() reloads and repositions on the target spine.
xSemaphoreTake(renderingMutex, portMAX_DELAY);
currentSpineIndex = targetSpineIndex;
nextPageNumber = 0;
@@ -367,8 +342,6 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
const int spineIdx = currentSpineIndex;
const std::string path = epub->getPath();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
// 1. Close the menu
exitActivity();
@@ -377,7 +350,7 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
this->renderer, this->mappedInput, epub, path, spineIdx, currentP, totalP,
[this] {
exitActivity();
updateRequired = true;
requestUpdate();
},
[this](const int newSpineIndex) {
if (currentSpineIndex != newSpineIndex) {
@@ -386,7 +359,7 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
section.reset();
}
exitActivity();
updateRequired = true;
requestUpdate();
},
[this](const int newSpineIndex, const int newPage) {
if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) {
@@ -395,10 +368,9 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
section.reset();
}
exitActivity();
updateRequired = true;
requestUpdate();
}));
xSemaphoreGive(renderingMutex);
break;
}
case EpubReaderMenuActivity::MenuAction::GO_TO_PERCENT: {
@@ -409,7 +381,6 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f;
}
const int initialPercent = clampPercent(static_cast<int>(bookProgress + 0.5f));
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new EpubReaderPercentSelectionActivity(
renderer, mappedInput, initialPercent,
@@ -417,14 +388,13 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
// Apply the new position and exit back to the reader.
jumpToPercent(percent);
exitActivity();
updateRequired = true;
requestUpdate();
},
[this]() {
// Cancel selection and return to the reader.
exitActivity();
updateRequired = true;
requestUpdate();
}));
xSemaphoreGive(renderingMutex);
break;
}
case EpubReaderMenuActivity::MenuAction::GO_HOME: {
@@ -457,7 +427,6 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
}
case EpubReaderMenuActivity::MenuAction::SYNC: {
if (KOREADER_STORE.hasCredentials()) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
const int currentPage = section ? section->currentPage : 0;
const int totalPages = section ? section->pageCount : 0;
exitActivity();
@@ -476,7 +445,6 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
}
pendingSubactivityExit = true;
}));
xSemaphoreGive(renderingMutex);
}
break;
}
@@ -509,20 +477,8 @@ void EpubReaderActivity::applyOrientation(const uint8_t orientation) {
xSemaphoreGive(renderingMutex);
}
void EpubReaderActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
// TODO: Failure handling
void EpubReaderActivity::renderScreen() {
void EpubReaderActivity::render(Activity::RenderLock&& lock) {
if (!epub) {
return;
}
@@ -643,7 +599,9 @@ void EpubReaderActivity::renderScreen() {
LOG_ERR("ERS", "Failed to load page from SD - clearing section cache");
section->clearCache();
section.reset();
return renderScreen();
requestUpdate(); // Try again after clearing cache
// TODO: prevent infinite loop if the page keeps failing to load for some reason
return;
}
const auto start = millis();
renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);

View File

@@ -1,9 +1,6 @@
#pragma once
#include <Epub.h>
#include <Epub/Section.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include "EpubReaderMenuActivity.h"
#include "activities/ActivityWithSubactivity.h"
@@ -11,8 +8,6 @@
class EpubReaderActivity final : public ActivityWithSubactivity {
std::shared_ptr<Epub> epub;
std::unique_ptr<Section> section = nullptr;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
int currentSpineIndex = 0;
int nextPageNumber = 0;
int pagesUntilFullRefresh = 0;
@@ -23,16 +18,12 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
bool pendingPercentJump = false;
// Normalized 0.0-1.0 progress within the target spine item, computed from book percentage.
float pendingSpineProgress = 0.0f;
bool updateRequired = false;
bool pendingSubactivityExit = false; // Defer subactivity exit to avoid use-after-free
bool pendingGoHome = false; // Defer go home to avoid race condition with display task
bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit
const std::function<void()> onGoBack;
const std::function<void()> onGoHome;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
void renderContents(std::unique_ptr<Page> page, int orientedMarginTop, int orientedMarginRight,
int orientedMarginBottom, int orientedMarginLeft);
void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const;
@@ -53,4 +44,5 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&& lock) override;
};

View File

@@ -24,11 +24,6 @@ int EpubReaderChapterSelectionActivity::getPageItems() const {
return std::max(1, availableHeight / lineHeight);
}
void EpubReaderChapterSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderChapterSelectionActivity*>(param);
self->displayTaskLoop();
}
void EpubReaderChapterSelectionActivity::onEnter() {
ActivityWithSubactivity::onEnter();
@@ -36,35 +31,16 @@ void EpubReaderChapterSelectionActivity::onEnter() {
return;
}
renderingMutex = xSemaphoreCreateMutex();
selectorIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
if (selectorIndex == -1) {
selectorIndex = 0;
}
// Trigger first update
updateRequired = true;
xTaskCreate(&EpubReaderChapterSelectionActivity::taskTrampoline, "EpubReaderChapterSelectionActivityTask",
4096, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
requestUpdate();
}
void EpubReaderChapterSelectionActivity::onExit() {
ActivityWithSubactivity::onExit();
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void EpubReaderChapterSelectionActivity::onExit() { ActivityWithSubactivity::onExit(); }
void EpubReaderChapterSelectionActivity::loop() {
if (subActivity) {
@@ -88,38 +64,26 @@ void EpubReaderChapterSelectionActivity::loop() {
buttonNavigator.onNextRelease([this, totalItems] {
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
updateRequired = true;
requestUpdate();
});
buttonNavigator.onPreviousRelease([this, totalItems] {
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
updateRequired = true;
requestUpdate();
});
buttonNavigator.onNextContinuous([this, totalItems, pageItems] {
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems);
updateRequired = true;
requestUpdate();
});
buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] {
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems);
updateRequired = true;
requestUpdate();
});
}
void EpubReaderChapterSelectionActivity::displayTaskLoop() {
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void EpubReaderChapterSelectionActivity::renderScreen() {
void EpubReaderChapterSelectionActivity::render(Activity::RenderLock&&) {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();

View File

@@ -1,8 +1,5 @@
#pragma once
#include <Epub.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <memory>
@@ -12,14 +9,12 @@
class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity {
std::shared_ptr<Epub> epub;
std::string epubPath;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
ButtonNavigator buttonNavigator;
int currentSpineIndex = 0;
int currentPage = 0;
int totalPagesInSpine = 0;
int selectorIndex = 0;
bool updateRequired = false;
const std::function<void()> onGoBack;
const std::function<void(int newSpineIndex)> onSelectSpineIndex;
const std::function<void(int newSpineIndex, int newPage)> onSyncPosition;
@@ -31,10 +26,6 @@ class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity
// Total TOC items count
int getTotalItems() const;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
public:
explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::shared_ptr<Epub>& epub, const std::string& epubPath,
@@ -54,4 +45,5 @@ class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
};

View File

@@ -8,39 +8,10 @@
void EpubReaderMenuActivity::onEnter() {
ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
updateRequired = true;
xTaskCreate(&EpubReaderMenuActivity::taskTrampoline, "EpubMenuTask", 4096, this, 1, &displayTaskHandle);
requestUpdate();
}
void EpubReaderMenuActivity::onExit() {
ActivityWithSubactivity::onExit();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void EpubReaderMenuActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderMenuActivity*>(param);
self->displayTaskLoop();
}
void EpubReaderMenuActivity::displayTaskLoop() {
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void EpubReaderMenuActivity::onExit() { ActivityWithSubactivity::onExit(); }
void EpubReaderMenuActivity::loop() {
if (subActivity) {
@@ -51,12 +22,12 @@ void EpubReaderMenuActivity::loop() {
// Handle navigation
buttonNavigator.onNext([this] {
selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size()));
updateRequired = true;
requestUpdate();
});
buttonNavigator.onPrevious([this] {
selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast<int>(menuItems.size()));
updateRequired = true;
requestUpdate();
});
// Use local variables for items we need to check after potential deletion
@@ -65,7 +36,7 @@ void EpubReaderMenuActivity::loop() {
if (selectedAction == MenuAction::ROTATE_SCREEN) {
// Cycle orientation preview locally; actual rotation happens on menu exit.
pendingOrientation = (pendingOrientation + 1) % orientationLabels.size();
updateRequired = true;
requestUpdate();
return;
}
@@ -84,7 +55,7 @@ void EpubReaderMenuActivity::loop() {
}
}
void EpubReaderMenuActivity::renderScreen() {
void EpubReaderMenuActivity::render(Activity::RenderLock&&) {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
const auto orientation = renderer.getOrientation();

View File

@@ -1,8 +1,5 @@
#pragma once
#include <Epub.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include <string>
@@ -32,6 +29,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
private:
struct MenuItem {
@@ -46,9 +44,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
{MenuAction::SYNC, "Sync Progress"}, {MenuAction::DELETE_CACHE, "Delete Book Cache"}};
int selectedIndex = 0;
bool updateRequired = false;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
ButtonNavigator buttonNavigator;
std::string title = "Reader Menu";
uint8_t pendingOrientation = 0;
@@ -59,8 +55,4 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
const std::function<void(uint8_t)> onBack;
const std::function<void(MenuAction)> onAction;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
};

View File

@@ -15,41 +15,10 @@ constexpr int kLargeStep = 10;
void EpubReaderPercentSelectionActivity::onEnter() {
ActivityWithSubactivity::onEnter();
// Set up rendering task and mark first frame dirty.
renderingMutex = xSemaphoreCreateMutex();
updateRequired = true;
xTaskCreate(&EpubReaderPercentSelectionActivity::taskTrampoline, "EpubPercentSlider", 4096, this, 1,
&displayTaskHandle);
requestUpdate();
}
void EpubReaderPercentSelectionActivity::onExit() {
ActivityWithSubactivity::onExit();
// Ensure the render task is stopped before freeing the mutex.
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void EpubReaderPercentSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<EpubReaderPercentSelectionActivity*>(param);
self->displayTaskLoop();
}
void EpubReaderPercentSelectionActivity::displayTaskLoop() {
while (true) {
// Render only when the view is dirty and no subactivity is running.
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void EpubReaderPercentSelectionActivity::onExit() { ActivityWithSubactivity::onExit(); }
void EpubReaderPercentSelectionActivity::adjustPercent(const int delta) {
// Apply delta and clamp within 0-100.
@@ -59,7 +28,7 @@ void EpubReaderPercentSelectionActivity::adjustPercent(const int delta) {
} else if (percent > 100) {
percent = 100;
}
updateRequired = true;
requestUpdate();
}
void EpubReaderPercentSelectionActivity::loop() {
@@ -86,7 +55,7 @@ void EpubReaderPercentSelectionActivity::loop() {
buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { adjustPercent(-kLargeStep); });
}
void EpubReaderPercentSelectionActivity::renderScreen() {
void EpubReaderPercentSelectionActivity::render(Activity::RenderLock&&) {
renderer.clearScreen();
// Title and numeric percent value.

View File

@@ -1,7 +1,4 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
@@ -23,15 +20,12 @@ class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) 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;
ButtonNavigator buttonNavigator;
// Callback invoked when the user confirms a percent.
@@ -39,10 +33,6 @@ class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity
// Callback invoked when the user cancels the slider.
const std::function<void()> onCancel;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
// Render the slider UI.
void renderScreen();
// Change the current percent by a delta and clamp within bounds.
void adjustPercent(int delta);
};

View File

@@ -40,11 +40,6 @@ void syncTimeWithNTP() {
}
} // namespace
void KOReaderSyncActivity::taskTrampoline(void* param) {
auto* self = static_cast<KOReaderSyncActivity*>(param);
self->displayTaskLoop();
}
void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
exitActivity();
@@ -60,7 +55,7 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
state = SYNCING;
statusMessage = "Syncing time...";
xSemaphoreGive(renderingMutex);
updateRequired = true;
requestUpdate();
// Sync time with NTP before making API requests
syncTimeWithNTP();
@@ -68,7 +63,7 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
statusMessage = "Calculating document hash...";
xSemaphoreGive(renderingMutex);
updateRequired = true;
requestUpdate();
performSync();
}
@@ -85,7 +80,7 @@ void KOReaderSyncActivity::performSync() {
state = SYNC_FAILED;
statusMessage = "Failed to calculate document hash";
xSemaphoreGive(renderingMutex);
updateRequired = true;
requestUpdate();
return;
}
@@ -94,8 +89,7 @@ void KOReaderSyncActivity::performSync() {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
statusMessage = "Fetching remote progress...";
xSemaphoreGive(renderingMutex);
updateRequired = true;
vTaskDelay(10 / portTICK_PERIOD_MS);
requestUpdateAndWait();
// Fetch remote progress
const auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress);
@@ -106,7 +100,7 @@ void KOReaderSyncActivity::performSync() {
state = NO_REMOTE_PROGRESS;
hasRemoteProgress = false;
xSemaphoreGive(renderingMutex);
updateRequired = true;
requestUpdate();
return;
}
@@ -115,7 +109,7 @@ void KOReaderSyncActivity::performSync() {
state = SYNC_FAILED;
statusMessage = KOReaderSyncClient::errorString(result);
xSemaphoreGive(renderingMutex);
updateRequired = true;
requestUpdate();
return;
}
@@ -132,7 +126,7 @@ void KOReaderSyncActivity::performSync() {
state = SHOWING_RESULT;
selectedOption = 0; // Default to "Apply"
xSemaphoreGive(renderingMutex);
updateRequired = true;
requestUpdate();
}
void KOReaderSyncActivity::performUpload() {
@@ -140,8 +134,8 @@ void KOReaderSyncActivity::performUpload() {
state = UPLOADING;
statusMessage = "Uploading progress...";
xSemaphoreGive(renderingMutex);
updateRequired = true;
vTaskDelay(10 / portTICK_PERIOD_MS);
requestUpdate();
requestUpdateAndWait();
// Convert current position to KOReader format
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine};
@@ -159,32 +153,23 @@ void KOReaderSyncActivity::performUpload() {
state = SYNC_FAILED;
statusMessage = KOReaderSyncClient::errorString(result);
xSemaphoreGive(renderingMutex);
updateRequired = true;
requestUpdate();
return;
}
xSemaphoreTake(renderingMutex, portMAX_DELAY);
state = UPLOAD_COMPLETE;
xSemaphoreGive(renderingMutex);
updateRequired = true;
requestUpdate();
}
void KOReaderSyncActivity::onEnter() {
ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
xTaskCreate(&KOReaderSyncActivity::taskTrampoline, "KOSyncTask",
4096, // Stack size (larger for network operations)
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
// Check for credentials first
if (!KOREADER_STORE.hasCredentials()) {
state = NO_CREDENTIALS;
updateRequired = true;
requestUpdate();
return;
}
@@ -197,7 +182,7 @@ void KOReaderSyncActivity::onEnter() {
LOG_DBG("KOSync", "Already connected to WiFi");
state = SYNCING;
statusMessage = "Syncing time...";
updateRequired = true;
requestUpdate();
// Perform sync directly (will be handled in loop)
xTaskCreate(
@@ -208,7 +193,7 @@ void KOReaderSyncActivity::onEnter() {
xSemaphoreTake(self->renderingMutex, portMAX_DELAY);
self->statusMessage = "Calculating document hash...";
xSemaphoreGive(self->renderingMutex);
self->updateRequired = true;
self->requestUpdate();
self->performSync();
vTaskDelete(nullptr);
},
@@ -230,30 +215,9 @@ void KOReaderSyncActivity::onExit() {
delay(100);
WiFi.mode(WIFI_OFF);
delay(100);
// Wait until not rendering to delete task
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void KOReaderSyncActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void KOReaderSyncActivity::render() {
void KOReaderSyncActivity::render(Activity::RenderLock&&) {
if (subActivity) {
return;
}
@@ -388,11 +352,11 @@ void KOReaderSyncActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
selectedOption = (selectedOption + 1) % 2; // Wrap around among 2 options
updateRequired = true;
requestUpdate();
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
selectedOption = (selectedOption + 1) % 2; // Wrap around among 2 options
updateRequired = true;
requestUpdate();
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {

View File

@@ -1,8 +1,5 @@
#pragma once
#include <Epub.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <functional>
#include <memory>
@@ -45,6 +42,7 @@ class KOReaderSyncActivity final : public ActivityWithSubactivity {
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
bool preventAutoSleep() override { return state == CONNECTING || state == SYNCING; }
private:
@@ -66,10 +64,6 @@ class KOReaderSyncActivity final : public ActivityWithSubactivity {
int currentPage;
int totalPagesInSpine;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false;
State state = WIFI_SELECTION;
std::string statusMessage;
std::string documentHash;
@@ -91,8 +85,4 @@ class KOReaderSyncActivity final : public ActivityWithSubactivity {
void onWifiSelectionComplete(bool success);
void performSync();
void performUpload();
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void render();
};

View File

@@ -23,11 +23,6 @@ constexpr uint32_t CACHE_MAGIC = 0x54585449; // "TXTI"
constexpr uint8_t CACHE_VERSION = 2; // Increment when cache format changes
} // namespace
void TxtReaderActivity::taskTrampoline(void* param) {
auto* self = static_cast<TxtReaderActivity*>(param);
self->displayTaskLoop();
}
void TxtReaderActivity::onEnter() {
ActivityWithSubactivity::onEnter();
@@ -53,8 +48,6 @@ void TxtReaderActivity::onEnter() {
break;
}
renderingMutex = xSemaphoreCreateMutex();
txt->setupCacheDir();
// Save current txt as last opened file and add to recent books
@@ -65,14 +58,7 @@ void TxtReaderActivity::onEnter() {
RECENT_BOOKS.addBook(filePath, fileName, "", "");
// Trigger first update
updateRequired = true;
xTaskCreate(&TxtReaderActivity::taskTrampoline, "TxtReaderActivityTask",
6144, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
requestUpdate();
}
void TxtReaderActivity::onExit() {
@@ -81,14 +67,6 @@ void TxtReaderActivity::onExit() {
// Reset orientation back to portrait for the rest of the UI
renderer.setOrientation(GfxRenderer::Orientation::Portrait);
// Wait until not rendering to delete task
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
pageOffsets.clear();
currentPageLines.clear();
APP_STATE.readerActivityLoadCount = 0;
@@ -134,22 +112,10 @@ void TxtReaderActivity::loop() {
if (prevTriggered && currentPage > 0) {
currentPage--;
updateRequired = true;
requestUpdate();
} else if (nextTriggered && currentPage < totalPages - 1) {
currentPage++;
updateRequired = true;
}
}
void TxtReaderActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
requestUpdate();
}
}
@@ -372,7 +338,7 @@ bool TxtReaderActivity::loadPageAtOffset(size_t offset, std::vector<std::string>
return !outLines.empty();
}
void TxtReaderActivity::renderScreen() {
void TxtReaderActivity::render(Activity::RenderLock&&) {
if (!txt) {
return;
}

View File

@@ -1,9 +1,6 @@
#pragma once
#include <Txt.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <vector>
@@ -12,12 +9,11 @@
class TxtReaderActivity final : public ActivityWithSubactivity {
std::unique_ptr<Txt> txt;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
int currentPage = 0;
int totalPages = 1;
int pagesUntilFullRefresh = 0;
bool updateRequired = false;
const std::function<void()> onGoBack;
const std::function<void()> onGoHome;
@@ -33,9 +29,6 @@ class TxtReaderActivity final : public ActivityWithSubactivity {
int cachedScreenMargin = 0;
uint8_t cachedParagraphAlignment = CrossPointSettings::LEFT_ALIGN;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
void renderPage();
void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const;
@@ -57,4 +50,5 @@ class TxtReaderActivity final : public ActivityWithSubactivity {
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
};

View File

@@ -24,11 +24,6 @@ constexpr unsigned long skipPageMs = 700;
constexpr unsigned long goHomeMs = 1000;
} // namespace
void XtcReaderActivity::taskTrampoline(void* param) {
auto* self = static_cast<XtcReaderActivity*>(param);
self->displayTaskLoop();
}
void XtcReaderActivity::onEnter() {
ActivityWithSubactivity::onEnter();
@@ -36,8 +31,6 @@ void XtcReaderActivity::onEnter() {
return;
}
renderingMutex = xSemaphoreCreateMutex();
xtc->setupCacheDir();
// Load saved progress
@@ -49,27 +42,12 @@ void XtcReaderActivity::onEnter() {
RECENT_BOOKS.addBook(xtc->getPath(), xtc->getTitle(), xtc->getAuthor(), xtc->getThumbBmpPath());
// Trigger first update
updateRequired = true;
xTaskCreate(&XtcReaderActivity::taskTrampoline, "XtcReaderActivityTask",
4096, // Stack size (smaller than EPUB since no parsing needed)
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
requestUpdate();
}
void XtcReaderActivity::onExit() {
ActivityWithSubactivity::onExit();
// Wait until not rendering to delete task
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
APP_STATE.readerActivityLoadCount = 0;
APP_STATE.saveToFile();
xtc.reset();
@@ -85,20 +63,18 @@ void XtcReaderActivity::loop() {
// Enter chapter selection activity
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new XtcReaderChapterSelectionActivity(
this->renderer, this->mappedInput, xtc, currentPage,
[this] {
exitActivity();
updateRequired = true;
requestUpdate();
},
[this](const uint32_t newPage) {
currentPage = newPage;
exitActivity();
updateRequired = true;
requestUpdate();
}));
xSemaphoreGive(renderingMutex);
}
}
@@ -135,7 +111,7 @@ void XtcReaderActivity::loop() {
// Handle end of book
if (currentPage >= xtc->getPageCount()) {
currentPage = xtc->getPageCount() - 1;
updateRequired = true;
requestUpdate();
return;
}
@@ -148,29 +124,17 @@ void XtcReaderActivity::loop() {
} else {
currentPage = 0;
}
updateRequired = true;
requestUpdate();
} else if (nextTriggered) {
currentPage += skipAmount;
if (currentPage >= xtc->getPageCount()) {
currentPage = xtc->getPageCount(); // Allow showing "End of book"
}
updateRequired = true;
requestUpdate();
}
}
void XtcReaderActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void XtcReaderActivity::renderScreen() {
void XtcReaderActivity::render(Activity::RenderLock&&) {
if (!xtc) {
return;
}

View File

@@ -8,25 +8,18 @@
#pragma once
#include <Xtc.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include "activities/ActivityWithSubactivity.h"
class XtcReaderActivity final : public ActivityWithSubactivity {
std::shared_ptr<Xtc> xtc;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
uint32_t currentPage = 0;
int pagesUntilFullRefresh = 0;
bool updateRequired = false;
const std::function<void()> onGoBack;
const std::function<void()> onGoHome;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
void renderPage();
void saveProgress() const;
void loadProgress();
@@ -41,4 +34,5 @@ class XtcReaderActivity final : public ActivityWithSubactivity {
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
};

View File

@@ -37,11 +37,6 @@ int XtcReaderChapterSelectionActivity::findChapterIndexForPage(uint32_t page) co
return 0;
}
void XtcReaderChapterSelectionActivity::taskTrampoline(void* param) {
auto* self = static_cast<XtcReaderChapterSelectionActivity*>(param);
self->displayTaskLoop();
}
void XtcReaderChapterSelectionActivity::onEnter() {
Activity::onEnter();
@@ -49,29 +44,12 @@ void XtcReaderChapterSelectionActivity::onEnter() {
return;
}
renderingMutex = xSemaphoreCreateMutex();
selectorIndex = findChapterIndexForPage(currentPage);
updateRequired = true;
xTaskCreate(&XtcReaderChapterSelectionActivity::taskTrampoline, "XtcReaderChapterSelectionActivityTask",
4096, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
requestUpdate();
}
void XtcReaderChapterSelectionActivity::onExit() {
Activity::onExit();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void XtcReaderChapterSelectionActivity::onExit() { Activity::onExit(); }
void XtcReaderChapterSelectionActivity::loop() {
const int pageItems = getPageItems();
@@ -88,38 +66,26 @@ void XtcReaderChapterSelectionActivity::loop() {
buttonNavigator.onNextRelease([this, totalItems] {
selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems);
updateRequired = true;
requestUpdate();
});
buttonNavigator.onPreviousRelease([this, totalItems] {
selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems);
updateRequired = true;
requestUpdate();
});
buttonNavigator.onNextContinuous([this, totalItems, pageItems] {
selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems);
updateRequired = true;
requestUpdate();
});
buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] {
selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems);
updateRequired = true;
requestUpdate();
});
}
void XtcReaderChapterSelectionActivity::displayTaskLoop() {
while (true) {
if (updateRequired) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
renderScreen();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void XtcReaderChapterSelectionActivity::renderScreen() {
void XtcReaderChapterSelectionActivity::render(Activity::RenderLock&&) {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();

View File

@@ -1,8 +1,5 @@
#pragma once
#include <Xtc.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>
#include <memory>
@@ -11,22 +8,16 @@
class XtcReaderChapterSelectionActivity final : public Activity {
std::shared_ptr<Xtc> xtc;
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
ButtonNavigator buttonNavigator;
uint32_t currentPage = 0;
int selectorIndex = 0;
bool updateRequired = false;
const std::function<void()> onGoBack;
const std::function<void(uint32_t newPage)> onSelectPage;
int getPageItems() const;
int findChapterIndexForPage(uint32_t page) const;
static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop();
void renderScreen();
public:
explicit XtcReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::shared_ptr<Xtc>& xtc, uint32_t currentPage,
@@ -40,4 +31,5 @@ class XtcReaderChapterSelectionActivity final : public Activity {
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
};