From a4f4784ed9e3ead603e5e04983a13a16c6116e75 Mon Sep 17 00:00:00 2001 From: Ken Van Hoeylandt Date: Fri, 14 Nov 2025 15:43:00 +0100 Subject: [PATCH] Add low memory warning (#417) --- .../MemoryChecker/assets/memory_alert.png | Bin 0 -> 273 bytes Tactility/Include/Tactility/lvgl/Statusbar.h | 13 ++- .../memorychecker/MemoryCheckerService.h | 33 +++++++ Tactility/Source/Tactility.cpp | 2 + Tactility/Source/lvgl/Statusbar.cpp | 6 +- .../memorychecker/MemoryCheckerService.cpp | 93 ++++++++++++++++++ .../Source/service/statusbar/Statusbar.cpp | 2 + 7 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 Data/system/service/MemoryChecker/assets/memory_alert.png create mode 100644 Tactility/Private/Tactility/service/memorychecker/MemoryCheckerService.h create mode 100644 Tactility/Source/service/memorychecker/MemoryCheckerService.cpp diff --git a/Data/system/service/MemoryChecker/assets/memory_alert.png b/Data/system/service/MemoryChecker/assets/memory_alert.png new file mode 100644 index 0000000000000000000000000000000000000000..fc678c3dd8a903113aba120ff716b301570cdf02 GIT binary patch literal 273 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jPK-BC>eK@{Ea{HEjtmSN z`?>!lvI6-E$sR$z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8ZKG z?e4-L!642cY{D{G94NwB;1OBOz`zf*hCh*Uh5hnOpdfpRr>`sfOKv`14W^6Izhr?z zp`I>|Ar`04PB!E^;K0F>-aJ9i_uv1kIa16ki{^+;f9WC-6UZ=U!kH`5zF*A3GyDyN zSPG^ndtBPQB~`HPNciP#3{zPTT +#include + +namespace tt::service::memorychecker { + +/** + * Runs a background timer that validates if there's sufficient memory available. + * It shows a statusbar icon when memory is low. It also outputs warning to the log. + */ +class MemoryCheckerService final : public Service { + + Mutex mutex = Mutex(Mutex::Type::Recursive); + Timer timer = Timer(Timer::Type::Periodic, [this] { onTimerUpdate(); }); + + // LVGL Statusbar icon + int8_t statusbarIconId = -1; + // Keep track of state to minimize UI updates + bool memoryLow = false; + + void onTimerUpdate(); + +public: + + bool onStart(ServiceContext& service) override; + + void onStop(ServiceContext& service) override; +}; + +} diff --git a/Tactility/Source/Tactility.cpp b/Tactility/Source/Tactility.cpp index 9339e455..6c201635 100644 --- a/Tactility/Source/Tactility.cpp +++ b/Tactility/Source/Tactility.cpp @@ -43,6 +43,7 @@ namespace service { // Secondary (UI) namespace gui { extern const ServiceManifest manifest; } namespace loader { extern const ServiceManifest manifest; } + namespace memorychecker { extern const ServiceManifest manifest; } namespace statusbar { extern const ServiceManifest manifest; } #if TT_FEATURE_SCREENSHOT_ENABLED namespace screenshot { extern const ServiceManifest manifest; } @@ -215,6 +216,7 @@ static void registerAndStartSecondaryServices() { addService(service::loader::manifest); addService(service::gui::manifest); addService(service::statusbar::manifest); + addService(service::memorychecker::manifest); #if TT_FEATURE_SCREENSHOT_ENABLED addService(service::screenshot::manifest); #endif diff --git a/Tactility/Source/lvgl/Statusbar.cpp b/Tactility/Source/lvgl/Statusbar.cpp index dd755810..0c64cc96 100644 --- a/Tactility/Source/lvgl/Statusbar.cpp +++ b/Tactility/Source/lvgl/Statusbar.cpp @@ -229,13 +229,13 @@ static void statusbar_event(TT_UNUSED const lv_obj_class_t* class_p, lv_event_t* } } -int8_t statusbar_icon_add(const std::string& image) { +int8_t statusbar_icon_add(const std::string& image, bool visible) { statusbar_data.mutex.lock(); int8_t result = -1; for (int8_t i = 0; i < STATUSBAR_ICON_LIMIT; ++i) { if (!statusbar_data.icons[i].claimed) { statusbar_data.icons[i].claimed = true; - statusbar_data.icons[i].visible = !image.empty(); + statusbar_data.icons[i].visible = visible; statusbar_data.icons[i].image = image; result = i; TT_LOG_D(TAG, "id %d: added", i); @@ -248,7 +248,7 @@ int8_t statusbar_icon_add(const std::string& image) { } int8_t statusbar_icon_add() { - return statusbar_icon_add(""); + return statusbar_icon_add("", false); } void statusbar_icon_remove(int8_t id) { diff --git a/Tactility/Source/service/memorychecker/MemoryCheckerService.cpp b/Tactility/Source/service/memorychecker/MemoryCheckerService.cpp new file mode 100644 index 00000000..75d7897e --- /dev/null +++ b/Tactility/Source/service/memorychecker/MemoryCheckerService.cpp @@ -0,0 +1,93 @@ +#include +#include +#include +#include +#include +#include + +namespace tt::service::memorychecker { + +constexpr const char* TAG = "MemoryChecker"; +constexpr TickType_t TIMER_UPDATE_INTERVAL = 1000U / portTICK_PERIOD_MS; + +// Total memory (in bytes) that should be free before warnings occur +constexpr auto TOTAL_FREE_THRESHOLD = 10'000; +// Smallest memory block size (in bytes) that should be available before warnings occur +constexpr auto LARGEST_FREE_BLOCK_THRESHOLD = 2'000; + +static size_t getInternalFree() { +#ifdef ESP_PLATFORM + return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); +#else + // PC mock data + return 1024 * 1024; +#endif +} + +static size_t getInternalLargestFreeBlock() { +#ifdef ESP_PLATFORM + return heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL); +#else + // PC mock data + return 1024 * 1024; +#endif +} + +static bool isMemoryLow() { + bool memory_low = false; + const auto total_free = getInternalFree(); + if (total_free < TOTAL_FREE_THRESHOLD) { + TT_LOG_W(TAG, "Internal memory low: %zu bytes", total_free); + memory_low = true; + } + + const auto largest_block = getInternalLargestFreeBlock(); + if (largest_block < LARGEST_FREE_BLOCK_THRESHOLD) { + TT_LOG_W(TAG, "Largest free internal memory block is %zu bytes", largest_block); + memory_low = true; + } + + return memory_low; +} + +bool MemoryCheckerService::onStart(ServiceContext& service) { + auto lock = mutex.asScopedLock(); + lock.lock(); + + auto icon_path = std::string("A:") + service.getPaths()->getAssetsPath("memory_alert.png"); + statusbarIconId = lvgl::statusbar_icon_add(icon_path, false); + lvgl::statusbar_icon_set_visibility(statusbarIconId, false); + + timer.setThreadPriority(Thread::Priority::Lower); + timer.start(TIMER_UPDATE_INTERVAL); + + return true; +} + +void MemoryCheckerService::onStop(ServiceContext& service) { + timer.stop(); + + // Lock after timer stop, because the timer task might still be busy and this way we await it + auto lock = mutex.asScopedLock(); + lock.lock(); + + lvgl::statusbar_icon_remove(statusbarIconId); +} + +void MemoryCheckerService::onTimerUpdate() { + auto lock = mutex.asScopedLock(); + lock.lock(); + + bool memory_low = isMemoryLow(); + if (memory_low != memoryLow) { + memoryLow = memory_low; + lvgl::statusbar_icon_set_visibility(statusbarIconId, memory_low); + } +} + +extern const ServiceManifest manifest = { + .id = "MemoryChecker", + .createService = create +}; + +} diff --git a/Tactility/Source/service/statusbar/Statusbar.cpp b/Tactility/Source/service/statusbar/Statusbar.cpp index 2130f320..58ffabf5 100644 --- a/Tactility/Source/service/statusbar/Statusbar.cpp +++ b/Tactility/Source/service/statusbar/Statusbar.cpp @@ -269,6 +269,8 @@ public: service->update(); }); + updateTimer->setThreadPriority(Thread::Priority::Lower); + // We want to try and scan more often in case of startup or scan lock failure updateTimer->start(1000);