Multi message storage (#8182)

* First try at multimessage storage and display

* Nrf built issue fix

* Message view mode

* Add channel name instead of channel slot

* trunk fix

* Fix for DM threading

* fix for message time

* rename of view mode to Conversations

* Reply in thread feature

* rename Select View Mode to Select Conversation

* dismiss all live fix

* Messages from phone show on screen

* Decoupled message packets from screen.cpp and cleaned up

* Cannedmessage cleanup and emotes fixed

* Ack on messages sent

* Ack message cleanup

* Dismiss feature fixed

* removed legacy temporary messages

* Emote picker fix

* Memory size debug

* Build error fix

* Sanity checks are okay sometimes

* Lengthen channel name and finalize cleanup removal of Broadcast

* Change DM to @ in order to unify on a single method

* Continue unifying display, also show message status on the "isMine" lines

* Add context for incoming messages

* Better to say "in" vs "on"

* crash fix for confirmation nodes

* Fix outbound labels based to avoid creating delays

* Eink autoscroll dissabled

* gating for message storage when not using a screen

* revert

* Build fail fix

* Don't error out with unset MAC address in unit tests

* Provide some extra spacing for low hanging characters in messages

* Reorder menu options and reword Respond

* Reword menus to better reflect actions

* Go to thread from favorite screen

* Reorder Favorite Action Menu with simple word modifications

* Consolidate wording on "Chats"

* Mute channel fix

* trunk fix

* Clean up how muting works along with when we wake the screen

* Fix builds for HELTEC_MESH_SOLAR

* Signal bars for message ack

* fix for notification renderer

* Remove duplicate code, fix more Chats, and fix C6L MessageRenderer

* Fix to many warnings related to BaseUI

* preset aware signal strength display

* More C6L fixes and clean up header lines

* Use text aligns for message layout where necessary

* Attempt to fix memory usage of invalidLifetime

* Update channel mute for adjusted protobuf

* Missed a comma in merge conflicts

* cleanup to get more space

* Trunk fixes

* Optimize Hi Rez Chirpy to save space

* more fixes

* More cleanup

* Remove used getConversationWith

* Remove unused dismissNewestMessage

* Fix another build error on occassion

* Dimiss key combo function deprecated

* More cleanup

* Fn symbol code removed

* Waypoint cleanup

* Trunk fix

* Fixup Waypoint screen with BaseUI code

* Implement Haruki's ClockRenderer and broadcast decomposeTime across various files.

* Revert "Implement Haruki's ClockRenderer and broadcast decomposeTime across various files."

This reverts commit 2f65721774.

* Implement Haruki's ClockRenderer and broadcast decomposeTime across various files. Attempt 2!

* remove memory usage debug

* Revert only RangeTestModule.cpp change

* Switch from dynamic std::string storage to fixed-size char[]

* Removing old left over code

* More optimization

* Free Heap when not on Message screen

* build error fixes

* Restore ellipsis to end of long names

* Remove legacy function renderMessageContent

* improved destination filtering

* force PKI

* cleanup

* Shorten longNames to not exceed message popups

* log messages sent from apps

* Trunk fix

* Improve layout of messages screen

* Fix potential crash for undefined variable

* Revert changes to RedirectablePrint.cpp

* Apply shortening to longNames in Select Destination

* Fix short name displays

* Fix sprintfOverlappingData issue

* Fix nullPointerRedundantCheck warning on ESP32

* Add "Delete All Chats" to all chat views

* Improve getSafeNodeName / sanitizeString code.

* Improve getSafeNodeName further

* Restore auto favorite; but only if not CLIENT_BASE

* Don't favorite if WE are CLIENT_BASE role

* Don't run message persistent in MUI

* Fix broken endifs

* Unkwnown nodes no longer show as ??? on message  thread

* More delete options and cleanup of code

* fix for delete this chat

* Message menu cleanup

* trunk fix

* Clean up some menu options and remove some Unit C6L ifdefines

* Rework Delete flow

* Desperate times call for desperate measures

* Create a background on the connected icon to reduce overlap impact

* Optimize code for background image

* Fix for Muzi_Base

* Trunk Fixes

* Remove the up/down shortcut to launch canned messages (#8370)

* Remove the up/down shortcut to launch canned messages

* Enabled MQTT and WEBSERVER by default (#8679)

Signed-off-by: kur1k0 <zhuzirun@m5stack.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>

---------

Signed-off-by: kur1k0 <zhuzirun@m5stack.com>
Co-authored-by: Riker <zhuzirun@m5stack.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>

* Correct string length calculation for signal bars

* Manual message scrolling

* Fix

* Restore CannedMessages on Home Frame

* UpDown situational destination for textMessage

* Correct up/down destinations on textMessage frame

* Update Screen.h for handleTextMessage

* Update Screen.cpp to repair a merge issue

* Add nudge scroll on UpDownEncoder devices.

* Set nodeName to maximum size

* Revert "Set nodeName to maximum size"

This reverts commit e254f39925.

* Reflow Node Lists and TLora Pager Views (#8942)

* Add files via upload

* Move files into the right place

* Short or Long Names for everyone!

* Add scrolling to Node list

* Pagination fix for Latest to oldest per page

* Page counters

* Dynamic scaling of column counts based upon screen size, clean up box drawing

* Reflow Node Lists and TLora Pager Views (#8942)

* Add files via upload

* Move files into the right place

* Short or Long Names for everyone!

* Add scrolling to Node list

* Pagination fix for Latest to oldest per page

* Page counters

* Dynamic scaling of column counts based upon screen size, clean up box drawing

* Update exempt labels for stale bot workflow

Adds triaged and backlog to the list of exempt labels.

* Update naming of Frame Visibility toggles

* Fix to scrolling

* Fix for content cutting off when from us

* Fix for "delete this chat" now it does delete the current one

* Rework isHighResolution to be an enum called ScreenResolution

* Migrate Unit C6L macro guards into currentResolution UltraLow checks

* Mistakes happen - restoring NodeList Renderer line

---------

Signed-off-by: kur1k0 <zhuzirun@m5stack.com>
Co-authored-by: Jason P <applewiz@mac.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
Co-authored-by: Riker <zhuzirun@m5stack.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: whywilson <m.tools@qq.com>
Co-authored-by: Tom Fifield <tom@tomfifield.net>
This commit is contained in:
HarukiToreda
2025-12-24 17:13:31 -05:00
committed by GitHub
parent e5c3eda2a2
commit 9da4396c6f
37 changed files with 3337 additions and 1656 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -75,7 +75,6 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
// === State/UI ===
bool shouldDraw();
bool hasMessages();
void showTemporaryMessage(const String &message);
void resetSearch();
void updateDestinationSelectionList();
void drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
@@ -153,10 +152,9 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
unsigned long lastUpdateMillis = 0;
String searchQuery;
String freetext;
String temporaryMessage;
// === Message Storage ===
char messageStore[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1];
char messageBuffer[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1];
char *messages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT];
int messagesCount = 0;
int currentMessageIndex = -1;
@@ -167,14 +165,11 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
NodeNum lastSentNode = 0; // Tracks the most recent node we sent a message to (for UI display)
ChannelIndex channel = 0; // Channel index used when sending a message
bool ack = false; // True = ACK received, False = NACK or failed
bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets
bool lastAckWasRelayed = false; // True if the ACK was relayed through intermediate nodes
uint8_t lastAckHopStart = 0; // Hop start value from the received ACK packet
uint8_t lastAckHopLimit = 0; // Hop limit value from the received ACK packet
float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI)
int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI)
bool ack = false; // True = ACK received, False = NACK or failed
bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets
float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI)
int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI)
uint32_t lastRequestId = 0; // tracks the request_id of our last sent packet
// === State Tracking ===
cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;

