#include "configuration.h" #if HAS_SCREEN #include "FSCommon.h" #include "MessageStore.h" #include "NodeDB.h" #include "SPILock.h" #include "SafeFile.h" #include "gps/RTC.h" #include "graphics/draw/MessageRenderer.h" using graphics::MessageRenderer::setThreadMode; using graphics::MessageRenderer::ThreadMode; // Calculate serialized size for a StoredMessage static inline size_t getMessageSize(const StoredMessage &m) { // serialized size = fixed 16 bytes + text length (capped at MAX_MESSAGE_SIZE) return 16 + std::min(MAX_MESSAGE_SIZE, strnlen(m.text, MAX_MESSAGE_SIZE)); } static inline void assignTimestamp(StoredMessage &sm) { uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); if (nowSecs) { sm.timestamp = nowSecs; sm.isBootRelative = false; } else { sm.timestamp = millis() / 1000; sm.isBootRelative = true; } } void MessageStore::logMemoryUsage(const char *context) const { size_t total = 0; for (const auto &m : messages) { total += getMessageSize(m); } LOG_DEBUG("MessageStore[%s]: %u messages, est %u bytes (~%u KB)", context, (unsigned)messages.size(), (unsigned)total, (unsigned)(total / 1024)); } MessageStore::MessageStore(const std::string &label) { filename = "/Messages_" + label + ".msgs"; } // Live message handling (RAM only) void MessageStore::addLiveMessage(StoredMessage &&msg) { if (liveMessages.size() >= MAX_MESSAGES_SAVED) { liveMessages.pop_front(); // keep only most recent N } // Use emplace_back with std::move to avoid extra copy liveMessages.emplace_back(std::move(msg)); } // Persistence queue (used only on shutdown/reboot) void MessageStore::addMessage(StoredMessage &&msg) { if (messages.size() >= MAX_MESSAGES_SAVED) { messages.pop_front(); } // Use emplace_back with std::move to avoid extra copy messages.emplace_back(std::move(msg)); } const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet) { StoredMessage sm; // Always use our local time (helper handles RTC vs boot time) 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'; if (packet.from == 0) { // Phone-originated (outgoing) sm.sender = nodeDB->getNodeNum(); // our node ID if (packet.decoded.dest == 0 || packet.decoded.dest == NODENUM_BROADCAST) { sm.dest = NODENUM_BROADCAST; sm.type = MessageType::BROADCAST; } else { sm.dest = packet.decoded.dest; sm.type = MessageType::DM_TO_US; } // Outgoing messages start as NONE until ACK/NACK arrives sm.ackStatus = AckStatus::NONE; } else { // Normal incoming sm.sender = packet.from; if (packet.to == NODENUM_BROADCAST || packet.decoded.dest == NODENUM_BROADCAST) { sm.dest = NODENUM_BROADCAST; sm.type = MessageType::BROADCAST; } else if (packet.to == nodeDB->getNodeNum()) { sm.dest = nodeDB->getNodeNum(); // DM to us sm.type = MessageType::DM_TO_US; } else { sm.dest = NODENUM_BROADCAST; // fallback sm.type = MessageType::BROADCAST; } // Received messages don’t wait for ACK mark as ACKED sm.ackStatus = AckStatus::ACKED; } addLiveMessage(std::move(sm)); // Return reference to the most recently stored message return liveMessages.back(); } // Outgoing/manual message void MessageStore::addFromString(uint32_t sender, uint8_t channelIndex, const std::string &text) { StoredMessage sm; // Always use our local time (helper handles RTC vs boot time) assignTimestamp(sm); sm.sender = sender; sm.channelIndex = channelIndex; strncpy(sm.text, text.c_str(), MAX_MESSAGE_SIZE - 1); sm.text[MAX_MESSAGE_SIZE - 1] = '\0'; // Default manual adds to broadcast sm.dest = NODENUM_BROADCAST; sm.type = MessageType::BROADCAST; // Outgoing messages start as NONE until ACK/NACK arrives sm.ackStatus = AckStatus::NONE; addLiveMessage(std::move(sm)); } #if ENABLE_MESSAGE_PERSISTENCE // Save RAM queue to flash (called on shutdown) void MessageStore::saveToFlash() { #ifdef FSCom // Copy live RAM buffer into persistence queue messages = liveMessages; spiLock->lock(); FSCom.mkdir("/"); // ensure root exists spiLock->unlock(); SafeFile f(filename.c_str(), false); spiLock->lock(); uint8_t count = messages.size(); f.write(&count, 1); for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) { const StoredMessage &m = messages.at(i); f.write((uint8_t *)&m.timestamp, sizeof(m.timestamp)); f.write((uint8_t *)&m.sender, sizeof(m.sender)); f.write((uint8_t *)&m.channelIndex, sizeof(m.channelIndex)); f.write((uint8_t *)&m.dest, sizeof(m.dest)); f.write((uint8_t *)m.text, strnlen(m.text, MAX_MESSAGE_SIZE)); f.write('\0'); // null terminator uint8_t bootFlag = m.isBootRelative ? 1 : 0; f.write(&bootFlag, 1); // persist boot-relative flag uint8_t statusByte = static_cast(m.ackStatus); f.write(&statusByte, 1); // persist ackStatus } spiLock->unlock(); f.close(); // Debug after saving logMemoryUsage("saveToFlash"); #else // Filesystem not available, skip persistence #endif } // Load persisted messages into RAM (called at boot) void MessageStore::loadFromFlash() { messages.clear(); liveMessages.clear(); #ifdef FSCom concurrency::LockGuard guard(spiLock); if (!FSCom.exists(filename.c_str())) return; auto f = FSCom.open(filename.c_str(), FILE_O_READ); if (!f) return; uint8_t count = 0; f.readBytes((char *)&count, 1); for (uint8_t i = 0; i < count && i < MAX_MESSAGES_SAVED; i++) { StoredMessage m; f.readBytes((char *)&m.timestamp, sizeof(m.timestamp)); f.readBytes((char *)&m.sender, sizeof(m.sender)); f.readBytes((char *)&m.channelIndex, sizeof(m.channelIndex)); f.readBytes((char *)&m.dest, sizeof(m.dest)); f.readBytes(m.text, MAX_MESSAGE_SIZE - 1); m.text[MAX_MESSAGE_SIZE - 1] = '\0'; // Try to read boot-relative flag (new format) uint8_t bootFlag = 0; if (f.available() > 0) { if (f.readBytes((char *)&bootFlag, 1) == 1) { m.isBootRelative = (bootFlag != 0); } else { // Old format, fallback heuristic m.isBootRelative = (m.timestamp < 60u * 60u * 24u * 7u); } } else { // Old format, fallback heuristic m.isBootRelative = (m.timestamp < 60u * 60u * 24u * 7u); } // Try to read ackStatus (newer format) if (f.available() > 0) { uint8_t statusByte = 0; if (f.readBytes((char *)&statusByte, 1) == 1) { m.ackStatus = static_cast(statusByte); } else { m.ackStatus = AckStatus::NONE; } } else { m.ackStatus = AckStatus::NONE; } // Recompute type from dest if (m.dest == NODENUM_BROADCAST) { m.type = MessageType::BROADCAST; } else { m.type = MessageType::DM_TO_US; } messages.push_back(m); liveMessages.push_back(m); // restore into RAM buffer } f.close(); // Debug after loading logMemoryUsage("loadFromFlash"); #endif } #else // Persistence disabled (saves flash space) void MessageStore::saveToFlash() {} void MessageStore::loadFromFlash() {} #endif // Clear all messages (RAM + persisted queue) void MessageStore::clearAllMessages() { liveMessages.clear(); messages.clear(); #ifdef FSCom SafeFile f(filename.c_str(), false); uint8_t count = 0; f.write(&count, 1); // write "0 messages" f.close(); #endif } // Internal helper: erase first or last message matching a predicate template static void eraseIf(std::deque &deque, Predicate pred, bool fromBack = false) { if (fromBack) { // Iterate from the back and erase the first match from the end for (auto it = deque.rbegin(); it != deque.rend(); ++it) { if (pred(*it)) { deque.erase(std::next(it).base()); break; } } } else { // Erase the first matching message from the front auto it = std::find_if(deque.begin(), deque.end(), pred); if (it != deque.end()) deque.erase(it); } } // Dismiss oldest message (RAM + persisted queue) void MessageStore::dismissOldestMessage() { eraseIf(liveMessages, [](StoredMessage &) { return true; }); eraseIf(messages, [](StoredMessage &) { return true; }); saveToFlash(); } // Dismiss oldest message in a specific channel void MessageStore::dismissOldestMessageInChannel(uint8_t channel) { auto pred = [channel](const StoredMessage &m) { return m.type == MessageType::BROADCAST && m.channelIndex == channel; }; eraseIf(liveMessages, pred); eraseIf(messages, pred); saveToFlash(); } // Dismiss oldest message in a direct conversation with a peer void MessageStore::dismissOldestMessageWithPeer(uint32_t peer) { auto pred = [peer](const StoredMessage &m) { if (m.type == MessageType::DM_TO_US) { uint32_t other = (m.sender == nodeDB->getNodeNum()) ? m.dest : m.sender; return other == peer; } return false; }; eraseIf(liveMessages, pred); eraseIf(messages, pred); saveToFlash(); } // Dismiss newest message (RAM + persisted queue) void MessageStore::dismissNewestMessage() { eraseIf(liveMessages, [](StoredMessage &) { return true; }, true); eraseIf(messages, [](StoredMessage &) { return true; }, true); saveToFlash(); } // Helper filters for future use std::deque MessageStore::getChannelMessages(uint8_t channel) const { std::deque result; for (const auto &m : liveMessages) { if (m.type == MessageType::BROADCAST && m.channelIndex == channel) { result.push_back(m); } } return result; } std::deque MessageStore::getDirectMessages() const { std::deque result; for (const auto &m : liveMessages) { if (m.type == MessageType::DM_TO_US) { result.push_back(m); } } return result; } // Upgrade boot-relative timestamps once RTC is valid // Only same-boot boot-relative messages are healed. // Persisted boot-relative messages from old boots stay ??? forever. void MessageStore::upgradeBootRelativeTimestamps() { uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); if (nowSecs == 0) return; // Still no valid RTC uint32_t bootNow = millis() / 1000; for (auto &m : liveMessages) { if (m.isBootRelative && m.timestamp <= bootNow) { uint32_t bootOffset = nowSecs - bootNow; m.timestamp += bootOffset; m.isBootRelative = false; } // else: persisted from old boot → stays ??? forever } for (auto &m : messages) { if (m.isBootRelative && m.timestamp <= bootNow) { uint32_t bootOffset = nowSecs - bootNow; m.timestamp += bootOffset; m.isBootRelative = false; } // else: persisted from old boot → stays ??? forever } } // Global definition MessageStore messageStore("default"); #endif