From 3a0f3520d17d9c3c32a21b2b0fe44af3109b6c95 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:40:44 -0500 Subject: [PATCH 1/2] BaseUI: Autosave Messages (#9269) * Autosave Messages * fix * Add logging, code cleanup, and add save on delete. * We already save as part of delete messages, no need to do it again * fix spelling errors * Updating comment --------- Co-authored-by: Jason P --- src/MessageStore.cpp | 90 ++++++++++++++++++++++++++++++- src/MessageStore.h | 5 ++ src/graphics/draw/MenuHandler.cpp | 32 ++--------- src/main.cpp | 6 +++ 4 files changed, 103 insertions(+), 30 deletions(-) diff --git a/src/MessageStore.cpp b/src/MessageStore.cpp index c96645b1c..22da418f5 100644 --- a/src/MessageStore.cpp +++ b/src/MessageStore.cpp @@ -13,6 +13,11 @@ #define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE) #endif +// Default autosave interval 2 hours, override per device later with -DMESSAGE_AUTOSAVE_INTERVAL_SEC=300 (etc) +#ifndef MESSAGE_AUTOSAVE_INTERVAL_SEC +#define MESSAGE_AUTOSAVE_INTERVAL_SEC (2 * 60 * 60) +#endif + // Global message text pool and state static char *g_messagePool = nullptr; static size_t g_poolWritePos = 0; @@ -102,6 +107,60 @@ void MessageStore::addLiveMessage(const StoredMessage &msg) pushWithLimit(liveMessages, msg); } +#if ENABLE_MESSAGE_PERSISTENCE +static bool g_messageStoreHasUnsavedChanges = false; +static uint32_t g_lastAutoSaveMs = 0; // last time we actually saved + +static inline uint32_t autosaveIntervalMs() +{ + uint32_t sec = (uint32_t)MESSAGE_AUTOSAVE_INTERVAL_SEC; + if (sec < 60) + sec = 60; + return sec * 1000UL; +} + +static inline bool reachedMs(uint32_t now, uint32_t target) +{ + return (int32_t)(now - target) >= 0; +} + +// Mark new messages in RAM that need to be saved later +static inline void markMessageStoreUnsaved() +{ + g_messageStoreHasUnsavedChanges = true; + + if (g_lastAutoSaveMs == 0) { + g_lastAutoSaveMs = millis(); + } +} + +// Called periodically from the main loop in main.cpp +static inline void autosaveTick(MessageStore *store) +{ + if (!store) + return; + + uint32_t now = millis(); + + if (g_lastAutoSaveMs == 0) { + g_lastAutoSaveMs = now; + return; + } + + if (!reachedMs(now, g_lastAutoSaveMs + autosaveIntervalMs())) + return; + + // Autosave interval reached, only save if there are unsaved messages. + if (g_messageStoreHasUnsavedChanges) { + LOG_INFO("Autosaving MessageStore to flash"); + store->saveToFlash(); + } else { + LOG_INFO("Autosave skipped, no changes to save"); + g_lastAutoSaveMs = now; + } +} +#endif + // Add from incoming/outgoing packet const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet) { @@ -131,6 +190,11 @@ const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &pa } addLiveMessage(sm); + +#if ENABLE_MESSAGE_PERSISTENCE + markMessageStoreUnsaved(); +#endif + return liveMessages.back(); } @@ -155,6 +219,10 @@ void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const st sm.ackStatus = AckStatus::NONE; addLiveMessage(sm); + +#if ENABLE_MESSAGE_PERSISTENCE + markMessageStoreUnsaved(); +#endif } #if ENABLE_MESSAGE_PERSISTENCE @@ -239,6 +307,10 @@ void MessageStore::saveToFlash() f.close(); #endif + + // Reset autosave state after any save + g_messageStoreHasUnsavedChanges = false; + g_lastAutoSaveMs = millis(); } void MessageStore::loadFromFlash() @@ -270,6 +342,9 @@ void MessageStore::loadFromFlash() f.close(); #endif + // Loading messages does not trigger an autosave + g_messageStoreHasUnsavedChanges = false; + g_lastAutoSaveMs = millis(); } #else @@ -290,6 +365,11 @@ void MessageStore::clearAllMessages() f.write(&count, 1); // write "0 messages" f.close(); #endif + +#if ENABLE_MESSAGE_PERSISTENCE + g_messageStoreHasUnsavedChanges = false; + g_lastAutoSaveMs = millis(); +#endif } // Internal helper: erase first or last message matching a predicate @@ -421,6 +501,14 @@ uint16_t MessageStore::storeText(const char *src, size_t len) return storeTextInPool(src, len); } +#if ENABLE_MESSAGE_PERSISTENCE +void messageStoreAutosaveTick() +{ + // Called from the main loop to check autosave timing + autosaveTick(&messageStore); +} +#endif + // Global definition MessageStore messageStore("default"); -#endif \ No newline at end of file +#endif diff --git a/src/MessageStore.h b/src/MessageStore.h index 41eb56b66..6203d8ed0 100644 --- a/src/MessageStore.h +++ b/src/MessageStore.h @@ -125,6 +125,11 @@ class MessageStore std::string filename; // Flash filename for persistence }; +#if ENABLE_MESSAGE_PERSISTENCE +// Called periodically from main loop to trigger time based autosave +void messageStoreAutosaveTick(); +#endif + // Global instance (defined in MessageStore.cpp) extern MessageStore messageStore; diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 7c17c8b92..d374ac0e3 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -449,7 +449,7 @@ void menuHandler::clockMenu() } void menuHandler::messageResponseMenu() { - enum optionsNumbers { Back = 0, ViewMode, DeleteAll, DeleteOldest, ReplyMenu, MuteChannel, Aloud, enumEnd }; + enum optionsNumbers { Back = 0, ViewMode, DeleteMenu, ReplyMenu, MuteChannel, Aloud, enumEnd }; static const char *optionsArray[enumEnd]; static int optionsEnumArray[enumEnd]; @@ -479,7 +479,7 @@ void menuHandler::messageResponseMenu() // Delete submenu optionsArray[options] = "Delete"; - optionsEnumArray[options++] = 900; + optionsEnumArray[options++] = DeleteMenu; #ifdef HAS_I2S optionsArray[options] = "Read Aloud"; @@ -520,34 +520,10 @@ void menuHandler::messageResponseMenu() nodeDB->saveToDisk(); } - // Delete submenu - } else if (selected == 900) { + } else if (selected == DeleteMenu) { menuHandler::menuQueue = menuHandler::delete_messages_menu; screen->runNow(); - // Delete oldest FIRST (only change) - } else if (selected == DeleteOldest) { - auto mode = graphics::MessageRenderer::getThreadMode(); - int ch = graphics::MessageRenderer::getThreadChannel(); - uint32_t peer = graphics::MessageRenderer::getThreadPeer(); - - if (mode == graphics::MessageRenderer::ThreadMode::ALL) { - // Global oldest - messageStore.deleteOldestMessage(); - } else if (mode == graphics::MessageRenderer::ThreadMode::CHANNEL) { - // Oldest in current channel - messageStore.deleteOldestMessageInChannel(ch); - } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { - // Oldest in current DM - messageStore.deleteOldestMessageWithPeer(peer); - } - - // Delete all messages - } else if (selected == DeleteAll) { - messageStore.clearAllMessages(); - graphics::MessageRenderer::clearThreadRegistries(); - graphics::MessageRenderer::clearMessageCache(); - #ifdef HAS_I2S } else if (selected == Aloud) { const meshtastic_MeshPacket &mp = devicestate.rx_text_message; @@ -716,7 +692,6 @@ void menuHandler::deleteMessagesMenu() } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { messageStore.deleteOldestMessageWithPeer(peer); } - return; } @@ -729,7 +704,6 @@ void menuHandler::deleteMessagesMenu() } else if (mode == graphics::MessageRenderer::ThreadMode::DIRECT) { messageStore.deleteAllMessagesWithPeer(peer); } - return; } }; diff --git a/src/main.cpp b/src/main.cpp index a9ed73bd7..d77767736 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -38,6 +38,9 @@ #include "target_specific.h" #include #include +#if HAS_SCREEN +#include "MessageStore.h" +#endif #ifdef ELECROW_ThinkNode_M5 PCA9557 io(0x18, &Wire); @@ -1652,6 +1655,9 @@ void loop() if (dispdev) static_cast(dispdev)->sdlLoop(); } +#endif +#if HAS_SCREEN && ENABLE_MESSAGE_PERSISTENCE + messageStoreAutosaveTick(); #endif long delayMsec = mainController.runOrDelay(); From ded4f57cb71ed08392a0ee58d2351fe6086a2ca1 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 13 Jan 2026 05:47:08 -0600 Subject: [PATCH 2/2] Partition name in manifest script (#9294) * Fix up T-Beam 1W HW_MODEL * Add part_name for bin files * app0 --- .github/workflows/build_firmware.yml | 4 ++-- bin/platformio-custom.py | 11 +++++++++++ src/platform/esp32/architecture.h | 2 ++ variants/esp32s3/t-beam-1w/platformio.ini | 9 +++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_firmware.yml b/.github/workflows/build_firmware.yml index 77260cafe..23690766a 100644 --- a/.github/workflows/build_firmware.yml +++ b/.github/workflows/build_firmware.yml @@ -91,8 +91,8 @@ jobs: if [[ -f "$manifest" ]]; then echo "Updating $manifest with $OTA_FILE (md5: $OTA_MD5, size: $OTA_SIZE)" # Add OTA entry to files array if not already present - jq --arg name "$OTA_FILE" --arg md5 "$OTA_MD5" --argjson bytes "$OTA_SIZE" \ - 'if .files | map(select(.name == $name)) | length == 0 then .files += [{"name": $name, "md5": $md5, "bytes": $bytes}] else . end' \ + jq --arg name "$OTA_FILE" --arg md5 "$OTA_MD5" --argjson bytes "$OTA_SIZE" --arg part "app1" \ + 'if .files | map(select(.name == $name)) | length == 0 then .files += [{"name": $name, "md5": $md5, "bytes": $bytes, "part_name": $part}] else . end' \ "$manifest" > "${manifest}.tmp" && mv "${manifest}.tmp" "$manifest" fi done diff --git a/bin/platformio-custom.py b/bin/platformio-custom.py index 7481500db..b75c66624 100644 --- a/bin/platformio-custom.py +++ b/bin/platformio-custom.py @@ -60,6 +60,14 @@ def manifest_gather(source, target, env): board_platform = env.BoardConfig().get("platform") board_mcu = env.BoardConfig().get("build.mcu").lower() needs_ota_suffix = board_platform == "nordicnrf52" + + # Mapping of bin files to their target partition names + # Maps the filename pattern to the partition name where it should be flashed + partition_map = { + f"{progname}.bin": "app0", # primary application slot (app0 / OTA_0) + lfsbin: "spiffs", # filesystem image flashed to spiffs + } + check_paths = [ progname, f"{progname}.elf", @@ -85,6 +93,9 @@ def manifest_gather(source, target, env): "md5": f.get_content_hash(), # Returns MD5 hash "bytes": f.get_size() # Returns file size in bytes } + # Add part_name if this file represents a partition that should be flashed + if p in partition_map: + d["part_name"] = partition_map[p] out.append(d) print(d) manifest_write(out, env) diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 085692f96..f34f1fc65 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -195,6 +195,8 @@ #define HW_VENDOR meshtastic_HardwareModel_LINK_32 #elif defined(T_DECK_PRO) #define HW_VENDOR meshtastic_HardwareModel_T_DECK_PRO +#elif defined(T_BEAM_1W) +#define HW_VENDOR meshtastic_HardwareModel_TBEAM_1_WATT #elif defined(T_LORA_PAGER) #define HW_VENDOR meshtastic_HardwareModel_T_LORA_PAGER #elif defined(HELTEC_V4) diff --git a/variants/esp32s3/t-beam-1w/platformio.ini b/variants/esp32s3/t-beam-1w/platformio.ini index 54ddb6c3e..9abf895db 100644 --- a/variants/esp32s3/t-beam-1w/platformio.ini +++ b/variants/esp32s3/t-beam-1w/platformio.ini @@ -1,5 +1,14 @@ ; LilyGo T-Beam-1W (1 Watt LoRa with external PA) [env:t-beam-1w] +custom_meshtastic_hw_model = 122 +custom_meshtastic_hw_model_slug = TBEAM_1_WATT +custom_meshtastic_architecture = esp32s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = LILYGO T-Beam 1W +custom_meshtastic_images = tbeam-1w.svg +custom_meshtastic_tags = LilyGo + extends = esp32s3_base board = t-beam-1w board_build.partitions = default_8MB.csv