View File

@@ -60,9 +60,7 @@ meshtastic_MeshPacket *DropzoneModule::sendConditions()
long hms = rtc_sec % SEC_PER_DAY;
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
hour = hms / SEC_PER_HOUR;
min = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN;
graphics::decomposeTime(rtc_sec, hour, min, sec);
}
// Check if the dropzone is open or closed by reading the analog pin

View File

@@ -1,10 +1,13 @@
#include "SystemCommandsModule.h"
#include "input/InputBroker.h"
#include "meshUtils.h"
#if HAS_SCREEN
#include "MessageStore.h"
#include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#endif
#include "GPS.h"
#include "MeshService.h"
#include "Module.h"
@@ -28,10 +31,7 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
switch (event->kbchar) {
// Fn key symbols
case INPUT_BROKER_MSG_FN_SYMBOL_ON:
IF_SCREEN(screen->setFunctionSymbol("Fn"));
return 0;
case INPUT_BROKER_MSG_FN_SYMBOL_OFF:
IF_SCREEN(screen->removeFunctionSymbol("Fn"));
return 0;
// Brightness
case INPUT_BROKER_MSG_BRIGHTNESS_UP:
@@ -78,6 +78,9 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
case INPUT_BROKER_MSG_REBOOT:
IF_SCREEN(screen->showSimpleBanner("Rebooting...", 0));
nodeDB->saveToDisk();
#if HAS_SCREEN
messageStore.saveToFlash();
#endif
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
// runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
return true;

View File

@@ -378,7 +378,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
int line = 1;
// === Set Title
const char *titleStr = (graphics::isHighResolution) ? "Environment" : "Env.";
const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Environment" : "Env.";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);

View File

@@ -117,7 +117,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s
int line = 1;
// === Set Title
const char *titleStr = (graphics::isHighResolution) ? "Power Telem." : "Power";
const char *titleStr = (graphics::currentResolution == graphics::ScreenResolution::High) ? "Power Telem." : "Power";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);

