mirror of
https://github.com/meshtastic/firmware.git
synced 2026-01-15 06:17:47 +00:00
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 commit2f65721774. * 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 commite254f39925. * 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:
@@ -23,7 +23,6 @@ extern graphics::Screen *screen;
|
||||
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
static uint32_t lastSwitchTime = 0;
|
||||
#else
|
||||
#endif
|
||||
namespace graphics
|
||||
{
|
||||
@@ -46,79 +45,119 @@ void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *
|
||||
}
|
||||
|
||||
// Static variables for dynamic cycling
|
||||
static NodeListMode currentMode = MODE_LAST_HEARD;
|
||||
static ListMode_Node currentMode_Nodes = MODE_LAST_HEARD;
|
||||
static ListMode_Location currentMode_Location = MODE_DISTANCE;
|
||||
static int scrollIndex = 0;
|
||||
// Popup overlay state
|
||||
static uint32_t popupTime = 0;
|
||||
static int popupTotal = 0;
|
||||
static int popupStart = 0;
|
||||
static int popupEnd = 0;
|
||||
static int popupPage = 1;
|
||||
static int popupMaxPage = 1;
|
||||
|
||||
static const uint32_t POPUP_DURATION_MS = 1000; // 1 second visible
|
||||
|
||||
// =============================
|
||||
// Scrolling Logic
|
||||
// =============================
|
||||
void scrollUp()
|
||||
{
|
||||
if (scrollIndex > 0)
|
||||
scrollIndex--;
|
||||
|
||||
popupTime = millis(); // show popup
|
||||
}
|
||||
|
||||
void scrollDown()
|
||||
{
|
||||
scrollIndex++;
|
||||
popupTime = millis();
|
||||
}
|
||||
|
||||
// =============================
|
||||
// Utility Functions
|
||||
// =============================
|
||||
|
||||
const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node)
|
||||
const char *getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int columnWidth)
|
||||
{
|
||||
const char *name = NULL;
|
||||
static char nodeName[16] = "?";
|
||||
if (config.display.use_long_node_name == true) {
|
||||
if (node->has_user && strlen(node->user.long_name) > 0) {
|
||||
name = node->user.long_name;
|
||||
} else {
|
||||
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
|
||||
}
|
||||
} else {
|
||||
if (node->has_user && strlen(node->user.short_name) > 0) {
|
||||
name = node->user.short_name;
|
||||
} else {
|
||||
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
|
||||
}
|
||||
static char nodeName[25]; // single static buffer we return
|
||||
nodeName[0] = '\0';
|
||||
|
||||
auto writeFallbackId = [&] {
|
||||
std::snprintf(nodeName, sizeof(nodeName), "(%04X)", static_cast<uint16_t>(node ? (node->num & 0xFFFF) : 0));
|
||||
};
|
||||
|
||||
// 1) Choose target candidate (long vs short) only if present
|
||||
const char *raw = nullptr;
|
||||
if (node && node->has_user) {
|
||||
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
|
||||
}
|
||||
|
||||
// Use sanitizeString() function and copy directly into nodeName
|
||||
std::string sanitized_name = sanitizeString(name ? name : "");
|
||||
// 2) Sanitize (empty if raw is null/empty)
|
||||
std::string s = (raw && *raw) ? sanitizeString(raw) : std::string{};
|
||||
|
||||
if (!sanitized_name.empty()) {
|
||||
strncpy(nodeName, sanitized_name.c_str(), sizeof(nodeName) - 1);
|
||||
nodeName[sizeof(nodeName) - 1] = '\0';
|
||||
// 3) Fallback if sanitize yields empty; otherwise copy safely (truncate if needed)
|
||||
if (s.empty() || s == "¿" || s.find_first_not_of("¿") == std::string::npos) {
|
||||
writeFallbackId();
|
||||
} else {
|
||||
snprintf(nodeName, sizeof(nodeName), "(%04X)", (uint16_t)(node->num & 0xFFFF));
|
||||
// %.*s ensures null-termination and safe truncation to buffer size - 1
|
||||
std::snprintf(nodeName, sizeof(nodeName), "%.*s", static_cast<int>(sizeof(nodeName) - 1), s.c_str());
|
||||
}
|
||||
|
||||
if (config.display.use_long_node_name == true) {
|
||||
int availWidth = (SCREEN_WIDTH / 2) - 65;
|
||||
// 4) Width-based truncation + ellipsis (long-name mode only)
|
||||
if (config.display.use_long_node_name && display) {
|
||||
int availWidth = columnWidth - ((currentResolution == ScreenResolution::High) ? 65 : 38);
|
||||
if (availWidth < 0)
|
||||
availWidth = 0;
|
||||
|
||||
size_t origLen = strlen(nodeName);
|
||||
while (nodeName[0] && display->getStringWidth(nodeName) > availWidth) {
|
||||
nodeName[strlen(nodeName) - 1] = '\0';
|
||||
const size_t beforeLen = std::strlen(nodeName);
|
||||
|
||||
// Trim from the end until it fits or is empty
|
||||
size_t len = beforeLen;
|
||||
while (len && display->getStringWidth(nodeName) > availWidth) {
|
||||
nodeName[--len] = '\0';
|
||||
}
|
||||
|
||||
// If we actually truncated, append "..." (ensure space remains in buffer)
|
||||
if (strlen(nodeName) < origLen) {
|
||||
size_t len = strlen(nodeName);
|
||||
size_t maxLen = sizeof(nodeName) - 4; // 3 for "..." and 1 for '\0'
|
||||
if (len > maxLen) {
|
||||
nodeName[maxLen] = '\0';
|
||||
len = maxLen;
|
||||
// If truncated, append "..." (respect buffer size)
|
||||
if (len < beforeLen) {
|
||||
// Make sure there's room for "..." and '\0'
|
||||
const size_t capForText = sizeof(nodeName) - 1; // leaving space for '\0'
|
||||
const size_t needed = 3; // "..."
|
||||
if (len > capForText - needed) {
|
||||
len = capForText - needed;
|
||||
nodeName[len] = '\0';
|
||||
}
|
||||
strcat(nodeName, "...");
|
||||
std::strcat(nodeName, "...");
|
||||
}
|
||||
}
|
||||
|
||||
return nodeName;
|
||||
}
|
||||
|
||||
const char *getCurrentModeTitle(int screenWidth)
|
||||
const char *getCurrentModeTitle_Nodes(int screenWidth)
|
||||
{
|
||||
switch (currentMode) {
|
||||
switch (currentMode_Nodes) {
|
||||
case MODE_LAST_HEARD:
|
||||
return "Last Heard";
|
||||
case MODE_HOP_SIGNAL:
|
||||
#ifdef USE_EINK
|
||||
return "Hops/Sig";
|
||||
#else
|
||||
return (isHighResolution) ? "Hops/Signal" : "Hops/Sig";
|
||||
return (currentResolution == ScreenResolution::High) ? "Hops/Signal" : "Hops/Sig";
|
||||
#endif
|
||||
default:
|
||||
return "Nodes";
|
||||
}
|
||||
}
|
||||
|
||||
const char *getCurrentModeTitle_Location(int screenWidth)
|
||||
{
|
||||
switch (currentMode_Location) {
|
||||
case MODE_DISTANCE:
|
||||
return "Distance";
|
||||
case MODE_BEARING:
|
||||
return "Bearings";
|
||||
default:
|
||||
return "Nodes";
|
||||
}
|
||||
@@ -137,10 +176,8 @@ int calculateMaxScroll(int totalEntries, int visibleRows)
|
||||
|
||||
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
|
||||
{
|
||||
int columnWidth = display->getWidth() / 2;
|
||||
int separatorX = x + columnWidth - 2;
|
||||
for (int y = yStart; y <= yEnd; y += 2) {
|
||||
display->setPixel(separatorX, y);
|
||||
display->setPixel(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +189,8 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
|
||||
int scrollbarX = display->getWidth() - 2;
|
||||
int scrollbarHeight = display->getHeight() - scrollStartY - 10;
|
||||
int thumbHeight = std::max(4, (scrollbarHeight * visibleNodeRows * columns) / totalEntries);
|
||||
int maxScroll = calculateMaxScroll(totalEntries, visibleNodeRows);
|
||||
int perPage = visibleNodeRows * columns;
|
||||
int maxScroll = std::max(0, (totalEntries - 1) / perPage);
|
||||
int thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll);
|
||||
|
||||
for (int i = 0; i < thumbHeight; i++) {
|
||||
@@ -167,9 +205,9 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
|
||||
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||
{
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
int timeOffset = (isHighResolution) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
|
||||
int timeOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
|
||||
|
||||
const char *nodeName = getSafeNodeName(display, node);
|
||||
const char *nodeName = getSafeNodeName(display, node, columnWidth);
|
||||
|
||||
char timeStr[10];
|
||||
uint32_t seconds = sinceLastSeen(node);
|
||||
@@ -188,9 +226,9 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
display->drawString(x + ((isHighResolution) ? 6 : 3), y, nodeName);
|
||||
display->drawString(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nodeName);
|
||||
if (node->is_favorite) {
|
||||
if (isHighResolution) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
} else {
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
@@ -209,19 +247,19 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
|
||||
int nameMaxWidth = columnWidth - 25;
|
||||
int barsOffset = (isHighResolution) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
|
||||
int hopOffset = (isHighResolution) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
|
||||
int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
|
||||
int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
|
||||
|
||||
int barsXOffset = columnWidth - barsOffset;
|
||||
|
||||
const char *nodeName = getSafeNodeName(display, node);
|
||||
const char *nodeName = getSafeNodeName(display, node, columnWidth);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
|
||||
display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
|
||||
display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName);
|
||||
if (node->is_favorite) {
|
||||
if (isHighResolution) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
} else {
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
@@ -256,9 +294,10 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||
{
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
||||
int nameMaxWidth =
|
||||
columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
||||
|
||||
const char *nodeName = getSafeNodeName(display, node);
|
||||
const char *nodeName = getSafeNodeName(display, node, columnWidth);
|
||||
char distStr[10] = "";
|
||||
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
@@ -311,9 +350,9 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
|
||||
display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName);
|
||||
if (node->is_favorite) {
|
||||
if (isHighResolution) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
} else {
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
@@ -321,26 +360,24 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
}
|
||||
|
||||
if (strlen(distStr) > 0) {
|
||||
int offset = (isHighResolution) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
|
||||
: (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
|
||||
int offset = (currentResolution == ScreenResolution::High)
|
||||
? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
|
||||
: (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
|
||||
int rightEdge = x + columnWidth - offset;
|
||||
int textWidth = display->getStringWidth(distStr);
|
||||
display->drawString(rightEdge - textWidth, y, distStr);
|
||||
}
|
||||
}
|
||||
|
||||
void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||
void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||
{
|
||||
switch (currentMode) {
|
||||
switch (currentMode_Nodes) {
|
||||
case MODE_LAST_HEARD:
|
||||
drawEntryLastHeard(display, node, x, y, columnWidth);
|
||||
break;
|
||||
case MODE_HOP_SIGNAL:
|
||||
drawEntryHopSignal(display, node, x, y, columnWidth);
|
||||
break;
|
||||
case MODE_DISTANCE:
|
||||
drawNodeDistance(display, node, x, y, columnWidth);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -351,15 +388,16 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
|
||||
// Adjust max text width depending on column and screen width
|
||||
int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
||||
int nameMaxWidth =
|
||||
columnWidth - ((currentResolution == ScreenResolution::High) ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
||||
|
||||
const char *nodeName = getSafeNodeName(display, node);
|
||||
const char *nodeName = getSafeNodeName(display, node, columnWidth);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
|
||||
display->drawStringMaxWidth(x + ((currentResolution == ScreenResolution::High) ? 6 : 3), y, nameMaxWidth, nodeName);
|
||||
if (node->is_favorite) {
|
||||
if (isHighResolution) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||
} else {
|
||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||
@@ -374,7 +412,7 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
return;
|
||||
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
int arrowXOffset = (isHighResolution) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18);
|
||||
int arrowXOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18);
|
||||
|
||||
int centerX = x + columnWidth - arrowXOffset;
|
||||
int centerY = y + FONT_HEIGHT_SMALL / 2;
|
||||
@@ -431,11 +469,6 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
locationScreen = true;
|
||||
else if (strcmp(title, "Distance") == 0)
|
||||
locationScreen = true;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
int columnWidth = display->getWidth();
|
||||
#else
|
||||
int columnWidth = display->getWidth() / 2;
|
||||
#endif
|
||||
display->clear();
|
||||
|
||||
// Draw the battery/time header
|
||||
@@ -444,39 +477,74 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
// Space below header
|
||||
y += COMMON_HEADER_HEIGHT;
|
||||
|
||||
int totalColumns = 1; // Default to 1 column
|
||||
|
||||
if (config.display.use_long_node_name) {
|
||||
if (SCREEN_WIDTH <= 240) {
|
||||
totalColumns = 1;
|
||||
} else if (SCREEN_WIDTH > 240) {
|
||||
totalColumns = 2;
|
||||
}
|
||||
} else {
|
||||
if (SCREEN_WIDTH <= 64) {
|
||||
totalColumns = 1;
|
||||
} else if (SCREEN_WIDTH > 64 && SCREEN_WIDTH <= 240) {
|
||||
totalColumns = 2;
|
||||
} else {
|
||||
totalColumns = 3;
|
||||
}
|
||||
}
|
||||
|
||||
int columnWidth = display->getWidth() / totalColumns;
|
||||
|
||||
int totalEntries = nodeDB->getNumMeshNodes();
|
||||
int totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
|
||||
int numskipped = 0;
|
||||
int visibleNodeRows = totalRowsAvailable;
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
int totalColumns = 1;
|
||||
#else
|
||||
int totalColumns = 2;
|
||||
#endif
|
||||
int startIndex = scrollIndex * visibleNodeRows * totalColumns;
|
||||
if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) {
|
||||
startIndex++; // skip own node
|
||||
}
|
||||
int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries);
|
||||
|
||||
// Build filtered + ordered list
|
||||
std::vector<int> drawList;
|
||||
drawList.reserve(totalEntries);
|
||||
for (int i = 0; i < totalEntries; i++) {
|
||||
auto *n = nodeDB->getMeshNodeByIndex(i);
|
||||
|
||||
if (!n)
|
||||
continue;
|
||||
if (n->num == nodeDB->getNodeNum())
|
||||
continue;
|
||||
if (locationScreen && !n->has_position)
|
||||
continue;
|
||||
|
||||
drawList.push_back(n->num);
|
||||
}
|
||||
totalEntries = drawList.size();
|
||||
int perPage = visibleNodeRows * totalColumns;
|
||||
|
||||
int maxScroll = 0;
|
||||
if (perPage > 0) {
|
||||
maxScroll = std::max(0, (totalEntries - 1) / perPage);
|
||||
}
|
||||
|
||||
if (scrollIndex > maxScroll)
|
||||
scrollIndex = maxScroll;
|
||||
int startIndex = scrollIndex * visibleNodeRows * totalColumns;
|
||||
int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries);
|
||||
int yOffset = 0;
|
||||
int col = 0;
|
||||
int lastNodeY = y;
|
||||
int shownCount = 0;
|
||||
int rowCount = 0;
|
||||
|
||||
for (int i = startIndex; i < endIndex; ++i) {
|
||||
if (locationScreen && !nodeDB->getMeshNodeByIndex(i)->has_position) {
|
||||
numskipped++;
|
||||
continue;
|
||||
}
|
||||
for (int idx = startIndex; idx < endIndex; idx++) {
|
||||
uint32_t nodeNum = drawList[idx];
|
||||
auto *node = nodeDB->getMeshNode(nodeNum);
|
||||
int xPos = x + (col * columnWidth);
|
||||
int yPos = y + yOffset;
|
||||
renderer(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth);
|
||||
|
||||
if (extras) {
|
||||
extras(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth, heading, lat, lon);
|
||||
}
|
||||
renderer(display, node, xPos, yPos, columnWidth);
|
||||
|
||||
if (extras)
|
||||
extras(display, node, xPos, yPos, columnWidth, heading, lat, lon);
|
||||
|
||||
lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
|
||||
yOffset += rowYOffset;
|
||||
@@ -495,17 +563,73 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
// This should correct the scrollbar
|
||||
totalEntries -= numskipped;
|
||||
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
// Draw column separator
|
||||
if (shownCount > 0) {
|
||||
if (currentResolution != ScreenResolution::UltraLow && shownCount > 0) {
|
||||
const int firstNodeY = y + 3;
|
||||
drawColumnSeparator(display, x, firstNodeY, lastNodeY);
|
||||
for (int horizontal_offset = 1; horizontal_offset < totalColumns; horizontal_offset++) {
|
||||
drawColumnSeparator(display, columnWidth * horizontal_offset, firstNodeY, lastNodeY);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
const int scrollStartY = y + 3;
|
||||
drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY);
|
||||
drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, totalColumns, scrollStartY);
|
||||
graphics::drawCommonFooter(display, x, y);
|
||||
|
||||
// Scroll Popup Overlay
|
||||
if (millis() - popupTime < POPUP_DURATION_MS) {
|
||||
popupTotal = totalEntries;
|
||||
|
||||
int perPage = visibleNodeRows * totalColumns;
|
||||
|
||||
popupStart = startIndex + 1;
|
||||
popupEnd = std::min(startIndex + perPage, totalEntries);
|
||||
|
||||
popupPage = (scrollIndex + 1);
|
||||
popupMaxPage = std::max(1, (totalEntries + perPage - 1) / perPage);
|
||||
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%d-%d/%d Pg %d/%d", popupStart, popupEnd, popupTotal, popupPage, popupMaxPage);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
// Box padding
|
||||
int padding = 2;
|
||||
int textW = display->getStringWidth(buf);
|
||||
int textH = FONT_HEIGHT_SMALL;
|
||||
int boxWidth = textW + padding * 3;
|
||||
int boxHeight = textH + padding * 2;
|
||||
|
||||
// Center of usable screen area:
|
||||
int headerHeight = FONT_HEIGHT_SMALL - 1;
|
||||
int footerHeight = FONT_HEIGHT_SMALL + 2;
|
||||
|
||||
int usableTop = headerHeight;
|
||||
int usableBottom = display->getHeight() - footerHeight;
|
||||
int usableHeight = usableBottom - usableTop;
|
||||
|
||||
// Center point inside usable area
|
||||
int boxLeft = (display->getWidth() - boxWidth) / 2;
|
||||
int boxTop = usableTop + (usableHeight - boxHeight) / 2;
|
||||
|
||||
// Draw Box
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
|
||||
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
|
||||
display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1);
|
||||
display->fillRect(boxLeft - 2, boxTop, 1, boxHeight);
|
||||
display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
|
||||
display->setColor(WHITE);
|
||||
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(boxLeft, boxTop, 1, 1);
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
|
||||
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
|
||||
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
|
||||
display->setColor(WHITE);
|
||||
|
||||
// Text
|
||||
display->drawString(boxLeft + padding, boxTop + padding, buf);
|
||||
}
|
||||
}
|
||||
|
||||
// =============================
|
||||
@@ -513,10 +637,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
// =============================
|
||||
|
||||
#ifndef USE_EINK
|
||||
void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
// Node list for Last Heard and Hop Signal views
|
||||
void drawDynamicListScreen_Nodes(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
// Static variables to track mode and duration
|
||||
static NodeListMode lastRenderedMode = MODE_COUNT;
|
||||
static ListMode_Node lastRenderedMode = MODE_COUNT_NODE;
|
||||
static unsigned long modeStartTime = 0;
|
||||
|
||||
unsigned long now = millis();
|
||||
@@ -529,23 +654,65 @@ void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
}
|
||||
#endif
|
||||
// On very first call (on boot or state enter)
|
||||
if (lastRenderedMode == MODE_COUNT) {
|
||||
currentMode = MODE_LAST_HEARD;
|
||||
if (lastRenderedMode == MODE_COUNT_NODE) {
|
||||
currentMode_Nodes = MODE_LAST_HEARD;
|
||||
modeStartTime = now;
|
||||
}
|
||||
|
||||
// Time to switch to next mode?
|
||||
if (now - modeStartTime >= getModeCycleIntervalMs()) {
|
||||
currentMode = static_cast<NodeListMode>((currentMode + 1) % MODE_COUNT);
|
||||
currentMode_Nodes = static_cast<ListMode_Node>((currentMode_Nodes + 1) % MODE_COUNT_NODE);
|
||||
modeStartTime = now;
|
||||
}
|
||||
|
||||
// Render screen based on currentMode
|
||||
const char *title = getCurrentModeTitle(display->getWidth());
|
||||
drawNodeListScreen(display, state, x, y, title, drawEntryDynamic);
|
||||
const char *title = getCurrentModeTitle_Nodes(display->getWidth());
|
||||
drawNodeListScreen(display, state, x, y, title, drawEntryDynamic_Nodes);
|
||||
|
||||
// Track the last mode to avoid reinitializing modeStartTime
|
||||
lastRenderedMode = currentMode;
|
||||
lastRenderedMode = currentMode_Nodes;
|
||||
}
|
||||
|
||||
// Node list for Distance and Bearings views
|
||||
void drawDynamicListScreen_Location(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
// Static variables to track mode and duration
|
||||
static ListMode_Location lastRenderedMode = MODE_COUNT_LOCATION;
|
||||
static unsigned long modeStartTime = 0;
|
||||
|
||||
unsigned long now = millis();
|
||||
|
||||
#if defined(M5STACK_UNITC6L)
|
||||
display->clear();
|
||||
if (now - lastSwitchTime >= 3000) {
|
||||
display->display();
|
||||
lastSwitchTime = now;
|
||||
}
|
||||
#endif
|
||||
// On very first call (on boot or state enter)
|
||||
if (lastRenderedMode == MODE_COUNT_LOCATION) {
|
||||
currentMode_Location = MODE_DISTANCE;
|
||||
modeStartTime = now;
|
||||
}
|
||||
|
||||
// Time to switch to next mode?
|
||||
if (now - modeStartTime >= getModeCycleIntervalMs()) {
|
||||
currentMode_Location = static_cast<ListMode_Location>((currentMode_Location + 1) % MODE_COUNT_LOCATION);
|
||||
modeStartTime = now;
|
||||
}
|
||||
|
||||
// Render screen based on currentMode
|
||||
const char *title = getCurrentModeTitle_Location(display->getWidth());
|
||||
|
||||
// Render screen based on currentMode_Location
|
||||
if (currentMode_Location == MODE_DISTANCE) {
|
||||
drawNodeListScreen(display, state, x, y, title, drawNodeDistance);
|
||||
} else if (currentMode_Location == MODE_BEARING) {
|
||||
drawNodeListWithCompasses(display, state, x, y);
|
||||
}
|
||||
|
||||
// Track the last mode to avoid reinitializing modeStartTime
|
||||
lastRenderedMode = currentMode_Location;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -566,14 +733,12 @@ void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
|
||||
#endif
|
||||
drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal);
|
||||
}
|
||||
|
||||
void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
const char *title = "Distance";
|
||||
drawNodeListScreen(display, state, x, y, title, drawNodeDistance);
|
||||
}
|
||||
#endif
|
||||
|
||||
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
float heading = 0;
|
||||
|
||||
Reference in New Issue
Block a user