From 62eaabc9408230477021d16b272ae7189537369a Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 15 Oct 2025 01:57:51 -0400 Subject: [PATCH] More optimization --- src/MessageStore.cpp | 140 ++++++++++++++++++++------ src/MessageStore.h | 47 +++++---- src/graphics/draw/MenuHandler.cpp | 3 - src/graphics/draw/MessageRenderer.cpp | 28 ++++-- src/graphics/draw/MessageRenderer.h | 2 +- src/modules/CannedMessageModule.cpp | 5 +- 6 files changed, 166 insertions(+), 59 deletions(-) diff --git a/src/MessageStore.cpp b/src/MessageStore.cpp index 143f9b575..1e60fb646 100644 --- a/src/MessageStore.cpp +++ b/src/MessageStore.cpp @@ -9,6 +9,48 @@ #include "graphics/draw/MessageRenderer.h" #include // memcpy +#ifndef MESSAGE_TEXT_POOL_SIZE +#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE) +#endif + +// Global message text pool and state +static char g_messagePool[MESSAGE_TEXT_POOL_SIZE]; +static size_t g_poolWritePos = 0; + +// Reset pool (called on boot or clear) +static inline void resetMessagePool() +{ + g_poolWritePos = 0; + memset(g_messagePool, 0, sizeof(g_messagePool)); +} + +// Allocate text in pool and return offset +// If not enough space remains, wrap around (ring buffer style) +static inline uint16_t storeTextInPool(const char *src, size_t len) +{ + if (len >= MAX_MESSAGE_SIZE) + len = MAX_MESSAGE_SIZE - 1; + + // Wrap pool if out of space + if (g_poolWritePos + len + 1 >= MESSAGE_TEXT_POOL_SIZE) { + g_poolWritePos = 0; + } + + uint16_t offset = g_poolWritePos; + memcpy(&g_messagePool[g_poolWritePos], src, len); + g_messagePool[g_poolWritePos + len] = '\0'; + g_poolWritePos += (len + 1); + return offset; +} + +// Retrieve a const pointer to message text by offset +static inline const char *getTextFromPool(uint16_t offset) +{ + if (offset >= MESSAGE_TEXT_POOL_SIZE) + return ""; + return &g_messagePool[offset]; +} + // Helper: assign a timestamp (RTC if available, else boot-relative) static inline void assignTimestamp(StoredMessage &sm) { @@ -40,6 +82,7 @@ template static inline void pushWithLimit(std::deque &queue, T & MessageStore::MessageStore(const std::string &label) { filename = "/Messages_" + label + ".msgs"; + resetMessagePool(); // initialize text pool on boot } // Live message handling (RAM only) @@ -56,24 +99,42 @@ void MessageStore::addLiveMessage(const StoredMessage &msg) const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet) { StoredMessage sm; - assignTimestamp(sm); // set timestamp (RTC or boot-relative) + assignTimestamp(sm); sm.channelIndex = packet.channel; - strncpy(sm.text, reinterpret_cast(packet.decoded.payload.bytes), MAX_MESSAGE_SIZE - 1); - sm.text[MAX_MESSAGE_SIZE - 1] = '\0'; + const char *payload = reinterpret_cast(packet.decoded.payload.bytes); + size_t len = strnlen(payload, MAX_MESSAGE_SIZE - 1); + sm.textOffset = storeTextInPool(payload, len); + sm.textLength = len; + + uint32_t localNode = nodeDB->getNodeNum(); + sm.sender = (packet.from == 0) ? localNode : packet.from; + sm.dest = packet.decoded.dest; + + // DM detection: use decoded.dest if valid, otherwise fallback to header 'to' + bool isDM = false; + uint32_t actualDest = sm.dest; + + if (actualDest == 0 || actualDest == 0xffffffff) { + actualDest = packet.to; + } + + if (actualDest != 0 && actualDest != NODENUM_BROADCAST && actualDest == localNode) { + isDM = true; + } + + // Incoming vs outgoing classification if (packet.from == 0) { - // Outgoing (phone-originated) - sm.sender = nodeDB->getNodeNum(); - sm.dest = (packet.decoded.dest == 0) ? NODENUM_BROADCAST : packet.decoded.dest; - sm.type = (sm.dest == NODENUM_BROADCAST) ? MessageType::BROADCAST : MessageType::DM_TO_US; + // Sent by us + sm.type = isDM ? MessageType::DM_TO_US : MessageType::BROADCAST; sm.ackStatus = AckStatus::NONE; } else { - // Incoming - sm.sender = packet.from; - sm.dest = packet.decoded.dest; - sm.type = (sm.dest == NODENUM_BROADCAST) ? MessageType::BROADCAST - : (sm.dest == nodeDB->getNodeNum()) ? MessageType::DM_TO_US - : MessageType::BROADCAST; + // Received from another node + if (isDM) { + sm.type = MessageType::DM_TO_US; + } else { + sm.type = MessageType::BROADCAST; + } sm.ackStatus = AckStatus::ACKED; } @@ -91,8 +152,8 @@ void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const st sm.sender = sender; sm.channelIndex = channelIndex; - strncpy(sm.text, text.c_str(), MAX_MESSAGE_SIZE - 1); - sm.text[MAX_MESSAGE_SIZE - 1] = '\0'; + sm.textOffset = storeTextInPool(text.c_str(), text.size()); + sm.textLength = text.size(); // Default manual adds to broadcast sm.dest = NODENUM_BROADCAST; @@ -106,18 +167,17 @@ void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const st #if ENABLE_MESSAGE_PERSISTENCE -// Use a compile-time constant so the array bound can be used in the struct -static constexpr size_t TEXT_LEN = MAX_MESSAGE_SIZE; - -// Compact, fixed-size on-flash representation +// Compact, fixed-size on-flash representation using offset + length struct __attribute__((packed)) StoredMessageRecord { uint32_t timestamp; uint32_t sender; uint8_t channelIndex; uint32_t dest; uint8_t isBootRelative; - uint8_t ackStatus; // static_cast(AckStatus) - char text[TEXT_LEN]; // null-terminated + uint8_t ackStatus; // static_cast(AckStatus) + uint8_t type; // static_cast(MessageType) + uint16_t textLength; // message length + char text[MAX_MESSAGE_SIZE]; // <-- store actual text here }; // Serialize one StoredMessage to flash @@ -130,9 +190,14 @@ static inline void writeMessageRecord(SafeFile &f, const StoredMessage &m) rec.dest = m.dest; rec.isBootRelative = m.isBootRelative; rec.ackStatus = static_cast(m.ackStatus); + rec.type = static_cast(m.type); + rec.textLength = m.textLength; + + // Copy the actual text into the record from RAM pool + const char *txt = getTextFromPool(m.textOffset); + strncpy(rec.text, txt, MAX_MESSAGE_SIZE - 1); + rec.text[MAX_MESSAGE_SIZE - 1] = '\0'; - strncpy(rec.text, m.text, TEXT_LEN - 1); - rec.text[TEXT_LEN - 1] = '\0'; f.write(reinterpret_cast(&rec), sizeof(rec)); } @@ -149,9 +214,13 @@ static inline bool readMessageRecord(File &f, StoredMessage &m) m.dest = rec.dest; m.isBootRelative = rec.isBootRelative; m.ackStatus = static_cast(rec.ackStatus); - strncpy(m.text, rec.text, MAX_MESSAGE_SIZE - 1); - m.text[MAX_MESSAGE_SIZE - 1] = '\0'; - m.type = (m.dest == NODENUM_BROADCAST) ? MessageType::BROADCAST : MessageType::DM_TO_US; + m.type = static_cast(rec.type); + m.textLength = rec.textLength; + + // 💡 Re-store text into pool and update offset + m.textLength = strnlen(rec.text, MAX_MESSAGE_SIZE - 1); + m.textOffset = storeTextInPool(rec.text, m.textLength); + return true; } @@ -183,6 +252,8 @@ void MessageStore::saveToFlash() void MessageStore::loadFromFlash() { liveMessages.clear(); + resetMessagePool(); // reset pool when loading + #ifdef FSCom concurrency::LockGuard guard(spiLock); @@ -219,6 +290,7 @@ void MessageStore::loadFromFlash() {} void MessageStore::clearAllMessages() { liveMessages.clear(); + resetMessagePool(); #ifdef FSCom SafeFile f(filename.c_str(), false); @@ -266,7 +338,7 @@ void MessageStore::dismissOldestMessageInChannel(uint8_t channel) saveToFlash(); } -// Dismiss oldest message in a direct conversation with a peer +// Dismiss oldest message in a direct chat with a node void MessageStore::dismissOldestMessageWithPeer(uint32_t peer) { auto pred = [peer](const StoredMessage &m) { @@ -279,7 +351,6 @@ void MessageStore::dismissOldestMessageWithPeer(uint32_t peer) saveToFlash(); } -// Helper filters for future use std::deque MessageStore::getChannelMessages(uint8_t channel) const { std::deque result; @@ -320,12 +391,23 @@ void MessageStore::upgradeBootRelativeTimestamps() m.timestamp += bootOffset; m.isBootRelative = false; } - // else: persisted from old boot → stays ??? forever } }; fix(liveMessages); } +const char *MessageStore::getText(const StoredMessage &msg) +{ + // Wrapper around the internal helper + return getTextFromPool(msg.textOffset); +} + +uint16_t MessageStore::storeText(const char *src, size_t len) +{ + // Wrapper around the internal helper + return storeTextInPool(src, len); +} + // Global definition MessageStore messageStore("default"); #endif \ No newline at end of file diff --git a/src/MessageStore.h b/src/MessageStore.h index a3e719be0..6ca2e3c13 100644 --- a/src/MessageStore.h +++ b/src/MessageStore.h @@ -27,10 +27,16 @@ // Internal alias used everywhere in code – do NOT redefine elsewhere. #define MAX_MESSAGES_SAVED MESSAGE_HISTORY_LIMIT -// Maximum text payload size per message in bytes (fixed). -// All messages use the same size to simplify memory handling and avoid dynamic allocations. +// Maximum text payload size per message in bytes. +// This still defines the max message length, but we no longer reserve this space per message. #define MAX_MESSAGE_SIZE 220 +// Total shared text pool size for all messages combined. +// The text pool is RAM-only. Text is re-stored from flash into the pool on boot. +#ifndef MESSAGE_TEXT_POOL_SIZE +#define MESSAGE_TEXT_POOL_SIZE (MAX_MESSAGES_SAVED * MAX_MESSAGE_SIZE) +#endif + // Explicit message classification enum class MessageType : uint8_t { BROADCAST = 0, // broadcast message @@ -46,25 +52,23 @@ enum class AckStatus : uint8_t { RELAYED = 4 // got an ACK from relay, not destination }; -// A single stored message in RAM and/or flash struct StoredMessage { - uint32_t timestamp; // When message was created (secs since boot or RTC) - uint32_t sender; // NodeNum of sender - uint8_t channelIndex; // Channel index used - char text[MAX_MESSAGE_SIZE]; // Fixed-size buffer for message text (null-terminated) + uint32_t timestamp; // When message was created (secs since boot or RTC) + uint32_t sender; // NodeNum of sender + uint8_t channelIndex; // Channel index used + uint32_t dest; // Destination node (broadcast or direct) + MessageType type; // Derived from dest (explicit classification) + bool isBootRelative; // true = millis()/1000 fallback; false = epoch/RTC absolute + AckStatus ackStatus; // Delivery status (only meaningful for our own sent messages) - uint32_t dest; // Destination node (broadcast or direct) - - MessageType type; // Derived from dest (explicit classification) - - bool isBootRelative; // true = millis()/1000 fallback; false = epoch/RTC absolute - - AckStatus ackStatus; // Delivery status (only meaningful for our own sent messages) + // Text storage metadata — rebuilt from flash at boot + uint16_t textOffset; // Offset into global text pool (valid only after loadFromFlash()) + uint16_t textLength; // Length of text in bytes // Default constructor initializes all fields safely StoredMessage() - : timestamp(0), sender(0), channelIndex(0), text(""), dest(0xffffffff), type(MessageType::BROADCAST), - isBootRelative(false), ackStatus(AckStatus::NONE) // start as NONE (waiting, no symbol) + : timestamp(0), sender(0), channelIndex(0), dest(0xffffffff), type(MessageType::BROADCAST), isBootRelative(false), + ackStatus(AckStatus::NONE), textOffset(0), textLength(0) { } }; @@ -87,7 +91,7 @@ class MessageStore void saveToFlash(); // Save messages to flash void loadFromFlash(); // Load messages from flash - // Clear all messages (RAM + persisted queue) + // Clear all messages (RAM + persisted queue + text pool) void clearAllMessages(); // Dismiss helpers @@ -107,6 +111,15 @@ class MessageStore // Upgrade boot-relative timestamps once RTC is valid void upgradeBootRelativeTimestamps(); + // Retrieve the C-string text for a stored message + static const char *getText(const StoredMessage &msg); + + // Allocate text into pool (used by sender-side code) + static uint16_t storeText(const char *src, size_t len); + + // Used when loading from flash to rebuild the text pool + static uint16_t rebuildTextFromFlash(const char *src, size_t len); + private: std::deque liveMessages; // Single in-RAM message buffer (also used for persistence) std::string filename; // Flash filename for persistence diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index ba9fea315..af42379cb 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -481,9 +481,6 @@ void menuHandler::messageResponseMenu() } else if (selected == DismissAll) { messageStore.clearAllMessages(); graphics::MessageRenderer::clearThreadRegistries(); - - // Reset back to "View All" - graphics::MessageRenderer::setThreadMode(graphics::MessageRenderer::ThreadMode::ALL); } else if (selected == DismissOldest) { auto mode = graphics::MessageRenderer::getThreadMode(); int ch = graphics::MessageRenderer::getThreadChannel(); diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index e982b32ca..6317e8094 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -416,6 +416,14 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 } if (filtered.empty()) { + // If current conversation is empty go back to ALL view + if (currentMode != ThreadMode::ALL) { + setThreadMode(ThreadMode::ALL); + resetScrollState(); + return; // Next draw will rerun in ALL mode + } + + // Still in ALL mode and no messages at all → show placeholder graphics::drawCommonHeader(display, x, y, titleStr); didReset = false; const char *messageString = "No messages"; @@ -528,8 +536,9 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 isHeader.push_back(true); ackForLine.push_back(m.ackStatus); - // Split message text into wrapped lines - std::vector wrapped = generateLines(display, "", m.text, textWidth); + const char *msgText = MessageStore::getText(m); + + std::vector wrapped = generateLines(display, "", msgText, textWidth); for (auto &ln : wrapped) { allLines.push_back(ln); isMine.push_back(mine); @@ -880,8 +889,11 @@ void handleNewMessage(const StoredMessage &sm, const meshtastic_MeshPacket &pack screen->showSimpleBanner(banner, inThread ? 1000 : 3000); } - // Always focus into the correct conversation thread when a message arrives - setThreadFor(sm, packet); + // Always focus into the correct conversation thread when a message with real text arrives + const char *msgText = MessageStore::getText(sm); + if (msgText && msgText[0] != '\0') { + setThreadFor(sm, packet); + } // Reset scroll for a clean start resetScrollState(); @@ -889,10 +901,12 @@ void handleNewMessage(const StoredMessage &sm, const meshtastic_MeshPacket &pack void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet) { - if (sm.type == MessageType::BROADCAST) { + if (sm.dest == NODENUM_BROADCAST || sm.type == MessageType::BROADCAST) { + // Broadcast setThreadMode(ThreadMode::CHANNEL, sm.channelIndex); - } else if (sm.type == MessageType::DM_TO_US) { - uint32_t peer = (packet.from == 0) ? sm.dest : sm.sender; + } else { + // Direct message + uint32_t peer = (packet.from != 0) ? sm.sender : sm.dest; setThreadMode(ThreadMode::DIRECT, -1, peer); } } diff --git a/src/graphics/draw/MessageRenderer.h b/src/graphics/draw/MessageRenderer.h index ab1edc4ae..1bcd0e5da 100644 --- a/src/graphics/draw/MessageRenderer.h +++ b/src/graphics/draw/MessageRenderer.h @@ -28,7 +28,7 @@ int getThreadChannel(); // Getter for current peer (valid if mode == DIRECT) uint32_t getThreadPeer(); -// --- Registry accessors for menuHandler --- +// Registry accessors for menuHandler const std::vector &getSeenChannels(); const std::vector &getSeenPeers(); diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index f88adb66c..bd5299626 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -1062,8 +1062,9 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha sm.sender = nodeDB->getNodeNum(); // us sm.channelIndex = channel; - strncpy(sm.text, message, MAX_MESSAGE_SIZE - 1); - sm.text[MAX_MESSAGE_SIZE - 1] = '\0'; + size_t len = strnlen(message, MAX_MESSAGE_SIZE - 1); + sm.textOffset = MessageStore::storeText(message, len); + sm.textLength = len; // Classify broadcast vs DM if (dest == NODENUM_BROADCAST) {