View File

@@ -1,10 +1,14 @@
#include "TextMessageModule.h"
#include "MeshService.h"
#include "MessageStore.h"
#include "NodeDB.h"
#include "PowerFSM.h"
#include "buzz.h"
#include "configuration.h"
#include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/MessageRenderer.h"
#include "main.h"
TextMessageModule *textMessageModule;
ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
@@ -15,14 +19,26 @@ ProcessMessage TextMessageModule::handleReceived(const meshtastic_MeshPacket &mp
#endif
// We only store/display messages destined for us.
// Keep a copy of the most recent text message.
devicestate.rx_text_message = mp;
devicestate.has_rx_text_message = true;
#if HAS_SCREEN
// Guard against running in MeshtasticUI
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
// Store in the central message history
const StoredMessage &sm = messageStore.addFromPacket(mp);
// Pass message to renderer (banner + thread switching + scroll reset)
// Use the global Screen singleton to retrieve the current OLED display
auto *display = screen ? screen->getDisplayDevice() : nullptr;
graphics::MessageRenderer::handleNewMessage(display, sm, mp);
}
#endif
// Only trigger screen wake if configuration allows it
if (shouldWakeOnReceivedMessage()) {
powerFSM.trigger(EVENT_RECEIVED_MSG);
}
// Notify any observers (e.g. external modules that care about packets)
notifyObservers(&mp);
return ProcessMessage::CONTINUE; // Let others look at this message also if they want

View File

@@ -3,7 +3,13 @@
#include "SinglePortModule.h"
/**
* Text message handling for meshtastic - draws on the OLED display the most recent received message
* Text message handling for Meshtastic.
*
* This module is responsible for receiving and storing incoming text messages
* from the mesh. It updates device state and notifies observers so that other
* components (such as the MessageRenderer) can later display or process them.
*
* Rendering of messages on screen is no longer done here.
*/
class TextMessageModule : public SinglePortModule, public Observable<const meshtastic_MeshPacket *>
{
@@ -15,10 +21,10 @@ class TextMessageModule : public SinglePortModule, public Observable<const mesht
protected:
/** Called to handle a particular incoming message
@return ProcessMessage::STOP if you've guaranteed you've handled this message and no other handlers should be considered for
it
*/
*
* @return ProcessMessage::STOP if you've guaranteed you've handled this
* message and no other handlers should be considered for it.
*/
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
virtual bool wantPacket(const meshtastic_MeshPacket *p) override;
};

View File

