diff --git a/firmware/lv_conf.h b/firmware/lv_conf.h index df62f9d..e4f20d1 100644 --- a/firmware/lv_conf.h +++ b/firmware/lv_conf.h @@ -13,10 +13,10 @@ /* Memory */ #define LV_MEM_CUSTOM 1 -#define LV_MEM_CUSTOM_INCLUDE -#define LV_MEM_CUSTOM_ALLOC malloc +#define LV_MEM_CUSTOM_INCLUDE "lv_psram_alloc.h" +#define LV_MEM_CUSTOM_ALLOC lv_psram_malloc #define LV_MEM_CUSTOM_FREE free -#define LV_MEM_CUSTOM_REALLOC realloc +#define LV_MEM_CUSTOM_REALLOC lv_psram_realloc /* Display — sized to the larger of supported boards (T-Watch Ultra 410x502). T-Deck Plus (320x240) uses less than the cap; the unused PSRAM is wasted diff --git a/firmware/lv_psram_alloc.h b/firmware/lv_psram_alloc.h new file mode 100644 index 0000000..e8bfb2e --- /dev/null +++ b/firmware/lv_psram_alloc.h @@ -0,0 +1,21 @@ +#pragma once +// LVGL memory allocator redirected to PSRAM on boards that have it. +// Falls back to DRAM if PSRAM alloc fails (fragmentation / OOM safety net). +// On targets without PSRAM (native_test) this is a transparent passthrough. +#ifdef BOARD_HAS_PSRAM +# include +static inline void* lv_psram_malloc(size_t size) { + void* p = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (!p) p = malloc(size); + return p; +} +static inline void* lv_psram_realloc(void* ptr, size_t size) { + void* p = heap_caps_realloc(ptr, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (!p && size > 0) p = realloc(ptr, size); + return p; +} +#else +# include +static inline void* lv_psram_malloc(size_t size) { return malloc(size); } +static inline void* lv_psram_realloc(void* ptr, size_t size) { return realloc(ptr, size); } +#endif diff --git a/firmware/src/config/ConfigManager.cpp b/firmware/src/config/ConfigManager.cpp index f9f15e4..9f9e42c 100644 --- a/firmware/src/config/ConfigManager.cpp +++ b/firmware/src/config/ConfigManager.cpp @@ -391,6 +391,7 @@ bool ConfigManager::parseJson(const String& json) { // Debug — missing block defaults to all-off _config.debug.screenshots = doc["debug"]["screenshots"] | defaults::SCREENSHOTS_ENABLED; + _config.debug.showMemory = doc["debug"]["show_memory"] | false; LOGF("[Config] Loaded: device=%s, contacts=%d, channels=%d\n", _config.deviceName.c_str(), @@ -560,6 +561,9 @@ String ConfigManager::toJson() const { if (_config.debug.screenshots) { doc["debug"]["screenshots"] = _config.debug.screenshots; } + if (_config.debug.showMemory) { + doc["debug"]["show_memory"] = _config.debug.showMemory; + } String output; serializeJsonPretty(doc, output); diff --git a/firmware/src/config/ConfigManager.h b/firmware/src/config/ConfigManager.h index deda623..da30dd0 100644 --- a/firmware/src/config/ConfigManager.h +++ b/firmware/src/config/ConfigManager.h @@ -126,6 +126,7 @@ struct BleConfig { }; struct DebugConfig { + bool showMemory = false; // Enable display of memory usage in status bar. bool screenshots = false; // Enable save-screen-to-SD (/screenshots/*.bmp). Debug aid, default off. }; diff --git a/firmware/src/i18n/I18n.cpp b/firmware/src/i18n/I18n.cpp index 5865c47..3cdd2af 100644 --- a/firmware/src/i18n/I18n.cpp +++ b/firmware/src/i18n/I18n.cpp @@ -134,6 +134,7 @@ const DefaultString DEFAULT_STRINGS[] = { {"lbl_lock_mode", "Lock Mode"}, {"lbl_auto_lock", "Auto-Lock"}, {"lbl_screenshots", "Save Screenshots"}, + {"lbl_show_memory", "Show Memory Usage"}, {"lbl_theme", "Theme"}, {"theme_dark", "Dark"}, {"theme_light", "Light"}, diff --git a/firmware/src/ui/ChatScreen.cpp b/firmware/src/ui/ChatScreen.cpp index ae65ff2..035263b 100644 --- a/firmware/src/ui/ChatScreen.cpp +++ b/firmware/src/ui/ChatScreen.cpp @@ -608,6 +608,7 @@ void ChatScreen::hide() { if (_cannedBtn) lv_group_remove_obj(_cannedBtn); } } + lv_obj_clean(_chatArea); lv_obj_add_flag(_screen, LV_OBJ_FLAG_HIDDEN); } diff --git a/firmware/src/ui/SettingsScreen.cpp b/firmware/src/ui/SettingsScreen.cpp index 32f94ac..ef42cd4 100644 --- a/firmware/src/ui/SettingsScreen.cpp +++ b/firmware/src/ui/SettingsScreen.cpp @@ -511,8 +511,9 @@ void SettingsScreen::buildDisplay() { 1, 255, cfg.display.kbdBrightness, String(cfg.display.kbdBrightness), true); addSwitchRowGated(t("lbl_emoji"), cfg.display.emoji, emojiToggleCb, nullptr, false); - // Screenshots fold into Display (no separate Debug screen / header). - addSwitchRowGated(t("lbl_screenshots"), cfg.debug.screenshots, screenshotsToggleCb, nullptr, false); + // Screenshots and debug overlays fold into Display (no separate Debug screen / header). + addSwitchRowGated(t("lbl_screenshots"), cfg.debug.screenshots, screenshotsToggleCb, nullptr, false); + addSwitchRowGated(t("lbl_show_memory"), cfg.debug.showMemory, showMemoryToggleCb, nullptr, false); } void SettingsScreen::buildMessaging() { @@ -2912,4 +2913,11 @@ void SettingsScreen::screenshotsToggleCb(lv_event_t* e) { g_dsDirty = true; } +void SettingsScreen::showMemoryToggleCb(lv_event_t* e) { + auto& mgr = ConfigManager::instance(); + lv_obj_t* sw = lv_event_get_target(e); + mgr.config().debug.showMemory = lv_obj_has_state(sw, LV_STATE_CHECKED); + g_dsDirty = true; +} + } // namespace mclite diff --git a/firmware/src/ui/SettingsScreen.h b/firmware/src/ui/SettingsScreen.h index 2bcecf3..d30637d 100644 --- a/firmware/src/ui/SettingsScreen.h +++ b/firmware/src/ui/SettingsScreen.h @@ -273,6 +273,7 @@ class SettingsScreen { static void themeRowCb(lv_event_t* e); // opens the theme picker (reboots to apply) static void emojiToggleCb(lv_event_t* e); static void screenshotsToggleCb(lv_event_t* e); + static void showMemoryToggleCb(lv_event_t* e); }; } // namespace mclite diff --git a/firmware/src/ui/StatusBar.cpp b/firmware/src/ui/StatusBar.cpp index 678efab..6497670 100644 --- a/firmware/src/ui/StatusBar.cpp +++ b/firmware/src/ui/StatusBar.cpp @@ -7,8 +7,10 @@ #include "../hal/Speaker.h" #include "../config/ConfigManager.h" #include "../util/TimeHelper.h" +#include "../util/log.h" #include "../net/WiFiManager.h" #include "../companion/CompanionService.h" +#include namespace mclite { @@ -120,6 +122,11 @@ void StatusBar::create(lv_obj_t* parent) { _lblTime = lv_label_create(_footer); lv_obj_set_style_text_font(_lblTime, FONT_STATUSBAR_ICON, 0); lv_obj_set_style_text_color(_lblTime, theme::TEXT_PRIMARY(), 0); + + _lblMem = lv_label_create(_footer); + lv_obj_set_style_text_font(_lblMem, FONT_SMALL, 0); + lv_obj_set_style_text_color(_lblMem, theme::TEXT_SECONDARY(), 0); + lv_label_set_text(_lblMem, ""); #else // T-Deck: single flex-row, device name left (grow), icons right. lv_obj_set_flex_flow(_bar, LV_FLEX_FLOW_ROW); @@ -147,6 +154,11 @@ void StatusBar::create(lv_obj_t* parent) { // --- Everything below is right-aligned (no flex_grow) --- + _lblMem = lv_label_create(_bar); + lv_obj_set_style_text_font(_lblMem, FONT_SMALL, 0); + lv_obj_set_style_text_color(_lblMem, theme::TEXT_SECONDARY(), 0); + lv_label_set_text(_lblMem, ""); + // Sound icon — clickable label, larger font for tap target _soundIcon = lv_label_create(_bar); lv_obj_set_style_text_font(_soundIcon, FONT_NORMAL, 0); @@ -307,6 +319,36 @@ void StatusBar::update() { lv_label_set_text(_lblTime, ""); } +#if defined(ESP32) + // Memory usage — PSRAM and internal DRAM, logged every update and shown in status bar + { + if (_lblMem) { + if (cfg.debug.showMemory) { + const size_t dram_free = heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + const size_t dram_tot = heap_caps_get_total_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + const size_t dram_min = heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + const size_t ps_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM); + const size_t ps_tot = heap_caps_get_total_size(MALLOC_CAP_SPIRAM); + const uint8_t ramPct = dram_tot > 0 ? (uint8_t)(100 - (dram_free * 100 / dram_tot)) : 0; + const uint8_t psrPct = ps_tot > 0 ? (uint8_t)(100 - (ps_free * 100 / ps_tot)) : 0; + char memBuf[16]; + if (ps_tot > 0) + snprintf(memBuf, sizeof(memBuf), "P%u%% R%u%%", (unsigned)psrPct, (unsigned)ramPct); + else + snprintf(memBuf, sizeof(memBuf), "R%u%%", (unsigned)ramPct); + lv_label_set_text(_lblMem, memBuf); + + LOGF("[Mem] DRAM %u/%u KB (min %u KB), PSRAM %u/%u KB\n", + (unsigned)(dram_free / 1024), (unsigned)(dram_tot / 1024), + (unsigned)(dram_min / 1024), + (unsigned)(ps_free / 1024), (unsigned)(ps_tot / 1024)); + } else { + lv_label_set_text(_lblMem, ""); + } + } + } +#endif + // GPS indicator — green=live, amber=last known, gray=no fix if (!cfg.gpsEnabled) { // GPS disabled in config: keep the icon present but dimmed so the diff --git a/firmware/src/ui/StatusBar.h b/firmware/src/ui/StatusBar.h index f9a014c..23f64e1 100644 --- a/firmware/src/ui/StatusBar.h +++ b/firmware/src/ui/StatusBar.h @@ -27,6 +27,7 @@ class StatusBar { lv_obj_t* _gpsIcon = nullptr; lv_obj_t* _wifiIcon = nullptr; // shown only while WiFi is connected lv_obj_t* _bleIcon = nullptr; // shown only while BLE companion is active + lv_obj_t* _lblMem = nullptr; // "P{X}% R{Y}%" PSRAM/RAM usage static void soundClickCb(lv_event_t* e); static void gpsClickCb(lv_event_t* e); diff --git a/sdcard/mclite/lang/de.json b/sdcard/mclite/lang/de.json index 8b23578..9b458a7 100644 --- a/sdcard/mclite/lang/de.json +++ b/sdcard/mclite/lang/de.json @@ -106,6 +106,7 @@ "lbl_kbd_backlight": "Tastenhelligkeit", "lbl_emoji": "Emoji-Auswahl", "lbl_screenshots": "Screenshots speichern", + "lbl_show_memory": "Speichernutzung zeigen", "lbl_boot_text": "Boot-Text", "lbl_history": "Verlauf speichern", "lbl_max_per_chat": "Max pro Chat", diff --git a/sdcard/mclite/lang/fr.json b/sdcard/mclite/lang/fr.json index 1a74e5e..2c78832 100644 --- a/sdcard/mclite/lang/fr.json +++ b/sdcard/mclite/lang/fr.json @@ -106,6 +106,7 @@ "lbl_kbd_backlight": "Lumi clavier", "lbl_emoji": "Emoji", "lbl_screenshots": "Sauver captures", + "lbl_show_memory": "Afficher memoire", "lbl_boot_text": "Texte demarrage", "lbl_history": "Sauver historique", "lbl_max_per_chat": "Max par chat", diff --git a/sdcard/mclite/lang/it.json b/sdcard/mclite/lang/it.json index 376bb10..a25ef65 100644 --- a/sdcard/mclite/lang/it.json +++ b/sdcard/mclite/lang/it.json @@ -106,6 +106,7 @@ "lbl_kbd_backlight": "Lumin tastiera", "lbl_emoji": "Emoji", "lbl_screenshots": "Salva screenshot", + "lbl_show_memory": "Mostra memoria", "lbl_boot_text": "Testo avvio", "lbl_history": "Salva cronologia", "lbl_max_per_chat": "Max per chat",