A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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>

authored by

Xuan-Son Nguyen
znelson
and committed by
GitHub
a616f42c 0508bfc1

+454 -1405
+58
src/activities/Activity.cpp
··· 1 + #include "Activity.h" 2 + 3 + void Activity::renderTaskTrampoline(void* param) { 4 + auto* self = static_cast<Activity*>(param); 5 + self->renderTaskLoop(); 6 + } 7 + 8 + void Activity::renderTaskLoop() { 9 + while (true) { 10 + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); 11 + { 12 + RenderLock lock(*this); 13 + render(std::move(lock)); 14 + } 15 + } 16 + } 17 + 18 + void Activity::onEnter() { 19 + xTaskCreate(&renderTaskTrampoline, name.c_str(), 20 + 8192, // Stack size 21 + this, // Parameters 22 + 1, // Priority 23 + &renderTaskHandle // Task handle 24 + ); 25 + assert(renderTaskHandle != nullptr && "Failed to create render task"); 26 + LOG_DBG("ACT", "Entering activity: %s", name.c_str()); 27 + } 28 + 29 + void Activity::onExit() { 30 + RenderLock lock(*this); // Ensure we don't delete the task while it's rendering 31 + if (renderTaskHandle) { 32 + vTaskDelete(renderTaskHandle); 33 + renderTaskHandle = nullptr; 34 + } 35 + 36 + LOG_DBG("ACT", "Exiting activity: %s", name.c_str()); 37 + } 38 + 39 + void Activity::requestUpdate() { 40 + // Using direct notification to signal the render task to update 41 + // Increment counter so multiple rapid calls won't be lost 42 + if (renderTaskHandle) { 43 + xTaskNotify(renderTaskHandle, 1, eIncrement); 44 + } 45 + } 46 + 47 + void Activity::requestUpdateAndWait() { 48 + // FIXME @ngxson : properly implement this using freeRTOS notification 49 + delay(100); 50 + } 51 + 52 + // RenderLock 53 + 54 + Activity::RenderLock::RenderLock(Activity& activity) : activity(activity) { 55 + xSemaphoreTake(activity.renderingMutex, portMAX_DELAY); 56 + } 57 + 58 + Activity::RenderLock::~RenderLock() { xSemaphoreGive(activity.renderingMutex); }
+41 -7
src/activities/Activity.h
··· 1 1 #pragma once 2 - 2 + #include <HardwareSerial.h> 3 3 #include <Logging.h> 4 + #include <freertos/FreeRTOS.h> 5 + #include <freertos/semphr.h> 6 + #include <freertos/task.h> 4 7 8 + #include <cassert> 5 9 #include <string> 6 10 #include <utility> 7 11 8 - class MappedInputManager; 9 - class GfxRenderer; 12 + #include "GfxRenderer.h" 13 + #include "MappedInputManager.h" 10 14 11 15 class Activity { 12 16 protected: ··· 14 18 GfxRenderer& renderer; 15 19 MappedInputManager& mappedInput; 16 20 21 + // Task to render and display the activity 22 + TaskHandle_t renderTaskHandle = nullptr; 23 + [[noreturn]] static void renderTaskTrampoline(void* param); 24 + [[noreturn]] virtual void renderTaskLoop(); 25 + 26 + // Mutex to protect rendering operations from being deleted mid-render 27 + SemaphoreHandle_t renderingMutex = nullptr; 28 + 17 29 public: 18 30 explicit Activity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput) 19 - : name(std::move(name)), renderer(renderer), mappedInput(mappedInput) {} 20 - virtual ~Activity() = default; 21 - virtual void onEnter() { LOG_DBG("ACT", "Entering activity: %s", name.c_str()); } 22 - virtual void onExit() { LOG_DBG("ACT", "Exiting activity: %s", name.c_str()); } 31 + : name(std::move(name)), renderer(renderer), mappedInput(mappedInput), renderingMutex(xSemaphoreCreateMutex()) { 32 + assert(renderingMutex != nullptr && "Failed to create rendering mutex"); 33 + } 34 + virtual ~Activity() { 35 + vSemaphoreDelete(renderingMutex); 36 + renderingMutex = nullptr; 37 + }; 38 + class RenderLock; 39 + virtual void onEnter(); 40 + virtual void onExit(); 23 41 virtual void loop() {} 42 + 43 + virtual void render(RenderLock&&) {} 44 + virtual void requestUpdate(); 45 + virtual void requestUpdateAndWait(); 46 + 24 47 virtual bool skipLoopDelay() { return false; } 25 48 virtual bool preventAutoSleep() { return false; } 26 49 virtual bool isReaderActivity() const { return false; } 50 + 51 + // RAII helper to lock rendering mutex for the duration of a scope. 52 + class RenderLock { 53 + Activity& activity; 54 + 55 + public: 56 + explicit RenderLock(Activity& activity); 57 + RenderLock(const RenderLock&) = delete; 58 + RenderLock& operator=(const RenderLock&) = delete; 59 + ~RenderLock(); 60 + }; 27 61 };
+27 -1
src/activities/ActivityWithSubactivity.cpp
··· 1 1 #include "ActivityWithSubactivity.h" 2 2 3 + void ActivityWithSubactivity::renderTaskLoop() { 4 + while (true) { 5 + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); 6 + { 7 + RenderLock lock(*this); 8 + if (!subActivity) { 9 + render(std::move(lock)); 10 + } 11 + // If subActivity is set, consume the notification but skip parent render 12 + // Note: the sub-activity will call its render() from its own display task 13 + } 14 + } 15 + } 16 + 3 17 void ActivityWithSubactivity::exitActivity() { 18 + // No need to lock, since onExit() already acquires its own lock 4 19 if (subActivity) { 20 + LOG_DBG("ACT", "Exiting subactivity..."); 5 21 subActivity->onExit(); 6 22 subActivity.reset(); 7 23 } 8 24 } 9 25 10 26 void ActivityWithSubactivity::enterNewActivity(Activity* activity) { 27 + // Acquire lock to avoid 2 activities rendering at the same time during transition 28 + RenderLock lock(*this); 11 29 subActivity.reset(activity); 12 30 subActivity->onEnter(); 13 31 } ··· 18 36 } 19 37 } 20 38 39 + void ActivityWithSubactivity::requestUpdate() { 40 + if (!subActivity) { 41 + Activity::requestUpdate(); 42 + } 43 + // Sub-activity should call their own requestUpdate() from their loop() function 44 + } 45 + 21 46 void ActivityWithSubactivity::onExit() { 47 + // No need to lock, onExit() already acquires its own lock 48 + exitActivity(); 22 49 Activity::onExit(); 23 - exitActivity(); 24 50 }
+4
src/activities/ActivityWithSubactivity.h
··· 8 8 std::unique_ptr<Activity> subActivity = nullptr; 9 9 void exitActivity(); 10 10 void enterNewActivity(Activity* activity); 11 + [[noreturn]] void renderTaskLoop() override; 11 12 12 13 public: 13 14 explicit ActivityWithSubactivity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput) 14 15 : Activity(std::move(name), renderer, mappedInput) {} 15 16 void loop() override; 17 + // Note: when a subactivity is active, parent requestUpdate() calls are ignored; 18 + // the subactivity should request its own renders. This pauses parent rendering until exit. 19 + void requestUpdate() override; 16 20 void onExit() override; 17 21 };
+22 -54
src/activities/browser/OpdsBookBrowserActivity.cpp
··· 19 19 constexpr int PAGE_ITEMS = 23; 20 20 } // namespace 21 21 22 - void OpdsBookBrowserActivity::taskTrampoline(void* param) { 23 - auto* self = static_cast<OpdsBookBrowserActivity*>(param); 24 - self->displayTaskLoop(); 25 - } 26 - 27 22 void OpdsBookBrowserActivity::onEnter() { 28 23 ActivityWithSubactivity::onEnter(); 29 24 30 - renderingMutex = xSemaphoreCreateMutex(); 31 25 state = BrowserState::CHECK_WIFI; 32 26 entries.clear(); 33 27 navigationHistory.clear(); ··· 35 29 selectorIndex = 0; 36 30 errorMessage.clear(); 37 31 statusMessage = "Checking WiFi..."; 38 - updateRequired = true; 39 - 40 - xTaskCreate(&OpdsBookBrowserActivity::taskTrampoline, "OpdsBookBrowserTask", 41 - 4096, // Stack size (larger for HTTP operations) 42 - this, // Parameters 43 - 1, // Priority 44 - &displayTaskHandle // Task handle 45 - ); 32 + requestUpdate(); 46 33 47 34 // Check WiFi and connect if needed, then fetch feed 48 35 checkAndConnectWifi(); ··· 54 41 // Turn off WiFi when exiting 55 42 WiFi.mode(WIFI_OFF); 56 43 57 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 58 - if (displayTaskHandle) { 59 - vTaskDelete(displayTaskHandle); 60 - displayTaskHandle = nullptr; 61 - } 62 - vSemaphoreDelete(renderingMutex); 63 - renderingMutex = nullptr; 64 44 entries.clear(); 65 45 navigationHistory.clear(); 66 46 } ··· 81 61 LOG_DBG("OPDS", "Retry: WiFi connected, retrying fetch"); 82 62 state = BrowserState::LOADING; 83 63 statusMessage = "Loading..."; 84 - updateRequired = true; 64 + requestUpdate(); 85 65 fetchFeed(currentPath); 86 66 } else { 87 67 // WiFi not connected - launch WiFi selection ··· 134 114 if (!entries.empty()) { 135 115 buttonNavigator.onNextRelease([this] { 136 116 selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entries.size()); 137 - updateRequired = true; 117 + requestUpdate(); 138 118 }); 139 119 140 120 buttonNavigator.onPreviousRelease([this] { 141 121 selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entries.size()); 142 - updateRequired = true; 122 + requestUpdate(); 143 123 }); 144 124 145 125 buttonNavigator.onNextContinuous([this] { 146 126 selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entries.size(), PAGE_ITEMS); 147 - updateRequired = true; 127 + requestUpdate(); 148 128 }); 149 129 150 130 buttonNavigator.onPreviousContinuous([this] { 151 131 selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, entries.size(), PAGE_ITEMS); 152 - updateRequired = true; 132 + requestUpdate(); 153 133 }); 154 134 } 155 135 } 156 136 } 157 137 158 - void OpdsBookBrowserActivity::displayTaskLoop() { 159 - while (true) { 160 - if (updateRequired) { 161 - updateRequired = false; 162 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 163 - render(); 164 - xSemaphoreGive(renderingMutex); 165 - } 166 - vTaskDelay(10 / portTICK_PERIOD_MS); 167 - } 168 - } 169 - 170 - void OpdsBookBrowserActivity::render() const { 138 + void OpdsBookBrowserActivity::render(Activity::RenderLock&&) { 171 139 renderer.clearScreen(); 172 140 173 141 const auto pageWidth = renderer.getScreenWidth(); ··· 260 228 if (strlen(serverUrl) == 0) { 261 229 state = BrowserState::ERROR; 262 230 errorMessage = "No server URL configured"; 263 - updateRequired = true; 231 + requestUpdate(); 264 232 return; 265 233 } 266 234 ··· 274 242 if (!HttpDownloader::fetchUrl(url, stream)) { 275 243 state = BrowserState::ERROR; 276 244 errorMessage = "Failed to fetch feed"; 277 - updateRequired = true; 245 + requestUpdate(); 278 246 return; 279 247 } 280 248 } ··· 282 250 if (!parser) { 283 251 state = BrowserState::ERROR; 284 252 errorMessage = "Failed to parse feed"; 285 - updateRequired = true; 253 + requestUpdate(); 286 254 return; 287 255 } 288 256 ··· 293 261 if (entries.empty()) { 294 262 state = BrowserState::ERROR; 295 263 errorMessage = "No entries found"; 296 - updateRequired = true; 264 + requestUpdate(); 297 265 return; 298 266 } 299 267 300 268 state = BrowserState::BROWSING; 301 - updateRequired = true; 269 + requestUpdate(); 302 270 } 303 271 304 272 void OpdsBookBrowserActivity::navigateToEntry(const OpdsEntry& entry) { ··· 310 278 statusMessage = "Loading..."; 311 279 entries.clear(); 312 280 selectorIndex = 0; 313 - updateRequired = true; 281 + requestUpdate(); 314 282 315 283 fetchFeed(currentPath); 316 284 } ··· 328 296 statusMessage = "Loading..."; 329 297 entries.clear(); 330 298 selectorIndex = 0; 331 - updateRequired = true; 299 + requestUpdate(); 332 300 333 301 fetchFeed(currentPath); 334 302 } ··· 339 307 statusMessage = book.title; 340 308 downloadProgress = 0; 341 309 downloadTotal = 0; 342 - updateRequired = true; 310 + requestUpdate(); 343 311 344 312 // Build full download URL 345 313 std::string downloadUrl = UrlUtils::buildUrl(SETTINGS.opdsServerUrl, book.href); ··· 357 325 HttpDownloader::downloadToFile(downloadUrl, filename, [this](const size_t downloaded, const size_t total) { 358 326 downloadProgress = downloaded; 359 327 downloadTotal = total; 360 - updateRequired = true; 328 + requestUpdate(); 361 329 }); 362 330 363 331 if (result == HttpDownloader::OK) { ··· 369 337 LOG_DBG("OPDS", "Cleared cache for: %s", filename.c_str()); 370 338 371 339 state = BrowserState::BROWSING; 372 - updateRequired = true; 340 + requestUpdate(); 373 341 } else { 374 342 state = BrowserState::ERROR; 375 343 errorMessage = "Download failed"; 376 - updateRequired = true; 344 + requestUpdate(); 377 345 } 378 346 } 379 347 ··· 382 350 if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) { 383 351 state = BrowserState::LOADING; 384 352 statusMessage = "Loading..."; 385 - updateRequired = true; 353 + requestUpdate(); 386 354 fetchFeed(currentPath); 387 355 return; 388 356 } ··· 393 361 394 362 void OpdsBookBrowserActivity::launchWifiSelection() { 395 363 state = BrowserState::WIFI_SELECTION; 396 - updateRequired = true; 364 + requestUpdate(); 397 365 398 366 enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, 399 367 [this](const bool connected) { onWifiSelectionComplete(connected); })); ··· 406 374 LOG_DBG("OPDS", "WiFi connected via selection, fetching feed"); 407 375 state = BrowserState::LOADING; 408 376 statusMessage = "Loading..."; 409 - updateRequired = true; 377 + requestUpdate(); 410 378 fetchFeed(currentPath); 411 379 } else { 412 380 LOG_DBG("OPDS", "WiFi selection cancelled/failed"); ··· 416 384 WiFi.mode(WIFI_OFF); 417 385 state = BrowserState::ERROR; 418 386 errorMessage = "WiFi connection failed"; 419 - updateRequired = true; 387 + requestUpdate(); 420 388 } 421 389 }
+1 -11
src/activities/browser/OpdsBookBrowserActivity.h
··· 1 1 #pragma once 2 2 #include <OpdsParser.h> 3 - #include <freertos/FreeRTOS.h> 4 - #include <freertos/semphr.h> 5 - #include <freertos/task.h> 6 3 7 4 #include <functional> 8 5 #include <string> ··· 34 31 void onEnter() override; 35 32 void onExit() override; 36 33 void loop() override; 34 + void render(Activity::RenderLock&&) override; 37 35 38 36 private: 39 - TaskHandle_t displayTaskHandle = nullptr; 40 - SemaphoreHandle_t renderingMutex = nullptr; 41 37 ButtonNavigator buttonNavigator; 42 - bool updateRequired = false; 43 - 44 38 BrowserState state = BrowserState::LOADING; 45 39 std::vector<OpdsEntry> entries; 46 40 std::vector<std::string> navigationHistory; // Stack of previous feed paths for back navigation ··· 52 46 size_t downloadTotal = 0; 53 47 54 48 const std::function<void()> onGoHome; 55 - 56 - static void taskTrampoline(void* param); 57 - [[noreturn]] void displayTaskLoop(); 58 - void render() const; 59 49 60 50 void checkAndConnectWifi(); 61 51 void launchWifiSelection();
+7 -42
src/activities/home/HomeActivity.cpp
··· 19 19 #include "fontIds.h" 20 20 #include "util/StringUtils.h" 21 21 22 - void HomeActivity::taskTrampoline(void* param) { 23 - auto* self = static_cast<HomeActivity*>(param); 24 - self->displayTaskLoop(); 25 - } 26 - 27 22 int HomeActivity::getMenuItemCount() const { 28 23 int count = 4; // My Library, Recents, File transfer, Settings 29 24 if (!recentBooks.empty()) { ··· 83 78 book.coverBmpPath = ""; 84 79 } 85 80 coverRendered = false; 86 - updateRequired = true; 81 + requestUpdate(); 87 82 } else if (StringUtils::checkFileExtension(book.path, ".xtch") || 88 83 StringUtils::checkFileExtension(book.path, ".xtc")) { 89 84 // Handle XTC file ··· 101 96 book.coverBmpPath = ""; 102 97 } 103 98 coverRendered = false; 104 - updateRequired = true; 99 + requestUpdate(); 105 100 } 106 101 } 107 102 } ··· 116 111 void HomeActivity::onEnter() { 117 112 Activity::onEnter(); 118 113 119 - renderingMutex = xSemaphoreCreateMutex(); 120 - 121 114 // Check if OPDS browser URL is configured 122 115 hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0; 123 116 ··· 127 120 loadRecentBooks(metrics.homeRecentBooksCount); 128 121 129 122 // Trigger first update 130 - updateRequired = true; 131 - 132 - xTaskCreate(&HomeActivity::taskTrampoline, "HomeActivityTask", 133 - 8192, // Stack size 134 - this, // Parameters 135 - 1, // Priority 136 - &displayTaskHandle // Task handle 137 - ); 123 + requestUpdate(); 138 124 } 139 125 140 126 void HomeActivity::onExit() { 141 127 Activity::onExit(); 142 128 143 - // Wait until not rendering to delete task to avoid killing mid-instruction to EPD 144 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 145 - if (displayTaskHandle) { 146 - vTaskDelete(displayTaskHandle); 147 - displayTaskHandle = nullptr; 148 - } 149 - vSemaphoreDelete(renderingMutex); 150 - renderingMutex = nullptr; 151 - 152 129 // Free the stored cover buffer if any 153 130 freeCoverBuffer(); 154 131 } ··· 200 177 201 178 buttonNavigator.onNext([this, menuCount] { 202 179 selectorIndex = ButtonNavigator::nextIndex(selectorIndex, menuCount); 203 - updateRequired = true; 180 + requestUpdate(); 204 181 }); 205 182 206 183 buttonNavigator.onPrevious([this, menuCount] { 207 184 selectorIndex = ButtonNavigator::previousIndex(selectorIndex, menuCount); 208 - updateRequired = true; 185 + requestUpdate(); 209 186 }); 210 187 211 188 if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { ··· 234 211 } 235 212 } 236 213 237 - void HomeActivity::displayTaskLoop() { 238 - while (true) { 239 - if (updateRequired) { 240 - updateRequired = false; 241 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 242 - render(); 243 - xSemaphoreGive(renderingMutex); 244 - } 245 - vTaskDelay(10 / portTICK_PERIOD_MS); 246 - } 247 - } 248 - 249 - void HomeActivity::render() { 214 + void HomeActivity::render(Activity::RenderLock&&) { 250 215 auto metrics = UITheme::getInstance().getMetrics(); 251 216 const auto pageWidth = renderer.getScreenWidth(); 252 217 const auto pageHeight = renderer.getScreenHeight(); ··· 282 247 283 248 if (!firstRenderDone) { 284 249 firstRenderDone = true; 285 - updateRequired = true; 250 + requestUpdate(); 286 251 } else if (!recentsLoaded && !recentsLoading) { 287 252 recentsLoading = true; 288 253 loadRecentCovers(metrics.homeCoverHeight);
+1 -10
src/activities/home/HomeActivity.h
··· 1 1 #pragma once 2 - #include <freertos/FreeRTOS.h> 3 - #include <freertos/semphr.h> 4 - #include <freertos/task.h> 5 - 6 2 #include <functional> 7 3 #include <vector> 8 4 ··· 14 10 struct Rect; 15 11 16 12 class HomeActivity final : public Activity { 17 - TaskHandle_t displayTaskHandle = nullptr; 18 - SemaphoreHandle_t renderingMutex = nullptr; 19 13 ButtonNavigator buttonNavigator; 20 14 int selectorIndex = 0; 21 - bool updateRequired = false; 22 15 bool recentsLoading = false; 23 16 bool recentsLoaded = false; 24 17 bool firstRenderDone = false; ··· 34 27 const std::function<void()> onFileTransferOpen; 35 28 const std::function<void()> onOpdsBrowserOpen; 36 29 37 - static void taskTrampoline(void* param); 38 - [[noreturn]] void displayTaskLoop(); 39 - void render(); 40 30 int getMenuItemCount() const; 41 31 bool storeCoverBuffer(); // Store frame buffer for cover image 42 32 bool restoreCoverBuffer(); // Restore frame buffer from stored cover ··· 60 50 void onEnter() override; 61 51 void onExit() override; 62 52 void loop() override; 53 + void render(Activity::RenderLock&&) override; 63 54 };
+8 -45
src/activities/home/MyLibraryActivity.cpp
··· 66 66 }); 67 67 } 68 68 69 - void MyLibraryActivity::taskTrampoline(void* param) { 70 - auto* self = static_cast<MyLibraryActivity*>(param); 71 - self->displayTaskLoop(); 72 - } 73 - 74 69 void MyLibraryActivity::loadFiles() { 75 70 files.clear(); 76 71 ··· 109 104 void MyLibraryActivity::onEnter() { 110 105 Activity::onEnter(); 111 106 112 - renderingMutex = xSemaphoreCreateMutex(); 113 - 114 107 loadFiles(); 115 - 116 108 selectorIndex = 0; 117 - updateRequired = true; 118 109 119 - xTaskCreate(&MyLibraryActivity::taskTrampoline, "MyLibraryActivityTask", 120 - 4096, // Stack size 121 - this, // Parameters 122 - 1, // Priority 123 - &displayTaskHandle // Task handle 124 - ); 110 + requestUpdate(); 125 111 } 126 112 127 113 void MyLibraryActivity::onExit() { 128 114 Activity::onExit(); 129 - 130 - // Wait until not rendering to delete task to avoid killing mid-instruction to EPD 131 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 132 - if (displayTaskHandle) { 133 - vTaskDelete(displayTaskHandle); 134 - displayTaskHandle = nullptr; 135 - } 136 - vSemaphoreDelete(renderingMutex); 137 - renderingMutex = nullptr; 138 - 139 115 files.clear(); 140 116 } 141 117 ··· 146 122 basepath = "/"; 147 123 loadFiles(); 148 124 selectorIndex = 0; 149 - updateRequired = true; 150 125 return; 151 126 } 152 127 ··· 162 137 basepath += files[selectorIndex].substr(0, files[selectorIndex].length() - 1); 163 138 loadFiles(); 164 139 selectorIndex = 0; 165 - updateRequired = true; 140 + requestUpdate(); 166 141 } else { 167 142 onSelectBook(basepath + files[selectorIndex]); 168 143 return; ··· 183 158 const std::string dirName = oldPath.substr(pos + 1) + "/"; 184 159 selectorIndex = findEntry(dirName); 185 160 186 - updateRequired = true; 161 + requestUpdate(); 187 162 } else { 188 163 onGoHome(); 189 164 } ··· 194 169 195 170 buttonNavigator.onNextRelease([this, listSize] { 196 171 selectorIndex = ButtonNavigator::nextIndex(static_cast<int>(selectorIndex), listSize); 197 - updateRequired = true; 172 + requestUpdate(); 198 173 }); 199 174 200 175 buttonNavigator.onPreviousRelease([this, listSize] { 201 176 selectorIndex = ButtonNavigator::previousIndex(static_cast<int>(selectorIndex), listSize); 202 - updateRequired = true; 177 + requestUpdate(); 203 178 }); 204 179 205 180 buttonNavigator.onNextContinuous([this, listSize, pageItems] { 206 181 selectorIndex = ButtonNavigator::nextPageIndex(static_cast<int>(selectorIndex), listSize, pageItems); 207 - updateRequired = true; 182 + requestUpdate(); 208 183 }); 209 184 210 185 buttonNavigator.onPreviousContinuous([this, listSize, pageItems] { 211 186 selectorIndex = ButtonNavigator::previousPageIndex(static_cast<int>(selectorIndex), listSize, pageItems); 212 - updateRequired = true; 187 + requestUpdate(); 213 188 }); 214 189 } 215 190 216 - void MyLibraryActivity::displayTaskLoop() { 217 - while (true) { 218 - if (updateRequired) { 219 - updateRequired = false; 220 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 221 - render(); 222 - xSemaphoreGive(renderingMutex); 223 - } 224 - vTaskDelay(10 / portTICK_PERIOD_MS); 225 - } 226 - } 227 - 228 - void MyLibraryActivity::render() const { 191 + void MyLibraryActivity::render(Activity::RenderLock&&) { 229 192 renderer.clearScreen(); 230 193 231 194 const auto pageWidth = renderer.getScreenWidth();
+1 -11
src/activities/home/MyLibraryActivity.h
··· 1 1 #pragma once 2 - #include <freertos/FreeRTOS.h> 3 - #include <freertos/semphr.h> 4 - #include <freertos/task.h> 5 - 6 2 #include <functional> 7 3 #include <string> 8 4 #include <vector> ··· 13 9 14 10 class MyLibraryActivity final : public Activity { 15 11 private: 16 - TaskHandle_t displayTaskHandle = nullptr; 17 - SemaphoreHandle_t renderingMutex = nullptr; 18 12 ButtonNavigator buttonNavigator; 19 13 20 14 size_t selectorIndex = 0; 21 - bool updateRequired = false; 22 15 23 16 // Files state 24 17 std::string basepath = "/"; ··· 27 20 // Callbacks 28 21 const std::function<void(const std::string& path)> onSelectBook; 29 22 const std::function<void()> onGoHome; 30 - 31 - static void taskTrampoline(void* param); 32 - [[noreturn]] void displayTaskLoop(); 33 - void render() const; 34 23 35 24 // Data loading 36 25 void loadFiles(); ··· 48 37 void onEnter() override; 49 38 void onExit() override; 50 39 void loop() override; 40 + void render(Activity::RenderLock&&) override; 51 41 };
+6 -42
src/activities/home/RecentBooksActivity.cpp
··· 15 15 constexpr unsigned long GO_HOME_MS = 1000; 16 16 } // namespace 17 17 18 - void RecentBooksActivity::taskTrampoline(void* param) { 19 - auto* self = static_cast<RecentBooksActivity*>(param); 20 - self->displayTaskLoop(); 21 - } 22 - 23 18 void RecentBooksActivity::loadRecentBooks() { 24 19 recentBooks.clear(); 25 20 const auto& books = RECENT_BOOKS.getBooks(); ··· 37 32 void RecentBooksActivity::onEnter() { 38 33 Activity::onEnter(); 39 34 40 - renderingMutex = xSemaphoreCreateMutex(); 41 - 42 35 // Load data 43 36 loadRecentBooks(); 44 37 45 38 selectorIndex = 0; 46 - updateRequired = true; 47 - 48 - xTaskCreate(&RecentBooksActivity::taskTrampoline, "RecentBooksActivityTask", 49 - 4096, // Stack size 50 - this, // Parameters 51 - 1, // Priority 52 - &displayTaskHandle // Task handle 53 - ); 39 + requestUpdate(); 54 40 } 55 41 56 42 void RecentBooksActivity::onExit() { 57 43 Activity::onExit(); 58 - 59 - // Wait until not rendering to delete task to avoid killing mid-instruction to EPD 60 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 61 - if (displayTaskHandle) { 62 - vTaskDelete(displayTaskHandle); 63 - displayTaskHandle = nullptr; 64 - } 65 - vSemaphoreDelete(renderingMutex); 66 - renderingMutex = nullptr; 67 - 68 44 recentBooks.clear(); 69 45 } 70 46 ··· 87 63 88 64 buttonNavigator.onNextRelease([this, listSize] { 89 65 selectorIndex = ButtonNavigator::nextIndex(static_cast<int>(selectorIndex), listSize); 90 - updateRequired = true; 66 + requestUpdate(); 91 67 }); 92 68 93 69 buttonNavigator.onPreviousRelease([this, listSize] { 94 70 selectorIndex = ButtonNavigator::previousIndex(static_cast<int>(selectorIndex), listSize); 95 - updateRequired = true; 71 + requestUpdate(); 96 72 }); 97 73 98 74 buttonNavigator.onNextContinuous([this, listSize, pageItems] { 99 75 selectorIndex = ButtonNavigator::nextPageIndex(static_cast<int>(selectorIndex), listSize, pageItems); 100 - updateRequired = true; 76 + requestUpdate(); 101 77 }); 102 78 103 79 buttonNavigator.onPreviousContinuous([this, listSize, pageItems] { 104 80 selectorIndex = ButtonNavigator::previousPageIndex(static_cast<int>(selectorIndex), listSize, pageItems); 105 - updateRequired = true; 81 + requestUpdate(); 106 82 }); 107 83 } 108 84 109 - void RecentBooksActivity::displayTaskLoop() { 110 - while (true) { 111 - if (updateRequired) { 112 - updateRequired = false; 113 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 114 - render(); 115 - xSemaphoreGive(renderingMutex); 116 - } 117 - vTaskDelay(10 / portTICK_PERIOD_MS); 118 - } 119 - } 120 - 121 - void RecentBooksActivity::render() const { 85 + void RecentBooksActivity::render(Activity::RenderLock&&) { 122 86 renderer.clearScreen(); 123 87 124 88 const auto pageWidth = renderer.getScreenWidth();
+1 -10
src/activities/home/RecentBooksActivity.h
··· 1 1 #pragma once 2 - #include <freertos/FreeRTOS.h> 3 - #include <freertos/semphr.h> 4 - #include <freertos/task.h> 5 2 6 3 #include <functional> 7 4 #include <string> ··· 13 10 14 11 class RecentBooksActivity final : public Activity { 15 12 private: 16 - TaskHandle_t displayTaskHandle = nullptr; 17 - SemaphoreHandle_t renderingMutex = nullptr; 18 13 ButtonNavigator buttonNavigator; 19 14 20 15 size_t selectorIndex = 0; 21 - bool updateRequired = false; 22 16 23 17 // Recent tab state 24 18 std::vector<RecentBook> recentBooks; ··· 27 21 const std::function<void(const std::string& path)> onSelectBook; 28 22 const std::function<void()> onGoHome; 29 23 30 - static void taskTrampoline(void* param); 31 - [[noreturn]] void displayTaskLoop(); 32 - void render() const; 33 - 34 24 // Data loading 35 25 void loadRecentBooks(); 36 26 ··· 42 32 void onEnter() override; 43 33 void onExit() override; 44 34 void loop() override; 35 + void render(Activity::RenderLock&&) override; 45 36 };
+6 -39
src/activities/network/CalibreConnectActivity.cpp
··· 14 14 constexpr const char* HOSTNAME = "crosspoint"; 15 15 } // namespace 16 16 17 - void CalibreConnectActivity::taskTrampoline(void* param) { 18 - auto* self = static_cast<CalibreConnectActivity*>(param); 19 - self->displayTaskLoop(); 20 - } 21 - 22 17 void CalibreConnectActivity::onEnter() { 23 18 ActivityWithSubactivity::onEnter(); 24 19 25 - renderingMutex = xSemaphoreCreateMutex(); 26 - updateRequired = true; 20 + requestUpdate(); 27 21 state = CalibreConnectState::WIFI_SELECTION; 28 22 connectedIP.clear(); 29 23 connectedSSID.clear(); ··· 35 29 lastCompleteAt = 0; 36 30 exitRequested = false; 37 31 38 - xTaskCreate(&CalibreConnectActivity::taskTrampoline, "CalibreConnectTask", 39 - 2048, // Stack size 40 - this, // Parameters 41 - 1, // Priority 42 - &displayTaskHandle // Task handle 43 - ); 44 - 45 32 if (WiFi.status() != WL_CONNECTED) { 46 33 enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, 47 34 [this](const bool connected) { onWifiSelectionComplete(connected); })); ··· 63 50 delay(30); 64 51 WiFi.mode(WIFI_OFF); 65 52 delay(30); 66 - 67 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 68 - if (displayTaskHandle) { 69 - vTaskDelete(displayTaskHandle); 70 - displayTaskHandle = nullptr; 71 - } 72 - vSemaphoreDelete(renderingMutex); 73 - renderingMutex = nullptr; 74 53 } 75 54 76 55 void CalibreConnectActivity::onWifiSelectionComplete(const bool connected) { ··· 92 71 93 72 void CalibreConnectActivity::startWebServer() { 94 73 state = CalibreConnectState::SERVER_STARTING; 95 - updateRequired = true; 74 + requestUpdate(); 96 75 97 76 if (MDNS.begin(HOSTNAME)) { 98 77 // mDNS is optional for the Calibre plugin but still helpful for users. ··· 104 83 105 84 if (webServer->isRunning()) { 106 85 state = CalibreConnectState::SERVER_RUNNING; 107 - updateRequired = true; 86 + requestUpdate(); 108 87 } else { 109 88 state = CalibreConnectState::ERROR; 110 - updateRequired = true; 89 + requestUpdate(); 111 90 } 112 91 } 113 92 ··· 178 157 changed = true; 179 158 } 180 159 if (changed) { 181 - updateRequired = true; 160 + requestUpdate(); 182 161 } 183 162 } 184 163 ··· 188 167 } 189 168 } 190 169 191 - void CalibreConnectActivity::displayTaskLoop() { 192 - while (true) { 193 - if (updateRequired) { 194 - updateRequired = false; 195 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 196 - render(); 197 - xSemaphoreGive(renderingMutex); 198 - } 199 - vTaskDelay(10 / portTICK_PERIOD_MS); 200 - } 201 - } 202 - 203 - void CalibreConnectActivity::render() const { 170 + void CalibreConnectActivity::render(Activity::RenderLock&&) { 204 171 if (state == CalibreConnectState::SERVER_RUNNING) { 205 172 renderer.clearScreen(); 206 173 renderServerRunning();
+1 -9
src/activities/network/CalibreConnectActivity.h
··· 1 1 #pragma once 2 - #include <freertos/FreeRTOS.h> 3 - #include <freertos/semphr.h> 4 - #include <freertos/task.h> 5 2 6 3 #include <functional> 7 4 #include <memory> ··· 17 14 * but renders Calibre-specific instructions instead of the web transfer UI. 18 15 */ 19 16 class CalibreConnectActivity final : public ActivityWithSubactivity { 20 - TaskHandle_t displayTaskHandle = nullptr; 21 - SemaphoreHandle_t renderingMutex = nullptr; 22 - bool updateRequired = false; 23 17 CalibreConnectState state = CalibreConnectState::WIFI_SELECTION; 24 18 const std::function<void()> onComplete; 25 19 ··· 34 28 unsigned long lastCompleteAt = 0; 35 29 bool exitRequested = false; 36 30 37 - static void taskTrampoline(void* param); 38 - [[noreturn]] void displayTaskLoop(); 39 - void render() const; 40 31 void renderServerRunning() const; 41 32 42 33 void onWifiSelectionComplete(bool connected); ··· 50 41 void onEnter() override; 51 42 void onExit() override; 52 43 void loop() override; 44 + void render(Activity::RenderLock&&) override; 53 45 bool skipLoopDelay() override { return webServer && webServer->isRunning(); } 54 46 bool preventAutoSleep() override { return webServer && webServer->isRunning(); } 55 47 };
+13 -58
src/activities/network/CrossPointWebServerActivity.cpp
··· 29 29 constexpr uint16_t DNS_PORT = 53; 30 30 } // namespace 31 31 32 - void CrossPointWebServerActivity::taskTrampoline(void* param) { 33 - auto* self = static_cast<CrossPointWebServerActivity*>(param); 34 - self->displayTaskLoop(); 35 - } 36 - 37 32 void CrossPointWebServerActivity::onEnter() { 38 33 ActivityWithSubactivity::onEnter(); 39 34 40 - LOG_DBG("WEBACT] [MEM", "Free heap at onEnter: %d bytes", ESP.getFreeHeap()); 41 - 42 - renderingMutex = xSemaphoreCreateMutex(); 35 + LOG_DBG("WEBACT", "Free heap at onEnter: %d bytes", ESP.getFreeHeap()); 43 36 44 37 // Reset state 45 38 state = WebServerActivityState::MODE_SELECTION; ··· 48 41 connectedIP.clear(); 49 42 connectedSSID.clear(); 50 43 lastHandleClientTime = 0; 51 - updateRequired = true; 52 - 53 - xTaskCreate(&CrossPointWebServerActivity::taskTrampoline, "WebServerActivityTask", 54 - 2048, // Stack size 55 - this, // Parameters 56 - 1, // Priority 57 - &displayTaskHandle // Task handle 58 - ); 44 + requestUpdate(); 59 45 60 46 // Launch network mode selection subactivity 61 47 LOG_DBG("WEBACT", "Launching NetworkModeSelectionActivity..."); ··· 68 54 void CrossPointWebServerActivity::onExit() { 69 55 ActivityWithSubactivity::onExit(); 70 56 71 - LOG_DBG("WEBACT] [MEM", "Free heap at onExit start: %d bytes", ESP.getFreeHeap()); 57 + LOG_DBG("WEBACT", "Free heap at onExit start: %d bytes", ESP.getFreeHeap()); 72 58 73 59 state = WebServerActivityState::SHUTTING_DOWN; 74 60 ··· 103 89 WiFi.mode(WIFI_OFF); 104 90 delay(30); // Allow WiFi hardware to power down 105 91 106 - LOG_DBG("WEBACT] [MEM", "Free heap after WiFi disconnect: %d bytes", ESP.getFreeHeap()); 107 - 108 - // Acquire mutex before deleting task 109 - LOG_DBG("WEBACT", "Acquiring rendering mutex before task deletion..."); 110 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 111 - 112 - // Delete the display task 113 - LOG_DBG("WEBACT", "Deleting display task..."); 114 - if (displayTaskHandle) { 115 - vTaskDelete(displayTaskHandle); 116 - displayTaskHandle = nullptr; 117 - LOG_DBG("WEBACT", "Display task deleted"); 118 - } 119 - 120 - // Delete the mutex 121 - LOG_DBG("WEBACT", "Deleting mutex..."); 122 - vSemaphoreDelete(renderingMutex); 123 - renderingMutex = nullptr; 124 - LOG_DBG("WEBACT", "Mutex deleted"); 125 - 126 - LOG_DBG("WEBACT] [MEM", "Free heap at onExit end: %d bytes", ESP.getFreeHeap()); 92 + LOG_DBG("WEBACT", "Free heap at onExit end: %d bytes", ESP.getFreeHeap()); 127 93 } 128 94 129 95 void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode) { ··· 165 131 } else { 166 132 // AP mode - start access point 167 133 state = WebServerActivityState::AP_STARTING; 168 - updateRequired = true; 134 + requestUpdate(); 169 135 startAccessPoint(); 170 136 } 171 137 } ··· 200 166 201 167 void CrossPointWebServerActivity::startAccessPoint() { 202 168 LOG_DBG("WEBACT", "Starting Access Point mode..."); 203 - LOG_DBG("WEBACT] [MEM", "Free heap before AP start: %d bytes", ESP.getFreeHeap()); 169 + LOG_DBG("WEBACT", "Free heap before AP start: %d bytes", ESP.getFreeHeap()); 204 170 205 171 // Configure and start the AP 206 172 WiFi.mode(WIFI_AP); ··· 248 214 dnsServer->start(DNS_PORT, "*", apIP); 249 215 LOG_DBG("WEBACT", "DNS server started for captive portal"); 250 216 251 - LOG_DBG("WEBACT] [MEM", "Free heap after AP start: %d bytes", ESP.getFreeHeap()); 217 + LOG_DBG("WEBACT", "Free heap after AP start: %d bytes", ESP.getFreeHeap()); 252 218 253 219 // Start the web server 254 220 startWebServer(); ··· 267 233 268 234 // Force an immediate render since we're transitioning from a subactivity 269 235 // that had its own rendering task. We need to make sure our display is shown. 270 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 271 - render(); 272 - xSemaphoreGive(renderingMutex); 236 + { 237 + RenderLock lock(*this); 238 + render(std::move(lock)); 239 + } 273 240 LOG_DBG("WEBACT", "Rendered File Transfer screen"); 274 241 } else { 275 242 LOG_ERR("WEBACT", "ERROR: Failed to start web server!"); ··· 312 279 LOG_DBG("WEBACT", "WiFi disconnected! Status: %d", wifiStatus); 313 280 // Show error and exit gracefully 314 281 state = WebServerActivityState::SHUTTING_DOWN; 315 - updateRequired = true; 282 + requestUpdate(); 316 283 return; 317 284 } 318 285 // Log weak signal warnings ··· 368 335 } 369 336 } 370 337 371 - void CrossPointWebServerActivity::displayTaskLoop() { 372 - while (true) { 373 - if (updateRequired) { 374 - updateRequired = false; 375 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 376 - render(); 377 - xSemaphoreGive(renderingMutex); 378 - } 379 - vTaskDelay(10 / portTICK_PERIOD_MS); 380 - } 381 - } 382 - 383 - void CrossPointWebServerActivity::render() const { 338 + void CrossPointWebServerActivity::render(Activity::RenderLock&&) { 384 339 // Only render our own UI when server is running 385 340 // Subactivities handle their own rendering 386 341 if (state == WebServerActivityState::SERVER_RUNNING) {
+1 -9
src/activities/network/CrossPointWebServerActivity.h
··· 1 1 #pragma once 2 - #include <freertos/FreeRTOS.h> 3 - #include <freertos/semphr.h> 4 - #include <freertos/task.h> 5 2 6 3 #include <functional> 7 4 #include <memory> ··· 31 28 * - Cleans up the server and shuts down WiFi on exit 32 29 */ 33 30 class CrossPointWebServerActivity final : public ActivityWithSubactivity { 34 - TaskHandle_t displayTaskHandle = nullptr; 35 - SemaphoreHandle_t renderingMutex = nullptr; 36 - bool updateRequired = false; 37 31 WebServerActivityState state = WebServerActivityState::MODE_SELECTION; 38 32 const std::function<void()> onGoBack; 39 33 ··· 51 45 // Performance monitoring 52 46 unsigned long lastHandleClientTime = 0; 53 47 54 - static void taskTrampoline(void* param); 55 - [[noreturn]] void displayTaskLoop(); 56 - void render() const; 57 48 void renderServerRunning() const; 58 49 59 50 void onNetworkModeSelected(NetworkMode mode); ··· 69 60 void onEnter() override; 70 61 void onExit() override; 71 62 void loop() override; 63 + void render(Activity::RenderLock&&) override; 72 64 bool skipLoopDelay() override { return webServer && webServer->isRunning(); } 73 65 bool preventAutoSleep() override { return webServer && webServer->isRunning(); } 74 66 };
+5 -42
src/activities/network/NetworkModeSelectionActivity.cpp
··· 16 16 }; 17 17 } // namespace 18 18 19 - void NetworkModeSelectionActivity::taskTrampoline(void* param) { 20 - auto* self = static_cast<NetworkModeSelectionActivity*>(param); 21 - self->displayTaskLoop(); 22 - } 23 - 24 19 void NetworkModeSelectionActivity::onEnter() { 25 20 Activity::onEnter(); 26 21 27 - renderingMutex = xSemaphoreCreateMutex(); 28 - 29 22 // Reset selection 30 23 selectedIndex = 0; 31 24 32 25 // Trigger first update 33 - updateRequired = true; 34 - 35 - xTaskCreate(&NetworkModeSelectionActivity::taskTrampoline, "NetworkModeTask", 36 - 2048, // Stack size 37 - this, // Parameters 38 - 1, // Priority 39 - &displayTaskHandle // Task handle 40 - ); 26 + requestUpdate(); 41 27 } 42 28 43 - void NetworkModeSelectionActivity::onExit() { 44 - Activity::onExit(); 45 - 46 - // Wait until not rendering to delete task 47 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 48 - if (displayTaskHandle) { 49 - vTaskDelete(displayTaskHandle); 50 - displayTaskHandle = nullptr; 51 - } 52 - vSemaphoreDelete(renderingMutex); 53 - renderingMutex = nullptr; 54 - } 29 + void NetworkModeSelectionActivity::onExit() { Activity::onExit(); } 55 30 56 31 void NetworkModeSelectionActivity::loop() { 57 32 // Handle back button - cancel ··· 75 50 // Handle navigation 76 51 buttonNavigator.onNext([this] { 77 52 selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEM_COUNT); 78 - updateRequired = true; 53 + requestUpdate(); 79 54 }); 80 55 81 56 buttonNavigator.onPrevious([this] { 82 57 selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEM_COUNT); 83 - updateRequired = true; 58 + requestUpdate(); 84 59 }); 85 60 } 86 61 87 - void NetworkModeSelectionActivity::displayTaskLoop() { 88 - while (true) { 89 - if (updateRequired) { 90 - updateRequired = false; 91 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 92 - render(); 93 - xSemaphoreGive(renderingMutex); 94 - } 95 - vTaskDelay(10 / portTICK_PERIOD_MS); 96 - } 97 - } 98 - 99 - void NetworkModeSelectionActivity::render() const { 62 + void NetworkModeSelectionActivity::render(Activity::RenderLock&&) { 100 63 renderer.clearScreen(); 101 64 102 65 const auto pageWidth = renderer.getScreenWidth();
+2 -10
src/activities/network/NetworkModeSelectionActivity.h
··· 1 1 #pragma once 2 - #include <freertos/FreeRTOS.h> 3 - #include <freertos/semphr.h> 4 - #include <freertos/task.h> 5 2 6 3 #include <functional> 7 4 ··· 21 18 * The onCancel callback is called if the user presses back. 22 19 */ 23 20 class NetworkModeSelectionActivity final : public Activity { 24 - TaskHandle_t displayTaskHandle = nullptr; 25 - SemaphoreHandle_t renderingMutex = nullptr; 26 21 ButtonNavigator buttonNavigator; 27 22 28 23 int selectedIndex = 0; 29 - bool updateRequired = false; 24 + 30 25 const std::function<void(NetworkMode)> onModeSelected; 31 26 const std::function<void()> onCancel; 32 - 33 - static void taskTrampoline(void* param); 34 - [[noreturn]] void displayTaskLoop(); 35 - void render() const; 36 27 37 28 public: 38 29 explicit NetworkModeSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, ··· 42 33 void onEnter() override; 43 34 void onExit() override; 44 35 void loop() override; 36 + void render(Activity::RenderLock&&) override; 45 37 };
+34 -88
src/activities/network/WifiSelectionActivity.cpp
··· 12 12 #include "components/UITheme.h" 13 13 #include "fontIds.h" 14 14 15 - void WifiSelectionActivity::taskTrampoline(void* param) { 16 - auto* self = static_cast<WifiSelectionActivity*>(param); 17 - self->displayTaskLoop(); 18 - } 19 - 20 15 void WifiSelectionActivity::onEnter() { 21 16 Activity::onEnter(); 22 - 23 - renderingMutex = xSemaphoreCreateMutex(); 24 17 25 18 // Load saved WiFi credentials - SD card operations need lock as we use SPI 26 19 // for both 27 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 28 - WIFI_STORE.loadFromFile(); 29 - xSemaphoreGive(renderingMutex); 20 + { 21 + RenderLock lock(*this); 22 + WIFI_STORE.loadFromFile(); 23 + } 30 24 31 25 // Reset state 32 26 selectedNetworkIndex = 0; ··· 49 43 mac[5]); 50 44 cachedMacAddress = std::string(macStr); 51 45 52 - // Task creation 53 - xTaskCreate(&WifiSelectionActivity::taskTrampoline, "WifiSelectionTask", 54 - 4096, // Stack size (larger for WiFi operations) 55 - this, // Parameters 56 - 1, // Priority 57 - &displayTaskHandle // Task handle 58 - ); 46 + // Trigger first update to show scanning message 47 + requestUpdate(); 59 48 60 49 // Attempt to auto-connect to the last network 61 50 if (allowAutoConnect) { ··· 70 59 usedSavedPassword = true; 71 60 autoConnecting = true; 72 61 attemptConnection(); 73 - updateRequired = true; 62 + requestUpdate(); 74 63 return; 75 64 } 76 65 } ··· 83 72 void WifiSelectionActivity::onExit() { 84 73 Activity::onExit(); 85 74 86 - LOG_DBG("WIFI] [MEM", "Free heap at onExit start: %d bytes", ESP.getFreeHeap()); 75 + LOG_DBG("WIFI", "Free heap at onExit start: %d bytes", ESP.getFreeHeap()); 87 76 88 77 // Stop any ongoing WiFi scan 89 78 LOG_DBG("WIFI", "Deleting WiFi scan..."); 90 79 WiFi.scanDelete(); 91 - LOG_DBG("WIFI] [MEM", "Free heap after scanDelete: %d bytes", ESP.getFreeHeap()); 80 + LOG_DBG("WIFI", "Free heap after scanDelete: %d bytes", ESP.getFreeHeap()); 92 81 93 82 // Note: We do NOT disconnect WiFi here - the parent activity 94 83 // (CrossPointWebServerActivity) manages WiFi connection state. We just clean 95 84 // up the scan and task. 96 85 97 - // Acquire mutex before deleting task to ensure task isn't using it 98 - // This prevents hangs/crashes if the task holds the mutex when deleted 99 - LOG_DBG("WIFI", "Acquiring rendering mutex before task deletion..."); 100 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 101 - 102 - // Delete the display task (we now hold the mutex, so task is blocked if it 103 - // needs it) 104 - LOG_DBG("WIFI", "Deleting display task..."); 105 - if (displayTaskHandle) { 106 - vTaskDelete(displayTaskHandle); 107 - displayTaskHandle = nullptr; 108 - LOG_DBG("WIFI", "Display task deleted"); 109 - } 110 - 111 - // Now safe to delete the mutex since we own it 112 - LOG_DBG("WIFI", "Deleting mutex..."); 113 - vSemaphoreDelete(renderingMutex); 114 - renderingMutex = nullptr; 115 - LOG_DBG("WIFI", "Mutex deleted"); 116 - 117 - LOG_DBG("WIFI] [MEM", "Free heap at onExit end: %d bytes", ESP.getFreeHeap()); 86 + LOG_DBG("WIFI", "Free heap at onExit end: %d bytes", ESP.getFreeHeap()); 118 87 } 119 88 120 89 void WifiSelectionActivity::startWifiScan() { 121 90 autoConnecting = false; 122 91 state = WifiSelectionState::SCANNING; 123 92 networks.clear(); 124 - updateRequired = true; 93 + requestUpdate(); 125 94 126 95 // Set WiFi mode to station 127 96 WiFi.mode(WIFI_STA); ··· 142 111 143 112 if (scanResult == WIFI_SCAN_FAILED) { 144 113 state = WifiSelectionState::NETWORK_LIST; 145 - updateRequired = true; 114 + requestUpdate(); 146 115 return; 147 116 } 148 117 ··· 191 160 WiFi.scanDelete(); 192 161 state = WifiSelectionState::NETWORK_LIST; 193 162 selectedNetworkIndex = 0; 194 - updateRequired = true; 163 + requestUpdate(); 195 164 } 196 165 197 166 void WifiSelectionActivity::selectNetwork(const int index) { ··· 221 190 // Show password entry 222 191 state = WifiSelectionState::PASSWORD_ENTRY; 223 192 // Don't allow screen updates while changing activity 224 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 225 193 enterNewActivity(new KeyboardEntryActivity( 226 194 renderer, mappedInput, "Enter WiFi Password", 227 195 "", // No initial text ··· 234 202 }, 235 203 [this] { 236 204 state = WifiSelectionState::NETWORK_LIST; 237 - updateRequired = true; 238 205 exitActivity(); 206 + requestUpdate(); 239 207 })); 240 - updateRequired = true; 241 - xSemaphoreGive(renderingMutex); 242 208 } else { 243 209 // Connect directly for open networks 244 210 attemptConnection(); ··· 250 216 connectionStartTime = millis(); 251 217 connectedIP.clear(); 252 218 connectionError.clear(); 253 - updateRequired = true; 219 + requestUpdate(); 254 220 255 221 WiFi.mode(WIFI_STA); 256 222 ··· 287 253 if (!usedSavedPassword && !enteredPassword.empty()) { 288 254 state = WifiSelectionState::SAVE_PROMPT; 289 255 savePromptSelection = 0; // Default to "Yes" 290 - updateRequired = true; 256 + requestUpdate(); 291 257 } else { 292 258 // Using saved password or open network - complete immediately 293 259 LOG_DBG("WIFI", ··· 304 270 connectionError = "Error: Network not found"; 305 271 } 306 272 state = WifiSelectionState::CONNECTION_FAILED; 307 - updateRequired = true; 273 + requestUpdate(); 308 274 return; 309 275 } 310 276 ··· 313 279 WiFi.disconnect(); 314 280 connectionError = "Error: Connection timeout"; 315 281 state = WifiSelectionState::CONNECTION_FAILED; 316 - updateRequired = true; 282 + requestUpdate(); 317 283 return; 318 284 } 319 285 } ··· 348 314 mappedInput.wasPressed(MappedInputManager::Button::Left)) { 349 315 if (savePromptSelection > 0) { 350 316 savePromptSelection--; 351 - updateRequired = true; 317 + requestUpdate(); 352 318 } 353 319 } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || 354 320 mappedInput.wasPressed(MappedInputManager::Button::Right)) { 355 321 if (savePromptSelection < 1) { 356 322 savePromptSelection++; 357 - updateRequired = true; 323 + requestUpdate(); 358 324 } 359 325 } else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { 360 326 if (savePromptSelection == 0) { 361 327 // User chose "Yes" - save the password 362 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 328 + RenderLock lock(*this); 363 329 WIFI_STORE.addCredential(selectedSSID, enteredPassword); 364 - xSemaphoreGive(renderingMutex); 365 330 } 366 331 // Complete - parent will start web server 367 332 onComplete(true); ··· 378 343 mappedInput.wasPressed(MappedInputManager::Button::Left)) { 379 344 if (forgetPromptSelection > 0) { 380 345 forgetPromptSelection--; 381 - updateRequired = true; 346 + requestUpdate(); 382 347 } 383 348 } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || 384 349 mappedInput.wasPressed(MappedInputManager::Button::Right)) { 385 350 if (forgetPromptSelection < 1) { 386 351 forgetPromptSelection++; 387 - updateRequired = true; 352 + requestUpdate(); 388 353 } 389 354 } else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { 390 355 if (forgetPromptSelection == 1) { 356 + RenderLock lock(*this); 391 357 // User chose "Forget network" - forget the network 392 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 393 358 WIFI_STORE.removeCredential(selectedSSID); 394 - xSemaphoreGive(renderingMutex); 395 359 // Update the network list to reflect the change 396 360 const auto network = find_if(networks.begin(), networks.end(), 397 361 [this](const WifiNetworkInfo& net) { return net.ssid == selectedSSID; }); ··· 430 394 // Go back to network list on failure for non-saved credentials 431 395 state = WifiSelectionState::NETWORK_LIST; 432 396 } 433 - updateRequired = true; 397 + requestUpdate(); 434 398 return; 435 399 } 436 400 } ··· 465 429 selectedSSID = networks[selectedNetworkIndex].ssid; 466 430 state = WifiSelectionState::FORGET_PROMPT; 467 431 forgetPromptSelection = 0; // Default to "Cancel" 468 - updateRequired = true; 432 + requestUpdate(); 469 433 return; 470 434 } 471 435 } ··· 473 437 // Handle navigation 474 438 buttonNavigator.onNext([this] { 475 439 selectedNetworkIndex = ButtonNavigator::nextIndex(selectedNetworkIndex, networks.size()); 476 - updateRequired = true; 440 + requestUpdate(); 477 441 }); 478 442 479 443 buttonNavigator.onPrevious([this] { 480 444 selectedNetworkIndex = ButtonNavigator::previousIndex(selectedNetworkIndex, networks.size()); 481 - updateRequired = true; 445 + requestUpdate(); 482 446 }); 483 447 } 484 448 } ··· 500 464 return " "; // Very weak 501 465 } 502 466 503 - void WifiSelectionActivity::displayTaskLoop() { 504 - while (true) { 505 - // If a subactivity is active, yield CPU time but don't render 506 - if (subActivity) { 507 - vTaskDelay(10 / portTICK_PERIOD_MS); 508 - continue; 509 - } 510 - 511 - // Don't render if we're in PASSWORD_ENTRY state - we're just transitioning 512 - // from the keyboard subactivity back to the main activity 513 - if (state == WifiSelectionState::PASSWORD_ENTRY) { 514 - vTaskDelay(10 / portTICK_PERIOD_MS); 515 - continue; 516 - } 517 - 518 - if (updateRequired) { 519 - updateRequired = false; 520 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 521 - render(); 522 - xSemaphoreGive(renderingMutex); 523 - } 524 - vTaskDelay(10 / portTICK_PERIOD_MS); 467 + void WifiSelectionActivity::render(Activity::RenderLock&&) { 468 + // Don't render if we're in PASSWORD_ENTRY state - we're just transitioning 469 + // from the keyboard subactivity back to the main activity 470 + if (state == WifiSelectionState::PASSWORD_ENTRY) { 471 + requestUpdateAndWait(); 472 + return; 525 473 } 526 - } 527 474 528 - void WifiSelectionActivity::render() const { 529 475 renderer.clearScreen(); 530 476 531 477 switch (state) {
+2 -9
src/activities/network/WifiSelectionActivity.h
··· 1 1 #pragma once 2 - #include <freertos/FreeRTOS.h> 3 - #include <freertos/semphr.h> 4 - #include <freertos/task.h> 5 2 6 3 #include <cstdint> 7 4 #include <functional> ··· 45 42 * The onComplete callback receives true if connected successfully, false if cancelled. 46 43 */ 47 44 class WifiSelectionActivity final : public ActivityWithSubactivity { 48 - TaskHandle_t displayTaskHandle = nullptr; 49 - SemaphoreHandle_t renderingMutex = nullptr; 50 45 ButtonNavigator buttonNavigator; 51 - bool updateRequired = false; 46 + 52 47 WifiSelectionState state = WifiSelectionState::SCANNING; 53 48 int selectedNetworkIndex = 0; 54 49 std::vector<WifiNetworkInfo> networks; ··· 85 80 static constexpr unsigned long CONNECTION_TIMEOUT_MS = 15000; 86 81 unsigned long connectionStartTime = 0; 87 82 88 - static void taskTrampoline(void* param); 89 - [[noreturn]] void displayTaskLoop(); 90 - void render() const; 91 83 void renderNetworkList() const; 92 84 void renderPasswordEntry() const; 93 85 void renderConnecting() const; ··· 112 104 void onEnter() override; 113 105 void onExit() override; 114 106 void loop() override; 107 + void render(Activity::RenderLock&&) override; 115 108 116 109 // Get the IP address after successful connection 117 110 const std::string& getConnectedIP() const { return connectedIP; }
+18 -60
src/activities/reader/EpubReaderActivity.cpp
··· 57 57 58 58 } // namespace 59 59 60 - void EpubReaderActivity::taskTrampoline(void* param) { 61 - auto* self = static_cast<EpubReaderActivity*>(param); 62 - self->displayTaskLoop(); 63 - } 64 - 65 60 void EpubReaderActivity::onEnter() { 66 61 ActivityWithSubactivity::onEnter(); 67 62 ··· 72 67 // Configure screen orientation based on settings 73 68 // NOTE: This affects layout math and must be applied before any render calls. 74 69 applyReaderOrientation(renderer, SETTINGS.orientation); 75 - 76 - renderingMutex = xSemaphoreCreateMutex(); 77 70 78 71 epub->setupCacheDir(); 79 72 ··· 108 101 RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), epub->getThumbBmpPath()); 109 102 110 103 // Trigger first update 111 - updateRequired = true; 112 - 113 - xTaskCreate(&EpubReaderActivity::taskTrampoline, "EpubReaderActivityTask", 114 - 8192, // Stack size 115 - this, // Parameters 116 - 1, // Priority 117 - &displayTaskHandle // Task handle 118 - ); 104 + requestUpdate(); 119 105 } 120 106 121 107 void EpubReaderActivity::onExit() { ··· 124 110 // Reset orientation back to portrait for the rest of the UI 125 111 renderer.setOrientation(GfxRenderer::Orientation::Portrait); 126 112 127 - // Wait until not rendering to delete task to avoid killing mid-instruction to EPD 128 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 129 - if (displayTaskHandle) { 130 - vTaskDelete(displayTaskHandle); 131 - displayTaskHandle = nullptr; 132 - } 133 - vSemaphoreDelete(renderingMutex); 134 - renderingMutex = nullptr; 135 113 APP_STATE.readerActivityLoadCount = 0; 136 114 APP_STATE.saveToFile(); 137 115 section.reset(); ··· 146 124 if (pendingSubactivityExit) { 147 125 pendingSubactivityExit = false; 148 126 exitActivity(); 149 - updateRequired = true; 127 + requestUpdate(); 150 128 skipNextButtonCheck = true; // Skip button processing to ignore stale events 151 129 } 152 130 // Deferred go home: process after subActivity->loop() returns to avoid race condition ··· 186 164 187 165 // Enter reader menu activity. 188 166 if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 189 - // Don't start activity transition while rendering 190 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 191 167 const int currentPage = section ? section->currentPage + 1 : 0; 192 168 const int totalPages = section ? section->pageCount : 0; 193 169 float bookProgress = 0.0f; ··· 201 177 this->renderer, this->mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, 202 178 SETTINGS.orientation, [this](const uint8_t orientation) { onReaderMenuBack(orientation); }, 203 179 [this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); })); 204 - xSemaphoreGive(renderingMutex); 205 180 } 206 181 207 182 // Long press BACK (1s+) goes to file selection ··· 238 213 if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) { 239 214 currentSpineIndex = epub->getSpineItemsCount() - 1; 240 215 nextPageNumber = UINT16_MAX; 241 - updateRequired = true; 216 + requestUpdate(); 242 217 return; 243 218 } 244 219 ··· 251 226 currentSpineIndex = nextTriggered ? currentSpineIndex + 1 : currentSpineIndex - 1; 252 227 section.reset(); 253 228 xSemaphoreGive(renderingMutex); 254 - updateRequired = true; 229 + requestUpdate(); 255 230 return; 256 231 } 257 232 258 233 // No current section, attempt to rerender the book 259 234 if (!section) { 260 - updateRequired = true; 235 + requestUpdate(); 261 236 return; 262 237 } 263 238 ··· 272 247 section.reset(); 273 248 xSemaphoreGive(renderingMutex); 274 249 } 275 - updateRequired = true; 250 + requestUpdate(); 276 251 } else { 277 252 if (section->currentPage < section->pageCount - 1) { 278 253 section->currentPage++; ··· 284 259 section.reset(); 285 260 xSemaphoreGive(renderingMutex); 286 261 } 287 - updateRequired = true; 262 + requestUpdate(); 288 263 } 289 264 } 290 265 ··· 293 268 // Apply the user-selected orientation when the menu is dismissed. 294 269 // This ensures the menu can be navigated without immediately rotating the screen. 295 270 applyOrientation(orientation); 296 - updateRequired = true; 271 + requestUpdate(); 297 272 } 298 273 299 274 // Translate an absolute percent into a spine index plus a normalized position ··· 349 324 pendingSpineProgress = 1.0f; 350 325 } 351 326 352 - // Reset state so renderScreen() reloads and repositions on the target spine. 327 + // Reset state so render() reloads and repositions on the target spine. 353 328 xSemaphoreTake(renderingMutex, portMAX_DELAY); 354 329 currentSpineIndex = targetSpineIndex; 355 330 nextPageNumber = 0; ··· 366 341 const int totalP = section ? section->pageCount : 0; 367 342 const int spineIdx = currentSpineIndex; 368 343 const std::string path = epub->getPath(); 369 - 370 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 371 344 372 345 // 1. Close the menu 373 346 exitActivity(); ··· 377 350 this->renderer, this->mappedInput, epub, path, spineIdx, currentP, totalP, 378 351 [this] { 379 352 exitActivity(); 380 - updateRequired = true; 353 + requestUpdate(); 381 354 }, 382 355 [this](const int newSpineIndex) { 383 356 if (currentSpineIndex != newSpineIndex) { ··· 386 359 section.reset(); 387 360 } 388 361 exitActivity(); 389 - updateRequired = true; 362 + requestUpdate(); 390 363 }, 391 364 [this](const int newSpineIndex, const int newPage) { 392 365 if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) { ··· 395 368 section.reset(); 396 369 } 397 370 exitActivity(); 398 - updateRequired = true; 371 + requestUpdate(); 399 372 })); 400 373 401 - xSemaphoreGive(renderingMutex); 402 374 break; 403 375 } 404 376 case EpubReaderMenuActivity::MenuAction::GO_TO_PERCENT: { ··· 409 381 bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; 410 382 } 411 383 const int initialPercent = clampPercent(static_cast<int>(bookProgress + 0.5f)); 412 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 413 384 exitActivity(); 414 385 enterNewActivity(new EpubReaderPercentSelectionActivity( 415 386 renderer, mappedInput, initialPercent, ··· 417 388 // Apply the new position and exit back to the reader. 418 389 jumpToPercent(percent); 419 390 exitActivity(); 420 - updateRequired = true; 391 + requestUpdate(); 421 392 }, 422 393 [this]() { 423 394 // Cancel selection and return to the reader. 424 395 exitActivity(); 425 - updateRequired = true; 396 + requestUpdate(); 426 397 })); 427 - xSemaphoreGive(renderingMutex); 428 398 break; 429 399 } 430 400 case EpubReaderMenuActivity::MenuAction::GO_HOME: { ··· 457 427 } 458 428 case EpubReaderMenuActivity::MenuAction::SYNC: { 459 429 if (KOREADER_STORE.hasCredentials()) { 460 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 461 430 const int currentPage = section ? section->currentPage : 0; 462 431 const int totalPages = section ? section->pageCount : 0; 463 432 exitActivity(); ··· 476 445 } 477 446 pendingSubactivityExit = true; 478 447 })); 479 - xSemaphoreGive(renderingMutex); 480 448 } 481 449 break; 482 450 } ··· 509 477 xSemaphoreGive(renderingMutex); 510 478 } 511 479 512 - void EpubReaderActivity::displayTaskLoop() { 513 - while (true) { 514 - if (updateRequired) { 515 - updateRequired = false; 516 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 517 - renderScreen(); 518 - xSemaphoreGive(renderingMutex); 519 - } 520 - vTaskDelay(10 / portTICK_PERIOD_MS); 521 - } 522 - } 523 - 524 480 // TODO: Failure handling 525 - void EpubReaderActivity::renderScreen() { 481 + void EpubReaderActivity::render(Activity::RenderLock&& lock) { 526 482 if (!epub) { 527 483 return; 528 484 } ··· 643 599 LOG_ERR("ERS", "Failed to load page from SD - clearing section cache"); 644 600 section->clearCache(); 645 601 section.reset(); 646 - return renderScreen(); 602 + requestUpdate(); // Try again after clearing cache 603 + // TODO: prevent infinite loop if the page keeps failing to load for some reason 604 + return; 647 605 } 648 606 const auto start = millis(); 649 607 renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
+1 -9
src/activities/reader/EpubReaderActivity.h
··· 1 1 #pragma once 2 2 #include <Epub.h> 3 3 #include <Epub/Section.h> 4 - #include <freertos/FreeRTOS.h> 5 - #include <freertos/semphr.h> 6 - #include <freertos/task.h> 7 4 8 5 #include "EpubReaderMenuActivity.h" 9 6 #include "activities/ActivityWithSubactivity.h" ··· 11 8 class EpubReaderActivity final : public ActivityWithSubactivity { 12 9 std::shared_ptr<Epub> epub; 13 10 std::unique_ptr<Section> section = nullptr; 14 - TaskHandle_t displayTaskHandle = nullptr; 15 - SemaphoreHandle_t renderingMutex = nullptr; 16 11 int currentSpineIndex = 0; 17 12 int nextPageNumber = 0; 18 13 int pagesUntilFullRefresh = 0; ··· 23 18 bool pendingPercentJump = false; 24 19 // Normalized 0.0-1.0 progress within the target spine item, computed from book percentage. 25 20 float pendingSpineProgress = 0.0f; 26 - bool updateRequired = false; 27 21 bool pendingSubactivityExit = false; // Defer subactivity exit to avoid use-after-free 28 22 bool pendingGoHome = false; // Defer go home to avoid race condition with display task 29 23 bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit 30 24 const std::function<void()> onGoBack; 31 25 const std::function<void()> onGoHome; 32 26 33 - static void taskTrampoline(void* param); 34 - [[noreturn]] void displayTaskLoop(); 35 - void renderScreen(); 36 27 void renderContents(std::unique_ptr<Page> page, int orientedMarginTop, int orientedMarginRight, 37 28 int orientedMarginBottom, int orientedMarginLeft); 38 29 void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const; ··· 53 44 void onEnter() override; 54 45 void onExit() override; 55 46 void loop() override; 47 + void render(Activity::RenderLock&& lock) override; 56 48 };
+7 -43
src/activities/reader/EpubReaderChapterSelectionActivity.cpp
··· 24 24 return std::max(1, availableHeight / lineHeight); 25 25 } 26 26 27 - void EpubReaderChapterSelectionActivity::taskTrampoline(void* param) { 28 - auto* self = static_cast<EpubReaderChapterSelectionActivity*>(param); 29 - self->displayTaskLoop(); 30 - } 31 - 32 27 void EpubReaderChapterSelectionActivity::onEnter() { 33 28 ActivityWithSubactivity::onEnter(); 34 29 35 30 if (!epub) { 36 31 return; 37 32 } 38 - 39 - renderingMutex = xSemaphoreCreateMutex(); 40 33 41 34 selectorIndex = epub->getTocIndexForSpineIndex(currentSpineIndex); 42 35 if (selectorIndex == -1) { ··· 44 37 } 45 38 46 39 // Trigger first update 47 - updateRequired = true; 48 - xTaskCreate(&EpubReaderChapterSelectionActivity::taskTrampoline, "EpubReaderChapterSelectionActivityTask", 49 - 4096, // Stack size 50 - this, // Parameters 51 - 1, // Priority 52 - &displayTaskHandle // Task handle 53 - ); 40 + requestUpdate(); 54 41 } 55 42 56 - void EpubReaderChapterSelectionActivity::onExit() { 57 - ActivityWithSubactivity::onExit(); 58 - 59 - // Wait until not rendering to delete task to avoid killing mid-instruction to EPD 60 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 61 - if (displayTaskHandle) { 62 - vTaskDelete(displayTaskHandle); 63 - displayTaskHandle = nullptr; 64 - } 65 - vSemaphoreDelete(renderingMutex); 66 - renderingMutex = nullptr; 67 - } 43 + void EpubReaderChapterSelectionActivity::onExit() { ActivityWithSubactivity::onExit(); } 68 44 69 45 void EpubReaderChapterSelectionActivity::loop() { 70 46 if (subActivity) { ··· 88 64 89 65 buttonNavigator.onNextRelease([this, totalItems] { 90 66 selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); 91 - updateRequired = true; 67 + requestUpdate(); 92 68 }); 93 69 94 70 buttonNavigator.onPreviousRelease([this, totalItems] { 95 71 selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems); 96 - updateRequired = true; 72 + requestUpdate(); 97 73 }); 98 74 99 75 buttonNavigator.onNextContinuous([this, totalItems, pageItems] { 100 76 selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems); 101 - updateRequired = true; 77 + requestUpdate(); 102 78 }); 103 79 104 80 buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] { 105 81 selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems); 106 - updateRequired = true; 82 + requestUpdate(); 107 83 }); 108 84 } 109 85 110 - void EpubReaderChapterSelectionActivity::displayTaskLoop() { 111 - while (true) { 112 - if (updateRequired && !subActivity) { 113 - updateRequired = false; 114 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 115 - renderScreen(); 116 - xSemaphoreGive(renderingMutex); 117 - } 118 - vTaskDelay(10 / portTICK_PERIOD_MS); 119 - } 120 - } 121 - 122 - void EpubReaderChapterSelectionActivity::renderScreen() { 86 + void EpubReaderChapterSelectionActivity::render(Activity::RenderLock&&) { 123 87 renderer.clearScreen(); 124 88 125 89 const auto pageWidth = renderer.getScreenWidth();
+2 -10
src/activities/reader/EpubReaderChapterSelectionActivity.h
··· 1 1 #pragma once 2 2 #include <Epub.h> 3 - #include <freertos/FreeRTOS.h> 4 - #include <freertos/semphr.h> 5 - #include <freertos/task.h> 6 3 7 4 #include <memory> 8 5 ··· 12 9 class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity { 13 10 std::shared_ptr<Epub> epub; 14 11 std::string epubPath; 15 - TaskHandle_t displayTaskHandle = nullptr; 16 - SemaphoreHandle_t renderingMutex = nullptr; 17 12 ButtonNavigator buttonNavigator; 18 13 int currentSpineIndex = 0; 19 14 int currentPage = 0; 20 15 int totalPagesInSpine = 0; 21 16 int selectorIndex = 0; 22 - bool updateRequired = false; 17 + 23 18 const std::function<void()> onGoBack; 24 19 const std::function<void(int newSpineIndex)> onSelectSpineIndex; 25 20 const std::function<void(int newSpineIndex, int newPage)> onSyncPosition; ··· 30 25 31 26 // Total TOC items count 32 27 int getTotalItems() const; 33 - 34 - static void taskTrampoline(void* param); 35 - [[noreturn]] void displayTaskLoop(); 36 - void renderScreen(); 37 28 38 29 public: 39 30 explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, ··· 54 45 void onEnter() override; 55 46 void onExit() override; 56 47 void loop() override; 48 + void render(Activity::RenderLock&&) override; 57 49 };
+6 -35
src/activities/reader/EpubReaderMenuActivity.cpp
··· 8 8 9 9 void EpubReaderMenuActivity::onEnter() { 10 10 ActivityWithSubactivity::onEnter(); 11 - renderingMutex = xSemaphoreCreateMutex(); 12 - updateRequired = true; 13 - 14 - xTaskCreate(&EpubReaderMenuActivity::taskTrampoline, "EpubMenuTask", 4096, this, 1, &displayTaskHandle); 11 + requestUpdate(); 15 12 } 16 13 17 - void EpubReaderMenuActivity::onExit() { 18 - ActivityWithSubactivity::onExit(); 19 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 20 - if (displayTaskHandle) { 21 - vTaskDelete(displayTaskHandle); 22 - displayTaskHandle = nullptr; 23 - } 24 - vSemaphoreDelete(renderingMutex); 25 - renderingMutex = nullptr; 26 - } 27 - 28 - void EpubReaderMenuActivity::taskTrampoline(void* param) { 29 - auto* self = static_cast<EpubReaderMenuActivity*>(param); 30 - self->displayTaskLoop(); 31 - } 32 - 33 - void EpubReaderMenuActivity::displayTaskLoop() { 34 - while (true) { 35 - if (updateRequired && !subActivity) { 36 - updateRequired = false; 37 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 38 - renderScreen(); 39 - xSemaphoreGive(renderingMutex); 40 - } 41 - vTaskDelay(10 / portTICK_PERIOD_MS); 42 - } 43 - } 14 + void EpubReaderMenuActivity::onExit() { ActivityWithSubactivity::onExit(); } 44 15 45 16 void EpubReaderMenuActivity::loop() { 46 17 if (subActivity) { ··· 51 22 // Handle navigation 52 23 buttonNavigator.onNext([this] { 53 24 selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size())); 54 - updateRequired = true; 25 + requestUpdate(); 55 26 }); 56 27 57 28 buttonNavigator.onPrevious([this] { 58 29 selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast<int>(menuItems.size())); 59 - updateRequired = true; 30 + requestUpdate(); 60 31 }); 61 32 62 33 // Use local variables for items we need to check after potential deletion ··· 65 36 if (selectedAction == MenuAction::ROTATE_SCREEN) { 66 37 // Cycle orientation preview locally; actual rotation happens on menu exit. 67 38 pendingOrientation = (pendingOrientation + 1) % orientationLabels.size(); 68 - updateRequired = true; 39 + requestUpdate(); 69 40 return; 70 41 } 71 42 ··· 84 55 } 85 56 } 86 57 87 - void EpubReaderMenuActivity::renderScreen() { 58 + void EpubReaderMenuActivity::render(Activity::RenderLock&&) { 88 59 renderer.clearScreen(); 89 60 const auto pageWidth = renderer.getScreenWidth(); 90 61 const auto orientation = renderer.getOrientation();
+2 -10
src/activities/reader/EpubReaderMenuActivity.h
··· 1 1 #pragma once 2 2 #include <Epub.h> 3 - #include <freertos/FreeRTOS.h> 4 - #include <freertos/semphr.h> 5 - #include <freertos/task.h> 6 3 7 4 #include <functional> 8 5 #include <string> ··· 32 29 void onEnter() override; 33 30 void onExit() override; 34 31 void loop() override; 32 + void render(Activity::RenderLock&&) override; 35 33 36 34 private: 37 35 struct MenuItem { ··· 46 44 {MenuAction::SYNC, "Sync Progress"}, {MenuAction::DELETE_CACHE, "Delete Book Cache"}}; 47 45 48 46 int selectedIndex = 0; 49 - bool updateRequired = false; 50 - TaskHandle_t displayTaskHandle = nullptr; 51 - SemaphoreHandle_t renderingMutex = nullptr; 47 + 52 48 ButtonNavigator buttonNavigator; 53 49 std::string title = "Reader Menu"; 54 50 uint8_t pendingOrientation = 0; ··· 59 55 60 56 const std::function<void(uint8_t)> onBack; 61 57 const std::function<void(MenuAction)> onAction; 62 - 63 - static void taskTrampoline(void* param); 64 - [[noreturn]] void displayTaskLoop(); 65 - void renderScreen(); 66 58 };
+4 -35
src/activities/reader/EpubReaderPercentSelectionActivity.cpp
··· 15 15 void EpubReaderPercentSelectionActivity::onEnter() { 16 16 ActivityWithSubactivity::onEnter(); 17 17 // Set up rendering task and mark first frame dirty. 18 - renderingMutex = xSemaphoreCreateMutex(); 19 - updateRequired = true; 20 - xTaskCreate(&EpubReaderPercentSelectionActivity::taskTrampoline, "EpubPercentSlider", 4096, this, 1, 21 - &displayTaskHandle); 18 + requestUpdate(); 22 19 } 23 20 24 - void EpubReaderPercentSelectionActivity::onExit() { 25 - ActivityWithSubactivity::onExit(); 26 - // Ensure the render task is stopped before freeing the mutex. 27 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 28 - if (displayTaskHandle) { 29 - vTaskDelete(displayTaskHandle); 30 - displayTaskHandle = nullptr; 31 - } 32 - vSemaphoreDelete(renderingMutex); 33 - renderingMutex = nullptr; 34 - } 35 - 36 - void EpubReaderPercentSelectionActivity::taskTrampoline(void* param) { 37 - auto* self = static_cast<EpubReaderPercentSelectionActivity*>(param); 38 - self->displayTaskLoop(); 39 - } 40 - 41 - void EpubReaderPercentSelectionActivity::displayTaskLoop() { 42 - while (true) { 43 - // Render only when the view is dirty and no subactivity is running. 44 - if (updateRequired && !subActivity) { 45 - updateRequired = false; 46 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 47 - renderScreen(); 48 - xSemaphoreGive(renderingMutex); 49 - } 50 - vTaskDelay(10 / portTICK_PERIOD_MS); 51 - } 52 - } 21 + void EpubReaderPercentSelectionActivity::onExit() { ActivityWithSubactivity::onExit(); } 53 22 54 23 void EpubReaderPercentSelectionActivity::adjustPercent(const int delta) { 55 24 // Apply delta and clamp within 0-100. ··· 59 28 } else if (percent > 100) { 60 29 percent = 100; 61 30 } 62 - updateRequired = true; 31 + requestUpdate(); 63 32 } 64 33 65 34 void EpubReaderPercentSelectionActivity::loop() { ··· 86 55 buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { adjustPercent(-kLargeStep); }); 87 56 } 88 57 89 - void EpubReaderPercentSelectionActivity::renderScreen() { 58 + void EpubReaderPercentSelectionActivity::render(Activity::RenderLock&&) { 90 59 renderer.clearScreen(); 91 60 92 61 // Title and numeric percent value.
+2 -12
src/activities/reader/EpubReaderPercentSelectionActivity.h
··· 1 1 #pragma once 2 - #include <freertos/FreeRTOS.h> 3 - #include <freertos/semphr.h> 4 - #include <freertos/task.h> 5 2 6 3 #include <functional> 7 4 ··· 23 20 void onEnter() override; 24 21 void onExit() override; 25 22 void loop() override; 23 + void render(Activity::RenderLock&&) override; 26 24 27 25 private: 28 26 // Current percent value (0-100) shown on the slider. 29 27 int percent = 0; 30 - // Render dirty flag for the task loop. 31 - bool updateRequired = false; 32 - // FreeRTOS task and mutex for rendering. 33 - TaskHandle_t displayTaskHandle = nullptr; 34 - SemaphoreHandle_t renderingMutex = nullptr; 28 + 35 29 ButtonNavigator buttonNavigator; 36 30 37 31 // Callback invoked when the user confirms a percent. ··· 39 33 // Callback invoked when the user cancels the slider. 40 34 const std::function<void()> onCancel; 41 35 42 - static void taskTrampoline(void* param); 43 - [[noreturn]] void displayTaskLoop(); 44 - // Render the slider UI. 45 - void renderScreen(); 46 36 // Change the current percent by a delta and clamp within bounds. 47 37 void adjustPercent(int delta); 48 38 };
+17 -53
src/activities/reader/KOReaderSyncActivity.cpp
··· 40 40 } 41 41 } // namespace 42 42 43 - void KOReaderSyncActivity::taskTrampoline(void* param) { 44 - auto* self = static_cast<KOReaderSyncActivity*>(param); 45 - self->displayTaskLoop(); 46 - } 47 - 48 43 void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { 49 44 exitActivity(); 50 45 ··· 60 55 state = SYNCING; 61 56 statusMessage = "Syncing time..."; 62 57 xSemaphoreGive(renderingMutex); 63 - updateRequired = true; 58 + requestUpdate(); 64 59 65 60 // Sync time with NTP before making API requests 66 61 syncTimeWithNTP(); ··· 68 63 xSemaphoreTake(renderingMutex, portMAX_DELAY); 69 64 statusMessage = "Calculating document hash..."; 70 65 xSemaphoreGive(renderingMutex); 71 - updateRequired = true; 66 + requestUpdate(); 72 67 73 68 performSync(); 74 69 } ··· 85 80 state = SYNC_FAILED; 86 81 statusMessage = "Failed to calculate document hash"; 87 82 xSemaphoreGive(renderingMutex); 88 - updateRequired = true; 83 + requestUpdate(); 89 84 return; 90 85 } 91 86 ··· 94 89 xSemaphoreTake(renderingMutex, portMAX_DELAY); 95 90 statusMessage = "Fetching remote progress..."; 96 91 xSemaphoreGive(renderingMutex); 97 - updateRequired = true; 98 - vTaskDelay(10 / portTICK_PERIOD_MS); 92 + requestUpdateAndWait(); 99 93 100 94 // Fetch remote progress 101 95 const auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress); ··· 106 100 state = NO_REMOTE_PROGRESS; 107 101 hasRemoteProgress = false; 108 102 xSemaphoreGive(renderingMutex); 109 - updateRequired = true; 103 + requestUpdate(); 110 104 return; 111 105 } 112 106 ··· 115 109 state = SYNC_FAILED; 116 110 statusMessage = KOReaderSyncClient::errorString(result); 117 111 xSemaphoreGive(renderingMutex); 118 - updateRequired = true; 112 + requestUpdate(); 119 113 return; 120 114 } 121 115 ··· 132 126 state = SHOWING_RESULT; 133 127 selectedOption = 0; // Default to "Apply" 134 128 xSemaphoreGive(renderingMutex); 135 - updateRequired = true; 129 + requestUpdate(); 136 130 } 137 131 138 132 void KOReaderSyncActivity::performUpload() { ··· 140 134 state = UPLOADING; 141 135 statusMessage = "Uploading progress..."; 142 136 xSemaphoreGive(renderingMutex); 143 - updateRequired = true; 144 - vTaskDelay(10 / portTICK_PERIOD_MS); 137 + requestUpdate(); 138 + requestUpdateAndWait(); 145 139 146 140 // Convert current position to KOReader format 147 141 CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine}; ··· 159 153 state = SYNC_FAILED; 160 154 statusMessage = KOReaderSyncClient::errorString(result); 161 155 xSemaphoreGive(renderingMutex); 162 - updateRequired = true; 156 + requestUpdate(); 163 157 return; 164 158 } 165 159 166 160 xSemaphoreTake(renderingMutex, portMAX_DELAY); 167 161 state = UPLOAD_COMPLETE; 168 162 xSemaphoreGive(renderingMutex); 169 - updateRequired = true; 163 + requestUpdate(); 170 164 } 171 165 172 166 void KOReaderSyncActivity::onEnter() { 173 167 ActivityWithSubactivity::onEnter(); 174 168 175 - renderingMutex = xSemaphoreCreateMutex(); 176 - 177 - xTaskCreate(&KOReaderSyncActivity::taskTrampoline, "KOSyncTask", 178 - 4096, // Stack size (larger for network operations) 179 - this, // Parameters 180 - 1, // Priority 181 - &displayTaskHandle // Task handle 182 - ); 183 - 184 169 // Check for credentials first 185 170 if (!KOREADER_STORE.hasCredentials()) { 186 171 state = NO_CREDENTIALS; 187 - updateRequired = true; 172 + requestUpdate(); 188 173 return; 189 174 } 190 175 ··· 197 182 LOG_DBG("KOSync", "Already connected to WiFi"); 198 183 state = SYNCING; 199 184 statusMessage = "Syncing time..."; 200 - updateRequired = true; 185 + requestUpdate(); 201 186 202 187 // Perform sync directly (will be handled in loop) 203 188 xTaskCreate( ··· 208 193 xSemaphoreTake(self->renderingMutex, portMAX_DELAY); 209 194 self->statusMessage = "Calculating document hash..."; 210 195 xSemaphoreGive(self->renderingMutex); 211 - self->updateRequired = true; 196 + self->requestUpdate(); 212 197 self->performSync(); 213 198 vTaskDelete(nullptr); 214 199 }, ··· 230 215 delay(100); 231 216 WiFi.mode(WIFI_OFF); 232 217 delay(100); 233 - 234 - // Wait until not rendering to delete task 235 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 236 - if (displayTaskHandle) { 237 - vTaskDelete(displayTaskHandle); 238 - displayTaskHandle = nullptr; 239 - } 240 - vSemaphoreDelete(renderingMutex); 241 - renderingMutex = nullptr; 242 218 } 243 219 244 - void KOReaderSyncActivity::displayTaskLoop() { 245 - while (true) { 246 - if (updateRequired) { 247 - updateRequired = false; 248 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 249 - render(); 250 - xSemaphoreGive(renderingMutex); 251 - } 252 - vTaskDelay(10 / portTICK_PERIOD_MS); 253 - } 254 - } 255 - 256 - void KOReaderSyncActivity::render() { 220 + void KOReaderSyncActivity::render(Activity::RenderLock&&) { 257 221 if (subActivity) { 258 222 return; 259 223 } ··· 388 352 if (mappedInput.wasPressed(MappedInputManager::Button::Up) || 389 353 mappedInput.wasPressed(MappedInputManager::Button::Left)) { 390 354 selectedOption = (selectedOption + 1) % 2; // Wrap around among 2 options 391 - updateRequired = true; 355 + requestUpdate(); 392 356 } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || 393 357 mappedInput.wasPressed(MappedInputManager::Button::Right)) { 394 358 selectedOption = (selectedOption + 1) % 2; // Wrap around among 2 options 395 - updateRequired = true; 359 + requestUpdate(); 396 360 } 397 361 398 362 if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
+1 -11
src/activities/reader/KOReaderSyncActivity.h
··· 1 1 #pragma once 2 2 #include <Epub.h> 3 - #include <freertos/FreeRTOS.h> 4 - #include <freertos/semphr.h> 5 - #include <freertos/task.h> 6 3 7 4 #include <functional> 8 5 #include <memory> ··· 45 42 void onEnter() override; 46 43 void onExit() override; 47 44 void loop() override; 45 + void render(Activity::RenderLock&&) override; 48 46 bool preventAutoSleep() override { return state == CONNECTING || state == SYNCING; } 49 47 50 48 private: ··· 66 64 int currentPage; 67 65 int totalPagesInSpine; 68 66 69 - TaskHandle_t displayTaskHandle = nullptr; 70 - SemaphoreHandle_t renderingMutex = nullptr; 71 - bool updateRequired = false; 72 - 73 67 State state = WIFI_SELECTION; 74 68 std::string statusMessage; 75 69 std::string documentHash; ··· 91 85 void onWifiSelectionComplete(bool success); 92 86 void performSync(); 93 87 void performUpload(); 94 - 95 - static void taskTrampoline(void* param); 96 - [[noreturn]] void displayTaskLoop(); 97 - void render(); 98 88 };
+4 -38
src/activities/reader/TxtReaderActivity.cpp
··· 23 23 constexpr uint8_t CACHE_VERSION = 2; // Increment when cache format changes 24 24 } // namespace 25 25 26 - void TxtReaderActivity::taskTrampoline(void* param) { 27 - auto* self = static_cast<TxtReaderActivity*>(param); 28 - self->displayTaskLoop(); 29 - } 30 - 31 26 void TxtReaderActivity::onEnter() { 32 27 ActivityWithSubactivity::onEnter(); 33 28 ··· 53 48 break; 54 49 } 55 50 56 - renderingMutex = xSemaphoreCreateMutex(); 57 - 58 51 txt->setupCacheDir(); 59 52 60 53 // Save current txt as last opened file and add to recent books ··· 65 58 RECENT_BOOKS.addBook(filePath, fileName, "", ""); 66 59 67 60 // Trigger first update 68 - updateRequired = true; 69 - 70 - xTaskCreate(&TxtReaderActivity::taskTrampoline, "TxtReaderActivityTask", 71 - 6144, // Stack size 72 - this, // Parameters 73 - 1, // Priority 74 - &displayTaskHandle // Task handle 75 - ); 61 + requestUpdate(); 76 62 } 77 63 78 64 void TxtReaderActivity::onExit() { ··· 81 67 // Reset orientation back to portrait for the rest of the UI 82 68 renderer.setOrientation(GfxRenderer::Orientation::Portrait); 83 69 84 - // Wait until not rendering to delete task 85 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 86 - if (displayTaskHandle) { 87 - vTaskDelete(displayTaskHandle); 88 - displayTaskHandle = nullptr; 89 - } 90 - vSemaphoreDelete(renderingMutex); 91 - renderingMutex = nullptr; 92 70 pageOffsets.clear(); 93 71 currentPageLines.clear(); 94 72 APP_STATE.readerActivityLoadCount = 0; ··· 134 112 135 113 if (prevTriggered && currentPage > 0) { 136 114 currentPage--; 137 - updateRequired = true; 115 + requestUpdate(); 138 116 } else if (nextTriggered && currentPage < totalPages - 1) { 139 117 currentPage++; 140 - updateRequired = true; 141 - } 142 - } 143 - 144 - void TxtReaderActivity::displayTaskLoop() { 145 - while (true) { 146 - if (updateRequired) { 147 - updateRequired = false; 148 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 149 - renderScreen(); 150 - xSemaphoreGive(renderingMutex); 151 - } 152 - vTaskDelay(10 / portTICK_PERIOD_MS); 118 + requestUpdate(); 153 119 } 154 120 } 155 121 ··· 372 338 return !outLines.empty(); 373 339 } 374 340 375 - void TxtReaderActivity::renderScreen() { 341 + void TxtReaderActivity::render(Activity::RenderLock&&) { 376 342 if (!txt) { 377 343 return; 378 344 }
+3 -9
src/activities/reader/TxtReaderActivity.h
··· 1 1 #pragma once 2 2 3 3 #include <Txt.h> 4 - #include <freertos/FreeRTOS.h> 5 - #include <freertos/semphr.h> 6 - #include <freertos/task.h> 7 4 8 5 #include <vector> 9 6 ··· 12 9 13 10 class TxtReaderActivity final : public ActivityWithSubactivity { 14 11 std::unique_ptr<Txt> txt; 15 - TaskHandle_t displayTaskHandle = nullptr; 16 - SemaphoreHandle_t renderingMutex = nullptr; 12 + 17 13 int currentPage = 0; 18 14 int totalPages = 1; 19 15 int pagesUntilFullRefresh = 0; 20 - bool updateRequired = false; 16 + 21 17 const std::function<void()> onGoBack; 22 18 const std::function<void()> onGoHome; 23 19 ··· 33 29 int cachedScreenMargin = 0; 34 30 uint8_t cachedParagraphAlignment = CrossPointSettings::LEFT_ALIGN; 35 31 36 - static void taskTrampoline(void* param); 37 - [[noreturn]] void displayTaskLoop(); 38 - void renderScreen(); 39 32 void renderPage(); 40 33 void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const; 41 34 ··· 57 50 void onEnter() override; 58 51 void onExit() override; 59 52 void loop() override; 53 + void render(Activity::RenderLock&&) override; 60 54 };
+7 -43
src/activities/reader/XtcReaderActivity.cpp
··· 24 24 constexpr unsigned long goHomeMs = 1000; 25 25 } // namespace 26 26 27 - void XtcReaderActivity::taskTrampoline(void* param) { 28 - auto* self = static_cast<XtcReaderActivity*>(param); 29 - self->displayTaskLoop(); 30 - } 31 - 32 27 void XtcReaderActivity::onEnter() { 33 28 ActivityWithSubactivity::onEnter(); 34 29 35 30 if (!xtc) { 36 31 return; 37 32 } 38 - 39 - renderingMutex = xSemaphoreCreateMutex(); 40 33 41 34 xtc->setupCacheDir(); 42 35 ··· 49 42 RECENT_BOOKS.addBook(xtc->getPath(), xtc->getTitle(), xtc->getAuthor(), xtc->getThumbBmpPath()); 50 43 51 44 // Trigger first update 52 - updateRequired = true; 53 - 54 - xTaskCreate(&XtcReaderActivity::taskTrampoline, "XtcReaderActivityTask", 55 - 4096, // Stack size (smaller than EPUB since no parsing needed) 56 - this, // Parameters 57 - 1, // Priority 58 - &displayTaskHandle // Task handle 59 - ); 45 + requestUpdate(); 60 46 } 61 47 62 48 void XtcReaderActivity::onExit() { 63 49 ActivityWithSubactivity::onExit(); 64 50 65 - // Wait until not rendering to delete task 66 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 67 - if (displayTaskHandle) { 68 - vTaskDelete(displayTaskHandle); 69 - displayTaskHandle = nullptr; 70 - } 71 - vSemaphoreDelete(renderingMutex); 72 - renderingMutex = nullptr; 73 51 APP_STATE.readerActivityLoadCount = 0; 74 52 APP_STATE.saveToFile(); 75 53 xtc.reset(); ··· 85 63 // Enter chapter selection activity 86 64 if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 87 65 if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) { 88 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 89 66 exitActivity(); 90 67 enterNewActivity(new XtcReaderChapterSelectionActivity( 91 68 this->renderer, this->mappedInput, xtc, currentPage, 92 69 [this] { 93 70 exitActivity(); 94 - updateRequired = true; 71 + requestUpdate(); 95 72 }, 96 73 [this](const uint32_t newPage) { 97 74 currentPage = newPage; 98 75 exitActivity(); 99 - updateRequired = true; 76 + requestUpdate(); 100 77 })); 101 - xSemaphoreGive(renderingMutex); 102 78 } 103 79 } 104 80 ··· 135 111 // Handle end of book 136 112 if (currentPage >= xtc->getPageCount()) { 137 113 currentPage = xtc->getPageCount() - 1; 138 - updateRequired = true; 114 + requestUpdate(); 139 115 return; 140 116 } 141 117 ··· 148 124 } else { 149 125 currentPage = 0; 150 126 } 151 - updateRequired = true; 127 + requestUpdate(); 152 128 } else if (nextTriggered) { 153 129 currentPage += skipAmount; 154 130 if (currentPage >= xtc->getPageCount()) { 155 131 currentPage = xtc->getPageCount(); // Allow showing "End of book" 156 132 } 157 - updateRequired = true; 133 + requestUpdate(); 158 134 } 159 135 } 160 136 161 - void XtcReaderActivity::displayTaskLoop() { 162 - while (true) { 163 - if (updateRequired) { 164 - updateRequired = false; 165 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 166 - renderScreen(); 167 - xSemaphoreGive(renderingMutex); 168 - } 169 - vTaskDelay(10 / portTICK_PERIOD_MS); 170 - } 171 - } 172 - 173 - void XtcReaderActivity::renderScreen() { 137 + void XtcReaderActivity::render(Activity::RenderLock&&) { 174 138 if (!xtc) { 175 139 return; 176 140 }
+3 -9
src/activities/reader/XtcReaderActivity.h
··· 8 8 #pragma once 9 9 10 10 #include <Xtc.h> 11 - #include <freertos/FreeRTOS.h> 12 - #include <freertos/semphr.h> 13 - #include <freertos/task.h> 14 11 15 12 #include "activities/ActivityWithSubactivity.h" 16 13 17 14 class XtcReaderActivity final : public ActivityWithSubactivity { 18 15 std::shared_ptr<Xtc> xtc; 19 - TaskHandle_t displayTaskHandle = nullptr; 20 - SemaphoreHandle_t renderingMutex = nullptr; 16 + 21 17 uint32_t currentPage = 0; 22 18 int pagesUntilFullRefresh = 0; 23 - bool updateRequired = false; 19 + 24 20 const std::function<void()> onGoBack; 25 21 const std::function<void()> onGoHome; 26 22 27 - static void taskTrampoline(void* param); 28 - [[noreturn]] void displayTaskLoop(); 29 - void renderScreen(); 30 23 void renderPage(); 31 24 void saveProgress() const; 32 25 void loadProgress(); ··· 41 34 void onEnter() override; 42 35 void onExit() override; 43 36 void loop() override; 37 + void render(Activity::RenderLock&&) override; 44 38 };
+7 -41
src/activities/reader/XtcReaderChapterSelectionActivity.cpp
··· 37 37 return 0; 38 38 } 39 39 40 - void XtcReaderChapterSelectionActivity::taskTrampoline(void* param) { 41 - auto* self = static_cast<XtcReaderChapterSelectionActivity*>(param); 42 - self->displayTaskLoop(); 43 - } 44 - 45 40 void XtcReaderChapterSelectionActivity::onEnter() { 46 41 Activity::onEnter(); 47 42 ··· 49 44 return; 50 45 } 51 46 52 - renderingMutex = xSemaphoreCreateMutex(); 53 47 selectorIndex = findChapterIndexForPage(currentPage); 54 48 55 - updateRequired = true; 56 - xTaskCreate(&XtcReaderChapterSelectionActivity::taskTrampoline, "XtcReaderChapterSelectionActivityTask", 57 - 4096, // Stack size 58 - this, // Parameters 59 - 1, // Priority 60 - &displayTaskHandle // Task handle 61 - ); 49 + requestUpdate(); 62 50 } 63 51 64 - void XtcReaderChapterSelectionActivity::onExit() { 65 - Activity::onExit(); 66 - 67 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 68 - if (displayTaskHandle) { 69 - vTaskDelete(displayTaskHandle); 70 - displayTaskHandle = nullptr; 71 - } 72 - vSemaphoreDelete(renderingMutex); 73 - renderingMutex = nullptr; 74 - } 52 + void XtcReaderChapterSelectionActivity::onExit() { Activity::onExit(); } 75 53 76 54 void XtcReaderChapterSelectionActivity::loop() { 77 55 const int pageItems = getPageItems(); ··· 88 66 89 67 buttonNavigator.onNextRelease([this, totalItems] { 90 68 selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); 91 - updateRequired = true; 69 + requestUpdate(); 92 70 }); 93 71 94 72 buttonNavigator.onPreviousRelease([this, totalItems] { 95 73 selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems); 96 - updateRequired = true; 74 + requestUpdate(); 97 75 }); 98 76 99 77 buttonNavigator.onNextContinuous([this, totalItems, pageItems] { 100 78 selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems); 101 - updateRequired = true; 79 + requestUpdate(); 102 80 }); 103 81 104 82 buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] { 105 83 selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems); 106 - updateRequired = true; 84 + requestUpdate(); 107 85 }); 108 86 } 109 87 110 - void XtcReaderChapterSelectionActivity::displayTaskLoop() { 111 - while (true) { 112 - if (updateRequired) { 113 - updateRequired = false; 114 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 115 - renderScreen(); 116 - xSemaphoreGive(renderingMutex); 117 - } 118 - vTaskDelay(10 / portTICK_PERIOD_MS); 119 - } 120 - } 121 - 122 - void XtcReaderChapterSelectionActivity::renderScreen() { 88 + void XtcReaderChapterSelectionActivity::render(Activity::RenderLock&&) { 123 89 renderer.clearScreen(); 124 90 125 91 const auto pageWidth = renderer.getScreenWidth();
+2 -10
src/activities/reader/XtcReaderChapterSelectionActivity.h
··· 1 1 #pragma once 2 2 #include <Xtc.h> 3 - #include <freertos/FreeRTOS.h> 4 - #include <freertos/semphr.h> 5 - #include <freertos/task.h> 6 3 7 4 #include <memory> 8 5 ··· 11 8 12 9 class XtcReaderChapterSelectionActivity final : public Activity { 13 10 std::shared_ptr<Xtc> xtc; 14 - TaskHandle_t displayTaskHandle = nullptr; 15 - SemaphoreHandle_t renderingMutex = nullptr; 16 11 ButtonNavigator buttonNavigator; 17 12 uint32_t currentPage = 0; 18 13 int selectorIndex = 0; 19 - bool updateRequired = false; 14 + 20 15 const std::function<void()> onGoBack; 21 16 const std::function<void(uint32_t newPage)> onSelectPage; 22 17 23 18 int getPageItems() const; 24 19 int findChapterIndexForPage(uint32_t page) const; 25 - 26 - static void taskTrampoline(void* param); 27 - [[noreturn]] void displayTaskLoop(); 28 - void renderScreen(); 29 20 30 21 public: 31 22 explicit XtcReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, ··· 40 31 void onEnter() override; 41 32 void onExit() override; 42 33 void loop() override; 34 + void render(Activity::RenderLock&&) override; 43 35 };
+33 -65
src/activities/settings/ButtonRemapActivity.cpp
··· 16 16 constexpr unsigned long kErrorDisplayMs = 1500; 17 17 } // namespace 18 18 19 - void ButtonRemapActivity::taskTrampoline(void* param) { 20 - auto* self = static_cast<ButtonRemapActivity*>(param); 21 - self->displayTaskLoop(); 22 - } 23 - 24 19 void ButtonRemapActivity::onEnter() { 25 20 Activity::onEnter(); 26 21 27 - renderingMutex = xSemaphoreCreateMutex(); 28 22 // Start with all roles unassigned to avoid duplicate blocking. 29 23 currentStep = 0; 30 24 tempMapping[0] = kUnassigned; ··· 33 27 tempMapping[3] = kUnassigned; 34 28 errorMessage.clear(); 35 29 errorUntil = 0; 36 - updateRequired = true; 37 - 38 - xTaskCreate(&ButtonRemapActivity::taskTrampoline, "ButtonRemapTask", 4096, this, 1, &displayTaskHandle); 30 + requestUpdate(); 39 31 } 40 32 41 - void ButtonRemapActivity::onExit() { 42 - Activity::onExit(); 33 + void ButtonRemapActivity::onExit() { Activity::onExit(); } 43 34 44 - // Ensure display task is stopped outside of active rendering. 45 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 46 - if (displayTaskHandle) { 47 - vTaskDelete(displayTaskHandle); 48 - displayTaskHandle = nullptr; 35 + void ButtonRemapActivity::loop() { 36 + // Clear any temporary warning after its timeout. 37 + if (errorUntil > 0 && millis() > errorUntil) { 38 + errorMessage.clear(); 39 + errorUntil = 0; 40 + requestUpdate(); 41 + return; 49 42 } 50 - vSemaphoreDelete(renderingMutex); 51 - renderingMutex = nullptr; 52 - } 53 43 54 - void ButtonRemapActivity::loop() { 55 44 // Side buttons: 56 45 // - Up: reset mapping to defaults and exit. 57 46 // - Down: cancel without saving. ··· 72 61 return; 73 62 } 74 63 75 - // Wait for the UI to refresh before accepting another assignment. 76 - // This avoids rapid double-presses that can advance the step without a visible redraw. 77 - if (updateRequired) { 78 - return; 79 - } 80 - 81 - // Wait for a front button press to assign to the current role. 82 - const int pressedButton = mappedInput.getPressedFrontButton(); 83 - if (pressedButton < 0) { 84 - return; 85 - } 86 - 87 - // Update temporary mapping and advance the remap step. 88 - // Only accept the press if this hardware button isn't already assigned elsewhere. 89 - if (!validateUnassigned(static_cast<uint8_t>(pressedButton))) { 90 - updateRequired = true; 91 - return; 92 - } 93 - tempMapping[currentStep] = static_cast<uint8_t>(pressedButton); 94 - currentStep++; 64 + { 65 + // Wait for the UI to refresh before accepting another assignment. 66 + // This avoids rapid double-presses that can advance the step without a visible redraw. 67 + requestUpdateAndWait(); 95 68 96 - if (currentStep >= kRoleCount) { 97 - // All roles assigned; save to settings and exit. 98 - applyTempMapping(); 99 - SETTINGS.saveToFile(); 100 - onBack(); 101 - return; 102 - } 103 - 104 - updateRequired = true; 105 - } 69 + // Wait for a front button press to assign to the current role. 70 + const int pressedButton = mappedInput.getPressedFrontButton(); 71 + if (pressedButton < 0) { 72 + return; 73 + } 106 74 107 - [[noreturn]] void ButtonRemapActivity::displayTaskLoop() { 108 - while (true) { 109 - if (updateRequired) { 110 - // Ensure render calls are serialized with UI thread changes. 111 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 112 - render(); 113 - updateRequired = false; 114 - xSemaphoreGive(renderingMutex); 75 + // Update temporary mapping and advance the remap step. 76 + // Only accept the press if this hardware button isn't already assigned elsewhere. 77 + if (!validateUnassigned(static_cast<uint8_t>(pressedButton))) { 78 + requestUpdate(); 79 + return; 115 80 } 81 + tempMapping[currentStep] = static_cast<uint8_t>(pressedButton); 82 + currentStep++; 116 83 117 - // Clear any temporary warning after its timeout. 118 - if (errorUntil > 0 && millis() > errorUntil) { 119 - errorMessage.clear(); 120 - errorUntil = 0; 121 - updateRequired = true; 84 + if (currentStep >= kRoleCount) { 85 + // All roles assigned; save to settings and exit. 86 + applyTempMapping(); 87 + SETTINGS.saveToFile(); 88 + onBack(); 89 + return; 122 90 } 123 91 124 - vTaskDelay(50 / portTICK_PERIOD_MS); 92 + requestUpdate(); 125 93 } 126 94 } 127 95 128 - void ButtonRemapActivity::render() { 96 + void ButtonRemapActivity::render(Activity::RenderLock&&) { 129 97 renderer.clearScreen(); 130 98 131 99 const auto pageWidth = renderer.getScreenWidth();
+1 -11
src/activities/settings/ButtonRemapActivity.h
··· 1 1 #pragma once 2 - #include <freertos/FreeRTOS.h> 3 - #include <freertos/semphr.h> 4 - #include <freertos/task.h> 5 2 6 3 #include <functional> 7 4 #include <string> ··· 17 14 void onEnter() override; 18 15 void onExit() override; 19 16 void loop() override; 17 + void render(Activity::RenderLock&&) override; 20 18 21 19 private: 22 20 // Rendering task state. 23 - TaskHandle_t displayTaskHandle = nullptr; 24 - SemaphoreHandle_t renderingMutex = nullptr; 25 - bool updateRequired = false; 26 21 27 22 // Callback used to exit the remap flow back to the settings list. 28 23 const std::function<void()> onBack; ··· 33 28 // Error banner timing (used when reassigning duplicate buttons). 34 29 unsigned long errorUntil = 0; 35 30 std::string errorMessage; 36 - 37 - // FreeRTOS task helpers. 38 - static void taskTrampoline(void* param); 39 - [[noreturn]] void displayTaskLoop(); 40 - void render(); 41 31 42 32 // Commit temporary mapping to settings. 43 33 void applyTempMapping();
+11 -50
src/activities/settings/CalibreSettingsActivity.cpp
··· 15 15 const char* menuNames[MENU_ITEMS] = {"OPDS Server URL", "Username", "Password"}; 16 16 } // namespace 17 17 18 - void CalibreSettingsActivity::taskTrampoline(void* param) { 19 - auto* self = static_cast<CalibreSettingsActivity*>(param); 20 - self->displayTaskLoop(); 21 - } 22 - 23 18 void CalibreSettingsActivity::onEnter() { 24 19 ActivityWithSubactivity::onEnter(); 25 20 26 - renderingMutex = xSemaphoreCreateMutex(); 27 21 selectedIndex = 0; 28 - updateRequired = true; 29 - 30 - xTaskCreate(&CalibreSettingsActivity::taskTrampoline, "CalibreSettingsTask", 31 - 4096, // Stack size 32 - this, // Parameters 33 - 1, // Priority 34 - &displayTaskHandle // Task handle 35 - ); 22 + requestUpdate(); 36 23 } 37 24 38 - void CalibreSettingsActivity::onExit() { 39 - ActivityWithSubactivity::onExit(); 40 - 41 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 42 - if (displayTaskHandle) { 43 - vTaskDelete(displayTaskHandle); 44 - displayTaskHandle = nullptr; 45 - } 46 - vSemaphoreDelete(renderingMutex); 47 - renderingMutex = nullptr; 48 - } 25 + void CalibreSettingsActivity::onExit() { ActivityWithSubactivity::onExit(); } 49 26 50 27 void CalibreSettingsActivity::loop() { 51 28 if (subActivity) { ··· 66 43 // Handle navigation 67 44 buttonNavigator.onNext([this] { 68 45 selectedIndex = (selectedIndex + 1) % MENU_ITEMS; 69 - updateRequired = true; 46 + requestUpdate(); 70 47 }); 71 48 72 49 buttonNavigator.onPrevious([this] { 73 50 selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS; 74 - updateRequired = true; 51 + requestUpdate(); 75 52 }); 76 53 } 77 54 78 55 void CalibreSettingsActivity::handleSelection() { 79 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 80 - 81 56 if (selectedIndex == 0) { 82 57 // OPDS Server URL 83 58 exitActivity(); ··· 90 65 SETTINGS.opdsServerUrl[sizeof(SETTINGS.opdsServerUrl) - 1] = '\0'; 91 66 SETTINGS.saveToFile(); 92 67 exitActivity(); 93 - updateRequired = true; 68 + requestUpdate(); 94 69 }, 95 70 [this]() { 96 71 exitActivity(); 97 - updateRequired = true; 72 + requestUpdate(); 98 73 })); 99 74 } else if (selectedIndex == 1) { 100 75 // Username ··· 108 83 SETTINGS.opdsUsername[sizeof(SETTINGS.opdsUsername) - 1] = '\0'; 109 84 SETTINGS.saveToFile(); 110 85 exitActivity(); 111 - updateRequired = true; 86 + requestUpdate(); 112 87 }, 113 88 [this]() { 114 89 exitActivity(); 115 - updateRequired = true; 90 + requestUpdate(); 116 91 })); 117 92 } else if (selectedIndex == 2) { 118 93 // Password ··· 126 101 SETTINGS.opdsPassword[sizeof(SETTINGS.opdsPassword) - 1] = '\0'; 127 102 SETTINGS.saveToFile(); 128 103 exitActivity(); 129 - updateRequired = true; 104 + requestUpdate(); 130 105 }, 131 106 [this]() { 132 107 exitActivity(); 133 - updateRequired = true; 108 + requestUpdate(); 134 109 })); 135 110 } 136 - 137 - xSemaphoreGive(renderingMutex); 138 111 } 139 112 140 - void CalibreSettingsActivity::displayTaskLoop() { 141 - while (true) { 142 - if (updateRequired && !subActivity) { 143 - updateRequired = false; 144 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 145 - render(); 146 - xSemaphoreGive(renderingMutex); 147 - } 148 - vTaskDelay(10 / portTICK_PERIOD_MS); 149 - } 150 - } 151 - 152 - void CalibreSettingsActivity::render() { 113 + void CalibreSettingsActivity::render(Activity::RenderLock&&) { 153 114 renderer.clearScreen(); 154 115 155 116 const auto pageWidth = renderer.getScreenWidth();
+1 -10
src/activities/settings/CalibreSettingsActivity.h
··· 1 1 #pragma once 2 - #include <freertos/FreeRTOS.h> 3 - #include <freertos/semphr.h> 4 - #include <freertos/task.h> 5 2 6 3 #include <functional> 7 4 ··· 21 18 void onEnter() override; 22 19 void onExit() override; 23 20 void loop() override; 21 + void render(Activity::RenderLock&&) override; 24 22 25 23 private: 26 - TaskHandle_t displayTaskHandle = nullptr; 27 - SemaphoreHandle_t renderingMutex = nullptr; 28 24 ButtonNavigator buttonNavigator; 29 - bool updateRequired = false; 30 25 31 26 int selectedIndex = 0; 32 27 const std::function<void()> onBack; 33 - 34 - static void taskTrampoline(void* param); 35 - [[noreturn]] void displayTaskLoop(); 36 - void render(); 37 28 void handleSelection(); 38 29 };
+6 -43
src/activities/settings/ClearCacheActivity.cpp
··· 8 8 #include "components/UITheme.h" 9 9 #include "fontIds.h" 10 10 11 - void ClearCacheActivity::taskTrampoline(void* param) { 12 - auto* self = static_cast<ClearCacheActivity*>(param); 13 - self->displayTaskLoop(); 14 - } 15 - 16 11 void ClearCacheActivity::onEnter() { 17 12 ActivityWithSubactivity::onEnter(); 18 13 19 - renderingMutex = xSemaphoreCreateMutex(); 20 14 state = WARNING; 21 - updateRequired = true; 22 - 23 - xTaskCreate(&ClearCacheActivity::taskTrampoline, "ClearCacheActivityTask", 24 - 4096, // Stack size 25 - this, // Parameters 26 - 1, // Priority 27 - &displayTaskHandle // Task handle 28 - ); 15 + requestUpdate(); 29 16 } 30 17 31 - void ClearCacheActivity::onExit() { 32 - ActivityWithSubactivity::onExit(); 18 + void ClearCacheActivity::onExit() { ActivityWithSubactivity::onExit(); } 33 19 34 - // Wait until not rendering to delete task to avoid killing mid-instruction to EPD 35 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 36 - if (displayTaskHandle) { 37 - vTaskDelete(displayTaskHandle); 38 - displayTaskHandle = nullptr; 39 - } 40 - vSemaphoreDelete(renderingMutex); 41 - renderingMutex = nullptr; 42 - } 43 - 44 - void ClearCacheActivity::displayTaskLoop() { 45 - while (true) { 46 - if (updateRequired) { 47 - updateRequired = false; 48 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 49 - render(); 50 - xSemaphoreGive(renderingMutex); 51 - } 52 - vTaskDelay(10 / portTICK_PERIOD_MS); 53 - } 54 - } 55 - 56 - void ClearCacheActivity::render() { 20 + void ClearCacheActivity::render(Activity::RenderLock&&) { 57 21 const auto pageHeight = renderer.getScreenHeight(); 58 22 59 23 renderer.clearScreen(); ··· 112 76 LOG_DBG("CLEAR_CACHE", "Failed to open cache directory"); 113 77 if (root) root.close(); 114 78 state = FAILED; 115 - updateRequired = true; 79 + requestUpdate(); 116 80 return; 117 81 } 118 82 ··· 147 111 LOG_DBG("CLEAR_CACHE", "Cache cleared: %d removed, %d failed", clearedCount, failedCount); 148 112 149 113 state = SUCCESS; 150 - updateRequired = true; 114 + requestUpdate(); 151 115 } 152 116 153 117 void ClearCacheActivity::loop() { ··· 157 121 xSemaphoreTake(renderingMutex, portMAX_DELAY); 158 122 state = CLEARING; 159 123 xSemaphoreGive(renderingMutex); 160 - updateRequired = true; 161 - vTaskDelay(10 / portTICK_PERIOD_MS); 124 + requestUpdateAndWait(); 162 125 163 126 clearCache(); 164 127 }
+2 -11
src/activities/settings/ClearCacheActivity.h
··· 1 1 #pragma once 2 2 3 - #include <freertos/FreeRTOS.h> 4 - #include <freertos/semphr.h> 5 - #include <freertos/task.h> 6 - 7 3 #include <functional> 8 4 9 5 #include "activities/ActivityWithSubactivity.h" ··· 17 13 void onEnter() override; 18 14 void onExit() override; 19 15 void loop() override; 16 + void render(Activity::RenderLock&&) override; 20 17 21 18 private: 22 19 enum State { WARNING, CLEARING, SUCCESS, FAILED }; 23 20 24 21 State state = WARNING; 25 - TaskHandle_t displayTaskHandle = nullptr; 26 - SemaphoreHandle_t renderingMutex = nullptr; 27 - bool updateRequired = false; 22 + 28 23 const std::function<void()> goBack; 29 24 30 25 int clearedCount = 0; 31 26 int failedCount = 0; 32 - 33 - static void taskTrampoline(void* param); 34 - [[noreturn]] void displayTaskLoop(); 35 - void render(); 36 27 void clearCache(); 37 28 };
+5 -43
src/activities/settings/KOReaderAuthActivity.cpp
··· 10 10 #include "components/UITheme.h" 11 11 #include "fontIds.h" 12 12 13 - void KOReaderAuthActivity::taskTrampoline(void* param) { 14 - auto* self = static_cast<KOReaderAuthActivity*>(param); 15 - self->displayTaskLoop(); 16 - } 17 - 18 13 void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) { 19 14 exitActivity(); 20 15 ··· 23 18 state = FAILED; 24 19 errorMessage = "WiFi connection failed"; 25 20 xSemaphoreGive(renderingMutex); 26 - updateRequired = true; 21 + requestUpdate(); 27 22 return; 28 23 } 29 24 ··· 31 26 state = AUTHENTICATING; 32 27 statusMessage = "Authenticating..."; 33 28 xSemaphoreGive(renderingMutex); 34 - updateRequired = true; 29 + requestUpdate(); 35 30 36 31 performAuthentication(); 37 32 } ··· 48 43 errorMessage = KOReaderSyncClient::errorString(result); 49 44 } 50 45 xSemaphoreGive(renderingMutex); 51 - updateRequired = true; 46 + requestUpdate(); 52 47 } 53 48 54 49 void KOReaderAuthActivity::onEnter() { 55 50 ActivityWithSubactivity::onEnter(); 56 51 57 - renderingMutex = xSemaphoreCreateMutex(); 58 - 59 - xTaskCreate(&KOReaderAuthActivity::taskTrampoline, "KOAuthTask", 60 - 4096, // Stack size 61 - this, // Parameters 62 - 1, // Priority 63 - &displayTaskHandle // Task handle 64 - ); 65 - 66 52 // Turn on WiFi 67 53 WiFi.mode(WIFI_STA); 68 54 ··· 70 56 if (WiFi.status() == WL_CONNECTED) { 71 57 state = AUTHENTICATING; 72 58 statusMessage = "Authenticating..."; 73 - updateRequired = true; 59 + requestUpdate(); 74 60 75 61 // Perform authentication in a separate task 76 62 xTaskCreate( ··· 96 82 delay(100); 97 83 WiFi.mode(WIFI_OFF); 98 84 delay(100); 99 - 100 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 101 - if (displayTaskHandle) { 102 - vTaskDelete(displayTaskHandle); 103 - displayTaskHandle = nullptr; 104 - } 105 - vSemaphoreDelete(renderingMutex); 106 - renderingMutex = nullptr; 107 85 } 108 86 109 - void KOReaderAuthActivity::displayTaskLoop() { 110 - while (true) { 111 - if (updateRequired && !subActivity) { 112 - updateRequired = false; 113 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 114 - render(); 115 - xSemaphoreGive(renderingMutex); 116 - } 117 - vTaskDelay(10 / portTICK_PERIOD_MS); 118 - } 119 - } 120 - 121 - void KOReaderAuthActivity::render() { 122 - if (subActivity) { 123 - return; 124 - } 125 - 87 + void KOReaderAuthActivity::render(Activity::RenderLock&&) { 126 88 renderer.clearScreen(); 127 89 renderer.drawCenteredText(UI_12_FONT_ID, 15, "KOReader Auth", true, EpdFontFamily::BOLD); 128 90
+1 -11
src/activities/settings/KOReaderAuthActivity.h
··· 1 1 #pragma once 2 - #include <freertos/FreeRTOS.h> 3 - #include <freertos/semphr.h> 4 - #include <freertos/task.h> 5 2 6 3 #include <functional> 7 4 ··· 20 17 void onEnter() override; 21 18 void onExit() override; 22 19 void loop() override; 20 + void render(Activity::RenderLock&&) override; 23 21 bool preventAutoSleep() override { return state == CONNECTING || state == AUTHENTICATING; } 24 22 25 23 private: 26 24 enum State { WIFI_SELECTION, CONNECTING, AUTHENTICATING, SUCCESS, FAILED }; 27 25 28 - TaskHandle_t displayTaskHandle = nullptr; 29 - SemaphoreHandle_t renderingMutex = nullptr; 30 - bool updateRequired = false; 31 - 32 26 State state = WIFI_SELECTION; 33 27 std::string statusMessage; 34 28 std::string errorMessage; ··· 37 31 38 32 void onWifiSelectionComplete(bool success); 39 33 void performAuthentication(); 40 - 41 - static void taskTrampoline(void* param); 42 - [[noreturn]] void displayTaskLoop(); 43 - void render(); 44 34 };
+13 -52
src/activities/settings/KOReaderSettingsActivity.cpp
··· 16 16 const char* menuNames[MENU_ITEMS] = {"Username", "Password", "Sync Server URL", "Document Matching", "Authenticate"}; 17 17 } // namespace 18 18 19 - void KOReaderSettingsActivity::taskTrampoline(void* param) { 20 - auto* self = static_cast<KOReaderSettingsActivity*>(param); 21 - self->displayTaskLoop(); 22 - } 23 - 24 19 void KOReaderSettingsActivity::onEnter() { 25 20 ActivityWithSubactivity::onEnter(); 26 21 27 - renderingMutex = xSemaphoreCreateMutex(); 28 22 selectedIndex = 0; 29 - updateRequired = true; 30 - 31 - xTaskCreate(&KOReaderSettingsActivity::taskTrampoline, "KOReaderSettingsTask", 32 - 4096, // Stack size 33 - this, // Parameters 34 - 1, // Priority 35 - &displayTaskHandle // Task handle 36 - ); 23 + requestUpdate(); 37 24 } 38 25 39 - void KOReaderSettingsActivity::onExit() { 40 - ActivityWithSubactivity::onExit(); 41 - 42 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 43 - if (displayTaskHandle) { 44 - vTaskDelete(displayTaskHandle); 45 - displayTaskHandle = nullptr; 46 - } 47 - vSemaphoreDelete(renderingMutex); 48 - renderingMutex = nullptr; 49 - } 26 + void KOReaderSettingsActivity::onExit() { ActivityWithSubactivity::onExit(); } 50 27 51 28 void KOReaderSettingsActivity::loop() { 52 29 if (subActivity) { ··· 67 44 // Handle navigation 68 45 buttonNavigator.onNext([this] { 69 46 selectedIndex = (selectedIndex + 1) % MENU_ITEMS; 70 - updateRequired = true; 47 + requestUpdate(); 71 48 }); 72 49 73 50 buttonNavigator.onPrevious([this] { 74 51 selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS; 75 - updateRequired = true; 52 + requestUpdate(); 76 53 }); 77 54 } 78 55 79 56 void KOReaderSettingsActivity::handleSelection() { 80 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 81 - 82 57 if (selectedIndex == 0) { 83 58 // Username 84 59 exitActivity(); ··· 90 65 KOREADER_STORE.setCredentials(username, KOREADER_STORE.getPassword()); 91 66 KOREADER_STORE.saveToFile(); 92 67 exitActivity(); 93 - updateRequired = true; 68 + requestUpdate(); 94 69 }, 95 70 [this]() { 96 71 exitActivity(); 97 - updateRequired = true; 72 + requestUpdate(); 98 73 })); 99 74 } else if (selectedIndex == 1) { 100 75 // Password ··· 107 82 KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), password); 108 83 KOREADER_STORE.saveToFile(); 109 84 exitActivity(); 110 - updateRequired = true; 85 + requestUpdate(); 111 86 }, 112 87 [this]() { 113 88 exitActivity(); 114 - updateRequired = true; 89 + requestUpdate(); 115 90 })); 116 91 } else if (selectedIndex == 2) { 117 92 // Sync Server URL - prefill with https:// if empty to save typing ··· 128 103 KOREADER_STORE.setServerUrl(urlToSave); 129 104 KOREADER_STORE.saveToFile(); 130 105 exitActivity(); 131 - updateRequired = true; 106 + requestUpdate(); 132 107 }, 133 108 [this]() { 134 109 exitActivity(); 135 - updateRequired = true; 110 + requestUpdate(); 136 111 })); 137 112 } else if (selectedIndex == 3) { 138 113 // Document Matching - toggle between Filename and Binary ··· 141 116 (current == DocumentMatchMethod::FILENAME) ? DocumentMatchMethod::BINARY : DocumentMatchMethod::FILENAME; 142 117 KOREADER_STORE.setMatchMethod(newMethod); 143 118 KOREADER_STORE.saveToFile(); 144 - updateRequired = true; 119 + requestUpdate(); 145 120 } else if (selectedIndex == 4) { 146 121 // Authenticate 147 122 if (!KOREADER_STORE.hasCredentials()) { ··· 152 127 exitActivity(); 153 128 enterNewActivity(new KOReaderAuthActivity(renderer, mappedInput, [this] { 154 129 exitActivity(); 155 - updateRequired = true; 130 + requestUpdate(); 156 131 })); 157 132 } 158 - 159 - xSemaphoreGive(renderingMutex); 160 133 } 161 134 162 - void KOReaderSettingsActivity::displayTaskLoop() { 163 - while (true) { 164 - if (updateRequired && !subActivity) { 165 - updateRequired = false; 166 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 167 - render(); 168 - xSemaphoreGive(renderingMutex); 169 - } 170 - vTaskDelay(10 / portTICK_PERIOD_MS); 171 - } 172 - } 173 - 174 - void KOReaderSettingsActivity::render() { 135 + void KOReaderSettingsActivity::render(Activity::RenderLock&&) { 175 136 renderer.clearScreen(); 176 137 177 138 const auto pageWidth = renderer.getScreenWidth();
+1 -9
src/activities/settings/KOReaderSettingsActivity.h
··· 1 1 #pragma once 2 - #include <freertos/FreeRTOS.h> 3 - #include <freertos/semphr.h> 4 - #include <freertos/task.h> 5 2 6 3 #include <functional> 7 4 ··· 21 18 void onEnter() override; 22 19 void onExit() override; 23 20 void loop() override; 21 + void render(Activity::RenderLock&&) override; 24 22 25 23 private: 26 - TaskHandle_t displayTaskHandle = nullptr; 27 - SemaphoreHandle_t renderingMutex = nullptr; 28 24 ButtonNavigator buttonNavigator; 29 - bool updateRequired = false; 30 25 31 26 int selectedIndex = 0; 32 27 const std::function<void()> onBack; 33 28 34 - static void taskTrampoline(void* param); 35 - [[noreturn]] void displayTaskLoop(); 36 - void render(); 37 29 void handleSelection(); 38 30 };
+27 -54
src/activities/settings/OtaUpdateActivity.cpp
··· 9 9 #include "fontIds.h" 10 10 #include "network/OtaUpdater.h" 11 11 12 - void OtaUpdateActivity::taskTrampoline(void* param) { 13 - auto* self = static_cast<OtaUpdateActivity*>(param); 14 - self->displayTaskLoop(); 15 - } 16 - 17 12 void OtaUpdateActivity::onWifiSelectionComplete(const bool success) { 18 13 exitActivity(); 19 14 ··· 28 23 xSemaphoreTake(renderingMutex, portMAX_DELAY); 29 24 state = CHECKING_FOR_UPDATE; 30 25 xSemaphoreGive(renderingMutex); 31 - updateRequired = true; 32 - vTaskDelay(10 / portTICK_PERIOD_MS); 26 + requestUpdateAndWait(); 27 + 33 28 const auto res = updater.checkForUpdate(); 34 29 if (res != OtaUpdater::OK) { 35 30 LOG_DBG("OTA", "Update check failed: %d", res); 36 31 xSemaphoreTake(renderingMutex, portMAX_DELAY); 37 32 state = FAILED; 38 33 xSemaphoreGive(renderingMutex); 39 - updateRequired = true; 34 + requestUpdate(); 40 35 return; 41 36 } 42 37 ··· 45 40 xSemaphoreTake(renderingMutex, portMAX_DELAY); 46 41 state = NO_UPDATE; 47 42 xSemaphoreGive(renderingMutex); 48 - updateRequired = true; 43 + requestUpdate(); 49 44 return; 50 45 } 51 46 52 47 xSemaphoreTake(renderingMutex, portMAX_DELAY); 53 48 state = WAITING_CONFIRMATION; 54 49 xSemaphoreGive(renderingMutex); 55 - updateRequired = true; 50 + requestUpdate(); 56 51 } 57 52 58 53 void OtaUpdateActivity::onEnter() { 59 54 ActivityWithSubactivity::onEnter(); 60 55 61 - renderingMutex = xSemaphoreCreateMutex(); 62 - 63 - xTaskCreate(&OtaUpdateActivity::taskTrampoline, "OtaUpdateActivityTask", 64 - 2048, // Stack size 65 - this, // Parameters 66 - 1, // Priority 67 - &displayTaskHandle // Task handle 68 - ); 69 - 70 56 // Turn on WiFi immediately 71 57 LOG_DBG("OTA", "Turning on WiFi..."); 72 58 WiFi.mode(WIFI_STA); ··· 85 71 delay(100); // Allow disconnect frame to be sent 86 72 WiFi.mode(WIFI_OFF); 87 73 delay(100); // Allow WiFi hardware to fully power down 88 - 89 - // Wait until not rendering to delete task to avoid killing mid-instruction to EPD 90 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 91 - if (displayTaskHandle) { 92 - vTaskDelete(displayTaskHandle); 93 - displayTaskHandle = nullptr; 94 - } 95 - vSemaphoreDelete(renderingMutex); 96 - renderingMutex = nullptr; 97 74 } 98 75 99 - void OtaUpdateActivity::displayTaskLoop() { 100 - while (true) { 101 - if (updateRequired || updater.getRender()) { 102 - updateRequired = false; 103 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 104 - render(); 105 - xSemaphoreGive(renderingMutex); 106 - } 107 - vTaskDelay(10 / portTICK_PERIOD_MS); 108 - } 109 - } 110 - 111 - void OtaUpdateActivity::render() { 76 + void OtaUpdateActivity::render(Activity::RenderLock&&) { 112 77 if (subActivity) { 113 78 // Subactivity handles its own rendering 114 79 return; ··· 182 147 } 183 148 184 149 void OtaUpdateActivity::loop() { 150 + // TODO @ngxson : refactor this logic later 151 + if (updater.getRender()) { 152 + requestUpdate(); 153 + } 154 + 185 155 if (subActivity) { 186 156 subActivity->loop(); 187 157 return; ··· 190 160 if (state == WAITING_CONFIRMATION) { 191 161 if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { 192 162 LOG_DBG("OTA", "New update available, starting download..."); 193 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 194 - state = UPDATE_IN_PROGRESS; 195 - xSemaphoreGive(renderingMutex); 196 - updateRequired = true; 197 - vTaskDelay(10 / portTICK_PERIOD_MS); 163 + { 164 + RenderLock lock(*this); 165 + state = UPDATE_IN_PROGRESS; 166 + } 167 + requestUpdate(); 168 + requestUpdateAndWait(); 198 169 const auto res = updater.installUpdate(); 199 170 200 171 if (res != OtaUpdater::OK) { 201 172 LOG_DBG("OTA", "Update failed: %d", res); 202 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 203 - state = FAILED; 204 - xSemaphoreGive(renderingMutex); 205 - updateRequired = true; 173 + { 174 + RenderLock lock(*this); 175 + state = FAILED; 176 + } 177 + requestUpdate(); 206 178 return; 207 179 } 208 180 209 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 210 - state = FINISHED; 211 - xSemaphoreGive(renderingMutex); 212 - updateRequired = true; 181 + { 182 + RenderLock lock(*this); 183 + state = FINISHED; 184 + } 185 + requestUpdate(); 213 186 } 214 187 215 188 if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
+1 -9
src/activities/settings/OtaUpdateActivity.h
··· 1 1 #pragma once 2 - #include <freertos/FreeRTOS.h> 3 - #include <freertos/semphr.h> 4 - #include <freertos/task.h> 5 2 6 3 #include "activities/ActivityWithSubactivity.h" 7 4 #include "network/OtaUpdater.h" ··· 21 18 // Can't initialize this to 0 or the first render doesn't happen 22 19 static constexpr unsigned int UNINITIALIZED_PERCENTAGE = 111; 23 20 24 - TaskHandle_t displayTaskHandle = nullptr; 25 - SemaphoreHandle_t renderingMutex = nullptr; 26 - bool updateRequired = false; 27 21 const std::function<void()> goBack; 28 22 State state = WIFI_SELECTION; 29 23 unsigned int lastUpdaterPercentage = UNINITIALIZED_PERCENTAGE; 30 24 OtaUpdater updater; 31 25 32 26 void onWifiSelectionComplete(bool success); 33 - static void taskTrampoline(void* param); 34 - [[noreturn]] void displayTaskLoop(); 35 - void render(); 36 27 37 28 public: 38 29 explicit OtaUpdateActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, ··· 41 32 void onEnter() override; 42 33 void onExit() override; 43 34 void loop() override; 35 + void render(Activity::RenderLock&&) override; 44 36 bool preventAutoSleep() override { return state == CHECKING_FOR_UPDATE || state == UPDATE_IN_PROGRESS; } 45 37 };
+10 -46
src/activities/settings/SettingsActivity.cpp
··· 17 17 18 18 const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"}; 19 19 20 - void SettingsActivity::taskTrampoline(void* param) { 21 - auto* self = static_cast<SettingsActivity*>(param); 22 - self->displayTaskLoop(); 23 - } 24 - 25 20 void SettingsActivity::onEnter() { 26 21 Activity::onEnter(); 27 - renderingMutex = xSemaphoreCreateMutex(); 28 22 29 23 // Build per-category vectors from the shared settings list 30 24 displaySettings.clear(); ··· 64 58 settingsCount = static_cast<int>(displaySettings.size()); 65 59 66 60 // Trigger first update 67 - updateRequired = true; 68 - 69 - xTaskCreate(&SettingsActivity::taskTrampoline, "SettingsActivityTask", 70 - 4096, // Stack size 71 - this, // Parameters 72 - 1, // Priority 73 - &displayTaskHandle // Task handle 74 - ); 61 + requestUpdate(); 75 62 } 76 63 77 64 void SettingsActivity::onExit() { 78 65 ActivityWithSubactivity::onExit(); 79 66 80 - // Wait until not rendering to delete task to avoid killing mid-instruction to EPD 81 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 82 - if (displayTaskHandle) { 83 - vTaskDelete(displayTaskHandle); 84 - displayTaskHandle = nullptr; 85 - } 86 - vSemaphoreDelete(renderingMutex); 87 - renderingMutex = nullptr; 88 - 89 67 UITheme::getInstance().reload(); // Re-apply theme in case it was changed 90 68 } 91 69 ··· 101 79 if (selectedSettingIndex == 0) { 102 80 selectedCategoryIndex = (selectedCategoryIndex < categoryCount - 1) ? (selectedCategoryIndex + 1) : 0; 103 81 hasChangedCategory = true; 104 - updateRequired = true; 82 + requestUpdate(); 105 83 } else { 106 84 toggleCurrentSetting(); 107 - updateRequired = true; 85 + requestUpdate(); 108 86 return; 109 87 } 110 88 } ··· 118 96 // Handle navigation 119 97 buttonNavigator.onNextRelease([this] { 120 98 selectedSettingIndex = ButtonNavigator::nextIndex(selectedSettingIndex, settingsCount + 1); 121 - updateRequired = true; 99 + requestUpdate(); 122 100 }); 123 101 124 102 buttonNavigator.onPreviousRelease([this] { 125 103 selectedSettingIndex = ButtonNavigator::previousIndex(selectedSettingIndex, settingsCount + 1); 126 - updateRequired = true; 104 + requestUpdate(); 127 105 }); 128 106 129 107 buttonNavigator.onNextContinuous([this, &hasChangedCategory] { 130 108 hasChangedCategory = true; 131 109 selectedCategoryIndex = ButtonNavigator::nextIndex(selectedCategoryIndex, categoryCount); 132 - updateRequired = true; 110 + requestUpdate(); 133 111 }); 134 112 135 113 buttonNavigator.onPreviousContinuous([this, &hasChangedCategory] { 136 114 hasChangedCategory = true; 137 115 selectedCategoryIndex = ButtonNavigator::previousIndex(selectedCategoryIndex, categoryCount); 138 - updateRequired = true; 116 + requestUpdate(); 139 117 }); 140 118 141 119 if (hasChangedCategory) { ··· 182 160 } 183 161 } else if (setting.type == SettingType::ACTION) { 184 162 auto enterSubActivity = [this](Activity* activity) { 185 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 186 163 exitActivity(); 187 164 enterNewActivity(activity); 188 - xSemaphoreGive(renderingMutex); 189 165 }; 190 166 191 167 auto onComplete = [this] { 192 168 exitActivity(); 193 - updateRequired = true; 169 + requestUpdate(); 194 170 }; 195 171 196 172 auto onCompleteBool = [this](bool) { 197 173 exitActivity(); 198 - updateRequired = true; 174 + requestUpdate(); 199 175 }; 200 176 201 177 switch (setting.action) { ··· 228 204 SETTINGS.saveToFile(); 229 205 } 230 206 231 - void SettingsActivity::displayTaskLoop() { 232 - while (true) { 233 - if (updateRequired && !subActivity) { 234 - updateRequired = false; 235 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 236 - render(); 237 - xSemaphoreGive(renderingMutex); 238 - } 239 - vTaskDelay(10 / portTICK_PERIOD_MS); 240 - } 241 - } 242 - 243 - void SettingsActivity::render() const { 207 + void SettingsActivity::render(Activity::RenderLock&&) { 244 208 renderer.clearScreen(); 245 209 246 210 const auto pageWidth = renderer.getScreenWidth();
+3 -10
src/activities/settings/SettingsActivity.h
··· 1 1 #pragma once 2 - #include <freertos/FreeRTOS.h> 3 - #include <freertos/semphr.h> 4 - #include <freertos/task.h> 5 2 6 3 #include <functional> 7 4 #include <string> ··· 135 132 }; 136 133 137 134 class SettingsActivity final : public ActivityWithSubactivity { 138 - TaskHandle_t displayTaskHandle = nullptr; 139 - SemaphoreHandle_t renderingMutex = nullptr; 140 135 ButtonNavigator buttonNavigator; 141 - bool updateRequired = false; 136 + 142 137 int selectedCategoryIndex = 0; // Currently selected category 143 138 int selectedSettingIndex = 0; 144 139 int settingsCount = 0; ··· 155 150 static constexpr int categoryCount = 4; 156 151 static const char* categoryNames[categoryCount]; 157 152 158 - static void taskTrampoline(void* param); 159 - [[noreturn]] void displayTaskLoop(); 160 - void render() const; 161 153 void enterCategory(int categoryIndex); 162 154 void toggleCurrentSetting(); 163 155 ··· 168 160 void onEnter() override; 169 161 void onExit() override; 170 162 void loop() override; 171 - }; 163 + void render(Activity::RenderLock&&) override; 164 + };
+9 -46
src/activities/util/KeyboardEntryActivity.cpp
··· 17 17 // Shift state strings 18 18 const char* const KeyboardEntryActivity::shiftString[3] = {"shift", "SHIFT", "LOCK"}; 19 19 20 - void KeyboardEntryActivity::taskTrampoline(void* param) { 21 - auto* self = static_cast<KeyboardEntryActivity*>(param); 22 - self->displayTaskLoop(); 23 - } 24 - 25 - void KeyboardEntryActivity::displayTaskLoop() { 26 - while (true) { 27 - if (updateRequired) { 28 - updateRequired = false; 29 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 30 - render(); 31 - xSemaphoreGive(renderingMutex); 32 - } 33 - vTaskDelay(10 / portTICK_PERIOD_MS); 34 - } 35 - } 36 - 37 20 void KeyboardEntryActivity::onEnter() { 38 21 Activity::onEnter(); 39 22 40 - renderingMutex = xSemaphoreCreateMutex(); 41 - 42 23 // Trigger first update 43 - updateRequired = true; 44 - 45 - xTaskCreate(&KeyboardEntryActivity::taskTrampoline, "KeyboardEntryActivity", 46 - 2048, // Stack size 47 - this, // Parameters 48 - 1, // Priority 49 - &displayTaskHandle // Task handle 50 - ); 24 + requestUpdate(); 51 25 } 52 26 53 - void KeyboardEntryActivity::onExit() { 54 - Activity::onExit(); 55 - 56 - // Wait until not rendering to delete task to avoid killing mid-instruction to EPD 57 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 58 - if (displayTaskHandle) { 59 - vTaskDelete(displayTaskHandle); 60 - displayTaskHandle = nullptr; 61 - } 62 - vSemaphoreDelete(renderingMutex); 63 - renderingMutex = nullptr; 64 - } 27 + void KeyboardEntryActivity::onExit() { Activity::onExit(); } 65 28 66 29 int KeyboardEntryActivity::getRowLength(const int row) const { 67 30 if (row < 0 || row >= NUM_ROWS) return 0; ··· 148 111 149 112 const int maxCol = getRowLength(selectedRow) - 1; 150 113 if (selectedCol > maxCol) selectedCol = maxCol; 151 - updateRequired = true; 114 + requestUpdate(); 152 115 }); 153 116 154 117 buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { ··· 156 119 157 120 const int maxCol = getRowLength(selectedRow) - 1; 158 121 if (selectedCol > maxCol) selectedCol = maxCol; 159 - updateRequired = true; 122 + requestUpdate(); 160 123 }); 161 124 162 125 buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { ··· 182 145 selectedCol = ButtonNavigator::previousIndex(selectedCol, maxCol + 1); 183 146 } 184 147 185 - updateRequired = true; 148 + requestUpdate(); 186 149 }); 187 150 188 151 buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { ··· 207 170 } else { 208 171 selectedCol = ButtonNavigator::nextIndex(selectedCol, maxCol + 1); 209 172 } 210 - updateRequired = true; 173 + requestUpdate(); 211 174 }); 212 175 213 176 // Selection 214 177 if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { 215 178 handleKeyPress(); 216 - updateRequired = true; 179 + requestUpdate(); 217 180 } 218 181 219 182 // Cancel ··· 221 184 if (onCancel) { 222 185 onCancel(); 223 186 } 224 - updateRequired = true; 187 + requestUpdate(); 225 188 } 226 189 } 227 190 228 - void KeyboardEntryActivity::render() const { 191 + void KeyboardEntryActivity::render(Activity::RenderLock&&) { 229 192 const auto pageWidth = renderer.getScreenWidth(); 230 193 231 194 renderer.clearScreen();
+2 -9
src/activities/util/KeyboardEntryActivity.h
··· 1 1 #pragma once 2 2 #include <GfxRenderer.h> 3 - #include <freertos/FreeRTOS.h> 4 - #include <freertos/semphr.h> 5 - #include <freertos/task.h> 6 3 7 4 #include <functional> 8 5 #include <string> ··· 57 54 void onEnter() override; 58 55 void onExit() override; 59 56 void loop() override; 57 + void render(Activity::RenderLock&&) override; 60 58 61 59 private: 62 60 std::string title; ··· 64 62 std::string text; 65 63 size_t maxLength; 66 64 bool isPassword; 67 - TaskHandle_t displayTaskHandle = nullptr; 68 - SemaphoreHandle_t renderingMutex = nullptr; 65 + 69 66 ButtonNavigator buttonNavigator; 70 - bool updateRequired = false; 71 67 72 68 // Keyboard state 73 69 int selectedRow = 0; ··· 92 88 static constexpr int BACKSPACE_COL = 7; 93 89 static constexpr int DONE_COL = 9; 94 90 95 - static void taskTrampoline(void* param); 96 - [[noreturn]] void displayTaskLoop(); 97 91 char getSelectedChar() const; 98 92 void handleKeyPress(); 99 93 int getRowLength(int row) const; 100 - void render() const; 101 94 void renderItemWithSelector(int x, int y, const char* item, bool isSelected) const; 102 95 };
+1 -1
src/network/OtaUpdater.cpp
··· 243 243 processedSize = esp_https_ota_get_image_len_read(ota_handle); 244 244 /* Sent signal to OtaUpdateActivity */ 245 245 render = true; 246 - vTaskDelay(10 / portTICK_PERIOD_MS); 246 + delay(100); // TODO: should we replace this with something better? 247 247 } while (esp_err == ESP_ERR_HTTPS_OTA_IN_PROGRESS); 248 248 249 249 /* Return back to default power saving for WiFi in case of failing */