@@ -2,6 +2,7 @@
#include "NodeDB.h"
#include "PowerFSM.h"
#include "configuration.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/draw/CompassRenderer.h"
#if HAS_SCREEN
@@ -14,6 +15,15 @@
WaypointModule *waypointModule;
static inline float degToRad(float deg)
{
return deg * PI / 180.0f;
}
static inline float radToDeg(float rad)
{
return rad * 180.0f / PI;
}
ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp)
{
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
@@ -52,31 +62,15 @@ ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp)
bool WaypointModule::shouldDraw()
{
#if !MESHTASTIC_EXCLUDE_WAYPOINT
if (screen == nullptr)
return false;
// If no waypoint to show
if (!devicestate.has_rx_waypoint)
if (!screen || !devicestate.has_rx_waypoint)
return false;
// Decode the message, to find the expiration time (is waypoint still valid)
// This handles "deletion" as well as expiration
meshtastic_Waypoint wp;
memset(&wp, 0, sizeof(wp));
meshtastic_Waypoint wp{}; // <- replaces memset
if (pb_decode_from_bytes(devicestate.rx_waypoint.decoded.payload.bytes, devicestate.rx_waypoint.decoded.payload.size,
&meshtastic_Waypoint_msg, &wp)) {
// Valid waypoint
if (wp.expire > getTime())
return devicestate.has_rx_waypoint = true;
// Expired, or deleted
else
return devicestate.has_rx_waypoint = false;
return wp.expire > getTime();
}
// If decoding failed
LOG_ERROR("Failed to decode waypoint");
devicestate.has_rx_waypoint = false;
return false;
return false; // no LOG_ERROR, no flag writes
#else
return false;
#endif
@@ -85,53 +79,46 @@ bool WaypointModule::shouldDraw()
/// Draw the last waypoint we received
void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
if (screen == nullptr)
if (!screen)
return;
// Prepare to draw
display->setFont(FONT_SMALL);
display->clear();
display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL);
int line = 1;
// Handle inverted display
// Unsure of expected behavior: for now, copy drawNodeInfo
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED)
display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL);
// === Set Title
const char *titleStr = "Waypoint";
// === Header ===
graphics::drawCommonHeader(display, x, y, titleStr);
const int w = display->getWidth();
const int h = display->getHeight();
// Decode the waypoint
const meshtastic_MeshPacket &mp = devicestate.rx_waypoint;
meshtastic_Waypoint wp;
memset(&wp, 0, sizeof(wp));
meshtastic_Waypoint wp{};
if (!pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Waypoint_msg, &wp)) {
// This *should* be caught by shouldDrawWaypoint, but we'll short-circuit here just in case
display->drawStringMaxWidth(0 + x, 0 + y, x + display->getWidth(), "Couldn't decode waypoint");
devicestate.has_rx_waypoint = false;
return;
}
// Get timestamp info. Will pass as a field to drawColumns
static char lastStr[20];
char lastStr[20];
getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr));
// Will contain distance information, passed as a field to drawColumns
static char distStr[20];
char distStr[20];
// Get our node, to use our own position
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
// Text fields to draw (left of compass)
// Last element must be NULL. This signals the end of the char*[] to drawColumns
const char *fields[] = {"Waypoint", lastStr, wp.name, distStr, NULL};
// Dimensions / co-ordinates for the compass/circle
int16_t compassX = 0, compassY = 0;
uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight());
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
compassX = x + display->getWidth() - compassDiam / 2 - 5;
compassY = y + display->getHeight() / 2;
} else {
compassX = x + display->getWidth() - compassDiam / 2 - 5;
compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2;
}
const uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(w, h);
const int16_t compassX = x + w - (compassDiam / 2) - 5;
const int16_t compassY = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT)
? y + h / 2
: y + FONT_HEIGHT_SMALL + (h - FONT_HEIGHT_SMALL) / 2;
// If our node has a position:
if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) {
@@ -141,7 +128,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
myHeading = 0;
} else {
if (screen->hasHeading())
myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians
myHeading = degToRad(screen->getHeading());
else
myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
}
@@ -157,46 +144,35 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther);
float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther;
bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI;
bearingToOtherDegrees = radToDeg(bearingToOtherDegrees);
// Distance to Waypoint
float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
if (d < (2 * MILES_TO_FEET))
snprintf(distStr, sizeof(distStr), "%.0fft %.0f°", d * METERS_TO_FEET, bearingToOtherDegrees);
else
snprintf(distStr, sizeof(distStr), "%.1fmi %.0f°", d * METERS_TO_FEET / MILES_TO_FEET, bearingToOtherDegrees);
float feet = d * METERS_TO_FEET;
snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°",
feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees);
} else {
if (d < 2000)
snprintf(distStr, sizeof(distStr), "%.0fm %.0f°", d, bearingToOtherDegrees);
else
snprintf(distStr, sizeof(distStr), "%.1fkm %.0f°", d / 1000, bearingToOtherDegrees);
snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000,
bearingToOtherDegrees);
}
}
// If our node doesn't have position
else {
// ? in the compass
display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?");
// ? in the distance field
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL)
strncpy(distStr, "? mi ?°", sizeof(distStr));
else
strncpy(distStr, "? km ?°", sizeof(distStr));
snprintf(distStr, sizeof(distStr), "? %s ?°",
(config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "mi" : "km");
}
// Draw compass circle
display->drawCircle(compassX, compassY, compassDiam / 2);
// Undo color-inversion, if set prior to drawing header
// Unsure of expected behavior? For now: copy drawNodeInfo
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
display->setColor(BLACK);
}
// Must be after distStr is populated
graphics::NodeListRenderer::drawColumns(display, x, y, fields);
display->setTextAlignment(TEXT_ALIGN_LEFT); // Something above me changes to a different alignment, forcing a fix here!
display->drawString(0, graphics::getTextPositions(display)[line++], lastStr);
display->drawString(0, graphics::getTextPositions(display)[line++], wp.name);
display->drawString(0, graphics::getTextPositions(display)[line++], wp.description);
display->drawString(0, graphics::getTextPositions(display)[line++], distStr);
}
#endif