From cd9489969f1f06181ebfd313306d736b3ddf0936 Mon Sep 17 00:00:00 2001 From: HarukiToreda Date: Sun, 31 May 2026 15:47:15 -0400 Subject: [PATCH] Unified Messagestore. --- src/MessageStore.cpp | 2 +- src/MessageStore.h | 2 +- .../Notification/NotificationApplet.cpp | 5 +- .../User/AllMessage/AllMessageApplet.cpp | 6 +- .../niche/InkHUD/Applets/User/DM/DMApplet.cpp | 4 +- .../ThreadedMessage/ThreadedMessageApplet.cpp | 76 +++------ .../ThreadedMessage/ThreadedMessageApplet.h | 3 +- src/graphics/niche/InkHUD/Events.cpp | 16 +- src/graphics/niche/InkHUD/InkHUD.cpp | 88 ++++++++++ src/graphics/niche/InkHUD/MessageStore.cpp | 156 ------------------ src/graphics/niche/InkHUD/MessageStore.h | 47 ------ src/graphics/niche/InkHUD/Persistence.cpp | 40 +++-- src/graphics/niche/InkHUD/Persistence.h | 12 +- src/graphics/niche/InkHUD/docs/README.md | 20 +-- src/main.cpp | 2 +- 15 files changed, 170 insertions(+), 309 deletions(-) delete mode 100644 src/graphics/niche/InkHUD/MessageStore.cpp delete mode 100644 src/graphics/niche/InkHUD/MessageStore.h diff --git a/src/MessageStore.cpp b/src/MessageStore.cpp index 0bd82c40b..a7e6c5662 100644 --- a/src/MessageStore.cpp +++ b/src/MessageStore.cpp @@ -1,5 +1,5 @@ #include "configuration.h" -#if HAS_SCREEN +#if HAS_SCREEN || defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS) #include "FSCommon.h" #include "MessageStore.h" #include "NodeDB.h" diff --git a/src/MessageStore.h b/src/MessageStore.h index 8f25c84f7..e88e59098 100644 --- a/src/MessageStore.h +++ b/src/MessageStore.h @@ -1,6 +1,6 @@ #pragma once -#if HAS_SCREEN +#if HAS_SCREEN || defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS) // Disable debug logging entirely on release builds of HELTEC_MESH_SOLAR for space constraints #if defined(HELTEC_MESH_SOLAR) diff --git a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp index 44d0a8089..195de8ecd 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp @@ -3,6 +3,7 @@ #include "./NotificationApplet.h" #include "./Notification.h" +#include "MessageStore.h" #include "graphics/niche/InkHUD/Persistence.h" #include "meshUtils.h" @@ -231,7 +232,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila bool msgIsBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST; // Pick source of message - const MessageStore::Message *message = + const StoredMessage *message = msgIsBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm; // Find info about the sender @@ -261,7 +262,7 @@ std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvaila text += hexifyNodeNum(message->sender); text += ": "; - text += message->text; + text += MessageStore::getText(*message); } } diff --git a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp index bbbe826b1..ec266771e 100644 --- a/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.cpp @@ -2,6 +2,8 @@ #include "./AllMessageApplet.h" +#include "MessageStore.h" + using namespace NicheGraphics; void InkHUD::AllMessageApplet::onActivate() @@ -37,7 +39,7 @@ int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket * void InkHUD::AllMessageApplet::onRender(bool full) { // Find newest message, regardless of whether DM or broadcast - MessageStore::Message *message; + StoredMessage *message; if (latestMessage->wasBroadcast) message = &latestMessage->broadcast; else @@ -96,7 +98,7 @@ void InkHUD::AllMessageApplet::onRender(bool full) // =================== // Parse any non-ascii chars in the message - std::string text = parse(message->text); + std::string text = parse(std::string(MessageStore::getText(*message))); // Extra gap below the header int16_t textTop = headerDivY + padDivH; diff --git a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp index c15152d3a..4940e69bf 100644 --- a/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/DM/DMApplet.cpp @@ -2,6 +2,8 @@ #include "./DMApplet.h" +#include "MessageStore.h" + using namespace NicheGraphics; void InkHUD::DMApplet::onActivate() @@ -92,7 +94,7 @@ void InkHUD::DMApplet::onRender(bool full) // =================== // Parse any non-ascii chars in the message - std::string text = parse(latestMessage->dm.text); + std::string text = parse(std::string(MessageStore::getText(latestMessage->dm))); // Extra gap below the header int16_t textTop = headerDivY + padDivH; diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp index 01bdc2224..c68a51bc2 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp @@ -7,19 +7,9 @@ using namespace NicheGraphics; -// Hard limits on how much message data to write to flash -// Avoid filling the storage if something goes wrong -// Normal usage should be well below this size -constexpr uint8_t MAX_MESSAGES_SAVED = 10; -constexpr uint32_t MAX_MESSAGE_SIZE = 250; - InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) : SinglePortModule("ThreadedMessageApplet", meshtastic_PortNum_TEXT_MESSAGE_APP), channelIndex(channelIndex) { - // Create the message store - // Will shortly attempt to load messages from RAM, if applet is active - // Label (filename in flash) is set from channel index - store = new MessageStore("ch" + to_string(channelIndex)); } void InkHUD::ThreadedMessageApplet::onRender(bool full) @@ -61,17 +51,24 @@ void InkHUD::ThreadedMessageApplet::onRender(bool full) const uint16_t msgW = (msgR - msgL) + 1; int16_t msgB = height() - 1; // Vertical cursor for drawing. Messages are bottom-aligned to this value. - uint8_t i = 0; // Index of stored message - // Loop over messages - // - until no messages left, or - // - until no part of message fits on screen - while (msgB >= (0 - fontSmall.lineHeight()) && i < store->messages.size()) { + // Iterate the global store newest-first, showing only broadcast messages on our channel + const auto &allMessages = messageStore.getLiveMessages(); + int msgIdx = (int)allMessages.size() - 1; + + while (msgB >= (0 - fontSmall.lineHeight()) && msgIdx >= 0) { + + const StoredMessage &m = allMessages.at(msgIdx); + + // Skip messages that don't belong to this channel or are DMs + if (m.type != MessageType::BROADCAST || m.channelIndex != channelIndex) { + msgIdx--; + continue; + } // Grab data for message - const MessageStore::Message &m = store->messages.at(i); - bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message - std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message + bool outgoing = (m.sender == myNodeInfo.my_node_num); + std::string bodyText = parse(std::string(MessageStore::getText(m))); // Parse any non-ascii chars // Cache bottom Y of message text // - Used when drawing vertical line alongside @@ -152,18 +149,13 @@ void InkHUD::ThreadedMessageApplet::onRender(bool full) // Move cursor up: padding before next message msgB -= fontSmall.lineHeight() * 0.5; - i++; + msgIdx--; } // End of loop: drawing each message // Fade effect: // Area immediately below the divider. Overdraw with sparse white lines. // Make text appear to pass behind the header hatchRegion(0, dividerY + 1, width(), fontSmall.lineHeight() / 3, 2, WHITE); - - // If we've run out of screen to draw messages, we can drop any leftover data from the queue - // Those messages have been pushed off the screen-top by newer ones - while (i < store->messages.size()) - store->messages.pop_back(); } // Code which runs when the applet begins running @@ -198,16 +190,8 @@ ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_Me if (mp.to != NODENUM_BROADCAST) return ProcessMessage::CONTINUE; - // Extract info into our slimmed-down "StoredMessage" type - MessageStore::Message newMessage; - newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time - newMessage.sender = mp.from; - newMessage.channelIndex = mp.channel; - newMessage.text = std::string((const char *)mp.decoded.payload.bytes, mp.decoded.payload.size); - - // Store newest message at front - // These records are used when rendering, and also stored in flash at shutdown - store->messages.push_front(newMessage); + // Store in the global messageStore — this handles sender, timestamp, channel, text, and ack status + messageStore.addFromPacket(mp); // If this was an incoming message, suggest that our applet becomes foreground, if permitted if (getFrom(&mp) != nodeDB->getNodeNum()) @@ -232,32 +216,22 @@ bool InkHUD::ThreadedMessageApplet::approveNotification(Notification &n) return true; } -// Save several recent messages to flash -// Stores the contents of ThreadedMessageApplet::messages -// Just enough messages to fill the display -// Messages are packed "back-to-back", to minimize blocks of flash used +// Save messages to flash via the global messageStore. +// The global store holds messages for all channels; no per-channel file is needed. void InkHUD::ThreadedMessageApplet::saveMessagesToFlash() { - // Create a label (will become the filename in flash) - std::string label = "ch" + to_string(channelIndex); - - store->saveToFlash(); + messageStore.saveToFlash(); } -// Load recent messages to flash -// Fills ThreadedMessageApplet::messages with previous messages -// Just enough messages have been stored to cover the display +// Messages are loaded once by InkHUD::begin() before applets start. +// Nothing to do here at per-applet activation time. void InkHUD::ThreadedMessageApplet::loadMessagesFromFlash() { - // Create a label (will become the filename in flash) - std::string label = "ch" + to_string(channelIndex); - - store->loadFromFlash(); + // No-op: messageStore.loadFromFlash() is called in InkHUD::begin() } // Code to run when device is shutting down // This is in addition to any onDeactivate() code, which will also run -// Todo: implement before a reboot also void InkHUD::ThreadedMessageApplet::onShutdown() { // Save our current set of messages to flash, provided the applet isn't disabled @@ -265,4 +239,4 @@ void InkHUD::ThreadedMessageApplet::onShutdown() saveMessagesToFlash(); } -#endif \ No newline at end of file +#endif diff --git a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h index 2cd2c4163..8ec7d48a5 100644 --- a/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h +++ b/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h @@ -20,8 +20,8 @@ Suggest a max of two channel, to minimize fs usage? #include "configuration.h" +#include "MessageStore.h" #include "graphics/niche/InkHUD/Applet.h" -#include "graphics/niche/InkHUD/MessageStore.h" #include "modules/TextMessageModule.h" @@ -49,7 +49,6 @@ class ThreadedMessageApplet : public Applet, public SinglePortModule void saveMessagesToFlash(); void loadMessagesFromFlash(); - MessageStore *store; // Messages, held in RAM for use, ready to save to flash on shutdown uint8_t channelIndex = 0; }; diff --git a/src/graphics/niche/InkHUD/Events.cpp b/src/graphics/niche/InkHUD/Events.cpp index d40cc2ca7..8a673b8c7 100644 --- a/src/graphics/niche/InkHUD/Events.cpp +++ b/src/graphics/niche/InkHUD/Events.cpp @@ -2,6 +2,7 @@ #include "./Events.h" +#include "MessageStore.h" #include "PowerFSM.h" #include "RTC.h" #include "buzz.h" @@ -514,6 +515,7 @@ int InkHUD::Events::beforeReboot(void *unused) inkhud->persistence->saveLatestMessage(); } else { NicheGraphics::clearFlashData(); + messageStore.clearAllMessages(); // also wipe the shared message store } // Note: no forceUpdate call here @@ -538,9 +540,8 @@ int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet) inkhud->persistence->latestMessage.wasBroadcast = isBroadcast(packet->to); // Pick the appropriate variable to store the message in - MessageStore::Message *storedMessage = inkhud->persistence->latestMessage.wasBroadcast - ? &inkhud->persistence->latestMessage.broadcast - : &inkhud->persistence->latestMessage.dm; + StoredMessage *storedMessage = inkhud->persistence->latestMessage.wasBroadcast ? &inkhud->persistence->latestMessage.broadcast + : &inkhud->persistence->latestMessage.dm; // Store nodenum of the sender // Applets can use this to fetch user data from nodedb, if they want @@ -554,10 +555,11 @@ int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet) // - (potentially) used to determine which applet to focus storedMessage->channelIndex = packet->channel; - // Store the text - // Need to specify manually how many bytes, because source not null-terminated - storedMessage->text = - std::string(&packet->decoded.payload.bytes[0], &packet->decoded.payload.bytes[packet->decoded.payload.size]); + // Store the text via the shared pool so getText() works on this StoredMessage + const char *payload = reinterpret_cast(packet->decoded.payload.bytes); + uint16_t payloadLen = packet->decoded.payload.size; + storedMessage->textOffset = MessageStore::storeText(payload, payloadLen); + storedMessage->textLength = payloadLen; return 0; // Tell caller to continue notifying other observers. (No reason to abort this event) } diff --git a/src/graphics/niche/InkHUD/InkHUD.cpp b/src/graphics/niche/InkHUD/InkHUD.cpp index 9ed88af5f..3c323802e 100644 --- a/src/graphics/niche/InkHUD/InkHUD.cpp +++ b/src/graphics/niche/InkHUD/InkHUD.cpp @@ -2,6 +2,10 @@ #include "./InkHUD.h" +#include "FSCommon.h" +#include "MessageStore.h" +#include "SPILock.h" +#include "concurrency/LockGuard.h" #include "./Applet.h" #include "./Events.h" #include "./Persistence.h" @@ -60,11 +64,95 @@ void InkHUD::InkHUD::notifyApplyingChanges() } } +// One-time migration from the old per-channel InkHUD message files (/NicheGraphics/ch*.msgs) +// to the firmware-wide MessageStore format (/Messages_default.msgs). +// Only runs when the new store loaded empty, meaning this is the first boot after the format change. +// Old files are deleted once migrated. +static void migrateOldInkHUDMessages() +{ +#ifdef FSCom + bool migrated = false; + constexpr uint8_t MAX_CHANNELS = 8; + constexpr uint32_t OLD_MAX_MSG_SIZE = 250; + + for (uint8_t ch = 0; ch < MAX_CHANNELS; ch++) { + std::string path = "/NicheGraphics/ch"; + path += std::to_string(ch); + path += ".msgs"; + + spiLock->lock(); + bool exists = FSCom.exists(path.c_str()); + spiLock->unlock(); + if (!exists) + continue; + + concurrency::LockGuard guard(spiLock); + + auto f = FSCom.open(path.c_str(), FILE_O_READ); + if (!f || f.size() == 0) { + if (f) + f.close(); + FSCom.remove(path.c_str()); + continue; + } + + uint8_t count = 0; + f.readBytes(reinterpret_cast(&count), 1); + + for (uint8_t i = 0; i < count; i++) { + StoredMessage sm; + f.readBytes(reinterpret_cast(&sm.timestamp), sizeof(sm.timestamp)); + f.readBytes(reinterpret_cast(&sm.sender), sizeof(sm.sender)); + f.readBytes(reinterpret_cast(&sm.channelIndex), sizeof(sm.channelIndex)); + + char textBuf[OLD_MAX_MSG_SIZE + 1] = {}; + uint32_t textLen = 0; + char c; + while (textLen < OLD_MAX_MSG_SIZE) { + if (f.readBytes(&c, 1) != 1) + break; + if (c == '\0') + break; + textBuf[textLen++] = c; + } + + sm.dest = NODENUM_BROADCAST; + sm.type = MessageType::BROADCAST; + sm.isBootRelative = false; + sm.ackStatus = AckStatus::ACKED; + sm.textOffset = MessageStore::storeText(textBuf, textLen); + sm.textLength = static_cast(textLen); + + messageStore.addLiveMessage(sm); + migrated = true; + } + + f.close(); + FSCom.remove(path.c_str()); + LOG_INFO("Migrated %u messages from %s", static_cast(count), path.c_str()); + } + + // Delete the old latest.msgs; the latestMessage cache will be re-derived from migrated channel messages + spiLock->lock(); + if (FSCom.exists("/NicheGraphics/latest.msgs")) + FSCom.remove("/NicheGraphics/latest.msgs"); + spiLock->unlock(); + + if (migrated) { + LOG_INFO("InkHUD message migration complete, saving to new format"); + messageStore.saveToFlash(); + } +#endif +} + // Start InkHUD! // Call this only after you have configured InkHUD void InkHUD::InkHUD::begin() { persistence->loadSettings(); + messageStore.loadFromFlash(); // Load persisted messages before deriving latestMessage cache + if (messageStore.getLiveMessages().empty()) + migrateOldInkHUDMessages(); // First boot after format change: import old per-channel files persistence->loadLatestMessage(); windowManager->begin(); diff --git a/src/graphics/niche/InkHUD/MessageStore.cpp b/src/graphics/niche/InkHUD/MessageStore.cpp deleted file mode 100644 index 671f38057..000000000 --- a/src/graphics/niche/InkHUD/MessageStore.cpp +++ /dev/null @@ -1,156 +0,0 @@ -#ifdef MESHTASTIC_INCLUDE_INKHUD - -#include "./MessageStore.h" - -#include "SafeFile.h" - -using namespace NicheGraphics; - -// Hard limits on how much message data to write to flash -// Avoid filling the storage if something goes wrong -// Normal usage should be well below this size -constexpr uint8_t MAX_MESSAGES_SAVED = 10; -constexpr uint32_t MAX_MESSAGE_SIZE = 250; - -InkHUD::MessageStore::MessageStore(const std::string &label) -{ - filename = ""; - filename += "/NicheGraphics"; - filename += "/"; - filename += label; - filename += ".msgs"; -} - -// Write the contents of the MessageStore::messages object to flash -// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD card. -// Need to lock and unlock around specific FS methods, as the SafeFile class takes the lock for itself internally -void InkHUD::MessageStore::saveToFlash() -{ - assert(!filename.empty()); - -#ifdef FSCom - // Make the directory, if doesn't already exist - // This is the same directory accessed by NicheGraphics::FlashData - spiLock->lock(); - FSCom.mkdir("/NicheGraphics"); - spiLock->unlock(); - - // Open or create the file - // No "full atomic": don't save then rename - auto f = SafeFile(filename.c_str(), false); - - LOG_INFO("Saving messages in %s", filename.c_str()); - - // Take firmware's SPI Lock while writing - spiLock->lock(); - - // 1st byte: how many messages will be written to store - f.write(messages.size()); - - // For each message - for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) { - Message &m = messages.at(i); - f.write(reinterpret_cast(&m.timestamp), sizeof(m.timestamp)); // Write timestamp. 4 bytes - f.write(reinterpret_cast(&m.sender), sizeof(m.sender)); // Write sender NodeId. 4 Bytes - f.write(reinterpret_cast(&m.channelIndex), sizeof(m.channelIndex)); // Write channel index. 1 Byte - f.write(reinterpret_cast(m.text.c_str()), - min((size_t)MAX_MESSAGE_SIZE, m.text.size())); // Write message text - f.write('\0'); // Append null term - LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", static_cast(i), - min((size_t)MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str()); - } - - // Release firmware's SPI lock, because SafeFile::close needs it - spiLock->unlock(); - - bool writeSucceeded = f.close(); - - if (!writeSucceeded) { - LOG_ERROR("Can't write data!"); - } -#else - LOG_ERROR("ERROR: Filesystem not implemented\n"); -#endif -} - -// Attempt to load the previous contents of the MessageStore:message deque from flash. -// Filename is controlled by the "label" parameter -// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD card. -void InkHUD::MessageStore::loadFromFlash() -{ - // Hopefully redundant. Initial intention is to only load / save once per boot. - messages.clear(); - -#ifdef FSCom - - // Take the firmware's SPI Lock, in case filesystem is on SD card - concurrency::LockGuard guard(spiLock); - - // Check that the file *does* actually exist - if (!FSCom.exists(filename.c_str())) { - LOG_WARN("'%s' not found. Using default values", filename.c_str()); - return; - } - - // Check that the file *does* actually exist - if (!FSCom.exists(filename.c_str())) { - LOG_INFO("'%s' not found.", filename.c_str()); - return; - } - - // Open the file - auto f = FSCom.open(filename.c_str(), FILE_O_READ); - - if (f.size() == 0) { - LOG_INFO("%s is empty", filename.c_str()); - f.close(); - return; - } - - // If opened, start reading - if (f) { - LOG_INFO("Loading threaded messages '%s'", filename.c_str()); - - // First byte: how many messages are in the flash store - uint8_t flashMessageCount = 0; - f.readBytes(reinterpret_cast(&flashMessageCount), 1); - LOG_DEBUG("Messages available: %u", static_cast(flashMessageCount)); - - // For each message - for (uint8_t i = 0; i < flashMessageCount && i < MAX_MESSAGES_SAVED; i++) { - Message m; - - // Read meta data (fixed width) - f.readBytes(reinterpret_cast(&m.timestamp), sizeof(m.timestamp)); - f.readBytes(reinterpret_cast(&m.sender), sizeof(m.sender)); - f.readBytes(reinterpret_cast(&m.channelIndex), sizeof(m.channelIndex)); - - // Read characters until we find a null term - char c; - while (m.text.size() < MAX_MESSAGE_SIZE) { - f.readBytes(&c, 1); - if (c != '\0') - m.text += c; - else - break; - } - - // Store in RAM - messages.push_back(m); - - LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", static_cast(i), m.timestamp, m.sender, - m.text.c_str()); - } - - f.close(); - } else { - LOG_ERROR("Could not open / read %s", filename.c_str()); - } -#else - LOG_ERROR("Filesystem not implemented"); - state = LoadFileState::NO_FILESYSTEM; -#endif - return; -} - -#endif \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/MessageStore.h b/src/graphics/niche/InkHUD/MessageStore.h deleted file mode 100644 index 55fb9b8cc..000000000 --- a/src/graphics/niche/InkHUD/MessageStore.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifdef MESHTASTIC_INCLUDE_INKHUD - -/* - -We hold a few recent messages, for features like the threaded message applet. -This class contains a struct for storing those messages, -and methods for serializing them to flash. - -*/ - -#pragma once - -#include "configuration.h" - -#include - -#include "mesh/MeshTypes.h" - -namespace NicheGraphics::InkHUD -{ - -class MessageStore -{ - public: - // A stored message - struct Message { - uint32_t timestamp; // Epoch seconds - NodeNum sender = 0; - uint8_t channelIndex; - std::string text; - }; - - MessageStore() = delete; - explicit MessageStore(const std::string &label); // Label determines filename in flash - - void saveToFlash(); - void loadFromFlash(); - - std::deque messages; // Interact with this object! - - private: - std::string filename; -}; - -} // namespace NicheGraphics::InkHUD - -#endif diff --git a/src/graphics/niche/InkHUD/Persistence.cpp b/src/graphics/niche/InkHUD/Persistence.cpp index 20909f2dc..814e07e51 100644 --- a/src/graphics/niche/InkHUD/Persistence.cpp +++ b/src/graphics/niche/InkHUD/Persistence.cpp @@ -17,22 +17,25 @@ void InkHUD::Persistence::loadSettings() LOG_WARN("Settings version changed. Using defaults"); } -// Load settings and latestMessage data +// Rebuild the latestMessage cache from the global messageStore. +// Called after messageStore.loadFromFlash() so that the most recent broadcast and DM +// are immediately available to applets (DMApplet, AllMessageApplet, NotificationApplet). void InkHUD::Persistence::loadLatestMessage() { - // Load previous "latestMessages" data from flash - MessageStore store("latest"); - store.loadFromFlash(); - - // Place into latestMessage struct, for convenient access - // Number of strings loaded determines whether last message was broadcast or dm - if (store.messages.size() == 1) { - latestMessage.dm = store.messages.at(0); - latestMessage.wasBroadcast = false; - } else if (store.messages.size() == 2) { - latestMessage.dm = store.messages.at(0); - latestMessage.broadcast = store.messages.at(1); - latestMessage.wasBroadcast = true; + for (const StoredMessage &m : messageStore.getLiveMessages()) { + if (m.type == MessageType::BROADCAST) { + if (m.timestamp >= latestMessage.broadcast.timestamp) { + latestMessage.broadcast = m; + if (m.timestamp >= latestMessage.dm.timestamp) + latestMessage.wasBroadcast = true; + } + } else if (m.type == MessageType::DM_TO_US) { + if (m.timestamp >= latestMessage.dm.timestamp) { + latestMessage.dm = m; + if (m.timestamp >= latestMessage.broadcast.timestamp) + latestMessage.wasBroadcast = false; + } + } } } @@ -42,15 +45,10 @@ void InkHUD::Persistence::saveSettings() FlashData::save(&settings, "settings"); } -// Save latestMessage data to flash +// Persist all messages via the global messageStore. void InkHUD::Persistence::saveLatestMessage() { - // Number of strings saved determines whether last message was broadcast or dm - MessageStore store("latest"); - store.messages.push_back(latestMessage.dm); - if (latestMessage.wasBroadcast) - store.messages.push_back(latestMessage.broadcast); - store.saveToFlash(); + messageStore.saveToFlash(); } /* diff --git a/src/graphics/niche/InkHUD/Persistence.h b/src/graphics/niche/InkHUD/Persistence.h index 6f783cb1c..357607ea5 100644 --- a/src/graphics/niche/InkHUD/Persistence.h +++ b/src/graphics/niche/InkHUD/Persistence.h @@ -15,7 +15,7 @@ The save / load mechanism is a shared NicheGraphics feature. #include "configuration.h" #include "./InkHUD.h" -#include "graphics/niche/InkHUD/MessageStore.h" +#include "MessageStore.h" #include "graphics/niche/Utils/FlashData.h" namespace NicheGraphics::InkHUD @@ -120,12 +120,12 @@ class Persistence }; // Most recently received text message - // Value is updated by InkHUD::WindowManager, as a courtesy to applets - // InkHUD keeps its own latest-message cache for applets. + // Value is updated by InkHUD::Events, as a courtesy to applets. + // Populated at boot from the global messageStore, then updated live on receive. struct LatestMessage { - MessageStore::Message broadcast; // Most recent message received broadcast - MessageStore::Message dm; // Most recent received DM - bool wasBroadcast; // True if most recent broadcast is newer than most recent dm + StoredMessage broadcast; // Most recent broadcast message received + StoredMessage dm; // Most recent DM received + bool wasBroadcast; // True if most recent broadcast is newer than most recent dm }; void loadSettings(); diff --git a/src/graphics/niche/InkHUD/docs/README.md b/src/graphics/niche/InkHUD/docs/README.md index 94af04fb2..54b4ad426 100644 --- a/src/graphics/niche/InkHUD/docs/README.md +++ b/src/graphics/niche/InkHUD/docs/README.md @@ -422,9 +422,11 @@ Stores InkHUD data in flash - settings - most recent text message received (both for broadcast and DM) -In rare cases, applets may store their own specific data separately (e.g. `ThreadedMessageApplet`) +Message history (used by `ThreadedMessageApplet`) is stored by the firmware-wide `MessageStore` (`src/MessageStore.h`), not by `Persistence` directly. -Data saved only on shutdown / reboot. Not saved if power is removed unexpectedly. +Settings are saved only on shutdown / reboot. Not saved if power is removed unexpectedly. + +Message history is saved periodically (every 2 hours by default), as well as on shutdown / reboot. --- @@ -466,18 +468,14 @@ Collected here, so various user applets don't all have to store their own copy o We keep this separate latest-message cache for this purpose, because: -- it is cleared by an outgoing text message -- we want to store both a recent broadcast and a recent DM +- we want to expose both the most recent broadcast and most recent DM independently +- applets like `DMApplet` and `NotificationApplet` need quick access without scanning the full message history #### Saving / Loading -_A bit of a hack.._ -Stored to flash using `InkHUD::MessageStore`, which is really intended for storing a thread of messages (see `ThreadedMessageApplet`). Used because it stores strings more efficiently than `FlashData.h`. +The `LatestMessage` cache is not persisted to its own file. On boot, `InkHUD::begin()` calls `messageStore.loadFromFlash()` first, then `Persistence::loadLatestMessage()` rebuilds the cache by scanning the loaded messages for the most recent broadcast and DM. -The hack is: - -- If most recent message was a DM, we only store the DM. -- If most recent message was a broadcast, we store both a DM and a broadcast. The DM may be 0-length string. +Text is stored in the firmware-wide shared text pool. Use `MessageStore::getText(msg)` to retrieve it from a `StoredMessage`. --- @@ -588,7 +586,7 @@ Button input is sometimes handled by a system applet. `InkHUD::Events` determine #### Factory Reset -The Events class handles the admin messages(s) which trigger factory reset. We set `Events::eraseOnReboot = true`, which causes `Events::onReboot` to erase the contents of InkHUD's data directory. We do this because some applets (e.g. ThreadedMessageApplet) save their own data to flash, so if we erased earlier, that data would get re-written during reboot. +The Events class handles the admin message(s) which trigger factory reset. We set `Events::eraseOnReboot = true`, which causes `Events::onReboot` to erase the contents of InkHUD's data directory (`/NicheGraphics/`) and also call `messageStore.clearAllMessages()` to wipe the firmware-wide message store (`/Messages_default.msgs`). Both are cleared during reboot rather than earlier, to avoid data being re-written by applets still running before shutdown. --- diff --git a/src/main.cpp b/src/main.cpp index 1dc078c62..2541c7c20 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1243,7 +1243,7 @@ void loop() } #endif #endif -#if HAS_SCREEN && ENABLE_MESSAGE_PERSISTENCE +#if (HAS_SCREEN || defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)) && ENABLE_MESSAGE_PERSISTENCE messageStoreAutosaveTick(); #endif long delayMsec = mainController.runOrDelay();