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 00000000..fc678c3d Binary files /dev/null and b/Data/system/service/MemoryChecker/assets/memory_alert.png differ diff --git a/Tactility/Include/Tactility/lvgl/Statusbar.h b/Tactility/Include/Tactility/lvgl/Statusbar.h index 63e88f49..644ef4c0 100644 --- a/Tactility/Include/Tactility/lvgl/Statusbar.h +++ b/Tactility/Include/Tactility/lvgl/Statusbar.h @@ -10,11 +10,22 @@ constexpr auto STATUSBAR_ICON_LIMIT = 8; constexpr auto STATUSBAR_ICON_SIZE = 20; constexpr auto STATUSBAR_HEIGHT = STATUSBAR_ICON_SIZE + 2; +/** Create a statusbar widget. Needs to be called with LVGL lock. */ lv_obj_t* statusbar_create(lv_obj_t* parent); -int8_t statusbar_icon_add(const std::string& image); + +/** Add an icon to the statusbar. Does not need to be called with LVGL lock. */ +int8_t statusbar_icon_add(const std::string& image, bool visible); + +/** Add an icon to the statusbar. Does not need to be called with LVGL lock. */ int8_t statusbar_icon_add(); + +/** Remove an icon from the statusbar. Does not need to be called with LVGL lock. */ void statusbar_icon_remove(int8_t id); + +/** Update an icon's image from the statusbar. Does not need to be called with LVGL lock. */ void statusbar_icon_set_image(int8_t id, const std::string& image); + +/** Update the visibility for an icon on the statusbar. Does not need to be called with LVGL lock. */ void statusbar_icon_set_visibility(int8_t id, bool visible); } // namespace diff --git a/Tactility/Private/Tactility/service/memorychecker/MemoryCheckerService.h b/Tactility/Private/Tactility/service/memorychecker/MemoryCheckerService.h new file mode 100644 index 00000000..b85ff40a --- /dev/null +++ b/Tactility/Private/Tactility/service/memorychecker/MemoryCheckerService.h @@ -0,0 +1,33 @@ +#pragma once + +#include "Tactility/service/Service.h" + +#include +#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);