diff --git a/src/MessageStore.cpp b/src/MessageStore.cpp index 322e13fe6..99c069b36 100644 --- a/src/MessageStore.cpp +++ b/src/MessageStore.cpp @@ -16,11 +16,10 @@ 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) - // NOTE: Using std::string length here (fast path we had originally) return 16 + std::min(static_cast(MAX_MESSAGE_SIZE), m.text.size()); } +// Helper: assign a timestamp (RTC if available, else boot-relative) static inline void assignTimestamp(StoredMessage &sm) { uint32_t nowSecs = getValidTime(RTCQuality::RTCQualityDevice, true); @@ -33,6 +32,21 @@ static inline void assignTimestamp(StoredMessage &sm) } } +// Generic push with cap (used by live + persisted queues) +template static inline void pushWithLimit(std::deque &queue, const T &msg) +{ + if (queue.size() >= MAX_MESSAGES_SAVED) + queue.pop_front(); + queue.push_back(msg); +} + +template static inline void pushWithLimit(std::deque &queue, T &&msg) +{ + if (queue.size() >= MAX_MESSAGES_SAVED) + queue.pop_front(); + queue.emplace_back(std::move(msg)); +} + void MessageStore::logMemoryUsage(const char *context) const { size_t total = 0; @@ -52,37 +66,24 @@ MessageStore::MessageStore(const std::string &label) // Live message handling (RAM only) void MessageStore::addLiveMessage(StoredMessage &&msg) { - if (liveMessages.size() >= MAX_MESSAGES_SAVED) { - liveMessages.pop_front(); // keep only most recent N - } - liveMessages.emplace_back(std::move(msg)); + pushWithLimit(liveMessages, std::move(msg)); } - void MessageStore::addLiveMessage(const StoredMessage &msg) { - if (liveMessages.size() >= MAX_MESSAGES_SAVED) { - liveMessages.pop_front(); // keep only most recent N - } - liveMessages.push_back(msg); + pushWithLimit(liveMessages, msg); } // Persistence queue (used only on shutdown/reboot) void MessageStore::addMessage(StoredMessage &&msg) { - if (messages.size() >= MAX_MESSAGES_SAVED) { - messages.pop_front(); - } - messages.emplace_back(std::move(msg)); + pushWithLimit(messages, std::move(msg)); } - void MessageStore::addMessage(const StoredMessage &msg) { - if (messages.size() >= MAX_MESSAGES_SAVED) { - messages.pop_front(); - } - messages.push_back(msg); + pushWithLimit(messages, msg); } +// Add from incoming/outgoing packet const StoredMessage &MessageStore::addFromPacket(const meshtastic_MeshPacket &packet) { StoredMessage sm; @@ -171,17 +172,18 @@ void MessageStore::saveToFlash() uint8_t count = messages.size(); f.write(&count, 1); - for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) { + for (uint8_t i = 0; i < count && 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)); + // Write text payload (capped at MAX_MESSAGE_SIZE) const size_t toWrite = std::min(static_cast(MAX_MESSAGE_SIZE), m.text.size()); if (toWrite) f.write((const uint8_t *)m.text.data(), toWrite); - f.write('\0'); // null terminator + f.write('\0'); uint8_t bootFlag = m.isBootRelative ? 1 : 0; f.write(&bootFlag, 1); // persist boot-relative flag @@ -224,9 +226,8 @@ void MessageStore::loadFromFlash() f.readBytes((char *)&m.channelIndex, sizeof(m.channelIndex)); f.readBytes((char *)&m.dest, sizeof(m.dest)); - // Fast text read: read until NUL or cap m.text.clear(); - m.text.reserve(64); // small reserve to avoid initial reallocs + m.text.reserve(64); char c; size_t readCount = 0; while (readCount < MAX_MESSAGE_SIZE) { @@ -238,21 +239,19 @@ void MessageStore::loadFromFlash() ++readCount; } - // Try to read boot-relative flag (new format) + // Try to read boot-relative flag 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) + // Try to read ackStatus if (f.available() > 0) { uint8_t statusByte = 0; if (f.readBytes((char *)&statusByte, 1) == 1) { @@ -408,25 +407,20 @@ void MessageStore::upgradeBootRelativeTimestamps() 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; + auto fix = [&](std::deque &dq) { + for (auto &m : dq) { + if (m.isBootRelative && m.timestamp <= bootNow) { + uint32_t bootOffset = nowSecs - bootNow; + m.timestamp += bootOffset; + m.isBootRelative = false; + } + // else: persisted from old boot → stays ??? forever } - // 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 - } + }; + fix(liveMessages); + fix(messages); } // Global definition MessageStore messageStore("default"); -#endif \ No newline at end of file +#endif diff --git a/src/MessageStore.h b/src/MessageStore.h index d20c2596a..944a5cb47 100644 --- a/src/MessageStore.h +++ b/src/MessageStore.h @@ -7,7 +7,8 @@ #define LOG_DEBUG(...) #endif -// Disable message persistence to flash if you’re short on space +// Enable or disable message persistence (flash storage) +// Define -DENABLE_MESSAGE_PERSISTENCE=0 in build_flags to disable it entirely #ifndef ENABLE_MESSAGE_PERSISTENCE #define ENABLE_MESSAGE_PERSISTENCE 1 #endif @@ -17,15 +18,17 @@ #include #include -// Max number of messages we’ll keep in history -#ifndef MAX_MESSAGES_SAVED -#define MAX_MESSAGES_SAVED 20 +// how many messages are stored (RAM + flash). +// Define -DMESSAGE_HISTORY_LIMIT=N in build_flags to control memory usage. +#ifndef MESSAGE_HISTORY_LIMIT +#define MESSAGE_HISTORY_LIMIT 20 #endif -// Keep a cap for serialized payloads -#ifndef MAX_MESSAGE_SIZE +// 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). #define MAX_MESSAGE_SIZE 220 -#endif // Explicit message classification enum class MessageType : uint8_t { BROADCAST = 0, DM_TO_US = 1 }; @@ -39,6 +42,7 @@ 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/RTC) uint32_t sender; // NodeNum of sender @@ -46,8 +50,6 @@ struct StoredMessage { std::string text; // UTF-8 text payload (dynamic now, no fixed size) // Destination node. - // 0xffffffff (NODENUM_BROADCAST) means broadcast, - // otherwise this is the NodeNum of the DM recipient. uint32_t dest; // Explicit classification (derived from dest when loading old messages) @@ -115,9 +117,9 @@ class MessageStore void logMemoryUsage(const char *context) const; private: - std::deque liveMessages; - std::deque messages; // persisted copy - std::string filename; + std::deque liveMessages; // current in-RAM messages + std::deque messages; // persisted copy on flash + std::string filename; // flash filename for persistence }; // Global instance (defined in MessageStore.cpp) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index c4d19ff5b..3e8557377 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -276,9 +276,7 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int } else { // otherwise, just display the module frame that's aligned with the current frame module_frame = state->currentFrame; - // LOG_DEBUG("Screen is not in transition. Frame: %d", module_frame); } - // LOG_DEBUG("Draw Module Frame %d", module_frame); MeshModule &pi = *moduleFrames.at(module_frame); pi.drawFrame(display, state, x, y); } @@ -329,7 +327,6 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O { graphics::normalFrames = new FrameCallback[MAX_NUM_NODES + NUM_EXTRA_FRAMES]; - LOG_INFO("Protobuf Value uiconfig.screen_rgb_color: %d", uiconfig.screen_rgb_color); int32_t rawRGB = uiconfig.screen_rgb_color; // Only validate the combined value once @@ -338,9 +335,6 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O int r = (rawRGB >> 16) & 0xFF; int g = (rawRGB >> 8) & 0xFF; int b = rawRGB & 0xFF; - - LOG_INFO("Values of r,g,b: %d, %d, %d", r, g, b); - if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) { TFT_MESH = COLOR565(static_cast(r), static_cast(g), static_cast(b)); } @@ -784,17 +778,17 @@ int32_t Screen::runOnce() break; case Cmd::ON_PRESS: if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - handleOnPress(); + showFrame(FrameDirection::NEXT); } break; case Cmd::SHOW_PREV_FRAME: if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - handleShowPrevFrame(); + showFrame(FrameDirection::PREVIOUS); } break; case Cmd::SHOW_NEXT_FRAME: if (NotificationRenderer::current_notification_type != notificationTypeEnum::text_input) { - handleShowNextFrame(); + showFrame(FrameDirection::NEXT); } break; case Cmd::START_ALERT_FRAME: { @@ -1266,17 +1260,14 @@ void Screen::hideCurrentFrame() uint8_t currentFrame = ui->getUiState()->currentFrame; bool dismissed = false; - if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { - LOG_DEBUG("Hide Waypoint"); + if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { // hide Waypoint devicestate.has_rx_waypoint = false; hiddenFrames.waypoint = true; dismissed = true; - } else if (currentFrame == framesetInfo.positions.wifi) { - LOG_DEBUG("Hide WiFi Screen"); + } else if (currentFrame == framesetInfo.positions.wifi) { // hide Wifi Screen hiddenFrames.wifi = true; dismissed = true; - } else if (currentFrame == framesetInfo.positions.lora) { - LOG_INFO("Hide LoRa"); + } else if (currentFrame == framesetInfo.positions.lora) { // hide lora screen hiddenFrames.lora = true; dismissed = true; } @@ -1371,30 +1362,21 @@ void Screen::handleOnPress() } } -void Screen::handleShowPrevFrame() +void Screen::showFrame(FrameDirection direction) { - // If screen was off, just wake it, otherwise go back to previous frame - // If we are in a transition, the press must have bounced, drop it. + // Only advance frames when UI is stable if (ui->getUiState()->frameState == FIXED) { - ui->previousFrame(); + + if (direction == FrameDirection::NEXT) { + ui->nextFrame(); + } else { + ui->previousFrame(); + } + lastScreenTransition = millis(); setFastFramerate(); - // Reset scroll state if we’re leaving the text message frame - graphics::MessageRenderer::resetScrollState(); - } -} - -void Screen::handleShowNextFrame() -{ - // If screen was off, just wake it, otherwise advance to next frame - // If we are in a transition, the press must have bounced, drop it. - if (ui->getUiState()->frameState == FIXED) { - ui->nextFrame(); - lastScreenTransition = millis(); - setFastFramerate(); - - // Reset scroll state if we’re leaving the text message frame + // Reset scroll state when switching away from text message screen graphics::MessageRenderer::resetScrollState(); } } @@ -1419,7 +1401,6 @@ void Screen::setFastFramerate() int Screen::handleStatusUpdate(const meshtastic::Status *arg) { - // LOG_DEBUG("Screen got status update %d", arg->getStatusType()); switch (arg->getStatusType()) { case STATUS_TYPE_NODE: if (showingNormalScreen && nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) { @@ -1516,9 +1497,9 @@ int Screen::handleInputEvent(const InputEvent *event) // If no modules are using the input, move between frames if (!inputIntercepted) { if (event->inputEvent == INPUT_BROKER_LEFT || event->inputEvent == INPUT_BROKER_ALT_PRESS) { - showPrevFrame(); + showFrame(FrameDirection::PREVIOUS); } else if (event->inputEvent == INPUT_BROKER_RIGHT || event->inputEvent == INPUT_BROKER_USER_PRESS) { - showNextFrame(); + showFrame(FrameDirection::NEXT); } else if (event->inputEvent == INPUT_BROKER_SELECT) { if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) { menuHandler::homeBaseMenu(); @@ -1557,7 +1538,7 @@ int Screen::handleInputEvent(const InputEvent *event) menuHandler::wifiBaseMenu(); } } else if (event->inputEvent == INPUT_BROKER_BACK) { - showPrevFrame(); + showFrame(FrameDirection::PREVIOUS); } else if (event->inputEvent == INPUT_BROKER_CANCEL) { setOn(false); } diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 9902bf03f..a1b9f7886 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -169,6 +169,8 @@ class Point namespace graphics { +enum class FrameDirection { NEXT, PREVIOUS }; + // Forward declarations class Screen; @@ -271,6 +273,7 @@ class Screen : public concurrency::OSThread void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); } void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); } void showNextFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_NEXT_FRAME}); } + void showFrame(FrameDirection direction); // generic alert start void startAlert(FrameCallback _alertFrame) @@ -631,8 +634,6 @@ class Screen : public concurrency::OSThread // Implementations of various commands, called from doTask(). void handleSetOn(bool on, FrameCallback einkScreensaver = NULL); void handleOnPress(); - void handleShowNextFrame(); - void handleShowPrevFrame(); void handleStartFirmwareUpdateScreen(); // Info collected by setFrames method. diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index 7ddd72e72..6f2eab485 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -36,7 +36,7 @@ static std::vector cachedLines; static std::vector cachedHeights; // UTF-8 skip helper -inline size_t utf8CharLen(uint8_t c) +static inline size_t utf8CharLen(uint8_t c) { if ((c & 0xE0) == 0xC0) return 2; @@ -75,7 +75,22 @@ std::string normalizeEmoji(const std::string &s) void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount) { - std::string renderLine = normalizeEmoji(line); + std::string renderLine; + for (size_t i = 0; i < line.size();) { + uint8_t c = (uint8_t)line[i]; + size_t len = utf8CharLen(c); + if (c == 0xEF && i + 2 < line.size() && (uint8_t)line[i + 1] == 0xB8 && (uint8_t)line[i + 2] == 0x8F) { + i += 3; + continue; + } + if (c == 0xF0 && i + 3 < line.size() && (uint8_t)line[i + 1] == 0x9F && (uint8_t)line[i + 2] == 0x8F && + ((uint8_t)line[i + 3] >= 0xBB && (uint8_t)line[i + 3] <= 0xBF)) { + i += 4; + continue; + } + renderLine.append(line, i, len); + i += len; + } int cursorX = x; const int fontHeight = FONT_HEIGHT_SMALL; @@ -260,14 +275,14 @@ const std::vector &getSeenPeers() return seenPeers; } -inline int centerYForRow(int y, int size) +static int centerYForRow(int y, int size) { int midY = y + (FONT_HEIGHT_SMALL / 2); return midY - (size / 2); } // Helpers for drawing status marks (thickened strokes) -void drawCheckMark(OLEDDisplay *display, int x, int y, int size = 8) +static void drawCheckMark(OLEDDisplay *display, int x, int y, int size) { int topY = centerYForRow(y, size); display->setColor(WHITE); @@ -277,7 +292,7 @@ void drawCheckMark(OLEDDisplay *display, int x, int y, int size = 8) display->drawLine(x + size / 3, topY + size + 1, x + size, topY + 1); } -void drawXMark(OLEDDisplay *display, int x, int y, int size = 8) +static void drawXMark(OLEDDisplay *display, int x, int y, int size = 8) { int topY = centerYForRow(y, size); display->setColor(WHITE); @@ -287,7 +302,7 @@ void drawXMark(OLEDDisplay *display, int x, int y, int size = 8) display->drawLine(x + size, topY + 1, x, topY + size + 1); } -void drawRelayMark(OLEDDisplay *display, int x, int y, int size = 8) +static void drawRelayMark(OLEDDisplay *display, int x, int y, int size = 8) { int r = size / 2; int centerY = centerYForRow(y, size) + r; @@ -299,7 +314,7 @@ void drawRelayMark(OLEDDisplay *display, int x, int y, int size = 8) display->drawLine(centerX - 1, centerY - 4, centerX + 1, centerY - 4); } -int getRenderedLineWidth(OLEDDisplay *display, const std::string &line, const Emote *emotes, int emoteCount) +static inline int getRenderedLineWidth(OLEDDisplay *display, const std::string &line, const Emote *emotes, int emoteCount) { std::string normalized = normalizeEmoji(line); int totalWidth = 0; @@ -471,35 +486,32 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // Build header line for this message meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(m.sender); - std::string senderStr_o = "???"; + char senderBuf[48] = "???"; if (node && node->has_user) { - senderStr_o = node->user.long_name; + strncpy(senderBuf, node->user.long_name, sizeof(senderBuf) - 1); + senderBuf[sizeof(senderBuf) - 1] = '\0'; } // If this is *our own* message, override sender to "Me" bool mine = (m.sender == nodeDB->getNodeNum()); if (mine) { - senderStr_o = "Me"; + strcpy(senderBuf, "Me"); } - const char *sender = senderStr_o.c_str(); + const char *sender = senderBuf; - if (display->getStringWidth(sender) + display->getStringWidth(timeBuf) + display->getStringWidth(chanType) + - display->getStringWidth(" @") + display->getStringWidth("... ") - 10 > - SCREEN_WIDTH) { - // truncate sender name if too long - int availWidth = SCREEN_WIDTH - display->getStringWidth(timeBuf) - display->getStringWidth(chanType) - - display->getStringWidth(" @") - display->getStringWidth("... ") - 10; + // If sender width too wide → truncate manually + int availWidth = SCREEN_WIDTH - display->getStringWidth(timeBuf) - display->getStringWidth(chanType) - + display->getStringWidth(" @") - display->getStringWidth("... ") - 10; - int len = static_cast(senderStr_o.size()); - while (len > 0 && display->getStringWidth(sender, len) > availWidth) { - --len; - } + while (strlen(senderBuf) > 0 && display->getStringWidth(senderBuf) > availWidth) { + senderBuf[strlen(senderBuf) - 1] = '\0'; + } - if (len < static_cast(senderStr_o.size())) { - senderStr_o = senderStr_o.substr(0, len) + "..."; - sender = senderStr_o.c_str(); - } + // Add ellipsis if needed + if (display->getStringWidth(senderBuf) > availWidth && strlen(senderBuf) >= 3) { + size_t len = strlen(senderBuf); + strcpy(&senderBuf[len - 3], "..."); } // Final header line diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index c0ee60545..a5de8a50f 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -1346,7 +1346,7 @@ int32_t CannedMessageModule::runOnce() this->freetext.substring(this->cursor); } this->cursor++; - uint16_t maxChars = meshtastic_Constants_DATA_PAYLOAD_LEN - (moduleConfig.canned_message.send_bell ? 1 : 0); + const uint16_t maxChars = 200 - (moduleConfig.canned_message.send_bell ? 1 : 0); if (this->freetext.length() > maxChars) { this->cursor = maxChars; this->freetext = this->freetext.substring(0, maxChars);