From 1dedd291fb4028557e99ba686c832f67ab2dcd6e Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:39:15 -0400 Subject: [PATCH 001/265] New Screens Introducing Default screen Nodelist (Last head nodes) Distance Screen Compass screen Hops and Signal Screen Improved node receipient list using navigation bar and search bar for canned messages --- src/graphics/Screen.cpp | 539 +++++++++++++++++++++++++++- src/modules/CannedMessageModule.cpp | 358 ++++++++++++++++-- src/modules/CannedMessageModule.h | 19 +- 3 files changed, 872 insertions(+), 44 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 0c18f3287..aa56f5c17 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1130,11 +1130,27 @@ static void drawGPS(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus display->drawString(x + 1, y - 2, "No GPS"); return; } + // Adjust position if we’re going to draw too wide + int maxDrawWidth = 6; // Position icon + + if (!gps->getHasLock()) { + maxDrawWidth += display->getStringWidth("No sats") + 2; // icon + text + buffer + } else { + maxDrawWidth += (5 * 2) + 8 + display->getStringWidth("99") + 2; // bars + sat icon + text + buffer + } + + if (x + maxDrawWidth > SCREEN_WIDTH) { + x = SCREEN_WIDTH - maxDrawWidth; + if (x < 0) x = 0; // Clamp to screen + } + display->drawFastImage(x, y, 6, 8, gps->getHasLock() ? imgPositionSolid : imgPositionEmpty); if (!gps->getHasLock()) { - display->drawString(x + 8, y - 2, "No sats"); + // Draw "No sats" to the right of the icon with slightly more gap + int textX = x + 9; // 6 (icon) + 3px spacing + display->drawString(textX, y - 2, "No sats"); if (config.display.heading_bold) - display->drawString(x + 9, y - 2, "No sats"); + display->drawString(textX + 1, y - 2, "No sats"); return; } else { char satsString[3]; @@ -1146,7 +1162,7 @@ static void drawGPS(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus bar[0] = ~((1 << (5 - i)) - 1); else bar[0] = 0b10000000; - // bar[1] = bar[0]; + display->drawFastImage(x + 9 + (i * 2), y, 2, 8, bar); } @@ -1155,12 +1171,14 @@ static void drawGPS(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus // Draw the number of satellites snprintf(satsString, sizeof(satsString), "%u", gps->getNumSatellites()); - display->drawString(x + 34, y - 2, satsString); + int textX = x + 34; + display->drawString(textX, y - 2, satsString); if (config.display.heading_bold) - display->drawString(x + 35, y - 2, satsString); + display->drawString(textX + 1, y - 2, satsString); } } + // Draw status when GPS is disabled or not present static void drawGPSpowerstat(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) { @@ -1294,6 +1312,11 @@ static int8_t prevFrame = -1; // Draw the arrow pointing to a node's location void Screen::drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) { + Serial.print("πŸ”„ [Node Heading] Raw Bearing (rad): "); + Serial.print(headingRadian); + Serial.print(" | (deg): "); + Serial.println(headingRadian * RAD_TO_DEG); + Point tip(0.0f, 0.5f), tail(0.0f, -0.35f); // pointing up initially float arrowOffsetX = 0.14f, arrowOffsetY = 1.0f; Point leftArrow(tip.x - arrowOffsetX, tip.y - arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y - arrowOffsetY); @@ -1312,6 +1335,10 @@ void Screen::drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t com display->drawLine(leftArrow.x, leftArrow.y, tail.x, tail.y); display->drawLine(rightArrow.x, rightArrow.y, tail.x, tail.y); */ + Serial.print("πŸ”₯ Arrow Tail X: "); Serial.print(tail.x); + Serial.print(" | Y: "); Serial.print(tail.y); + Serial.print(" | Tip X: "); Serial.print(tip.x); + Serial.print(" | Tip Y: "); Serial.println(tip.y); #ifdef USE_EINK display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); #else @@ -1352,6 +1379,9 @@ void Screen::getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength) void Screen::drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading) { + Serial.print("🧭 [Main Compass] Raw Heading (deg): "); + Serial.println(myHeading * RAD_TO_DEG); + // If north is supposed to be at the top of the compass we want rotation to be +0 if (config.display.compass_north_top) myHeading = -0; @@ -1369,13 +1399,10 @@ void Screen::drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t co rosePoints[i]->scale(compassDiam); rosePoints[i]->translate(compassX, compassY); } - - /* changed the N sign to a small circle on the compass circle. - display->drawLine(N1.x, N1.y, N3.x, N3.y); - display->drawLine(N2.x, N2.y, N4.x, N4.y); - display->drawLine(N1.x, N1.y, N4.x, N4.y); - */ display->drawCircle(NC1.x, NC1.y, 4); // North sign circle, 4px radius is sufficient for all displays. + Serial.print("πŸ”₯ North Marker X: "); Serial.print(NC1.x); + Serial.print(" | Y: "); Serial.println(NC1.y); + } uint16_t Screen::getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) @@ -1521,6 +1548,476 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ // Must be after distStr is populated screen->drawColumns(display, x, y, fields); } +void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r) { + // Center rectangles + display->fillRect(x + r, y, w - 2 * r, h); + display->fillRect(x, y + r, r, h - 2 * r); + display->fillRect(x + w - r, y + r, r, h - 2 * r); + + // Rounded corners + display->fillCircle(x + r, y + r, r); // Top-left + display->fillCircle(x + w - r - 1, y + r, r); // Top-right + display->fillCircle(x + r, y + h - r - 1, r); // Bottom-left + display->fillCircle(x + w - r - 1, y + h - r - 1, r); // Bottom-right +} +// Each node entry holds a reference to its info and how long ago it was heard from +struct NodeEntry { + meshtastic_NodeInfoLite *node; + uint32_t lastHeard; +}; + +// Calculates bearing between two lat/lon points (used for compass) +float calculateBearing(double lat1, double lon1, double lat2, double lon2) { + double dLon = (lon2 - lon1) * DEG_TO_RAD; + lat1 = lat1 * DEG_TO_RAD; + lat2 = lat2 * DEG_TO_RAD; + + double y = sin(dLon) * cos(lat2); + double x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon); + double initialBearing = atan2(y, x); + + return fmod((initialBearing * RAD_TO_DEG + 360), 360); // Normalize to 0-360Β° +} + +// Grabs all nodes from the DB and sorts them (favorites and most recently heard first) +void retrieveAndSortNodes(std::vector &nodeList) { + size_t numNodes = nodeDB->getNumMeshNodes(); + + for (size_t i = 0; i < numNodes; i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + if (!node || node->num == nodeDB->getNodeNum()) continue; // Skip self + nodeList.push_back({node, sinceLastSeen(node)}); + } + + std::sort(nodeList.begin(), nodeList.end(), [](const NodeEntry &a, const NodeEntry &b) { + bool aFav = a.node->is_favorite; + bool bFav = b.node->is_favorite; + if (aFav != bFav) return aFav > bFav; + if (a.lastHeard == 0 || a.lastHeard == UINT32_MAX) return false; + if (b.lastHeard == 0 || b.lastHeard == UINT32_MAX) return true; + return a.lastHeard < b.lastHeard; + }); +} + +// Helper: Fallback-NodeID if emote is on ShortName for display purposes +String getSafeNodeName(meshtastic_NodeInfoLite *node) { + String nodeName = "?"; + + if (node->has_user && strlen(node->user.short_name) > 0) { + bool valid = true; + const char* name = node->user.short_name; + + for (size_t i = 0; i < strlen(name); i++) { + uint8_t c = (uint8_t)name[i]; + if (c < 32 || c > 126) { + valid = false; + break; + } + } + + if (valid) { + nodeName = name; + } else { + // fallback: last 4 hex digits of node ID, no prefix + char idStr[6]; + snprintf(idStr, sizeof(idStr), "%04X", (uint16_t)(node->num & 0xFFFF)); + nodeName = String(idStr); + } + } + + if (node->is_favorite) nodeName = "*" + nodeName; + return nodeName; +} + +// Draws the top header bar (optionally inverted or bold) +void drawScreenHeader(OLEDDisplay *display, const char *title, int16_t x, int16_t y) { + bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); + bool isBold = config.display.heading_bold; + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + int screenWidth = display->getWidth(); + int textWidth = display->getStringWidth(title); + int titleX = (screenWidth - textWidth) / 2; // Centered X position + + if (isInverted) { + drawRoundedHighlight(display, 0, y, screenWidth, FONT_HEIGHT_SMALL - 2, 2); // Full width from 0 + display->setColor(BLACK); + } + // Fake bold by drawing again with slight offset + display->drawString(titleX, y, title); + if (isBold) display->drawString(titleX + 1, y, title); + + display->setColor(WHITE); +} + +// Draws separator line +void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) { + int columnWidth = display->getWidth() / 2; + int separatorX = x + columnWidth - 2; + display->drawLine(separatorX, yStart, separatorX, yEnd - 3); +} + +// Draws node name with how long ago it was last heard from +void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { + int screenWidth = display->getWidth(); + bool isLeftCol = (x < screenWidth / 2); + + // Adjust offset based on column and screen width + int timeOffset = (screenWidth > 128) ? (isLeftCol ? 41 : 45) : (isLeftCol ? 24 : 30);//offset large screen (?Left:Right column), offset small screen (?Left:Right column) + + String nodeName = getSafeNodeName(node); + + char timeStr[10]; + uint32_t seconds = sinceLastSeen(node); + if (seconds == 0 || seconds == UINT32_MAX) { + snprintf(timeStr, sizeof(timeStr), "? "); + } else { + uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; + snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"), + (days ? days : hours ? hours : minutes), (days ? 'd' : hours ? 'h' : 'm')); + } + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawString(x, y, nodeName); + display->drawString(x + columnWidth - timeOffset, y, timeStr); +} +// Draws each node's name, hop count, and signal bars +void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { + int screenWidth = display->getWidth(); + bool isLeftCol = (x < screenWidth / 2); + + int nameMaxWidth = columnWidth - 25; + int barsOffset = (screenWidth > 128) ? (isLeftCol ? 26 : 30) : (isLeftCol ? 17 : 19);//offset large screen (?Left:Right column), offset small screen (?Left:Right column) + int hopOffset = (screenWidth > 128) ? (isLeftCol ? 32 : 38) : (isLeftCol ? 18 : 20);//offset large screen (?Left:Right column), offset small screen (?Left:Right column) + + int barsXOffset = columnWidth - barsOffset; + + String nodeName = getSafeNodeName(node); + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawStringMaxWidth(x, y, nameMaxWidth, nodeName); + + char hopStr[6] = ""; + if (node->has_hops_away && node->hops_away > 0) + snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away); + + if (hopStr[0] != '\0') { + int hopX = x + columnWidth - hopOffset - display->getStringWidth(hopStr); + display->drawString(hopX, y, hopStr); + } + + // Signal bars based on SNR + int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; + int barWidth = 2; + int barStartX = x + barsXOffset; + int barStartY = y + (FONT_HEIGHT_SMALL / 2) + 2; + + for (int b = 0; b < 4; b++) { + if (b < bars) { + int height = 2 + (b * 2); + display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height); + } + } +} + +// Typedef for passing different render functions into one reusable screen function +typedef void (*EntryRenderer)(OLEDDisplay*, meshtastic_NodeInfoLite*, int16_t, int16_t, int); + +// Shared function that renders all node screens (LastHeard, Hop/Signal) +void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer) { + int columnWidth = display->getWidth() / 2; + int yOffset = FONT_HEIGHT_SMALL - 3; + int col = 0, lastNodeY = y; + + display->clear(); + drawScreenHeader(display, title, x, y); + + std::vector nodeList; + retrieveAndSortNodes(nodeList); + + for (const auto &entry : nodeList) { + int xPos = x + (col * columnWidth); + renderer(display, entry.node, xPos, y + yOffset, columnWidth); + lastNodeY = std::max(lastNodeY, y + yOffset + FONT_HEIGHT_SMALL); + yOffset += FONT_HEIGHT_SMALL - 3; + + if (y + yOffset > display->getHeight() - FONT_HEIGHT_SMALL) { + yOffset = FONT_HEIGHT_SMALL - 3; + col++; + if (col > 1) break; + } + } + // Draw separator between columns + drawColumnSeparator(display, x, y + FONT_HEIGHT_SMALL - 2, lastNodeY); +} + +// Public screen function: shows how recently nodes were heard +static void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + drawNodeListScreen(display, state, x, y, "Node List", drawEntryLastHeard); +} + +// Public screen function: shows hop count + signal strength +static void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + drawNodeListScreen(display, state, x, y, "Hops/Signal", drawEntryHopSignal); +} + + + + + +// Helper function: Draw a single node entry for Node List (Modified for Compass Screen) +void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { + int screenWidth = display->getWidth(); + bool isLeftCol = (x < screenWidth / 2); + + // Adjust max text width depending on column and screen width + int nameMaxWidth = columnWidth - (screenWidth > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + + String nodeName = getSafeNodeName(node); + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawStringMaxWidth(x, y, nameMaxWidth, nodeName); +} + +// Extra compass element drawer (injects compass arrows) +typedef void (*CompassExtraRenderer)(OLEDDisplay*, meshtastic_NodeInfoLite*, int16_t, int16_t, int columnWidth, float myHeading, double userLat, double userLon); + +void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, double userLat, double userLon) { + if (!nodeDB->hasValidPosition(node)) return; + + int screenWidth = display->getWidth(); + bool isLeftCol = (x < screenWidth / 2); + + double nodeLat = node->position.latitude_i * 1e-7; + double nodeLon = node->position.longitude_i * 1e-7; + float bearingToNode = calculateBearing(userLat, userLon, nodeLat, nodeLon); + float relativeBearing = fmod((bearingToNode - myHeading + 360), 360); + float arrowAngle = relativeBearing * DEG_TO_RAD; + + // Adaptive offset for compass icon based on screen width + column + int arrowXOffset = (screenWidth > 128) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); + + int compassX = x + columnWidth - arrowXOffset; + int compassY = y + FONT_HEIGHT_SMALL / 2; + int size = FONT_HEIGHT_SMALL / 2 - 2; + int arrowLength = size - 2; + + int xEnd = compassX + arrowLength * cos(arrowAngle); + int yEnd = compassY - arrowLength * sin(arrowAngle); + + display->fillCircle(compassX, compassY, size); + display->drawCircle(compassX, compassY, size); + display->drawLine(compassX, compassY, xEnd, yEnd); +} + +// Generic node+compass renderer (like drawNodeListScreen but with compass support) +void drawNodeListWithExtrasScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, + EntryRenderer renderer, CompassExtraRenderer extras) { + int columnWidth = display->getWidth() / 2; + int yOffset = FONT_HEIGHT_SMALL - 3; + int col = 0, lastNodeY = y; + + display->clear(); + drawScreenHeader(display, title, x, y); + + std::vector nodeList; + retrieveAndSortNodes(nodeList); + + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + double userLat = 0.0, userLon = 0.0; + bool hasUserPosition = nodeDB->hasValidPosition(ourNode); + if (hasUserPosition) { + userLat = ourNode->position.latitude_i * 1e-7; + userLon = ourNode->position.longitude_i * 1e-7; + } + + float myHeading = screen->hasHeading() ? screen->getHeading() : 0.0f; + + for (const auto &entry : nodeList) { + int xPos = x + (col * columnWidth); + renderer(display, entry.node, xPos, y + yOffset, columnWidth); + + if (hasUserPosition && extras) { + extras(display, entry.node, xPos, y + yOffset, columnWidth, myHeading, userLat, userLon); + } + + lastNodeY = std::max(lastNodeY, y + yOffset + FONT_HEIGHT_SMALL); + yOffset += FONT_HEIGHT_SMALL - 3; + + if (y + yOffset > display->getHeight() - FONT_HEIGHT_SMALL) { + yOffset = FONT_HEIGHT_SMALL - 3; + col++; + if (col > 1) break; + } + } + + drawColumnSeparator(display, x, y + FONT_HEIGHT_SMALL - 2, lastNodeY); +} + +// Public screen entry for compass +static void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + drawNodeListWithExtrasScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow); +} + +void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { + int screenWidth = display->getWidth(); + bool isLeftCol = (x < screenWidth / 2); + int nameMaxWidth = columnWidth - (screenWidth > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + + String nodeName = getSafeNodeName(node); + char distStr[10] = ""; + + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + + if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { + double lat1 = ourNode->position.latitude_i * 1e-7; + double lon1 = ourNode->position.longitude_i * 1e-7; + double lat2 = node->position.latitude_i * 1e-7; + double lon2 = node->position.longitude_i * 1e-7; + + double earthRadiusKm = 6371.0; + double dLat = (lat2 - lat1) * DEG_TO_RAD; + double dLon = (lon2 - lon1) * DEG_TO_RAD; + + double a = sin(dLat / 2) * sin(dLat / 2) + + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * + sin(dLon / 2) * sin(dLon / 2); + double c = 2 * atan2(sqrt(a), sqrt(1 - a)); + double distanceKm = earthRadiusKm * c; + + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + double miles = distanceKm * 0.621371; + if (miles < 0.1) { + snprintf(distStr, sizeof(distStr), "%dft", (int)(miles * 5280)); // show feet + } else if (miles < 10.0) { + snprintf(distStr, sizeof(distStr), "%.1fmi", miles); // 1 decimal + } else { + snprintf(distStr, sizeof(distStr), "%dmi", (int)miles); // no decimal + } + } else { + if (distanceKm < 1.0) { + snprintf(distStr, sizeof(distStr), "%dm", (int)(distanceKm * 1000)); // show meters + } else if (distanceKm < 10.0) { + snprintf(distStr, sizeof(distStr), "%.1fkm", distanceKm); // 1 decimal + } else { + snprintf(distStr, sizeof(distStr), "%dkm", (int)distanceKm); // no decimal + } + } + } + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawStringMaxWidth(x, y, nameMaxWidth, nodeName); + + if (strlen(distStr) > 0) { + int offset = (screenWidth > 128) ? (isLeftCol ? 55 : 63) : (isLeftCol ? 32 : 37); + display->drawString(x + columnWidth - offset, y, distStr); + } +} + +static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + drawNodeListScreen(display, state, x, y, "Distances", drawNodeDistance); +} + + +static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); + bool isBold = config.display.heading_bold; + + const int xOffset = 3; // Padding for top row edges + + // Top row highlight (like drawScreenHeader) + if (isInverted) { + drawRoundedHighlight(display, 0, y, SCREEN_WIDTH, FONT_HEIGHT_SMALL - 2, 2); + display->setColor(BLACK); + } + + // Top row: Battery icon, %, Voltage + drawBattery(display, x + xOffset, y + 1, imgBattery, powerStatus); + + char percentStr[8]; + snprintf(percentStr, sizeof(percentStr), "%d%%", powerStatus->getBatteryChargePercent()); + int percentX = x + xOffset + 18; + display->drawString(percentX, y, percentStr); + + char voltStr[10]; + int batV = powerStatus->getBatteryVoltageMv() / 1000; + int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; + snprintf(voltStr, sizeof(voltStr), "%d.%02dV", batV, batCv); + int voltX = SCREEN_WIDTH - xOffset - display->getStringWidth(voltStr); + display->drawString(voltX, y, voltStr); + + // Bold only for header row + if (isBold) { + display->drawString(percentX + 1, y, percentStr); + display->drawString(voltX + 1, y, voltStr); + } + + display->setColor(WHITE); + + // === Temporarily disable bold for second row === + bool origBold = config.display.heading_bold; + config.display.heading_bold = false; + + // Second row: Node count and satellite info + int secondRowY = y + FONT_HEIGHT_SMALL + 1; + drawNodes(display, x, secondRowY, nodeStatus); + +#if HAS_GPS + if (config.position.fixed_position) { + drawGPS(display, SCREEN_WIDTH - 44, secondRowY, gpsStatus); + } else if (!gpsStatus || !gpsStatus->getIsConnected()) { + // Show fallback: either "GPS off" or "No GPS" + String displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + int posX = SCREEN_WIDTH - display->getStringWidth(displayLine) - 2; + display->drawString(posX, secondRowY, displayLine); + } else { + drawGPS(display, SCREEN_WIDTH - 44, secondRowY, gpsStatus); + } +#endif + + // Restore original bold setting + config.display.heading_bold = origBold; + + // Third row: Centered LongName + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { + const char* longName = ourNode->user.long_name; + int textWidth = display->getStringWidth(longName); + int nameX = (SCREEN_WIDTH - textWidth) / 2; + int nameY = y + (FONT_HEIGHT_SMALL + 1) * 2; + display->drawString(nameX, nameY, longName); + } + + // Fourth row: Centered uptime string (e.g. 1m, 2h, 3d) + uint32_t uptime = millis() / 1000; + char uptimeStr[6]; + uint32_t minutes = uptime / 60; + uint32_t hours = minutes / 60; + uint32_t days = hours / 24; + + if (days > 365) { + snprintf(uptimeStr, sizeof(uptimeStr), "?"); + } else { + snprintf(uptimeStr, sizeof(uptimeStr), "%d%c", + days ? days : hours ? hours : minutes ? minutes : (int)uptime, + days ? 'd' : hours ? 'h' : minutes ? 'm' : 's'); + } + + int uptimeY = y + (FONT_HEIGHT_SMALL + 1) * 3; + int uptimeX = (SCREEN_WIDTH - display->getStringWidth(uptimeStr)) / 2; + display->drawString(uptimeX, uptimeY, uptimeStr); +} + #if defined(ESP_PLATFORM) && defined(USE_ST7789) SPIClass SPI1(HSPI); @@ -2147,6 +2644,12 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = drawTextMessageFrame; } + normalFrames[numframes++] = drawDefaultScreen; + normalFrames[numframes++] = drawLastHeardScreen; + normalFrames[numframes++] = drawDistanceScreen; + normalFrames[numframes++] = drawNodeListWithCompasses; + normalFrames[numframes++] = drawHopSignalScreen; + // then all the nodes // We only show a few nodes in our scrolling list - because meshes with many nodes would have too many screens size_t numToShow = min(numMeshNodes, 4U); @@ -2664,14 +3167,19 @@ void DebugInfo::drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *stat // minutes %= 60; // hours %= 24; + // Show uptime as days, hours, minutes OR seconds + std::string uptime = screen->drawTimeDelta(days, hours, minutes, seconds); + + // Line 1 (Still) + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); + if (config.display.heading_bold) + display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); + display->setColor(WHITE); // Setup string to assemble analogClock string std::string analogClock = ""; - // Show uptime as days, hours, minutes OR seconds - std::string uptime = screen->drawTimeDelta(days, hours, minutes, seconds); - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone if (rtc_sec > 0) { long hms = rtc_sec % SEC_PER_DAY; @@ -2704,9 +3212,6 @@ void DebugInfo::drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *stat analogClock += timebuf; } - // Line 1 - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); - // Line 2 display->drawString(x, y + FONT_HEIGHT_SMALL * 1, analogClock.c_str()); diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 2a5ec00ab..1e83b3d1a 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -121,6 +121,73 @@ int CannedMessageModule::splitConfiguredMessages() return this->messagesCount; } +void CannedMessageModule::resetSearch() { + LOG_INFO("Resetting search, restoring full destination list"); + updateFilteredNodes(); // Reload all nodes and channels + requestFocus(); +} +void CannedMessageModule::updateFilteredNodes() { + static size_t lastNumMeshNodes = 0; // Track the last known node count + static String lastSearchQuery = ""; // Track last search query + + size_t numMeshNodes = nodeDB->getNumMeshNodes(); + + // If the number of nodes has changed, force an update + bool nodesChanged = (numMeshNodes != lastNumMeshNodes); + lastNumMeshNodes = numMeshNodes; + + // Also check if search query changed + if (searchQuery == lastSearchQuery && !nodesChanged) return; + + lastSearchQuery = searchQuery; + needsUpdate = false; + + this->filteredNodes.clear(); + this->activeChannelIndices.clear(); + + NodeNum myNodeNum = nodeDB->getNodeNum(); + + for (size_t i = 0; i < numMeshNodes; i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + if (!node || node->num == myNodeNum) continue; + + String nodeName = node->user.long_name; + String lowerNodeName = nodeName; + String lowerSearchQuery = searchQuery; + + lowerNodeName.toLowerCase(); + lowerSearchQuery.toLowerCase(); + + if (searchQuery.length() == 0 || lowerNodeName.indexOf(lowerSearchQuery) != -1) { + this->filteredNodes.push_back({node, sinceLastSeen(node)}); + } + } + + // Populate active channels + this->activeChannelIndices.clear(); + std::vector seenChannels; + for (uint8_t i = 0; i < channels.getNumChannels(); i++) { + String channelName = channels.getName(i); + if (channelName.length() > 0 && std::find(seenChannels.begin(), seenChannels.end(), channelName) == seenChannels.end()) { + this->activeChannelIndices.push_back(i); + seenChannels.push_back(channelName); + } + } + + // Sort nodes by favorite status and last seen time + std::sort(this->filteredNodes.begin(), this->filteredNodes.end(), [](const NodeEntry &a, const NodeEntry &b) { + if (a.node->is_favorite != b.node->is_favorite) { + return a.node->is_favorite > b.node->is_favorite; // Favorited nodes first + } + return a.lastHeard < b.lastHeard; // Otherwise, sort by last heard (oldest first) + }); + + // πŸ”Ή If nodes have changed, refresh the screen + if (nodesChanged) { + LOG_INFO("Nodes changed, forcing UI refresh."); + screen->forceDisplay(); + } +} int CannedMessageModule::handleInputEvent(const InputEvent *event) { @@ -136,17 +203,157 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) { return 0; // Ignore input while sending } - bool validEvent = false; - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP)) { - if (this->messagesCount > 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; - validEvent = true; + static int lastDestIndex = -1; // Cache the last index + bool selectionChanged = false; // Track if UI needs redrawing + + bool isUp = event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP); + bool isDown = event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); + + if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { + if (event->kbchar >= 32 && event->kbchar <= 126) { + this->searchQuery += event->kbchar; + return 0; } + + size_t numMeshNodes = this->filteredNodes.size(); + int totalEntries = numMeshNodes + this->activeChannelIndices.size(); + int columns = 2; + int totalRows = (totalEntries + columns - 1) / columns; + int maxScrollIndex = std::max(0, totalRows - this->visibleRows); + scrollIndex = std::max(0, std::min(scrollIndex, maxScrollIndex)); + + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK)) { + if (this->searchQuery.length() > 0) { + this->searchQuery.remove(this->searchQuery.length() - 1); + } + if (this->searchQuery.length() == 0) { + resetSearch(); // Function to restore all destinations + } + return 0; + } + + bool needsRedraw = false; + + // πŸ”Ό UP Navigation in Node Selection + if (isUp) { + if ((this->destIndex / columns) <= scrollIndex) { + if (scrollIndex > 0) { + scrollIndex--; + needsRedraw = true; + } + } else if (this->destIndex >= columns) { + this->destIndex -= columns; + } + } + + // πŸ”½ DOWN Navigation in Node Selection + if (isDown) { + if ((this->destIndex / columns) >= (scrollIndex + this->visibleRows - 1)) { + if (scrollIndex < maxScrollIndex) { + scrollIndex++; + needsRedraw = true; + } + } else if (this->destIndex + columns < totalEntries) { + this->destIndex += columns; + } + } + + + // β—€ LEFT Navigation (Wrap to previous row OR last row) + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) { + if (this->destIndex % columns == 0) { + if (this->destIndex >= columns) { + this->destIndex = this->destIndex - columns + (columns - 1); + } else { + int lastRowStart = ((totalEntries - 1) / columns) * columns; + this->destIndex = std::min(lastRowStart + (columns - 1), totalEntries - 1); + } + } else { + this->destIndex--; + } + } + + // β–Ά RIGHT Navigation (Wrap to next row OR first row) + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) { + int nextIndex = this->destIndex + 1; + if ((this->destIndex + 1) % columns == 0 || nextIndex >= totalEntries) { + if (this->destIndex + columns < totalEntries) { + this->destIndex = this->destIndex + columns - (columns - 1); + } else { + this->destIndex = 0; + } + } else { + this->destIndex++; + } + } + + if (this->destSelect != CANNED_MESSAGE_DESTINATION_TYPE_NODE) { + if (isUp && this->messagesCount > 0) { + this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; + return 0; + } + if (isDown && this->messagesCount > 0) { + this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; + return 0; + } + } + // Only refresh UI when needed + if (needsRedraw) { + screen->forceDisplay(); + } + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) { + if (this->destIndex < static_cast(this->activeChannelIndices.size())) { + this->dest = NODENUM_BROADCAST; + this->channel = this->activeChannelIndices[this->destIndex]; + } else { + int nodeIndex = this->destIndex - static_cast(this->activeChannelIndices.size()); + if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { + meshtastic_NodeInfoLite *selectedNode = this->filteredNodes[nodeIndex].node; + if (selectedNode) { + this->dest = selectedNode->num; + this->channel = selectedNode->channel; + } + } + } + + // βœ… Now correctly switches to FreeText screen with selected node/channel + this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; + screen->forceDisplay(); + return 0; + } + + // Handle Cancel (ESC) + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) { + this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; // Ensure return to main screen + this->searchQuery = ""; + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->notifyObservers(&e); + + screen->forceDisplay(); + return 0; // πŸš€ Prevents input from affecting canned messages + } + + return 0; // πŸš€ FINAL EARLY EXIT: Stops the function from continuing into canned message handling } - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN)) { - if (this->messagesCount > 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; - validEvent = true; + // If we reach here, we are NOT in Select Destination mode. + // The remaining logic is for canned message handling. + bool validEvent = false; + if (this->destSelect != CANNED_MESSAGE_DESTINATION_TYPE_NODE) { + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP)) { + if (this->messagesCount > 0) { + this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; + validEvent = true; + } + } + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN)) { + if (this->messagesCount > 0) { + this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; + validEvent = true; + } } } if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) { @@ -174,6 +381,15 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) } } if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) { + // If in Node Selection Mode, exit and return to FreeText Mode + if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { + updateFilteredNodes(); // Ensure the filtered node list is refreshed before selecting + this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; + this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + return 0; + } + + // Default behavior for Cancel in other modes UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen this->currentMessageIndex = -1; @@ -186,6 +402,8 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; this->notifyObservers(&e); + screen->forceDisplay(); // Ensure the UI updates properly + return 0; } if ((event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK)) || (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) || @@ -430,6 +648,7 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha int32_t CannedMessageModule::runOnce() { + updateFilteredNodes(); if (((!moduleConfig.canned_message.enabled) && !CANNED_MESSAGE_MODULE_ENABLE) || (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { temporaryMessage = ""; @@ -469,7 +688,7 @@ int32_t CannedMessageModule::runOnce() } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { if (this->freetext.length() > 0) { - sendText(this->dest, indexChannels[this->channel], this->freetext.c_str(), true); + sendText(this->dest, this->channel, this->freetext.c_str(), true); this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; } else { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; @@ -483,7 +702,7 @@ int32_t CannedMessageModule::runOnce() #if defined(USE_VIRTUAL_KEYBOARD) sendText(this->dest, indexChannels[this->channel], this->messages[this->currentMessageIndex], true); #else - sendText(NODENUM_BROADCAST, channels.getPrimaryIndex(), this->messages[this->currentMessageIndex], true); + sendText(this->dest, this->channel, this->messages[this->currentMessageIndex], true); #endif } this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; @@ -624,13 +843,23 @@ int32_t CannedMessageModule::runOnce() this->cursor--; } break; - case 0x09: // tab - if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL) { - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; - } else if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL; - } else { - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; + case 0x09: // Tab key (Switch to Destination Selection Mode) + { + if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) { + // Enter selection screen + this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; + this->destIndex = 0; // Reset to first node/channel + this->scrollIndex = 0; // Reset scrolling + this->runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + + // Ensure UI updates correctly + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->notifyObservers(&e); + } + + // If already inside the selection screen, do nothing (prevent exiting) + return 0; } break; case INPUT_BROKER_MSG_LEFT: @@ -986,7 +1215,10 @@ bool CannedMessageModule::interceptingKeyboardInput() #if !HAS_TFT void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + this->displayHeight = display->getHeight(); // Store display height for later use char buffer[50]; + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); if (temporaryMessage.length() != 0) { requestFocus(); // Tell Screen::setFrames to move to our module's frame @@ -997,6 +1229,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st } else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) { requestFocus(); // Tell Screen::setFrames to move to our module's frame EINK_ADD_FRAMEFLAG(display, COSMETIC); // Clean after this popup. Layout makes ghosting particularly obvious + display->setTextAlignment(TEXT_ALIGN_CENTER); #ifdef USE_EINK display->setFont(FONT_SMALL); // No chunky text @@ -1055,11 +1288,83 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); display->drawString(10 + x, 0 + y + FONT_HEIGHT_SMALL, "Canned Message\nModule disabled."); + } else if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { + requestFocus(); + updateFilteredNodes(); + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + int titleY = 2; + String titleText = "Select Destination"; + titleText += searchQuery.length() > 0 ? " [" + searchQuery + "]" : " [ ]"; + display->drawString(display->getWidth() / 2 - display->getStringWidth(titleText) / 2, titleY, titleText); + + int rowYOffset = titleY + FONT_HEIGHT_SMALL; // Adjusted for search box spacing + int numActiveChannels = this->activeChannelIndices.size(); + int totalEntries = numActiveChannels + this->filteredNodes.size(); + int columns = 2; + this->visibleRows = (display->getHeight() - (titleY + FONT_HEIGHT_SMALL)) / FONT_HEIGHT_SMALL; + if (this->visibleRows < 1) this->visibleRows = 1; + + // Ensure scrolling within bounds + if (scrollIndex > totalEntries / columns) scrollIndex = totalEntries / columns; + if (scrollIndex < 0) scrollIndex = 0; + + for (int row = 0; row < visibleRows; row++) { + int itemIndex = (scrollIndex + row) * columns; + for (int col = 0; col < columns; col++) { + if (itemIndex >= totalEntries) break; + + int xOffset = col * (display->getWidth() / columns); + int yOffset = row * FONT_HEIGHT_SMALL + rowYOffset; + String entryText; + + // Draw Channels First + if (itemIndex < numActiveChannels) { + uint8_t channelIndex = this->activeChannelIndices[itemIndex]; + entryText = String("@") + String(channels.getName(channelIndex)); + } + // Then Draw Nodes + else { + int nodeIndex = itemIndex - numActiveChannels; + if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { + meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; + entryText = node ? (node->is_favorite ? "* " + String(node->user.long_name) : String(node->user.long_name)) : "?"; + } + } + + // Prevent Empty Names + if (entryText.length() == 0 || entryText == "Unknown") entryText = "?"; + + // Trim if Too Long + while (display->getStringWidth(entryText + "-") > (display->getWidth() / columns - 4)) { + entryText = entryText.substring(0, entryText.length() - 1); + } + + // Highlight Selection + if (itemIndex == destIndex) { + display->fillRect(xOffset, yOffset, display->getStringWidth(entryText) + 4, FONT_HEIGHT_SMALL + 2); + display->setColor(BLACK); + } + display->drawString(xOffset + 2, yOffset, entryText); + display->setColor(WHITE); + itemIndex++; + } + } + if (totalEntries > visibleRows * columns) { + display->drawRect(display->getWidth() - 6, rowYOffset, 4, visibleRows * FONT_HEIGHT_SMALL); + int totalPages = (totalEntries + columns - 1) / columns; + int scrollHeight = (visibleRows * FONT_HEIGHT_SMALL * visibleRows) / (totalPages); + int scrollPos = rowYOffset + ((visibleRows * FONT_HEIGHT_SMALL) * scrollIndex) / totalPages; + display->fillRect(display->getWidth() - 6, scrollPos, 4, scrollHeight); + } + screen->forceDisplay(); } else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { requestFocus(); // Tell Screen::setFrames to move to our module's frame #if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) - EInkDynamicDisplay *einkDisplay = static_cast(display); - einkDisplay->enableUnlimitedFastMode(); // Enable unlimited fast refresh while typing + EInkDynamicDisplay* einkDisplay = static_cast(display); + einkDisplay->enableUnlimitedFastMode(); // Enable unlimited fast refresh while typing #endif #if defined(USE_VIRTUAL_KEYBOARD) @@ -1075,23 +1380,24 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st switch (this->destSelect) { case CANNED_MESSAGE_DESTINATION_TYPE_NODE: display->drawStringf(1 + x, 0 + y, buffer, "To: >%s<@%s", cannedMessageModule->getNodeName(this->dest), - channels.getName(indexChannels[this->channel])); + channels.getName(this->channel)); + LOG_INFO("Displaying recipient: Node=%s (ID=%d)", cannedMessageModule->getNodeName(this->dest), this->dest); display->drawStringf(0 + x, 0 + y, buffer, "To: >%s<@%s", cannedMessageModule->getNodeName(this->dest), - channels.getName(indexChannels[this->channel])); + channels.getName(this->channel)); break; case CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL: display->drawStringf(1 + x, 0 + y, buffer, "To: %s@>%s<", cannedMessageModule->getNodeName(this->dest), - channels.getName(indexChannels[this->channel])); + channels.getName(this->channel)); display->drawStringf(0 + x, 0 + y, buffer, "To: %s@>%s<", cannedMessageModule->getNodeName(this->dest), - channels.getName(indexChannels[this->channel])); + channels.getName(this->channel)); break; default: if (display->getWidth() > 128) { display->drawStringf(0 + x, 0 + y, buffer, "To: %s@%s", cannedMessageModule->getNodeName(this->dest), - channels.getName(indexChannels[this->channel])); + channels.getName(this->channel)); } else { display->drawStringf(0 + x, 0 + y, buffer, "To: %.5s@%.5s", cannedMessageModule->getNodeName(this->dest), - channels.getName(indexChannels[this->channel])); + channels.getName(this->channel)); } break; } diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index a91933a0f..f044d4e85 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -14,6 +14,7 @@ enum cannedMessageModuleRunState { CANNED_MESSAGE_RUN_STATE_ACTION_SELECT, CANNED_MESSAGE_RUN_STATE_ACTION_UP, CANNED_MESSAGE_RUN_STATE_ACTION_DOWN, + CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION }; enum cannedMessageDestinationType { @@ -43,10 +44,23 @@ struct Letter { #define CANNED_MESSAGE_MODULE_ENABLE 0 #endif +struct NodeEntry { + meshtastic_NodeInfoLite *node; + uint32_t lastHeard; +}; + class CannedMessageModule : public SinglePortModule, public Observable, private concurrency::OSThread { CallbackObserver inputObserver = CallbackObserver(this, &CannedMessageModule::handleInputEvent); + private: + int displayHeight = 64; // Default to a common value, update dynamically + int destIndex = 0; // Tracks currently selected node/channel in selection mode + int scrollIndex = 0; // Tracks scrolling position in node selection grid + int visibleRows = 0; + bool needsUpdate = true; + String searchQuery; + std::vector activeChannelIndices; public: CannedMessageModule(); @@ -65,7 +79,10 @@ class CannedMessageModule : public SinglePortModule, public Observable filteredNodes; + String nodeSelectionInput; String drawWithCursor(String text, int cursor); #ifdef RAK14014 From 7fce089540d92d6e39c2aa17a976350ff27f8463 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 28 Mar 2025 21:03:37 -0400 Subject: [PATCH 002/265] Support to enlarge battery image depending on screen size I adjusted the battery code to recognize the screen size and scale the battery based on that to fit on the top header. --- src/graphics/Screen.cpp | 43 ++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index aa56f5c17..ade9eab4e 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -424,21 +424,38 @@ static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *img { static const uint8_t powerBar[3] = {0x81, 0xBD, 0xBD}; static const uint8_t lightning[8] = {0xA1, 0xA1, 0xA5, 0xAD, 0xB5, 0xA5, 0x85, 0x85}; - // Clear the bar area on the battery image + + // Clear the bar area inside the battery image for (int i = 1; i < 14; i++) { imgBuffer[i] = 0x81; } - // If charging, draw a charging indicator + + // Fill with lightning or power bars if (powerStatus->getIsCharging()) { memcpy(imgBuffer + 3, lightning, 8); - // If not charging, Draw power bars } else { for (int i = 0; i < 4; i++) { if (powerStatus->getBatteryChargePercent() >= 25 * i) memcpy(imgBuffer + 1 + (i * 3), powerBar, 3); } } - display->drawFastImage(x, y, 16, 8, imgBuffer); + + // Slightly more conservative scaling based on screen width + int screenWidth = display->getWidth(); + int scale = 1; + + if (screenWidth >= 200) scale = 2; + if (screenWidth >= 300) scale = 3; + + // Draw scaled battery image (16 columns Γ— 8 rows) + for (int col = 0; col < 16; col++) { + uint8_t colBits = imgBuffer[col]; + for (int row = 0; row < 8; row++) { + if (colBits & (1 << row)) { + display->fillRect(x + col * scale, y + row * scale, scale, scale); + } + } + } } #if defined(DISPLAY_CLOCK_FRAME) @@ -1937,29 +1954,33 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i // Top row highlight (like drawScreenHeader) if (isInverted) { - drawRoundedHighlight(display, 0, y, SCREEN_WIDTH, FONT_HEIGHT_SMALL - 2, 2); + drawRoundedHighlight(display, 0, y, SCREEN_WIDTH, FONT_HEIGHT_SMALL - 1, 2); display->setColor(BLACK); } // Top row: Battery icon, %, Voltage - drawBattery(display, x + xOffset, y + 1, imgBattery, powerStatus); + int batteryYOffset = 2; // Adjust for vertical alignment + int textYOffset = 2; // Match text with battery height + drawBattery(display, x + xOffset, y + batteryYOffset, imgBattery, powerStatus); char percentStr[8]; snprintf(percentStr, sizeof(percentStr), "%d%%", powerStatus->getBatteryChargePercent()); - int percentX = x + xOffset + 18; - display->drawString(percentX, y, percentStr); + int screenWidth = display->getWidth(); + int batteryOffset = screenWidth > 128 ? 34 : 18; + int percentX = x + xOffset + batteryOffset; + display->drawString(percentX, y + textYOffset, percentStr); char voltStr[10]; int batV = powerStatus->getBatteryVoltageMv() / 1000; int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; snprintf(voltStr, sizeof(voltStr), "%d.%02dV", batV, batCv); int voltX = SCREEN_WIDTH - xOffset - display->getStringWidth(voltStr); - display->drawString(voltX, y, voltStr); + display->drawString(voltX, y + textYOffset, voltStr); // Bold only for header row if (isBold) { - display->drawString(percentX + 1, y, percentStr); - display->drawString(voltX + 1, y, voltStr); + display->drawString(percentX + 1, y + textYOffset, percentStr); + display->drawString(voltX + 1, y + textYOffset, voltStr); } display->setColor(WHITE); From 41c44353f71458d3517d0e0e91b883826877f8fa Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 28 Mar 2025 21:23:41 -0400 Subject: [PATCH 003/265] removed the 3x scale --- src/graphics/Screen.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index ade9eab4e..8642aaa5f 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -445,7 +445,7 @@ static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *img int scale = 1; if (screenWidth >= 200) scale = 2; - if (screenWidth >= 300) scale = 3; + if (screenWidth >= 300) scale = 2; // Do NOT go higher than 2 // Draw scaled battery image (16 columns Γ— 8 rows) for (int col = 0; col < 16; col++) { From c9e71173de455ec9b8aedd34255359226ffbc4da Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 29 Mar 2025 02:47:08 -0400 Subject: [PATCH 004/265] centered header contents while highlighted Instead of manually centeing the contents of the headers, it now calculates the center on its own, better for other screen resolutions --- src/graphics/Screen.cpp | 67 +++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 8642aaa5f..3c004b9e7 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1565,6 +1565,8 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ // Must be after distStr is populated screen->drawColumns(display, x, y, fields); } + +// h! Makes header invert rounder void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r) { // Center rectangles display->fillRect(x + r, y, w - 2 * r, h); @@ -1577,13 +1579,13 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, display->fillCircle(x + r, y + h - r - 1, r); // Bottom-left display->fillCircle(x + w - r - 1, y + h - r - 1, r); // Bottom-right } -// Each node entry holds a reference to its info and how long ago it was heard from +// h! Each node entry holds a reference to its info and how long ago it was heard from struct NodeEntry { meshtastic_NodeInfoLite *node; uint32_t lastHeard; }; -// Calculates bearing between two lat/lon points (used for compass) +// h! Calculates bearing between two lat/lon points (used for compass) float calculateBearing(double lat1, double lon1, double lat2, double lon2) { double dLon = (lon2 - lon1) * DEG_TO_RAD; lat1 = lat1 * DEG_TO_RAD; @@ -1656,15 +1658,22 @@ void drawScreenHeader(OLEDDisplay *display, const char *title, int16_t x, int16_ int screenWidth = display->getWidth(); int textWidth = display->getStringWidth(title); - int titleX = (screenWidth - textWidth) / 2; // Centered X position + int titleX = (screenWidth - textWidth) / 2; + + // Height of highlight row + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + + // Y offset to vertically center text in rounded bar + int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; if (isInverted) { - drawRoundedHighlight(display, 0, y, screenWidth, FONT_HEIGHT_SMALL - 2, 2); // Full width from 0 + drawRoundedHighlight(display, 0, y, screenWidth, highlightHeight, 2); display->setColor(BLACK); } - // Fake bold by drawing again with slight offset - display->drawString(titleX, y, title); - if (isBold) display->drawString(titleX + 1, y, title); + + // Draw text centered vertically and horizontally + display->drawString(titleX, textY, title); + if (isBold) display->drawString(titleX + 1, textY, title); display->setColor(WHITE); } @@ -1950,46 +1959,49 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); bool isBold = config.display.heading_bold; - const int xOffset = 3; // Padding for top row edges + const int xOffset = 3; + const int highlightHeight = FONT_HEIGHT_SMALL - 1; - // Top row highlight (like drawScreenHeader) + // Draw header background highlight if (isInverted) { - drawRoundedHighlight(display, 0, y, SCREEN_WIDTH, FONT_HEIGHT_SMALL - 1, 2); + drawRoundedHighlight(display, 0, y, SCREEN_WIDTH, highlightHeight, 2); display->setColor(BLACK); } - // Top row: Battery icon, %, Voltage - int batteryYOffset = 2; // Adjust for vertical alignment - int textYOffset = 2; // Match text with battery height - drawBattery(display, x + xOffset, y + batteryYOffset, imgBattery, powerStatus); + // === TOP ROW: Battery, %, Voltage === + int screenWidth = display->getWidth(); + + // Draw battery icon slightly inset from top + drawBattery(display, x + xOffset, y + 2, imgBattery, powerStatus); + + // Calculate vertical center for text (centered in header row) + int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; char percentStr[8]; snprintf(percentStr, sizeof(percentStr), "%d%%", powerStatus->getBatteryChargePercent()); - int screenWidth = display->getWidth(); + int batteryOffset = screenWidth > 128 ? 34 : 18; int percentX = x + xOffset + batteryOffset; - display->drawString(percentX, y + textYOffset, percentStr); + display->drawString(percentX, textY, percentStr); char voltStr[10]; int batV = powerStatus->getBatteryVoltageMv() / 1000; int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; snprintf(voltStr, sizeof(voltStr), "%d.%02dV", batV, batCv); int voltX = SCREEN_WIDTH - xOffset - display->getStringWidth(voltStr); - display->drawString(voltX, y + textYOffset, voltStr); + display->drawString(voltX, textY, voltStr); - // Bold only for header row if (isBold) { - display->drawString(percentX + 1, y + textYOffset, percentStr); - display->drawString(voltX + 1, y + textYOffset, voltStr); + display->drawString(percentX + 1, textY, percentStr); + display->drawString(voltX + 1, textY, voltStr); } display->setColor(WHITE); - // === Temporarily disable bold for second row === + // === Second Row: Node and GPS === bool origBold = config.display.heading_bold; config.display.heading_bold = false; - // Second row: Node count and satellite info int secondRowY = y + FONT_HEIGHT_SMALL + 1; drawNodes(display, x, secondRowY, nodeStatus); @@ -1997,7 +2009,6 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i if (config.position.fixed_position) { drawGPS(display, SCREEN_WIDTH - 44, secondRowY, gpsStatus); } else if (!gpsStatus || !gpsStatus->getIsConnected()) { - // Show fallback: either "GPS off" or "No GPS" String displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; int posX = SCREEN_WIDTH - display->getStringWidth(displayLine) - 2; display->drawString(posX, secondRowY, displayLine); @@ -2006,10 +2017,9 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i } #endif - // Restore original bold setting config.display.heading_bold = origBold; - // Third row: Centered LongName + // === Third Row: LongName Centered === meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { const char* longName = ourNode->user.long_name; @@ -2019,12 +2029,10 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i display->drawString(nameX, nameY, longName); } - // Fourth row: Centered uptime string (e.g. 1m, 2h, 3d) + // === Fourth Row: Uptime === uint32_t uptime = millis() / 1000; char uptimeStr[6]; - uint32_t minutes = uptime / 60; - uint32_t hours = minutes / 60; - uint32_t days = hours / 24; + uint32_t minutes = uptime / 60, hours = minutes / 60, days = hours / 24; if (days > 365) { snprintf(uptimeStr, sizeof(uptimeStr), "?"); @@ -2040,6 +2048,7 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i } + #if defined(ESP_PLATFORM) && defined(USE_ST7789) SPIClass SPI1(HSPI); #endif From bd20c742873377ed4c3532deabd68154a3691afd Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 29 Mar 2025 04:21:43 -0400 Subject: [PATCH 005/265] Added scrollbar code for node list screens --- src/graphics/Screen.cpp | 84 ++++++++++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 13 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 3c004b9e7..020a9b1d6 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1598,6 +1598,31 @@ float calculateBearing(double lat1, double lon1, double lat2, double lon2) { return fmod((initialBearing * RAD_TO_DEG + 360), 360); // Normalize to 0-360Β° } +// Shared scroll index state for node screens +static int scrollIndex = 0; + +// Helper: Calculates max scroll index based on total entries +int calculateMaxScroll(int totalEntries, int visibleRows) { + int totalRows = (totalEntries + 1) / 2; + return std::max(0, totalRows - visibleRows); +} + +// Helper: Draw vertical scrollbar matching CannedMessageModule style +void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollIndex, int columns, int rowYOffset) { + int totalPages = (totalEntries + columns - 1) / columns; + if (totalPages <= visibleNodeRows) return; // no scrollbar needed + + int scrollAreaHeight = visibleNodeRows * (FONT_HEIGHT_SMALL - 3); // true pixel height used per row + int scrollbarX = display->getWidth() - 6; + int scrollbarWidth = 4; + + int scrollBarHeight = (scrollAreaHeight * visibleNodeRows) / totalPages; + int scrollBarY = rowYOffset + (scrollAreaHeight * scrollIndex) / totalPages; + + display->drawRect(scrollbarX, rowYOffset, scrollbarWidth, scrollAreaHeight); + display->fillRect(scrollbarX, scrollBarY, scrollbarWidth, scrollBarHeight); +} + // Grabs all nodes from the DB and sorts them (favorites and most recently heard first) void retrieveAndSortNodes(std::vector &nodeList) { size_t numNodes = nodeDB->getNumMeshNodes(); @@ -1756,8 +1781,9 @@ typedef void (*EntryRenderer)(OLEDDisplay*, meshtastic_NodeInfoLite*, int16_t, i // Shared function that renders all node screens (LastHeard, Hop/Signal) void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer) { int columnWidth = display->getWidth() / 2; - int yOffset = FONT_HEIGHT_SMALL - 3; - int col = 0, lastNodeY = y; + int totalRowsAvailable = (display->getHeight() - y - FONT_HEIGHT_SMALL) / (FONT_HEIGHT_SMALL - 3); + int visibleNodeRows = std::min(6, totalRowsAvailable); + int rowYOffset = FONT_HEIGHT_SMALL - 3; display->clear(); drawScreenHeader(display, title, x, y); @@ -1765,20 +1791,35 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t std::vector nodeList; retrieveAndSortNodes(nodeList); - for (const auto &entry : nodeList) { + int totalEntries = nodeList.size(); + int maxScroll = calculateMaxScroll(totalEntries, visibleNodeRows); + scrollIndex = std::min(scrollIndex, maxScroll); + + int startIndex = scrollIndex * visibleNodeRows * 2; + int endIndex = std::min(startIndex + visibleNodeRows * 2, totalEntries); + + int yOffset = rowYOffset; + int col = 0; + int lastNodeY = y; + int shownCount = 0; + + for (int i = startIndex; i < endIndex; ++i) { int xPos = x + (col * columnWidth); - renderer(display, entry.node, xPos, y + yOffset, columnWidth); + int yPos = y + yOffset; + renderer(display, nodeList[i].node, xPos, yPos, columnWidth); lastNodeY = std::max(lastNodeY, y + yOffset + FONT_HEIGHT_SMALL); yOffset += FONT_HEIGHT_SMALL - 3; + shownCount++; if (y + yOffset > display->getHeight() - FONT_HEIGHT_SMALL) { - yOffset = FONT_HEIGHT_SMALL - 3; + yOffset = rowYOffset; col++; if (col > 1) break; } } - // Draw separator between columns + drawColumnSeparator(display, x, y + FONT_HEIGHT_SMALL - 2, lastNodeY); + drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, rowYOffset); } // Public screen function: shows how recently nodes were heard @@ -1845,8 +1886,9 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 void drawNodeListWithExtrasScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer, CompassExtraRenderer extras) { int columnWidth = display->getWidth() / 2; - int yOffset = FONT_HEIGHT_SMALL - 3; - int col = 0, lastNodeY = y; + int totalRowsAvailable = (display->getHeight() - y - FONT_HEIGHT_SMALL) / (FONT_HEIGHT_SMALL - 3); + int visibleNodeRows = std::min(6, totalRowsAvailable); + int rowYOffset = FONT_HEIGHT_SMALL - 3; display->clear(); drawScreenHeader(display, title, x, y); @@ -1854,6 +1896,10 @@ void drawNodeListWithExtrasScreen(OLEDDisplay *display, OLEDDisplayUiState *stat std::vector nodeList; retrieveAndSortNodes(nodeList); + int totalEntries = nodeList.size(); + int maxScroll = calculateMaxScroll(totalEntries, visibleNodeRows); + scrollIndex = std::min(scrollIndex, maxScroll); + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); double userLat = 0.0, userLon = 0.0; bool hasUserPosition = nodeDB->hasValidPosition(ourNode); @@ -1864,27 +1910,40 @@ void drawNodeListWithExtrasScreen(OLEDDisplay *display, OLEDDisplayUiState *stat float myHeading = screen->hasHeading() ? screen->getHeading() : 0.0f; - for (const auto &entry : nodeList) { + int startIndex = scrollIndex * visibleNodeRows * 2; + int endIndex = std::min(startIndex + visibleNodeRows * 2, totalEntries); + + int yOffset = rowYOffset; + int col = 0; + int lastNodeY = y; + int shownCount = 0; + + for (int i = startIndex; i < endIndex; ++i) { int xPos = x + (col * columnWidth); - renderer(display, entry.node, xPos, y + yOffset, columnWidth); + int yPos = y + yOffset; + + renderer(display, nodeList[i].node, xPos, yPos, columnWidth); if (hasUserPosition && extras) { - extras(display, entry.node, xPos, y + yOffset, columnWidth, myHeading, userLat, userLon); + extras(display, nodeList[i].node, xPos, yPos, columnWidth, myHeading, userLat, userLon); } lastNodeY = std::max(lastNodeY, y + yOffset + FONT_HEIGHT_SMALL); yOffset += FONT_HEIGHT_SMALL - 3; + shownCount++; if (y + yOffset > display->getHeight() - FONT_HEIGHT_SMALL) { - yOffset = FONT_HEIGHT_SMALL - 3; + yOffset = rowYOffset; col++; if (col > 1) break; } } drawColumnSeparator(display, x, y + FONT_HEIGHT_SMALL - 2, lastNodeY); + drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, rowYOffset); } + // Public screen entry for compass static void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { drawNodeListWithExtrasScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow); @@ -2048,7 +2107,6 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i } - #if defined(ESP_PLATFORM) && defined(USE_ST7789) SPIClass SPI1(HSPI); #endif From 62a6c91c7071a7c3c3f3f5b55f0923ace4b86d1e Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 29 Mar 2025 17:47:51 -0400 Subject: [PATCH 006/265] color change for the screen on the T114 --- src/graphics/Screen.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 020a9b1d6..607f4fd56 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2124,6 +2124,7 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O ST7789_MISO, ST7789_SCK); #else dispdev = new ST7789Spi(&SPI1, ST7789_RESET, ST7789_RS, ST7789_NSS, GEOMETRY_RAWMODE, TFT_WIDTH, TFT_HEIGHT); + static_cast(dispdev)->setRGB(COLOR565(255, 255, 128)); #endif #elif defined(USE_SSD1306) dispdev = new SSD1306Wire(address.address, -1, -1, geometry, From 72f6fde772bf53e469384c95bcbc46749d0925eb Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 29 Mar 2025 19:22:09 -0400 Subject: [PATCH 007/265] Added "Uptime:" to the uptime value --- src/graphics/Screen.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 607f4fd56..ed2958e4d 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2101,9 +2101,11 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i days ? 'd' : hours ? 'h' : minutes ? 'm' : 's'); } + char uptimeFullStr[16]; + snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); + int uptimeX = (SCREEN_WIDTH - display->getStringWidth(uptimeFullStr)) / 2; int uptimeY = y + (FONT_HEIGHT_SMALL + 1) * 3; - int uptimeX = (SCREEN_WIDTH - display->getStringWidth(uptimeStr)) / 2; - display->drawString(uptimeX, uptimeY, uptimeStr); + display->drawString(uptimeX, uptimeY, uptimeFullStr); } From 213a178d71bd8bcb546222015217f891895a0c5c Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 29 Mar 2025 20:10:05 -0400 Subject: [PATCH 008/265] Added local Time to banner screen --- src/graphics/Screen.cpp | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index ed2958e4d..2c016d9f5 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2027,7 +2027,7 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i display->setColor(BLACK); } - // === TOP ROW: Battery, %, Voltage === + // === TOP ROW: Battery, %, Time === int screenWidth = display->getWidth(); // Draw battery icon slightly inset from top @@ -2036,23 +2036,45 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i // Calculate vertical center for text (centered in header row) int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + // Battery Percentage char percentStr[8]; snprintf(percentStr, sizeof(percentStr), "%d%%", powerStatus->getBatteryChargePercent()); int batteryOffset = screenWidth > 128 ? 34 : 18; int percentX = x + xOffset + batteryOffset; display->drawString(percentX, textY, percentStr); + if (isBold) display->drawString(percentX + 1, textY, percentStr); + // --- Voltage (Commented out) --- + /* char voltStr[10]; int batV = powerStatus->getBatteryVoltageMv() / 1000; int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; snprintf(voltStr, sizeof(voltStr), "%d.%02dV", batV, batCv); int voltX = SCREEN_WIDTH - xOffset - display->getStringWidth(voltStr); display->drawString(voltX, textY, voltStr); + if (isBold) display->drawString(voltX + 1, textY, voltStr); + */ - if (isBold) { - display->drawString(percentX + 1, textY, percentStr); - display->drawString(voltX + 1, textY, voltStr); + // --- Local Time --- + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // local time + if (rtc_sec > 0) { + long hms = rtc_sec % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + int hour = hms / SEC_PER_HOUR; + int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + + bool isPM = hour >= 12; + hour = hour % 12; + if (hour == 0) hour = 12; + + char timeStr[10]; + snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "PM" : "AM"); + + int timeX = SCREEN_WIDTH - xOffset - display->getStringWidth(timeStr); + display->drawString(timeX, textY, timeStr); + if (isBold) display->drawString(timeX + 1, textY, timeStr); } display->setColor(WHITE); @@ -2108,7 +2130,6 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i display->drawString(uptimeX, uptimeY, uptimeFullStr); } - #if defined(ESP_PLATFORM) && defined(USE_ST7789) SPIClass SPI1(HSPI); #endif From a3a0c1492369b6cc9ec64701bd950c7ec7ef4f5c Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 29 Mar 2025 20:27:59 -0400 Subject: [PATCH 009/265] Made the header it's own code to be easier to create future screens --- src/graphics/Screen.cpp | 46 +++++++++++++++++------------------------ 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 2c016d9f5..875bce3bd 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2010,54 +2010,37 @@ static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, } -static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) { bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); bool isBold = config.display.heading_bold; - const int xOffset = 3; const int highlightHeight = FONT_HEIGHT_SMALL - 1; + int screenWidth = display->getWidth(); + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); - // Draw header background highlight if (isInverted) { drawRoundedHighlight(display, 0, y, SCREEN_WIDTH, highlightHeight, 2); display->setColor(BLACK); } - // === TOP ROW: Battery, %, Time === - int screenWidth = display->getWidth(); - - // Draw battery icon slightly inset from top + // Battery icon drawBattery(display, x + xOffset, y + 2, imgBattery, powerStatus); - // Calculate vertical center for text (centered in header row) + // Centered text Y int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - // Battery Percentage + // Battery % char percentStr[8]; snprintf(percentStr, sizeof(percentStr), "%d%%", powerStatus->getBatteryChargePercent()); - int batteryOffset = screenWidth > 128 ? 34 : 18; int percentX = x + xOffset + batteryOffset; display->drawString(percentX, textY, percentStr); if (isBold) display->drawString(percentX + 1, textY, percentStr); - // --- Voltage (Commented out) --- - /* - char voltStr[10]; - int batV = powerStatus->getBatteryVoltageMv() / 1000; - int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; - snprintf(voltStr, sizeof(voltStr), "%d.%02dV", batV, batCv); - int voltX = SCREEN_WIDTH - xOffset - display->getStringWidth(voltStr); - display->drawString(voltX, textY, voltStr); - if (isBold) display->drawString(voltX + 1, textY, voltStr); - */ - - // --- Local Time --- - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // local time + // Optional: Local time + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); if (rtc_sec > 0) { long hms = rtc_sec % SEC_PER_DAY; hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; @@ -2078,6 +2061,14 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i } display->setColor(WHITE); +} +static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // === Header === + drawCommonHeader(display, x, y); // === Second Row: Node and GPS === bool origBold = config.display.heading_bold; @@ -2130,6 +2121,7 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i display->drawString(uptimeX, uptimeY, uptimeFullStr); } + #if defined(ESP_PLATFORM) && defined(USE_ST7789) SPIClass SPI1(HSPI); #endif From 0480ddd2662ad91003d182a22fb74a59a439b924 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 29 Mar 2025 23:22:54 -0400 Subject: [PATCH 010/265] Compass and Location Screen --- src/graphics/Screen.cpp | 86 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 875bce3bd..0c6fd44b2 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2121,6 +2121,91 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i display->drawString(uptimeX, uptimeY, uptimeFullStr); } +static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // === Header === + drawCommonHeader(display, x, y); + + // Row Y offset just like drawNodeListScreen + int rowYOffset = FONT_HEIGHT_SMALL - 3; + int rowY = y + rowYOffset; + + // === Second Row: My Location (centered) === + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(display->getWidth() / 2, rowY, "My Location"); + display->setTextAlignment(TEXT_ALIGN_LEFT); + +#if HAS_GPS + // === Update GeoCoord === + geoCoord.updateCoords( + int32_t(gpsStatus->getLatitude()), + int32_t(gpsStatus->getLongitude()), + int32_t(gpsStatus->getAltitude()) + ); + + // === Compass Heading === + float heading = 0; +#ifdef HAS_MAG_COMPASS + heading = radians(magCompass->getHeadingDegrees()); +#else + heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7); +#endif + + // === Third Row: Altitude === + rowY += rowYOffset; + char altStr[32]; + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + snprintf(altStr, sizeof(altStr), "Alt: %.1fft", geoCoord.getAltitude() * METERS_TO_FEET); + } else { + snprintf(altStr, sizeof(altStr), "Alt: %.1fm", geoCoord.getAltitude()); + } + display->drawString(x + 2, rowY, altStr); + + // === Fourth Row: Latitude === + rowY += rowYOffset; + char latStr[32]; + snprintf(latStr, sizeof(latStr), "Lat: %.5f", geoCoord.getLatitude() * 1e-7); + display->drawString(x + 2, rowY, latStr); + + // === Fifth Row: Longitude === + rowY += rowYOffset; + char lonStr[32]; + snprintf(lonStr, sizeof(lonStr), "Lon: %.5f", geoCoord.getLongitude() * 1e-7); + display->drawString(x + 2, rowY, lonStr); + + // === Draw Compass if heading is valid === + if (screen->hasHeading()) { + // === Draw Compass on Right Side (One Row Down, 3px left) === + uint16_t compassDiam = Screen::getCompassDiam(SCREEN_WIDTH, SCREEN_HEIGHT); + int16_t compassX = x + SCREEN_WIDTH - compassDiam / 2 - 8; + int16_t compassY = y + SCREEN_HEIGHT / 2 + rowYOffset; + + screen->drawCompassNorth(display, compassX, compassY, heading); + screen->drawNodeHeading(display, compassX, compassY, compassDiam, -heading); + display->drawCircle(compassX, compassY, compassDiam / 2); + + // === Draw moving "N" label slightly inside edge of circle === + float northAngle = -heading; + float radius = compassDiam / 2; + int16_t nX = compassX + (radius - 1) * sin(northAngle); // nudged 1px inward + int16_t nY = compassY - (radius - 1) * cos(northAngle); // nudged 1px inward + + int16_t nLabelWidth = display->getStringWidth("N") + 2; + int16_t nLabelHeight = FONT_HEIGHT_SMALL + 1; + display->setColor(BLACK); // erase circle behind N + display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeight / 2, nLabelWidth, nLabelHeight); + display->setColor(WHITE); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + } +#endif +} + + #if defined(ESP_PLATFORM) && defined(USE_ST7789) SPIClass SPI1(HSPI); @@ -2753,6 +2838,7 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = drawDistanceScreen; normalFrames[numframes++] = drawNodeListWithCompasses; normalFrames[numframes++] = drawHopSignalScreen; + normalFrames[numframes++] = drawCompassAndLocationScreen; // then all the nodes // We only show a few nodes in our scrolling list - because meshes with many nodes would have too many screens From 2fc678132294dd641691616a12426e6d21d1f4d2 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 29 Mar 2025 23:46:54 -0400 Subject: [PATCH 011/265] Added xaositek screens --- src/graphics/Screen.cpp | 102 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 0c6fd44b2..19ef5dba9 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2120,7 +2120,107 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i int uptimeY = y + (FONT_HEIGHT_SMALL + 1) * 3; display->drawString(uptimeX, uptimeY, uptimeFullStr); } +// **************************** +// * BatteryDeviceLoRa Screen * +// **************************** +static void drawBatteryDeviceLoRa(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + // === Header === + drawCommonHeader(display, x, y); + + // === Second Row: MAC ID and Region === + bool origBold = config.display.heading_bold; + config.display.heading_bold = false; + + int secondRowY = y + FONT_HEIGHT_SMALL + 1; + + // Get our hardware ID + uint8_t dmac[6]; + getMacAddr(dmac); + snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); + +#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ + !defined(DISPLAY_FORCE_SMALL_FONTS) + display->drawFastImage(x, y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgInfoL1); + display->drawFastImage(x, y + 11 + FONT_HEIGHT_SMALL, 12, 8, imgInfoL2); +#else + display->drawFastImage(x, y + 2 + FONT_HEIGHT_SMALL, 8, 8, imgInfo); +#endif + + display->drawString(x + 14, secondRowY, ourId); + + const char *region = myRegion ? myRegion->name : NULL; + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(region), secondRowY, region); + + config.display.heading_bold = origBold; + + // === Third Row: Channel and Channel Utilization === + int thirdRowY = y + (FONT_HEIGHT_SMALL * 2) + 1; + char channelStr[20]; + { + snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); + } + display->drawString(x, thirdRowY, channelStr); + + // Display Channel Utilization + char chUtil[13]; + snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), thirdRowY, chUtil); + + // === Fourth Row: Uptime === + uint32_t uptime = millis() / 1000; + char uptimeStr[6]; + uint32_t minutes = uptime / 60, hours = minutes / 60, days = hours / 24; + + if (days > 365) { + snprintf(uptimeStr, sizeof(uptimeStr), "?"); + } else { + snprintf(uptimeStr, sizeof(uptimeStr), "%d%c", + days ? days + : hours ? hours + : minutes ? minutes + : (int)uptime, + days ? 'd' + : hours ? 'h' + : minutes ? 'm' + : 's'); + } + + char uptimeFullStr[16]; + snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); + int uptimeX = (SCREEN_WIDTH - display->getStringWidth(uptimeFullStr)) / 2; + int uptimeY = y + (FONT_HEIGHT_SMALL + 1) * 3; + display->drawString(uptimeX, uptimeY, uptimeFullStr); +} + +// **************************** +// * Activity Screen * +// **************************** +static void drawActivity(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // === Header === + drawCommonHeader(display, x, y); + + // === Second Row: Draw any log messages === + bool origBold = config.display.heading_bold; + config.display.heading_bold = false; + + int secondRowY = y + FONT_HEIGHT_SMALL + 1; + display->drawLogBuffer(x, secondRowY); +} + +// **************************** +// * Activity Screen * +// **************************** static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -2838,7 +2938,9 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = drawDistanceScreen; normalFrames[numframes++] = drawNodeListWithCompasses; normalFrames[numframes++] = drawHopSignalScreen; + normalFrames[numframes++] = drawBatteryDeviceLoRa; normalFrames[numframes++] = drawCompassAndLocationScreen; + normalFrames[numframes++] = drawActivity; // then all the nodes // We only show a few nodes in our scrolling list - because meshes with many nodes would have too many screens From 50db11fff68c66cea3624b3ff4ba03e69d5514e6 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 30 Mar 2025 01:25:22 -0400 Subject: [PATCH 012/265] Compass north fix --- src/graphics/Screen.cpp | 55 +++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 19ef5dba9..3f9687d1f 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2010,7 +2010,7 @@ static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, } -void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) { +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char* title = nullptr) { bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); bool isBold = config.display.heading_bold; const int xOffset = 3; @@ -2026,7 +2026,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) { } // Battery icon - drawBattery(display, x + xOffset, y + 2, imgBattery, powerStatus); + drawBattery(display, x + xOffset - 2, y + 2, imgBattery, powerStatus); // Centered text Y int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; @@ -2034,13 +2034,14 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) { // Battery % char percentStr[8]; snprintf(percentStr, sizeof(percentStr), "%d%%", powerStatus->getBatteryChargePercent()); - int batteryOffset = screenWidth > 128 ? 34 : 18; + int batteryOffset = screenWidth > 128 ? 34 : 16; int percentX = x + xOffset + batteryOffset; display->drawString(percentX, textY, percentStr); if (isBold) display->drawString(percentX + 1, textY, percentStr); // Optional: Local time uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); + int timeX = screenWidth; if (rtc_sec > 0) { long hms = rtc_sec % SEC_PER_DAY; hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; @@ -2055,13 +2056,22 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) { char timeStr[10]; snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "PM" : "AM"); - int timeX = SCREEN_WIDTH - xOffset - display->getStringWidth(timeStr); + timeX = SCREEN_WIDTH - xOffset - display->getStringWidth(timeStr); display->drawString(timeX, textY, timeStr); if (isBold) display->drawString(timeX + 1, textY, timeStr); } + // === Centered Title (between battery % and time) === + if (title) { + display->setTextAlignment(TEXT_ALIGN_CENTER); + int centerX = SCREEN_WIDTH / 2; + display->drawString(centerX, textY, title); + if (isBold) display->drawString(centerX + 1, textY, title); + } + display->setColor(WHITE); } + static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -2208,7 +2218,7 @@ static void drawActivity(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ display->setFont(FONT_SMALL); // === Header === - drawCommonHeader(display, x, y); + drawCommonHeader(display, x, y, "Log"); // === Second Row: Draw any log messages === bool origBold = config.display.heading_bold; @@ -2219,7 +2229,7 @@ static void drawActivity(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ } // **************************** -// * Activity Screen * +// * My Position Screen * // **************************** static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); @@ -2233,9 +2243,9 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat int rowYOffset = FONT_HEIGHT_SMALL - 3; int rowY = y + rowYOffset; - // === Second Row: My Location (centered) === - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(display->getWidth() / 2, rowY, "My Location"); + // === Second Row: My Location === + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawString(x + 2, rowY, "My Location:"); display->setTextAlignment(TEXT_ALIGN_LEFT); #if HAS_GPS @@ -2246,13 +2256,17 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat int32_t(gpsStatus->getAltitude()) ); - // === Compass Heading === - float heading = 0; -#ifdef HAS_MAG_COMPASS - heading = radians(magCompass->getHeadingDegrees()); -#else - heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7); -#endif + // === Determine Compass Heading === + float heading; + bool validHeading = false; + + if (screen->hasHeading()) { + heading = radians(screen->getHeading()); + validHeading = true; + } else { + heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7); + validHeading = !isnan(heading); + } // === Third Row: Altitude === rowY += rowYOffset; @@ -2277,8 +2291,7 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat display->drawString(x + 2, rowY, lonStr); // === Draw Compass if heading is valid === - if (screen->hasHeading()) { - // === Draw Compass on Right Side (One Row Down, 3px left) === + if (validHeading) { uint16_t compassDiam = Screen::getCompassDiam(SCREEN_WIDTH, SCREEN_HEIGHT); int16_t compassX = x + SCREEN_WIDTH - compassDiam / 2 - 8; int16_t compassY = y + SCREEN_HEIGHT / 2 + rowYOffset; @@ -2290,12 +2303,12 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat // === Draw moving "N" label slightly inside edge of circle === float northAngle = -heading; float radius = compassDiam / 2; - int16_t nX = compassX + (radius - 1) * sin(northAngle); // nudged 1px inward - int16_t nY = compassY - (radius - 1) * cos(northAngle); // nudged 1px inward + int16_t nX = compassX + (radius - 1) * sin(northAngle); + int16_t nY = compassY - (radius - 1) * cos(northAngle); int16_t nLabelWidth = display->getStringWidth("N") + 2; int16_t nLabelHeight = FONT_HEIGHT_SMALL + 1; - display->setColor(BLACK); // erase circle behind N + display->setColor(BLACK); display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeight / 2, nLabelWidth, nLabelHeight); display->setColor(WHITE); display->setFont(FONT_SMALL); From 1fd95d85b8d80dc7c8e701923b8d032c35992cbf Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 30 Mar 2025 01:42:34 -0400 Subject: [PATCH 013/265] Bug was making bold get undone --- src/graphics/Screen.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 3f9687d1f..6236343a1 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2221,13 +2221,9 @@ static void drawActivity(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ drawCommonHeader(display, x, y, "Log"); // === Second Row: Draw any log messages === - bool origBold = config.display.heading_bold; - config.display.heading_bold = false; - int secondRowY = y + FONT_HEIGHT_SMALL + 1; display->drawLogBuffer(x, secondRowY); } - // **************************** // * My Position Screen * // **************************** From aff0834f8e9a9061dded8f8875aabbb8994fb937 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 30 Mar 2025 16:53:44 -0400 Subject: [PATCH 014/265] New Memory screen --- src/graphics/Screen.cpp | 159 ++++++++++++++++++++++++++++++++++------ 1 file changed, 135 insertions(+), 24 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 6236343a1..9fbf45234 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -50,6 +50,7 @@ along with this program. If not, see . #include "modules/WaypointModule.h" #include "sleep.h" #include "target_specific.h" +#include "FSCommon.h" #if HAS_WIFI && !defined(ARCH_PORTDUINO) #include "mesh/wifi/WiFiAPClient.h" @@ -2010,42 +2011,40 @@ static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, } -void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char* title = nullptr) { - bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); - bool isBold = config.display.heading_bold; +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) { + const bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); + const bool isBold = config.display.heading_bold; const int xOffset = 3; const int highlightHeight = FONT_HEIGHT_SMALL - 1; - int screenWidth = display->getWidth(); + const int screenWidth = display->getWidth(); display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); + // Draw background highlight if (isInverted) { - drawRoundedHighlight(display, 0, y, SCREEN_WIDTH, highlightHeight, 2); + drawRoundedHighlight(display, x, y, screenWidth, highlightHeight, 2); display->setColor(BLACK); } // Battery icon drawBattery(display, x + xOffset - 2, y + 2, imgBattery, powerStatus); - // Centered text Y - int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + // Text baseline + const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; // Battery % char percentStr[8]; snprintf(percentStr, sizeof(percentStr), "%d%%", powerStatus->getBatteryChargePercent()); - int batteryOffset = screenWidth > 128 ? 34 : 16; - int percentX = x + xOffset + batteryOffset; + const int batteryOffset = screenWidth > 128 ? 34 : 16; + const int percentX = x + xOffset + batteryOffset; display->drawString(percentX, textY, percentStr); if (isBold) display->drawString(percentX + 1, textY, percentStr); - // Optional: Local time + // Time (right side) uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); - int timeX = screenWidth; if (rtc_sec > 0) { - long hms = rtc_sec % SEC_PER_DAY; - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - + long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; int hour = hms / SEC_PER_HOUR; int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; @@ -2056,22 +2055,15 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char* ti char timeStr[10]; snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "PM" : "AM"); - timeX = SCREEN_WIDTH - xOffset - display->getStringWidth(timeStr); + int timeX = x + screenWidth - xOffset - display->getStringWidth(timeStr); display->drawString(timeX, textY, timeStr); if (isBold) display->drawString(timeX + 1, textY, timeStr); } - // === Centered Title (between battery % and time) === - if (title) { - display->setTextAlignment(TEXT_ALIGN_CENTER); - int centerX = SCREEN_WIDTH / 2; - display->drawString(centerX, textY, title); - if (isBold) display->drawString(centerX + 1, textY, title); - } - display->setColor(WHITE); } + static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -2218,7 +2210,27 @@ static void drawActivity(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ display->setFont(FONT_SMALL); // === Header === - drawCommonHeader(display, x, y, "Log"); + drawCommonHeader(display, x, y); + + // === Draw title === + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + + // Use black text if display is inverted + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->setColor(BLACK); + } + + display->setTextAlignment(TEXT_ALIGN_CENTER); + const int centerX = x + SCREEN_WIDTH / 2; + display->drawString(centerX, textY, "Log"); + + if (config.display.heading_bold) { + display->drawString(centerX + 1, textY, "Log"); + } + + // Restore default color after drawing + display->setColor(WHITE); // === Second Row: Draw any log messages === int secondRowY = y + FONT_HEIGHT_SMALL + 1; @@ -2314,6 +2326,104 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat #endif } +// **************************** +// * Memory Screen * +// **************************** +static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->clear(); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === Header === + drawCommonHeader(display, x, y); + + // === Draw title === + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->setColor(BLACK); + } + + display->setTextAlignment(TEXT_ALIGN_CENTER); + const int centerX = x + SCREEN_WIDTH / 2; + display->drawString(centerX, textY, "Memory"); + if (config.display.heading_bold) { + display->drawString(centerX + 1, textY, "Memory"); + } + display->setColor(WHITE); + + // === Layout === + const int screenWidth = display->getWidth(); + const int rowYOffset = FONT_HEIGHT_SMALL - 3; + const int barHeight = 6; + + const int labelX = x; + const int barsOffset = (screenWidth > 128) ? 24 : 0; + const int barX = x + 40 + barsOffset; + const int barWidth = SCREEN_WIDTH - barX - 35; + const int textRightX = x + SCREEN_WIDTH - 2; + + int rowY = y + rowYOffset; + + auto drawUsageRow = [&](const char* label, uint32_t used, uint32_t total) { + if (total == 0) return; + + int percent = (used * 100) / total; + int fillWidth = (used * barWidth) / total; + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawString(labelX, rowY, label); + + int barY = rowY + (FONT_HEIGHT_SMALL - barHeight) / 2; + display->setColor(WHITE); + display->drawRect(barX, barY, barWidth, barHeight); + + if (percent >= 80) display->setColor(BLACK); + display->fillRect(barX, barY, fillWidth, barHeight); + display->setColor(WHITE); + + char percentStr[6]; + snprintf(percentStr, sizeof(percentStr), "%3d%%", percent); + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(textRightX, rowY, percentStr); + + rowY += rowYOffset; + }; + + // === Memory values === + uint32_t heapUsed = memGet.getHeapSize() - memGet.getFreeHeap(); + uint32_t heapTotal = memGet.getHeapSize(); + + uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram(); + uint32_t psramTotal = memGet.getPsramSize(); + + uint32_t flashUsed = 0, flashTotal = 0; + + #ifdef ESP32 + flashUsed = FSCom.usedBytes(); + flashTotal = FSCom.totalBytes(); + #endif + +#ifdef HAS_SDCARD + uint32_t sdUsed = 0, sdTotal = 0; + bool hasSD = SD.cardType() != CARD_NONE; + if (hasSD) { + sdUsed = SD.usedBytes(); + sdTotal = SD.totalBytes(); + } +#else + bool hasSD = false; + uint32_t sdUsed = 0, sdTotal = 0; +#endif + + // === Draw memory rows + drawUsageRow("Heap:", heapUsed, heapTotal); + drawUsageRow("PSRAM:", psramUsed, psramTotal); +#ifdef ESP32 + if (flashTotal > 0) drawUsageRow("Flash:", flashUsed, flashTotal); +#endif + if (hasSD && sdTotal > 0) drawUsageRow("SD:", sdUsed, sdTotal); +} #if defined(ESP_PLATFORM) && defined(USE_ST7789) @@ -2942,6 +3052,7 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = drawTextMessageFrame; } + normalFrames[numframes++] = drawMemoryScreen; normalFrames[numframes++] = drawDefaultScreen; normalFrames[numframes++] = drawLastHeardScreen; normalFrames[numframes++] = drawDistanceScreen; From 7181e1a296845c2d38e7033d8405a38ae54c556e Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 30 Mar 2025 16:54:57 -0400 Subject: [PATCH 015/265] changed order --- src/graphics/Screen.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 9fbf45234..60c5f67d0 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -3052,7 +3052,6 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = drawTextMessageFrame; } - normalFrames[numframes++] = drawMemoryScreen; normalFrames[numframes++] = drawDefaultScreen; normalFrames[numframes++] = drawLastHeardScreen; normalFrames[numframes++] = drawDistanceScreen; @@ -3060,6 +3059,7 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = drawHopSignalScreen; normalFrames[numframes++] = drawBatteryDeviceLoRa; normalFrames[numframes++] = drawCompassAndLocationScreen; + normalFrames[numframes++] = drawMemoryScreen; normalFrames[numframes++] = drawActivity; // then all the nodes From f60c4ec5bc47e90bc64b79b3e66d19a2ae562005 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 30 Mar 2025 17:42:52 -0400 Subject: [PATCH 016/265] Conditional tittle and data based on screen size --- src/graphics/Screen.cpp | 55 ++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 60c5f67d0..3a24f9680 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2340,27 +2340,28 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in // === Draw title === const int highlightHeight = FONT_HEIGHT_SMALL - 1; const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int screenWidth = display->getWidth(); + const char* titleStr = (screenWidth > 128) ? "Memory" : "Mem"; + const int centerX = x + SCREEN_WIDTH / 2; + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { display->setColor(BLACK); } display->setTextAlignment(TEXT_ALIGN_CENTER); - const int centerX = x + SCREEN_WIDTH / 2; - display->drawString(centerX, textY, "Memory"); + display->drawString(centerX, textY, titleStr); if (config.display.heading_bold) { - display->drawString(centerX + 1, textY, "Memory"); + display->drawString(centerX + 1, textY, titleStr); } display->setColor(WHITE); // === Layout === - const int screenWidth = display->getWidth(); const int rowYOffset = FONT_HEIGHT_SMALL - 3; const int barHeight = 6; const int labelX = x; const int barsOffset = (screenWidth > 128) ? 24 : 0; const int barX = x + 40 + barsOffset; - const int barWidth = SCREEN_WIDTH - barX - 35; const int textRightX = x + SCREEN_WIDTH - 2; int rowY = y + rowYOffset; @@ -2369,23 +2370,36 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in if (total == 0) return; int percent = (used * 100) / total; - int fillWidth = (used * barWidth) / total; + char combinedStr[24]; + if (screenWidth > 128) { + snprintf(combinedStr, sizeof(combinedStr), "%3d%% %lu/%luKB", percent, used / 1024, total / 1024); + } else { + snprintf(combinedStr, sizeof(combinedStr), "%3d%%", percent); + } + + int textWidth = display->getStringWidth(combinedStr); + int adjustedBarWidth = SCREEN_WIDTH - barX - textWidth - 6; + if (adjustedBarWidth < 10) adjustedBarWidth = 10; // prevent weird bar if display is too small + + int fillWidth = (used * adjustedBarWidth) / total; + + // Label display->setTextAlignment(TEXT_ALIGN_LEFT); display->drawString(labelX, rowY, label); + // Bar int barY = rowY + (FONT_HEIGHT_SMALL - barHeight) / 2; display->setColor(WHITE); - display->drawRect(barX, barY, barWidth, barHeight); + display->drawRect(barX, barY, adjustedBarWidth, barHeight); if (percent >= 80) display->setColor(BLACK); display->fillRect(barX, barY, fillWidth, barHeight); display->setColor(WHITE); - char percentStr[6]; - snprintf(percentStr, sizeof(percentStr), "%3d%%", percent); + // Value string display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(textRightX, rowY, percentStr); + display->drawString(SCREEN_WIDTH - 2, rowY, combinedStr); rowY += rowYOffset; }; @@ -2398,30 +2412,27 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in uint32_t psramTotal = memGet.getPsramSize(); uint32_t flashUsed = 0, flashTotal = 0; - #ifdef ESP32 flashUsed = FSCom.usedBytes(); flashTotal = FSCom.totalBytes(); #endif -#ifdef HAS_SDCARD uint32_t sdUsed = 0, sdTotal = 0; - bool hasSD = SD.cardType() != CARD_NONE; - if (hasSD) { - sdUsed = SD.usedBytes(); - sdTotal = SD.totalBytes(); - } -#else bool hasSD = false; - uint32_t sdUsed = 0, sdTotal = 0; -#endif + #ifdef HAS_SDCARD + hasSD = SD.cardType() != CARD_NONE; + if (hasSD) { + sdUsed = SD.usedBytes(); + sdTotal = SD.totalBytes(); + } + #endif // === Draw memory rows drawUsageRow("Heap:", heapUsed, heapTotal); drawUsageRow("PSRAM:", psramUsed, psramTotal); -#ifdef ESP32 + #ifdef ESP32 if (flashTotal > 0) drawUsageRow("Flash:", flashUsed, flashTotal); -#endif + #endif if (hasSD && sdTotal > 0) drawUsageRow("SD:", sdUsed, sdTotal); } From 99ca59b8a101af754a5c0cccb234d3f44b65db19 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 30 Mar 2025 23:26:33 -0400 Subject: [PATCH 017/265] Jason's cleanup --- src/graphics/Screen.cpp | 461 +++++++++++++++++++++++++++++----------- 1 file changed, 342 insertions(+), 119 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 3a24f9680..dd925b0fc 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -30,6 +30,7 @@ along with this program. If not, see . #if !MESHTASTIC_EXCLUDE_GPS #include "GPS.h" #endif +#include "FSCommon.h" #include "MeshService.h" #include "NodeDB.h" #include "error.h" @@ -50,7 +51,6 @@ along with this program. If not, see . #include "modules/WaypointModule.h" #include "sleep.h" #include "target_specific.h" -#include "FSCommon.h" #if HAS_WIFI && !defined(ARCH_PORTDUINO) #include "mesh/wifi/WiFiAPClient.h" @@ -119,6 +119,18 @@ static bool heartbeat = false; #define SCREEN_WIDTH display->getWidth() #define SCREEN_HEIGHT display->getHeight() +// Pre-defined lines; this is intended to be used AFTER the common header +#define compactFirstLine (FONT_HEIGHT_SMALL - 3) * 1 +#define compactSecondLine (FONT_HEIGHT_SMALL - 3) * 2 +#define compactThirdLine (FONT_HEIGHT_SMALL - 3) * 3 +#define compactFourthLine (FONT_HEIGHT_SMALL - 3) * 4 +#define compactFifthLine (FONT_HEIGHT_SMALL - 3) * 5 + +#define standardFirstLine (FONT_HEIGHT_SMALL + 1) * 1 +#define standardSecondLine (FONT_HEIGHT_SMALL + 1) * 2 +#define standardThirdLine (FONT_HEIGHT_SMALL + 1) * 3 +#define standardFourthLine (FONT_HEIGHT_SMALL + 1) * 4 + #include "graphics/ScreenFonts.h" #include @@ -445,8 +457,10 @@ static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *img int screenWidth = display->getWidth(); int scale = 1; - if (screenWidth >= 200) scale = 2; - if (screenWidth >= 300) scale = 2; // Do NOT go higher than 2 + if (screenWidth >= 200) + scale = 2; + if (screenWidth >= 300) + scale = 2; // Do NOT go higher than 2 // Draw scaled battery image (16 columns Γ— 8 rows) for (int col = 0; col < 16; col++) { @@ -1152,20 +1166,21 @@ static void drawGPS(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus int maxDrawWidth = 6; // Position icon if (!gps->getHasLock()) { - maxDrawWidth += display->getStringWidth("No sats") + 2; // icon + text + buffer + maxDrawWidth += display->getStringWidth("No sats") + 2; // icon + text + buffer } else { maxDrawWidth += (5 * 2) + 8 + display->getStringWidth("99") + 2; // bars + sat icon + text + buffer } if (x + maxDrawWidth > SCREEN_WIDTH) { x = SCREEN_WIDTH - maxDrawWidth; - if (x < 0) x = 0; // Clamp to screen + if (x < 0) + x = 0; // Clamp to screen } display->drawFastImage(x, y, 6, 8, gps->getHasLock() ? imgPositionSolid : imgPositionEmpty); if (!gps->getHasLock()) { // Draw "No sats" to the right of the icon with slightly more gap - int textX = x + 9; // 6 (icon) + 3px spacing + int textX = x + 9; // 6 (icon) + 3px spacing display->drawString(textX, y - 2, "No sats"); if (config.display.heading_bold) display->drawString(textX + 1, y - 2, "No sats"); @@ -1196,7 +1211,6 @@ static void drawGPS(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus } } - // Draw status when GPS is disabled or not present static void drawGPSpowerstat(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) { @@ -1334,7 +1348,7 @@ void Screen::drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t com Serial.print(headingRadian); Serial.print(" | (deg): "); Serial.println(headingRadian * RAD_TO_DEG); - + Point tip(0.0f, 0.5f), tail(0.0f, -0.35f); // pointing up initially float arrowOffsetX = 0.14f, arrowOffsetY = 1.0f; Point leftArrow(tip.x - arrowOffsetX, tip.y - arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y - arrowOffsetY); @@ -1353,10 +1367,14 @@ void Screen::drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t com display->drawLine(leftArrow.x, leftArrow.y, tail.x, tail.y); display->drawLine(rightArrow.x, rightArrow.y, tail.x, tail.y); */ - Serial.print("πŸ”₯ Arrow Tail X: "); Serial.print(tail.x); - Serial.print(" | Y: "); Serial.print(tail.y); - Serial.print(" | Tip X: "); Serial.print(tip.x); - Serial.print(" | Tip Y: "); Serial.println(tip.y); + Serial.print("πŸ”₯ Arrow Tail X: "); + Serial.print(tail.x); + Serial.print(" | Y: "); + Serial.print(tail.y); + Serial.print(" | Tip X: "); + Serial.print(tip.x); + Serial.print(" | Tip Y: "); + Serial.println(tip.y); #ifdef USE_EINK display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); #else @@ -1418,9 +1436,10 @@ void Screen::drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t co rosePoints[i]->translate(compassX, compassY); } display->drawCircle(NC1.x, NC1.y, 4); // North sign circle, 4px radius is sufficient for all displays. - Serial.print("πŸ”₯ North Marker X: "); Serial.print(NC1.x); - Serial.print(" | Y: "); Serial.println(NC1.y); - + Serial.print("πŸ”₯ North Marker X: "); + Serial.print(NC1.x); + Serial.print(" | Y: "); + Serial.println(NC1.y); } uint16_t Screen::getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) @@ -1568,17 +1587,18 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ } // h! Makes header invert rounder -void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r) { +void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r) +{ // Center rectangles display->fillRect(x + r, y, w - 2 * r, h); display->fillRect(x, y + r, r, h - 2 * r); display->fillRect(x + w - r, y + r, r, h - 2 * r); // Rounded corners - display->fillCircle(x + r, y + r, r); // Top-left - display->fillCircle(x + w - r - 1, y + r, r); // Top-right - display->fillCircle(x + r, y + h - r - 1, r); // Bottom-left - display->fillCircle(x + w - r - 1, y + h - r - 1, r); // Bottom-right + display->fillCircle(x + r, y + r, r); // Top-left + display->fillCircle(x + w - r - 1, y + r, r); // Top-right + display->fillCircle(x + r, y + h - r - 1, r); // Bottom-left + display->fillCircle(x + w - r - 1, y + h - r - 1, r); // Bottom-right } // h! Each node entry holds a reference to its info and how long ago it was heard from struct NodeEntry { @@ -1587,7 +1607,8 @@ struct NodeEntry { }; // h! Calculates bearing between two lat/lon points (used for compass) -float calculateBearing(double lat1, double lon1, double lat2, double lon2) { +float calculateBearing(double lat1, double lon1, double lat2, double lon2) +{ double dLon = (lon2 - lon1) * DEG_TO_RAD; lat1 = lat1 * DEG_TO_RAD; lat2 = lat2 * DEG_TO_RAD; @@ -1596,22 +1617,25 @@ float calculateBearing(double lat1, double lon1, double lat2, double lon2) { double x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon); double initialBearing = atan2(y, x); - return fmod((initialBearing * RAD_TO_DEG + 360), 360); // Normalize to 0-360Β° + return fmod((initialBearing * RAD_TO_DEG + 360), 360); // Normalize to 0-360Β° } // Shared scroll index state for node screens static int scrollIndex = 0; // Helper: Calculates max scroll index based on total entries -int calculateMaxScroll(int totalEntries, int visibleRows) { +int calculateMaxScroll(int totalEntries, int visibleRows) +{ int totalRows = (totalEntries + 1) / 2; return std::max(0, totalRows - visibleRows); } // Helper: Draw vertical scrollbar matching CannedMessageModule style -void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollIndex, int columns, int rowYOffset) { +void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollIndex, int columns, int rowYOffset) +{ int totalPages = (totalEntries + columns - 1) / columns; - if (totalPages <= visibleNodeRows) return; // no scrollbar needed + if (totalPages <= visibleNodeRows) + return; // no scrollbar needed int scrollAreaHeight = visibleNodeRows * (FONT_HEIGHT_SMALL - 3); // true pixel height used per row int scrollbarX = display->getWidth() - 6; @@ -1625,32 +1649,38 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, } // Grabs all nodes from the DB and sorts them (favorites and most recently heard first) -void retrieveAndSortNodes(std::vector &nodeList) { +void retrieveAndSortNodes(std::vector &nodeList) +{ size_t numNodes = nodeDB->getNumMeshNodes(); for (size_t i = 0; i < numNodes; i++) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - if (!node || node->num == nodeDB->getNodeNum()) continue; // Skip self + if (!node || node->num == nodeDB->getNodeNum()) + continue; // Skip self nodeList.push_back({node, sinceLastSeen(node)}); } std::sort(nodeList.begin(), nodeList.end(), [](const NodeEntry &a, const NodeEntry &b) { bool aFav = a.node->is_favorite; bool bFav = b.node->is_favorite; - if (aFav != bFav) return aFav > bFav; - if (a.lastHeard == 0 || a.lastHeard == UINT32_MAX) return false; - if (b.lastHeard == 0 || b.lastHeard == UINT32_MAX) return true; + if (aFav != bFav) + return aFav > bFav; + if (a.lastHeard == 0 || a.lastHeard == UINT32_MAX) + return false; + if (b.lastHeard == 0 || b.lastHeard == UINT32_MAX) + return true; return a.lastHeard < b.lastHeard; }); } // Helper: Fallback-NodeID if emote is on ShortName for display purposes -String getSafeNodeName(meshtastic_NodeInfoLite *node) { +String getSafeNodeName(meshtastic_NodeInfoLite *node) +{ String nodeName = "?"; if (node->has_user && strlen(node->user.short_name) > 0) { bool valid = true; - const char* name = node->user.short_name; + const char *name = node->user.short_name; for (size_t i = 0; i < strlen(name); i++) { uint8_t c = (uint8_t)name[i]; @@ -1670,12 +1700,14 @@ String getSafeNodeName(meshtastic_NodeInfoLite *node) { } } - if (node->is_favorite) nodeName = "*" + nodeName; + if (node->is_favorite) + nodeName = "*" + nodeName; return nodeName; } // Draws the top header bar (optionally inverted or bold) -void drawScreenHeader(OLEDDisplay *display, const char *title, int16_t x, int16_t y) { +void drawScreenHeader(OLEDDisplay *display, const char *title, int16_t x, int16_t y) +{ bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); bool isBold = config.display.heading_bold; @@ -1699,25 +1731,31 @@ void drawScreenHeader(OLEDDisplay *display, const char *title, int16_t x, int16_ // Draw text centered vertically and horizontally display->drawString(titleX, textY, title); - if (isBold) display->drawString(titleX + 1, textY, title); + if (isBold) + display->drawString(titleX + 1, textY, title); display->setColor(WHITE); } // Draws separator line -void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) { +void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) +{ int columnWidth = display->getWidth() / 2; int separatorX = x + columnWidth - 2; display->drawLine(separatorX, yStart, separatorX, yEnd - 3); } // Draws node name with how long ago it was last heard from -void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { +void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ int screenWidth = display->getWidth(); bool isLeftCol = (x < screenWidth / 2); // Adjust offset based on column and screen width - int timeOffset = (screenWidth > 128) ? (isLeftCol ? 41 : 45) : (isLeftCol ? 24 : 30);//offset large screen (?Left:Right column), offset small screen (?Left:Right column) + int timeOffset = + (screenWidth > 128) + ? (isLeftCol ? 41 : 45) + : (isLeftCol ? 24 : 30); // offset large screen (?Left:Right column), offset small screen (?Left:Right column) String nodeName = getSafeNodeName(node); @@ -1728,7 +1766,12 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int } else { uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"), - (days ? days : hours ? hours : minutes), (days ? 'd' : hours ? 'h' : 'm')); + (days ? days + : hours ? hours + : minutes), + (days ? 'd' + : hours ? 'h' + : 'm')); } display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -1737,13 +1780,20 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->drawString(x + columnWidth - timeOffset, y, timeStr); } // Draws each node's name, hop count, and signal bars -void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { +void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ int screenWidth = display->getWidth(); bool isLeftCol = (x < screenWidth / 2); int nameMaxWidth = columnWidth - 25; - int barsOffset = (screenWidth > 128) ? (isLeftCol ? 26 : 30) : (isLeftCol ? 17 : 19);//offset large screen (?Left:Right column), offset small screen (?Left:Right column) - int hopOffset = (screenWidth > 128) ? (isLeftCol ? 32 : 38) : (isLeftCol ? 18 : 20);//offset large screen (?Left:Right column), offset small screen (?Left:Right column) + int barsOffset = + (screenWidth > 128) + ? (isLeftCol ? 26 : 30) + : (isLeftCol ? 17 : 19); // offset large screen (?Left:Right column), offset small screen (?Left:Right column) + int hopOffset = + (screenWidth > 128) + ? (isLeftCol ? 32 : 38) + : (isLeftCol ? 18 : 20); // offset large screen (?Left:Right column), offset small screen (?Left:Right column) int barsXOffset = columnWidth - barsOffset; @@ -1777,10 +1827,12 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int } // Typedef for passing different render functions into one reusable screen function -typedef void (*EntryRenderer)(OLEDDisplay*, meshtastic_NodeInfoLite*, int16_t, int16_t, int); +typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); // Shared function that renders all node screens (LastHeard, Hop/Signal) -void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer) { +void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, + EntryRenderer renderer) +{ int columnWidth = display->getWidth() / 2; int totalRowsAvailable = (display->getHeight() - y - FONT_HEIGHT_SMALL) / (FONT_HEIGHT_SMALL - 3); int visibleNodeRows = std::min(6, totalRowsAvailable); @@ -1815,7 +1867,8 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t if (y + yOffset > display->getHeight() - FONT_HEIGHT_SMALL) { yOffset = rowYOffset; col++; - if (col > 1) break; + if (col > 1) + break; } } @@ -1824,21 +1877,20 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t } // Public screen function: shows how recently nodes were heard -static void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { +static void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ drawNodeListScreen(display, state, x, y, "Node List", drawEntryLastHeard); } // Public screen function: shows hop count + signal strength -static void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { +static void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ drawNodeListScreen(display, state, x, y, "Hops/Signal", drawEntryHopSignal); } - - - - // Helper function: Draw a single node entry for Node List (Modified for Compass Screen) -void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { +void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ int screenWidth = display->getWidth(); bool isLeftCol = (x < screenWidth / 2); @@ -1853,10 +1905,14 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 } // Extra compass element drawer (injects compass arrows) -typedef void (*CompassExtraRenderer)(OLEDDisplay*, meshtastic_NodeInfoLite*, int16_t, int16_t, int columnWidth, float myHeading, double userLat, double userLon); +typedef void (*CompassExtraRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int columnWidth, float myHeading, + double userLat, double userLon); -void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, double userLat, double userLon) { - if (!nodeDB->hasValidPosition(node)) return; +void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, + double userLat, double userLon) +{ + if (!nodeDB->hasValidPosition(node)) + return; int screenWidth = display->getWidth(); bool isLeftCol = (x < screenWidth / 2); @@ -1885,7 +1941,8 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 // Generic node+compass renderer (like drawNodeListScreen but with compass support) void drawNodeListWithExtrasScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, - EntryRenderer renderer, CompassExtraRenderer extras) { + EntryRenderer renderer, CompassExtraRenderer extras) +{ int columnWidth = display->getWidth() / 2; int totalRowsAvailable = (display->getHeight() - y - FONT_HEIGHT_SMALL) / (FONT_HEIGHT_SMALL - 3); int visibleNodeRows = std::min(6, totalRowsAvailable); @@ -1936,7 +1993,8 @@ void drawNodeListWithExtrasScreen(OLEDDisplay *display, OLEDDisplayUiState *stat if (y + yOffset > display->getHeight() - FONT_HEIGHT_SMALL) { yOffset = rowYOffset; col++; - if (col > 1) break; + if (col > 1) + break; } } @@ -1944,13 +2002,14 @@ void drawNodeListWithExtrasScreen(OLEDDisplay *display, OLEDDisplayUiState *stat drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, rowYOffset); } - // Public screen entry for compass -static void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { +static void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ drawNodeListWithExtrasScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow); } -void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { +void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ int screenWidth = display->getWidth(); bool isLeftCol = (x < screenWidth / 2); int nameMaxWidth = columnWidth - (screenWidth > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); @@ -1970,28 +2029,27 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 double dLat = (lat2 - lat1) * DEG_TO_RAD; double dLon = (lon2 - lon1) * DEG_TO_RAD; - double a = sin(dLat / 2) * sin(dLat / 2) + - cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * - sin(dLon / 2) * sin(dLon / 2); + double a = + sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); double c = 2 * atan2(sqrt(a), sqrt(1 - a)); double distanceKm = earthRadiusKm * c; if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { double miles = distanceKm * 0.621371; if (miles < 0.1) { - snprintf(distStr, sizeof(distStr), "%dft", (int)(miles * 5280)); // show feet + snprintf(distStr, sizeof(distStr), "%dft", (int)(miles * 5280)); // show feet } else if (miles < 10.0) { - snprintf(distStr, sizeof(distStr), "%.1fmi", miles); // 1 decimal + snprintf(distStr, sizeof(distStr), "%.1fmi", miles); // 1 decimal } else { - snprintf(distStr, sizeof(distStr), "%dmi", (int)miles); // no decimal + snprintf(distStr, sizeof(distStr), "%dmi", (int)miles); // no decimal } } else { if (distanceKm < 1.0) { - snprintf(distStr, sizeof(distStr), "%dm", (int)(distanceKm * 1000)); // show meters + snprintf(distStr, sizeof(distStr), "%dm", (int)(distanceKm * 1000)); // show meters } else if (distanceKm < 10.0) { - snprintf(distStr, sizeof(distStr), "%.1fkm", distanceKm); // 1 decimal + snprintf(distStr, sizeof(distStr), "%.1fkm", distanceKm); // 1 decimal } else { - snprintf(distStr, sizeof(distStr), "%dkm", (int)distanceKm); // no decimal + snprintf(distStr, sizeof(distStr), "%dkm", (int)distanceKm); // no decimal } } } @@ -2006,12 +2064,13 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 } } -static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { +static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ drawNodeListScreen(display, state, x, y, "Distances", drawNodeDistance); } - -void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) { +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) +{ const bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); const bool isBold = config.display.heading_bold; const int xOffset = 3; @@ -2039,7 +2098,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) { const int batteryOffset = screenWidth > 128 ? 34 : 16; const int percentX = x + xOffset + batteryOffset; display->drawString(percentX, textY, percentStr); - if (isBold) display->drawString(percentX + 1, textY, percentStr); + if (isBold) + display->drawString(percentX + 1, textY, percentStr); // Time (right side) uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); @@ -2050,21 +2110,23 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) { bool isPM = hour >= 12; hour = hour % 12; - if (hour == 0) hour = 12; + if (hour == 0) + hour = 12; char timeStr[10]; snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "PM" : "AM"); int timeX = x + screenWidth - xOffset - display->getStringWidth(timeStr); display->drawString(timeX, textY, timeStr); - if (isBold) display->drawString(timeX + 1, textY, timeStr); + if (isBold) + display->drawString(timeX + 1, textY, timeStr); } display->setColor(WHITE); } - -static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { +static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); @@ -2083,7 +2145,8 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i if (config.position.fixed_position) { drawGPS(display, SCREEN_WIDTH - 44, secondRowY, gpsStatus); } else if (!gpsStatus || !gpsStatus->getIsConnected()) { - String displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + String displayLine = + config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; int posX = SCREEN_WIDTH - display->getStringWidth(displayLine) - 2; display->drawString(posX, secondRowY, displayLine); } else { @@ -2096,7 +2159,7 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i // === Third Row: LongName Centered === meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { - const char* longName = ourNode->user.long_name; + const char *longName = ourNode->user.long_name; int textWidth = display->getStringWidth(longName); int nameX = (SCREEN_WIDTH - textWidth) / 2; int nameY = y + (FONT_HEIGHT_SMALL + 1) * 2; @@ -2112,8 +2175,14 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i snprintf(uptimeStr, sizeof(uptimeStr), "?"); } else { snprintf(uptimeStr, sizeof(uptimeStr), "%d%c", - days ? days : hours ? hours : minutes ? minutes : (int)uptime, - days ? 'd' : hours ? 'h' : minutes ? 'm' : 's'); + days ? days + : hours ? hours + : minutes ? minutes + : (int)uptime, + days ? 'd' + : hours ? 'h' + : minutes ? 'm' + : 's'); } char uptimeFullStr[16]; @@ -2122,6 +2191,155 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i int uptimeY = y + (FONT_HEIGHT_SMALL + 1) * 3; display->drawString(uptimeX, uptimeY, uptimeFullStr); } + +// **************************** +// * Device Focused Screen * +// **************************** +static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // === Header === + drawCommonHeader(display, x, y); + + // === First Row: Node and GPS === + bool origBold = config.display.heading_bold; + config.display.heading_bold = false; + + drawNodes(display, x, compactFirstLine + 3, nodeStatus); + +#if HAS_GPS + if (config.position.fixed_position) { + drawGPS(display, SCREEN_WIDTH - 44, compactFirstLine + 3, gpsStatus); + } else if (!gpsStatus || !gpsStatus->getIsConnected()) { + String displayLine = + config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + int posX = SCREEN_WIDTH - display->getStringWidth(displayLine) - 2; + display->drawString(posX, compactFirstLine + 3, displayLine); + } else { + drawGPS(display, SCREEN_WIDTH - 44, compactFirstLine + 3, gpsStatus); + } +#endif + + config.display.heading_bold = origBold; + + // === Second Row: MAC ID and Channel Utilization === + + // Get our hardware ID + uint8_t dmac[6]; + getMacAddr(dmac); + snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); + +#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ + !defined(DISPLAY_FORCE_SMALL_FONTS) + display->drawFastImage(x, compactSecondLine + 3, 12, 8, imgInfoL1); + display->drawFastImage(x, compactSecondLine + 11, 12, 8, imgInfoL2); +#else + display->drawFastImage(x, compactSecondLine + 2, 8, 8, imgInfo); +#endif + + display->drawString(x + 14, compactSecondLine, ourId); + + // Display Channel Utilization + char chUtil[13]; + snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), compactSecondLine, chUtil); + + // === Third Row: LongName Centered === + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { + const char *longName = ourNode->user.long_name; + int textWidth = display->getStringWidth(longName); + int nameX = (SCREEN_WIDTH - textWidth) / 2; + int nameY = y + (FONT_HEIGHT_SMALL + 1) * 3; + display->drawString(nameX, compactThirdLine, longName); + } + + // === Fourth Row: Uptime === + uint32_t uptime = millis() / 1000; + char uptimeStr[6]; + uint32_t minutes = uptime / 60, hours = minutes / 60, days = hours / 24; + + if (days > 365) { + snprintf(uptimeStr, sizeof(uptimeStr), "?"); + } else { + snprintf(uptimeStr, sizeof(uptimeStr), "%d%c", + days ? days + : hours ? hours + : minutes ? minutes + : (int)uptime, + days ? 'd' + : hours ? 'h' + : minutes ? 'm' + : 's'); + } + + char uptimeFullStr[16]; + snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); + int uptimeX = (SCREEN_WIDTH - display->getStringWidth(uptimeFullStr)) / 2; + display->drawString(uptimeX, compactFourthLine, uptimeFullStr); +} + +// **************************** +// * LoRa Focused Screen * +// **************************** +static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // === Header === + drawCommonHeader(display, x, y); + + // === First Row: MAC ID and Region === + bool origBold = config.display.heading_bold; + config.display.heading_bold = false; + + // Get our hardware ID + uint8_t dmac[6]; + getMacAddr(dmac); + snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); + +#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ + !defined(DISPLAY_FORCE_SMALL_FONTS) + display->drawFastImage(x, compactFirstLine + 5, 12, 8, imgInfoL1); + display->drawFastImage(x, compactFirstLine + 12, 12, 8, imgInfoL2); +#else + display->drawFastImage(x, compactFirstLine + 3, 8, 8, imgInfo); +#endif + + display->drawString(x + 14, compactFirstLine, ourId); + + const char *region = myRegion ? myRegion->name : NULL; + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(region), compactFirstLine, region); + + config.display.heading_bold = origBold; + + // === Second Row: Channel and Channel Utilization === + char chUtil[13]; + snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); + display->drawString(x, compactSecondLine, chUtil); + + char channelStr[20]; + { + snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); + } + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(channelStr), compactSecondLine, channelStr); + + // === Third Row: Node Name === + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { + const char *longName = ourNode->user.long_name; + int uptimeX = (SCREEN_WIDTH - display->getStringWidth(longName)) / 2; + display->drawString(uptimeX, compactThirdLine, longName); + } +} + // **************************** // * BatteryDeviceLoRa Screen * // **************************** @@ -2239,7 +2457,8 @@ static void drawActivity(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ // **************************** // * My Position Screen * // **************************** -static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { +static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); @@ -2258,11 +2477,8 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat #if HAS_GPS // === Update GeoCoord === - geoCoord.updateCoords( - int32_t(gpsStatus->getLatitude()), - int32_t(gpsStatus->getLongitude()), - int32_t(gpsStatus->getAltitude()) - ); + geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()), + int32_t(gpsStatus->getAltitude())); // === Determine Compass Heading === float heading; @@ -2329,7 +2545,8 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat // **************************** // * Memory Screen * // **************************** -static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { +static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ display->clear(); display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -2341,7 +2558,7 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in const int highlightHeight = FONT_HEIGHT_SMALL - 1; const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; const int screenWidth = display->getWidth(); - const char* titleStr = (screenWidth > 128) ? "Memory" : "Mem"; + const char *titleStr = (screenWidth > 128) ? "Memory" : "Mem"; const int centerX = x + SCREEN_WIDTH / 2; if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { @@ -2366,8 +2583,9 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in int rowY = y + rowYOffset; - auto drawUsageRow = [&](const char* label, uint32_t used, uint32_t total) { - if (total == 0) return; + auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total) { + if (total == 0) + return; int percent = (used * 100) / total; @@ -2380,7 +2598,8 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in int textWidth = display->getStringWidth(combinedStr); int adjustedBarWidth = SCREEN_WIDTH - barX - textWidth - 6; - if (adjustedBarWidth < 10) adjustedBarWidth = 10; // prevent weird bar if display is too small + if (adjustedBarWidth < 10) + adjustedBarWidth = 10; // prevent weird bar if display is too small int fillWidth = (used * adjustedBarWidth) / total; @@ -2393,7 +2612,8 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in display->setColor(WHITE); display->drawRect(barX, barY, adjustedBarWidth, barHeight); - if (percent >= 80) display->setColor(BLACK); + if (percent >= 80) + display->setColor(BLACK); display->fillRect(barX, barY, fillWidth, barHeight); display->setColor(WHITE); @@ -2412,31 +2632,32 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in uint32_t psramTotal = memGet.getPsramSize(); uint32_t flashUsed = 0, flashTotal = 0; - #ifdef ESP32 - flashUsed = FSCom.usedBytes(); - flashTotal = FSCom.totalBytes(); - #endif +#ifdef ESP32 + flashUsed = FSCom.usedBytes(); + flashTotal = FSCom.totalBytes(); +#endif uint32_t sdUsed = 0, sdTotal = 0; bool hasSD = false; - #ifdef HAS_SDCARD - hasSD = SD.cardType() != CARD_NONE; - if (hasSD) { - sdUsed = SD.usedBytes(); - sdTotal = SD.totalBytes(); - } - #endif +#ifdef HAS_SDCARD + hasSD = SD.cardType() != CARD_NONE; + if (hasSD) { + sdUsed = SD.usedBytes(); + sdTotal = SD.totalBytes(); + } +#endif // === Draw memory rows drawUsageRow("Heap:", heapUsed, heapTotal); drawUsageRow("PSRAM:", psramUsed, psramTotal); - #ifdef ESP32 - if (flashTotal > 0) drawUsageRow("Flash:", flashUsed, flashTotal); - #endif - if (hasSD && sdTotal > 0) drawUsageRow("SD:", sdUsed, sdTotal); +#ifdef ESP32 + if (flashTotal > 0) + drawUsageRow("Flash:", flashUsed, flashTotal); +#endif + if (hasSD && sdTotal > 0) + drawUsageRow("SD:", sdUsed, sdTotal); } - #if defined(ESP_PLATFORM) && defined(USE_ST7789) SPIClass SPI1(HSPI); #endif @@ -3063,32 +3284,34 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = drawTextMessageFrame; } - normalFrames[numframes++] = drawDefaultScreen; + // normalFrames[numframes++] = drawDefaultScreen; + normalFrames[numframes++] = drawDeviceFocused; normalFrames[numframes++] = drawLastHeardScreen; normalFrames[numframes++] = drawDistanceScreen; - normalFrames[numframes++] = drawNodeListWithCompasses; + normalFrames[numframes++] = drawNodeListWithCompasses; normalFrames[numframes++] = drawHopSignalScreen; - normalFrames[numframes++] = drawBatteryDeviceLoRa; + // normalFrames[numframes++] = drawBatteryDeviceLoRa; + normalFrames[numframes++] = drawLoRaFocused; normalFrames[numframes++] = drawCompassAndLocationScreen; normalFrames[numframes++] = drawMemoryScreen; normalFrames[numframes++] = drawActivity; // then all the nodes // We only show a few nodes in our scrolling list - because meshes with many nodes would have too many screens - size_t numToShow = min(numMeshNodes, 4U); - for (size_t i = 0; i < numToShow; i++) - normalFrames[numframes++] = drawNodeInfo; + // size_t numToShow = min(numMeshNodes, 4U); + // for (size_t i = 0; i < numToShow; i++) + // normalFrames[numframes++] = drawNodeInfo; // then the debug info // // Since frames are basic function pointers, we have to use a helper to // call a method on debugInfo object. - fsi.positions.log = numframes; - normalFrames[numframes++] = &Screen::drawDebugInfoTrampoline; + // fsi.positions.log = numframes; + // normalFrames[numframes++] = &Screen::drawDebugInfoTrampoline; // call a method on debugInfoScreen object (for more details) - fsi.positions.settings = numframes; - normalFrames[numframes++] = &Screen::drawDebugInfoSettingsTrampoline; + // fsi.positions.settings = numframes; + // normalFrames[numframes++] = &Screen::drawDebugInfoSettingsTrampoline; fsi.positions.wifi = numframes; #if HAS_WIFI && !defined(ARCH_PORTDUINO) From 71f774aa37ff0b82014bda968269f53a308c8461 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 30 Mar 2025 23:47:56 -0400 Subject: [PATCH 018/265] Adde Lora screen tittle and removed old code --- src/graphics/Screen.cpp | 166 +++++----------------------------------- 1 file changed, 19 insertions(+), 147 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index dd925b0fc..b536c18b0 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2125,73 +2125,6 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) display->setColor(WHITE); } -static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - - // === Header === - drawCommonHeader(display, x, y); - - // === Second Row: Node and GPS === - bool origBold = config.display.heading_bold; - config.display.heading_bold = false; - - int secondRowY = y + FONT_HEIGHT_SMALL + 1; - drawNodes(display, x, secondRowY, nodeStatus); - -#if HAS_GPS - if (config.position.fixed_position) { - drawGPS(display, SCREEN_WIDTH - 44, secondRowY, gpsStatus); - } else if (!gpsStatus || !gpsStatus->getIsConnected()) { - String displayLine = - config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; - int posX = SCREEN_WIDTH - display->getStringWidth(displayLine) - 2; - display->drawString(posX, secondRowY, displayLine); - } else { - drawGPS(display, SCREEN_WIDTH - 44, secondRowY, gpsStatus); - } -#endif - - config.display.heading_bold = origBold; - - // === Third Row: LongName Centered === - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { - const char *longName = ourNode->user.long_name; - int textWidth = display->getStringWidth(longName); - int nameX = (SCREEN_WIDTH - textWidth) / 2; - int nameY = y + (FONT_HEIGHT_SMALL + 1) * 2; - display->drawString(nameX, nameY, longName); - } - - // === Fourth Row: Uptime === - uint32_t uptime = millis() / 1000; - char uptimeStr[6]; - uint32_t minutes = uptime / 60, hours = minutes / 60, days = hours / 24; - - if (days > 365) { - snprintf(uptimeStr, sizeof(uptimeStr), "?"); - } else { - snprintf(uptimeStr, sizeof(uptimeStr), "%d%c", - days ? days - : hours ? hours - : minutes ? minutes - : (int)uptime, - days ? 'd' - : hours ? 'h' - : minutes ? 'm' - : 's'); - } - - char uptimeFullStr[16]; - snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); - int uptimeX = (SCREEN_WIDTH - display->getStringWidth(uptimeFullStr)) / 2; - int uptimeY = y + (FONT_HEIGHT_SMALL + 1) * 3; - display->drawString(uptimeX, uptimeY, uptimeFullStr); -} - // **************************** // * Device Focused Screen * // **************************** @@ -2295,6 +2228,25 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int // === Header === drawCommonHeader(display, x, y); + // === Draw title === + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int screenWidth = display->getWidth(); + const char *titleStr = (screenWidth > 128) ? "LoRa Info" : "LoRa"; + const int centerX = x + SCREEN_WIDTH / 2; + + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->setColor(BLACK); + } + + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(centerX, textY, titleStr); + if (config.display.heading_bold) { + display->drawString(centerX + 1, textY, titleStr); + } + display->setColor(WHITE); + display->setTextAlignment(TEXT_ALIGN_LEFT); + // === First Row: MAC ID and Region === bool origBold = config.display.heading_bold; config.display.heading_bold = false; @@ -2340,84 +2292,6 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int } } -// **************************** -// * BatteryDeviceLoRa Screen * -// **************************** -static void drawBatteryDeviceLoRa(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - - // === Header === - drawCommonHeader(display, x, y); - - // === Second Row: MAC ID and Region === - bool origBold = config.display.heading_bold; - config.display.heading_bold = false; - - int secondRowY = y + FONT_HEIGHT_SMALL + 1; - - // Get our hardware ID - uint8_t dmac[6]; - getMacAddr(dmac); - snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); - -#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ - !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x, y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgInfoL1); - display->drawFastImage(x, y + 11 + FONT_HEIGHT_SMALL, 12, 8, imgInfoL2); -#else - display->drawFastImage(x, y + 2 + FONT_HEIGHT_SMALL, 8, 8, imgInfo); -#endif - - display->drawString(x + 14, secondRowY, ourId); - - const char *region = myRegion ? myRegion->name : NULL; - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(region), secondRowY, region); - - config.display.heading_bold = origBold; - - // === Third Row: Channel and Channel Utilization === - int thirdRowY = y + (FONT_HEIGHT_SMALL * 2) + 1; - char channelStr[20]; - { - snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); - } - display->drawString(x, thirdRowY, channelStr); - - // Display Channel Utilization - char chUtil[13]; - snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), thirdRowY, chUtil); - - // === Fourth Row: Uptime === - uint32_t uptime = millis() / 1000; - char uptimeStr[6]; - uint32_t minutes = uptime / 60, hours = minutes / 60, days = hours / 24; - - if (days > 365) { - snprintf(uptimeStr, sizeof(uptimeStr), "?"); - } else { - snprintf(uptimeStr, sizeof(uptimeStr), "%d%c", - days ? days - : hours ? hours - : minutes ? minutes - : (int)uptime, - days ? 'd' - : hours ? 'h' - : minutes ? 'm' - : 's'); - } - - char uptimeFullStr[16]; - snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); - int uptimeX = (SCREEN_WIDTH - display->getStringWidth(uptimeFullStr)) / 2; - int uptimeY = y + (FONT_HEIGHT_SMALL + 1) * 3; - display->drawString(uptimeX, uptimeY, uptimeFullStr); -} - // **************************** // * Activity Screen * // **************************** @@ -3284,13 +3158,11 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = drawTextMessageFrame; } - // normalFrames[numframes++] = drawDefaultScreen; normalFrames[numframes++] = drawDeviceFocused; normalFrames[numframes++] = drawLastHeardScreen; normalFrames[numframes++] = drawDistanceScreen; normalFrames[numframes++] = drawNodeListWithCompasses; normalFrames[numframes++] = drawHopSignalScreen; - // normalFrames[numframes++] = drawBatteryDeviceLoRa; normalFrames[numframes++] = drawLoRaFocused; normalFrames[numframes++] = drawCompassAndLocationScreen; normalFrames[numframes++] = drawMemoryScreen; From ca1e09d7805917d13cb1e6e9f92531235963a8fa Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 31 Mar 2025 00:33:01 -0400 Subject: [PATCH 019/265] Added GPS tittle --- src/graphics/Screen.cpp | 49 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index b536c18b0..ecd1a8b1d 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2340,16 +2340,55 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat // === Header === drawCommonHeader(display, x, y); + // === Draw title === + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int screenWidth = display->getWidth(); + const char *titleStr = "GPS"; + const int centerX = x + SCREEN_WIDTH / 2; + + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->setColor(BLACK); + } + + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(centerX, textY, titleStr); + if (config.display.heading_bold) { + display->drawString(centerX + 1, textY, titleStr); + } + display->setColor(WHITE); + display->setTextAlignment(TEXT_ALIGN_LEFT); + // Row Y offset just like drawNodeListScreen int rowYOffset = FONT_HEIGHT_SMALL - 3; int rowY = y + rowYOffset; - // === Second Row: My Location === - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawString(x + 2, rowY, "My Location:"); - display->setTextAlignment(TEXT_ALIGN_LEFT); - +// === Second Row: My Location === #if HAS_GPS + if (config.position.fixed_position) { + display->drawString(x + 2, compactFirstLine, "Sat:"); + if (screenWidth > 128) { + drawGPS(display, x + 32, compactFirstLine + 3, gpsStatus); + } else { + drawGPS(display, x + 23, compactFirstLine + 3, gpsStatus); + } + } else if (!gpsStatus || !gpsStatus->getIsConnected()) { + String displayLine = + config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + display->drawString(x + 2, compactFirstLine, "Sat:"); + if (screenWidth > 128) { + display->drawString(x + 32, compactFirstLine + 3, displayLine); + } else { + display->drawString(x + 23, compactFirstLine + 3, displayLine); + } + } else { + display->drawString(x + 2, compactFirstLine, "Sat:"); + if (screenWidth > 128) { + drawGPS(display, x + 32, compactFirstLine + 3, gpsStatus); + } else { + drawGPS(display, x + 23, compactFirstLine + 3, gpsStatus); + } + } // === Update GeoCoord === geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()), int32_t(gpsStatus->getAltitude())); From 9993306751183a0a139606034489f035a9bbb137 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 31 Mar 2025 14:34:01 -0400 Subject: [PATCH 020/265] Add heap leak counter will be removed to be toggled in the future. --- src/graphics/Screen.cpp | 66 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 5d4205e8e..a534c0698 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2488,7 +2488,6 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in // === Layout === const int rowYOffset = FONT_HEIGHT_SMALL - 3; const int barHeight = 6; - const int labelX = x; const int barsOffset = (screenWidth > 128) ? 24 : 0; const int barX = x + 40 + barsOffset; @@ -2496,9 +2495,14 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in int rowY = y + rowYOffset; - auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total) { - if (total == 0) - return; + // === Heap delta tracking === + static uint32_t previousHeapFree = 0; + static int32_t lastHeapDelta = 0; + static int32_t totalHeapDelta = 0; + static int deltaChangeCount = 0; + + auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { + if (total == 0) return; int percent = (used * 100) / total; @@ -2535,6 +2539,42 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in display->drawString(SCREEN_WIDTH - 2, rowY, combinedStr); rowY += rowYOffset; + + // === Heap delta (inline with heap row only) + if (isHeap && previousHeapFree > 0) { + int32_t delta = (int32_t)(memGet.getFreeHeap() - previousHeapFree); + lastHeapDelta = delta; + + if (delta != 0) { + totalHeapDelta += delta; + deltaChangeCount++; + + char deltaStr[16]; + snprintf(deltaStr, sizeof(deltaStr), "%ld", delta); + + int deltaX = centerX - display->getStringWidth(deltaStr) / 2 - 8; + int deltaY = rowY + 1; + + // Triangle + if (delta > 0) { + display->drawLine(deltaX, deltaY + 6, deltaX + 3, deltaY); + display->drawLine(deltaX + 3, deltaY, deltaX + 6, deltaY + 6); + display->drawLine(deltaX, deltaY + 6, deltaX + 6, deltaY + 6); + } else { + display->drawLine(deltaX, deltaY, deltaX + 3, deltaY + 6); + display->drawLine(deltaX + 3, deltaY + 6, deltaX + 6, deltaY); + display->drawLine(deltaX, deltaY, deltaX + 6, deltaY); + } + + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(centerX + 6, deltaY, deltaStr); + rowY += rowYOffset; + } + } + + if (isHeap) { + previousHeapFree = memGet.getFreeHeap(); + } }; // === Memory values === @@ -2561,7 +2601,7 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in #endif // === Draw memory rows - drawUsageRow("Heap:", heapUsed, heapTotal); + drawUsageRow("Heap:", heapUsed, heapTotal, true); drawUsageRow("PSRAM:", psramUsed, psramTotal); #ifdef ESP32 if (flashTotal > 0) @@ -2569,6 +2609,22 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in #endif if (hasSD && sdTotal > 0) drawUsageRow("SD:", sdUsed, sdTotal); + + // === Final summary (always visible) + char summaryStr[32]; + snprintf(summaryStr, sizeof(summaryStr), "seen: %dx total: %ldB", deltaChangeCount, totalHeapDelta); + + // Draw triangle manually + int triY = rowY + 4; + int triX = centerX - display->getStringWidth(summaryStr) / 2 - 8; + + display->drawLine(triX, triY + 6, triX + 3, triY); + display->drawLine(triX + 3, triY, triX + 6, triY + 6); + display->drawLine(triX, triY + 6, triX + 6, triY + 6); + + // Draw the text to the right of the triangle + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawString(triX + 10, rowY + 2, summaryStr); } #if defined(ESP_PLATFORM) && defined(USE_ST7789) From 7554ff6c57926fef8bc8d740da1bac52b6f89650 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 1 Apr 2025 01:43:01 -0400 Subject: [PATCH 021/265] Update Screen.cpp --- src/graphics/Screen.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index a534c0698..623ca484a 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2008,6 +2008,10 @@ static void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState * drawNodeListWithExtrasScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow); } +// ******************************** +// * Node List Distance Screen * +// ******************************** + void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { int screenWidth = display->getWidth(); @@ -2069,6 +2073,10 @@ static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, drawNodeListScreen(display, state, x, y, "Distances", drawNodeDistance); } +// *********************** +// * Common Header * +// *********************** + void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) { const bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); From bb961e855e7fb572dc68ea371f3f716ad6ea7312 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 1 Apr 2025 01:49:06 -0400 Subject: [PATCH 022/265] Update Screen.cpp --- src/graphics/Screen.cpp | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 623ca484a..924b09393 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2153,14 +2153,22 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i #if HAS_GPS if (config.position.fixed_position) { - drawGPS(display, SCREEN_WIDTH - 44, compactFirstLine + 3, gpsStatus); + if (SCREEN_WIDTH > 128) { + drawGPS(display, SCREEN_WIDTH - 53, compactFirstLine + 3, gpsStatus); + } else { + drawGPS(display, SCREEN_WIDTH - 47, compactFirstLine + 3, gpsStatus); + } } else if (!gpsStatus || !gpsStatus->getIsConnected()) { String displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; int posX = SCREEN_WIDTH - display->getStringWidth(displayLine) - 2; display->drawString(posX, compactFirstLine + 3, displayLine); } else { - drawGPS(display, SCREEN_WIDTH - 44, compactFirstLine + 3, gpsStatus); + if (SCREEN_WIDTH > 128) { + drawGPS(display, SCREEN_WIDTH - 53, compactFirstLine + 3, gpsStatus); + } else { + drawGPS(display, SCREEN_WIDTH - 47, compactFirstLine + 3, gpsStatus); + } } #endif @@ -2291,13 +2299,21 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int } display->drawString(x + SCREEN_WIDTH - display->getStringWidth(channelStr), compactSecondLine, channelStr); - // === Third Row: Node Name === + // === Third Row: Node longName === meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { const char *longName = ourNode->user.long_name; int uptimeX = (SCREEN_WIDTH - display->getStringWidth(longName)) / 2; display->drawString(uptimeX, compactThirdLine, longName); } + + // === Fourth Row: Node shortName === + char buf[25]; + snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); + if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { + int uptimeX = (SCREEN_WIDTH - display->getStringWidth(owner.short_name)) / 2; + display->drawString(uptimeX, compactFourthLine, owner.short_name); + } } // **************************** @@ -2332,9 +2348,8 @@ static void drawActivity(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ // Restore default color after drawing display->setColor(WHITE); - // === Second Row: Draw any log messages === - int secondRowY = y + FONT_HEIGHT_SMALL + 1; - display->drawLogBuffer(x, secondRowY); + // === First Line: Draw any log messages === + display->drawLogBuffer(x, compactFirstLine); } // **************************** // * My Position Screen * @@ -2373,6 +2388,8 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat // === Second Row: My Location === #if HAS_GPS + bool origBold = config.display.heading_bold; + config.display.heading_bold = false; if (config.position.fixed_position) { display->drawString(x + 2, compactFirstLine, "Sat:"); if (screenWidth > 128) { @@ -2397,6 +2414,7 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat drawGPS(display, x + 23, compactFirstLine + 3, gpsStatus); } } + config.display.heading_bold = origBold; // === Update GeoCoord === geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()), int32_t(gpsStatus->getAltitude())); From 6ee7644070e6fce6789a8428a816586b1e9b4ec7 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 1 Apr 2025 03:55:43 -0400 Subject: [PATCH 023/265] Offset everything by 1 down --- src/graphics/Screen.cpp | 131 +++++++++++++++++++++++----------------- 1 file changed, 76 insertions(+), 55 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 924b09393..9bdaefd8f 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2076,9 +2076,11 @@ static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, // *********************** // * Common Header * // *********************** - void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) { + constexpr int HEADER_OFFSET_Y = 1; + y += HEADER_OFFSET_Y; + const bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); const bool isBold = config.display.heading_bold; const int xOffset = 3; @@ -2088,28 +2090,35 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); - // Draw background highlight + // === Background highlight === if (isInverted) { drawRoundedHighlight(display, x, y, screenWidth, highlightHeight, 2); display->setColor(BLACK); } - // Battery icon - drawBattery(display, x + xOffset - 2, y + 2, imgBattery, powerStatus); - - // Text baseline + // === Text baseline === const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - // Battery % + // === Battery icon (scale-aware vertical centering) === + int batteryScale = 1; + if (screenWidth >= 200) batteryScale = 2; + if (screenWidth >= 300) batteryScale = 2; // Just in case + + int batteryHeight = 8 * batteryScale; + int batteryY = y + (highlightHeight - batteryHeight) / 2; + drawBattery(display, x + xOffset - 2, batteryY, imgBattery, powerStatus); + + // === Battery % text === char percentStr[8]; snprintf(percentStr, sizeof(percentStr), "%d%%", powerStatus->getBatteryChargePercent()); + const int batteryOffset = screenWidth > 128 ? 34 : 16; const int percentX = x + xOffset + batteryOffset; display->drawString(percentX, textY, percentStr); if (isBold) display->drawString(percentX + 1, textY, percentStr); - // Time (right side) + // === Time string (right-aligned) === uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); if (rtc_sec > 0) { long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; @@ -2124,7 +2133,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) char timeStr[10]; snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "PM" : "AM"); - int timeX = x + screenWidth - xOffset - display->getStringWidth(timeStr); + int timeX = screenWidth - xOffset - display->getStringWidth(timeStr) - 1; display->drawString(timeX, textY, timeStr); if (isBold) display->drawString(timeX + 1, textY, timeStr); @@ -2133,6 +2142,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) display->setColor(WHITE); } + // **************************** // * Device Focused Screen * // **************************** @@ -2145,29 +2155,32 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i // === Header === drawCommonHeader(display, x, y); + // === Content below header === + const int layoutYOffset = 1; + // === First Row: Node and GPS === bool origBold = config.display.heading_bold; config.display.heading_bold = false; - drawNodes(display, x, compactFirstLine + 3, nodeStatus); + drawNodes(display, x, compactFirstLine + 3 + layoutYOffset, nodeStatus); #if HAS_GPS if (config.position.fixed_position) { if (SCREEN_WIDTH > 128) { - drawGPS(display, SCREEN_WIDTH - 53, compactFirstLine + 3, gpsStatus); + drawGPS(display, SCREEN_WIDTH - 53, compactFirstLine + 3 + layoutYOffset, gpsStatus); } else { - drawGPS(display, SCREEN_WIDTH - 47, compactFirstLine + 3, gpsStatus); + drawGPS(display, SCREEN_WIDTH - 47, compactFirstLine + 3 + layoutYOffset, gpsStatus); } } else if (!gpsStatus || !gpsStatus->getIsConnected()) { String displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; int posX = SCREEN_WIDTH - display->getStringWidth(displayLine) - 2; - display->drawString(posX, compactFirstLine + 3, displayLine); + display->drawString(posX, compactFirstLine + 3 + layoutYOffset, displayLine); } else { if (SCREEN_WIDTH > 128) { - drawGPS(display, SCREEN_WIDTH - 53, compactFirstLine + 3, gpsStatus); + drawGPS(display, SCREEN_WIDTH - 53, compactFirstLine + 3 + layoutYOffset, gpsStatus); } else { - drawGPS(display, SCREEN_WIDTH - 47, compactFirstLine + 3, gpsStatus); + drawGPS(display, SCREEN_WIDTH - 47, compactFirstLine + 3 + layoutYOffset, gpsStatus); } } #endif @@ -2184,18 +2197,18 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x, compactSecondLine + 3, 12, 8, imgInfoL1); - display->drawFastImage(x, compactSecondLine + 11, 12, 8, imgInfoL2); + display->drawFastImage(x, compactSecondLine + 3 + layoutYOffset, 12, 8, imgInfoL1); + display->drawFastImage(x, compactSecondLine + 11 + layoutYOffset, 12, 8, imgInfoL2); #else - display->drawFastImage(x, compactSecondLine + 2, 8, 8, imgInfo); + display->drawFastImage(x, compactSecondLine + 2 + layoutYOffset, 8, 8, imgInfo); #endif - display->drawString(x + 14, compactSecondLine, ourId); + display->drawString(x + 14, compactSecondLine + layoutYOffset, ourId); // Display Channel Utilization char chUtil[13]; snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), compactSecondLine, chUtil); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), compactSecondLine + layoutYOffset, chUtil); // === Third Row: LongName Centered === meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); @@ -2203,8 +2216,7 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i const char *longName = ourNode->user.long_name; int textWidth = display->getStringWidth(longName); int nameX = (SCREEN_WIDTH - textWidth) / 2; - int nameY = y + (FONT_HEIGHT_SMALL + 1) * 3; - display->drawString(nameX, compactThirdLine, longName); + display->drawString(nameX, compactThirdLine + layoutYOffset, longName); } // === Fourth Row: Uptime === @@ -2229,7 +2241,7 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i char uptimeFullStr[16]; snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); int uptimeX = (SCREEN_WIDTH - display->getStringWidth(uptimeFullStr)) / 2; - display->drawString(uptimeX, compactFourthLine, uptimeFullStr); + display->drawString(uptimeX, compactFourthLine + layoutYOffset, uptimeFullStr); } // **************************** @@ -2244,9 +2256,12 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int // === Header === drawCommonHeader(display, x, y); - // === Draw title === + // === Adjust layout to match shifted header === + const int layoutYOffset = 1; + + // === Draw title (aligned with header baseline) === const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int textY = y + layoutYOffset + (highlightHeight - FONT_HEIGHT_SMALL) / 2; const int screenWidth = display->getWidth(); const char *titleStr = (screenWidth > 128) ? "LoRa Info" : "LoRa"; const int centerX = x + SCREEN_WIDTH / 2; @@ -2275,44 +2290,42 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x, compactFirstLine + 5, 12, 8, imgInfoL1); - display->drawFastImage(x, compactFirstLine + 12, 12, 8, imgInfoL2); + display->drawFastImage(x, compactFirstLine + 5 + layoutYOffset, 12, 8, imgInfoL1); + display->drawFastImage(x, compactFirstLine + 12 + layoutYOffset, 12, 8, imgInfoL2); #else - display->drawFastImage(x, compactFirstLine + 3, 8, 8, imgInfo); + display->drawFastImage(x, compactFirstLine + 3 + layoutYOffset, 8, 8, imgInfo); #endif - display->drawString(x + 14, compactFirstLine, ourId); + display->drawString(x + 14, compactFirstLine + layoutYOffset, ourId); const char *region = myRegion ? myRegion->name : NULL; - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(region), compactFirstLine, region); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(region), compactFirstLine + layoutYOffset, region); config.display.heading_bold = origBold; // === Second Row: Channel and Channel Utilization === char chUtil[13]; snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); - display->drawString(x, compactSecondLine, chUtil); + display->drawString(x, compactSecondLine + layoutYOffset, chUtil); char channelStr[20]; - { - snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); - } - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(channelStr), compactSecondLine, channelStr); + snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(channelStr), compactSecondLine + layoutYOffset, channelStr); // === Third Row: Node longName === meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { const char *longName = ourNode->user.long_name; - int uptimeX = (SCREEN_WIDTH - display->getStringWidth(longName)) / 2; - display->drawString(uptimeX, compactThirdLine, longName); + int nameX = (SCREEN_WIDTH - display->getStringWidth(longName)) / 2; + display->drawString(nameX, compactThirdLine + layoutYOffset, longName); } // === Fourth Row: Node shortName === char buf[25]; snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { - int uptimeX = (SCREEN_WIDTH - display->getStringWidth(owner.short_name)) / 2; - display->drawString(uptimeX, compactFourthLine, owner.short_name); + int shortNameX = (SCREEN_WIDTH - display->getStringWidth(owner.short_name)) / 2; + display->drawString(shortNameX, compactFourthLine + layoutYOffset, owner.short_name); } } @@ -2329,8 +2342,9 @@ static void drawActivity(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ drawCommonHeader(display, x, y); // === Draw title === + const int layoutYOffset = 1; const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int textY = y + layoutYOffset + (highlightHeight - FONT_HEIGHT_SMALL) / 2; // Use black text if display is inverted if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { @@ -2349,8 +2363,9 @@ static void drawActivity(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ display->setColor(WHITE); // === First Line: Draw any log messages === - display->drawLogBuffer(x, compactFirstLine); + display->drawLogBuffer(x, compactFirstLine + layoutYOffset); } + // **************************** // * My Position Screen * // **************************** @@ -2363,9 +2378,12 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat // === Header === drawCommonHeader(display, x, y); + // === Adjust layout to match shifted header === + const int layoutYOffset = 1; + // === Draw title === const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int textY = y + layoutYOffset + (highlightHeight - FONT_HEIGHT_SMALL) / 2; const int screenWidth = display->getWidth(); const char *titleStr = "GPS"; const int centerX = x + SCREEN_WIDTH / 2; @@ -2384,37 +2402,38 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat // Row Y offset just like drawNodeListScreen int rowYOffset = FONT_HEIGHT_SMALL - 3; - int rowY = y + rowYOffset; + int rowY = y + rowYOffset + layoutYOffset; -// === Second Row: My Location === + // === Second Row: My Location === #if HAS_GPS bool origBold = config.display.heading_bold; config.display.heading_bold = false; if (config.position.fixed_position) { - display->drawString(x + 2, compactFirstLine, "Sat:"); + display->drawString(x + 2, compactFirstLine + layoutYOffset, "Sat:"); if (screenWidth > 128) { - drawGPS(display, x + 32, compactFirstLine + 3, gpsStatus); + drawGPS(display, x + 32, compactFirstLine + 3 + layoutYOffset, gpsStatus); } else { - drawGPS(display, x + 23, compactFirstLine + 3, gpsStatus); + drawGPS(display, x + 23, compactFirstLine + 3 + layoutYOffset, gpsStatus); } } else if (!gpsStatus || !gpsStatus->getIsConnected()) { String displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; - display->drawString(x + 2, compactFirstLine, "Sat:"); + display->drawString(x + 2, compactFirstLine + layoutYOffset, "Sat:"); if (screenWidth > 128) { - display->drawString(x + 32, compactFirstLine + 3, displayLine); + display->drawString(x + 32, compactFirstLine + 3 + layoutYOffset, displayLine); } else { - display->drawString(x + 23, compactFirstLine + 3, displayLine); + display->drawString(x + 23, compactFirstLine + 3 + layoutYOffset, displayLine); } } else { - display->drawString(x + 2, compactFirstLine, "Sat:"); + display->drawString(x + 2, compactFirstLine + layoutYOffset, "Sat:"); if (screenWidth > 128) { - drawGPS(display, x + 32, compactFirstLine + 3, gpsStatus); + drawGPS(display, x + 32, compactFirstLine + 3 + layoutYOffset, gpsStatus); } else { - drawGPS(display, x + 23, compactFirstLine + 3, gpsStatus); + drawGPS(display, x + 23, compactFirstLine + 3 + layoutYOffset, gpsStatus); } } config.display.heading_bold = origBold; + // === Update GeoCoord === geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()), int32_t(gpsStatus->getAltitude())); @@ -2494,8 +2513,9 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in drawCommonHeader(display, x, y); // === Draw title === + const int layoutYOffset = 1; const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int textY = y + layoutYOffset + (highlightHeight - FONT_HEIGHT_SMALL) / 2; const int screenWidth = display->getWidth(); const char *titleStr = (screenWidth > 128) ? "Memory" : "Mem"; const int centerX = x + SCREEN_WIDTH / 2; @@ -2519,7 +2539,7 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in const int barX = x + 40 + barsOffset; const int textRightX = x + SCREEN_WIDTH - 2; - int rowY = y + rowYOffset; + int rowY = y + rowYOffset + layoutYOffset; // === Heap delta tracking === static uint32_t previousHeapFree = 0; @@ -2653,6 +2673,7 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in display->drawString(triX + 10, rowY + 2, summaryStr); } + #if defined(ESP_PLATFORM) && defined(USE_ST7789) SPIClass SPI1(HSPI); #endif From efc69550ef0e077372a02859c356f2cd3d330454 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 1 Apr 2025 10:12:02 -0400 Subject: [PATCH 024/265] Corrected rounding --- src/graphics/Screen.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 9bdaefd8f..31a4171f3 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1594,10 +1594,10 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, display->fillRect(x, y + r, r, h - 2 * r); display->fillRect(x + w - r, y + r, r, h - 2 * r); - // Rounded corners - display->fillCircle(x + r, y + r, r); // Top-left + // Rounded corners β€” visually balanced + display->fillCircle(x + r + 1, y + r, r); // Top-left display->fillCircle(x + w - r - 1, y + r, r); // Top-right - display->fillCircle(x + r, y + h - r - 1, r); // Bottom-left + display->fillCircle(x + r + 1, y + h - r - 1, r); // Bottom-left display->fillCircle(x + w - r - 1, y + h - r - 1, r); // Bottom-right } // h! Each node entry holds a reference to its info and how long ago it was heard from From c271515fb0860f1163ceb3bd75b6972c023f3a13 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 1 Apr 2025 11:39:55 -0400 Subject: [PATCH 025/265] Centered battery --- src/graphics/Screen.cpp | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 31a4171f3..75bb46a3e 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1586,7 +1586,9 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ screen->drawColumns(display, x, y, fields); } -// h! Makes header invert rounder +// ********************************* +// *Rounding Header when inverted * +// ********************************* void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r) { // Center rectangles @@ -1594,7 +1596,7 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, display->fillRect(x, y + r, r, h - 2 * r); display->fillRect(x + w - r, y + r, r, h - 2 * r); - // Rounded corners β€” visually balanced + // Rounded corners β€” visually balanced and centered display->fillCircle(x + r + 1, y + r, r); // Top-left display->fillCircle(x + w - r - 1, y + r, r); // Top-right display->fillCircle(x + r + 1, y + h - r - 1, r); // Bottom-left @@ -2078,7 +2080,8 @@ static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, // *********************** void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) { - constexpr int HEADER_OFFSET_Y = 1; + // Shift header down to avoid clipping on high-DPI screens + constexpr int HEADER_OFFSET_Y = 2; y += HEADER_OFFSET_Y; const bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); @@ -2102,11 +2105,15 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) // === Battery icon (scale-aware vertical centering) === int batteryScale = 1; if (screenWidth >= 200) batteryScale = 2; - if (screenWidth >= 300) batteryScale = 2; // Just in case int batteryHeight = 8 * batteryScale; - int batteryY = y + (highlightHeight - batteryHeight) / 2; - drawBattery(display, x + xOffset - 2, batteryY, imgBattery, powerStatus); + int batteryY = y + (highlightHeight / 2) - (batteryHeight / 2); + + // Only shift right 3px if screen is wider than 128 + int batteryX = x + xOffset - 2; + if (screenWidth > 128) batteryX += 2; + + drawBattery(display, batteryX, batteryY, imgBattery, powerStatus); // === Battery % text === char percentStr[8]; @@ -2127,8 +2134,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) bool isPM = hour >= 12; hour = hour % 12; - if (hour == 0) - hour = 12; + if (hour == 0) hour = 12; char timeStr[10]; snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "PM" : "AM"); @@ -2142,7 +2148,6 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) display->setColor(WHITE); } - // **************************** // * Device Focused Screen * // **************************** From c693cd59a9a06fa3b26c2a38f64ed42300ef162a Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 1 Apr 2025 13:51:21 -0400 Subject: [PATCH 026/265] Update Screen.cpp --- src/graphics/Screen.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 75bb46a3e..0663666f4 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2080,7 +2080,6 @@ static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, // *********************** void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) { - // Shift header down to avoid clipping on high-DPI screens constexpr int HEADER_OFFSET_Y = 2; y += HEADER_OFFSET_Y; From 2f8a1dba8fda07cbaaea2d76e694dc6dfbcf9675 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 2 Apr 2025 02:40:34 -0400 Subject: [PATCH 027/265] Rows adjustment --- src/graphics/Screen.cpp | 118 +++++++++++++++++++--------------------- 1 file changed, 57 insertions(+), 61 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 0663666f4..737b89802 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -120,11 +120,11 @@ static bool heartbeat = false; #define SCREEN_HEIGHT display->getHeight() // Pre-defined lines; this is intended to be used AFTER the common header -#define compactFirstLine (FONT_HEIGHT_SMALL - 3) * 1 -#define compactSecondLine (FONT_HEIGHT_SMALL - 3) * 2 -#define compactThirdLine (FONT_HEIGHT_SMALL - 3) * 3 -#define compactFourthLine (FONT_HEIGHT_SMALL - 3) * 4 -#define compactFifthLine (FONT_HEIGHT_SMALL - 3) * 5 +#define compactFirstLine ((FONT_HEIGHT_SMALL - 1) * 1) +#define compactSecondLine ((FONT_HEIGHT_SMALL - 1) * 2) - 2 +#define compactThirdLine ((FONT_HEIGHT_SMALL - 1) * 3) - 4 +#define compactFourthLine ((FONT_HEIGHT_SMALL - 1) * 4) - 6 +#define compactFifthLine ((FONT_HEIGHT_SMALL - 1) * 5) - 8 #define standardFirstLine (FONT_HEIGHT_SMALL + 1) * 1 #define standardSecondLine (FONT_HEIGHT_SMALL + 1) * 2 @@ -1181,9 +1181,9 @@ static void drawGPS(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus if (!gps->getHasLock()) { // Draw "No sats" to the right of the icon with slightly more gap int textX = x + 9; // 6 (icon) + 3px spacing - display->drawString(textX, y - 2, "No sats"); + display->drawString(textX, y - 3, "No sats"); if (config.display.heading_bold) - display->drawString(textX + 1, y - 2, "No sats"); + display->drawString(textX + 1, y - 3, "No sats"); return; } else { char satsString[3]; @@ -2103,14 +2103,16 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) // === Battery icon (scale-aware vertical centering) === int batteryScale = 1; - if (screenWidth >= 200) batteryScale = 2; + if (screenWidth >= 200) + batteryScale = 2; int batteryHeight = 8 * batteryScale; int batteryY = y + (highlightHeight / 2) - (batteryHeight / 2); // Only shift right 3px if screen is wider than 128 int batteryX = x + xOffset - 2; - if (screenWidth > 128) batteryX += 2; + if (screenWidth > 128) + batteryX += 2; drawBattery(display, batteryX, batteryY, imgBattery, powerStatus); @@ -2133,7 +2135,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) bool isPM = hour >= 12; hour = hour % 12; - if (hour == 0) hour = 12; + if (hour == 0) + hour = 12; char timeStr[10]; snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "PM" : "AM"); @@ -2160,31 +2163,30 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i drawCommonHeader(display, x, y); // === Content below header === - const int layoutYOffset = 1; // === First Row: Node and GPS === bool origBold = config.display.heading_bold; config.display.heading_bold = false; - drawNodes(display, x, compactFirstLine + 3 + layoutYOffset, nodeStatus); + drawNodes(display, x, compactFirstLine + 3, nodeStatus); #if HAS_GPS if (config.position.fixed_position) { if (SCREEN_WIDTH > 128) { - drawGPS(display, SCREEN_WIDTH - 53, compactFirstLine + 3 + layoutYOffset, gpsStatus); + drawGPS(display, SCREEN_WIDTH - 53, compactFirstLine + 3, gpsStatus); } else { - drawGPS(display, SCREEN_WIDTH - 47, compactFirstLine + 3 + layoutYOffset, gpsStatus); + drawGPS(display, SCREEN_WIDTH - 47, compactFirstLine + 3, gpsStatus); } } else if (!gpsStatus || !gpsStatus->getIsConnected()) { String displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; int posX = SCREEN_WIDTH - display->getStringWidth(displayLine) - 2; - display->drawString(posX, compactFirstLine + 3 + layoutYOffset, displayLine); + display->drawString(posX, compactFirstLine, displayLine); } else { if (SCREEN_WIDTH > 128) { - drawGPS(display, SCREEN_WIDTH - 53, compactFirstLine + 3 + layoutYOffset, gpsStatus); + drawGPS(display, SCREEN_WIDTH - 53, compactFirstLine + 3, gpsStatus); } else { - drawGPS(display, SCREEN_WIDTH - 47, compactFirstLine + 3 + layoutYOffset, gpsStatus); + drawGPS(display, SCREEN_WIDTH - 47, compactFirstLine + 3, gpsStatus); } } #endif @@ -2201,18 +2203,19 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x, compactSecondLine + 3 + layoutYOffset, 12, 8, imgInfoL1); - display->drawFastImage(x, compactSecondLine + 11 + layoutYOffset, 12, 8, imgInfoL2); + display->drawFastImage(x, compactSecondLine + 3, 12, 8, imgInfoL1); + display->drawFastImage(x, compactSecondLine + 11, 12, 8, imgInfoL2); #else - display->drawFastImage(x, compactSecondLine + 2 + layoutYOffset, 8, 8, imgInfo); + display->drawFastImage(x, compactSecondLine + 2, 8, 8, imgInfo); #endif - display->drawString(x + 14, compactSecondLine + layoutYOffset, ourId); + int i_xoffset = (SCREEN_WIDTH > 128) ? 14 : 10; + display->drawString(x + i_xoffset, compactSecondLine, ourId); // Display Channel Utilization char chUtil[13]; snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), compactSecondLine + layoutYOffset, chUtil); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), compactSecondLine, chUtil); // === Third Row: LongName Centered === meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); @@ -2220,7 +2223,7 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i const char *longName = ourNode->user.long_name; int textWidth = display->getStringWidth(longName); int nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, compactThirdLine + layoutYOffset, longName); + display->drawString(nameX, compactThirdLine, longName); } // === Fourth Row: Uptime === @@ -2245,7 +2248,7 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i char uptimeFullStr[16]; snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); int uptimeX = (SCREEN_WIDTH - display->getStringWidth(uptimeFullStr)) / 2; - display->drawString(uptimeX, compactFourthLine + layoutYOffset, uptimeFullStr); + display->drawString(uptimeX, compactFourthLine, uptimeFullStr); } // **************************** @@ -2260,12 +2263,9 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int // === Header === drawCommonHeader(display, x, y); - // === Adjust layout to match shifted header === - const int layoutYOffset = 1; - // === Draw title (aligned with header baseline) === const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + layoutYOffset + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int textY = y + 2 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; const int screenWidth = display->getWidth(); const char *titleStr = (screenWidth > 128) ? "LoRa Info" : "LoRa"; const int centerX = x + SCREEN_WIDTH / 2; @@ -2294,34 +2294,35 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x, compactFirstLine + 5 + layoutYOffset, 12, 8, imgInfoL1); - display->drawFastImage(x, compactFirstLine + 12 + layoutYOffset, 12, 8, imgInfoL2); + display->drawFastImage(x, compactFirstLine + 5, 12, 8, imgInfoL1); + display->drawFastImage(x, compactFirstLine + 12, 12, 8, imgInfoL2); #else - display->drawFastImage(x, compactFirstLine + 3 + layoutYOffset, 8, 8, imgInfo); + display->drawFastImage(x, compactFirstLine + 3, 8, 8, imgInfo); #endif - display->drawString(x + 14, compactFirstLine + layoutYOffset, ourId); + int i_xoffset = (SCREEN_WIDTH > 128) ? 14 : 10; + display->drawString(x + i_xoffset, compactFirstLine, ourId); const char *region = myRegion ? myRegion->name : NULL; - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(region), compactFirstLine + layoutYOffset, region); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(region), compactFirstLine, region); config.display.heading_bold = origBold; // === Second Row: Channel and Channel Utilization === char chUtil[13]; snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); - display->drawString(x, compactSecondLine + layoutYOffset, chUtil); + display->drawString(x, compactSecondLine, chUtil); char channelStr[20]; snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(channelStr), compactSecondLine + layoutYOffset, channelStr); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(channelStr), compactSecondLine, channelStr); // === Third Row: Node longName === meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { const char *longName = ourNode->user.long_name; int nameX = (SCREEN_WIDTH - display->getStringWidth(longName)) / 2; - display->drawString(nameX, compactThirdLine + layoutYOffset, longName); + display->drawString(nameX, compactThirdLine, longName); } // === Fourth Row: Node shortName === @@ -2329,7 +2330,7 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { int shortNameX = (SCREEN_WIDTH - display->getStringWidth(owner.short_name)) / 2; - display->drawString(shortNameX, compactFourthLine + layoutYOffset, owner.short_name); + display->drawString(shortNameX, compactFourthLine, owner.short_name); } } @@ -2346,9 +2347,8 @@ static void drawActivity(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ drawCommonHeader(display, x, y); // === Draw title === - const int layoutYOffset = 1; const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + layoutYOffset + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int textY = y + 2 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; // Use black text if display is inverted if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { @@ -2367,7 +2367,7 @@ static void drawActivity(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ display->setColor(WHITE); // === First Line: Draw any log messages === - display->drawLogBuffer(x, compactFirstLine + layoutYOffset); + display->drawLogBuffer(x, compactFirstLine); } // **************************** @@ -2382,12 +2382,9 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat // === Header === drawCommonHeader(display, x, y); - // === Adjust layout to match shifted header === - const int layoutYOffset = 1; - // === Draw title === const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + layoutYOffset + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int textY = y + 2 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; const int screenWidth = display->getWidth(); const char *titleStr = "GPS"; const int centerX = x + SCREEN_WIDTH / 2; @@ -2406,34 +2403,34 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat // Row Y offset just like drawNodeListScreen int rowYOffset = FONT_HEIGHT_SMALL - 3; - int rowY = y + rowYOffset + layoutYOffset; + int rowY = y + rowYOffset; // === Second Row: My Location === #if HAS_GPS bool origBold = config.display.heading_bold; config.display.heading_bold = false; if (config.position.fixed_position) { - display->drawString(x + 2, compactFirstLine + layoutYOffset, "Sat:"); + display->drawString(x + 2, compactFirstLine, "Sat:"); if (screenWidth > 128) { - drawGPS(display, x + 32, compactFirstLine + 3 + layoutYOffset, gpsStatus); + drawGPS(display, x + 32, compactFirstLine + 3, gpsStatus); } else { - drawGPS(display, x + 23, compactFirstLine + 3 + layoutYOffset, gpsStatus); + drawGPS(display, x + 23, compactFirstLine + 3, gpsStatus); } } else if (!gpsStatus || !gpsStatus->getIsConnected()) { String displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; - display->drawString(x + 2, compactFirstLine + layoutYOffset, "Sat:"); + display->drawString(x + 2, compactFirstLine, "Sat:"); if (screenWidth > 128) { - display->drawString(x + 32, compactFirstLine + 3 + layoutYOffset, displayLine); + display->drawString(x + 32, compactFirstLine, displayLine); } else { - display->drawString(x + 23, compactFirstLine + 3 + layoutYOffset, displayLine); + display->drawString(x + 23, compactFirstLine, displayLine); } } else { - display->drawString(x + 2, compactFirstLine + layoutYOffset, "Sat:"); + display->drawString(x + 2, compactFirstLine, "Sat:"); if (screenWidth > 128) { - drawGPS(display, x + 32, compactFirstLine + 3 + layoutYOffset, gpsStatus); + drawGPS(display, x + 32, compactFirstLine + 3, gpsStatus); } else { - drawGPS(display, x + 23, compactFirstLine + 3 + layoutYOffset, gpsStatus); + drawGPS(display, x + 23, compactFirstLine + 3, gpsStatus); } } config.display.heading_bold = origBold; @@ -2517,9 +2514,8 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in drawCommonHeader(display, x, y); // === Draw title === - const int layoutYOffset = 1; const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + layoutYOffset + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int textY = y + 2 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; const int screenWidth = display->getWidth(); const char *titleStr = (screenWidth > 128) ? "Memory" : "Mem"; const int centerX = x + SCREEN_WIDTH / 2; @@ -2543,7 +2539,7 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in const int barX = x + 40 + barsOffset; const int textRightX = x + SCREEN_WIDTH - 2; - int rowY = y + rowYOffset + layoutYOffset; + int rowY = y + rowYOffset; // === Heap delta tracking === static uint32_t previousHeapFree = 0; @@ -2552,7 +2548,8 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in static int deltaChangeCount = 0; auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { - if (total == 0) return; + if (total == 0) + return; int percent = (used * 100) / total; @@ -2667,17 +2664,16 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in // Draw triangle manually int triY = rowY + 4; int triX = centerX - display->getStringWidth(summaryStr) / 2 - 8; - + display->drawLine(triX, triY + 6, triX + 3, triY); display->drawLine(triX + 3, triY, triX + 6, triY + 6); display->drawLine(triX, triY + 6, triX + 6, triY + 6); - + // Draw the text to the right of the triangle display->setTextAlignment(TEXT_ALIGN_LEFT); display->drawString(triX + 10, rowY + 2, summaryStr); } - #if defined(ESP_PLATFORM) && defined(USE_ST7789) SPIClass SPI1(HSPI); #endif From 22b44ce7e69f10dbd47ae47e5231a86250563e0b Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 2 Apr 2025 03:32:13 -0400 Subject: [PATCH 028/265] Offset Header for Node list screens --- src/graphics/Screen.cpp | 51 ++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 737b89802..cf29f0a1a 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1710,8 +1710,12 @@ String getSafeNodeName(meshtastic_NodeInfoLite *node) // Draws the top header bar (optionally inverted or bold) void drawScreenHeader(OLEDDisplay *display, const char *title, int16_t x, int16_t y) { - bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); - bool isBold = config.display.heading_bold; + constexpr int HEADER_OFFSET_Y = 2; + y += HEADER_OFFSET_Y; + + const bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); + const bool isBold = config.display.heading_bold; + const int highlightHeight = FONT_HEIGHT_SMALL - 1; display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -1720,18 +1724,15 @@ void drawScreenHeader(OLEDDisplay *display, const char *title, int16_t x, int16_ int textWidth = display->getStringWidth(title); int titleX = (screenWidth - textWidth) / 2; - // Height of highlight row - const int highlightHeight = FONT_HEIGHT_SMALL - 1; - - // Y offset to vertically center text in rounded bar - int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - + // === Background highlight === if (isInverted) { drawRoundedHighlight(display, 0, y, screenWidth, highlightHeight, 2); display->setColor(BLACK); } - // Draw text centered vertically and horizontally + // === Text baseline === + int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + display->drawString(titleX, textY, title); if (isBold) display->drawString(titleX + 1, textY, title); @@ -1835,25 +1836,26 @@ typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer) { + constexpr int headerOffsetY = 1; + int columnWidth = display->getWidth() / 2; - int totalRowsAvailable = (display->getHeight() - y - FONT_HEIGHT_SMALL) / (FONT_HEIGHT_SMALL - 3); - int visibleNodeRows = std::min(6, totalRowsAvailable); int rowYOffset = FONT_HEIGHT_SMALL - 3; display->clear(); drawScreenHeader(display, title, x, y); + y += headerOffsetY; std::vector nodeList; retrieveAndSortNodes(nodeList); int totalEntries = nodeList.size(); - int maxScroll = calculateMaxScroll(totalEntries, visibleNodeRows); - scrollIndex = std::min(scrollIndex, maxScroll); + int totalRowsAvailable = (display->getHeight() - y - FONT_HEIGHT_SMALL) / (FONT_HEIGHT_SMALL - 3); + int visibleNodeRows = std::min(6, totalRowsAvailable); int startIndex = scrollIndex * visibleNodeRows * 2; int endIndex = std::min(startIndex + visibleNodeRows * 2, totalEntries); - int yOffset = rowYOffset; + int yOffset = rowYOffset + headerOffsetY; int col = 0; int lastNodeY = y; int shownCount = 0; @@ -1867,17 +1869,18 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t shownCount++; if (y + yOffset > display->getHeight() - FONT_HEIGHT_SMALL) { - yOffset = rowYOffset; + yOffset = rowYOffset + headerOffsetY; col++; if (col > 1) break; } } - drawColumnSeparator(display, x, y + FONT_HEIGHT_SMALL - 2, lastNodeY); + drawColumnSeparator(display, x, y + FONT_HEIGHT_SMALL, lastNodeY + headerOffsetY); drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, rowYOffset); } + // Public screen function: shows how recently nodes were heard static void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { @@ -1945,20 +1948,21 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 void drawNodeListWithExtrasScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer, CompassExtraRenderer extras) { + constexpr int headerOffsetY = 1; + int columnWidth = display->getWidth() / 2; - int totalRowsAvailable = (display->getHeight() - y - FONT_HEIGHT_SMALL) / (FONT_HEIGHT_SMALL - 3); - int visibleNodeRows = std::min(6, totalRowsAvailable); int rowYOffset = FONT_HEIGHT_SMALL - 3; display->clear(); drawScreenHeader(display, title, x, y); + y += headerOffsetY; std::vector nodeList; retrieveAndSortNodes(nodeList); int totalEntries = nodeList.size(); - int maxScroll = calculateMaxScroll(totalEntries, visibleNodeRows); - scrollIndex = std::min(scrollIndex, maxScroll); + int totalRowsAvailable = (display->getHeight() - y - FONT_HEIGHT_SMALL) / (FONT_HEIGHT_SMALL - 3); + int visibleNodeRows = std::min(6, totalRowsAvailable); meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); double userLat = 0.0, userLon = 0.0; @@ -1973,7 +1977,7 @@ void drawNodeListWithExtrasScreen(OLEDDisplay *display, OLEDDisplayUiState *stat int startIndex = scrollIndex * visibleNodeRows * 2; int endIndex = std::min(startIndex + visibleNodeRows * 2, totalEntries); - int yOffset = rowYOffset; + int yOffset = rowYOffset + headerOffsetY; int col = 0; int lastNodeY = y; int shownCount = 0; @@ -1993,17 +1997,16 @@ void drawNodeListWithExtrasScreen(OLEDDisplay *display, OLEDDisplayUiState *stat shownCount++; if (y + yOffset > display->getHeight() - FONT_HEIGHT_SMALL) { - yOffset = rowYOffset; + yOffset = rowYOffset + headerOffsetY; col++; if (col > 1) break; } } - drawColumnSeparator(display, x, y + FONT_HEIGHT_SMALL - 2, lastNodeY); + drawColumnSeparator(display, x, y + FONT_HEIGHT_SMALL, lastNodeY + headerOffsetY); drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, rowYOffset); } - // Public screen entry for compass static void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { From af7a70ce082ec99c29441142a1411cf8e82c4bad Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 2 Apr 2025 04:23:45 -0400 Subject: [PATCH 029/265] Compas offset --- src/graphics/Screen.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index cf29f0a1a..b9d200c33 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2480,7 +2480,7 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat if (validHeading) { uint16_t compassDiam = Screen::getCompassDiam(SCREEN_WIDTH, SCREEN_HEIGHT); int16_t compassX = x + SCREEN_WIDTH - compassDiam / 2 - 8; - int16_t compassY = y + SCREEN_HEIGHT / 2 + rowYOffset; + int16_t compassY = y + SCREEN_HEIGHT / 2 + (rowYOffset / 2); screen->drawCompassNorth(display, compassX, compassY, heading); screen->drawNodeHeading(display, compassX, compassY, compassDiam, -heading); From 6f47035420630c4abf7fdd1ce05f6911aac0f42d Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 2 Apr 2025 04:45:25 -0400 Subject: [PATCH 030/265] Memory screen fix --- src/graphics/Screen.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index b9d200c33..fcc583a3c 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2535,18 +2535,19 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in display->setColor(WHITE); // === Layout === + // Offset the content start position without affecting title + int contentY = y + FONT_HEIGHT_SMALL; + const int rowYOffset = FONT_HEIGHT_SMALL - 3; const int barHeight = 6; const int labelX = x; const int barsOffset = (screenWidth > 128) ? 24 : 0; const int barX = x + 40 + barsOffset; - const int textRightX = x + SCREEN_WIDTH - 2; - int rowY = y + rowYOffset; + int rowY = contentY; // === Heap delta tracking === static uint32_t previousHeapFree = 0; - static int32_t lastHeapDelta = 0; static int32_t totalHeapDelta = 0; static int deltaChangeCount = 0; @@ -2566,7 +2567,7 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in int textWidth = display->getStringWidth(combinedStr); int adjustedBarWidth = SCREEN_WIDTH - barX - textWidth - 6; if (adjustedBarWidth < 10) - adjustedBarWidth = 10; // prevent weird bar if display is too small + adjustedBarWidth = 10; int fillWidth = (used * adjustedBarWidth) / total; @@ -2593,7 +2594,6 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in // === Heap delta (inline with heap row only) if (isHeap && previousHeapFree > 0) { int32_t delta = (int32_t)(memGet.getFreeHeap() - previousHeapFree); - lastHeapDelta = delta; if (delta != 0) { totalHeapDelta += delta; From 52eb4eca03e1f7419bb642951db913c32ecc4e5a Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 2 Apr 2025 19:51:45 -0400 Subject: [PATCH 031/265] Delta chainge indicator commented out --- src/graphics/Screen.cpp | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index fcc583a3c..39b5ca995 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2535,9 +2535,7 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in display->setColor(WHITE); // === Layout === - // Offset the content start position without affecting title int contentY = y + FONT_HEIGHT_SMALL; - const int rowYOffset = FONT_HEIGHT_SMALL - 3; const int barHeight = 6; const int labelX = x; @@ -2546,10 +2544,12 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in int rowY = contentY; - // === Heap delta tracking === + // === Heap delta tracking (disabled) === + /* static uint32_t previousHeapFree = 0; static int32_t totalHeapDelta = 0; static int deltaChangeCount = 0; + */ auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { if (total == 0) @@ -2591,10 +2591,10 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in rowY += rowYOffset; - // === Heap delta (inline with heap row only) + // === Heap delta display (disabled) === + /* if (isHeap && previousHeapFree > 0) { int32_t delta = (int32_t)(memGet.getFreeHeap() - previousHeapFree); - if (delta != 0) { totalHeapDelta += delta; deltaChangeCount++; @@ -2625,6 +2625,7 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in if (isHeap) { previousHeapFree = memGet.getFreeHeap(); } + */ }; // === Memory values === @@ -2659,22 +2660,6 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in #endif if (hasSD && sdTotal > 0) drawUsageRow("SD:", sdUsed, sdTotal); - - // === Final summary (always visible) - char summaryStr[32]; - snprintf(summaryStr, sizeof(summaryStr), "seen: %dx total: %ldB", deltaChangeCount, totalHeapDelta); - - // Draw triangle manually - int triY = rowY + 4; - int triX = centerX - display->getStringWidth(summaryStr) / 2 - 8; - - display->drawLine(triX, triY + 6, triX + 3, triY); - display->drawLine(triX + 3, triY, triX + 6, triY + 6); - display->drawLine(triX, triY + 6, triX + 6, triY + 6); - - // Draw the text to the right of the triangle - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawString(triX + 10, rowY + 2, summaryStr); } #if defined(ESP_PLATFORM) && defined(USE_ST7789) From e4e8d28831d625273b354e45317348b35e005b49 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 2 Apr 2025 20:37:14 -0400 Subject: [PATCH 032/265] Refactored Device Focus screen and messages --- src/graphics/Screen.cpp | 99 ++++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 46 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 39b5ca995..128994fce 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1012,21 +1012,38 @@ static void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state uint8_t timestampHours, timestampMinutes; int32_t daysAgo; bool useTimestamp = deltaToTimestamp(seconds, ×tampHours, ×tampMinutes, &daysAgo); + char from_string[75]; + + if (useTimestamp) { + std::string prefix = (daysAgo == 1 && display->width() >= 200) ? "Yesterday" : "At"; + + if (config.display.use_12h_clock) { + std::string meridiem = "AM"; + if (timestampHours >= 12) { + meridiem = "PM"; + } + if (timestampHours > 12) { + timestampHours -= 12; + } + if (timestampHours == 00) { + timestampHours = 12; + } + + snprintf(from_string, sizeof(from_string), "%s %d:%02d%s from", prefix.c_str(), timestampHours, timestampMinutes, + meridiem.c_str()); + } else { + snprintf(from_string, sizeof(from_string), "%s %d:%02d from", prefix.c_str(), timestampHours, timestampMinutes); + } + } // If bold, draw twice, shifting right by one pixel for (uint8_t xOff = 0; xOff <= (config.display.heading_bold ? 1 : 0); xOff++) { // Show a timestamp if received today, but longer than 15 minutes ago if (useTimestamp && minutes >= 15 && daysAgo == 0) { - display->drawStringf(xOff + x, 0 + y, tempBuf, "At %02hu:%02hu from %s", timestampHours, timestampMinutes, + display->drawStringf(xOff + x, 0 + y, tempBuf, "%s %s", from_string, (node && node->has_user) ? node->user.short_name : "???"); - } - // Timestamp yesterday (if display is wide enough) - else if (useTimestamp && daysAgo == 1 && display->width() >= 200) { - display->drawStringf(xOff + x, 0 + y, tempBuf, "Yesterday %02hu:%02hu from %s", timestampHours, timestampMinutes, - (node && node->has_user) ? node->user.short_name : "???"); - } - // Otherwise, show a time delta else { + } else { display->drawStringf(xOff + x, 0 + y, tempBuf, "%s ago from %s", screen->drawTimeDelta(days, hours, minutes, seconds).c_str(), (node && node->has_user) ? node->user.short_name : "???"); @@ -1880,7 +1897,6 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, rowYOffset); } - // Public screen function: shows how recently nodes were heard static void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { @@ -2167,10 +2183,13 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i // === Content below header === - // === First Row: Node and GPS === + // === First Row: Region / Channel Utilization and GPS === bool origBold = config.display.heading_bold; config.display.heading_bold = false; + // Display Region and Channel Utilization + config.display.heading_bold = false; + drawNodes(display, x, compactFirstLine + 3, nodeStatus); #if HAS_GPS @@ -2196,40 +2215,7 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i config.display.heading_bold = origBold; - // === Second Row: MAC ID and Channel Utilization === - - // Get our hardware ID - uint8_t dmac[6]; - getMacAddr(dmac); - snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); - -#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ - !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x, compactSecondLine + 3, 12, 8, imgInfoL1); - display->drawFastImage(x, compactSecondLine + 11, 12, 8, imgInfoL2); -#else - display->drawFastImage(x, compactSecondLine + 2, 8, 8, imgInfo); -#endif - - int i_xoffset = (SCREEN_WIDTH > 128) ? 14 : 10; - display->drawString(x + i_xoffset, compactSecondLine, ourId); - - // Display Channel Utilization - char chUtil[13]; - snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), compactSecondLine, chUtil); - - // === Third Row: LongName Centered === - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { - const char *longName = ourNode->user.long_name; - int textWidth = display->getStringWidth(longName); - int nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, compactThirdLine, longName); - } - - // === Fourth Row: Uptime === + // === Second Row: Uptime and Voltage === uint32_t uptime = millis() / 1000; char uptimeStr[6]; uint32_t minutes = uptime / 60, hours = minutes / 60, days = hours / 24; @@ -2250,8 +2236,29 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i char uptimeFullStr[16]; snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); - int uptimeX = (SCREEN_WIDTH - display->getStringWidth(uptimeFullStr)) / 2; - display->drawString(uptimeX, compactFourthLine, uptimeFullStr); + display->drawString(x, compactSecondLine, uptimeFullStr); + + char batStr[20]; + if (powerStatus->getHasBattery()) { + int batV = powerStatus->getBatteryVoltageMv() / 1000; + int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; + snprintf(batStr, sizeof(batStr), "%01d.%02dV", batV, batCv); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(batStr), compactSecondLine, batStr); + } else { + display->drawString(x + SCREEN_WIDTH - display->getStringWidth("USB"), compactSecondLine, String("USB")); + } + + // === Third Row: LongName Centered === + // Blank + + // === Fourth Row: LongName Centered === + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { + const char *longName = ourNode->user.long_name; + int textWidth = display->getStringWidth(longName); + int nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, compactFourthLine, longName); + } } // **************************** From 047ccbcb5524f060db0626a9aa0d093cc0df8980 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 2 Apr 2025 20:55:42 -0400 Subject: [PATCH 033/265] Update Screen.cpp --- src/graphics/Screen.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 128994fce..e1ba469ef 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1042,7 +1042,6 @@ static void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state if (useTimestamp && minutes >= 15 && daysAgo == 0) { display->drawStringf(xOff + x, 0 + y, tempBuf, "%s %s", from_string, (node && node->has_user) ? node->user.short_name : "???"); - else { } else { display->drawStringf(xOff + x, 0 + y, tempBuf, "%s ago from %s", screen->drawTimeDelta(days, hours, minutes, seconds).c_str(), From b7aaf3ae471f7a0e03fbf9550caafef62884c2bf Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 2 Apr 2025 21:30:30 -0400 Subject: [PATCH 034/265] Column separator and scrillbar alignment --- src/graphics/Screen.cpp | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index e1ba469ef..4c01b8e14 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1892,8 +1892,18 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t } } - drawColumnSeparator(display, x, y + FONT_HEIGHT_SMALL, lastNodeY + headerOffsetY); - drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, rowYOffset); + if (shownCount > 0) { + // yStart = top of first node = y (header start) + header height + spacing + const int headerOffsetYLocal = 1; // Matches earlier in this function + const int headerHeight = FONT_HEIGHT_SMALL - 1; // from drawScreenHeader() + int yStart = y + headerHeight + headerOffsetYLocal; + drawColumnSeparator(display, x, yStart, lastNodeY + headerOffsetY); + } + + const int headerOffsetYLocal = 1; + const int headerHeight = FONT_HEIGHT_SMALL - 1; + int firstNodeY = y + headerHeight + headerOffsetYLocal; + drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, firstNodeY); } // Public screen function: shows how recently nodes were heard @@ -2019,8 +2029,18 @@ void drawNodeListWithExtrasScreen(OLEDDisplay *display, OLEDDisplayUiState *stat } } - drawColumnSeparator(display, x, y + FONT_HEIGHT_SMALL, lastNodeY + headerOffsetY); - drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, rowYOffset); + if (shownCount > 0) { + // yStart = top of first node = y (header start) + header height + spacing + const int headerOffsetYLocal = 1; // Matches earlier in this function + const int headerHeight = FONT_HEIGHT_SMALL - 1; // from drawScreenHeader() + int yStart = y + headerHeight + headerOffsetYLocal; + drawColumnSeparator(display, x, yStart, lastNodeY + headerOffsetY); + } + + const int headerOffsetYLocal = 1; + const int headerHeight = FONT_HEIGHT_SMALL - 1; + int firstNodeY = y + headerHeight + headerOffsetYLocal; + drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, firstNodeY); } // Public screen entry for compass static void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) From 13b4093d84096449a9370fd14b4026302cbfa1f4 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 3 Apr 2025 01:03:57 -0400 Subject: [PATCH 035/265] GPS compass sizing fix --- src/graphics/Screen.cpp | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 4c01b8e14..6c0332422 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2430,7 +2430,7 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat display->setColor(WHITE); display->setTextAlignment(TEXT_ALIGN_LEFT); - // Row Y offset just like drawNodeListScreen + // Row Y offset int rowYOffset = FONT_HEIGHT_SMALL - 3; int rowY = y + rowYOffset; @@ -2504,24 +2504,32 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat // === Draw Compass if heading is valid === if (validHeading) { - uint16_t compassDiam = Screen::getCompassDiam(SCREEN_WIDTH, SCREEN_HEIGHT); - int16_t compassX = x + SCREEN_WIDTH - compassDiam / 2 - 8; - int16_t compassY = y + SCREEN_HEIGHT / 2 + (rowYOffset / 2); - - screen->drawCompassNorth(display, compassX, compassY, heading); + const int16_t topY = compactFirstLine; + const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height + const int16_t usableHeight = bottomY - topY - 5; + + int16_t compassRadius = usableHeight / 2; + if (compassRadius < 8) compassRadius = 8; + const int16_t compassDiam = compassRadius * 2; + const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; + + // Center vertically and nudge down slightly to keep "N" clear of header + const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + + // Draw compass screen->drawNodeHeading(display, compassX, compassY, compassDiam, -heading); - display->drawCircle(compassX, compassY, compassDiam / 2); - - // === Draw moving "N" label slightly inside edge of circle === + display->drawCircle(compassX, compassY, compassRadius); + + // "N" label float northAngle = -heading; - float radius = compassDiam / 2; + float radius = compassRadius; int16_t nX = compassX + (radius - 1) * sin(northAngle); int16_t nY = compassY - (radius - 1) * cos(northAngle); - int16_t nLabelWidth = display->getStringWidth("N") + 2; - int16_t nLabelHeight = FONT_HEIGHT_SMALL + 1; + int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; + display->setColor(BLACK); - display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeight / 2, nLabelWidth, nLabelHeight); + display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); display->setColor(WHITE); display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_CENTER); From 6270c5663cbf5f96a03cb14b3bb20dcbb903cc56 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 3 Apr 2025 10:47:25 -0400 Subject: [PATCH 036/265] Update Screen.cpp --- src/graphics/Screen.cpp | 95 +++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 51 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 6c0332422..66c13e72e 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1894,12 +1894,12 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t if (shownCount > 0) { // yStart = top of first node = y (header start) + header height + spacing - const int headerOffsetYLocal = 1; // Matches earlier in this function + const int headerOffsetYLocal = 1; // Matches earlier in this function const int headerHeight = FONT_HEIGHT_SMALL - 1; // from drawScreenHeader() int yStart = y + headerHeight + headerOffsetYLocal; drawColumnSeparator(display, x, yStart, lastNodeY + headerOffsetY); } - + const int headerOffsetYLocal = 1; const int headerHeight = FONT_HEIGHT_SMALL - 1; int firstNodeY = y + headerHeight + headerOffsetYLocal; @@ -2031,12 +2031,12 @@ void drawNodeListWithExtrasScreen(OLEDDisplay *display, OLEDDisplayUiState *stat if (shownCount > 0) { // yStart = top of first node = y (header start) + header height + spacing - const int headerOffsetYLocal = 1; // Matches earlier in this function + const int headerOffsetYLocal = 1; // Matches earlier in this function const int headerHeight = FONT_HEIGHT_SMALL - 1; // from drawScreenHeader() int yStart = y + headerHeight + headerOffsetYLocal; drawColumnSeparator(display, x, yStart, lastNodeY + headerOffsetY); } - + const int headerOffsetYLocal = 1; const int headerHeight = FONT_HEIGHT_SMALL - 1; int firstNodeY = y + headerHeight + headerOffsetYLocal; @@ -2311,61 +2311,52 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int display->setColor(WHITE); display->setTextAlignment(TEXT_ALIGN_LEFT); - // === First Row: MAC ID and Region === - bool origBold = config.display.heading_bold; - config.display.heading_bold = false; + // === First Row: Region / Radio Preset === + auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); + + // Display Region and Radio Preset + char regionradiopreset[25]; + const char *region = myRegion ? myRegion->name : NULL; + + const char *preset = (screenWidth > 128) ? "Preset" : "Prst"; + + snprintf(regionradiopreset, sizeof(regionradiopreset), "%s: %s/%s", preset, region, mode); + display->drawString(x, compactFirstLine, regionradiopreset); + + // char channelStr[20]; + // snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); + // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(channelStr), compactFirstLine, channelStr); + + // === Second Row: Channel Utilization === + char chUtil[25]; + snprintf(chUtil, sizeof(chUtil), "ChUtil: %2.0f%%", airTime->channelUtilizationPercent()); + display->drawString(x, compactSecondLine, chUtil); + + // === Third Row: Channel Utilization === // Get our hardware ID uint8_t dmac[6]; getMacAddr(dmac); snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); -#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ - !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x, compactFirstLine + 5, 12, 8, imgInfoL1); - display->drawFastImage(x, compactFirstLine + 12, 12, 8, imgInfoL2); -#else - display->drawFastImage(x, compactFirstLine + 3, 8, 8, imgInfo); -#endif + char shortnameble[35]; + snprintf(shortnameble, sizeof(shortnameble), "%s: %s/%s", "Short/BLE", haveGlyphs(owner.short_name) ? owner.short_name : "", + ourId); + display->drawString(x, compactThirdLine, shortnameble); - int i_xoffset = (SCREEN_WIDTH > 128) ? 14 : 10; - display->drawString(x + i_xoffset, compactFirstLine, ourId); - - const char *region = myRegion ? myRegion->name : NULL; - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(region), compactFirstLine, region); - - config.display.heading_bold = origBold; - - // === Second Row: Channel and Channel Utilization === - char chUtil[13]; - snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); - display->drawString(x, compactSecondLine, chUtil); - - char channelStr[20]; - snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(channelStr), compactSecondLine, channelStr); - - // === Third Row: Node longName === + // === Fourth Row: Node longName === meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { - const char *longName = ourNode->user.long_name; - int nameX = (SCREEN_WIDTH - display->getStringWidth(longName)) / 2; - display->drawString(nameX, compactThirdLine, longName); - } - - // === Fourth Row: Node shortName === - char buf[25]; - snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); - if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { - int shortNameX = (SCREEN_WIDTH - display->getStringWidth(owner.short_name)) / 2; - display->drawString(shortNameX, compactFourthLine, owner.short_name); + char devicelongname[55]; + snprintf(devicelongname, sizeof(devicelongname), "%s: %s", "Name", ourNode->user.long_name); + display->drawString(x, compactFourthLine, devicelongname); } } // **************************** // * Activity Screen * // **************************** +/* static void drawActivity(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); @@ -2398,6 +2389,7 @@ static void drawActivity(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ // === First Line: Draw any log messages === display->drawLogBuffer(x, compactFirstLine); } +*/ // **************************** // * My Position Screen * @@ -2507,19 +2499,20 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat const int16_t topY = compactFirstLine; const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height const int16_t usableHeight = bottomY - topY - 5; - + int16_t compassRadius = usableHeight / 2; - if (compassRadius < 8) compassRadius = 8; + if (compassRadius < 8) + compassRadius = 8; const int16_t compassDiam = compassRadius * 2; const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; - + // Center vertically and nudge down slightly to keep "N" clear of header const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; - + // Draw compass screen->drawNodeHeading(display, compassX, compassY, compassDiam, -heading); display->drawCircle(compassX, compassY, compassRadius); - + // "N" label float northAngle = -heading; float radius = compassRadius; @@ -2527,7 +2520,7 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat int16_t nY = compassY - (radius - 1) * cos(northAngle); int16_t nLabelWidth = display->getStringWidth("N") + 2; int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; - + display->setColor(BLACK); display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); display->setColor(WHITE); @@ -3335,7 +3328,7 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = drawLoRaFocused; normalFrames[numframes++] = drawCompassAndLocationScreen; normalFrames[numframes++] = drawMemoryScreen; - normalFrames[numframes++] = drawActivity; + // normalFrames[numframes++] = drawActivity; // then all the nodes // We only show a few nodes in our scrolling list - because meshes with many nodes would have too many screens From 7856e069a515465538fb8fa1e0c5514cd63ac4fb Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 4 Apr 2025 00:15:04 -0400 Subject: [PATCH 037/265] Update Screen.cpp --- src/graphics/Screen.cpp | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 66c13e72e..d1e56b911 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2212,11 +2212,17 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i drawNodes(display, x, compactFirstLine + 3, nodeStatus); #if HAS_GPS + auto number_of_satellites = gpsStatus->getNumSatellites(); + int gps_rightchar_offset = (SCREEN_WIDTH > 128) ? -53 : -47; + if (number_of_satellites < 10) { + gps_rightchar_offset += (SCREEN_WIDTH > 128) ? 12 : 6; + } + if (config.position.fixed_position) { if (SCREEN_WIDTH > 128) { - drawGPS(display, SCREEN_WIDTH - 53, compactFirstLine + 3, gpsStatus); + drawGPS(display, SCREEN_WIDTH + gps_rightchar_offset, compactFirstLine + 3, gpsStatus); } else { - drawGPS(display, SCREEN_WIDTH - 47, compactFirstLine + 3, gpsStatus); + drawGPS(display, SCREEN_WIDTH + gps_rightchar_offset, compactFirstLine + 3, gpsStatus); } } else if (!gpsStatus || !gpsStatus->getIsConnected()) { String displayLine = @@ -2225,9 +2231,9 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i display->drawString(posX, compactFirstLine, displayLine); } else { if (SCREEN_WIDTH > 128) { - drawGPS(display, SCREEN_WIDTH - 53, compactFirstLine + 3, gpsStatus); + drawGPS(display, SCREEN_WIDTH + gps_rightchar_offset, compactFirstLine + 3, gpsStatus); } else { - drawGPS(display, SCREEN_WIDTH - 47, compactFirstLine + 3, gpsStatus); + drawGPS(display, SCREEN_WIDTH + gps_rightchar_offset, compactFirstLine + 3, gpsStatus); } } #endif @@ -2426,7 +2432,7 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat int rowYOffset = FONT_HEIGHT_SMALL - 3; int rowY = y + rowYOffset; - // === Second Row: My Location === + // === First Row: My Location === #if HAS_GPS bool origBold = config.display.heading_bold; config.display.heading_bold = false; @@ -2472,27 +2478,22 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat validHeading = !isnan(heading); } - // === Third Row: Altitude === - rowY += rowYOffset; - char altStr[32]; - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - snprintf(altStr, sizeof(altStr), "Alt: %.1fft", geoCoord.getAltitude() * METERS_TO_FEET); - } else { - snprintf(altStr, sizeof(altStr), "Alt: %.1fm", geoCoord.getAltitude()); - } - display->drawString(x + 2, rowY, altStr); + // === Second Row: Altitude === + String displayLine; + displayLine = "Alt: " + String(geoCoord.getAltitude()) + "m"; + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) + displayLine = "Alt: " + String(geoCoord.getAltitude() * METERS_TO_FEET) + "ft"; + display->drawString(x + 2, compactSecondLine, displayLine); - // === Fourth Row: Latitude === - rowY += rowYOffset; + // === Third Row: Latitude === char latStr[32]; snprintf(latStr, sizeof(latStr), "Lat: %.5f", geoCoord.getLatitude() * 1e-7); - display->drawString(x + 2, rowY, latStr); + display->drawString(x + 2, compactThirdLine, latStr); // === Fifth Row: Longitude === - rowY += rowYOffset; char lonStr[32]; snprintf(lonStr, sizeof(lonStr), "Lon: %.5f", geoCoord.getLongitude() * 1e-7); - display->drawString(x + 2, rowY, lonStr); + display->drawString(x + 2, compactFourthLine, lonStr); // === Draw Compass if heading is valid === if (validHeading) { @@ -4032,4 +4033,4 @@ int Screen::handleAdminMessage(const meshtastic_AdminMessage *arg) } // namespace graphics #else graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {} -#endif // HAS_SCREEN \ No newline at end of file +#endif // HAS_SCREEN From 2711c53b5f0ed5d77e5e28ce9b5aa6dafe3ffebc Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 4 Apr 2025 01:13:15 -0400 Subject: [PATCH 038/265] Improved message screen with scrolling --- src/graphics/Screen.cpp | 325 +++++++++++++++++++++++----------------- 1 file changed, 190 insertions(+), 135 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index d1e56b911..4e46d9a9f 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -980,146 +980,214 @@ bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int validCached = true; return validCached; } - -/// Draw the last text message we received -static void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r) { - // the max length of this buffer is much longer than we can possibly print - static char tempBuf[237]; + display->fillRect(x + r, y, w - 2 * r, h); + display->fillRect(x, y + r, r, h - 2 * r); + display->fillRect(x + w - r, y + r, r, h - 2 * r); + display->fillCircle(x + r + 1, y + r, r); + display->fillCircle(x + w - r - 1, y + r, r); + display->fillCircle(x + r + 1, y + h - r - 1, r); + display->fillCircle(x + w - r - 1, y + h - r - 1, r); +} +// **************************** +// * Tex Message Screen * +// **************************** +void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ const meshtastic_MeshPacket &mp = devicestate.rx_text_message; - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); - // LOG_DEBUG("Draw text message from 0x%x: %s", mp.from, - // mp.decoded.variant.data.decoded.bytes); + const char *msg = reinterpret_cast(mp.decoded.payload.bytes); - // Demo for drawStringMaxWidth: - // with the third parameter you can define the width after which words will - // be wrapped. Currently only spaces and "-" are allowed for wrapping + // === Setup display formatting === display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - } - // For time delta - uint32_t seconds = sinceReceived(&mp); - uint32_t minutes = seconds / 60; - uint32_t hours = minutes / 60; - uint32_t days = hours / 24; + const int screenWidth = display->getWidth(); + const int screenHeight = display->getHeight(); + const int navHeight = FONT_HEIGHT_SMALL; // space reserved at bottom + const int scrollBottom = screenHeight - navHeight; + const int usableHeight = scrollBottom; + const int textWidth = screenWidth; + const int cornerRadius = 2; - // For timestamp + bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); + bool isBold = config.display.heading_bold; + + // === Construct Header String === + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); + char headerStr[80]; + const char *sender = (node && node->has_user) ? node->user.short_name : "???"; + uint32_t seconds = sinceReceived(&mp), minutes = seconds / 60, hours = minutes / 60, days = hours / 24; uint8_t timestampHours, timestampMinutes; int32_t daysAgo; bool useTimestamp = deltaToTimestamp(seconds, ×tampHours, ×tampMinutes, &daysAgo); - char from_string[75]; - - if (useTimestamp) { - std::string prefix = (daysAgo == 1 && display->width() >= 200) ? "Yesterday" : "At"; + if (useTimestamp && minutes >= 15 && daysAgo == 0) { + std::string prefix = (daysAgo == 1 && screenWidth >= 200) ? "Yesterday" : "At"; + std::string meridiem = "AM"; if (config.display.use_12h_clock) { - std::string meridiem = "AM"; - if (timestampHours >= 12) { - meridiem = "PM"; - } - if (timestampHours > 12) { - timestampHours -= 12; - } - if (timestampHours == 00) { - timestampHours = 12; - } - - snprintf(from_string, sizeof(from_string), "%s %d:%02d%s from", prefix.c_str(), timestampHours, timestampMinutes, - meridiem.c_str()); + if (timestampHours >= 12) meridiem = "PM"; + if (timestampHours > 12) timestampHours -= 12; + if (timestampHours == 0) timestampHours = 12; + snprintf(headerStr, sizeof(headerStr), "%s %d:%02d%s from %s", prefix.c_str(), timestampHours, timestampMinutes, meridiem.c_str(), sender); } else { - snprintf(from_string, sizeof(from_string), "%s %d:%02d from", prefix.c_str(), timestampHours, timestampMinutes); + snprintf(headerStr, sizeof(headerStr), "%s %d:%02d from %s", prefix.c_str(), timestampHours, timestampMinutes, sender); } - } - - // If bold, draw twice, shifting right by one pixel - for (uint8_t xOff = 0; xOff <= (config.display.heading_bold ? 1 : 0); xOff++) { - // Show a timestamp if received today, but longer than 15 minutes ago - if (useTimestamp && minutes >= 15 && daysAgo == 0) { - display->drawStringf(xOff + x, 0 + y, tempBuf, "%s %s", from_string, - (node && node->has_user) ? node->user.short_name : "???"); - } else { - display->drawStringf(xOff + x, 0 + y, tempBuf, "%s ago from %s", - screen->drawTimeDelta(days, hours, minutes, seconds).c_str(), - (node && node->has_user) ? node->user.short_name : "???"); - } - } - - display->setColor(WHITE); -#ifndef EXCLUDE_EMOJI - const char *msg = reinterpret_cast(mp.decoded.payload.bytes); - if (strcmp(msg, "\U0001F44D") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - thumbs_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - thumbs_height) / 2 + 2 + 5, thumbs_width, thumbs_height, - thumbup); - } else if (strcmp(msg, "\U0001F44E") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - thumbs_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - thumbs_height) / 2 + 2 + 5, thumbs_width, thumbs_height, - thumbdown); - } else if (strcmp(msg, "\U0001F60A") == 0 || strcmp(msg, "\U0001F600") == 0 || strcmp(msg, "\U0001F642") == 0 || - strcmp(msg, "\U0001F609") == 0 || - strcmp(msg, "\U0001F601") == 0) { // matches 5 different common smileys, so that the phone user doesn't have to - // remember which one is compatible - display->drawXbm(x + (SCREEN_WIDTH - smiley_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - smiley_height) / 2 + 2 + 5, smiley_width, smiley_height, - smiley); - } else if (strcmp(msg, "❓") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - question_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - question_height) / 2 + 2 + 5, question_width, question_height, - question); - } else if (strcmp(msg, "‼️") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - bang_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - bang_height) / 2 + 2 + 5, - bang_width, bang_height, bang); - } else if (strcmp(msg, "\U0001F4A9") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - poo_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - poo_height) / 2 + 2 + 5, - poo_width, poo_height, poo); - } else if (strcmp(msg, "\U0001F923") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - haha_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - haha_height) / 2 + 2 + 5, - haha_width, haha_height, haha); - } else if (strcmp(msg, "\U0001F44B") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - wave_icon_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - wave_icon_height) / 2 + 2 + 5, wave_icon_width, - wave_icon_height, wave_icon); - } else if (strcmp(msg, "\U0001F920") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - cowboy_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - cowboy_height) / 2 + 2 + 5, cowboy_width, cowboy_height, - cowboy); - } else if (strcmp(msg, "\U0001F42D") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - deadmau5_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - deadmau5_height) / 2 + 2 + 5, deadmau5_width, deadmau5_height, - deadmau5); - } else if (strcmp(msg, "\xE2\x98\x80\xEF\xB8\x8F") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - sun_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - sun_height) / 2 + 2 + 5, - sun_width, sun_height, sun); - } else if (strcmp(msg, "\u2614") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - rain_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - rain_height) / 2 + 2 + 10, - rain_width, rain_height, rain); - } else if (strcmp(msg, "☁️") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - cloud_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - cloud_height) / 2 + 2 + 5, cloud_width, cloud_height, cloud); - } else if (strcmp(msg, "🌫️") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - fog_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - fog_height) / 2 + 2 + 5, - fog_width, fog_height, fog); - } else if (strcmp(msg, "\U0001F608") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - devil_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - devil_height) / 2 + 2 + 5, devil_width, devil_height, devil); - } else if (strcmp(msg, "β™₯️") == 0 || strcmp(msg, "\U0001F9E1") == 0 || strcmp(msg, "\U00002763") == 0 || - strcmp(msg, "\U00002764") == 0 || strcmp(msg, "\U0001F495") == 0 || strcmp(msg, "\U0001F496") == 0 || - strcmp(msg, "\U0001F497") == 0 || strcmp(msg, "\U0001F498") == 0) { - display->drawXbm(x + (SCREEN_WIDTH - heart_width) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - heart_height) / 2 + 2 + 5, heart_width, heart_height, heart); } else { - snprintf(tempBuf, sizeof(tempBuf), "%s", mp.decoded.payload.bytes); - display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), tempBuf); + snprintf(headerStr, sizeof(headerStr), "%s ago from %s", screen->drawTimeDelta(days, hours, minutes, seconds).c_str(), sender); + } + +#ifndef EXCLUDE_EMOJI + // === Check for Emote and Draw It === + struct Emote { + const char *code; + const uint8_t *bitmap; + int width, height; + }; + + const Emote emotes[] = { + { "\U0001F44D", thumbup, thumbs_width, thumbs_height }, + { "\U0001F44E", thumbdown, thumbs_width, thumbs_height }, + { "\U0001F60A", smiley, smiley_width, smiley_height }, + { "\U0001F600", smiley, smiley_width, smiley_height }, + { "\U0001F642", smiley, smiley_width, smiley_height }, + { "\U0001F609", smiley, smiley_width, smiley_height }, + { "\U0001F601", smiley, smiley_width, smiley_height }, + { "❓", question, question_width, question_height }, + { "‼️", bang, bang_width, bang_height }, + { "\U0001F4A9", poo, poo_width, poo_height }, + { "\U0001F923", haha, haha_width, haha_height }, + { "\U0001F44B", wave_icon, wave_icon_width, wave_icon_height }, + { "\U0001F920", cowboy, cowboy_width, cowboy_height }, + { "\U0001F42D", deadmau5, deadmau5_width, deadmau5_height }, + { "β˜€οΈ", sun, sun_width, sun_height }, + { "\xE2\x98\x80\xEF\xB8\x8F", sun, sun_width, sun_height }, + { "β˜”", rain, rain_width, rain_height }, + { "\u2614", rain, rain_width, rain_height }, + { "☁️", cloud, cloud_width, cloud_height }, + { "🌫️", fog, fog_width, fog_height }, + { "\U0001F608", devil, devil_width, devil_height }, + { "β™₯️", heart, heart_width, heart_height }, + { "\U0001F9E1", heart, heart_width, heart_height }, + { "\U00002763", heart, heart_width, heart_height }, + { "\U00002764", heart, heart_width, heart_height }, + { "\U0001F495", heart, heart_width, heart_height }, + { "\U0001F496", heart, heart_width, heart_height }, + { "\U0001F497", heart, heart_width, heart_height }, + { "\U0001F498", heart, heart_width, heart_height } + }; + + for (const Emote &e : emotes) { + if (strcmp(msg, e.code) == 0) { + // Draw header before showing emoji + if (isInverted) { + drawRoundedHighlight(display, x, 0, screenWidth, FONT_HEIGHT_SMALL - 1, cornerRadius); + display->setColor(BLACK); + display->drawString(x + 3, 0, headerStr); + if (isBold) display->drawString(x + 4, 0, headerStr); + display->setColor(WHITE); + } else { + display->drawString(x, 0, headerStr); + } + + // Then draw emoji below header + display->drawXbm((screenWidth - e.width) / 2, (screenHeight - e.height) / 2 + FONT_HEIGHT_SMALL, e.width, e.height, e.bitmap); + return; + } } -#else - snprintf(tempBuf, sizeof(tempBuf), "%s", mp.decoded.payload.bytes); - display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), tempBuf); #endif + + // === Word-wrap and build line list === + char messageBuf[237]; + snprintf(messageBuf, sizeof(messageBuf), "%s", msg); + + std::vector lines; + lines.push_back(std::string(headerStr)); // Header line is always first + + std::string word, line; + int msgLen = strlen(messageBuf); + for (int i = 0; i <= msgLen; ++i) { + char ch = messageBuf[i]; + if (ch == ' ' || ch == '\0') { + if (!word.empty()) { + if (display->getStringWidth((line + word).c_str()) > textWidth) { + lines.push_back(line); + line = word + ' '; + } else { + line += word + ' '; + } + word.clear(); + } + if (ch == '\0' && !line.empty()) { + lines.push_back(line); + } + } else { + word += ch; + } + } + + // === Scrolling logic === + const float rowHeight = FONT_HEIGHT_SMALL - 1; + const int totalHeight = lines.size() * rowHeight; + const int scrollStop = std::max(0, totalHeight - usableHeight); + + static float scrollY = 0.0f; + static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0; + static bool waitingToReset = false, scrollStarted = false; + + uint32_t now = millis(); + + // === Smooth scrolling adjustment === + // You can tweak this divisor to change how smooth it scrolls. + // Lower = smoother, but can feel slow. + float delta = (now - lastTime) / 400.0f; + lastTime = now; + + const float scrollSpeed = 2.0f; // pixels per second + + // Delay scrolling start by 2 seconds + if (scrollStartDelay == 0) scrollStartDelay = now; + if (!scrollStarted && now - scrollStartDelay > 2000) scrollStarted = true; + + if (totalHeight > usableHeight) { + if (scrollStarted) { + if (!waitingToReset) { + scrollY += delta * scrollSpeed; + if (scrollY >= scrollStop) { + scrollY = scrollStop; + waitingToReset = true; + pauseStart = now; + } + } else if (now - pauseStart > 3000) { + scrollY = 0; + waitingToReset = false; + scrollStarted = false; + scrollStartDelay = now; + } + } + } else { + scrollY = 0; + } + + int scrollOffset = static_cast(scrollY); + int yOffset = -scrollOffset; + + // === Render visible lines === + for (size_t i = 0; i < lines.size(); ++i) { + int lineY = static_cast(i * rowHeight + yOffset); + if (lineY > -rowHeight && lineY < scrollBottom) { + if (i == 0 && isInverted) { + drawRoundedHighlight(display, x, lineY, screenWidth, FONT_HEIGHT_SMALL - 1, cornerRadius); + display->setColor(BLACK); + display->drawString(x + 3, lineY, lines[i].c_str()); + if (isBold) display->drawString(x + 4, lineY, lines[i].c_str()); + display->setColor(WHITE); + } else { + display->drawString(x, lineY, lines[i].c_str()); + } + } + } } /// Draw a series of fields in a column, wrapping to multiple columns if needed @@ -1605,19 +1673,6 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ // ********************************* // *Rounding Header when inverted * // ********************************* -void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r) -{ - // Center rectangles - display->fillRect(x + r, y, w - 2 * r, h); - display->fillRect(x, y + r, r, h - 2 * r); - display->fillRect(x + w - r, y + r, r, h - 2 * r); - - // Rounded corners β€” visually balanced and centered - display->fillCircle(x + r + 1, y + r, r); // Top-left - display->fillCircle(x + w - r - 1, y + r, r); // Top-right - display->fillCircle(x + r + 1, y + h - r - 1, r); // Bottom-left - display->fillCircle(x + w - r - 1, y + h - r - 1, r); // Bottom-right -} // h! Each node entry holds a reference to its info and how long ago it was heard from struct NodeEntry { meshtastic_NodeInfoLite *node; From 6e0cca16d18ead45b0891a802f9a8a4deefd7230 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 4 Apr 2025 01:21:53 -0400 Subject: [PATCH 039/265] center Emotes --- src/graphics/Screen.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 4e46d9a9f..6f0b5a5fb 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1092,7 +1092,10 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 } // Then draw emoji below header - display->drawXbm((screenWidth - e.width) / 2, (screenHeight - e.height) / 2 + FONT_HEIGHT_SMALL, e.width, e.height, e.bitmap); + int remainingHeight = screenHeight - FONT_HEIGHT_SMALL - navHeight; + int emoteY = FONT_HEIGHT_SMALL + (remainingHeight - e.height) / 2; + + display->drawXbm((screenWidth - e.width) / 2, emoteY, e.width, e.height, e.bitmap); return; } } From 65f00e94743f94d5d25f65d97f65a121e7577991 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 4 Apr 2025 01:36:51 -0400 Subject: [PATCH 040/265] Animated emotes --- src/graphics/Screen.cpp | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 6f0b5a5fb..e7db8070a 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -999,13 +999,12 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 const meshtastic_MeshPacket &mp = devicestate.rx_text_message; const char *msg = reinterpret_cast(mp.decoded.payload.bytes); - // === Setup display formatting === display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); const int screenWidth = display->getWidth(); const int screenHeight = display->getHeight(); - const int navHeight = FONT_HEIGHT_SMALL; // space reserved at bottom + const int navHeight = FONT_HEIGHT_SMALL; const int scrollBottom = screenHeight - navHeight; const int usableHeight = scrollBottom; const int textWidth = screenWidth; @@ -1014,7 +1013,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); bool isBold = config.display.heading_bold; - // === Construct Header String === + // === Header Construction === meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); char headerStr[80]; const char *sender = (node && node->has_user) ? node->user.short_name : "???"; @@ -1039,7 +1038,19 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 } #ifndef EXCLUDE_EMOJI - // === Check for Emote and Draw It === + // === Bounce animation setup === + static uint32_t lastBounceTime = 0; + static int bounceY = 0; + const int bounceRange = 2; // Max pixels to bounce up/down + const int bounceInterval = 60; // How quickly to change bounce direction (ms) + + uint32_t now = millis(); + if (now - lastBounceTime >= bounceInterval) { + lastBounceTime = now; + bounceY = (bounceY + 1) % (bounceRange * 2); + } + + // === Emote rendering === struct Emote { const char *code; const uint8_t *bitmap; @@ -1080,7 +1091,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 for (const Emote &e : emotes) { if (strcmp(msg, e.code) == 0) { - // Draw header before showing emoji + // Draw the header if (isInverted) { drawRoundedHighlight(display, x, 0, screenWidth, FONT_HEIGHT_SMALL - 1, cornerRadius); display->setColor(BLACK); @@ -1091,10 +1102,9 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 display->drawString(x, 0, headerStr); } - // Then draw emoji below header + // Center the emote below header + apply bounce int remainingHeight = screenHeight - FONT_HEIGHT_SMALL - navHeight; - int emoteY = FONT_HEIGHT_SMALL + (remainingHeight - e.height) / 2; - + int emoteY = FONT_HEIGHT_SMALL + (remainingHeight - e.height) / 2 + bounceY - bounceRange; display->drawXbm((screenWidth - e.width) / 2, emoteY, e.width, e.height, e.bitmap); return; } @@ -1138,9 +1148,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 static float scrollY = 0.0f; static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0; static bool waitingToReset = false, scrollStarted = false; - - uint32_t now = millis(); - + // === Smooth scrolling adjustment === // You can tweak this divisor to change how smooth it scrolls. // Lower = smoother, but can feel slow. From a1df41a9e0ac66e50679738afe50c26a5fa9a744 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 4 Apr 2025 10:53:26 -0400 Subject: [PATCH 041/265] offset by 2 and removed Activity Screen -General cleanup of unused screens -Temporarily commented out SD card code on Memory screen -Removed Activity Screen -Slight adjustment to content layout --- src/graphics/Screen.cpp | 68 +++++++---------------------------------- 1 file changed, 11 insertions(+), 57 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index e7db8070a..a23218663 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2275,11 +2275,11 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i // Display Region and Channel Utilization config.display.heading_bold = false; - drawNodes(display, x, compactFirstLine + 3, nodeStatus); + drawNodes(display, x + 2, compactFirstLine + 3, nodeStatus); #if HAS_GPS auto number_of_satellites = gpsStatus->getNumSatellites(); - int gps_rightchar_offset = (SCREEN_WIDTH > 128) ? -53 : -47; + int gps_rightchar_offset = (SCREEN_WIDTH > 128) ? -55 : -49; if (number_of_satellites < 10) { gps_rightchar_offset += (SCREEN_WIDTH > 128) ? 12 : 6; } @@ -2327,16 +2327,16 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i char uptimeFullStr[16]; snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); - display->drawString(x, compactSecondLine, uptimeFullStr); + display->drawString(x + 2, compactSecondLine, uptimeFullStr); char batStr[20]; if (powerStatus->getHasBattery()) { int batV = powerStatus->getBatteryVoltageMv() / 1000; int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; snprintf(batStr, sizeof(batStr), "%01d.%02dV", batV, batCv); - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(batStr), compactSecondLine, batStr); + display->drawString(x - 2 + SCREEN_WIDTH - display->getStringWidth(batStr), compactSecondLine, batStr); } else { - display->drawString(x + SCREEN_WIDTH - display->getStringWidth("USB"), compactSecondLine, String("USB")); + display->drawString(x - 2 + SCREEN_WIDTH - display->getStringWidth("USB"), compactSecondLine, String("USB")); } // === Third Row: LongName Centered === @@ -2384,26 +2384,18 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int display->setTextAlignment(TEXT_ALIGN_LEFT); // === First Row: Region / Radio Preset === - auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); - // Display Region and Radio Preset char regionradiopreset[25]; const char *region = myRegion ? myRegion->name : NULL; - const char *preset = (screenWidth > 128) ? "Preset" : "Prst"; - snprintf(regionradiopreset, sizeof(regionradiopreset), "%s: %s/%s", preset, region, mode); - display->drawString(x, compactFirstLine, regionradiopreset); - - // char channelStr[20]; - // snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); - // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(channelStr), compactFirstLine, channelStr); + display->drawString(x + 2, compactFirstLine, regionradiopreset); // === Second Row: Channel Utilization === char chUtil[25]; snprintf(chUtil, sizeof(chUtil), "ChUtil: %2.0f%%", airTime->channelUtilizationPercent()); - display->drawString(x, compactSecondLine, chUtil); + display->drawString(x + 2, compactSecondLine, chUtil); // === Third Row: Channel Utilization === // Get our hardware ID @@ -2414,55 +2406,17 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int char shortnameble[35]; snprintf(shortnameble, sizeof(shortnameble), "%s: %s/%s", "Short/BLE", haveGlyphs(owner.short_name) ? owner.short_name : "", ourId); - display->drawString(x, compactThirdLine, shortnameble); + display->drawString(x + 2, compactThirdLine, shortnameble); // === Fourth Row: Node longName === meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { char devicelongname[55]; snprintf(devicelongname, sizeof(devicelongname), "%s: %s", "Name", ourNode->user.long_name); - display->drawString(x, compactFourthLine, devicelongname); + display->drawString(x + 2, compactFourthLine, devicelongname); } } -// **************************** -// * Activity Screen * -// **************************** -/* -static void drawActivity(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - - // === Header === - drawCommonHeader(display, x, y); - - // === Draw title === - const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + 2 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - - // Use black text if display is inverted - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->setColor(BLACK); - } - - display->setTextAlignment(TEXT_ALIGN_CENTER); - const int centerX = x + SCREEN_WIDTH / 2; - display->drawString(centerX, textY, "Log"); - - if (config.display.heading_bold) { - display->drawString(centerX + 1, textY, "Log"); - } - - // Restore default color after drawing - display->setColor(WHITE); - - // === First Line: Draw any log messages === - display->drawLogBuffer(x, compactFirstLine); -} -*/ - // **************************** // * My Position Screen * // **************************** @@ -2737,6 +2691,7 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in uint32_t sdUsed = 0, sdTotal = 0; bool hasSD = false; +/* #ifdef HAS_SDCARD hasSD = SD.cardType() != CARD_NONE; if (hasSD) { @@ -2744,7 +2699,7 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in sdTotal = SD.totalBytes(); } #endif - +*/ // === Draw memory rows drawUsageRow("Heap:", heapUsed, heapTotal, true); drawUsageRow("PSRAM:", psramUsed, psramTotal); @@ -3395,7 +3350,6 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = drawLoRaFocused; normalFrames[numframes++] = drawCompassAndLocationScreen; normalFrames[numframes++] = drawMemoryScreen; - // normalFrames[numframes++] = drawActivity; // then all the nodes // We only show a few nodes in our scrolling list - because meshes with many nodes would have too many screens From 75490f410b6af1472c3f2654568c139aae5f4adf Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 4 Apr 2025 23:04:59 -0400 Subject: [PATCH 042/265] Scrolling issues fixed --- .vscode/settings.json | 53 +++++++++++++++++++++++++++++++++++++++++ src/graphics/Screen.cpp | 34 +++++++++++++------------- 2 files changed, 71 insertions(+), 16 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 81deca8f9..d37702476 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,58 @@ }, "[powershell]": { "editor.defaultFormatter": "ms-vscode.powershell" + }, + "files.associations": { + "array": "cpp", + "atomic": "cpp", + "*.tcc": "cpp", + "cctype": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "csignal": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "list": "cpp", + "unordered_map": "cpp", + "unordered_set": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "map": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "string": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "fstream": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "iosfwd": "cpp", + "iostream": "cpp", + "istream": "cpp", + "limits": "cpp", + "new": "cpp", + "ostream": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "cinttypes": "cpp", + "typeinfo": "cpp" } } diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index a23218663..1ca305746 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1118,27 +1118,29 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 std::vector lines; lines.push_back(std::string(headerStr)); // Header line is always first - std::string word, line; - int msgLen = strlen(messageBuf); - for (int i = 0; i <= msgLen; ++i) { + std::string line, word; + for (int i = 0; messageBuf[i]; ++i) { char ch = messageBuf[i]; - if (ch == ' ' || ch == '\0') { - if (!word.empty()) { - if (display->getStringWidth((line + word).c_str()) > textWidth) { - lines.push_back(line); - line = word + ' '; - } else { - line += word + ' '; - } - word.clear(); - } - if (ch == '\0' && !line.empty()) { - lines.push_back(line); - } + if (ch == '\n') { + if (!word.empty()) line += word; + if (!line.empty()) lines.push_back(line); + line.clear(); + word.clear(); + } else if (ch == ' ') { + line += word + ' '; + word.clear(); } else { word += ch; + std::string test = line + word; + if (display->getStringWidth(test.c_str()) > textWidth + 4) { + if (!line.empty()) lines.push_back(line); + line = word; + word.clear(); + } } } + if (!word.empty()) line += word; + if (!line.empty()) lines.push_back(line); // === Scrolling logic === const float rowHeight = FONT_HEIGHT_SMALL - 1; From 52d1d8d7c8db9a52c640a4c49537a17c8b7ea8da Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 5 Apr 2025 00:10:06 -0400 Subject: [PATCH 043/265] Minor alignment adjustments in common header, changed time to be a/p versus AM/PM --- src/graphics/Screen.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 1ca305746..bd39ff6b5 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2191,7 +2191,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) const bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); const bool isBold = config.display.heading_bold; - const int xOffset = 3; + const int xOffset = 4; const int highlightHeight = FONT_HEIGHT_SMALL - 1; const int screenWidth = display->getWidth(); @@ -2245,12 +2245,14 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) hour = 12; char timeStr[10]; - snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "PM" : "AM"); + snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); - int timeX = screenWidth - xOffset - display->getStringWidth(timeStr) - 1; + int timeX = screenWidth + 3 - xOffset - display->getStringWidth(timeStr); + if (screenWidth > 128) + timeX -= 1; display->drawString(timeX, textY, timeStr); if (isBold) - display->drawString(timeX + 1, textY, timeStr); + display->drawString(timeX - 1, textY, timeStr); } display->setColor(WHITE); From 2b7bc6696febeb6a0503248fffc412021b7e38da Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 5 Apr 2025 15:28:39 -0400 Subject: [PATCH 044/265] Fixed Node list screens --- src/graphics/Screen.cpp | 403 +++++++++++++++++++++++----------------- 1 file changed, 234 insertions(+), 169 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index bd39ff6b5..e8871358f 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -980,6 +980,9 @@ bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int validCached = true; return validCached; } +// ********************************* +// *Rounding Header when inverted * +// ********************************* void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r) { display->fillRect(x + r, y, w - 2 * r, h); @@ -990,7 +993,144 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, display->fillCircle(x + r + 1, y + h - r - 1, r); display->fillCircle(x + w - r - 1, y + h - r - 1, r); } +bool isBoltVisible = true; +uint32_t lastBlink = 0; +const uint32_t blinkInterval = 500; +// *********************** +// * Common Header * +// *********************** +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) +{ + constexpr int HEADER_OFFSET_Y = 2; + y += HEADER_OFFSET_Y; + const bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); + const bool isBold = config.display.heading_bold; + const int xOffset = 4; + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + const int screenWidth = display->getWidth(); + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === Background highlight === + if (isInverted) { + drawRoundedHighlight(display, x, y, screenWidth, highlightHeight, 2); + display->setColor(BLACK); + } + + // === Text baseline === + const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + + // === Battery dynamically scaled === + const int nubSize = 2; + const int batteryLong = screenWidth > 200 ? 28 : 24; // Wider on bigger TFTs + const int batteryShort = highlightHeight - nubSize - 2; + + int batteryX = x + xOffset; + int batteryY = y + (highlightHeight - batteryShort) / 2 + nubSize; + + int chargePercent = powerStatus->getBatteryChargePercent(); + + bool isCharging = powerStatus->getIsCharging() == OptionalBool::OptTrue; + uint32_t now = millis(); + if (isCharging && now - lastBlink > blinkInterval) { + isBoltVisible = !isBoltVisible; + lastBlink = now; + } + + if (screenWidth > 128) { + // === Horizontal battery === + batteryY = y + (highlightHeight - batteryShort) / 2; + + // Battery outline + display->drawRect(batteryX, batteryY, batteryLong, batteryShort); + + // Nub + display->fillRect( + batteryX + batteryLong, + batteryY + (batteryShort / 2) - 3, + nubSize + 2, 6 + ); + + if (isCharging && isBoltVisible) { + // Lightning bolt + const int boltX = batteryX + batteryLong / 2 - 4; + const int boltY = batteryY + 2; // Padding top + + // Top fat bar (same) + display->fillRect(boltX, boltY, 6, 2); + + // Middle slanted lines + display->drawLine(boltX + 0, boltY + 2, boltX + 3, boltY + 6); + display->drawLine(boltX + 1, boltY + 2, boltX + 4, boltY + 6); + display->drawLine(boltX + 2, boltY + 2, boltX + 5, boltY + 6); + + // Tapered tail + display->drawLine(boltX + 3, boltY + 6, boltX + 0, batteryY + batteryShort - 3); + display->drawLine(boltX + 4, boltY + 6, boltX + 1, batteryY + batteryShort - 3); + } else if (!isCharging) { + int fillWidth = (batteryLong - 2) * chargePercent / 100; + int fillX = batteryX + 1; + display->fillRect(fillX, batteryY + 1, fillWidth, batteryShort - 2); + } + } else { + // === Vertical battery === + const int batteryWidth = 8; + const int batteryHeight = batteryShort; + const int totalBatteryHeight = batteryHeight + nubSize; + batteryX += -2; + batteryY = y + (highlightHeight - totalBatteryHeight) / 2 + nubSize; + + display->fillRect(batteryX + 2, batteryY - nubSize, 4, nubSize); // Nub + display->drawRect(batteryX, batteryY, batteryWidth, batteryHeight); // Body + + if (isCharging && isBoltVisible) { + display->drawLine(batteryX + 4, batteryY + 1, batteryX + 2, batteryY + 4); + display->drawLine(batteryX + 2, batteryY + 4, batteryX + 4, batteryY + 4); + display->drawLine(batteryX + 4, batteryY + 4, batteryX + 3, batteryY + 7); + } else if (!isCharging) { + int fillHeight = (batteryHeight - 2) * chargePercent / 100; + int fillY = batteryY + batteryHeight - 1 - fillHeight; + display->fillRect(batteryX + 1, fillY, batteryWidth - 2, fillHeight); + } + } + + // === Battery % Text === + char percentStr[8]; + snprintf(percentStr, sizeof(percentStr), "%d%%", chargePercent); + + const int batteryOffset = screenWidth > 128 ? 34 : 9; + const int percentX = x + xOffset + batteryOffset; + display->drawString(percentX, textY, percentStr); + if (isBold) + display->drawString(percentX + 1, textY, percentStr); + + // === Time string (right-aligned) === + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); + if (rtc_sec > 0) { + long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; + int hour = hms / SEC_PER_HOUR; + int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + + bool isPM = hour >= 12; + hour = hour % 12; + if (hour == 0) + hour = 12; + + char timeStr[10]; + snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); + + int timeX = screenWidth + 3 - xOffset - display->getStringWidth(timeStr); + if (screenWidth > 128) + timeX -= 1; + display->drawString(timeX, textY, timeStr); + if (isBold) + display->drawString(timeX - 1, textY, timeStr); + } + + display->setColor(WHITE); +} // **************************** // * Tex Message Screen * // **************************** @@ -1683,9 +1823,6 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ screen->drawColumns(display, x, y, fields); } -// ********************************* -// *Rounding Header when inverted * -// ********************************* // h! Each node entry holds a reference to its info and how long ago it was heard from struct NodeEntry { meshtastic_NodeInfoLite *node; @@ -1717,23 +1854,26 @@ int calculateMaxScroll(int totalEntries, int visibleRows) } // Helper: Draw vertical scrollbar matching CannedMessageModule style -void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollIndex, int columns, int rowYOffset) +void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollIndex, int columns, int scrollStartY) { - int totalPages = (totalEntries + columns - 1) / columns; - if (totalPages <= visibleNodeRows) - return; // no scrollbar needed + const int rowHeight = FONT_HEIGHT_SMALL - 3; - int scrollAreaHeight = visibleNodeRows * (FONT_HEIGHT_SMALL - 3); // true pixel height used per row - int scrollbarX = display->getWidth() - 6; - int scrollbarWidth = 4; + // Visual rows across both columns + const int totalVisualRows = (totalEntries + columns - 1) / columns; - int scrollBarHeight = (scrollAreaHeight * visibleNodeRows) / totalPages; - int scrollBarY = rowYOffset + (scrollAreaHeight * scrollIndex) / totalPages; + if (totalVisualRows <= visibleNodeRows) + return; // Don't draw scrollbar if everything fits - display->drawRect(scrollbarX, rowYOffset, scrollbarWidth, scrollAreaHeight); + const int scrollAreaHeight = visibleNodeRows * rowHeight; + const int scrollbarX = display->getWidth() - 6; + const int scrollbarWidth = 4; + + const int scrollBarHeight = (scrollAreaHeight * visibleNodeRows) / totalVisualRows; + const int scrollBarY = scrollStartY + (scrollAreaHeight * scrollIndex) / totalVisualRows; + + display->drawRect(scrollbarX, scrollStartY, scrollbarWidth, scrollAreaHeight); display->fillRect(scrollbarX, scrollBarY, scrollbarWidth, scrollBarHeight); } - // Grabs all nodes from the DB and sorts them (favorites and most recently heard first) void retrieveAndSortNodes(std::vector &nodeList) { @@ -1791,45 +1931,12 @@ String getSafeNodeName(meshtastic_NodeInfoLite *node) return nodeName; } -// Draws the top header bar (optionally inverted or bold) -void drawScreenHeader(OLEDDisplay *display, const char *title, int16_t x, int16_t y) -{ - constexpr int HEADER_OFFSET_Y = 2; - y += HEADER_OFFSET_Y; - - const bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); - const bool isBold = config.display.heading_bold; - const int highlightHeight = FONT_HEIGHT_SMALL - 1; - - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - int screenWidth = display->getWidth(); - int textWidth = display->getStringWidth(title); - int titleX = (screenWidth - textWidth) / 2; - - // === Background highlight === - if (isInverted) { - drawRoundedHighlight(display, 0, y, screenWidth, highlightHeight, 2); - display->setColor(BLACK); - } - - // === Text baseline === - int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - - display->drawString(titleX, textY, title); - if (isBold) - display->drawString(titleX + 1, textY, title); - - display->setColor(WHITE); -} - // Draws separator line void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) { int columnWidth = display->getWidth() / 2; int separatorX = x + columnWidth - 2; - display->drawLine(separatorX, yStart, separatorX, yEnd - 3); + display->drawLine(separatorX, yStart, separatorX, yEnd); } // Draws node name with how long ago it was last heard from @@ -1907,7 +2014,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int for (int b = 0; b < 4; b++) { if (b < bars) { - int height = 2 + (b * 2); + int height = (b * 2); display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height); } } @@ -1920,26 +2027,50 @@ typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer) { - constexpr int headerOffsetY = 1; + const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; + const int rowYOffset = FONT_HEIGHT_SMALL - 3; int columnWidth = display->getWidth() / 2; - int rowYOffset = FONT_HEIGHT_SMALL - 3; + int screenWidth = display->getWidth(); display->clear(); - drawScreenHeader(display, title, x, y); - y += headerOffsetY; + // === Draw the battery/time header === + drawCommonHeader(display, x, y); + + // === Manually draw the centered title within the header === + const int highlightHeight = COMMON_HEADER_HEIGHT; + const int textY = y + 2 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int centerX = x + screenWidth / 2; + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) + display->setColor(BLACK); + + display->drawString(centerX, textY, title); + if (config.display.heading_bold) + display->drawString(centerX + 1, textY, title); + + display->setColor(WHITE); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === Space below header === + y += COMMON_HEADER_HEIGHT; + + // === Fetch and display sorted node list === std::vector nodeList; retrieveAndSortNodes(nodeList); int totalEntries = nodeList.size(); - int totalRowsAvailable = (display->getHeight() - y - FONT_HEIGHT_SMALL) / (FONT_HEIGHT_SMALL - 3); - int visibleNodeRows = std::min(6, totalRowsAvailable); + int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; + int visibleNodeRows = totalRowsAvailable; int startIndex = scrollIndex * visibleNodeRows * 2; int endIndex = std::min(startIndex + visibleNodeRows * 2, totalEntries); - int yOffset = rowYOffset + headerOffsetY; + int yOffset = 0; int col = 0; int lastNodeY = y; int shownCount = 0; @@ -1948,32 +2079,29 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t int xPos = x + (col * columnWidth); int yPos = y + yOffset; renderer(display, nodeList[i].node, xPos, yPos, columnWidth); - lastNodeY = std::max(lastNodeY, y + yOffset + FONT_HEIGHT_SMALL); - yOffset += FONT_HEIGHT_SMALL - 3; + lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); + yOffset += rowYOffset; shownCount++; if (y + yOffset > display->getHeight() - FONT_HEIGHT_SMALL) { - yOffset = rowYOffset + headerOffsetY; + yOffset = 0; col++; if (col > 1) break; } } + // === Draw column separator if (shownCount > 0) { - // yStart = top of first node = y (header start) + header height + spacing - const int headerOffsetYLocal = 1; // Matches earlier in this function - const int headerHeight = FONT_HEIGHT_SMALL - 1; // from drawScreenHeader() - int yStart = y + headerHeight + headerOffsetYLocal; - drawColumnSeparator(display, x, yStart, lastNodeY + headerOffsetY); + const int firstNodeY = y + 3; // where the first node row starts + drawColumnSeparator(display, x, firstNodeY, lastNodeY); } - const int headerOffsetYLocal = 1; - const int headerHeight = FONT_HEIGHT_SMALL - 1; - int firstNodeY = y + headerHeight + headerOffsetYLocal; - drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, firstNodeY); + const int scrollStartY = y + 3; + drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY); } + // Public screen function: shows how recently nodes were heard static void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { @@ -1983,7 +2111,7 @@ static void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, // Public screen function: shows hop count + signal strength static void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - drawNodeListScreen(display, state, x, y, "Hops/Signal", drawEntryHopSignal); + drawNodeListScreen(display, state, x, y, "Hop|Sig", drawEntryHopSignal); } // Helper function: Draw a single node entry for Node List (Modified for Compass Screen) @@ -2041,21 +2169,43 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 void drawNodeListWithExtrasScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer, CompassExtraRenderer extras) { - constexpr int headerOffsetY = 1; + const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; + const int rowYOffset = FONT_HEIGHT_SMALL - 3; int columnWidth = display->getWidth() / 2; - int rowYOffset = FONT_HEIGHT_SMALL - 3; + int screenWidth = display->getWidth(); display->clear(); - drawScreenHeader(display, title, x, y); - y += headerOffsetY; + + // === Draw common header (battery + time) + drawCommonHeader(display, x, y); + + // === Draw title inside header (centered) + const int highlightHeight = COMMON_HEADER_HEIGHT; + const int textY = y + 2 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int centerX = x + screenWidth / 2; + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) + display->setColor(BLACK); + + display->drawString(centerX, textY, title); + if (config.display.heading_bold) + display->drawString(centerX + 1, textY, title); + + display->setColor(WHITE); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === Space below header + y += COMMON_HEADER_HEIGHT; std::vector nodeList; retrieveAndSortNodes(nodeList); int totalEntries = nodeList.size(); - int totalRowsAvailable = (display->getHeight() - y - FONT_HEIGHT_SMALL) / (FONT_HEIGHT_SMALL - 3); - int visibleNodeRows = std::min(6, totalRowsAvailable); + int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; + int visibleNodeRows = totalRowsAvailable; meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); double userLat = 0.0, userLon = 0.0; @@ -2070,7 +2220,7 @@ void drawNodeListWithExtrasScreen(OLEDDisplay *display, OLEDDisplayUiState *stat int startIndex = scrollIndex * visibleNodeRows * 2; int endIndex = std::min(startIndex + visibleNodeRows * 2, totalEntries); - int yOffset = rowYOffset + headerOffsetY; + int yOffset = 0; int col = 0; int lastNodeY = y; int shownCount = 0; @@ -2085,30 +2235,26 @@ void drawNodeListWithExtrasScreen(OLEDDisplay *display, OLEDDisplayUiState *stat extras(display, nodeList[i].node, xPos, yPos, columnWidth, myHeading, userLat, userLon); } - lastNodeY = std::max(lastNodeY, y + yOffset + FONT_HEIGHT_SMALL); - yOffset += FONT_HEIGHT_SMALL - 3; + lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); + yOffset += rowYOffset; shownCount++; if (y + yOffset > display->getHeight() - FONT_HEIGHT_SMALL) { - yOffset = rowYOffset + headerOffsetY; + yOffset = 0; col++; if (col > 1) break; } } + // === Draw column separator if (shownCount > 0) { - // yStart = top of first node = y (header start) + header height + spacing - const int headerOffsetYLocal = 1; // Matches earlier in this function - const int headerHeight = FONT_HEIGHT_SMALL - 1; // from drawScreenHeader() - int yStart = y + headerHeight + headerOffsetYLocal; - drawColumnSeparator(display, x, yStart, lastNodeY + headerOffsetY); + const int firstNodeY = y + 3; // where the first node row starts + drawColumnSeparator(display, x, firstNodeY, lastNodeY); } - const int headerOffsetYLocal = 1; - const int headerHeight = FONT_HEIGHT_SMALL - 1; - int firstNodeY = y + headerHeight + headerOffsetYLocal; - drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, firstNodeY); + const int scrollStartY = y + 3; + drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY); } // Public screen entry for compass static void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) @@ -2181,83 +2327,6 @@ static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, drawNodeListScreen(display, state, x, y, "Distances", drawNodeDistance); } -// *********************** -// * Common Header * -// *********************** -void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) -{ - constexpr int HEADER_OFFSET_Y = 2; - y += HEADER_OFFSET_Y; - - const bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); - const bool isBold = config.display.heading_bold; - const int xOffset = 4; - const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int screenWidth = display->getWidth(); - - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - // === Background highlight === - if (isInverted) { - drawRoundedHighlight(display, x, y, screenWidth, highlightHeight, 2); - display->setColor(BLACK); - } - - // === Text baseline === - const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - - // === Battery icon (scale-aware vertical centering) === - int batteryScale = 1; - if (screenWidth >= 200) - batteryScale = 2; - - int batteryHeight = 8 * batteryScale; - int batteryY = y + (highlightHeight / 2) - (batteryHeight / 2); - - // Only shift right 3px if screen is wider than 128 - int batteryX = x + xOffset - 2; - if (screenWidth > 128) - batteryX += 2; - - drawBattery(display, batteryX, batteryY, imgBattery, powerStatus); - - // === Battery % text === - char percentStr[8]; - snprintf(percentStr, sizeof(percentStr), "%d%%", powerStatus->getBatteryChargePercent()); - - const int batteryOffset = screenWidth > 128 ? 34 : 16; - const int percentX = x + xOffset + batteryOffset; - display->drawString(percentX, textY, percentStr); - if (isBold) - display->drawString(percentX + 1, textY, percentStr); - - // === Time string (right-aligned) === - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); - if (rtc_sec > 0) { - long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; - int hour = hms / SEC_PER_HOUR; - int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - - bool isPM = hour >= 12; - hour = hour % 12; - if (hour == 0) - hour = 12; - - char timeStr[10]; - snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); - - int timeX = screenWidth + 3 - xOffset - display->getStringWidth(timeStr); - if (screenWidth > 128) - timeX -= 1; - display->drawString(timeX, textY, timeStr); - if (isBold) - display->drawString(timeX - 1, textY, timeStr); - } - - display->setColor(WHITE); -} - // **************************** // * Device Focused Screen * // **************************** @@ -2452,10 +2521,6 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat display->setColor(WHITE); display->setTextAlignment(TEXT_ALIGN_LEFT); - // Row Y offset - int rowYOffset = FONT_HEIGHT_SMALL - 3; - int rowY = y + rowYOffset; - // === First Row: My Location === #if HAS_GPS bool origBold = config.display.heading_bold; From ae47de152c81b35905c4e150954e49fc03bdd650 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 5 Apr 2025 19:07:33 -0400 Subject: [PATCH 045/265] Node list cleanup and unification --- src/graphics/Screen.cpp | 396 +++++++++++++++------------------------- 1 file changed, 149 insertions(+), 247 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index e8871358f..d0e06a778 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1939,93 +1939,11 @@ void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_ display->drawLine(separatorX, yStart, separatorX, yEnd); } -// Draws node name with how long ago it was last heard from -void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) -{ - int screenWidth = display->getWidth(); - bool isLeftCol = (x < screenWidth / 2); - - // Adjust offset based on column and screen width - int timeOffset = - (screenWidth > 128) - ? (isLeftCol ? 41 : 45) - : (isLeftCol ? 24 : 30); // offset large screen (?Left:Right column), offset small screen (?Left:Right column) - - String nodeName = getSafeNodeName(node); - - char timeStr[10]; - uint32_t seconds = sinceLastSeen(node); - if (seconds == 0 || seconds == UINT32_MAX) { - snprintf(timeStr, sizeof(timeStr), "? "); - } else { - uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; - snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"), - (days ? days - : hours ? hours - : minutes), - (days ? 'd' - : hours ? 'h' - : 'm')); - } - - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawString(x, y, nodeName); - display->drawString(x + columnWidth - timeOffset, y, timeStr); -} -// Draws each node's name, hop count, and signal bars -void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) -{ - int screenWidth = display->getWidth(); - bool isLeftCol = (x < screenWidth / 2); - - int nameMaxWidth = columnWidth - 25; - int barsOffset = - (screenWidth > 128) - ? (isLeftCol ? 26 : 30) - : (isLeftCol ? 17 : 19); // offset large screen (?Left:Right column), offset small screen (?Left:Right column) - int hopOffset = - (screenWidth > 128) - ? (isLeftCol ? 32 : 38) - : (isLeftCol ? 18 : 20); // offset large screen (?Left:Right column), offset small screen (?Left:Right column) - - int barsXOffset = columnWidth - barsOffset; - - String nodeName = getSafeNodeName(node); - - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x, y, nameMaxWidth, nodeName); - - char hopStr[6] = ""; - if (node->has_hops_away && node->hops_away > 0) - snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away); - - if (hopStr[0] != '\0') { - int hopX = x + columnWidth - hopOffset - display->getStringWidth(hopStr); - display->drawString(hopX, y, hopStr); - } - - // Signal bars based on SNR - int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; - int barWidth = 2; - int barStartX = x + barsXOffset; - int barStartY = y + (FONT_HEIGHT_SMALL / 2) + 2; - - for (int b = 0; b < 4; b++) { - if (b < bars) { - int height = (b * 2); - display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height); - } - } -} - -// Typedef for passing different render functions into one reusable screen function typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); +typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int columnWidth, float heading, double lat, double lon); -// Shared function that renders all node screens (LastHeard, Hop/Signal) -void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, - EntryRenderer renderer) +void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, + const char *title, EntryRenderer renderer, NodeExtrasRenderer extras = nullptr) { const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; const int rowYOffset = FONT_HEIGHT_SMALL - 3; @@ -2101,171 +2019,95 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY); } - -// Public screen function: shows how recently nodes were heard -static void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - drawNodeListScreen(display, state, x, y, "Node List", drawEntryLastHeard); -} - -// Public screen function: shows hop count + signal strength -static void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - drawNodeListScreen(display, state, x, y, "Hop|Sig", drawEntryHopSignal); -} - -// Helper function: Draw a single node entry for Node List (Modified for Compass Screen) -void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +// **************************** +// * Last Heard Screen * +// **************************** +void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { int screenWidth = display->getWidth(); bool isLeftCol = (x < screenWidth / 2); - // Adjust max text width depending on column and screen width - int nameMaxWidth = columnWidth - (screenWidth > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + // Adjust offset based on column and screen width + int timeOffset = + (screenWidth > 128) + ? (isLeftCol ? 41 : 45) + : (isLeftCol ? 24 : 30); // offset large screen (?Left:Right column), offset small screen (?Left:Right column) + + String nodeName = getSafeNodeName(node); + + char timeStr[10]; + uint32_t seconds = sinceLastSeen(node); + if (seconds == 0 || seconds == UINT32_MAX) { + snprintf(timeStr, sizeof(timeStr), "? "); + } else { + uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; + snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"), + (days ? days + : hours ? hours + : minutes), + (days ? 'd' + : hours ? 'h' + : 'm')); + } + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawString(x, y, nodeName); + display->drawString(x + columnWidth - timeOffset, y, timeStr); +} + +// **************************** +// * Hops / Signal Screen * +// **************************** +void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + int screenWidth = display->getWidth(); + bool isLeftCol = (x < screenWidth / 2); + + int nameMaxWidth = columnWidth - 25; + int barsOffset = + (screenWidth > 128) + ? (isLeftCol ? 26 : 30) + : (isLeftCol ? 17 : 19); // offset large screen (?Left:Right column), offset small screen (?Left:Right column) + int hopOffset = + (screenWidth > 128) + ? (isLeftCol ? 32 : 38) + : (isLeftCol ? 18 : 20); // offset large screen (?Left:Right column), offset small screen (?Left:Right column) + + int barsXOffset = columnWidth - barsOffset; String nodeName = getSafeNodeName(node); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); display->drawStringMaxWidth(x, y, nameMaxWidth, nodeName); -} -// Extra compass element drawer (injects compass arrows) -typedef void (*CompassExtraRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int columnWidth, float myHeading, - double userLat, double userLon); + char hopStr[6] = ""; + if (node->has_hops_away && node->hops_away > 0) + snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away); -void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, - double userLat, double userLon) -{ - if (!nodeDB->hasValidPosition(node)) - return; - - int screenWidth = display->getWidth(); - bool isLeftCol = (x < screenWidth / 2); - - double nodeLat = node->position.latitude_i * 1e-7; - double nodeLon = node->position.longitude_i * 1e-7; - float bearingToNode = calculateBearing(userLat, userLon, nodeLat, nodeLon); - float relativeBearing = fmod((bearingToNode - myHeading + 360), 360); - float arrowAngle = relativeBearing * DEG_TO_RAD; - - // Adaptive offset for compass icon based on screen width + column - int arrowXOffset = (screenWidth > 128) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); - - int compassX = x + columnWidth - arrowXOffset; - int compassY = y + FONT_HEIGHT_SMALL / 2; - int size = FONT_HEIGHT_SMALL / 2 - 2; - int arrowLength = size - 2; - - int xEnd = compassX + arrowLength * cos(arrowAngle); - int yEnd = compassY - arrowLength * sin(arrowAngle); - - display->fillCircle(compassX, compassY, size); - display->drawCircle(compassX, compassY, size); - display->drawLine(compassX, compassY, xEnd, yEnd); -} - -// Generic node+compass renderer (like drawNodeListScreen but with compass support) -void drawNodeListWithExtrasScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, - EntryRenderer renderer, CompassExtraRenderer extras) -{ - const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; - const int rowYOffset = FONT_HEIGHT_SMALL - 3; - - int columnWidth = display->getWidth() / 2; - int screenWidth = display->getWidth(); - - display->clear(); - - // === Draw common header (battery + time) - drawCommonHeader(display, x, y); - - // === Draw title inside header (centered) - const int highlightHeight = COMMON_HEADER_HEIGHT; - const int textY = y + 2 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - const int centerX = x + screenWidth / 2; - - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) - display->setColor(BLACK); - - display->drawString(centerX, textY, title); - if (config.display.heading_bold) - display->drawString(centerX + 1, textY, title); - - display->setColor(WHITE); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - // === Space below header - y += COMMON_HEADER_HEIGHT; - - std::vector nodeList; - retrieveAndSortNodes(nodeList); - - int totalEntries = nodeList.size(); - int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; - int visibleNodeRows = totalRowsAvailable; - - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - double userLat = 0.0, userLon = 0.0; - bool hasUserPosition = nodeDB->hasValidPosition(ourNode); - if (hasUserPosition) { - userLat = ourNode->position.latitude_i * 1e-7; - userLon = ourNode->position.longitude_i * 1e-7; + if (hopStr[0] != '\0') { + int hopX = x + columnWidth - hopOffset - display->getStringWidth(hopStr); + display->drawString(hopX, y, hopStr); } - float myHeading = screen->hasHeading() ? screen->getHeading() : 0.0f; + // Signal bars based on SNR + int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; + int barWidth = 2; + int barStartX = x + barsXOffset; + int barStartY = y + (FONT_HEIGHT_SMALL / 2) + 2; - int startIndex = scrollIndex * visibleNodeRows * 2; - int endIndex = std::min(startIndex + visibleNodeRows * 2, totalEntries); - - int yOffset = 0; - int col = 0; - int lastNodeY = y; - int shownCount = 0; - - for (int i = startIndex; i < endIndex; ++i) { - int xPos = x + (col * columnWidth); - int yPos = y + yOffset; - - renderer(display, nodeList[i].node, xPos, yPos, columnWidth); - - if (hasUserPosition && extras) { - extras(display, nodeList[i].node, xPos, yPos, columnWidth, myHeading, userLat, userLon); - } - - lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); - yOffset += rowYOffset; - shownCount++; - - if (y + yOffset > display->getHeight() - FONT_HEIGHT_SMALL) { - yOffset = 0; - col++; - if (col > 1) - break; + for (int b = 0; b < 4; b++) { + if (b < bars) { + int height = (b * 2); + display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height); } } - - // === Draw column separator - if (shownCount > 0) { - const int firstNodeY = y + 3; // where the first node row starts - drawColumnSeparator(display, x, firstNodeY, lastNodeY); - } - - const int scrollStartY = y + 3; - drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY); -} -// Public screen entry for compass -static void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - drawNodeListWithExtrasScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow); } -// ******************************** -// * Node List Distance Screen * -// ******************************** - +// ************************** +// * Distance Screen * +// ************************** void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { int screenWidth = display->getWidth(); @@ -2322,11 +2164,71 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 } } -static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ +// Public screen function: shows how recently nodes were heard +static void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + drawNodeListScreen(display, state, x, y, "Node List", drawEntryLastHeard); +} + +// Public screen function: shows hop count + signal strength +static void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + drawNodeListScreen(display, state, x, y, "Hop|Sig", drawEntryHopSignal); +} + +static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { drawNodeListScreen(display, state, x, y, "Distances", drawNodeDistance); } +// Helper function: Draw a single node entry for Node List (Modified for Compass Screen) +void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + int screenWidth = display->getWidth(); + bool isLeftCol = (x < screenWidth / 2); + + // Adjust max text width depending on column and screen width + int nameMaxWidth = columnWidth - (screenWidth > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + + String nodeName = getSafeNodeName(node); + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawStringMaxWidth(x, y, nameMaxWidth, nodeName); +} + +void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, + double userLat, double userLon) +{ + if (!nodeDB->hasValidPosition(node)) + return; + + int screenWidth = display->getWidth(); + bool isLeftCol = (x < screenWidth / 2); + + double nodeLat = node->position.latitude_i * 1e-7; + double nodeLon = node->position.longitude_i * 1e-7; + float bearingToNode = calculateBearing(userLat, userLon, nodeLat, nodeLon); + float relativeBearing = fmod((bearingToNode - myHeading + 360), 360); + float arrowAngle = relativeBearing * DEG_TO_RAD; + + // Adaptive offset for compass icon based on screen width + column + int arrowXOffset = (screenWidth > 128) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); + + int compassX = x + columnWidth - arrowXOffset; + int compassY = y + FONT_HEIGHT_SMALL / 2; + int size = FONT_HEIGHT_SMALL / 2 - 2; + int arrowLength = size - 2; + + int xEnd = compassX + arrowLength * cos(arrowAngle); + int yEnd = compassY - arrowLength * sin(arrowAngle); + + display->fillCircle(compassX, compassY, size); + display->drawCircle(compassX, compassY, size); + display->drawLine(compassX, compassY, xEnd, yEnd); +} +// Public screen entry for compass +static void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow); +} + // **************************** // * Device Focused Screen * // **************************** @@ -3420,22 +3322,22 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = drawCompassAndLocationScreen; normalFrames[numframes++] = drawMemoryScreen; - // then all the nodes - // We only show a few nodes in our scrolling list - because meshes with many nodes would have too many screens - // size_t numToShow = min(numMeshNodes, 4U); - // for (size_t i = 0; i < numToShow; i++) - // normalFrames[numframes++] = drawNodeInfo; +// then all the nodes +// We only show a few nodes in our scrolling list - because meshes with many nodes would have too many screens +size_t numToShow = min(numMeshNodes, 4U); +for (size_t i = 0; i < numToShow; i++) +normalFrames[numframes++] = drawNodeInfo; - // then the debug info - // - // Since frames are basic function pointers, we have to use a helper to - // call a method on debugInfo object. - // fsi.positions.log = numframes; - // normalFrames[numframes++] = &Screen::drawDebugInfoTrampoline; +// then the debug info - // call a method on debugInfoScreen object (for more details) - // fsi.positions.settings = numframes; - // normalFrames[numframes++] = &Screen::drawDebugInfoSettingsTrampoline; +// Since frames are basic function pointers, we have to use a helper to +// call a method on debugInfo object. +fsi.positions.log = numframes; +normalFrames[numframes++] = &Screen::drawDebugInfoTrampoline; + +// call a method on debugInfoScreen object (for more details) +fsi.positions.settings = numframes; +normalFrames[numframes++] = &Screen::drawDebugInfoSettingsTrampoline; fsi.positions.wifi = numframes; #if HAS_WIFI && !defined(ARCH_PORTDUINO) From 225e2726f3c0b990a0619388f93144d55c7b4309 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 5 Apr 2025 19:47:44 -0400 Subject: [PATCH 046/265] Fixed mini compass and rearranged screen order --- src/graphics/Screen.cpp | 75 +++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index d0e06a778..6936ac268 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1943,7 +1943,8 @@ typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int columnWidth, float heading, double lat, double lon); void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, - const char *title, EntryRenderer renderer, NodeExtrasRenderer extras = nullptr) + const char *title, EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, + float heading = 0, double lat = 0, double lon = 0) { const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; const int rowYOffset = FONT_HEIGHT_SMALL - 3; @@ -1997,6 +1998,12 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t int xPos = x + (col * columnWidth); int yPos = y + yOffset; renderer(display, nodeList[i].node, xPos, yPos, columnWidth); + + // βœ… Actually render the compass arrow + if (extras) { + extras(display, nodeList[i].node, xPos, yPos, columnWidth, heading, lat, lon); + } + lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); yOffset += rowYOffset; shownCount++; @@ -2011,7 +2018,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t // === Draw column separator if (shownCount > 0) { - const int firstNodeY = y + 3; // where the first node row starts + const int firstNodeY = y + 3; drawColumnSeparator(display, x, firstNodeY, lastNodeY); } @@ -2209,24 +2216,50 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 float relativeBearing = fmod((bearingToNode - myHeading + 360), 360); float arrowAngle = relativeBearing * DEG_TO_RAD; - // Adaptive offset for compass icon based on screen width + column + // Compass position int arrowXOffset = (screenWidth > 128) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); - int compassX = x + columnWidth - arrowXOffset; int compassY = y + FONT_HEIGHT_SMALL / 2; - int size = FONT_HEIGHT_SMALL / 2 - 2; - int arrowLength = size - 2; - int xEnd = compassX + arrowLength * cos(arrowAngle); - int yEnd = compassY - arrowLength * sin(arrowAngle); + int radius = FONT_HEIGHT_SMALL / 2 - 3; - display->fillCircle(compassX, compassY, size); - display->drawCircle(compassX, compassY, size); + // Arrow should go from center to edge (not beyond) + int xEnd = compassX + radius * cos(arrowAngle); + int yEnd = compassY - radius * sin(arrowAngle); + + // Draw circle outline + display->drawCircle(compassX, compassY, radius); + + // Draw thin arrow line from center to circle edge display->drawLine(compassX, compassY, xEnd, yEnd); } + // Public screen entry for compass -static void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow); +static void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + float heading = 0; + bool validHeading = false; + double lat = 0; + double lon = 0; + +#if HAS_GPS + geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()), + int32_t(gpsStatus->getAltitude())); + lat = geoCoord.getLatitude() * 1e-7; + lon = geoCoord.getLongitude() * 1e-7; + + if (screen->hasHeading()) { + heading = screen->getHeading(); // degrees + validHeading = true; + } else { + heading = screen->estimatedHeading(lat, lon); + validHeading = !isnan(heading); + } +#endif + + if (!validHeading) return; + + drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon); } // **************************** @@ -3314,30 +3347,30 @@ void Screen::setFrames(FrameFocus focus) } normalFrames[numframes++] = drawDeviceFocused; + normalFrames[numframes++] = drawLoRaFocused; normalFrames[numframes++] = drawLastHeardScreen; normalFrames[numframes++] = drawDistanceScreen; - normalFrames[numframes++] = drawNodeListWithCompasses; normalFrames[numframes++] = drawHopSignalScreen; - normalFrames[numframes++] = drawLoRaFocused; + normalFrames[numframes++] = drawNodeListWithCompasses; normalFrames[numframes++] = drawCompassAndLocationScreen; normalFrames[numframes++] = drawMemoryScreen; // then all the nodes // We only show a few nodes in our scrolling list - because meshes with many nodes would have too many screens -size_t numToShow = min(numMeshNodes, 4U); -for (size_t i = 0; i < numToShow; i++) -normalFrames[numframes++] = drawNodeInfo; +//size_t numToShow = min(numMeshNodes, 4U); +//for (size_t i = 0; i < numToShow; i++) +//normalFrames[numframes++] = drawNodeInfo; // then the debug info // Since frames are basic function pointers, we have to use a helper to // call a method on debugInfo object. -fsi.positions.log = numframes; -normalFrames[numframes++] = &Screen::drawDebugInfoTrampoline; +//fsi.positions.log = numframes; +//normalFrames[numframes++] = &Screen::drawDebugInfoTrampoline; // call a method on debugInfoScreen object (for more details) -fsi.positions.settings = numframes; -normalFrames[numframes++] = &Screen::drawDebugInfoSettingsTrampoline; +//fsi.positions.settings = numframes; +//normalFrames[numframes++] = &Screen::drawDebugInfoSettingsTrampoline; fsi.positions.wifi = numframes; #if HAS_WIFI && !defined(ARCH_PORTDUINO) From 08bbff260c99abbf86a9203cb4d49a4336459460 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 5 Apr 2025 21:23:16 -0400 Subject: [PATCH 047/265] Adjustments --- src/graphics/Screen.cpp | 44 ++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 6936ac268..4138af532 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2172,16 +2172,19 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 } // Public screen function: shows how recently nodes were heard -static void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { +static void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ drawNodeListScreen(display, state, x, y, "Node List", drawEntryLastHeard); } // Public screen function: shows hop count + signal strength -static void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { +static void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ drawNodeListScreen(display, state, x, y, "Hop|Sig", drawEntryHopSignal); } -static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { +static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ drawNodeListScreen(display, state, x, y, "Distances", drawNodeDistance); } @@ -2283,13 +2286,13 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i // Display Region and Channel Utilization config.display.heading_bold = false; - drawNodes(display, x + 2, compactFirstLine + 3, nodeStatus); + drawNodes(display, x + 1, compactFirstLine + 3, nodeStatus); #if HAS_GPS auto number_of_satellites = gpsStatus->getNumSatellites(); - int gps_rightchar_offset = (SCREEN_WIDTH > 128) ? -55 : -49; + int gps_rightchar_offset = (SCREEN_WIDTH > 128) ? -52 : -46; if (number_of_satellites < 10) { - gps_rightchar_offset += (SCREEN_WIDTH > 128) ? 12 : 6; + gps_rightchar_offset += (SCREEN_WIDTH > 128) ? 14 : 6; } if (config.position.fixed_position) { @@ -2335,16 +2338,16 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i char uptimeFullStr[16]; snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); - display->drawString(x + 2, compactSecondLine, uptimeFullStr); + display->drawString(x, compactSecondLine, uptimeFullStr); char batStr[20]; if (powerStatus->getHasBattery()) { int batV = powerStatus->getBatteryVoltageMv() / 1000; int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; snprintf(batStr, sizeof(batStr), "%01d.%02dV", batV, batCv); - display->drawString(x - 2 + SCREEN_WIDTH - display->getStringWidth(batStr), compactSecondLine, batStr); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(batStr), compactSecondLine, batStr); } else { - display->drawString(x - 2 + SCREEN_WIDTH - display->getStringWidth("USB"), compactSecondLine, String("USB")); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth("USB"), compactSecondLine, String("USB")); } // === Third Row: LongName Centered === @@ -2356,7 +2359,8 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i const char *longName = ourNode->user.long_name; int textWidth = display->getStringWidth(longName); int nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, compactFourthLine, longName); + int yOffset = (SCREEN_WIDTH > 128) ? 0 : 7; + display->drawString(nameX, compactFourthLine - yOffset, longName); } } @@ -2398,12 +2402,12 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int const char *region = myRegion ? myRegion->name : NULL; const char *preset = (screenWidth > 128) ? "Preset" : "Prst"; snprintf(regionradiopreset, sizeof(regionradiopreset), "%s: %s/%s", preset, region, mode); - display->drawString(x + 2, compactFirstLine, regionradiopreset); + display->drawString(x, compactFirstLine, regionradiopreset); // === Second Row: Channel Utilization === char chUtil[25]; snprintf(chUtil, sizeof(chUtil), "ChUtil: %2.0f%%", airTime->channelUtilizationPercent()); - display->drawString(x + 2, compactSecondLine, chUtil); + display->drawString(x, compactSecondLine, chUtil); // === Third Row: Channel Utilization === // Get our hardware ID @@ -2414,14 +2418,14 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int char shortnameble[35]; snprintf(shortnameble, sizeof(shortnameble), "%s: %s/%s", "Short/BLE", haveGlyphs(owner.short_name) ? owner.short_name : "", ourId); - display->drawString(x + 2, compactThirdLine, shortnameble); + display->drawString(x, compactThirdLine, shortnameble); // === Fourth Row: Node longName === meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { char devicelongname[55]; snprintf(devicelongname, sizeof(devicelongname), "%s: %s", "Name", ourNode->user.long_name); - display->drawString(x + 2, compactFourthLine, devicelongname); + display->drawString(x, compactFourthLine, devicelongname); } } @@ -2461,7 +2465,7 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat bool origBold = config.display.heading_bold; config.display.heading_bold = false; if (config.position.fixed_position) { - display->drawString(x + 2, compactFirstLine, "Sat:"); + display->drawString(x, compactFirstLine, "Sat:"); if (screenWidth > 128) { drawGPS(display, x + 32, compactFirstLine + 3, gpsStatus); } else { @@ -2470,14 +2474,14 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat } else if (!gpsStatus || !gpsStatus->getIsConnected()) { String displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; - display->drawString(x + 2, compactFirstLine, "Sat:"); + display->drawString(x, compactFirstLine, "Sat:"); if (screenWidth > 128) { display->drawString(x + 32, compactFirstLine, displayLine); } else { display->drawString(x + 23, compactFirstLine, displayLine); } } else { - display->drawString(x + 2, compactFirstLine, "Sat:"); + display->drawString(x, compactFirstLine, "Sat:"); if (screenWidth > 128) { drawGPS(display, x + 32, compactFirstLine + 3, gpsStatus); } else { @@ -2507,17 +2511,17 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat displayLine = "Alt: " + String(geoCoord.getAltitude()) + "m"; if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) displayLine = "Alt: " + String(geoCoord.getAltitude() * METERS_TO_FEET) + "ft"; - display->drawString(x + 2, compactSecondLine, displayLine); + display->drawString(x, compactSecondLine, displayLine); // === Third Row: Latitude === char latStr[32]; snprintf(latStr, sizeof(latStr), "Lat: %.5f", geoCoord.getLatitude() * 1e-7); - display->drawString(x + 2, compactThirdLine, latStr); + display->drawString(x, compactThirdLine, latStr); // === Fifth Row: Longitude === char lonStr[32]; snprintf(lonStr, sizeof(lonStr), "Lon: %.5f", geoCoord.getLongitude() * 1e-7); - display->drawString(x + 2, compactFourthLine, lonStr); + display->drawString(x, compactFourthLine, lonStr); // === Draw Compass if heading is valid === if (validHeading) { From b7218f4a539b18a0b27bdbc1a7f64fe22205f8d8 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 5 Apr 2025 21:23:27 -0400 Subject: [PATCH 048/265] Update Screen.cpp --- src/graphics/Screen.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 4138af532..f14556bc7 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -3353,8 +3353,8 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = drawDeviceFocused; normalFrames[numframes++] = drawLoRaFocused; normalFrames[numframes++] = drawLastHeardScreen; - normalFrames[numframes++] = drawDistanceScreen; normalFrames[numframes++] = drawHopSignalScreen; + normalFrames[numframes++] = drawDistanceScreen; normalFrames[numframes++] = drawNodeListWithCompasses; normalFrames[numframes++] = drawCompassAndLocationScreen; normalFrames[numframes++] = drawMemoryScreen; From 9cdeedfcbdaa5c88e1d85ddfeb2f7ac078ee3f0d Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 5 Apr 2025 21:40:40 -0400 Subject: [PATCH 049/265] Update Screen.cpp --- src/graphics/Screen.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index f14556bc7..08d8c90c2 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -3351,12 +3351,12 @@ void Screen::setFrames(FrameFocus focus) } normalFrames[numframes++] = drawDeviceFocused; - normalFrames[numframes++] = drawLoRaFocused; normalFrames[numframes++] = drawLastHeardScreen; normalFrames[numframes++] = drawHopSignalScreen; normalFrames[numframes++] = drawDistanceScreen; normalFrames[numframes++] = drawNodeListWithCompasses; normalFrames[numframes++] = drawCompassAndLocationScreen; + normalFrames[numframes++] = drawLoRaFocused; normalFrames[numframes++] = drawMemoryScreen; // then all the nodes From afc710a868dc038520468e52d509d7fb087e04a8 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 5 Apr 2025 22:37:05 -0400 Subject: [PATCH 050/265] Mini compass screen shape change --- src/graphics/Screen.cpp | 42 +++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 08d8c90c2..31d2a762d 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2203,7 +2203,6 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->setFont(FONT_SMALL); display->drawStringMaxWidth(x, y, nameMaxWidth, nodeName); } - void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, double userLat, double userLon) { @@ -2212,31 +2211,46 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 int screenWidth = display->getWidth(); bool isLeftCol = (x < screenWidth / 2); + int arrowXOffset = (screenWidth > 128) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); + + int centerX = x + columnWidth - arrowXOffset; + int centerY = y + FONT_HEIGHT_SMALL / 2; double nodeLat = node->position.latitude_i * 1e-7; double nodeLon = node->position.longitude_i * 1e-7; float bearingToNode = calculateBearing(userLat, userLon, nodeLat, nodeLon); float relativeBearing = fmod((bearingToNode - myHeading + 360), 360); - float arrowAngle = relativeBearing * DEG_TO_RAD; + float angle = relativeBearing * DEG_TO_RAD; - // Compass position - int arrowXOffset = (screenWidth > 128) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); - int compassX = x + columnWidth - arrowXOffset; - int compassY = y + FONT_HEIGHT_SMALL / 2; + // Shrink size by 2px + int size = FONT_HEIGHT_SMALL - 5; + float halfSize = size / 2.0; - int radius = FONT_HEIGHT_SMALL / 2 - 3; + // Point of the arrow + int tipX = centerX + halfSize * cos(angle); + int tipY = centerY - halfSize * sin(angle); - // Arrow should go from center to edge (not beyond) - int xEnd = compassX + radius * cos(arrowAngle); - int yEnd = compassY - radius * sin(arrowAngle); + float baseAngle = radians(35); + float sideLen = halfSize * 0.95; + float notchInset = halfSize * 0.35; - // Draw circle outline - display->drawCircle(compassX, compassY, radius); + // Left and right corners + int leftX = centerX + sideLen * cos(angle + PI - baseAngle); + int leftY = centerY - sideLen * sin(angle + PI - baseAngle); - // Draw thin arrow line from center to circle edge - display->drawLine(compassX, compassY, xEnd, yEnd); + int rightX = centerX + sideLen * cos(angle + PI + baseAngle); + int rightY = centerY - sideLen * sin(angle + PI + baseAngle); + + // Center notch (cut-in) + int notchX = centerX - notchInset * cos(angle); + int notchY = centerY + notchInset * sin(angle); + + // Draw the chevron-style arrowhead + display->fillTriangle(tipX, tipY, leftX, leftY, notchX, notchY); + display->fillTriangle(tipX, tipY, notchX, notchY, rightX, rightY); } + // Public screen entry for compass static void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { From c49f20f96c3cbee0cf9045b00941504dad7a2e8b Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 5 Apr 2025 23:29:47 -0400 Subject: [PATCH 051/265] Battery Circumcision --- src/graphics/Screen.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 31d2a762d..6520e189b 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1024,7 +1024,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) // === Battery dynamically scaled === const int nubSize = 2; - const int batteryLong = screenWidth > 200 ? 28 : 24; // Wider on bigger TFTs + const int batteryLong = screenWidth > 200 ? 29 : 25; // Was 28/24 const int batteryShort = highlightHeight - nubSize - 2; int batteryX = x + xOffset; @@ -1050,7 +1050,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) display->fillRect( batteryX + batteryLong, batteryY + (batteryShort / 2) - 3, - nubSize + 2, 6 + nubSize + 1, 6 ); if (isCharging && isBoltVisible) { @@ -1077,12 +1077,12 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) } else { // === Vertical battery === const int batteryWidth = 8; - const int batteryHeight = batteryShort; + const int batteryHeight = batteryShort + 1; const int totalBatteryHeight = batteryHeight + nubSize; batteryX += -2; batteryY = y + (highlightHeight - totalBatteryHeight) / 2 + nubSize; - display->fillRect(batteryX + 2, batteryY - nubSize, 4, nubSize); // Nub + display->fillRect(batteryX + 2, batteryY - (nubSize - 1), 4, nubSize - 1); // Nub display->drawRect(batteryX, batteryY, batteryWidth, batteryHeight); // Body if (isCharging && isBoltVisible) { From 01f7cd998af164e65b43921b00c90630cadb5693 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 5 Apr 2025 23:41:35 -0400 Subject: [PATCH 052/265] re-circumcised --- src/graphics/Screen.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 6520e189b..7d708f26b 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1050,7 +1050,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) display->fillRect( batteryX + batteryLong, batteryY + (batteryShort / 2) - 3, - nubSize + 1, 6 + nubSize, 6 ); if (isCharging && isBoltVisible) { From 33cfe14d4a6f081017c3f209bfd9c951551834f3 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 6 Apr 2025 04:31:40 -0400 Subject: [PATCH 053/265] Cleanup for nodeinfo --- src/graphics/Screen.cpp | 160 +++++++++++++++++++++++++--------------- 1 file changed, 100 insertions(+), 60 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 7d708f26b..27b3f215d 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1704,90 +1704,134 @@ uint16_t Screen::getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) return diam - 20; }; +// ********************* +// * Node Info * +// ********************* static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - // We only advance our nodeIndex if the frame # has changed - because - // drawNodeInfo will be called repeatedly while the frame is shown + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // === Header === + drawCommonHeader(display, x, y); + + // === Reset color in case inverted mode left it BLACK === + display->setColor(WHITE); + + // === Advance to next favorite node when frame changes === if (state->currentFrame != prevFrame) { prevFrame = state->currentFrame; - nodeIndex = (nodeIndex + 1) % nodeDB->getNumMeshNodes(); - meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(nodeIndex); - if (n->num == nodeDB->getNodeNum()) { - // Don't show our node, just skip to next - nodeIndex = (nodeIndex + 1) % nodeDB->getNumMeshNodes(); - n = nodeDB->getMeshNodeByIndex(nodeIndex); - } + int attempts = 0; + int total = nodeDB->getNumMeshNodes(); + do { + nodeIndex = (nodeIndex + 1) % total; + meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(nodeIndex); + if (n && n->is_favorite && n->num != nodeDB->getNodeNum()) { + break; + } + } while (++attempts < total); } meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(nodeIndex); + if (!node || !node->is_favorite || node->num == nodeDB->getNodeNum()) return; - display->setFont(FONT_SMALL); + // === Draw Title (centered safe short name or ID) === + static char titleBuf[20]; + const char *titleStr = nullptr; - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); - - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + bool valid = node->has_user && strlen(node->user.short_name) > 0; + if (valid) { + for (size_t i = 0; i < strlen(node->user.short_name); i++) { + uint8_t c = (uint8_t)node->user.short_name[i]; + if (c < 32 || c > 126) { + valid = false; + break; + } + } } - const char *username = node->has_user ? node->user.long_name : "Unknown Name"; + if (valid) { + titleStr = node->user.short_name; + } else { + snprintf(titleBuf, sizeof(titleBuf), "%04X", (uint16_t)(node->num & 0xFFFF)); + titleStr = titleBuf; + } + const int screenWidth = display->getWidth(); + const int centerX = x + screenWidth / 2; + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + const int headerOffsetY = 2; + const int titleY = y + headerOffsetY + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + + display->setTextAlignment(TEXT_ALIGN_CENTER); + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->setColor(BLACK); + } + display->drawString(centerX, titleY, titleStr); + if (config.display.heading_bold) { + display->drawString(centerX + 1, titleY, titleStr); + } + display->setColor(WHITE); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === First Row: Last Heard === + static char lastStr[20]; + screen->getTimeAgoStr(sinceLastSeen(node), lastStr, sizeof(lastStr)); + display->drawString(x, compactFirstLine, lastStr); + + // === Second Row: Signal / Hops === static char signalStr[20]; - - // section here to choose whether to display hops away rather than signal strength if more than 0 hops away. if (node->hops_away > 0) { snprintf(signalStr, sizeof(signalStr), "Hops Away: %d", node->hops_away); } else { snprintf(signalStr, sizeof(signalStr), "Signal: %d%%", clamp((int)((node->snr + 10) * 5), 0, 100)); } + display->drawString(x, compactSecondLine, signalStr); - static char lastStr[20]; - screen->getTimeAgoStr(sinceLastSeen(node), lastStr, sizeof(lastStr)); - + // === Third Row: Distance and Bearing === static char distStr[20]; + strncpy(distStr, "? km ?Β°", sizeof(distStr)); if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - strncpy(distStr, "? mi ?Β°", sizeof(distStr)); // might not have location data - } else { - strncpy(distStr, "? km ?Β°", sizeof(distStr)); + strncpy(distStr, "? mi ?Β°", sizeof(distStr)); } + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - const char *fields[] = {username, lastStr, signalStr, distStr, NULL}; - int16_t compassX = 0, compassY = 0; - uint16_t compassDiam = Screen::getCompassDiam(SCREEN_WIDTH, SCREEN_HEIGHT); - // coordinates for the center of the compass/circle - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - compassX = x + SCREEN_WIDTH - compassDiam / 2 - 5; - compassY = y + SCREEN_HEIGHT / 2; - } else { - compassX = x + SCREEN_WIDTH - compassDiam / 2 - 5; - compassY = y + FONT_HEIGHT_SMALL + (SCREEN_HEIGHT - FONT_HEIGHT_SMALL) / 2; - } + // === Match GPS screen compass position === + const int16_t topY = compactFirstLine; + const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); + const int16_t usableHeight = bottomY - topY - 5; + + int16_t compassRadius = usableHeight / 2; + if (compassRadius < 8) compassRadius = 8; + const int16_t compassDiam = compassRadius * 2; + const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; + const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + bool hasNodeHeading = false; - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { const meshtastic_PositionLite &op = ourNode->position; - float myHeading; - if (screen->hasHeading()) - myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians - else - myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + float myHeading = screen->hasHeading() + ? radians(screen->getHeading()) + : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + screen->drawCompassNorth(display, compassX, compassY, myHeading); if (nodeDB->hasValidPosition(node)) { - // display direction toward node hasNodeHeading = true; const meshtastic_PositionLite &p = node->position; - float d = - GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - float bearingToOther = - GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); - // If the top of the compass is a static north then bearingToOther can be drawn on the compass directly - // If the top of the compass is not a static north we need adjust bearingToOther based on heading + float d = GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), + DegD(op.latitude_i), DegD(op.longitude_i)); + + float bearingToOther = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), + DegD(p.latitude_i), DegD(p.longitude_i)); + if (!config.display.compass_north_top) bearingToOther -= myHeading; + screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther; @@ -1797,8 +1841,7 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ 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); + snprintf(distStr, sizeof(distStr), "%.1fmi %.0fΒ°", d * METERS_TO_FEET / MILES_TO_FEET, bearingToOtherDegrees); } else { if (d < 2000) snprintf(distStr, sizeof(distStr), "%.0fm %.0fΒ°", d, bearingToOtherDegrees); @@ -1807,20 +1850,17 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ } } } + + display->drawString(x, compactThirdLine, distStr); + if (!hasNodeHeading) { - // direction to node is unknown so display question mark - // Debug info for gps lock errors - // LOG_DEBUG("ourNode %d, ourPos %d, theirPos %d", !!ourNode, ourNode && hasValidPosition(ourNode), - // hasValidPosition(node)); display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?"); } - display->drawCircle(compassX, compassY, compassDiam / 2); - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->setColor(BLACK); - } - // Must be after distStr is populated - screen->drawColumns(display, x, y, fields); + display->drawCircle(compassX, compassY, compassRadius); + + // === Final reset to WHITE to ensure clean state for next frame === + display->setColor(WHITE); } // h! Each node entry holds a reference to its info and how long ago it was heard from From 74325ba439140989a84060940058cbc1e2a52c96 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 6 Apr 2025 14:28:45 -0400 Subject: [PATCH 054/265] Cleanup and labeling borth variants to tft --- src/graphics/Screen.cpp | 20 +------------------- variants/heltec_mesh_node_t114/variant.h | 1 + variants/heltec_vision_master_t190/variant.h | 4 +++- 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 27b3f215d..ed6a27e64 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1132,7 +1132,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) display->setColor(WHITE); } // **************************** -// * Tex Message Screen * +// * Text Message Screen * // **************************** void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { @@ -1581,11 +1581,6 @@ static int8_t prevFrame = -1; // Draw the arrow pointing to a node's location void Screen::drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) { - Serial.print("πŸ”„ [Node Heading] Raw Bearing (rad): "); - Serial.print(headingRadian); - Serial.print(" | (deg): "); - Serial.println(headingRadian * RAD_TO_DEG); - Point tip(0.0f, 0.5f), tail(0.0f, -0.35f); // pointing up initially float arrowOffsetX = 0.14f, arrowOffsetY = 1.0f; Point leftArrow(tip.x - arrowOffsetX, tip.y - arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y - arrowOffsetY); @@ -1604,14 +1599,6 @@ void Screen::drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t com display->drawLine(leftArrow.x, leftArrow.y, tail.x, tail.y); display->drawLine(rightArrow.x, rightArrow.y, tail.x, tail.y); */ - Serial.print("πŸ”₯ Arrow Tail X: "); - Serial.print(tail.x); - Serial.print(" | Y: "); - Serial.print(tail.y); - Serial.print(" | Tip X: "); - Serial.print(tip.x); - Serial.print(" | Tip Y: "); - Serial.println(tip.y); #ifdef USE_EINK display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); #else @@ -1672,11 +1659,6 @@ void Screen::drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t co rosePoints[i]->scale(compassDiam); rosePoints[i]->translate(compassX, compassY); } - display->drawCircle(NC1.x, NC1.y, 4); // North sign circle, 4px radius is sufficient for all displays. - Serial.print("πŸ”₯ North Marker X: "); - Serial.print(NC1.x); - Serial.print(" | Y: "); - Serial.println(NC1.y); } uint16_t Screen::getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) diff --git a/variants/heltec_mesh_node_t114/variant.h b/variants/heltec_mesh_node_t114/variant.h index 426085a26..c3d148d0a 100644 --- a/variants/heltec_mesh_node_t114/variant.h +++ b/variants/heltec_mesh_node_t114/variant.h @@ -56,6 +56,7 @@ extern "C" { #define TFT_WIDTH 240 #define TFT_OFFSET_X 0 #define TFT_OFFSET_Y 0 +#ifndef HAS_TFT // #define TFT_OFFSET_ROTATION 0 // #define SCREEN_ROTATE // #define SCREEN_TRANSITION_FRAMERATE 5 diff --git a/variants/heltec_vision_master_t190/variant.h b/variants/heltec_vision_master_t190/variant.h index 1da3f9971..aeb4c9adb 100644 --- a/variants/heltec_vision_master_t190/variant.h +++ b/variants/heltec_vision_master_t190/variant.h @@ -1,3 +1,4 @@ +#ifndef HAS_TFT #define BUTTON_PIN 0 #define BUTTON_PIN_SECONDARY 21 // Second built-in button #define BUTTON_SECONDARY_CANNEDMESSAGES // By default, use the secondary button as canned message input @@ -69,4 +70,5 @@ #define SX126X_RESET LORA_RESET #define SX126X_DIO2_AS_RF_SWITCH -#define SX126X_DIO3_TCXO_VOLTAGE 1.8 \ No newline at end of file +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 +#endif // HAS_TFT \ No newline at end of file From 396fc1824ee85724645660d1613140799c6989e9 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 6 Apr 2025 14:55:00 -0400 Subject: [PATCH 055/265] removed tft tag --- variants/heltec_mesh_node_t114/variant.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/variants/heltec_mesh_node_t114/variant.h b/variants/heltec_mesh_node_t114/variant.h index c3d148d0a..798c3538a 100644 --- a/variants/heltec_mesh_node_t114/variant.h +++ b/variants/heltec_mesh_node_t114/variant.h @@ -56,7 +56,7 @@ extern "C" { #define TFT_WIDTH 240 #define TFT_OFFSET_X 0 #define TFT_OFFSET_Y 0 -#ifndef HAS_TFT + // #define TFT_OFFSET_ROTATION 0 // #define SCREEN_ROTATE // #define SCREEN_TRANSITION_FRAMERATE 5 From 99f6b398b389d5fffecc898719b26d32a6b1b112 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 6 Apr 2025 15:14:51 -0400 Subject: [PATCH 056/265] Cleanup --- src/graphics/Screen.cpp | 83 +++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 48 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index ed6a27e64..8c3e3a85c 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -454,12 +454,11 @@ static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *img } // Slightly more conservative scaling based on screen width - int screenWidth = display->getWidth(); int scale = 1; - if (screenWidth >= 200) + if (SCREEN_WIDTH >= 200) scale = 2; - if (screenWidth >= 300) + if (SCREEN_WIDTH >= 300) scale = 2; // Do NOT go higher than 2 // Draw scaled battery image (16 columns Γ— 8 rows) @@ -1008,14 +1007,13 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) const bool isBold = config.display.heading_bold; const int xOffset = 4; const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int screenWidth = display->getWidth(); display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); // === Background highlight === if (isInverted) { - drawRoundedHighlight(display, x, y, screenWidth, highlightHeight, 2); + drawRoundedHighlight(display, x, y, SCREEN_WIDTH, highlightHeight, 2); display->setColor(BLACK); } @@ -1024,7 +1022,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) // === Battery dynamically scaled === const int nubSize = 2; - const int batteryLong = screenWidth > 200 ? 29 : 25; // Was 28/24 + const int batteryLong = SCREEN_WIDTH > 200 ? 29 : 25; // Was 28/24 const int batteryShort = highlightHeight - nubSize - 2; int batteryX = x + xOffset; @@ -1039,7 +1037,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) lastBlink = now; } - if (screenWidth > 128) { + if (SCREEN_WIDTH > 128) { // === Horizontal battery === batteryY = y + (highlightHeight - batteryShort) / 2; @@ -1100,7 +1098,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) char percentStr[8]; snprintf(percentStr, sizeof(percentStr), "%d%%", chargePercent); - const int batteryOffset = screenWidth > 128 ? 34 : 9; + const int batteryOffset = SCREEN_WIDTH > 128 ? 34 : 9; const int percentX = x + xOffset + batteryOffset; display->drawString(percentX, textY, percentStr); if (isBold) @@ -1121,8 +1119,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) char timeStr[10]; snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); - int timeX = screenWidth + 3 - xOffset - display->getStringWidth(timeStr); - if (screenWidth > 128) + int timeX = SCREEN_WIDTH + 3 - xOffset - display->getStringWidth(timeStr); + if (SCREEN_WIDTH > 128) timeX -= 1; display->drawString(timeX, textY, timeStr); if (isBold) @@ -1142,12 +1140,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - const int screenWidth = display->getWidth(); const int screenHeight = display->getHeight(); const int navHeight = FONT_HEIGHT_SMALL; const int scrollBottom = screenHeight - navHeight; const int usableHeight = scrollBottom; - const int textWidth = screenWidth; + const int textWidth = SCREEN_WIDTH; const int cornerRadius = 2; bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); @@ -1163,7 +1160,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 bool useTimestamp = deltaToTimestamp(seconds, ×tampHours, ×tampMinutes, &daysAgo); if (useTimestamp && minutes >= 15 && daysAgo == 0) { - std::string prefix = (daysAgo == 1 && screenWidth >= 200) ? "Yesterday" : "At"; + std::string prefix = (daysAgo == 1 && SCREEN_WIDTH >= 200) ? "Yesterday" : "At"; std::string meridiem = "AM"; if (config.display.use_12h_clock) { if (timestampHours >= 12) meridiem = "PM"; @@ -1233,7 +1230,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 if (strcmp(msg, e.code) == 0) { // Draw the header if (isInverted) { - drawRoundedHighlight(display, x, 0, screenWidth, FONT_HEIGHT_SMALL - 1, cornerRadius); + drawRoundedHighlight(display, x, 0, SCREEN_WIDTH, FONT_HEIGHT_SMALL - 1, cornerRadius); display->setColor(BLACK); display->drawString(x + 3, 0, headerStr); if (isBold) display->drawString(x + 4, 0, headerStr); @@ -1245,7 +1242,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // Center the emote below header + apply bounce int remainingHeight = screenHeight - FONT_HEIGHT_SMALL - navHeight; int emoteY = FONT_HEIGHT_SMALL + (remainingHeight - e.height) / 2 + bounceY - bounceRange; - display->drawXbm((screenWidth - e.width) / 2, emoteY, e.width, e.height, e.bitmap); + display->drawXbm((SCREEN_WIDTH - e.width) / 2, emoteY, e.width, e.height, e.bitmap); return; } } @@ -1331,7 +1328,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 int lineY = static_cast(i * rowHeight + yOffset); if (lineY > -rowHeight && lineY < scrollBottom) { if (i == 0 && isInverted) { - drawRoundedHighlight(display, x, lineY, screenWidth, FONT_HEIGHT_SMALL - 1, cornerRadius); + drawRoundedHighlight(display, x, lineY, SCREEN_WIDTH, FONT_HEIGHT_SMALL - 1, cornerRadius); display->setColor(BLACK); display->drawString(x + 3, lineY, lines[i].c_str()); if (isBold) display->drawString(x + 4, lineY, lines[i].c_str()); @@ -1741,8 +1738,7 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ titleStr = titleBuf; } - const int screenWidth = display->getWidth(); - const int centerX = x + screenWidth / 2; + const int centerX = x + SCREEN_WIDTH / 2; const int highlightHeight = FONT_HEIGHT_SMALL - 1; const int headerOffsetY = 2; const int titleY = y + headerOffsetY + (highlightHeight - FONT_HEIGHT_SMALL) / 2; @@ -1972,7 +1968,6 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t const int rowYOffset = FONT_HEIGHT_SMALL - 3; int columnWidth = display->getWidth() / 2; - int screenWidth = display->getWidth(); display->clear(); @@ -1982,7 +1977,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t // === Manually draw the centered title within the header === const int highlightHeight = COMMON_HEADER_HEIGHT; const int textY = y + 2 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - const int centerX = x + screenWidth / 2; + const int centerX = x + SCREEN_WIDTH / 2; display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_CENTER); @@ -2053,12 +2048,11 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t // **************************** void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { - int screenWidth = display->getWidth(); - bool isLeftCol = (x < screenWidth / 2); + bool isLeftCol = (x < SCREEN_WIDTH / 2); // Adjust offset based on column and screen width int timeOffset = - (screenWidth > 128) + (SCREEN_WIDTH > 128) ? (isLeftCol ? 41 : 45) : (isLeftCol ? 24 : 30); // offset large screen (?Left:Right column), offset small screen (?Left:Right column) @@ -2090,16 +2084,15 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int // **************************** void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { - int screenWidth = display->getWidth(); - bool isLeftCol = (x < screenWidth / 2); + bool isLeftCol = (x < SCREEN_WIDTH / 2); int nameMaxWidth = columnWidth - 25; int barsOffset = - (screenWidth > 128) + (SCREEN_WIDTH > 128) ? (isLeftCol ? 26 : 30) : (isLeftCol ? 17 : 19); // offset large screen (?Left:Right column), offset small screen (?Left:Right column) int hopOffset = - (screenWidth > 128) + (SCREEN_WIDTH > 128) ? (isLeftCol ? 32 : 38) : (isLeftCol ? 18 : 20); // offset large screen (?Left:Right column), offset small screen (?Left:Right column) @@ -2139,9 +2132,8 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int // ************************** void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { - int screenWidth = display->getWidth(); - bool isLeftCol = (x < screenWidth / 2); - int nameMaxWidth = columnWidth - (screenWidth > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); String nodeName = getSafeNodeName(node); char distStr[10] = ""; @@ -2188,7 +2180,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->drawStringMaxWidth(x, y, nameMaxWidth, nodeName); if (strlen(distStr) > 0) { - int offset = (screenWidth > 128) ? (isLeftCol ? 55 : 63) : (isLeftCol ? 32 : 37); + int offset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 55 : 63) : (isLeftCol ? 32 : 37); display->drawString(x + columnWidth - offset, y, distStr); } } @@ -2213,11 +2205,10 @@ static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, // Helper function: Draw a single node entry for Node List (Modified for Compass Screen) void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { - int screenWidth = display->getWidth(); - bool isLeftCol = (x < screenWidth / 2); + bool isLeftCol = (x < SCREEN_WIDTH / 2); // Adjust max text width depending on column and screen width - int nameMaxWidth = columnWidth - (screenWidth > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); String nodeName = getSafeNodeName(node); @@ -2231,9 +2222,8 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 if (!nodeDB->hasValidPosition(node)) return; - int screenWidth = display->getWidth(); - bool isLeftCol = (x < screenWidth / 2); - int arrowXOffset = (screenWidth > 128) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int arrowXOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); int centerX = x + columnWidth - arrowXOffset; int centerY = y + FONT_HEIGHT_SMALL / 2; @@ -2415,8 +2405,7 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int // === Draw title (aligned with header baseline) === const int highlightHeight = FONT_HEIGHT_SMALL - 1; const int textY = y + 2 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - const int screenWidth = display->getWidth(); - const char *titleStr = (screenWidth > 128) ? "LoRa Info" : "LoRa"; + const char *titleStr = (SCREEN_WIDTH > 128) ? "LoRa Info" : "LoRa"; const int centerX = x + SCREEN_WIDTH / 2; if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { @@ -2436,7 +2425,7 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int // Display Region and Radio Preset char regionradiopreset[25]; const char *region = myRegion ? myRegion->name : NULL; - const char *preset = (screenWidth > 128) ? "Preset" : "Prst"; + const char *preset = (SCREEN_WIDTH > 128) ? "Preset" : "Prst"; snprintf(regionradiopreset, sizeof(regionradiopreset), "%s: %s/%s", preset, region, mode); display->drawString(x, compactFirstLine, regionradiopreset); @@ -2480,7 +2469,6 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat // === Draw title === const int highlightHeight = FONT_HEIGHT_SMALL - 1; const int textY = y + 2 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - const int screenWidth = display->getWidth(); const char *titleStr = "GPS"; const int centerX = x + SCREEN_WIDTH / 2; @@ -2502,7 +2490,7 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat config.display.heading_bold = false; if (config.position.fixed_position) { display->drawString(x, compactFirstLine, "Sat:"); - if (screenWidth > 128) { + if (SCREEN_WIDTH > 128) { drawGPS(display, x + 32, compactFirstLine + 3, gpsStatus); } else { drawGPS(display, x + 23, compactFirstLine + 3, gpsStatus); @@ -2511,14 +2499,14 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat String displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; display->drawString(x, compactFirstLine, "Sat:"); - if (screenWidth > 128) { + if (SCREEN_WIDTH > 128) { display->drawString(x + 32, compactFirstLine, displayLine); } else { display->drawString(x + 23, compactFirstLine, displayLine); } } else { display->drawString(x, compactFirstLine, "Sat:"); - if (screenWidth > 128) { + if (SCREEN_WIDTH > 128) { drawGPS(display, x + 32, compactFirstLine + 3, gpsStatus); } else { drawGPS(display, x + 23, compactFirstLine + 3, gpsStatus); @@ -2611,8 +2599,7 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in // === Draw title === const int highlightHeight = FONT_HEIGHT_SMALL - 1; const int textY = y + 2 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - const int screenWidth = display->getWidth(); - const char *titleStr = (screenWidth > 128) ? "Memory" : "Mem"; + const char *titleStr = (SCREEN_WIDTH > 128) ? "Memory" : "Mem"; const int centerX = x + SCREEN_WIDTH / 2; if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { @@ -2631,7 +2618,7 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in const int rowYOffset = FONT_HEIGHT_SMALL - 3; const int barHeight = 6; const int labelX = x; - const int barsOffset = (screenWidth > 128) ? 24 : 0; + const int barsOffset = (SCREEN_WIDTH > 128) ? 24 : 0; const int barX = x + 40 + barsOffset; int rowY = contentY; @@ -2650,7 +2637,7 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in int percent = (used * 100) / total; char combinedStr[24]; - if (screenWidth > 128) { + if (SCREEN_WIDTH > 128) { snprintf(combinedStr, sizeof(combinedStr), "%3d%% %lu/%luKB", percent, used / 1024, total / 1024); } else { snprintf(combinedStr, sizeof(combinedStr), "%3d%%", percent); From d5d20fe33f915b4dc70c50b24796053541cf8bf6 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 6 Apr 2025 15:57:19 -0400 Subject: [PATCH 057/265] Smart portrait logic for battery --- src/graphics/Screen.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 8c3e3a85c..dcd08c770 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1022,7 +1022,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) // === Battery dynamically scaled === const int nubSize = 2; - const int batteryLong = SCREEN_WIDTH > 200 ? 29 : 25; // Was 28/24 + const int batteryLong = SCREEN_WIDTH > 200 ? 29 : 25; const int batteryShort = highlightHeight - nubSize - 2; int batteryX = x + xOffset; @@ -1037,7 +1037,10 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) lastBlink = now; } - if (SCREEN_WIDTH > 128) { + // βœ… Hybrid condition: wide screen AND landscape layout + bool useHorizontalBattery = (SCREEN_WIDTH > 128 && SCREEN_WIDTH > SCREEN_HEIGHT); + + if (useHorizontalBattery) { // === Horizontal battery === batteryY = y + (highlightHeight - batteryShort) / 2; @@ -1098,7 +1101,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) char percentStr[8]; snprintf(percentStr, sizeof(percentStr), "%d%%", chargePercent); - const int batteryOffset = SCREEN_WIDTH > 128 ? 34 : 9; + const int batteryOffset = useHorizontalBattery ? 34 : 9; const int percentX = x + xOffset + batteryOffset; display->drawString(percentX, textY, percentStr); if (isBold) From cdbf0bec2dd8ee365b14e9d3696af6755689b9dc Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 6 Apr 2025 16:41:06 -0400 Subject: [PATCH 058/265] Cleanup --- src/graphics/Screen.cpp | 147 ++++++++++++++++++++++------------------ 1 file changed, 80 insertions(+), 67 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index dcd08c770..743d4ec58 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1048,16 +1048,12 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) display->drawRect(batteryX, batteryY, batteryLong, batteryShort); // Nub - display->fillRect( - batteryX + batteryLong, - batteryY + (batteryShort / 2) - 3, - nubSize, 6 - ); + display->fillRect(batteryX + batteryLong, batteryY + (batteryShort / 2) - 3, nubSize, 6); if (isCharging && isBoltVisible) { // Lightning bolt const int boltX = batteryX + batteryLong / 2 - 4; - const int boltY = batteryY + 2; // Padding top + const int boltY = batteryY + 2; // Padding top // Top fat bar (same) display->fillRect(boltX, boltY, 6, 2); @@ -1143,9 +1139,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - const int screenHeight = display->getHeight(); const int navHeight = FONT_HEIGHT_SMALL; - const int scrollBottom = screenHeight - navHeight; + const int scrollBottom = SCREEN_HEIGHT - navHeight; const int usableHeight = scrollBottom; const int textWidth = SCREEN_WIDTH; const int cornerRadius = 2; @@ -1166,22 +1161,28 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 std::string prefix = (daysAgo == 1 && SCREEN_WIDTH >= 200) ? "Yesterday" : "At"; std::string meridiem = "AM"; if (config.display.use_12h_clock) { - if (timestampHours >= 12) meridiem = "PM"; - if (timestampHours > 12) timestampHours -= 12; - if (timestampHours == 0) timestampHours = 12; - snprintf(headerStr, sizeof(headerStr), "%s %d:%02d%s from %s", prefix.c_str(), timestampHours, timestampMinutes, meridiem.c_str(), sender); + if (timestampHours >= 12) + meridiem = "PM"; + if (timestampHours > 12) + timestampHours -= 12; + if (timestampHours == 0) + timestampHours = 12; + snprintf(headerStr, sizeof(headerStr), "%s %d:%02d%s from %s", prefix.c_str(), timestampHours, timestampMinutes, + meridiem.c_str(), sender); } else { - snprintf(headerStr, sizeof(headerStr), "%s %d:%02d from %s", prefix.c_str(), timestampHours, timestampMinutes, sender); + snprintf(headerStr, sizeof(headerStr), "%s %d:%02d from %s", prefix.c_str(), timestampHours, timestampMinutes, + sender); } } else { - snprintf(headerStr, sizeof(headerStr), "%s ago from %s", screen->drawTimeDelta(days, hours, minutes, seconds).c_str(), sender); + snprintf(headerStr, sizeof(headerStr), "%s ago from %s", screen->drawTimeDelta(days, hours, minutes, seconds).c_str(), + sender); } #ifndef EXCLUDE_EMOJI // === Bounce animation setup === static uint32_t lastBounceTime = 0; static int bounceY = 0; - const int bounceRange = 2; // Max pixels to bounce up/down + const int bounceRange = 2; // Max pixels to bounce up/down const int bounceInterval = 60; // How quickly to change bounce direction (ms) uint32_t now = millis(); @@ -1236,14 +1237,15 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 drawRoundedHighlight(display, x, 0, SCREEN_WIDTH, FONT_HEIGHT_SMALL - 1, cornerRadius); display->setColor(BLACK); display->drawString(x + 3, 0, headerStr); - if (isBold) display->drawString(x + 4, 0, headerStr); + if (isBold) + display->drawString(x + 4, 0, headerStr); display->setColor(WHITE); } else { display->drawString(x, 0, headerStr); } // Center the emote below header + apply bounce - int remainingHeight = screenHeight - FONT_HEIGHT_SMALL - navHeight; + int remainingHeight = SCREEN_HEIGHT - FONT_HEIGHT_SMALL - navHeight; int emoteY = FONT_HEIGHT_SMALL + (remainingHeight - e.height) / 2 + bounceY - bounceRange; display->drawXbm((SCREEN_WIDTH - e.width) / 2, emoteY, e.width, e.height, e.bitmap); return; @@ -1256,14 +1258,16 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 snprintf(messageBuf, sizeof(messageBuf), "%s", msg); std::vector lines; - lines.push_back(std::string(headerStr)); // Header line is always first + lines.push_back(std::string(headerStr)); // Header line is always first std::string line, word; for (int i = 0; messageBuf[i]; ++i) { char ch = messageBuf[i]; if (ch == '\n') { - if (!word.empty()) line += word; - if (!line.empty()) lines.push_back(line); + if (!word.empty()) + line += word; + if (!line.empty()) + lines.push_back(line); line.clear(); word.clear(); } else if (ch == ' ') { @@ -1273,14 +1277,17 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 word += ch; std::string test = line + word; if (display->getStringWidth(test.c_str()) > textWidth + 4) { - if (!line.empty()) lines.push_back(line); + if (!line.empty()) + lines.push_back(line); line = word; word.clear(); } } } - if (!word.empty()) line += word; - if (!line.empty()) lines.push_back(line); + if (!word.empty()) + line += word; + if (!line.empty()) + lines.push_back(line); // === Scrolling logic === const float rowHeight = FONT_HEIGHT_SMALL - 1; @@ -1290,7 +1297,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 static float scrollY = 0.0f; static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0; static bool waitingToReset = false, scrollStarted = false; - + // === Smooth scrolling adjustment === // You can tweak this divisor to change how smooth it scrolls. // Lower = smoother, but can feel slow. @@ -1300,8 +1307,10 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 const float scrollSpeed = 2.0f; // pixels per second // Delay scrolling start by 2 seconds - if (scrollStartDelay == 0) scrollStartDelay = now; - if (!scrollStarted && now - scrollStartDelay > 2000) scrollStarted = true; + if (scrollStartDelay == 0) + scrollStartDelay = now; + if (!scrollStarted && now - scrollStartDelay > 2000) + scrollStarted = true; if (totalHeight > usableHeight) { if (scrollStarted) { @@ -1334,7 +1343,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 drawRoundedHighlight(display, x, lineY, SCREEN_WIDTH, FONT_HEIGHT_SMALL - 1, cornerRadius); display->setColor(BLACK); display->drawString(x + 3, lineY, lines[i].c_str()); - if (isBold) display->drawString(x + 4, lineY, lines[i].c_str()); + if (isBold) + display->drawString(x + 4, lineY, lines[i].c_str()); display->setColor(WHITE); } else { display->drawString(x, lineY, lines[i].c_str()); @@ -1717,7 +1727,8 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ } meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(nodeIndex); - if (!node || !node->is_favorite || node->num == nodeDB->getNodeNum()) return; + if (!node || !node->is_favorite || node->num == nodeDB->getNodeNum()) + return; // === Draw Title (centered safe short name or ID) === static char titleBuf[20]; @@ -1786,7 +1797,8 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ const int16_t usableHeight = bottomY - topY - 5; int16_t compassRadius = usableHeight / 2; - if (compassRadius < 8) compassRadius = 8; + if (compassRadius < 8) + compassRadius = 8; const int16_t compassDiam = compassRadius * 2; const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; @@ -1794,9 +1806,8 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ bool hasNodeHeading = false; if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { const meshtastic_PositionLite &op = ourNode->position; - float myHeading = screen->hasHeading() - ? radians(screen->getHeading()) - : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + float myHeading = screen->hasHeading() ? radians(screen->getHeading()) + : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); screen->drawCompassNorth(display, compassX, compassY, myHeading); @@ -1804,11 +1815,11 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ hasNodeHeading = true; const meshtastic_PositionLite &p = node->position; - float d = GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), - DegD(op.latitude_i), DegD(op.longitude_i)); + float d = + GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - float bearingToOther = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), - DegD(p.latitude_i), DegD(p.longitude_i)); + float bearingToOther = + GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); if (!config.display.compass_north_top) bearingToOther -= myHeading; @@ -1822,7 +1833,8 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ 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); + snprintf(distStr, sizeof(distStr), "%.1fmi %.0fΒ°", d * METERS_TO_FEET / MILES_TO_FEET, + bearingToOtherDegrees); } else { if (d < 2000) snprintf(distStr, sizeof(distStr), "%.0fm %.0fΒ°", d, bearingToOtherDegrees); @@ -1961,11 +1973,12 @@ void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_ } typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); -typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int columnWidth, float heading, double lat, double lon); +typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int columnWidth, float heading, + double lat, double lon); -void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, - const char *title, EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, - float heading = 0, double lat = 0, double lon = 0) +void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, + EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0, + double lon = 0) { const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; const int rowYOffset = FONT_HEIGHT_SMALL - 3; @@ -2124,7 +2137,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int for (int b = 0; b < 4; b++) { if (b < bars) { - int height = (b * 2); + int height = (b * 2); display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height); } } @@ -2265,7 +2278,6 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->fillTriangle(tipX, tipY, notchX, notchY, rightX, rightY); } - // Public screen entry for compass static void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { @@ -2281,7 +2293,7 @@ static void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState * lon = geoCoord.getLongitude() * 1e-7; if (screen->hasHeading()) { - heading = screen->getHeading(); // degrees + heading = screen->getHeading(); // degrees validHeading = true; } else { heading = screen->estimatedHeading(lat, lon); @@ -2289,7 +2301,8 @@ static void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState * } #endif - if (!validHeading) return; + if (!validHeading) + return; drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon); } @@ -2725,15 +2738,15 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in uint32_t sdUsed = 0, sdTotal = 0; bool hasSD = false; -/* -#ifdef HAS_SDCARD - hasSD = SD.cardType() != CARD_NONE; - if (hasSD) { - sdUsed = SD.usedBytes(); - sdTotal = SD.totalBytes(); - } -#endif -*/ + /* + #ifdef HAS_SDCARD + hasSD = SD.cardType() != CARD_NONE; + if (hasSD) { + sdUsed = SD.usedBytes(); + sdTotal = SD.totalBytes(); + } + #endif + */ // === Draw memory rows drawUsageRow("Heap:", heapUsed, heapTotal, true); drawUsageRow("PSRAM:", psramUsed, psramTotal); @@ -3385,22 +3398,22 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = drawLoRaFocused; normalFrames[numframes++] = drawMemoryScreen; -// then all the nodes -// We only show a few nodes in our scrolling list - because meshes with many nodes would have too many screens -//size_t numToShow = min(numMeshNodes, 4U); -//for (size_t i = 0; i < numToShow; i++) -//normalFrames[numframes++] = drawNodeInfo; + // then all the nodes + // We only show a few nodes in our scrolling list - because meshes with many nodes would have too many screens + // size_t numToShow = min(numMeshNodes, 4U); + // for (size_t i = 0; i < numToShow; i++) + // normalFrames[numframes++] = drawNodeInfo; -// then the debug info + // then the debug info -// Since frames are basic function pointers, we have to use a helper to -// call a method on debugInfo object. -//fsi.positions.log = numframes; -//normalFrames[numframes++] = &Screen::drawDebugInfoTrampoline; + // Since frames are basic function pointers, we have to use a helper to + // call a method on debugInfo object. + // fsi.positions.log = numframes; + // normalFrames[numframes++] = &Screen::drawDebugInfoTrampoline; -// call a method on debugInfoScreen object (for more details) -//fsi.positions.settings = numframes; -//normalFrames[numframes++] = &Screen::drawDebugInfoSettingsTrampoline; + // call a method on debugInfoScreen object (for more details) + // fsi.positions.settings = numframes; + // normalFrames[numframes++] = &Screen::drawDebugInfoSettingsTrampoline; fsi.positions.wifi = numframes; #if HAS_WIFI && !defined(ARCH_PORTDUINO) From 7c4ac8905989bad7cb32d9bc97b666e63a4bdcf6 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 7 Apr 2025 22:33:16 -0400 Subject: [PATCH 059/265] Text message notification --- src/graphics/Screen.cpp | 83 ++++++++++++++++++++++++++++++++++------- src/graphics/Screen.h | 2 +- src/graphics/images.h | 13 +++++++ 3 files changed, 84 insertions(+), 14 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 743d4ec58..aa3465fee 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -159,7 +159,7 @@ static bool haveGlyphs(const char *str) LOG_DEBUG("haveGlyphs=%d", have); return have; } - +bool hasUnreadMessage = false; /** * Draw the icon with extra info printed around the corners */ @@ -995,11 +995,16 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, bool isBoltVisible = true; uint32_t lastBlink = 0; const uint32_t blinkInterval = 500; +static uint32_t lastMailBlink = 0; +static bool isMailIconVisible = true; +constexpr uint32_t mailBlinkInterval = 500; + // *********************** // * Common Header * // *********************** void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) { + LOG_INFO("drawCommonHeader: hasUnreadMessage = %s", hasUnreadMessage ? "true" : "false"); constexpr int HEADER_OFFSET_Y = 2; y += HEADER_OFFSET_Y; @@ -1094,15 +1099,20 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) } // === Battery % Text === - char percentStr[8]; - snprintf(percentStr, sizeof(percentStr), "%d%%", chargePercent); + char chargeStr[4]; + snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent); + int chargeNumWidth = display->getStringWidth(chargeStr); const int batteryOffset = useHorizontalBattery ? 34 : 9; const int percentX = x + xOffset + batteryOffset; - display->drawString(percentX, textY, percentStr); - if (isBold) - display->drawString(percentX + 1, textY, percentStr); + display->drawString(percentX, textY, chargeStr); + display->drawString(percentX + chargeNumWidth - 1, textY, "%"); + + if (isBold) { + display->drawString(percentX + 1, textY, chargeStr); + display->drawString(percentX + chargeNumWidth, textY, "%"); + } // === Time string (right-aligned) === uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); if (rtc_sec > 0) { @@ -1118,9 +1128,49 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) char timeStr[10]; snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); - int timeX = SCREEN_WIDTH + 3 - xOffset - display->getStringWidth(timeStr); - if (SCREEN_WIDTH > 128) - timeX -= 1; + int timeStrWidth = display->getStringWidth(timeStr); + int timeX = SCREEN_WIDTH - xOffset - timeStrWidth + 4;// time to the right by 4 + + // Mail icon next to time (drawn as 'M' in a tight square) + if (hasUnreadMessage) { + if (now - lastMailBlink > mailBlinkInterval) { + isMailIconVisible = !isMailIconVisible; + lastMailBlink = now; + } + + if (isMailIconVisible) { + const bool isWide = useHorizontalBattery; + + if (isWide) { + // Dimensions for the wide mail icon + const int iconW = 16; + const int iconH = 12; + + const int iconX = timeX - iconW - 3; + const int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; + + // Draw envelope rectangle + display->drawRect(iconX, iconY, iconW, iconH); + + // Define envelope corners and center + const int leftX = iconX + 1; + const int rightX = iconX + iconW - 2; + const int topY = iconY + 1; + const int bottomY = iconY + iconH - 2; + const int centerX = iconX + iconW / 2; + const int peakY = bottomY - 1; // Slightly raised peak for visual centering + + // Draw "M" diagonals + display->drawLine(leftX, topY, centerX, peakY); + display->drawLine(rightX, topY, centerX, peakY); + } else { + // Small icon for non-wide screens + const int iconX = timeX - mail_width + 2;//move mail icon by 2 closer to the time + const int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + display->drawXbm(iconX, iconY, mail_width, mail_height, mail); + } + } + } display->drawString(timeX, textY, timeStr); if (isBold) display->drawString(timeX - 1, textY, timeStr); @@ -1133,6 +1183,9 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) // **************************** void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + // Clear the unread message indicator when viewing the message + hasUnreadMessage = false; + const meshtastic_MeshPacket &mp = devicestate.rx_text_message; const char *msg = reinterpret_cast(mp.decoded.payload.bytes); @@ -1319,13 +1372,13 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 if (scrollY >= scrollStop) { scrollY = scrollStop; waitingToReset = true; - pauseStart = now; + pauseStart = lastTime; } - } else if (now - pauseStart > 3000) { + } else if (lastTime - pauseStart > 3000) { scrollY = 0; waitingToReset = false; scrollStarted = false; - scrollStartDelay = now; + scrollStartDelay = lastTime; } } } else { @@ -3446,6 +3499,7 @@ void Screen::setFrames(FrameFocus focus) ui->switchToFrame(fsi.positions.fault); break; case FOCUS_TEXTMESSAGE: + hasUnreadMessage = false; // βœ… Clear when message is *viewed* ui->switchToFrame(fsi.positions.textMessage); break; case FOCUS_MODULE: @@ -4004,6 +4058,8 @@ int Screen::handleStatusUpdate(const meshtastic::Status *arg) return 0; } + +//Handles when message is received would jump to text message frame. int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) { if (showingNormalScreen) { @@ -4013,7 +4069,8 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) // Incoming message else - setFrames(FOCUS_TEXTMESSAGE); // Focus on the new message + //setFrames(FOCUS_TEXTMESSAGE); // Focus on the new message + hasUnreadMessage = true; //Tells the UI that there's a new message and tiggers header to draw Mail Icon } return 0; diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index ce416156f..6fd84e8fd 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -90,7 +90,7 @@ class Screen /// Convert an integer GPS coords to a floating point #define DegD(i) (i * 1e-7) - +extern bool hasUnreadMessage; namespace { /// A basic 2D point class for drawing diff --git a/src/graphics/images.h b/src/graphics/images.h index b757dcf30..54e567d65 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -212,6 +212,19 @@ static unsigned char poo[] PROGMEM = { 0x4C, 0xD8, 0x63, 0x06, 0xF8, 0xFC, 0x36, 0x06, 0xFE, 0x0F, 0x9C, 0x1F, 0x07, 0x03, 0xC0, 0x30, 0x03, 0x00, 0x78, 0x20, 0x01, 0x00, 0x1F, 0x20, 0x03, 0xE0, 0x03, 0x20, 0x07, 0x7E, 0x04, 0x30, 0xFE, 0x0F, 0xFC, 0x1F, 0xF0, 0x00, 0xF0, 0x0F, }; + +#define mail_width 10 +#define mail_height 7 +static const unsigned char mail[] PROGMEM = { + 0b11111111, 0b00, // Top line + 0b10000001, 0b00, // Edges + 0b11000011, 0b00, // Diagonals start + 0b10100101, 0b00, // Inner M part + 0b10011001, 0b00, // Inner M part + 0b10000001, 0b00, // Edges + 0b11111111, 0b00 // Bottom line +}; + #endif #include "img/icon.xbm" From 2a4582da206def43c63ea98404b0adf7cc5b80a4 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 8 Apr 2025 01:05:52 -0400 Subject: [PATCH 060/265] Fix for Mail notification not drawing screen. --- src/graphics/Screen.cpp | 62 ++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index aa3465fee..d46c1f7e7 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1158,14 +1158,14 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) const int topY = iconY + 1; const int bottomY = iconY + iconH - 2; const int centerX = iconX + iconW / 2; - const int peakY = bottomY - 1; // Slightly raised peak for visual centering + const int peakY = bottomY - 1; // Draw "M" diagonals display->drawLine(leftX, topY, centerX, peakY); display->drawLine(rightX, topY, centerX, peakY); } else { // Small icon for non-wide screens - const int iconX = timeX - mail_width + 2;//move mail icon by 2 closer to the time + const int iconX = timeX - mail_width; const int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; display->drawXbm(iconX, iconY, mail_width, mail_height, mail); } @@ -3412,11 +3412,8 @@ void Screen::setFrames(FrameFocus focus) // Check if the module being drawn has requested focus // We will honor this request later, if setFrames was triggered by a UIFrameEvent MeshModule *m = *i; - if (m->isRequestingFocus()) { + if (m->isRequestingFocus()) fsi.positions.focusedModule = numframes; - } - - // Identify the position of specific modules, if we need to know this later if (m == waypointModule) fsi.positions.waypoint = numframes; @@ -3436,8 +3433,10 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = screen->digitalWatchFace ? &Screen::drawDigitalClockFrame : &Screen::drawAnalogClockFrame; #endif - // If we have a text message - show it next, unless it's a phone message and we aren't using any special modules - if (devicestate.has_rx_text_message && shouldDrawMessage(&devicestate.rx_text_message)) { + // βœ… Declare this early so it’s available in FOCUS_PRESERVE block + bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message); + + if (willInsertTextMessage) { fsi.positions.textMessage = numframes; normalFrames[numframes++] = drawTextMessageFrame; } @@ -3484,8 +3483,7 @@ void Screen::setFrames(FrameFocus focus) // Add function overlay here. This can show when notifications muted, modifier key is active etc static OverlayCallback functionOverlay[] = {drawFunctionOverlay}; - static const int functionOverlayCount = sizeof(functionOverlay) / sizeof(functionOverlay[0]); - ui->setOverlays(functionOverlay, functionOverlayCount); + ui->setOverlays(functionOverlay, sizeof(functionOverlay) / sizeof(functionOverlay[0])); prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list // just changed) @@ -3508,14 +3506,19 @@ void Screen::setFrames(FrameFocus focus) ui->switchToFrame(fsi.positions.focusedModule); break; - case FOCUS_PRESERVE: - // If we can identify which type of frame "originalPosition" was, can move directly to it in the new frameset + case FOCUS_PRESERVE: { const FramesetInfo &oldFsi = this->framesetInfo; - if (originalPosition == oldFsi.positions.log) + + // βœ… Fix: Account for new message insertion shifting frame positions + if (willInsertTextMessage && fsi.positions.textMessage <= originalPosition) { + originalPosition++; + } + + if (originalPosition == oldFsi.positions.log && fsi.positions.log < fsi.frameCount) ui->switchToFrame(fsi.positions.log); - else if (originalPosition == oldFsi.positions.settings) + else if (originalPosition == oldFsi.positions.settings && fsi.positions.settings < fsi.frameCount) ui->switchToFrame(fsi.positions.settings); - else if (originalPosition == oldFsi.positions.wifi) + else if (originalPosition == oldFsi.positions.wifi && fsi.positions.wifi < fsi.frameCount) ui->switchToFrame(fsi.positions.wifi); // If frame count has decreased @@ -3527,15 +3530,13 @@ void Screen::setFrames(FrameFocus focus) // Unless that would put us "out of bounds" (< 0) else ui->switchToFrame(0); - } - - // If we're not sure exactly which frame we were on, at least return to the same frame number - // (node frames; module frames) - else + } else if (originalPosition < fsi.frameCount) ui->switchToFrame(originalPosition); - + else + ui->switchToFrame(fsi.frameCount - 1); break; } + } // Store the info about this frameset, for future setFrames calls this->framesetInfo = fsi; @@ -4063,15 +4064,18 @@ int Screen::handleStatusUpdate(const meshtastic::Status *arg) int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) { if (showingNormalScreen) { - // Outgoing message - if (packet->from == 0) - setFrames(FOCUS_PRESERVE); // Return to same frame (quietly hiding the rx text message frame) - - // Incoming message - else - //setFrames(FOCUS_TEXTMESSAGE); // Focus on the new message - hasUnreadMessage = true; //Tells the UI that there's a new message and tiggers header to draw Mail Icon + if (packet->from == 0) { + // Outgoing message + setFrames(FOCUS_PRESERVE); // Stay on same frame, silently add/remove frames + } else { + // Incoming message + //setFrames(FOCUS_TEXTMESSAGE); // Focus on the new message + devicestate.has_rx_text_message = true; // Needed to include the message frame + hasUnreadMessage = true; // Enables mail icon in the header + setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view + forceDisplay(); // Forces screen redraw (this works in your codebase) } +} return 0; } From 38b118fb8bd56e93738e081a3f2babd403a39a25 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 8 Apr 2025 01:15:08 -0400 Subject: [PATCH 061/265] Update Screen.cpp --- src/graphics/Screen.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index d46c1f7e7..696312818 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -3510,7 +3510,7 @@ void Screen::setFrames(FrameFocus focus) const FramesetInfo &oldFsi = this->framesetInfo; // βœ… Fix: Account for new message insertion shifting frame positions - if (willInsertTextMessage && fsi.positions.textMessage <= originalPosition) { + if (willInsertTextMessage && oldFsi.positions.textMessage == 0 && fsi.positions.textMessage <= originalPosition) { originalPosition++; } From 383ae7a82f7cc9452fe1dddcbf6eacbbb5259b3a Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 8 Apr 2025 01:41:37 -0400 Subject: [PATCH 062/265] New lightning bolts for battery --- src/graphics/Screen.cpp | 110 ++++++++++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 39 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 696312818..985b69a74 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1025,9 +1025,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) // === Text baseline === const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - // === Battery dynamically scaled === + // === Battery Vertical and Horizontal === const int nubSize = 2; - const int batteryLong = SCREEN_WIDTH > 200 ? 29 : 25; const int batteryShort = highlightHeight - nubSize - 2; int batteryX = x + xOffset; @@ -1047,54 +1046,87 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) if (useHorizontalBattery) { // === Horizontal battery === - batteryY = y + (highlightHeight - batteryShort) / 2; + batteryX = 2; + batteryY = 4; + // Basic battery design and all related pieces + const unsigned char batteryBitmap[] PROGMEM = { + 0b11111110, 0b00000000, 0b11110000, 0b00000111, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, + 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, + 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, + 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, + 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, + 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, + 0b00000000, 0b00001000, 0b11111110, 0b00000000, 0b11110000, 0b00000111}; - // Battery outline - display->drawRect(batteryX, batteryY, batteryLong, batteryShort); + // This is the left and right bars for the fill in + const unsigned char batteryBitmap_sidegaps[] PROGMEM = { + 0b11111111, 0b00001111, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11111111, 0b00001111}; - // Nub - display->fillRect(batteryX + batteryLong, batteryY + (batteryShort / 2) - 3, nubSize, 6); + // Lightning Bolt + const unsigned char lightning_bolt[] PROGMEM = { + 0b11110000, 0b00000000, 0b11110000, 0b00000000, 0b01110000, 0b00000000, 0b00111000, 0b00000000, 0b00111100, + 0b00000000, 0b11111100, 0b00000000, 0b01111110, 0b00000000, 0b00111000, 0b00000000, 0b00110000, 0b00000000, + 0b00010000, 0b00000000, 0b00010000, 0b00000000, 0b00001000, 0b00000000, 0b00001000, 0b00000000}; + + display->drawXbm(batteryX, batteryY, 29, 15, batteryBitmap); if (isCharging && isBoltVisible) { - // Lightning bolt - const int boltX = batteryX + batteryLong / 2 - 4; - const int boltY = batteryY + 2; // Padding top - - // Top fat bar (same) - display->fillRect(boltX, boltY, 6, 2); - - // Middle slanted lines - display->drawLine(boltX + 0, boltY + 2, boltX + 3, boltY + 6); - display->drawLine(boltX + 1, boltY + 2, boltX + 4, boltY + 6); - display->drawLine(boltX + 2, boltY + 2, boltX + 5, boltY + 6); - - // Tapered tail - display->drawLine(boltX + 3, boltY + 6, boltX + 0, batteryY + batteryShort - 3); - display->drawLine(boltX + 4, boltY + 6, boltX + 1, batteryY + batteryShort - 3); + display->drawXbm(batteryX + 9, batteryY + 1, 9, 13, lightning_bolt); + } else if (isCharging && !isBoltVisible) { + display->drawXbm(batteryX + 8, batteryY, 12, 15, batteryBitmap_sidegaps); } else if (!isCharging) { - int fillWidth = (batteryLong - 2) * chargePercent / 100; - int fillX = batteryX + 1; - display->fillRect(fillX, batteryY + 1, fillWidth, batteryShort - 2); + display->drawXbm(batteryX + 8, batteryY, 12, 15, batteryBitmap_sidegaps); + int fillWidth = 24 * chargePercent / 100; + int fillX = batteryX + fillWidth; + display->fillRect(batteryX + 1, batteryY + 1, fillX, 13); } } else { // === Vertical battery === - const int batteryWidth = 8; - const int batteryHeight = batteryShort + 1; - const int totalBatteryHeight = batteryHeight + nubSize; - batteryX += -2; - batteryY = y + (highlightHeight - totalBatteryHeight) / 2 + nubSize; + batteryX = 1; + batteryY = 3; + // Basic battery design and all related pieces + const unsigned char batteryBitmap[] PROGMEM = { + 0b00011100, // ..###.. + 0b00111110, // .#####. + 0b01000001, // #.....# + 0b01000001, // #.....# + 0b00000000, // ....... + 0b00000000, // ....... + 0b00000000, // ....... + 0b01000001, // #.....# + 0b01000001, // #.....# + 0b01000001, // #.....# + 0b00111110 // .#####. + }; + // This is the left and right bars for the fill in + const unsigned char batteryBitmap_sidegaps[] PROGMEM = { + 0b10000010, // #.....# + 0b10000010, // #.....# + 0b10000010, // #.....# + }; + // Lightning Bolt + const unsigned char lightning_bolt[] PROGMEM = { + 0b00000100, // Column 0 + 0b00000110, // Column 1 + 0b00011111, // Column 2 + 0b00001100, // Column 3 + 0b00000100 // Column 4 + }; - display->fillRect(batteryX + 2, batteryY - (nubSize - 1), 4, nubSize - 1); // Nub - display->drawRect(batteryX, batteryY, batteryWidth, batteryHeight); // Body + display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap); if (isCharging && isBoltVisible) { - display->drawLine(batteryX + 4, batteryY + 1, batteryX + 2, batteryY + 4); - display->drawLine(batteryX + 2, batteryY + 4, batteryX + 4, batteryY + 4); - display->drawLine(batteryX + 4, batteryY + 4, batteryX + 3, batteryY + 7); + display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt); + } else if (isCharging && !isBoltVisible) { + display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps); } else if (!isCharging) { - int fillHeight = (batteryHeight - 2) * chargePercent / 100; - int fillY = batteryY + batteryHeight - 1 - fillHeight; - display->fillRect(batteryX + 1, fillY, batteryWidth - 2, fillHeight); + display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps); + int fillHeight = 8 * chargePercent / 100; + int fillY = batteryY - fillHeight; + display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight); } } @@ -1103,7 +1135,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent); int chargeNumWidth = display->getStringWidth(chargeStr); - const int batteryOffset = useHorizontalBattery ? 34 : 9; + const int batteryOffset = useHorizontalBattery ? 28 : 6; const int percentX = x + xOffset + batteryOffset; display->drawString(percentX, textY, chargeStr); From ebea34520d2cd7e1d517f00e5dda9a3de411a579 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 8 Apr 2025 11:19:13 -0400 Subject: [PATCH 063/265] Cleanup --- src/graphics/Screen.cpp | 124 +++++++++++----------------------------- src/graphics/images.h | 45 ++++++++++++--- 2 files changed, 71 insertions(+), 98 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 985b69a74..ac2865eba 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1022,18 +1022,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) display->setColor(BLACK); } - // === Text baseline === - const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - // === Battery Vertical and Horizontal === - const int nubSize = 2; - const int batteryShort = highlightHeight - nubSize - 2; - - int batteryX = x + xOffset; - int batteryY = y + (highlightHeight - batteryShort) / 2 + nubSize; - int chargePercent = powerStatus->getBatteryChargePercent(); - bool isCharging = powerStatus->getIsCharging() == OptionalBool::OptTrue; uint32_t now = millis(); if (isCharging && now - lastBlink > blinkInterval) { @@ -1046,90 +1036,43 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) if (useHorizontalBattery) { // === Horizontal battery === - batteryX = 2; - batteryY = 4; - // Basic battery design and all related pieces - const unsigned char batteryBitmap[] PROGMEM = { - 0b11111110, 0b00000000, 0b11110000, 0b00000111, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, - 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, - 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, - 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, - 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, - 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, - 0b00000000, 0b00001000, 0b11111110, 0b00000000, 0b11110000, 0b00000111}; + int batteryX = 2; + int batteryY = 4; - // This is the left and right bars for the fill in - const unsigned char batteryBitmap_sidegaps[] PROGMEM = { - 0b11111111, 0b00001111, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11111111, 0b00001111}; - - // Lightning Bolt - const unsigned char lightning_bolt[] PROGMEM = { - 0b11110000, 0b00000000, 0b11110000, 0b00000000, 0b01110000, 0b00000000, 0b00111000, 0b00000000, 0b00111100, - 0b00000000, 0b11111100, 0b00000000, 0b01111110, 0b00000000, 0b00111000, 0b00000000, 0b00110000, 0b00000000, - 0b00010000, 0b00000000, 0b00010000, 0b00000000, 0b00001000, 0b00000000, 0b00001000, 0b00000000}; - - display->drawXbm(batteryX, batteryY, 29, 15, batteryBitmap); + display->drawXbm(batteryX, batteryY, 29, 15, batteryBitmap_h); if (isCharging && isBoltVisible) { - display->drawXbm(batteryX + 9, batteryY + 1, 9, 13, lightning_bolt); + display->drawXbm(batteryX + 9, batteryY + 1, 9, 13, lightning_bolt_h); } else if (isCharging && !isBoltVisible) { - display->drawXbm(batteryX + 8, batteryY, 12, 15, batteryBitmap_sidegaps); - } else if (!isCharging) { - display->drawXbm(batteryX + 8, batteryY, 12, 15, batteryBitmap_sidegaps); + display->drawXbm(batteryX + 8, batteryY, 12, 15, batteryBitmap_sidegaps_h); + } else { + display->drawXbm(batteryX + 8, batteryY, 12, 15, batteryBitmap_sidegaps_h); int fillWidth = 24 * chargePercent / 100; int fillX = batteryX + fillWidth; display->fillRect(batteryX + 1, batteryY + 1, fillX, 13); } } else { // === Vertical battery === - batteryX = 1; - batteryY = 3; - // Basic battery design and all related pieces - const unsigned char batteryBitmap[] PROGMEM = { - 0b00011100, // ..###.. - 0b00111110, // .#####. - 0b01000001, // #.....# - 0b01000001, // #.....# - 0b00000000, // ....... - 0b00000000, // ....... - 0b00000000, // ....... - 0b01000001, // #.....# - 0b01000001, // #.....# - 0b01000001, // #.....# - 0b00111110 // .#####. - }; - // This is the left and right bars for the fill in - const unsigned char batteryBitmap_sidegaps[] PROGMEM = { - 0b10000010, // #.....# - 0b10000010, // #.....# - 0b10000010, // #.....# - }; - // Lightning Bolt - const unsigned char lightning_bolt[] PROGMEM = { - 0b00000100, // Column 0 - 0b00000110, // Column 1 - 0b00011111, // Column 2 - 0b00001100, // Column 3 - 0b00000100 // Column 4 - }; + int batteryX = 1; + int batteryY = 3; - display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap); + display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v); if (isCharging && isBoltVisible) { - display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt); + display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v); } else if (isCharging && !isBoltVisible) { - display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps); - } else if (!isCharging) { - display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps); + display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v); + } else { + display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v); int fillHeight = 8 * chargePercent / 100; int fillY = batteryY - fillHeight; display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight); } } + // === Text baseline === + const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + // === Battery % Text === char chargeStr[4]; snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent); @@ -1161,7 +1104,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); int timeStrWidth = display->getStringWidth(timeStr); - int timeX = SCREEN_WIDTH - xOffset - timeStrWidth + 4;// time to the right by 4 + int timeX = SCREEN_WIDTH - xOffset - timeStrWidth + 4; // time to the right by 4 // Mail icon next to time (drawn as 'M' in a tight square) if (hasUnreadMessage) { @@ -1169,21 +1112,21 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) isMailIconVisible = !isMailIconVisible; lastMailBlink = now; } - + if (isMailIconVisible) { const bool isWide = useHorizontalBattery; - + if (isWide) { // Dimensions for the wide mail icon const int iconW = 16; const int iconH = 12; - + const int iconX = timeX - iconW - 3; const int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; - + // Draw envelope rectangle display->drawRect(iconX, iconY, iconW, iconH); - + // Define envelope corners and center const int leftX = iconX + 1; const int rightX = iconX + iconW - 2; @@ -1191,7 +1134,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) const int bottomY = iconY + iconH - 2; const int centerX = iconX + iconW / 2; const int peakY = bottomY - 1; - + // Draw "M" diagonals display->drawLine(leftX, topY, centerX, peakY); display->drawLine(rightX, topY, centerX, peakY); @@ -3444,7 +3387,7 @@ void Screen::setFrames(FrameFocus focus) // Check if the module being drawn has requested focus // We will honor this request later, if setFrames was triggered by a UIFrameEvent MeshModule *m = *i; - if (m->isRequestingFocus()) + if (m->isRequestingFocus()) fsi.positions.focusedModule = numframes; if (m == waypointModule) fsi.positions.waypoint = numframes; @@ -3529,7 +3472,7 @@ void Screen::setFrames(FrameFocus focus) ui->switchToFrame(fsi.positions.fault); break; case FOCUS_TEXTMESSAGE: - hasUnreadMessage = false; // βœ… Clear when message is *viewed* + hasUnreadMessage = false; // βœ… Clear when message is *viewed* ui->switchToFrame(fsi.positions.textMessage); break; case FOCUS_MODULE: @@ -4091,8 +4034,7 @@ int Screen::handleStatusUpdate(const meshtastic::Status *arg) return 0; } - -//Handles when message is received would jump to text message frame. +// Handles when message is received would jump to text message frame. int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) { if (showingNormalScreen) { @@ -4101,13 +4043,13 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) setFrames(FOCUS_PRESERVE); // Stay on same frame, silently add/remove frames } else { // Incoming message - //setFrames(FOCUS_TEXTMESSAGE); // Focus on the new message - devicestate.has_rx_text_message = true; // Needed to include the message frame - hasUnreadMessage = true; // Enables mail icon in the header - setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view - forceDisplay(); // Forces screen redraw (this works in your codebase) + // setFrames(FOCUS_TEXTMESSAGE); // Focus on the new message + devicestate.has_rx_text_message = true; // Needed to include the message frame + hasUnreadMessage = true; // Enables mail icon in the header + setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view + forceDisplay(); // Forces screen redraw (this works in your codebase) + } } -} return 0; } diff --git a/src/graphics/images.h b/src/graphics/images.h index 54e567d65..1a2a5422e 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -37,6 +37,37 @@ const uint8_t imgQuestion[] PROGMEM = {0xbf, 0x41, 0xc0, 0x8b, 0xdb, 0x70, 0xa1, const uint8_t imgSF[] PROGMEM = {0xd2, 0xb7, 0xad, 0xbb, 0x92, 0x01, 0xfd, 0xfd, 0x15, 0x85, 0xf5}; #endif +// === Horizontal battery === +// Basic battery design and all related pieces +const unsigned char batteryBitmap_h[] PROGMEM = { + 0b11111110, 0b00000000, 0b11110000, 0b00000111, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, + 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, + 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, + 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, + 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, + 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b11111110, 0b00000000, 0b11110000, 0b00000111}; + +// This is the left and right bars for the fill in +const unsigned char batteryBitmap_sidegaps_h[] PROGMEM = { + 0b11111111, 0b00001111, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11111111, 0b00001111}; + +// Lightning Bolt +const unsigned char lightning_bolt_h[] PROGMEM = { + 0b11110000, 0b00000000, 0b11110000, 0b00000000, 0b01110000, 0b00000000, 0b00111000, 0b00000000, 0b00111100, + 0b00000000, 0b11111100, 0b00000000, 0b01111110, 0b00000000, 0b00111000, 0b00000000, 0b00110000, 0b00000000, + 0b00010000, 0b00000000, 0b00010000, 0b00000000, 0b00001000, 0b00000000, 0b00001000, 0b00000000}; + +// === Vertical battery === +// Basic battery design and all related pieces +const unsigned char batteryBitmap_v[] PROGMEM = {0b00011100, 0b00111110, 0b01000001, 0b01000001, 0b00000000, 0b00000000, + 0b00000000, 0b01000001, 0b01000001, 0b01000001, 0b00111110}; +// This is the left and right bars for the fill in +const unsigned char batteryBitmap_sidegaps_v[] PROGMEM = {0b10000010, 0b10000010, 0b10000010}; +// Lightning Bolt +const unsigned char lightning_bolt_v[] PROGMEM = {0b00000100, 0b00000110, 0b00011111, 0b00001100, 0b00000100}; + #ifndef EXCLUDE_EMOJI #define thumbs_height 25 #define thumbs_width 25 @@ -216,13 +247,13 @@ static unsigned char poo[] PROGMEM = { #define mail_width 10 #define mail_height 7 static const unsigned char mail[] PROGMEM = { - 0b11111111, 0b00, // Top line - 0b10000001, 0b00, // Edges - 0b11000011, 0b00, // Diagonals start - 0b10100101, 0b00, // Inner M part - 0b10011001, 0b00, // Inner M part - 0b10000001, 0b00, // Edges - 0b11111111, 0b00 // Bottom line + 0b11111111, 0b00, // Top line + 0b10000001, 0b00, // Edges + 0b11000011, 0b00, // Diagonals start + 0b10100101, 0b00, // Inner M part + 0b10011001, 0b00, // Inner M part + 0b10000001, 0b00, // Edges + 0b11111111, 0b00 // Bottom line }; #endif From 09fd3c078263bbb4bd66a53d0a633a1bfde5b6c1 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:07:00 -0400 Subject: [PATCH 064/265] Prototype Cycling screens --- src/graphics/Screen.cpp | 46 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index ac2865eba..7a555aba4 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1884,6 +1884,10 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ display->setColor(WHITE); } +typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); +typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int columnWidth, float heading, + double lat, double lon); + // h! Each node entry holds a reference to its info and how long ago it was heard from struct NodeEntry { meshtastic_NodeInfoLite *node; @@ -2000,9 +2004,6 @@ void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_ display->drawLine(separatorX, yStart, separatorX, yEnd); } -typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); -typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int columnWidth, float heading, - double lat, double lon); void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0, @@ -2229,6 +2230,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 } } + // Public screen function: shows how recently nodes were heard static void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { @@ -2246,6 +2248,39 @@ static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, drawNodeListScreen(display, state, x, y, "Distances", drawNodeDistance); } + +//Cycles Node list screens +static EntryRenderer entryRenderers[] = { + drawEntryLastHeard, + drawEntryHopSignal +}; + +static const int NUM_RENDERERS = sizeof(entryRenderers) / sizeof(entryRenderers[0]); +static unsigned long lastSwitchTime = 0; +static int currentRendererIndex = 0; +static const unsigned long RENDER_INTERVAL_MS = 2000; + +static void drawCyclingNodeScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + unsigned long now = millis(); + + // βœ… Reset to first view on initial entry to screen + if (state->ticksSinceLastStateSwitch == 0) { + currentRendererIndex = 0; + lastSwitchTime = now; + } + + // ⏱️ Cycle content every interval + if (now - lastSwitchTime >= RENDER_INTERVAL_MS) { + lastSwitchTime = now; + currentRendererIndex = (currentRendererIndex + 1) % NUM_RENDERERS; + } + + EntryRenderer currentRenderer = entryRenderers[currentRendererIndex]; + const char *titles[] = {"Last Heard", "Hop|Sig"}; + + drawNodeListScreen(display, state, x, y, titles[currentRendererIndex], currentRenderer); +} // Helper function: Draw a single node entry for Node List (Modified for Compass Screen) void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { @@ -3417,8 +3452,9 @@ void Screen::setFrames(FrameFocus focus) } normalFrames[numframes++] = drawDeviceFocused; - normalFrames[numframes++] = drawLastHeardScreen; - normalFrames[numframes++] = drawHopSignalScreen; + normalFrames[numframes++] = drawCyclingNodeScreen; + //normalFrames[numframes++] = drawLastHeardScreen; + //normalFrames[numframes++] = drawHopSignalScreen; normalFrames[numframes++] = drawDistanceScreen; normalFrames[numframes++] = drawNodeListWithCompasses; normalFrames[numframes++] = drawCompassAndLocationScreen; From f03f547e9ee9c78cbb1e45a815be7a20dc1b3ada Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 9 Apr 2025 01:10:15 -0400 Subject: [PATCH 065/265] Terrible clock format change --- src/graphics/Screen.cpp | 41 ++++++++++++++++++----------------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 7a555aba4..b68a40d17 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1095,13 +1095,16 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) int hour = hms / SEC_PER_HOUR; int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - bool isPM = hour >= 12; - hour = hour % 12; - if (hour == 0) - hour = 12; - char timeStr[10]; - snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); + + snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); + if (config.display.use_12h_clock) { + bool isPM = hour >= 12; + hour = hour % 12; + if (hour == 0) + hour = 12; + snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); + } int timeStrWidth = display->getStringWidth(timeStr); int timeX = SCREEN_WIDTH - xOffset - timeStrWidth + 4; // time to the right by 4 @@ -1187,16 +1190,13 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 if (useTimestamp && minutes >= 15 && daysAgo == 0) { std::string prefix = (daysAgo == 1 && SCREEN_WIDTH >= 200) ? "Yesterday" : "At"; - std::string meridiem = "AM"; if (config.display.use_12h_clock) { - if (timestampHours >= 12) - meridiem = "PM"; - if (timestampHours > 12) - timestampHours -= 12; + bool isPM = timestampHours >= 12; + timestampHours = timestampHours % 12; if (timestampHours == 0) timestampHours = 12; snprintf(headerStr, sizeof(headerStr), "%s %d:%02d%s from %s", prefix.c_str(), timestampHours, timestampMinutes, - meridiem.c_str(), sender); + isPM ? "p" : "a", sender); } else { snprintf(headerStr, sizeof(headerStr), "%s %d:%02d from %s", prefix.c_str(), timestampHours, timestampMinutes, sender); @@ -1887,7 +1887,7 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int columnWidth, float heading, double lat, double lon); - + // h! Each node entry holds a reference to its info and how long ago it was heard from struct NodeEntry { meshtastic_NodeInfoLite *node; @@ -2004,7 +2004,6 @@ void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_ display->drawLine(separatorX, yStart, separatorX, yEnd); } - void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0, double lon = 0) @@ -2230,7 +2229,6 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 } } - // Public screen function: shows how recently nodes were heard static void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { @@ -2248,12 +2246,8 @@ static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, drawNodeListScreen(display, state, x, y, "Distances", drawNodeDistance); } - -//Cycles Node list screens -static EntryRenderer entryRenderers[] = { - drawEntryLastHeard, - drawEntryHopSignal -}; +// Cycles Node list screens +static EntryRenderer entryRenderers[] = {drawEntryLastHeard, drawEntryHopSignal}; static const int NUM_RENDERERS = sizeof(entryRenderers) / sizeof(entryRenderers[0]); static unsigned long lastSwitchTime = 0; @@ -2501,6 +2495,7 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int // === First Row: Region / Radio Preset === auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); + // Display Region and Radio Preset char regionradiopreset[25]; const char *region = myRegion ? myRegion->name : NULL; @@ -3453,8 +3448,8 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = drawDeviceFocused; normalFrames[numframes++] = drawCyclingNodeScreen; - //normalFrames[numframes++] = drawLastHeardScreen; - //normalFrames[numframes++] = drawHopSignalScreen; + // normalFrames[numframes++] = drawLastHeardScreen; + // normalFrames[numframes++] = drawHopSignalScreen; normalFrames[numframes++] = drawDistanceScreen; normalFrames[numframes++] = drawNodeListWithCompasses; normalFrames[numframes++] = drawCompassAndLocationScreen; From 9f56e613f24d0fbc0251955e7a87aa776c3153db Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 9 Apr 2025 17:54:47 -0400 Subject: [PATCH 066/265] Refactored Lora Screen --- src/graphics/Screen.cpp | 115 ++++++++++++++++++++++++---------------- 1 file changed, 69 insertions(+), 46 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index b68a40d17..9aae3efec 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1226,37 +1226,35 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 int width, height; }; - const Emote emotes[] = { - { "\U0001F44D", thumbup, thumbs_width, thumbs_height }, - { "\U0001F44E", thumbdown, thumbs_width, thumbs_height }, - { "\U0001F60A", smiley, smiley_width, smiley_height }, - { "\U0001F600", smiley, smiley_width, smiley_height }, - { "\U0001F642", smiley, smiley_width, smiley_height }, - { "\U0001F609", smiley, smiley_width, smiley_height }, - { "\U0001F601", smiley, smiley_width, smiley_height }, - { "❓", question, question_width, question_height }, - { "‼️", bang, bang_width, bang_height }, - { "\U0001F4A9", poo, poo_width, poo_height }, - { "\U0001F923", haha, haha_width, haha_height }, - { "\U0001F44B", wave_icon, wave_icon_width, wave_icon_height }, - { "\U0001F920", cowboy, cowboy_width, cowboy_height }, - { "\U0001F42D", deadmau5, deadmau5_width, deadmau5_height }, - { "β˜€οΈ", sun, sun_width, sun_height }, - { "\xE2\x98\x80\xEF\xB8\x8F", sun, sun_width, sun_height }, - { "β˜”", rain, rain_width, rain_height }, - { "\u2614", rain, rain_width, rain_height }, - { "☁️", cloud, cloud_width, cloud_height }, - { "🌫️", fog, fog_width, fog_height }, - { "\U0001F608", devil, devil_width, devil_height }, - { "β™₯️", heart, heart_width, heart_height }, - { "\U0001F9E1", heart, heart_width, heart_height }, - { "\U00002763", heart, heart_width, heart_height }, - { "\U00002764", heart, heart_width, heart_height }, - { "\U0001F495", heart, heart_width, heart_height }, - { "\U0001F496", heart, heart_width, heart_height }, - { "\U0001F497", heart, heart_width, heart_height }, - { "\U0001F498", heart, heart_width, heart_height } - }; + const Emote emotes[] = {{"\U0001F44D", thumbup, thumbs_width, thumbs_height}, + {"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, + {"\U0001F60A", smiley, smiley_width, smiley_height}, + {"\U0001F600", smiley, smiley_width, smiley_height}, + {"\U0001F642", smiley, smiley_width, smiley_height}, + {"\U0001F609", smiley, smiley_width, smiley_height}, + {"\U0001F601", smiley, smiley_width, smiley_height}, + {"❓", question, question_width, question_height}, + {"‼️", bang, bang_width, bang_height}, + {"\U0001F4A9", poo, poo_width, poo_height}, + {"\U0001F923", haha, haha_width, haha_height}, + {"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height}, + {"\U0001F920", cowboy, cowboy_width, cowboy_height}, + {"\U0001F42D", deadmau5, deadmau5_width, deadmau5_height}, + {"β˜€οΈ", sun, sun_width, sun_height}, + {"\xE2\x98\x80\xEF\xB8\x8F", sun, sun_width, sun_height}, + {"β˜”", rain, rain_width, rain_height}, + {"\u2614", rain, rain_width, rain_height}, + {"☁️", cloud, cloud_width, cloud_height}, + {"🌫️", fog, fog_width, fog_height}, + {"\U0001F608", devil, devil_width, devil_height}, + {"β™₯️", heart, heart_width, heart_height}, + {"\U0001F9E1", heart, heart_width, heart_height}, + {"\U00002763", heart, heart_width, heart_height}, + {"\U00002764", heart, heart_width, heart_height}, + {"\U0001F495", heart, heart_width, heart_height}, + {"\U0001F496", heart, heart_width, heart_height}, + {"\U0001F497", heart, heart_width, heart_height}, + {"\U0001F498", heart, heart_width, heart_height}}; for (const Emote &e : emotes) { if (strcmp(msg, e.code) == 0) { @@ -2499,33 +2497,58 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int // Display Region and Radio Preset char regionradiopreset[25]; const char *region = myRegion ? myRegion->name : NULL; - const char *preset = (SCREEN_WIDTH > 128) ? "Preset" : "Prst"; - snprintf(regionradiopreset, sizeof(regionradiopreset), "%s: %s/%s", preset, region, mode); - display->drawString(x, compactFirstLine, regionradiopreset); + // const char *preset = (SCREEN_WIDTH > 128) ? "Preset" : "Prst"; + // snprintf(regionradiopreset, sizeof(regionradiopreset), "%s: %s/%s", preset, region, mode); + snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); + int textWidth = display->getStringWidth(regionradiopreset); + int nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, compactFirstLine, regionradiopreset); // === Second Row: Channel Utilization === - char chUtil[25]; - snprintf(chUtil, sizeof(chUtil), "ChUtil: %2.0f%%", airTime->channelUtilizationPercent()); - display->drawString(x, compactSecondLine, chUtil); - - // === Third Row: Channel Utilization === // Get our hardware ID uint8_t dmac[6]; getMacAddr(dmac); snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); char shortnameble[35]; - snprintf(shortnameble, sizeof(shortnameble), "%s: %s/%s", "Short/BLE", haveGlyphs(owner.short_name) ? owner.short_name : "", - ourId); - display->drawString(x, compactThirdLine, shortnameble); + // snprintf(shortnameble, sizeof(shortnameble), "%s: %s/%s", "Short/BLE", haveGlyphs(owner.short_name) ? owner.short_name : + // "", ourId); + snprintf(shortnameble, sizeof(shortnameble), "%s_%s", haveGlyphs(owner.short_name) ? owner.short_name : "", ourId); + textWidth = display->getStringWidth(shortnameble); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, compactSecondLine, shortnameble); - // === Fourth Row: Node longName === + // === Third Row: Node longName === meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { - char devicelongname[55]; - snprintf(devicelongname, sizeof(devicelongname), "%s: %s", "Name", ourNode->user.long_name); - display->drawString(x, compactFourthLine, devicelongname); + const char *longName = ourNode->user.long_name; + textWidth = display->getStringWidth(longName); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, compactThirdLine, longName); } + + // === Fourth Row: Channel Utilization === + const char *chUtil = "ChUtil:"; + char chUtilPercentage[10]; + int desperatecenteringattempt = SCREEN_WIDTH / 2; + snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); + textWidth = display->getStringWidth(chUtil); + + int chUtil_x = (SCREEN_WIDTH > 128) ? textWidth + 10 : textWidth + 5; + int chUtil_y = compactFourthLine + 3; + + int width = (SCREEN_WIDTH > 128) ? 100 : 50; + int height = (SCREEN_WIDTH > 128) ? 12 : 7; + int percent = airTime->channelUtilizationPercent(); + int fillWidth = (width * percent) / 100; + + display->drawString(x, compactFourthLine, chUtil); + display->drawRect(chUtil_x, chUtil_y, width, height); + if (fillWidth > 0) { + display->fillRect(chUtil_x + 1, chUtil_y + 1, fillWidth - 1, height - 2); + } + int extraoffset = (SCREEN_WIDTH > 128) ? 6 : 3; + display->drawString(x + chUtil_x + width + extraoffset, compactFourthLine, chUtilPercentage); } // **************************** From f7849f2bd4b9641a148dc11eb2bc41ca8e89203d Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 9 Apr 2025 23:59:26 -0400 Subject: [PATCH 067/265] Adjustments to Lora Focus screen --- src/graphics/Screen.cpp | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 4feaefd75..686f4a071 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -30,8 +30,8 @@ along with this program. If not, see . #if !MESHTASTIC_EXCLUDE_GPS #include "GPS.h" #endif -#include "FSCommon.h" #include "ButtonThread.h" +#include "FSCommon.h" #include "MeshService.h" #include "NodeDB.h" #include "error.h" @@ -2498,8 +2498,6 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int // Display Region and Radio Preset char regionradiopreset[25]; const char *region = myRegion ? myRegion->name : NULL; - // const char *preset = (SCREEN_WIDTH > 128) ? "Preset" : "Prst"; - // snprintf(regionradiopreset, sizeof(regionradiopreset), "%s: %s/%s", preset, region, mode); snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); int textWidth = display->getStringWidth(regionradiopreset); int nameX = (SCREEN_WIDTH - textWidth) / 2; @@ -2512,8 +2510,6 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); char shortnameble[35]; - // snprintf(shortnameble, sizeof(shortnameble), "%s: %s/%s", "Short/BLE", haveGlyphs(owner.short_name) ? owner.short_name : - // "", ourId); snprintf(shortnameble, sizeof(shortnameble), "%s_%s", haveGlyphs(owner.short_name) ? owner.short_name : "", ourId); textWidth = display->getStringWidth(shortnameble); nameX = (SCREEN_WIDTH - textWidth) / 2; @@ -2531,25 +2527,27 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int // === Fourth Row: Channel Utilization === const char *chUtil = "ChUtil:"; char chUtilPercentage[10]; - int desperatecenteringattempt = SCREEN_WIDTH / 2; snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); - textWidth = display->getStringWidth(chUtil); - int chUtil_x = (SCREEN_WIDTH > 128) ? textWidth + 10 : textWidth + 5; + int chUtil_x = (SCREEN_WIDTH > 128) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; int chUtil_y = compactFourthLine + 3; - int width = (SCREEN_WIDTH > 128) ? 100 : 50; - int height = (SCREEN_WIDTH > 128) ? 12 : 7; - int percent = airTime->channelUtilizationPercent(); - int fillWidth = (width * percent) / 100; - - display->drawString(x, compactFourthLine, chUtil); - display->drawRect(chUtil_x, chUtil_y, width, height); - if (fillWidth > 0) { - display->fillRect(chUtil_x + 1, chUtil_y + 1, fillWidth - 1, height - 2); - } + int chutil_bar_width = (SCREEN_WIDTH > 128) ? 100 : 50; + int chutil_bar_height = (SCREEN_WIDTH > 128) ? 12 : 7; int extraoffset = (SCREEN_WIDTH > 128) ? 6 : 3; - display->drawString(x + chUtil_x + width + extraoffset, compactFourthLine, chUtilPercentage); + int percent = airTime->channelUtilizationPercent(); + int fillWidth = (chutil_bar_width * percent) / 100; + + int centerofscreen = SCREEN_WIDTH / 2; + int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; + int starting_position = centerofscreen - total_line_content_width; + + display->drawString(starting_position, compactFourthLine, chUtil); + display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); + if (fillWidth > 0) { + display->fillRect(starting_position + chUtil_x + 1, chUtil_y + 1, fillWidth - 1, chutil_bar_height - 2); + } + display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, compactFourthLine, chUtilPercentage); } // **************************** From 5fa236c77da6a5742e2e05815749f2dba3770bea Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 10 Apr 2025 00:37:39 -0400 Subject: [PATCH 068/265] Conbined distance screen into cycling screen --- src/graphics/Screen.cpp | 58 ++++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 686f4a071..e7182e248 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2189,6 +2189,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 double lat2 = node->position.latitude_i * 1e-7; double lon2 = node->position.longitude_i * 1e-7; + // Haversine formula to calculate distance between two lat/lon points double earthRadiusKm = 6371.0; double dLat = (lat2 - lat1) * DEG_TO_RAD; double dLon = (lon2 - lon1) * DEG_TO_RAD; @@ -2198,26 +2199,28 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 double c = 2 * atan2(sqrt(a), sqrt(1 - a)); double distanceKm = earthRadiusKm * c; + // Convert to imperial or metric string based on config if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { double miles = distanceKm * 0.621371; if (miles < 0.1) { - snprintf(distStr, sizeof(distStr), "%dft", (int)(miles * 5280)); // show feet + snprintf(distStr, sizeof(distStr), "%dft", (int)(miles * 5280)); } else if (miles < 10.0) { - snprintf(distStr, sizeof(distStr), "%.1fmi", miles); // 1 decimal + snprintf(distStr, sizeof(distStr), "%.1fmi", miles); } else { - snprintf(distStr, sizeof(distStr), "%dmi", (int)miles); // no decimal + snprintf(distStr, sizeof(distStr), "%dmi", (int)miles); } } else { if (distanceKm < 1.0) { - snprintf(distStr, sizeof(distStr), "%dm", (int)(distanceKm * 1000)); // show meters + snprintf(distStr, sizeof(distStr), "%dm", (int)(distanceKm * 1000)); } else if (distanceKm < 10.0) { - snprintf(distStr, sizeof(distStr), "%.1fkm", distanceKm); // 1 decimal + snprintf(distStr, sizeof(distStr), "%.1fkm", distanceKm); } else { - snprintf(distStr, sizeof(distStr), "%dkm", (int)distanceKm); // no decimal + snprintf(distStr, sizeof(distStr), "%dkm", (int)distanceKm); } } } + // Render node name and distance display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); display->drawStringMaxWidth(x, y, nameMaxWidth, nodeName); @@ -2228,6 +2231,8 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 } } +#ifdef USE_EINK + // Public screen function: shows how recently nodes were heard static void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { @@ -2240,24 +2245,46 @@ static void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, drawNodeListScreen(display, state, x, y, "Hop|Sig", drawEntryHopSignal); } +// Public screen function: shows distance to each node static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { drawNodeListScreen(display, state, x, y, "Distances", drawNodeDistance); } -// Cycles Node list screens -static EntryRenderer entryRenderers[] = {drawEntryLastHeard, drawEntryHopSignal}; +#endif // USE_EINK +// Array of rendering functions to rotate through +static EntryRenderer entryRenderers[] = { + drawEntryLastHeard, // Shows time since last heard + drawEntryHopSignal, // Shows hop count and signal bars + drawNodeDistance // New: Shows physical distance +}; + +static const char *titles[] = { + "Last Heard", + "Hop|Sig", + "Distances" // Corresponding title +}; + +// Count of total renderers (auto-sized) static const int NUM_RENDERERS = sizeof(entryRenderers) / sizeof(entryRenderers[0]); + +// Tracks last time a switch occurred static unsigned long lastSwitchTime = 0; + +// Index of the currently active renderer static int currentRendererIndex = 0; + +// How long to show each view (milliseconds) static const unsigned long RENDER_INTERVAL_MS = 2000; + +// Master function to draw the rotating node list screens static void drawCyclingNodeScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { unsigned long now = millis(); - // βœ… Reset to first view on initial entry to screen + // Reset to first view on initial entry to screen if (state->ticksSinceLastStateSwitch == 0) { currentRendererIndex = 0; lastSwitchTime = now; @@ -2269,11 +2296,13 @@ static void drawCyclingNodeScreen(OLEDDisplay *display, OLEDDisplayUiState *stat currentRendererIndex = (currentRendererIndex + 1) % NUM_RENDERERS; } + // Get the correct renderer and title for the current screen EntryRenderer currentRenderer = entryRenderers[currentRendererIndex]; - const char *titles[] = {"Last Heard", "Hop|Sig"}; + // Show the screen drawNodeListScreen(display, state, x, y, titles[currentRendererIndex], currentRenderer); } + // Helper function: Draw a single node entry for Node List (Modified for Compass Screen) void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { @@ -3470,11 +3499,16 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = drawTextMessageFrame; } + normalFrames[numframes++] = drawDeviceFocused; normalFrames[numframes++] = drawCyclingNodeScreen; - // normalFrames[numframes++] = drawLastHeardScreen; - // normalFrames[numframes++] = drawHopSignalScreen; + +// Show detailed node views only on E-Ink builds +#ifdef USE_EINK + normalFrames[numframes++] = drawLastHeardScreen; + normalFrames[numframes++] = drawHopSignalScreen; normalFrames[numframes++] = drawDistanceScreen; +#endif normalFrames[numframes++] = drawNodeListWithCompasses; normalFrames[numframes++] = drawCompassAndLocationScreen; normalFrames[numframes++] = drawLoRaFocused; From 35d4784c5c518f56c50e5d71ac58124119d0f998 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 10 Apr 2025 02:10:42 -0400 Subject: [PATCH 069/265] Node list cleanup and optimization --- src/graphics/Screen.cpp | 324 ++++++++++++++++++++-------------------- 1 file changed, 165 insertions(+), 159 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index e7182e248..118a40dee 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1883,16 +1883,41 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ display->setColor(WHITE); } -typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); -typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int columnWidth, float heading, - double lat, double lon); +// Combined dynamic node list frame cycling through LastHeard, HopSignal, and Distance modes +// Uses a single frame and changes data every few seconds (E-Ink variant is separate) + +// ============================= +// Shared Types and Structures +// ============================= +typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); +typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double); + -// h! Each node entry holds a reference to its info and how long ago it was heard from struct NodeEntry { meshtastic_NodeInfoLite *node; uint32_t lastHeard; + float cachedDistance = -1.0f; // Only used in distance mode }; +// ============================= +// Shared Enums and Timing Logic +// ============================= +enum NodeListMode { + MODE_LAST_HEARD = 0, + MODE_HOP_SIGNAL = 1, + MODE_DISTANCE = 2, + MODE_COUNT = 3 +}; + +static NodeListMode currentMode = MODE_LAST_HEARD; +static unsigned long lastModeSwitchTime = 0; +static int scrollIndex = 0; + +// Use dynamic timing based on mode +unsigned long getModeCycleIntervalMs() { + return (currentMode == MODE_DISTANCE) ? 4000 : 2000; +} + // h! Calculates bearing between two lat/lon points (used for compass) float calculateBearing(double lat1, double lon1, double lat2, double lon2) { @@ -1907,71 +1932,21 @@ float calculateBearing(double lat1, double lon1, double lat2, double lon2) return fmod((initialBearing * RAD_TO_DEG + 360), 360); // Normalize to 0-360Β° } -// Shared scroll index state for node screens -static int scrollIndex = 0; - -// Helper: Calculates max scroll index based on total entries int calculateMaxScroll(int totalEntries, int visibleRows) { int totalRows = (totalEntries + 1) / 2; return std::max(0, totalRows - visibleRows); } -// Helper: Draw vertical scrollbar matching CannedMessageModule style -void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollIndex, int columns, int scrollStartY) -{ - const int rowHeight = FONT_HEIGHT_SMALL - 3; - - // Visual rows across both columns - const int totalVisualRows = (totalEntries + columns - 1) / columns; - - if (totalVisualRows <= visibleNodeRows) - return; // Don't draw scrollbar if everything fits - - const int scrollAreaHeight = visibleNodeRows * rowHeight; - const int scrollbarX = display->getWidth() - 6; - const int scrollbarWidth = 4; - - const int scrollBarHeight = (scrollAreaHeight * visibleNodeRows) / totalVisualRows; - const int scrollBarY = scrollStartY + (scrollAreaHeight * scrollIndex) / totalVisualRows; - - display->drawRect(scrollbarX, scrollStartY, scrollbarWidth, scrollAreaHeight); - display->fillRect(scrollbarX, scrollBarY, scrollbarWidth, scrollBarHeight); -} -// Grabs all nodes from the DB and sorts them (favorites and most recently heard first) -void retrieveAndSortNodes(std::vector &nodeList) -{ - size_t numNodes = nodeDB->getNumMeshNodes(); - - for (size_t i = 0; i < numNodes; i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - if (!node || node->num == nodeDB->getNodeNum()) - continue; // Skip self - nodeList.push_back({node, sinceLastSeen(node)}); - } - - std::sort(nodeList.begin(), nodeList.end(), [](const NodeEntry &a, const NodeEntry &b) { - bool aFav = a.node->is_favorite; - bool bFav = b.node->is_favorite; - if (aFav != bFav) - return aFav > bFav; - if (a.lastHeard == 0 || a.lastHeard == UINT32_MAX) - return false; - if (b.lastHeard == 0 || b.lastHeard == UINT32_MAX) - return true; - return a.lastHeard < b.lastHeard; - }); -} - -// Helper: Fallback-NodeID if emote is on ShortName for display purposes +// ============================= +// Node Sorting and Scroll Helpers +// ============================= String getSafeNodeName(meshtastic_NodeInfoLite *node) { String nodeName = "?"; - if (node->has_user && strlen(node->user.short_name) > 0) { bool valid = true; const char *name = node->user.short_name; - for (size_t i = 0; i < strlen(name); i++) { uint8_t c = (uint8_t)name[i]; if (c < 32 || c > 126) { @@ -1979,23 +1954,63 @@ String getSafeNodeName(meshtastic_NodeInfoLite *node) break; } } - if (valid) { nodeName = name; } else { - // fallback: last 4 hex digits of node ID, no prefix char idStr[6]; snprintf(idStr, sizeof(idStr), "%04X", (uint16_t)(node->num & 0xFFFF)); nodeName = String(idStr); } } - - if (node->is_favorite) - nodeName = "*" + nodeName; + if (node->is_favorite) nodeName = "*" + nodeName; return nodeName; } -// Draws separator line +void retrieveAndSortNodes(std::vector &nodeList) +{ + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + bool hasValidSelf = nodeDB->hasValidPosition(ourNode); + + size_t numNodes = nodeDB->getNumMeshNodes(); + for (size_t i = 0; i < numNodes; i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + if (!node || node->num == nodeDB->getNodeNum()) continue; + + NodeEntry entry; + entry.node = node; + entry.lastHeard = sinceLastSeen(node); + entry.cachedDistance = -1.0f; + + // Pre-calculate distance if we're about to render distance screen + if (currentMode == MODE_DISTANCE && hasValidSelf && nodeDB->hasValidPosition(node)) { + float lat1 = ourNode->position.latitude_i * 1e-7f; + float lon1 = ourNode->position.longitude_i * 1e-7f; + float lat2 = node->position.latitude_i * 1e-7f; + float lon2 = node->position.longitude_i * 1e-7f; + + float dLat = (lat2 - lat1) * DEG_TO_RAD; + float dLon = (lon2 - lon1) * DEG_TO_RAD; + float a = sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); + float c = 2 * atan2(sqrt(a), sqrt(1 - a)); + entry.cachedDistance = 6371.0f * c; // Earth radius in km + } + + nodeList.push_back(entry); + } + + std::sort(nodeList.begin(), nodeList.end(), [](const NodeEntry &a, const NodeEntry &b) { + bool aFav = a.node->is_favorite; + bool bFav = b.node->is_favorite; + if (aFav != bFav) return aFav > bFav; + if (a.lastHeard == 0 || a.lastHeard == UINT32_MAX) return false; + if (b.lastHeard == 0 || b.lastHeard == UINT32_MAX) return true; + return a.lastHeard < b.lastHeard; + }); +} + + + + void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) { int columnWidth = display->getWidth() / 2; @@ -2003,6 +2018,23 @@ void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_ display->drawLine(separatorX, yStart, separatorX, yEnd); } +void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollIndex, int columns, int scrollStartY) +{ + const int rowHeight = FONT_HEIGHT_SMALL - 3; + const int totalVisualRows = (totalEntries + columns - 1) / columns; + if (totalVisualRows <= visibleNodeRows) return; + const int scrollAreaHeight = visibleNodeRows * rowHeight; + const int scrollbarX = display->getWidth() - 6; + const int scrollbarWidth = 4; + const int scrollBarHeight = (scrollAreaHeight * visibleNodeRows) / totalVisualRows; + const int scrollBarY = scrollStartY + (scrollAreaHeight * scrollIndex) / totalVisualRows; + display->drawRect(scrollbarX, scrollStartY, scrollbarWidth, scrollAreaHeight); + display->fillRect(scrollbarX, scrollBarY, scrollbarWidth, scrollBarHeight); +} + +// ============================= +// Shared Node List Screen Logic +// ============================= void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0, double lon = 0) @@ -2086,18 +2118,13 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY); } -// **************************** -// * Last Heard Screen * -// **************************** +// ============================= +// Shared Dynamic Entry Renderers +// ============================= void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); - - // Adjust offset based on column and screen width - int timeOffset = - (SCREEN_WIDTH > 128) - ? (isLeftCol ? 41 : 45) - : (isLeftCol ? 24 : 30); // offset large screen (?Left:Right column), offset small screen (?Left:Right column) + int timeOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 41 : 45) : (isLeftCol ? 24 : 30); String nodeName = getSafeNodeName(node); @@ -2108,12 +2135,8 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int } else { uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"), - (days ? days - : hours ? hours - : minutes), - (days ? 'd' - : hours ? 'h' - : 'm')); + (days ? days : hours ? hours : minutes), + (days ? 'd' : hours ? 'h' : 'm')); } display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -2130,15 +2153,8 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int bool isLeftCol = (x < SCREEN_WIDTH / 2); int nameMaxWidth = columnWidth - 25; - int barsOffset = - (SCREEN_WIDTH > 128) - ? (isLeftCol ? 26 : 30) - : (isLeftCol ? 17 : 19); // offset large screen (?Left:Right column), offset small screen (?Left:Right column) - int hopOffset = - (SCREEN_WIDTH > 128) - ? (isLeftCol ? 32 : 38) - : (isLeftCol ? 18 : 20); // offset large screen (?Left:Right column), offset small screen (?Left:Right column) - + int barsOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 26 : 30) : (isLeftCol ? 17 : 19); + int hopOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 32 : 38) : (isLeftCol ? 18 : 20); int barsXOffset = columnWidth - barsOffset; String nodeName = getSafeNodeName(node); @@ -2156,7 +2172,6 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->drawString(hopX, y, hopStr); } - // Signal bars based on SNR int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; int barWidth = 2; int barStartX = x + barsXOffset; @@ -2182,45 +2197,38 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 char distStr[10] = ""; meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { double lat1 = ourNode->position.latitude_i * 1e-7; double lon1 = ourNode->position.longitude_i * 1e-7; double lat2 = node->position.latitude_i * 1e-7; double lon2 = node->position.longitude_i * 1e-7; - // Haversine formula to calculate distance between two lat/lon points double earthRadiusKm = 6371.0; double dLat = (lat2 - lat1) * DEG_TO_RAD; double dLon = (lon2 - lon1) * DEG_TO_RAD; - double a = - sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); + double a = sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); double c = 2 * atan2(sqrt(a), sqrt(1 - a)); double distanceKm = earthRadiusKm * c; - // Convert to imperial or metric string based on config if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { double miles = distanceKm * 0.621371; - if (miles < 0.1) { + if (miles < 0.1) snprintf(distStr, sizeof(distStr), "%dft", (int)(miles * 5280)); - } else if (miles < 10.0) { + else if (miles < 10.0) snprintf(distStr, sizeof(distStr), "%.1fmi", miles); - } else { + else snprintf(distStr, sizeof(distStr), "%dmi", (int)miles); - } } else { - if (distanceKm < 1.0) { + if (distanceKm < 1.0) snprintf(distStr, sizeof(distStr), "%dm", (int)(distanceKm * 1000)); - } else if (distanceKm < 10.0) { + else if (distanceKm < 10.0) snprintf(distStr, sizeof(distStr), "%.1fkm", distanceKm); - } else { + else snprintf(distStr, sizeof(distStr), "%dkm", (int)distanceKm); - } } } - // Render node name and distance display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); display->drawStringMaxWidth(x, y, nameMaxWidth, nodeName); @@ -2231,77 +2239,74 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 } } -#ifdef USE_EINK - -// Public screen function: shows how recently nodes were heard -static void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +// ============================= +// Dynamic Unified Entry Renderer +// ============================= +void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { - drawNodeListScreen(display, state, x, y, "Node List", drawEntryLastHeard); + switch (currentMode) { + 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; // Silences warning for MODE_COUNT or unexpected values + } } -// Public screen function: shows hop count + signal strength -static void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +const char* getCurrentModeTitle() { - drawNodeListScreen(display, state, x, y, "Hop|Sig", drawEntryHopSignal); + switch (currentMode) { + case MODE_LAST_HEARD: return "Node List"; + case MODE_HOP_SIGNAL: return "Hop|Sig"; + case MODE_DISTANCE: return "Distances"; + default: return "Nodes"; + } } -// Public screen function: shows distance to each node -static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - drawNodeListScreen(display, state, x, y, "Distances", drawNodeDistance); -} - -#endif // USE_EINK - -// Array of rendering functions to rotate through -static EntryRenderer entryRenderers[] = { - drawEntryLastHeard, // Shows time since last heard - drawEntryHopSignal, // Shows hop count and signal bars - drawNodeDistance // New: Shows physical distance -}; - -static const char *titles[] = { - "Last Heard", - "Hop|Sig", - "Distances" // Corresponding title -}; - -// Count of total renderers (auto-sized) -static const int NUM_RENDERERS = sizeof(entryRenderers) / sizeof(entryRenderers[0]); - -// Tracks last time a switch occurred -static unsigned long lastSwitchTime = 0; - -// Index of the currently active renderer -static int currentRendererIndex = 0; - -// How long to show each view (milliseconds) -static const unsigned long RENDER_INTERVAL_MS = 2000; - - -// Master function to draw the rotating node list screens -static void drawCyclingNodeScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +// ============================= +// OLED/TFT Version (cycles every few seconds) +// ============================= +#ifndef USE_EINK +static void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { unsigned long now = millis(); - // Reset to first view on initial entry to screen + // Always start with MODE_LAST_HEARD on screen entry if (state->ticksSinceLastStateSwitch == 0) { - currentRendererIndex = 0; - lastSwitchTime = now; + currentMode = MODE_LAST_HEARD; + lastModeSwitchTime = now; } - // ⏱️ Cycle content every interval - if (now - lastSwitchTime >= RENDER_INTERVAL_MS) { - lastSwitchTime = now; - currentRendererIndex = (currentRendererIndex + 1) % NUM_RENDERERS; + if (now - lastModeSwitchTime >= getModeCycleIntervalMs()) { + lastModeSwitchTime = now; + currentMode = static_cast((currentMode + 1) % MODE_COUNT); } - // Get the correct renderer and title for the current screen - EntryRenderer currentRenderer = entryRenderers[currentRendererIndex]; - - // Show the screen - drawNodeListScreen(display, state, x, y, titles[currentRendererIndex], currentRenderer); + const char* title = getCurrentModeTitle(); + drawNodeListScreen(display, state, x, y, title, drawEntryDynamic); } +#endif + +// ============================= +// E-Ink Version (mode set once per boot) +// ============================= +#ifdef USE_EINK +static void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + if (state->ticksSinceLastStateSwitch == 0) { + currentMode = MODE_LAST_HEARD; + } + const char* title = getCurrentModeTitle(); + drawNodeListScreen(display, state, x, y, title, drawEntryDynamic); +} +#endif + // Helper function: Draw a single node entry for Node List (Modified for Compass Screen) void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) @@ -3501,7 +3506,7 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = drawDeviceFocused; - normalFrames[numframes++] = drawCyclingNodeScreen; + normalFrames[numframes++] = drawDynamicNodeListScreen; // Show detailed node views only on E-Ink builds #ifdef USE_EINK @@ -3509,6 +3514,7 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = drawHopSignalScreen; normalFrames[numframes++] = drawDistanceScreen; #endif + normalFrames[numframes++] = drawNodeListWithCompasses; normalFrames[numframes++] = drawCompassAndLocationScreen; normalFrames[numframes++] = drawLoRaFocused; From bf9a7d3a7fe40f1e2f0fcf8cf9fda45e3cfb2ded Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 10 Apr 2025 02:22:53 -0400 Subject: [PATCH 070/265] Scroll on message screen restarts on visit --- src/graphics/Screen.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 118a40dee..3223dcc17 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1325,6 +1325,14 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0; static bool waitingToReset = false, scrollStarted = false; + // === Reset scroll state when entering screen === + if (state->ticksSinceLastStateSwitch == 0) { + scrollY = 0; + scrollStartDelay = now; + lastTime = now; + waitingToReset = false; + scrollStarted = false; + } // === Smooth scrolling adjustment === // You can tweak this divisor to change how smooth it scrolls. // Lower = smoother, but can feel slow. From 6b2e03e9d21d7fe094d1984583d6b62f73d6cbb2 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 10 Apr 2025 03:30:52 -0400 Subject: [PATCH 071/265] Last heard fix --- src/graphics/Screen.cpp | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 3223dcc17..e10702979 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1923,7 +1923,7 @@ static int scrollIndex = 0; // Use dynamic timing based on mode unsigned long getModeCycleIntervalMs() { - return (currentMode == MODE_DISTANCE) ? 4000 : 2000; + return (currentMode == MODE_DISTANCE) ? 3000 : 2000; } // h! Calculates bearing between two lat/lon points (used for compass) @@ -2283,21 +2283,30 @@ const char* getCurrentModeTitle() #ifndef USE_EINK static void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + // Static variables to track mode and duration + static NodeListMode lastRenderedMode = MODE_COUNT; + static unsigned long modeStartTime = 0; + unsigned long now = millis(); - // Always start with MODE_LAST_HEARD on screen entry - if (state->ticksSinceLastStateSwitch == 0) { + // On very first call (on boot or state enter) + if (lastRenderedMode == MODE_COUNT) { currentMode = MODE_LAST_HEARD; - lastModeSwitchTime = now; + modeStartTime = now; } - if (now - lastModeSwitchTime >= getModeCycleIntervalMs()) { - lastModeSwitchTime = now; + // Time to switch to next mode? + if (now - modeStartTime >= getModeCycleIntervalMs()) { currentMode = static_cast((currentMode + 1) % MODE_COUNT); + modeStartTime = now; } + // Render screen based on currentMode const char* title = getCurrentModeTitle(); drawNodeListScreen(display, state, x, y, title, drawEntryDynamic); + + // Track the last mode to avoid reinitializing modeStartTime + lastRenderedMode = currentMode; } #endif From 56fbfe13aeb63cf30d5ab299c6d92896ae93b546 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 10 Apr 2025 03:45:59 -0400 Subject: [PATCH 072/265] Fix the tittle name --- src/graphics/Screen.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index e10702979..797f4d6a4 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1923,7 +1923,8 @@ static int scrollIndex = 0; // Use dynamic timing based on mode unsigned long getModeCycleIntervalMs() { - return (currentMode == MODE_DISTANCE) ? 3000 : 2000; + //return (currentMode == MODE_DISTANCE) ? 3000 : 2000; + return 2000; } // h! Calculates bearing between two lat/lon points (used for compass) @@ -2267,13 +2268,17 @@ void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 } } -const char* getCurrentModeTitle() +const char* getCurrentModeTitle(int screenWidth) { switch (currentMode) { - case MODE_LAST_HEARD: return "Node List"; - case MODE_HOP_SIGNAL: return "Hop|Sig"; - case MODE_DISTANCE: return "Distances"; - default: return "Nodes"; + case MODE_LAST_HEARD: + return "Node List"; + case MODE_HOP_SIGNAL: + return (screenWidth > 128) ? "Hops|Signals" : "Hop|Sig"; + case MODE_DISTANCE: + return "Distances"; + default: + return "Nodes"; } } @@ -2302,7 +2307,7 @@ static void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState * } // Render screen based on currentMode - const char* title = getCurrentModeTitle(); + const char* title = getCurrentModeTitle(display->getWidth()); drawNodeListScreen(display, state, x, y, title, drawEntryDynamic); // Track the last mode to avoid reinitializing modeStartTime From f1f6b6338082296f49e108747713c0a6631f3c07 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 10 Apr 2025 04:29:42 -0400 Subject: [PATCH 073/265] Pull back on scrolling change --- src/graphics/Screen.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 797f4d6a4..6307b67b8 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1325,14 +1325,6 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0; static bool waitingToReset = false, scrollStarted = false; - // === Reset scroll state when entering screen === - if (state->ticksSinceLastStateSwitch == 0) { - scrollY = 0; - scrollStartDelay = now; - lastTime = now; - waitingToReset = false; - scrollStarted = false; - } // === Smooth scrolling adjustment === // You can tweak this divisor to change how smooth it scrolls. // Lower = smoother, but can feel slow. From 9e4847840a3da0a4c081fdf9a2ae0bc4712b0fd0 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 11 Apr 2025 00:51:06 -0400 Subject: [PATCH 074/265] GPS tittle offset -GPS Enabled -GPS Disabled -GPS Not Present -GPS Disabled with Fixed Position -GPS Not Present with Fixed Position -GPS Enabled - Toggle off and on -GPS Disabled - Toggle on and off --- src/graphics/Screen.cpp | 167 ++++++++++++++++++++++------------------ 1 file changed, 91 insertions(+), 76 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 6307b67b8..34ac88bbc 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1006,7 +1006,7 @@ constexpr uint32_t mailBlinkInterval = 500; void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) { LOG_INFO("drawCommonHeader: hasUnreadMessage = %s", hasUnreadMessage ? "true" : "false"); - constexpr int HEADER_OFFSET_Y = 2; + constexpr int HEADER_OFFSET_Y = 1; y += HEADER_OFFSET_Y; const bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); @@ -1038,7 +1038,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) if (useHorizontalBattery) { // === Horizontal battery === int batteryX = 2; - int batteryY = 4; + int batteryY = HEADER_OFFSET_Y + 2; display->drawXbm(batteryX, batteryY, 29, 15, batteryBitmap_h); @@ -1055,7 +1055,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) } else { // === Vertical battery === int batteryX = 1; - int batteryY = 3; + int batteryY = HEADER_OFFSET_Y + 1; display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v); @@ -1404,10 +1404,19 @@ void Screen::drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char } // Draw nodes status -static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const NodeStatus *nodeStatus) +static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const NodeStatus *nodeStatus, int node_offset = 0, + bool show_total = true) { char usersString[20]; - snprintf(usersString, sizeof(usersString), "%d/%d", nodeStatus->getNumOnline(), nodeStatus->getNumTotal()); + int nodes_online = (nodeStatus->getNumOnline() > 0) ? nodeStatus->getNumOnline() + node_offset : 0; + + snprintf(usersString, sizeof(usersString), "%d", nodes_online); + + if (show_total) { + int nodes_total = (nodeStatus->getNumTotal() > 0) ? nodeStatus->getNumTotal() + node_offset : 0; + snprintf(usersString, sizeof(usersString), "%d/%d", nodes_online, nodes_total); + } + #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) @@ -1416,8 +1425,12 @@ static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const NodeStat display->drawFastImage(x, y, 8, 8, imgUser); #endif display->drawString(x + 10, y - 2, usersString); - if (config.display.heading_bold) - display->drawString(x + 11, y - 2, usersString); + int string_offset = (SCREEN_WIDTH > 128) ? 2 : 1; + if (!show_total) { + display->drawString(x + 10 + display->getStringWidth(usersString) + string_offset, y - 2, "online"); + if (config.display.heading_bold) + display->drawString(x + 11 + display->getStringWidth(usersString) + string_offset, y - 2, "online"); + } } #if HAS_GPS // Draw GPS status summary @@ -1892,7 +1905,6 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double); - struct NodeEntry { meshtastic_NodeInfoLite *node; uint32_t lastHeard; @@ -1902,20 +1914,16 @@ struct NodeEntry { // ============================= // Shared Enums and Timing Logic // ============================= -enum NodeListMode { - MODE_LAST_HEARD = 0, - MODE_HOP_SIGNAL = 1, - MODE_DISTANCE = 2, - MODE_COUNT = 3 -}; +enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 }; static NodeListMode currentMode = MODE_LAST_HEARD; static unsigned long lastModeSwitchTime = 0; static int scrollIndex = 0; // Use dynamic timing based on mode -unsigned long getModeCycleIntervalMs() { - //return (currentMode == MODE_DISTANCE) ? 3000 : 2000; +unsigned long getModeCycleIntervalMs() +{ + // return (currentMode == MODE_DISTANCE) ? 3000 : 2000; return 2000; } @@ -1963,7 +1971,8 @@ String getSafeNodeName(meshtastic_NodeInfoLite *node) nodeName = String(idStr); } } - if (node->is_favorite) nodeName = "*" + nodeName; + if (node->is_favorite) + nodeName = "*" + nodeName; return nodeName; } @@ -1975,7 +1984,8 @@ void retrieveAndSortNodes(std::vector &nodeList) size_t numNodes = nodeDB->getNumMeshNodes(); for (size_t i = 0; i < numNodes; i++) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - if (!node || node->num == nodeDB->getNodeNum()) continue; + if (!node || node->num == nodeDB->getNodeNum()) + continue; NodeEntry entry; entry.node = node; @@ -1991,7 +2001,8 @@ void retrieveAndSortNodes(std::vector &nodeList) float dLat = (lat2 - lat1) * DEG_TO_RAD; float dLon = (lon2 - lon1) * DEG_TO_RAD; - float a = sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); + float a = + sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); float c = 2 * atan2(sqrt(a), sqrt(1 - a)); entry.cachedDistance = 6371.0f * c; // Earth radius in km } @@ -2002,16 +2013,16 @@ void retrieveAndSortNodes(std::vector &nodeList) std::sort(nodeList.begin(), nodeList.end(), [](const NodeEntry &a, const NodeEntry &b) { bool aFav = a.node->is_favorite; bool bFav = b.node->is_favorite; - if (aFav != bFav) return aFav > bFav; - if (a.lastHeard == 0 || a.lastHeard == UINT32_MAX) return false; - if (b.lastHeard == 0 || b.lastHeard == UINT32_MAX) return true; + if (aFav != bFav) + return aFav > bFav; + if (a.lastHeard == 0 || a.lastHeard == UINT32_MAX) + return false; + if (b.lastHeard == 0 || b.lastHeard == UINT32_MAX) + return true; return a.lastHeard < b.lastHeard; }); } - - - void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) { int columnWidth = display->getWidth() / 2; @@ -2023,7 +2034,8 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, { const int rowHeight = FONT_HEIGHT_SMALL - 3; const int totalVisualRows = (totalEntries + columns - 1) / columns; - if (totalVisualRows <= visibleNodeRows) return; + if (totalVisualRows <= visibleNodeRows) + return; const int scrollAreaHeight = visibleNodeRows * rowHeight; const int scrollbarX = display->getWidth() - 6; const int scrollbarWidth = 4; @@ -2052,7 +2064,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t // === Manually draw the centered title within the header === const int highlightHeight = COMMON_HEADER_HEIGHT; - const int textY = y + 2 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; const int centerX = x + SCREEN_WIDTH / 2; display->setFont(FONT_SMALL); @@ -2136,8 +2148,12 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int } else { uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"), - (days ? days : hours ? hours : minutes), - (days ? 'd' : hours ? 'h' : 'm')); + (days ? days + : hours ? hours + : minutes), + (days ? 'd' + : hours ? 'h' + : 'm')); } display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -2155,7 +2171,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int int nameMaxWidth = columnWidth - 25; int barsOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 26 : 30) : (isLeftCol ? 17 : 19); - int hopOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 32 : 38) : (isLeftCol ? 18 : 20); + int hopOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 32 : 38) : (isLeftCol ? 18 : 20); int barsXOffset = columnWidth - barsOffset; String nodeName = getSafeNodeName(node); @@ -2208,7 +2224,8 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 double dLat = (lat2 - lat1) * DEG_TO_RAD; double dLon = (lon2 - lon1) * DEG_TO_RAD; - double a = sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); + double a = + sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); double c = 2 * atan2(sqrt(a), sqrt(1 - a)); double distanceKm = earthRadiusKm * c; @@ -2246,31 +2263,31 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { switch (currentMode) { - 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; // Silences warning for MODE_COUNT or unexpected values + 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; // Silences warning for MODE_COUNT or unexpected values } } -const char* getCurrentModeTitle(int screenWidth) +const char *getCurrentModeTitle(int screenWidth) { switch (currentMode) { - case MODE_LAST_HEARD: - return "Node List"; - case MODE_HOP_SIGNAL: - return (screenWidth > 128) ? "Hops|Signals" : "Hop|Sig"; - case MODE_DISTANCE: - return "Distances"; - default: - return "Nodes"; + case MODE_LAST_HEARD: + return "Node List"; + case MODE_HOP_SIGNAL: + return (screenWidth > 128) ? "Hops|Signals" : "Hop|Sig"; + case MODE_DISTANCE: + return "Distances"; + default: + return "Nodes"; } } @@ -2299,7 +2316,7 @@ static void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState * } // Render screen based on currentMode - const char* title = getCurrentModeTitle(display->getWidth()); + const char *title = getCurrentModeTitle(display->getWidth()); drawNodeListScreen(display, state, x, y, title, drawEntryDynamic); // Track the last mode to avoid reinitializing modeStartTime @@ -2316,12 +2333,11 @@ static void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState * if (state->ticksSinceLastStateSwitch == 0) { currentMode = MODE_LAST_HEARD; } - const char* title = getCurrentModeTitle(); + const char *title = getCurrentModeTitle(); drawNodeListScreen(display, state, x, y, title, drawEntryDynamic); } #endif - // Helper function: Draw a single node entry for Node List (Modified for Compass Screen) void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { @@ -2432,32 +2448,30 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i // Display Region and Channel Utilization config.display.heading_bold = false; - drawNodes(display, x + 1, compactFirstLine + 3, nodeStatus); + drawNodes(display, x + 1, compactFirstLine + 3, nodeStatus, -1, false); #if HAS_GPS auto number_of_satellites = gpsStatus->getNumSatellites(); - int gps_rightchar_offset = (SCREEN_WIDTH > 128) ? -52 : -46; + int gps_rightchar_offset = (SCREEN_WIDTH > 128) ? -30 : -46; if (number_of_satellites < 10) { - gps_rightchar_offset += (SCREEN_WIDTH > 128) ? 14 : 6; + gps_rightchar_offset -= (SCREEN_WIDTH > 128) ? 14 : 6; + } + if (!gpsStatus || !gpsStatus->getIsConnected()) { + gps_rightchar_offset = (SCREEN_WIDTH > 128) ? -20 : 2; } - if (config.position.fixed_position) { - if (SCREEN_WIDTH > 128) { - drawGPS(display, SCREEN_WIDTH + gps_rightchar_offset, compactFirstLine + 3, gpsStatus); + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + String displayLine = ""; + if (config.position.fixed_position) { + gps_rightchar_offset = (SCREEN_WIDTH > 128) ? -80 : -50; + displayLine = "Fixed GPS"; } else { - drawGPS(display, SCREEN_WIDTH + gps_rightchar_offset, compactFirstLine + 3, gpsStatus); + gps_rightchar_offset = (SCREEN_WIDTH > 128) ? -58 : -38; + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } - } else if (!gpsStatus || !gpsStatus->getIsConnected()) { - String displayLine = - config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; - int posX = SCREEN_WIDTH - display->getStringWidth(displayLine) - 2; - display->drawString(posX, compactFirstLine, displayLine); + display->drawString(SCREEN_WIDTH + gps_rightchar_offset, compactFirstLine, displayLine); } else { - if (SCREEN_WIDTH > 128) { - drawGPS(display, SCREEN_WIDTH + gps_rightchar_offset, compactFirstLine + 3, gpsStatus); - } else { - drawGPS(display, SCREEN_WIDTH + gps_rightchar_offset, compactFirstLine + 3, gpsStatus); - } + drawGPS(display, SCREEN_WIDTH + gps_rightchar_offset, compactFirstLine + 3, gpsStatus); } #endif @@ -2524,7 +2538,7 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int // === Draw title (aligned with header baseline) === const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + 2 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; const char *titleStr = (SCREEN_WIDTH > 128) ? "LoRa Info" : "LoRa"; const int centerX = x + SCREEN_WIDTH / 2; @@ -2541,6 +2555,8 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int display->setTextAlignment(TEXT_ALIGN_LEFT); // === First Row: Region / Radio Preset === + drawNodes(display, x, compactFirstLine + 3, nodeStatus, 0, true); + auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); // Display Region and Radio Preset @@ -2549,7 +2565,7 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); int textWidth = display->getStringWidth(regionradiopreset); int nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, compactFirstLine, regionradiopreset); + display->drawString(SCREEN_WIDTH - textWidth, compactFirstLine, regionradiopreset); // === Second Row: Channel Utilization === // Get our hardware ID @@ -2612,7 +2628,7 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat // === Draw title === const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + 2 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; const char *titleStr = "GPS"; const int centerX = x + SCREEN_WIDTH / 2; @@ -2742,7 +2758,7 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in // === Draw title === const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + 2 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; const char *titleStr = (SCREEN_WIDTH > 128) ? "Memory" : "Mem"; const int centerX = x + SCREEN_WIDTH / 2; @@ -3518,7 +3534,6 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = drawTextMessageFrame; } - normalFrames[numframes++] = drawDeviceFocused; normalFrames[numframes++] = drawDynamicNodeListScreen; From d90b721f7b791638fbc98fbbed39e6ebb53e28fa Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 11 Apr 2025 01:30:46 -0400 Subject: [PATCH 075/265] Values on node list aligned --- src/graphics/Screen.cpp | 71 +++++++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 34ac88bbc..19ae654eb 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2137,14 +2137,16 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); - int timeOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 41 : 45) : (isLeftCol ? 24 : 30); + int timeOffset = (SCREEN_WIDTH > 128) + ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) + : (isLeftCol ? 3 : 7); // Offset for Narrow Screens (Left Column:Right Column) String nodeName = getSafeNodeName(node); char timeStr[10]; uint32_t seconds = sinceLastSeen(node); if (seconds == 0 || seconds == UINT32_MAX) { - snprintf(timeStr, sizeof(timeStr), "? "); + snprintf(timeStr, sizeof(timeStr), "?"); } else { uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"), @@ -2159,7 +2161,10 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); display->drawString(x, y, nodeName); - display->drawString(x + columnWidth - timeOffset, y, timeStr); + + int rightEdge = x + columnWidth - timeOffset; + int textWidth = display->getStringWidth(timeStr); + display->drawString(rightEdge - textWidth, y, timeStr); } // **************************** @@ -2170,8 +2175,12 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int bool isLeftCol = (x < SCREEN_WIDTH / 2); int nameMaxWidth = columnWidth - 25; - int barsOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 26 : 30) : (isLeftCol ? 17 : 19); - int hopOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 32 : 38) : (isLeftCol ? 18 : 20); + int barsOffset = (SCREEN_WIDTH > 128) + ? (isLeftCol ? 16 : 20) // Offset for Wide Screens (Left Column:Right Column) + : (isLeftCol ? 15 : 19); // Offset for Narrow Screens (Left Column:Right Column) + int hopOffset = (SCREEN_WIDTH > 128) + ? (isLeftCol ? 22 : 28) // Offset for Wide Screens (Left Column:Right Column) + : (isLeftCol ? 18 : 20); // Offset for Narrow Screens (Left Column:Right Column) int barsXOffset = columnWidth - barsOffset; String nodeName = getSafeNodeName(node); @@ -2185,8 +2194,9 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away); if (hopStr[0] != '\0') { - int hopX = x + columnWidth - hopOffset - display->getStringWidth(hopStr); - display->drawString(hopX, y, hopStr); + int rightEdge = x + columnWidth - hopOffset; + int textWidth = display->getStringWidth(hopStr); + display->drawString(rightEdge - textWidth, y, hopStr); } int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; @@ -2231,19 +2241,33 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { double miles = distanceKm * 0.621371; - if (miles < 0.1) - snprintf(distStr, sizeof(distStr), "%dft", (int)(miles * 5280)); - else if (miles < 10.0) - snprintf(distStr, sizeof(distStr), "%.1fmi", miles); - else - snprintf(distStr, sizeof(distStr), "%dmi", (int)miles); + if (miles < 0.1) { + int feet = (int)(miles * 5280); + if (feet < 1000) + snprintf(distStr, sizeof(distStr), "%dft", feet); + else + snprintf(distStr, sizeof(distStr), "ΒΌmi"); // 4-char max + } else { + int roundedMiles = (int)(miles + 0.5); + if (roundedMiles < 1000) + snprintf(distStr, sizeof(distStr), "%dmi", roundedMiles); + else + snprintf(distStr, sizeof(distStr), "999"); // Max display cap + } } else { - if (distanceKm < 1.0) - snprintf(distStr, sizeof(distStr), "%dm", (int)(distanceKm * 1000)); - else if (distanceKm < 10.0) - snprintf(distStr, sizeof(distStr), "%.1fkm", distanceKm); - else - snprintf(distStr, sizeof(distStr), "%dkm", (int)distanceKm); + if (distanceKm < 1.0) { + int meters = (int)(distanceKm * 1000); + if (meters < 1000) + snprintf(distStr, sizeof(distStr), "%dm", meters); + else + snprintf(distStr, sizeof(distStr), "1k"); + } else { + int km = (int)(distanceKm + 0.5); + if (km < 1000) + snprintf(distStr, sizeof(distStr), "%dk", km); + else + snprintf(distStr, sizeof(distStr), "999"); + } } } @@ -2252,8 +2276,13 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->drawStringMaxWidth(x, y, nameMaxWidth, nodeName); if (strlen(distStr) > 0) { - int offset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 55 : 63) : (isLeftCol ? 32 : 37); - display->drawString(x + columnWidth - offset, y, distStr); + int offset = + (SCREEN_WIDTH > 128) + ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) + : (isLeftCol ? 5 : 8); // Offset for Narrow Screens (Left Column:Right Column) + int rightEdge = x + columnWidth - offset; + int textWidth = display->getStringWidth(distStr); + display->drawString(rightEdge - textWidth, y, distStr); } } From 900a7c4c5e96c0a89f48f81922e8460b1717faa7 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 11 Apr 2025 20:45:45 -0400 Subject: [PATCH 076/265] Update Screen.cpp --- src/graphics/Screen.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 19ae654eb..c51e1a956 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1405,7 +1405,7 @@ void Screen::drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char // Draw nodes status static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const NodeStatus *nodeStatus, int node_offset = 0, - bool show_total = true) + bool show_total = true, String additional_words = "") { char usersString[20]; int nodes_online = (nodeStatus->getNumOnline() > 0) ? nodeStatus->getNumOnline() + node_offset : 0; @@ -1426,10 +1426,10 @@ static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const NodeStat #endif display->drawString(x + 10, y - 2, usersString); int string_offset = (SCREEN_WIDTH > 128) ? 2 : 1; - if (!show_total) { - display->drawString(x + 10 + display->getStringWidth(usersString) + string_offset, y - 2, "online"); + if (additional_words != "") { + display->drawString(x + 10 + display->getStringWidth(usersString) + string_offset, y - 2, additional_words); if (config.display.heading_bold) - display->drawString(x + 11 + display->getStringWidth(usersString) + string_offset, y - 2, "online"); + display->drawString(x + 11 + display->getStringWidth(usersString) + string_offset, y - 2, additional_words); } } #if HAS_GPS @@ -2246,13 +2246,13 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 if (feet < 1000) snprintf(distStr, sizeof(distStr), "%dft", feet); else - snprintf(distStr, sizeof(distStr), "ΒΌmi"); // 4-char max + snprintf(distStr, sizeof(distStr), "ΒΌmi"); // 4-char max } else { int roundedMiles = (int)(miles + 0.5); if (roundedMiles < 1000) snprintf(distStr, sizeof(distStr), "%dmi", roundedMiles); else - snprintf(distStr, sizeof(distStr), "999"); // Max display cap + snprintf(distStr, sizeof(distStr), "999"); // Max display cap } } else { if (distanceKm < 1.0) { @@ -2477,13 +2477,13 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i // Display Region and Channel Utilization config.display.heading_bold = false; - drawNodes(display, x + 1, compactFirstLine + 3, nodeStatus, -1, false); + drawNodes(display, x + 1, compactFirstLine + 3, nodeStatus, -1, false, "online"); #if HAS_GPS auto number_of_satellites = gpsStatus->getNumSatellites(); - int gps_rightchar_offset = (SCREEN_WIDTH > 128) ? -30 : -46; + int gps_rightchar_offset = (SCREEN_WIDTH > 128) ? -51 : -46; if (number_of_satellites < 10) { - gps_rightchar_offset -= (SCREEN_WIDTH > 128) ? 14 : 6; + gps_rightchar_offset += (SCREEN_WIDTH > 128) ? 8 : 6; } if (!gpsStatus || !gpsStatus->getIsConnected()) { gps_rightchar_offset = (SCREEN_WIDTH > 128) ? -20 : 2; From f1fda7bdeb414bbe15bc2a05c20357f2867a8eb2 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 13 Apr 2025 23:34:10 -0400 Subject: [PATCH 077/265] Lots fixes --- src/graphics/Screen.cpp | 76 ++++++++++++++++------------------------- src/graphics/Screen.h | 1 + 2 files changed, 31 insertions(+), 46 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index c51e1a956..b7826a4e4 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -3618,52 +3618,29 @@ void Screen::setFrames(FrameFocus focus) // Focus on a specific frame, in the frame set we just created switch (focus) { - case FOCUS_DEFAULT: - ui->switchToFrame(0); // First frame - break; - case FOCUS_FAULT: - ui->switchToFrame(fsi.positions.fault); - break; - case FOCUS_TEXTMESSAGE: - hasUnreadMessage = false; // βœ… Clear when message is *viewed* - ui->switchToFrame(fsi.positions.textMessage); - break; - case FOCUS_MODULE: - // Whichever frame was marked by MeshModule::requestFocus(), if any - // If no module requested focus, will show the first frame instead - ui->switchToFrame(fsi.positions.focusedModule); - break; + case FOCUS_DEFAULT: + ui->switchToFrame(fsi.positions.deviceFocused); + break; + case FOCUS_FAULT: + ui->switchToFrame(fsi.positions.fault); + break; + case FOCUS_TEXTMESSAGE: + hasUnreadMessage = false; // βœ… Clear when message is *viewed* + ui->switchToFrame(fsi.positions.textMessage); + break; + case FOCUS_MODULE: + // Whichever frame was marked by MeshModule::requestFocus(), if any + // If no module requested focus, will show the first frame instead + ui->switchToFrame(fsi.positions.focusedModule); + break; - case FOCUS_PRESERVE: { - const FramesetInfo &oldFsi = this->framesetInfo; - - // βœ… Fix: Account for new message insertion shifting frame positions - if (willInsertTextMessage && oldFsi.positions.textMessage == 0 && fsi.positions.textMessage <= originalPosition) { - originalPosition++; - } - - if (originalPosition == oldFsi.positions.log && fsi.positions.log < fsi.frameCount) - ui->switchToFrame(fsi.positions.log); - else if (originalPosition == oldFsi.positions.settings && fsi.positions.settings < fsi.frameCount) - ui->switchToFrame(fsi.positions.settings); - else if (originalPosition == oldFsi.positions.wifi && fsi.positions.wifi < fsi.frameCount) - ui->switchToFrame(fsi.positions.wifi); - - // If frame count has decreased - else if (fsi.frameCount < oldFsi.frameCount) { - uint8_t numDropped = oldFsi.frameCount - fsi.frameCount; - // Move n frames backwards - if (numDropped <= originalPosition) - ui->switchToFrame(originalPosition - numDropped); - // Unless that would put us "out of bounds" (< 0) + case FOCUS_PRESERVE: + // 🚫 No more adjustment β€” force stay on same index + if (originalPosition < fsi.frameCount) + ui->switchToFrame(originalPosition); else - ui->switchToFrame(0); - } else if (originalPosition < fsi.frameCount) - ui->switchToFrame(originalPosition); - else - ui->switchToFrame(fsi.frameCount - 1); - break; - } + ui->switchToFrame(fsi.frameCount - 1); + break; } // Store the info about this frameset, for future setFrames calls @@ -3687,13 +3664,20 @@ void Screen::dismissCurrentFrame() uint8_t currentFrame = ui->getUiState()->currentFrame; bool dismissed = false; - if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) { + // Only dismiss if the text message frame is currently valid and visible + if (framesetInfo.positions.textMessage != 255 && + currentFrame == framesetInfo.positions.textMessage && + devicestate.has_rx_text_message) + { LOG_INFO("Dismiss Text Message"); devicestate.has_rx_text_message = false; + memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); // βœ… clear message dismissed = true; } - else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { + else if (currentFrame == framesetInfo.positions.waypoint && + devicestate.has_rx_waypoint) + { LOG_DEBUG("Dismiss Waypoint"); devicestate.has_rx_waypoint = false; dismissed = true; diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 6fd84e8fd..a66593d9f 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -607,6 +607,7 @@ class Screen : public concurrency::OSThread uint8_t log = 0; uint8_t settings = 0; uint8_t wifi = 0; + uint8_t deviceFocused = 0; } positions; uint8_t frameCount = 0; From 257165431fb5ce902c4b078e1a49263103f7eff8 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 14 Apr 2025 02:19:04 -0400 Subject: [PATCH 078/265] Update Screen.cpp --- src/graphics/Screen.cpp | 42 +++++++++++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index b7826a4e4..9f99f1153 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1005,7 +1005,6 @@ constexpr uint32_t mailBlinkInterval = 500; // *********************** void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) { - LOG_INFO("drawCommonHeader: hasUnreadMessage = %s", hasUnreadMessage ? "true" : "false"); constexpr int HEADER_OFFSET_Y = 1; y += HEADER_OFFSET_Y; @@ -1032,7 +1031,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) lastBlink = now; } - // βœ… Hybrid condition: wide screen AND landscape layout + // Hybrid condition: wide screen AND landscape layout bool useHorizontalBattery = (SCREEN_WIDTH > 128 && SCREEN_WIDTH > SCREEN_HEIGHT); if (useHorizontalBattery) { @@ -1157,6 +1156,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) display->setColor(WHITE); } + // **************************** // * Text Message Screen * // **************************** @@ -2628,18 +2628,44 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int int chutil_bar_width = (SCREEN_WIDTH > 128) ? 100 : 50; int chutil_bar_height = (SCREEN_WIDTH > 128) ? 12 : 7; int extraoffset = (SCREEN_WIDTH > 128) ? 6 : 3; - int percent = airTime->channelUtilizationPercent(); - int fillWidth = (chutil_bar_width * percent) / 100; + int chutil_percent = airTime->channelUtilizationPercent(); int centerofscreen = SCREEN_WIDTH / 2; int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; int starting_position = centerofscreen - total_line_content_width; display->drawString(starting_position, compactFourthLine, chUtil); - display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); - if (fillWidth > 0) { - display->fillRect(starting_position + chUtil_x + 1, chUtil_y + 1, fillWidth - 1, chutil_bar_height - 2); + + // Weighting for nonlinear segments + float milestone1 = 25; + float milestone2 = 40; + float weight1 = 0.4; // Weight for 0–25% + float weight2 = 0.33; // Weight for 25–40% + float weight3 = 0.27; // Weight for 40–100% + float totalWeight = weight1 + weight2 + weight3; + + int seg1 = chutil_bar_width * (weight1 / totalWeight); + int seg2 = chutil_bar_width * (weight2 / totalWeight); + int seg3 = chutil_bar_width * (weight3 / totalWeight); + + int fillRight = 0; + + if (chutil_percent <= milestone1) { + fillRight = (seg1 * (chutil_percent / milestone1)); + } else if (chutil_percent <= milestone2) { + fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); + } else { + fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); } + + // Draw outline + display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); + + // Fill progress + if (fillRight > 0) { + display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); + } + display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, compactFourthLine, chUtilPercentage); } @@ -3635,7 +3661,7 @@ void Screen::setFrames(FrameFocus focus) break; case FOCUS_PRESERVE: - // 🚫 No more adjustment β€” force stay on same index + // No more adjustment β€” force stay on same index if (originalPosition < fsi.frameCount) ui->switchToFrame(originalPosition); else From 5f245177bc2381d1270921c12086e16e1062c6c4 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 21 Apr 2025 19:51:40 -0400 Subject: [PATCH 079/265] Added tick marks for Channel Util bar --- src/graphics/Screen.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 9f99f1153..29bd680fa 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2636,12 +2636,17 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int display->drawString(starting_position, compactFourthLine, chUtil); + // Force 56% or higher to show a full 100% bar, text would still show related percent. + if (chutil_percent >= 61) { + chutil_percent = 100; + } + // Weighting for nonlinear segments float milestone1 = 25; float milestone2 = 40; - float weight1 = 0.4; // Weight for 0–25% - float weight2 = 0.33; // Weight for 25–40% - float weight3 = 0.27; // Weight for 40–100% + float weight1 = 0.45; // Weight for 0–25% + float weight2 = 0.35; // Weight for 25–40% + float weight3 = 0.20; // Weight for 40–100% float totalWeight = weight1 + weight2 + weight3; int seg1 = chutil_bar_width * (weight1 / totalWeight); From df631d480b2eeb345210e76a11d3c00e75c47f7d Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 4 May 2025 15:07:44 -0400 Subject: [PATCH 080/265] Fix to battery logo on Eink --- src/graphics/Screen.cpp | 86 ++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 44 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 7192bf27a..f6abe1e7b 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -157,7 +157,7 @@ static bool haveGlyphs(const char *str) } } - LOG_DEBUG("haveGlyphs=%d", have); + // LOG_DEBUG("haveGlyphs=%d", have); return have; } bool hasUnreadMessage = false; @@ -1055,6 +1055,9 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) // === Vertical battery === int batteryX = 1; int batteryY = HEADER_OFFSET_Y + 1; +#ifdef USE_EINK + batteryY = batteryY + 2; +#endif display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v); @@ -1079,7 +1082,11 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) int chargeNumWidth = display->getStringWidth(chargeStr); const int batteryOffset = useHorizontalBattery ? 28 : 6; +#ifdef USE_EINK + const int percentX = x + xOffset + batteryOffset - 2; +#else const int percentX = x + xOffset + batteryOffset; +#endif display->drawString(percentX, textY, chargeStr); display->drawString(percentX + chargeNumWidth - 1, textY, "%"); @@ -2137,9 +2144,8 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { bool isLeftCol = (x < SCREEN_WIDTH / 2); - int timeOffset = (SCREEN_WIDTH > 128) - ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) - : (isLeftCol ? 3 : 7); // Offset for Narrow Screens (Left Column:Right Column) + int timeOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) + : (isLeftCol ? 3 : 7); // Offset for Narrow Screens (Left Column:Right Column) String nodeName = getSafeNodeName(node); @@ -2175,12 +2181,10 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int bool isLeftCol = (x < SCREEN_WIDTH / 2); int nameMaxWidth = columnWidth - 25; - int barsOffset = (SCREEN_WIDTH > 128) - ? (isLeftCol ? 16 : 20) // Offset for Wide Screens (Left Column:Right Column) - : (isLeftCol ? 15 : 19); // Offset for Narrow Screens (Left Column:Right Column) - int hopOffset = (SCREEN_WIDTH > 128) - ? (isLeftCol ? 22 : 28) // Offset for Wide Screens (Left Column:Right Column) - : (isLeftCol ? 18 : 20); // Offset for Narrow Screens (Left Column:Right Column) + int barsOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 16 : 20) // Offset for Wide Screens (Left Column:Right Column) + : (isLeftCol ? 15 : 19); // Offset for Narrow Screens (Left Column:Right Column) + int hopOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 22 : 28) // Offset for Wide Screens (Left Column:Right Column) + : (isLeftCol ? 18 : 20); // Offset for Narrow Screens (Left Column:Right Column) int barsXOffset = columnWidth - barsOffset; String nodeName = getSafeNodeName(node); @@ -2276,10 +2280,8 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->drawStringMaxWidth(x, y, nameMaxWidth, nodeName); if (strlen(distStr) > 0) { - int offset = - (SCREEN_WIDTH > 128) - ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) - : (isLeftCol ? 5 : 8); // Offset for Narrow Screens (Left Column:Right Column) + int offset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) + : (isLeftCol ? 5 : 8); // Offset for Narrow Screens (Left Column:Right Column) int rightEdge = x + columnWidth - offset; int textWidth = display->getStringWidth(distStr); display->drawString(rightEdge - textWidth, y, distStr); @@ -2362,7 +2364,7 @@ static void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState * if (state->ticksSinceLastStateSwitch == 0) { currentMode = MODE_LAST_HEARD; } - const char *title = getCurrentModeTitle(); + const char *title = getCurrentModeTitle(display->getWidth()); drawNodeListScreen(display, state, x, y, title, drawEntryDynamic); } #endif @@ -3651,29 +3653,29 @@ void Screen::setFrames(FrameFocus focus) // Focus on a specific frame, in the frame set we just created switch (focus) { - case FOCUS_DEFAULT: - ui->switchToFrame(fsi.positions.deviceFocused); - break; - case FOCUS_FAULT: - ui->switchToFrame(fsi.positions.fault); - break; - case FOCUS_TEXTMESSAGE: - hasUnreadMessage = false; // βœ… Clear when message is *viewed* - ui->switchToFrame(fsi.positions.textMessage); - break; - case FOCUS_MODULE: - // Whichever frame was marked by MeshModule::requestFocus(), if any - // If no module requested focus, will show the first frame instead - ui->switchToFrame(fsi.positions.focusedModule); - break; + case FOCUS_DEFAULT: + ui->switchToFrame(fsi.positions.deviceFocused); + break; + case FOCUS_FAULT: + ui->switchToFrame(fsi.positions.fault); + break; + case FOCUS_TEXTMESSAGE: + hasUnreadMessage = false; // βœ… Clear when message is *viewed* + ui->switchToFrame(fsi.positions.textMessage); + break; + case FOCUS_MODULE: + // Whichever frame was marked by MeshModule::requestFocus(), if any + // If no module requested focus, will show the first frame instead + ui->switchToFrame(fsi.positions.focusedModule); + break; - case FOCUS_PRESERVE: - // No more adjustment β€” force stay on same index - if (originalPosition < fsi.frameCount) - ui->switchToFrame(originalPosition); - else - ui->switchToFrame(fsi.frameCount - 1); - break; + case FOCUS_PRESERVE: + // No more adjustment β€” force stay on same index + if (originalPosition < fsi.frameCount) + ui->switchToFrame(originalPosition); + else + ui->switchToFrame(fsi.frameCount - 1); + break; } // Store the info about this frameset, for future setFrames calls @@ -3698,19 +3700,15 @@ void Screen::dismissCurrentFrame() bool dismissed = false; // Only dismiss if the text message frame is currently valid and visible - if (framesetInfo.positions.textMessage != 255 && - currentFrame == framesetInfo.positions.textMessage && - devicestate.has_rx_text_message) - { + if (framesetInfo.positions.textMessage != 255 && currentFrame == framesetInfo.positions.textMessage && + devicestate.has_rx_text_message) { LOG_INFO("Dismiss Text Message"); devicestate.has_rx_text_message = false; memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); // βœ… clear message dismissed = true; } - else if (currentFrame == framesetInfo.positions.waypoint && - devicestate.has_rx_waypoint) - { + else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { LOG_DEBUG("Dismiss Waypoint"); devicestate.has_rx_waypoint = false; dismissed = true; From 33093e28fa25817c2d0cbcca3fb5a6f1aa2e9994 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 4 May 2025 15:08:39 -0400 Subject: [PATCH 081/265] Memory bar fix --- src/graphics/Screen.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index f6abe1e7b..4a70c1905 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2860,9 +2860,10 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in char combinedStr[24]; if (SCREEN_WIDTH > 128) { - snprintf(combinedStr, sizeof(combinedStr), "%3d%% %lu/%luKB", percent, used / 1024, total / 1024); + snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %lu/%luKB", (percent > 80) ? "! " : "", percent, used / 1024, + total / 1024); } else { - snprintf(combinedStr, sizeof(combinedStr), "%3d%%", percent); + snprintf(combinedStr, sizeof(combinedStr), "%s%3d%%", (percent > 80) ? "! " : "", percent); } int textWidth = display->getStringWidth(combinedStr); @@ -2881,8 +2882,6 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in display->setColor(WHITE); display->drawRect(barX, barY, adjustedBarWidth, barHeight); - if (percent >= 80) - display->setColor(BLACK); display->fillRect(barX, barY, fillWidth, barHeight); display->setColor(WHITE); From 75c5080fd9575caf49891b8269b6025bdffbd54d Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 4 May 2025 20:21:02 -0400 Subject: [PATCH 082/265] New Feature Iconed Screen navigation bar. --- src/graphics/Screen.cpp | 205 +++++++++++++++++++++++++++++----------- src/graphics/Screen.h | 3 +- src/graphics/images.h | 142 ++++++++++++++++++++++++++++ 3 files changed, 294 insertions(+), 56 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 4a70c1905..b11239773 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2369,6 +2369,26 @@ static void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState * } #endif +// Add these below (still inside #ifdef USE_EINK if you prefer): +#ifdef USE_EINK +static void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const char *title = "Node List"; + drawNodeListScreen(display, state, x, y, title, drawEntryLastHeard); +} + +static void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const char *title = (display->getWidth() > 128) ? "Hops|Signals" : "Hop|Sig"; + drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal); +} + +static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const char *title = "Distances"; + drawNodeListScreen(display, state, x, y, title, drawNodeDistance); +} +#endif // Helper function: Draw a single node entry for Node List (Modified for Compass Screen) void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) { @@ -3116,13 +3136,60 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) screenOn = on; } } +static int8_t lastFrameIndex = -1; +static uint32_t lastFrameChangeTime = 0; +constexpr uint32_t ICON_DISPLAY_DURATION_MS = 1000; + +void drawCustomFrameIcons(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + int currentFrame = state->currentFrame; + + // Detect frame change and record time + if (currentFrame != lastFrameIndex) { + lastFrameIndex = currentFrame; + lastFrameChangeTime = millis(); + } + + // Only show bar briefly after switching frames + if (millis() - lastFrameChangeTime > ICON_DISPLAY_DURATION_MS) return; + + const int iconSize = 8; + const int spacing = 2; + size_t totalIcons = screen->indicatorIcons.size(); + if (totalIcons == 0) return; + + int totalWidth = totalIcons * iconSize + (totalIcons - 1) * spacing; + int xStart = (SCREEN_WIDTH - totalWidth) / 2; + int y = SCREEN_HEIGHT - iconSize - 1; + + // Clear background under icon bar to avoid overlaps + display->setColor(BLACK); + display->fillRect(xStart - 1, y - 2, totalWidth + 2, iconSize + 4); + display->setColor(WHITE); + + for (size_t i = 0; i < totalIcons; ++i) { + const uint8_t* icon = screen->indicatorIcons[i]; + int x = xStart + i * (iconSize + spacing); + + if (i == static_cast(currentFrame)) { + // Draw white box and invert icon for visibility + display->setColor(WHITE); + display->fillRect(x - 1, y - 1, iconSize + 2, iconSize + 2); + display->setColor(BLACK); + display->drawXbm(x, y, iconSize, iconSize, icon); + display->setColor(WHITE); + } else { + display->drawXbm(x, y, iconSize, iconSize, icon); + } + } +} void Screen::setup() { - // We don't set useDisplay until setup() is called, because some boards have a declaration of this object but the device - // is never found when probing i2c and therefore we don't call setup and never want to do (invalid) accesses to this device. + // === Enable display rendering === useDisplay = true; + // === Detect OLED subtype (if supported by board variant) === #ifdef AutoOLEDWire_h if (isAUTOOled) static_cast(dispdev)->setDetected(model); @@ -3133,110 +3200,110 @@ void Screen::setup() #endif #if defined(USE_ST7789) && defined(TFT_MESH) - // Heltec T114 and T190: honor a custom text color, if defined in variant.h + // Apply custom RGB color (e.g. Heltec T114/T190) static_cast(dispdev)->setRGB(TFT_MESH); #endif - // Initialising the UI will init the display too. + // === Initialize display and UI system === ui->init(); - displayWidth = dispdev->width(); displayHeight = dispdev->height(); - ui->setTimePerTransition(0); + ui->setTimePerTransition(0); // Disable animation delays + ui->setIndicatorPosition(BOTTOM); // Not used (indicators disabled below) + ui->setIndicatorDirection(LEFT_RIGHT); // Not used (indicators disabled below) + ui->setFrameAnimation(SLIDE_LEFT); // Used only when indicators are active + ui->disableAllIndicators(); // Disable page indicator dots + ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance - ui->setIndicatorPosition(BOTTOM); - // Defines where the first frame is located in the bar. - ui->setIndicatorDirection(LEFT_RIGHT); - ui->setFrameAnimation(SLIDE_LEFT); - // Don't show the page swipe dots while in boot screen. - ui->disableAllIndicators(); - // Store a pointer to Screen so we can get to it from static functions. - ui->getUiState()->userData = this; + // === Set custom overlay callbacks === + static OverlayCallback overlays[] = { + drawFunctionOverlay, // For mute/buzzer modifiers etc. + drawCustomFrameIcons // Custom indicator icons for each frame + }; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); - // Set the utf8 conversion function + // === Enable UTF-8 to display mapping === dispdev->setFontTableLookupFunction(customFontTableLookup); #ifdef USERPREFS_OEM_TEXT - logo_timeout *= 2; // Double the time if we have a custom logo + logo_timeout *= 2; // Give more time for branded boot logos #endif - // Add frames. - EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); - alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + // === Configure alert frames (e.g., "Resuming..." or region name) === + EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip slow refresh + alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { #ifdef ARCH_ESP32 - if (wakeCause == ESP_SLEEP_WAKEUP_TIMER || wakeCause == ESP_SLEEP_WAKEUP_EXT1) { + if (wakeCause == ESP_SLEEP_WAKEUP_TIMER || wakeCause == ESP_SLEEP_WAKEUP_EXT1) drawFrameText(display, state, x, y, "Resuming..."); - } else + else #endif { - // Draw region in upper left - const char *region = myRegion ? myRegion->name : NULL; + const char *region = myRegion ? myRegion->name : nullptr; drawIconScreen(region, display, state, x, y); } }; ui->setFrames(alertFrames, 1); - // No overlays. - ui->setOverlays(nullptr, 0); + ui->disableAutoTransition(); // Require manual navigation between frames - // Require presses to switch between frames. - ui->disableAutoTransition(); - - // Set up a log buffer with 3 lines, 32 chars each. + // === Log buffer for on-screen logs (3 lines max) === dispdev->setLogBuffer(3, 32); + // === Optional screen mirroring or flipping (e.g. for T-Beam orientation) === #ifdef SCREEN_MIRROR dispdev->mirrorScreen(); #else - // Standard behaviour is to FLIP the screen (needed on T-Beam). If this config item is set, unflip it, and thereby logically - // flip it. If you have a headache now, you're welcome. if (!config.display.flip_screen) { -#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || \ - defined(ST7789_CS) || defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) + #if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ + defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) static_cast(dispdev)->flipScreenVertically(); -#elif defined(USE_ST7789) + #elif defined(USE_ST7789) static_cast(dispdev)->flipScreenVertically(); -#else + #else dispdev->flipScreenVertically(); -#endif + #endif } #endif - // Get our hardware ID + // === Generate device ID from MAC address === uint8_t dmac[6]; getMacAddr(dmac); snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); + #if ARCH_PORTDUINO - handleSetOn(false); // force clean init + handleSetOn(false); // Ensure proper init for Arduino targets #endif - // Turn on the display. + // === Turn on display and trigger first draw === handleSetOn(true); - - // On some ssd1306 clones, the first draw command is discarded, so draw it - // twice initially. Skip this for EINK Displays to save a few seconds during boot ui->update(); #ifndef USE_EINK - ui->update(); + ui->update(); // Some SSD1306 clones drop the first draw, so run twice #endif serialSinceMsec = millis(); + // === Optional touchscreen support === #if ARCH_PORTDUINO && !HAS_TFT if (settingsMap[touchscreenModule]) { - touchScreenImpl1 = - new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); + touchScreenImpl1 = new TouchScreenImpl1( + dispdev->getWidth(), dispdev->getHeight(), + static_cast(dispdev)->getTouch + ); touchScreenImpl1->init(); } #elif HAS_TOUCHSCREEN - touchScreenImpl1 = - new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); + touchScreenImpl1 = new TouchScreenImpl1( + dispdev->getWidth(), dispdev->getHeight(), + static_cast(dispdev)->getTouch + ); touchScreenImpl1->init(); #endif - // Subscribe to status updates + // === Subscribe to device status updates === powerStatusObserver.observe(&powerStatus->onNewStatus); gpsStatusObserver.observe(&gpsStatus->onNewStatus); nodeStatusObserver.observe(&nodeStatus->onNewStatus); + #if !MESHTASTIC_EXCLUDE_ADMIN adminMessageObserver.observe(adminModule); #endif @@ -3245,7 +3312,7 @@ void Screen::setup() if (inputBroker) inputObserver.observe(inputBroker); - // Modules can notify screen about refresh + // === Notify modules that support UI events === MeshModule::observeUIEvents(&uiFrameEventObserver); } @@ -3394,6 +3461,9 @@ int32_t Screen::runOnce() // Switch to a low framerate (to save CPU) when we are not in transition // but we should only call setTargetFPS when framestate changes, because // otherwise that breaks animations. + // === Auto-hide indicator icons unless in transition === + OLEDDisplayUiState *state = ui->getUiState(); + if (targetFramerate != IDLE_FRAMERATE && ui->getUiState()->frameState == FIXED) { // oldFrameState = ui->getUiState()->frameState; targetFramerate = IDLE_FRAMERATE; @@ -3409,8 +3479,8 @@ int32_t Screen::runOnce() if (config.display.auto_screen_carousel_secs > 0 && !Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) { -// If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead -// Carousel is potentially a major source of E-Ink display wear + // If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead + // Carousel is potentially a major source of E-Ink display wear #if !defined(EINK_BACKGROUND_USES_FAST) EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); #endif @@ -3532,6 +3602,7 @@ void Screen::setFrames(FrameFocus focus) LOG_DEBUG("Show standard frames"); showingNormalScreen = true; + indicatorIcons.clear(); #ifdef USE_EINK // If user has disabled the screensaver, warn them after boot static bool warnedScreensaverDisabled = false; @@ -3573,6 +3644,7 @@ void Screen::setFrames(FrameFocus focus) if (m == waypointModule) fsi.positions.waypoint = numframes; + indicatorIcons.push_back(icon_module); numframes++; } @@ -3582,6 +3654,7 @@ void Screen::setFrames(FrameFocus focus) fsi.positions.fault = numframes; if (error_code) { normalFrames[numframes++] = drawCriticalFaultFrame; + indicatorIcons.push_back(icon_error); focus = FOCUS_FAULT; // Change our "focus" parameter, to ensure we show the fault frame } @@ -3595,22 +3668,40 @@ void Screen::setFrames(FrameFocus focus) if (willInsertTextMessage) { fsi.positions.textMessage = numframes; normalFrames[numframes++] = drawTextMessageFrame; + indicatorIcons.push_back(icon_mail); } normalFrames[numframes++] = drawDeviceFocused; + indicatorIcons.push_back(icon_home); + +#ifndef USE_EINK normalFrames[numframes++] = drawDynamicNodeListScreen; + indicatorIcons.push_back(icon_nodes); +#endif // Show detailed node views only on E-Ink builds #ifdef USE_EINK normalFrames[numframes++] = drawLastHeardScreen; + indicatorIcons.push_back(icon_nodes); + normalFrames[numframes++] = drawHopSignalScreen; + indicatorIcons.push_back(icon_signal); + normalFrames[numframes++] = drawDistanceScreen; + indicatorIcons.push_back(icon_distance); #endif normalFrames[numframes++] = drawNodeListWithCompasses; + indicatorIcons.push_back(icon_list); + normalFrames[numframes++] = drawCompassAndLocationScreen; + indicatorIcons.push_back(icon_compass); + normalFrames[numframes++] = drawLoRaFocused; + indicatorIcons.push_back(icon_radio); + normalFrames[numframes++] = drawMemoryScreen; + indicatorIcons.push_back(icon_memory); // then all the nodes // We only show a few nodes in our scrolling list - because meshes with many nodes would have too many screens @@ -3632,20 +3723,24 @@ void Screen::setFrames(FrameFocus focus) fsi.positions.wifi = numframes; #if HAS_WIFI && !defined(ARCH_PORTDUINO) if (isWifiAvailable()) { - // call a method on debugInfoScreen object (for more details) normalFrames[numframes++] = &Screen::drawDebugInfoWiFiTrampoline; + indicatorIcons.push_back(icon_wifi); } #endif fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE + this->frameCount = numframes; // βœ… Save frame count for use in custom overlay LOG_DEBUG("Finished build frames. numframes: %d", numframes); ui->setFrames(normalFrames, numframes); - ui->enableAllIndicators(); + ui->disableAllIndicators(); // Add function overlay here. This can show when notifications muted, modifier key is active etc - static OverlayCallback functionOverlay[] = {drawFunctionOverlay}; - ui->setOverlays(functionOverlay, sizeof(functionOverlay) / sizeof(functionOverlay[0])); + static OverlayCallback overlays[] = { + drawFunctionOverlay, + drawCustomFrameIcons + }; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list // just changed) diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index a66593d9f..8d101108f 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -181,9 +181,10 @@ class Screen : public concurrency::OSThread public: explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); - + size_t frameCount = 0; // Total number of active frames ~Screen(); + std::vector indicatorIcons; // Per-frame custom icon pointers Screen(const Screen &) = delete; Screen &operator=(const Screen &) = delete; diff --git a/src/graphics/images.h b/src/graphics/images.h index 1387cc60c..af459faa6 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -256,6 +256,148 @@ static const unsigned char mail[] PROGMEM = { 0b11111111, 0b00 // Bottom line }; +// πŸ“¬ Mail / Message +const uint8_t icon_mail[] PROGMEM = { + 0b11111111, // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ top border + 0b10000001, // β–ˆ β–ˆ sides + 0b11000011, // β–ˆβ–ˆ β–ˆβ–ˆ diagonal + 0b10100101, // β–ˆ β–ˆ β–ˆ β–ˆ inner M + 0b10011001, // β–ˆ β–ˆβ–ˆ β–ˆ inner M + 0b10000001, // β–ˆ β–ˆ sides + 0b11111111, // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ bottom + 0b00000000 // (padding) +}; + +// πŸ“ GPS Screen / Location Pin +const uint8_t icon_compass[] PROGMEM = { + 0b00011000, // β–ˆβ–ˆ + 0b00111100, // β–ˆβ–ˆβ–ˆβ–ˆ + 0b01100110, // β–ˆβ–ˆ β–ˆβ–ˆ + 0b01000010, // β–ˆ β–ˆ + 0b01000010, // β–ˆ β–ˆ + 0b00111100, // β–ˆβ–ˆβ–ˆβ–ˆ + 0b00011000, // β–ˆβ–ˆ + 0b00010000 // β–ˆ +}; + +const uint8_t icon_radio[] PROGMEM = { + 0b00111000, // β–‘β–ˆβ–ˆβ–ˆβ–‘ + 0b01000100, // β–ˆβ–‘β–‘β–‘β–ˆ + 0b10000010, // β–ˆβ–‘β–‘β–‘β–‘β–ˆ + 0b00010000, // β–‘β–‘β–ˆβ–‘ + 0b00010000, // β–‘β–‘β–ˆβ–‘ + 0b00111000, // β–‘β–ˆβ–ˆβ–ˆβ–‘ + 0b01111100, // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ + 0b00000000 // β–‘β–‘β–‘β–‘β–‘ +}; + +// πŸͺ™ Memory Drum Icon (Barrel shape with cuts on the sides) +const uint8_t icon_memory[] PROGMEM = { + 0b00111100, // β–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘ + 0b01111110, // β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘ + 0b11100111, // β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ + 0b11100111, // β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ + 0b11100111, // β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ + 0b11100111, // β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ + 0b01111110, // β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘ + 0b00111100 // β–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘ +}; + +// 🌐 Wi-Fi +const uint8_t icon_wifi[] PROGMEM = { + 0b00000000, + 0b00011000, + 0b00111100, + 0b01111110, + 0b11011011, + 0b00011000, + 0b00011000, + 0b00000000 +}; + +// πŸ“„ Paper/List Icon (for DynamicNodeListScreen) +const uint8_t icon_nodes[] PROGMEM = { + 0b11111111, // Top edge of paper + 0b10000001, // Left & right margin + 0b10101001, // β€’β€’β€’ line + 0b10000001, // + 0b10101001, // β€’β€’β€’ line + 0b10000001, // + 0b11111111, // Bottom edge + 0b00000000 // +}; + +// ➀ Chevron Triangle Arrow Icon (8x8) +const uint8_t icon_list[] PROGMEM = { + 0b00011000, // β–‘β–‘β–ˆβ–ˆβ–‘β–‘ + 0b00011100, // β–‘β–‘β–ˆβ–ˆβ–ˆβ–‘ + 0b00011110, // β–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆ + 0b11111111, // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ + 0b00011110, // β–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆ + 0b00011100, // β–‘β–‘β–ˆβ–ˆβ–ˆβ–‘ + 0b00011000, // β–‘β–‘β–ˆβ–ˆβ–‘β–‘ + 0b00000000 // β–‘β–‘β–‘β–‘β–‘β–‘ +}; + +// πŸ“Ά Signal Bars Icon (left to right, small to large with spacing) +const uint8_t icon_signal[] PROGMEM = { + 0b00000000, // β–‘β–‘β–‘β–‘β–‘β–‘β–‘ + 0b10000000, // β–‘β–‘β–‘β–‘β–‘β–‘β–‘ + 0b10100000, // β–‘β–‘β–‘β–‘β–ˆβ–‘β–ˆ + 0b10100000, // β–‘β–‘β–‘β–‘β–ˆβ–‘β–ˆ + 0b10101000, // β–‘β–‘β–ˆβ–‘β–ˆβ–‘β–ˆ + 0b10101000, // β–‘β–‘β–ˆβ–‘β–ˆβ–‘β–ˆ + 0b10101010, // β–ˆβ–‘β–ˆβ–‘β–ˆβ–‘β–ˆ + 0b11111111 // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ +}; + +// ↔️ Distance / Measurement Icon (double-ended arrow) +const uint8_t icon_distance[] PROGMEM = { + 0b00000000, // β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ + 0b10000001, // β–ˆβ–‘β–‘β–‘β–‘β–‘β–ˆ arrowheads + 0b01000010, // β–‘β–ˆβ–‘β–‘β–‘β–ˆβ–‘ + 0b00100100, // β–‘β–‘β–ˆβ–‘β–ˆβ–‘β–‘ + 0b00011000, // β–‘β–‘β–‘β–ˆβ–ˆβ–‘β–‘ center + 0b00100100, // β–‘β–‘β–ˆβ–‘β–ˆβ–‘β–‘ + 0b01000010, // β–‘β–ˆβ–‘β–‘β–‘β–ˆβ–‘ + 0b10000001 // β–ˆβ–‘β–‘β–‘β–‘β–‘β–ˆ +}; + +// ⚠️ Error / Fault +const uint8_t icon_error[] PROGMEM = { + 0b00011000, // β–‘β–‘β–‘β–ˆβ–ˆβ–‘β–‘β–‘ + 0b00011000, // β–‘β–‘β–‘β–ˆβ–ˆβ–‘β–‘β–‘ + 0b00011000, // β–‘β–‘β–‘β–ˆβ–ˆβ–‘β–‘β–‘ + 0b00011000, // β–‘β–‘β–‘β–ˆβ–ˆβ–‘β–‘β–‘ + 0b00000000, // β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ + 0b00011000, // β–‘β–‘β–‘β–ˆβ–ˆβ–‘β–‘β–‘ + 0b00000000, // β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ + 0b00000000 // β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ +}; + +// 🏠 Optimized Home Icon (8x8) +const uint8_t icon_home[] PROGMEM = { + 0b00011000, // β–ˆβ–ˆ + 0b00111100, // β–ˆβ–ˆβ–ˆβ–ˆ + 0b01111110, // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ + 0b11111111, // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ + 0b11000011, // β–ˆβ–ˆ β–ˆβ–ˆ + 0b11011011, // β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ + 0b11011011, // β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ + 0b11111111 // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ +}; + +// πŸ”§ Generic module (gear-like shape) +const uint8_t icon_module[] PROGMEM = { + 0b00011000, // β–‘β–‘β–‘β–ˆβ–ˆβ–‘β–‘β–‘ + 0b00111100, // β–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘ + 0b01111110, // β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘ + 0b11011011, // β–ˆβ–ˆβ–‘β–ˆβ–ˆβ–‘β–ˆβ–ˆ + 0b11011011, // β–ˆβ–ˆβ–‘β–ˆβ–ˆβ–‘β–ˆβ–ˆ + 0b01111110, // β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘ + 0b00111100, // β–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘ + 0b00011000 // β–‘β–‘β–‘β–ˆβ–ˆβ–‘β–‘β–‘ +}; #endif #include "img/icon.xbm" From 66a06230c41566dcd6b67e311686052dffe9c511 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 5 May 2025 20:10:00 -0400 Subject: [PATCH 083/265] New Icons for navigation bar - Submitted by JasonP --- src/graphics/Screen.cpp | 123 +++++++++++++++++++++++++--------------- src/graphics/images.h | 117 ++++++++++++++++++-------------------- 2 files changed, 132 insertions(+), 108 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index b11239773..f2eb0956f 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -135,6 +135,20 @@ static bool heartbeat = false; #include "graphics/ScreenFonts.h" #include +void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display) +{ + for (int row = 0; row < height; row++) { + uint8_t rowMask = (1 << row); + for (int col = 0; col < width; col++) { + uint8_t colData = pgm_read_byte(&bitmapXBM[col]); + if (colData & rowMask) { + // Note: rows become X, columns become Y after transpose + display->fillRect(x + row * 2, y + col * 2, 2, 2); + } + } + } +} + #define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) // Check if the display can render a string (detect special chars; emoji) @@ -3138,8 +3152,10 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) } static int8_t lastFrameIndex = -1; static uint32_t lastFrameChangeTime = 0; -constexpr uint32_t ICON_DISPLAY_DURATION_MS = 1000; +// constexpr uint32_t ICON_DISPLAY_DURATION_MS = 1250; +constexpr uint32_t ICON_DISPLAY_DURATION_MS = 10250; +// Bottom navigation icons void drawCustomFrameIcons(OLEDDisplay *display, OLEDDisplayUiState *state) { int currentFrame = state->currentFrame; @@ -3151,37 +3167,61 @@ void drawCustomFrameIcons(OLEDDisplay *display, OLEDDisplayUiState *state) } // Only show bar briefly after switching frames - if (millis() - lastFrameChangeTime > ICON_DISPLAY_DURATION_MS) return; + if (millis() - lastFrameChangeTime > ICON_DISPLAY_DURATION_MS) + return; - const int iconSize = 8; - const int spacing = 2; - size_t totalIcons = screen->indicatorIcons.size(); - if (totalIcons == 0) return; + const bool useBigIcons = (SCREEN_WIDTH > 128); + const int iconSize = useBigIcons ? 16 : 8; + const int spacing = useBigIcons ? 8 : 4; + const int bigOffset = useBigIcons ? 1 : 0; - int totalWidth = totalIcons * iconSize + (totalIcons - 1) * spacing; - int xStart = (SCREEN_WIDTH - totalWidth) / 2; - int y = SCREEN_HEIGHT - iconSize - 1; + const size_t totalIcons = screen->indicatorIcons.size(); + if (totalIcons == 0) + return; - // Clear background under icon bar to avoid overlaps + const int totalWidth = totalIcons * iconSize + (totalIcons - 1) * spacing; + const int xStart = (SCREEN_WIDTH - totalWidth) / 2; + const int y = SCREEN_HEIGHT - iconSize - 1; + + // Pre-calculate bounding rect + const int rectX = xStart - 2 - bigOffset; + const int rectWidth = totalWidth + 4 + (bigOffset * 2); + const int rectHeight = iconSize + 6; + + // Clear background and draw border display->setColor(BLACK); - display->fillRect(xStart - 1, y - 2, totalWidth + 2, iconSize + 4); + display->fillRect(rectX + 1, y - 2, rectWidth - 2, rectHeight - 2); display->setColor(WHITE); + display->drawRect(rectX, y - 2, rectWidth, rectHeight); + // Icon drawing loop for (size_t i = 0; i < totalIcons; ++i) { - const uint8_t* icon = screen->indicatorIcons[i]; - int x = xStart + i * (iconSize + spacing); + const uint8_t *icon = screen->indicatorIcons[i]; + const int x = xStart + i * (iconSize + spacing); + const bool isActive = (i == static_cast(currentFrame)); - if (i == static_cast(currentFrame)) { - // Draw white box and invert icon for visibility + if (isActive) { display->setColor(WHITE); - display->fillRect(x - 1, y - 1, iconSize + 2, iconSize + 2); + display->fillRect(x - 2, y - 2, iconSize + 4, iconSize + 4); display->setColor(BLACK); - display->drawXbm(x, y, iconSize, iconSize, icon); - display->setColor(WHITE); + } + + if (useBigIcons) { + drawScaledXBitmap16x16(x, y, 8, 8, icon, display); } else { display->drawXbm(x, y, iconSize, iconSize, icon); } + + if (isActive) { + display->setColor(WHITE); + } } + + // Knock the corners off the square + display->setColor(BLACK); + display->drawRect(rectX, y - 2, 1, 1); + display->drawRect(rectX + rectWidth - 1, y - 2, 1, 1); + display->setColor(WHITE); } void Screen::setup() @@ -3209,17 +3249,17 @@ void Screen::setup() displayWidth = dispdev->width(); displayHeight = dispdev->height(); - ui->setTimePerTransition(0); // Disable animation delays - ui->setIndicatorPosition(BOTTOM); // Not used (indicators disabled below) - ui->setIndicatorDirection(LEFT_RIGHT); // Not used (indicators disabled below) - ui->setFrameAnimation(SLIDE_LEFT); // Used only when indicators are active - ui->disableAllIndicators(); // Disable page indicator dots - ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance + ui->setTimePerTransition(0); // Disable animation delays + ui->setIndicatorPosition(BOTTOM); // Not used (indicators disabled below) + ui->setIndicatorDirection(LEFT_RIGHT); // Not used (indicators disabled below) + ui->setFrameAnimation(SLIDE_LEFT); // Used only when indicators are active + ui->disableAllIndicators(); // Disable page indicator dots + ui->getUiState()->userData = this; // Allow static callbacks to access Screen instance // === Set custom overlay callbacks === static OverlayCallback overlays[] = { - drawFunctionOverlay, // For mute/buzzer modifiers etc. - drawCustomFrameIcons // Custom indicator icons for each frame + drawFunctionOverlay, // For mute/buzzer modifiers etc. + drawCustomFrameIcons // Custom indicator icons for each frame }; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); @@ -3254,14 +3294,14 @@ void Screen::setup() dispdev->mirrorScreen(); #else if (!config.display.flip_screen) { - #if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ - defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) +#if defined(ST7701_CS) || defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \ + defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) static_cast(dispdev)->flipScreenVertically(); - #elif defined(USE_ST7789) +#elif defined(USE_ST7789) static_cast(dispdev)->flipScreenVertically(); - #else +#else dispdev->flipScreenVertically(); - #endif +#endif } #endif @@ -3271,7 +3311,7 @@ void Screen::setup() snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); #if ARCH_PORTDUINO - handleSetOn(false); // Ensure proper init for Arduino targets + handleSetOn(false); // Ensure proper init for Arduino targets #endif // === Turn on display and trigger first draw === @@ -3285,17 +3325,13 @@ void Screen::setup() // === Optional touchscreen support === #if ARCH_PORTDUINO && !HAS_TFT if (settingsMap[touchscreenModule]) { - touchScreenImpl1 = new TouchScreenImpl1( - dispdev->getWidth(), dispdev->getHeight(), - static_cast(dispdev)->getTouch - ); + touchScreenImpl1 = + new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); touchScreenImpl1->init(); } #elif HAS_TOUCHSCREEN - touchScreenImpl1 = new TouchScreenImpl1( - dispdev->getWidth(), dispdev->getHeight(), - static_cast(dispdev)->getTouch - ); + touchScreenImpl1 = + new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); touchScreenImpl1->init(); #endif @@ -3728,7 +3764,7 @@ void Screen::setFrames(FrameFocus focus) } #endif - fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE + fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE this->frameCount = numframes; // βœ… Save frame count for use in custom overlay LOG_DEBUG("Finished build frames. numframes: %d", numframes); @@ -3736,10 +3772,7 @@ void Screen::setFrames(FrameFocus focus) ui->disableAllIndicators(); // Add function overlay here. This can show when notifications muted, modifier key is active etc - static OverlayCallback overlays[] = { - drawFunctionOverlay, - drawCustomFrameIcons - }; + static OverlayCallback overlays[] = {drawFunctionOverlay, drawCustomFrameIcons}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list diff --git a/src/graphics/images.h b/src/graphics/images.h index af459faa6..3f7986048 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -258,85 +258,76 @@ static const unsigned char mail[] PROGMEM = { // πŸ“¬ Mail / Message const uint8_t icon_mail[] PROGMEM = { + 0b00000000, // (padding) 0b11111111, // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ top border 0b10000001, // β–ˆ β–ˆ sides 0b11000011, // β–ˆβ–ˆ β–ˆβ–ˆ diagonal 0b10100101, // β–ˆ β–ˆ β–ˆ β–ˆ inner M 0b10011001, // β–ˆ β–ˆβ–ˆ β–ˆ inner M 0b10000001, // β–ˆ β–ˆ sides - 0b11111111, // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ bottom - 0b00000000 // (padding) + 0b11111111 // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ bottom }; // πŸ“ GPS Screen / Location Pin -const uint8_t icon_compass[] PROGMEM = { - 0b00011000, // β–ˆβ–ˆ - 0b00111100, // β–ˆβ–ˆβ–ˆβ–ˆ - 0b01100110, // β–ˆβ–ˆ β–ˆβ–ˆ - 0b01000010, // β–ˆ β–ˆ - 0b01000010, // β–ˆ β–ˆ - 0b00111100, // β–ˆβ–ˆβ–ˆβ–ˆ - 0b00011000, // β–ˆβ–ˆ - 0b00010000 // β–ˆ +const unsigned char icon_compass[] PROGMEM = { + 0x3C, // Row 0: ..####.. + 0x52, // Row 1: .#..#.#. + 0x91, // Row 2: #...#..# + 0x91, // Row 3: #...#..# + 0x91, // Row 4: #...#..# + 0x81, // Row 5: #......# + 0x42, // Row 6: .#....#. + 0x3C // Row 7: ..####.. }; const uint8_t icon_radio[] PROGMEM = { - 0b00111000, // β–‘β–ˆβ–ˆβ–ˆβ–‘ - 0b01000100, // β–ˆβ–‘β–‘β–‘β–ˆ - 0b10000010, // β–ˆβ–‘β–‘β–‘β–‘β–ˆ - 0b00010000, // β–‘β–‘β–ˆβ–‘ - 0b00010000, // β–‘β–‘β–ˆβ–‘ - 0b00111000, // β–‘β–ˆβ–ˆβ–ˆβ–‘ - 0b01111100, // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ - 0b00000000 // β–‘β–‘β–‘β–‘β–‘ + 0x0F, // Row 0: ####.... + 0x10, // Row 1: ....#... + 0x27, // Row 2: ###..#.. + 0x48, // Row 3: ...#..#. + 0x93, // Row 4: ##..#..# + 0xA4, // Row 5: ..#..#.# + 0xA8, // Row 6: ...#.#.# + 0xA9 // Row 7: #..#.#.# }; -// πŸͺ™ Memory Drum Icon (Barrel shape with cuts on the sides) +// πŸͺ™ Memory Icon const uint8_t icon_memory[] PROGMEM = { - 0b00111100, // β–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘ - 0b01111110, // β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘ - 0b11100111, // β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ - 0b11100111, // β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ - 0b11100111, // β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ - 0b11100111, // β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ - 0b01111110, // β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘ - 0b00111100 // β–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘ + 0x24, // Row 0: ..#..#.. + 0x3C, // Row 1: ..####.. + 0xC3, // Row 2: ##....## + 0x5A, // Row 3: .#.##.#. + 0x5A, // Row 4: .#.##.#. + 0xC3, // Row 5: ##....## + 0x3C, // Row 6: ..####.. + 0x24 // Row 7: ..#..#.. }; // 🌐 Wi-Fi -const uint8_t icon_wifi[] PROGMEM = { - 0b00000000, - 0b00011000, - 0b00111100, - 0b01111110, - 0b11011011, - 0b00011000, - 0b00011000, - 0b00000000 -}; +const uint8_t icon_wifi[] PROGMEM = {0b00000000, 0b00011000, 0b00111100, 0b01111110, + 0b11011011, 0b00011000, 0b00011000, 0b00000000}; -// πŸ“„ Paper/List Icon (for DynamicNodeListScreen) const uint8_t icon_nodes[] PROGMEM = { - 0b11111111, // Top edge of paper - 0b10000001, // Left & right margin - 0b10101001, // β€’β€’β€’ line - 0b10000001, // - 0b10101001, // β€’β€’β€’ line - 0b10000001, // - 0b11111111, // Bottom edge - 0b00000000 // + 0xF9, // Row 0 #..####### + 0x00, // Row 1 + 0xF9, // Row 2 #..####### + 0x00, // Row 3 + 0xF9, // Row 4 #..####### + 0x00, // Row 5 + 0xF9, // Row 6 #..####### + 0x00 // Row 7 }; // ➀ Chevron Triangle Arrow Icon (8x8) const uint8_t icon_list[] PROGMEM = { - 0b00011000, // β–‘β–‘β–ˆβ–ˆβ–‘β–‘ - 0b00011100, // β–‘β–‘β–ˆβ–ˆβ–ˆβ–‘ - 0b00011110, // β–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆ - 0b11111111, // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ - 0b00011110, // β–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆ - 0b00011100, // β–‘β–‘β–ˆβ–ˆβ–ˆβ–‘ - 0b00011000, // β–‘β–‘β–ˆβ–ˆβ–‘β–‘ - 0b00000000 // β–‘β–‘β–‘β–‘β–‘β–‘ + 0x10, // Row 0: ...#.... + 0x10, // Row 1: ...#.... + 0x38, // Row 2: ..###... + 0x38, // Row 3: ..###... + 0x7C, // Row 4: .#####.. + 0x6C, // Row 5: .##.##.. + 0xC6, // Row 6: ##...##. + 0x82 // Row 7: #.....#. }; // πŸ“Ά Signal Bars Icon (left to right, small to large with spacing) @@ -377,14 +368,14 @@ const uint8_t icon_error[] PROGMEM = { // 🏠 Optimized Home Icon (8x8) const uint8_t icon_home[] PROGMEM = { - 0b00011000, // β–ˆβ–ˆ - 0b00111100, // β–ˆβ–ˆβ–ˆβ–ˆ - 0b01111110, // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ - 0b11111111, // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ - 0b11000011, // β–ˆβ–ˆ β–ˆβ–ˆ - 0b11011011, // β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ - 0b11011011, // β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ - 0b11111111 // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ + 0b00011000, // β–ˆβ–ˆ + 0b00111100, // β–ˆβ–ˆβ–ˆβ–ˆ + 0b01111110, // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ + 0b11111111, // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ + 0b11000011, // β–ˆβ–ˆ β–ˆβ–ˆ + 0b11011011, // β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ + 0b11011011, // β–ˆβ–ˆ β–ˆβ–ˆ β–ˆβ–ˆ + 0b11111111 // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ }; // πŸ”§ Generic module (gear-like shape) From 849e06497a6589eac9f37139a036a34da031b1b7 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 5 May 2025 21:27:20 -0400 Subject: [PATCH 084/265] Telemetry Screen Module --- src/graphics/Screen.cpp | 215 +---------------- src/graphics/SharedUIDisplay.cpp | 164 +++++++++++++ src/graphics/SharedUIDisplay.h | 37 +++ .../Telemetry/EnvironmentTelemetry.cpp | 216 ++++++++++-------- src/modules/Telemetry/PowerTelemetry.cpp | 61 +++-- 5 files changed, 369 insertions(+), 324 deletions(-) create mode 100644 src/graphics/SharedUIDisplay.cpp create mode 100644 src/graphics/SharedUIDisplay.h diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index f2eb0956f..ad121610a 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -52,6 +52,7 @@ along with this program. If not, see . #include "modules/WaypointModule.h" #include "sleep.h" #include "target_specific.h" +#include "graphics/SharedUIDisplay.h" #if HAS_WIFI && !defined(ARCH_PORTDUINO) #include "mesh/wifi/WiFiAPClient.h" @@ -115,23 +116,6 @@ GeoCoord geoCoord; static bool heartbeat = false; #endif -// Quick access to screen dimensions from static drawing functions -// DEPRECATED. To-do: move static functions inside Screen class -#define SCREEN_WIDTH display->getWidth() -#define SCREEN_HEIGHT display->getHeight() - -// Pre-defined lines; this is intended to be used AFTER the common header -#define compactFirstLine ((FONT_HEIGHT_SMALL - 1) * 1) -#define compactSecondLine ((FONT_HEIGHT_SMALL - 1) * 2) - 2 -#define compactThirdLine ((FONT_HEIGHT_SMALL - 1) * 3) - 4 -#define compactFourthLine ((FONT_HEIGHT_SMALL - 1) * 4) - 6 -#define compactFifthLine ((FONT_HEIGHT_SMALL - 1) * 5) - 8 - -#define standardFirstLine (FONT_HEIGHT_SMALL + 1) * 1 -#define standardSecondLine (FONT_HEIGHT_SMALL + 1) * 2 -#define standardThirdLine (FONT_HEIGHT_SMALL + 1) * 3 -#define standardFourthLine (FONT_HEIGHT_SMALL + 1) * 4 - #include "graphics/ScreenFonts.h" #include @@ -995,188 +979,6 @@ bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int return validCached; } // ********************************* -// *Rounding Header when inverted * -// ********************************* -void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r) -{ - display->fillRect(x + r, y, w - 2 * r, h); - display->fillRect(x, y + r, r, h - 2 * r); - display->fillRect(x + w - r, y + r, r, h - 2 * r); - display->fillCircle(x + r + 1, y + r, r); - display->fillCircle(x + w - r - 1, y + r, r); - display->fillCircle(x + r + 1, y + h - r - 1, r); - display->fillCircle(x + w - r - 1, y + h - r - 1, r); -} -bool isBoltVisible = true; -uint32_t lastBlink = 0; -const uint32_t blinkInterval = 500; -static uint32_t lastMailBlink = 0; -static bool isMailIconVisible = true; -constexpr uint32_t mailBlinkInterval = 500; - -// *********************** -// * Common Header * -// *********************** -void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) -{ - constexpr int HEADER_OFFSET_Y = 1; - y += HEADER_OFFSET_Y; - - const bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); - const bool isBold = config.display.heading_bold; - const int xOffset = 4; - const int highlightHeight = FONT_HEIGHT_SMALL - 1; - - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - // === Background highlight === - if (isInverted) { - drawRoundedHighlight(display, x, y, SCREEN_WIDTH, highlightHeight, 2); - display->setColor(BLACK); - } - - // === Battery Vertical and Horizontal === - int chargePercent = powerStatus->getBatteryChargePercent(); - bool isCharging = powerStatus->getIsCharging() == OptionalBool::OptTrue; - uint32_t now = millis(); - if (isCharging && now - lastBlink > blinkInterval) { - isBoltVisible = !isBoltVisible; - lastBlink = now; - } - - // Hybrid condition: wide screen AND landscape layout - bool useHorizontalBattery = (SCREEN_WIDTH > 128 && SCREEN_WIDTH > SCREEN_HEIGHT); - - if (useHorizontalBattery) { - // === Horizontal battery === - int batteryX = 2; - int batteryY = HEADER_OFFSET_Y + 2; - - display->drawXbm(batteryX, batteryY, 29, 15, batteryBitmap_h); - - if (isCharging && isBoltVisible) { - display->drawXbm(batteryX + 9, batteryY + 1, 9, 13, lightning_bolt_h); - } else if (isCharging && !isBoltVisible) { - display->drawXbm(batteryX + 8, batteryY, 12, 15, batteryBitmap_sidegaps_h); - } else { - display->drawXbm(batteryX + 8, batteryY, 12, 15, batteryBitmap_sidegaps_h); - int fillWidth = 24 * chargePercent / 100; - int fillX = batteryX + fillWidth; - display->fillRect(batteryX + 1, batteryY + 1, fillX, 13); - } - } else { - // === Vertical battery === - int batteryX = 1; - int batteryY = HEADER_OFFSET_Y + 1; -#ifdef USE_EINK - batteryY = batteryY + 2; -#endif - - display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v); - - if (isCharging && isBoltVisible) { - display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v); - } else if (isCharging && !isBoltVisible) { - display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v); - } else { - display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v); - int fillHeight = 8 * chargePercent / 100; - int fillY = batteryY - fillHeight; - display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight); - } - } - - // === Text baseline === - const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - - // === Battery % Text === - char chargeStr[4]; - snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent); - - int chargeNumWidth = display->getStringWidth(chargeStr); - const int batteryOffset = useHorizontalBattery ? 28 : 6; -#ifdef USE_EINK - const int percentX = x + xOffset + batteryOffset - 2; -#else - const int percentX = x + xOffset + batteryOffset; -#endif - - display->drawString(percentX, textY, chargeStr); - display->drawString(percentX + chargeNumWidth - 1, textY, "%"); - - if (isBold) { - display->drawString(percentX + 1, textY, chargeStr); - display->drawString(percentX + chargeNumWidth, textY, "%"); - } - // === Time string (right-aligned) === - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); - if (rtc_sec > 0) { - long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; - int hour = hms / SEC_PER_HOUR; - int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - - char timeStr[10]; - - snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); - if (config.display.use_12h_clock) { - bool isPM = hour >= 12; - hour = hour % 12; - if (hour == 0) - hour = 12; - snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); - } - - int timeStrWidth = display->getStringWidth(timeStr); - int timeX = SCREEN_WIDTH - xOffset - timeStrWidth + 4; // time to the right by 4 - - // Mail icon next to time (drawn as 'M' in a tight square) - if (hasUnreadMessage) { - if (now - lastMailBlink > mailBlinkInterval) { - isMailIconVisible = !isMailIconVisible; - lastMailBlink = now; - } - - if (isMailIconVisible) { - const bool isWide = useHorizontalBattery; - - if (isWide) { - // Dimensions for the wide mail icon - const int iconW = 16; - const int iconH = 12; - - const int iconX = timeX - iconW - 3; - const int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; - - // Draw envelope rectangle - display->drawRect(iconX, iconY, iconW, iconH); - - // Define envelope corners and center - const int leftX = iconX + 1; - const int rightX = iconX + iconW - 2; - const int topY = iconY + 1; - const int bottomY = iconY + iconH - 2; - const int centerX = iconX + iconW / 2; - const int peakY = bottomY - 1; - - // Draw "M" diagonals - display->drawLine(leftX, topY, centerX, peakY); - display->drawLine(rightX, topY, centerX, peakY); - } else { - // Small icon for non-wide screens - const int iconX = timeX - mail_width; - const int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - display->drawXbm(iconX, iconY, mail_width, mail_height, mail); - } - } - } - display->drawString(timeX, textY, timeStr); - if (isBold) - display->drawString(timeX - 1, textY, timeStr); - } - - display->setColor(WHITE); -} // **************************** // * Text Message Screen * @@ -1767,7 +1569,7 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ display->setFont(FONT_SMALL); // === Header === - drawCommonHeader(display, x, y); + graphics::drawCommonHeader(display, x, y); // === Reset color in case inverted mode left it BLACK === display->setColor(WHITE); @@ -2081,7 +1883,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t display->clear(); // === Draw the battery/time header === - drawCommonHeader(display, x, y); + graphics::drawCommonHeader(display, x, y); // === Manually draw the centered title within the header === const int highlightHeight = COMMON_HEADER_HEIGHT; @@ -2502,7 +2304,7 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i display->setFont(FONT_SMALL); // === Header === - drawCommonHeader(display, x, y); + graphics::drawCommonHeader(display, x, y); // === Content below header === @@ -2599,7 +2401,7 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int display->setFont(FONT_SMALL); // === Header === - drawCommonHeader(display, x, y); + graphics::drawCommonHeader(display, x, y); // === Draw title (aligned with header baseline) === const int highlightHeight = FONT_HEIGHT_SMALL - 1; @@ -2720,7 +2522,7 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat display->setFont(FONT_SMALL); // === Header === - drawCommonHeader(display, x, y); + graphics::drawCommonHeader(display, x, y); // === Draw title === const int highlightHeight = FONT_HEIGHT_SMALL - 1; @@ -2850,7 +2652,7 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in display->setTextAlignment(TEXT_ALIGN_LEFT); // === Header === - drawCommonHeader(display, x, y); + graphics::drawCommonHeader(display, x, y); // === Draw title === const int highlightHeight = FONT_HEIGHT_SMALL - 1; @@ -3152,8 +2954,7 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) } static int8_t lastFrameIndex = -1; static uint32_t lastFrameChangeTime = 0; -// constexpr uint32_t ICON_DISPLAY_DURATION_MS = 1250; -constexpr uint32_t ICON_DISPLAY_DURATION_MS = 10250; +constexpr uint32_t ICON_DISPLAY_DURATION_MS = 1250; // Bottom navigation icons void drawCustomFrameIcons(OLEDDisplay *display, OLEDDisplayUiState *state) diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp new file mode 100644 index 000000000..b6a0a4d46 --- /dev/null +++ b/src/graphics/SharedUIDisplay.cpp @@ -0,0 +1,164 @@ +#include "graphics/SharedUIDisplay.h" +#include "graphics/ScreenFonts.h" +#include "main.h" +#include "power.h" +#include "meshtastic/config.pb.h" +#include "RTC.h" +#include +#include + +namespace graphics { + +// === Shared External State === +bool hasUnreadMessage = false; + +// === Internal State === +bool isBoltVisibleShared = true; +uint32_t lastBlinkShared = 0; +bool isMailIconVisible = true; +uint32_t lastMailBlink = 0; + +// ********************************* +// * Rounded Header when inverted * +// ********************************* +void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r) +{ + display->fillRect(x + r, y, w - 2 * r, h); + display->fillRect(x, y + r, r, h - 2 * r); + display->fillRect(x + w - r, y + r, r, h - 2 * r); + display->fillCircle(x + r + 1, y + r, r); + display->fillCircle(x + w - r - 1, y + r, r); + display->fillCircle(x + r + 1, y + h - r - 1, r); + display->fillCircle(x + w - r - 1, y + h - r - 1, r); +} + +// ************************* +// * Common Header Drawing * +// ************************* +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) +{ + constexpr int HEADER_OFFSET_Y = 1; + y += HEADER_OFFSET_Y; + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + const int xOffset = 4; + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + const bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); + const bool isBold = config.display.heading_bold; + + const int screenW = display->getWidth(); + const int screenH = display->getHeight(); + + if (isInverted) { + drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2); + display->setColor(BLACK); + } + + int chargePercent = powerStatus->getBatteryChargePercent(); + bool isCharging = powerStatus->getIsCharging() == meshtastic::OptionalBool::OptTrue; + uint32_t now = millis(); + if (isCharging && now - lastBlinkShared > 500) { + isBoltVisibleShared = !isBoltVisibleShared; + lastBlinkShared = now; + } + + bool useHorizontalBattery = (screenW > 128 && screenW > screenH); + const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + + // === Battery Icons === + if (useHorizontalBattery) { + int batteryX = 2; + int batteryY = HEADER_OFFSET_Y + 2; + display->drawXbm(batteryX, batteryY, 29, 15, batteryBitmap_h); + if (isCharging && isBoltVisibleShared) { + display->drawXbm(batteryX + 9, batteryY + 1, 9, 13, lightning_bolt_h); + } else { + display->drawXbm(batteryX + 8, batteryY, 12, 15, batteryBitmap_sidegaps_h); + int fillWidth = 24 * chargePercent / 100; + display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 13); + } + } else { + int batteryX = 1; + int batteryY = HEADER_OFFSET_Y + 1; + #ifdef USE_EINK + batteryY += 2; + #endif + display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v); + if (isCharging && isBoltVisibleShared) { + display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v); + } else { + display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v); + int fillHeight = 8 * chargePercent / 100; + int fillY = batteryY - fillHeight; + display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight); + } + } + + // === Battery % Text === + char chargeStr[4]; + snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent); + int chargeNumWidth = display->getStringWidth(chargeStr); + const int batteryOffset = useHorizontalBattery ? 28 : 6; +#ifdef USE_EINK + const int percentX = x + xOffset + batteryOffset - 2; +#else + const int percentX = x + xOffset + batteryOffset; +#endif + display->drawString(percentX, textY, chargeStr); + display->drawString(percentX + chargeNumWidth - 1, textY, "%"); + if (isBold) { + display->drawString(percentX + 1, textY, chargeStr); + display->drawString(percentX + chargeNumWidth, textY, "%"); + } + + // === Time and Mail Icon === + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); + if (rtc_sec > 0) { + long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; + int hour = hms / SEC_PER_HOUR; + int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + + char timeStr[10]; + snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); + if (config.display.use_12h_clock) { + bool isPM = hour >= 12; + hour %= 12; + if (hour == 0) hour = 12; + snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); + } + + int timeStrWidth = display->getStringWidth(timeStr); + int timeX = screenW - xOffset - timeStrWidth + 4; + + if (hasUnreadMessage) { + if (now - lastMailBlink > 500) { + isMailIconVisible = !isMailIconVisible; + lastMailBlink = now; + } + + if (isMailIconVisible) { + if (useHorizontalBattery) { + int iconW = 16, iconH = 12; + int iconX = timeX - iconW - 3; + int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; + display->drawRect(iconX, iconY, iconW, iconH); + display->drawLine(iconX + 1, iconY + 1, iconX + iconW / 2, iconY + iconH - 2); + display->drawLine(iconX + iconW - 2, iconY + 1, iconX + iconW / 2, iconY + iconH - 2); + } else { + int iconX = timeX - mail_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + display->drawXbm(iconX, iconY, mail_width, mail_height, mail); + } + } + } + + display->drawString(timeX, textY, timeStr); + if (isBold) display->drawString(timeX - 1, textY, timeStr); + } + + display->setColor(WHITE); +} + +} // namespace graphics diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h new file mode 100644 index 000000000..99508efde --- /dev/null +++ b/src/graphics/SharedUIDisplay.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +namespace graphics { + +// ======================= +// Shared UI Helpers +// ======================= + +// Compact line layout +#define compactFirstLine ((FONT_HEIGHT_SMALL - 1) * 1) +#define compactSecondLine ((FONT_HEIGHT_SMALL - 1) * 2) - 2 +#define compactThirdLine ((FONT_HEIGHT_SMALL - 1) * 3) - 4 +#define compactFourthLine ((FONT_HEIGHT_SMALL - 1) * 4) - 6 +#define compactFifthLine ((FONT_HEIGHT_SMALL - 1) * 5) - 8 + +// Standard line layout +#define standardFirstLine (FONT_HEIGHT_SMALL + 1) * 1 +#define standardSecondLine (FONT_HEIGHT_SMALL + 1) * 2 +#define standardThirdLine (FONT_HEIGHT_SMALL + 1) * 3 +#define standardFourthLine (FONT_HEIGHT_SMALL + 1) * 4 + +// Quick screen access +#define SCREEN_WIDTH display->getWidth() +#define SCREEN_HEIGHT display->getHeight() + +// Shared state (declare inside namespace) +extern bool hasUnreadMessage; + +// Rounded highlight (used for inverted headers) +void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r); + +// Shared battery/time/mail header +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y); + +} // namespace graphics diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 32c660bbf..fd2d3b219 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -17,6 +17,8 @@ #include "target_specific.h" #include #include +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL // Sensors @@ -25,6 +27,9 @@ #include "Sensor/RCWL9620Sensor.h" #include "Sensor/nullSensor.h" +namespace graphics { + extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y); +} #if __has_include() #include "Sensor/AHT10.h" AHT10Sensor aht10Sensor; @@ -312,119 +317,130 @@ bool EnvironmentTelemetryModule::wantUIFrame() void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - display->setTextAlignment(TEXT_ALIGN_LEFT); + // === Setup display === + display->clear(); display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); - if (lastMeasurementPacket == nullptr) { - // If there's no valid packet, display "Environment" - display->drawString(x, y, "Environment"); - display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement"); + // Draw shared header bar (battery, time, etc.) + graphics::drawCommonHeader(display, x, y); + + // === Draw Title (Centered under header) === + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + const int titleY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const char *titleStr = "Environment"; + const int centerX = x + SCREEN_WIDTH / 2; + + // Use black text on white background if in inverted mode + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) + display->setColor(BLACK); + + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(centerX, titleY, titleStr); // Centered title + if (config.display.heading_bold) + display->drawString(centerX + 1, titleY, titleStr); // Bold effect via 1px offset + + // Restore text color & alignment + display->setColor(WHITE); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === Row spacing setup === + const int rowHeight = FONT_HEIGHT_SMALL - 4; + int currentY = compactFirstLine; + + // === Show "No Telemetry" if no data available === + if (!lastMeasurementPacket) { + display->drawString(x, currentY, "No Telemetry"); return; } - // Decode the last measurement packet - meshtastic_Telemetry lastMeasurement; - uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); - const char *lastSender = getSenderShortName(*lastMeasurementPacket); - + // Decode the telemetry message from the latest received packet const meshtastic_Data &p = lastMeasurementPacket->decoded; - if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) { - display->drawString(x, y, "Measurement Error"); - LOG_ERROR("Unable to decode last packet"); + meshtastic_Telemetry telemetry; + if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &telemetry)) { + display->drawString(x, currentY, "No Telemetry"); return; } - // Display "Env. From: ..." on its own - display->drawString(x, y, "Env. From: " + String(lastSender) + " (" + String(agoSecs) + "s)"); + const auto &m = telemetry.variant.environment_metrics; - // Prepare sensor data strings - String sensorData[10]; - int sensorCount = 0; + // Check if any telemetry field has valid data + bool hasAny = m.has_temperature || m.has_relative_humidity || m.barometric_pressure != 0 || + m.iaq != 0 || m.voltage != 0 || m.current != 0 || m.lux != 0 || + m.white_lux != 0 || m.weight != 0 || m.distance != 0 || m.radiation != 0; - if (lastMeasurement.variant.environment_metrics.has_temperature || - lastMeasurement.variant.environment_metrics.has_relative_humidity) { - String last_temp = String(lastMeasurement.variant.environment_metrics.temperature, 0) + "Β°C"; - if (moduleConfig.telemetry.environment_display_fahrenheit) { - last_temp = - String(UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.environment_metrics.temperature), 0) + "Β°F"; + if (!hasAny) { + display->drawString(x, currentY, "No Telemetry"); + return; + } + + // === First line: Show sender name + time since received (left), and first metric (right) === + const char *sender = getSenderShortName(*lastMeasurementPacket); + uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); + String agoStr = (agoSecs > 864000) ? "?" : + (agoSecs > 3600) ? String(agoSecs / 3600) + "h" : + (agoSecs > 60) ? String(agoSecs / 60) + "m" : + String(agoSecs) + "s"; + + String leftStr = String(sender) + " (" + agoStr + ")"; + display->drawString(x, currentY, leftStr); // Left side: who and when + + // === Collect sensor readings as label strings (no icons) === + std::vector entries; + + if (m.has_temperature) { + String tempStr = moduleConfig.telemetry.environment_display_fahrenheit + ? "Tmp: " + String(UnitConversions::CelsiusToFahrenheit(m.temperature), 1) + "Β°F" + : "Tmp: " + String(m.temperature, 1) + "Β°C"; + entries.push_back(tempStr); + } + if (m.has_relative_humidity) + entries.push_back("Hum: " + String(m.relative_humidity, 0) + "%"); + if (m.barometric_pressure != 0) + entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa"); + if (m.iaq != 0) { + String aqi = "IAQ: " + String(m.iaq); + if (m.iaq < 50) aqi += " (Good)"; + else if (m.iaq < 100) aqi += " (Moderate)"; + else aqi += " (!)"; + entries.push_back(aqi); + } + if (m.voltage != 0 || m.current != 0) + entries.push_back(String(m.voltage, 1) + "V / " + String(m.current, 0) + "mA"); + if (m.lux != 0) + entries.push_back("Light: " + String(m.lux, 0) + "lx"); + if (m.white_lux != 0) + entries.push_back("White: " + String(m.white_lux, 0) + "lx"); + if (m.weight != 0) + entries.push_back("Weight: " + String(m.weight, 0) + "kg"); + if (m.distance != 0) + entries.push_back("Level: " + String(m.distance, 0) + "mm"); + if (m.radiation != 0) + entries.push_back("Rad: " + String(m.radiation, 2) + " Β΅R/h"); + + // === Show first available metric on top-right of first line === + if (!entries.empty()) { + String valueStr = entries.front(); + int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr); + display->drawString(rightX, currentY, valueStr); + entries.erase(entries.begin()); // Remove from queue + } + + // === Advance to next line for remaining telemetry entries === + currentY += rowHeight; + + // === Draw remaining entries in 2-column format (left and right) === + for (size_t i = 0; i < entries.size(); i += 2) { + // Left column + display->drawString(x, currentY, entries[i]); + + // Right column if it exists + if (i + 1 < entries.size()) { + int rightX = SCREEN_WIDTH / 2; + display->drawString(rightX, currentY, entries[i + 1]); } - sensorData[sensorCount++] = - "Temp/Hum: " + last_temp + " / " + String(lastMeasurement.variant.environment_metrics.relative_humidity, 0) + "%"; - } - - if (lastMeasurement.variant.environment_metrics.barometric_pressure != 0) { - sensorData[sensorCount++] = - "Press: " + String(lastMeasurement.variant.environment_metrics.barometric_pressure, 0) + "hPA"; - } - - if (lastMeasurement.variant.environment_metrics.voltage != 0) { - sensorData[sensorCount++] = "Volt/Cur: " + String(lastMeasurement.variant.environment_metrics.voltage, 0) + "V / " + - String(lastMeasurement.variant.environment_metrics.current, 0) + "mA"; - } - - if (lastMeasurement.variant.environment_metrics.iaq != 0) { - sensorData[sensorCount++] = "IAQ: " + String(lastMeasurement.variant.environment_metrics.iaq); - } - - if (lastMeasurement.variant.environment_metrics.distance != 0) { - sensorData[sensorCount++] = "Water Level: " + String(lastMeasurement.variant.environment_metrics.distance, 0) + "mm"; - } - - if (lastMeasurement.variant.environment_metrics.weight != 0) { - sensorData[sensorCount++] = "Weight: " + String(lastMeasurement.variant.environment_metrics.weight, 0) + "kg"; - } - - if (lastMeasurement.variant.environment_metrics.radiation != 0) { - sensorData[sensorCount++] = "Rad: " + String(lastMeasurement.variant.environment_metrics.radiation, 2) + "Β΅R/h"; - } - - if (lastMeasurement.variant.environment_metrics.lux != 0) { - sensorData[sensorCount++] = "Illuminance: " + String(lastMeasurement.variant.environment_metrics.lux, 2) + "lx"; - } - - if (lastMeasurement.variant.environment_metrics.white_lux != 0) { - sensorData[sensorCount++] = "W_Lux: " + String(lastMeasurement.variant.environment_metrics.white_lux, 2) + "lx"; - } - - static int scrollOffset = 0; - static bool scrollingDown = true; - static uint32_t lastScrollTime = millis(); - - // Determine how many lines we can fit on display - // Calculated once only: display dimensions don't change during runtime. - static int maxLines = 0; - if (!maxLines) { - const int16_t paddingTop = _fontHeight(FONT_SMALL); // Heading text - const int16_t paddingBottom = 8; // Indicator dots - maxLines = (display->getHeight() - paddingTop - paddingBottom) / _fontHeight(FONT_SMALL); - assert(maxLines > 0); - } - - // Draw as many lines of data as we can fit - int linesToShow = min(maxLines, sensorCount); - for (int i = 0; i < linesToShow; i++) { - int index = (scrollOffset + i) % sensorCount; - display->drawString(x, y += _fontHeight(FONT_SMALL), sensorData[index]); - } - - // Only scroll if there are more than 3 sensor data lines - if (sensorCount > 3) { - // Update scroll offset every 5 seconds - if (millis() - lastScrollTime > 5000) { - if (scrollingDown) { - scrollOffset++; - if (scrollOffset + linesToShow >= sensorCount) { - scrollingDown = false; - } - } else { - scrollOffset--; - if (scrollOffset <= 0) { - scrollingDown = true; - } - } - lastScrollTime = millis(); - } + currentY += rowHeight; } } diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index 54ec90dae..5190a5947 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -14,6 +14,7 @@ #include "power.h" #include "sleep.h" #include "target_specific.h" +#include "graphics/SharedUIDisplay.h" #define FAILED_STATE_SENSOR_READ_MULTIPLIER 10 #define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true @@ -21,6 +22,10 @@ #include "graphics/ScreenFonts.h" #include +namespace graphics { + extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y); +} + int32_t PowerTelemetryModule::runOnce() { if (sleepOnNextExecution == true) { @@ -103,13 +108,33 @@ bool PowerTelemetryModule::wantUIFrame() void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); + graphics::drawCommonHeader(display, x, y); // Shared UI header + + // === Draw title (aligned with header baseline) === + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const char *titleStr = (SCREEN_WIDTH > 128) ? "Power Telem." : "Power"; + const int centerX = x + SCREEN_WIDTH / 2; + + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->setColor(BLACK); + } + + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(centerX, textY, titleStr); + if (config.display.heading_bold) { + display->drawString(centerX + 1, textY, titleStr); + } + display->setColor(WHITE); + display->setTextAlignment(TEXT_ALIGN_LEFT); + if (lastMeasurementPacket == nullptr) { - // In case of no valid packet, display "Power Telemetry", "No measurement" - display->drawString(x, y, "Power Telemetry"); - display->drawString(x, y += _fontHeight(FONT_SMALL), "No measurement"); + // In case of no valid packet, display "Power Telemetry", "No measurement" + display->drawString(x, compactFirstLine, "No measurement"); return; } @@ -120,29 +145,31 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s const meshtastic_Data &p = lastMeasurementPacket->decoded; if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) { - display->drawString(x, y, "Measurement Error"); + display->drawString(x, compactFirstLine, "Measurement Error"); LOG_ERROR("Unable to decode last packet"); return; } // Display "Pow. From: ..." - display->drawString(x, y, "Pow. From: " + String(lastSender) + "(" + String(agoSecs) + "s)"); + display->drawString(x, compactFirstLine, "Pow. From: " + String(lastSender) + " (" + String(agoSecs) + "s)"); // Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags - if (lastMeasurement.variant.power_metrics.has_ch1_voltage || lastMeasurement.variant.power_metrics.has_ch1_current) { - display->drawString(x, y += _fontHeight(FONT_SMALL), - "Ch1: " + String(lastMeasurement.variant.power_metrics.ch1_voltage, 2) + "V " + - String(lastMeasurement.variant.power_metrics.ch1_current, 0) + "mA"); + const auto &m = lastMeasurement.variant.power_metrics; + int lineY = compactSecondLine; + + auto drawLine = [&](const char *label, float voltage, float current) { + display->drawString(x, lineY, String(label) + ": " + String(voltage, 2) + "V " + String(current, 0) + "mA"); + lineY += _fontHeight(FONT_SMALL); + }; + + if (m.has_ch1_voltage || m.has_ch1_current) { + drawLine("Ch1", m.ch1_voltage, m.ch1_current); } - if (lastMeasurement.variant.power_metrics.has_ch2_voltage || lastMeasurement.variant.power_metrics.has_ch2_current) { - display->drawString(x, y += _fontHeight(FONT_SMALL), - "Ch2: " + String(lastMeasurement.variant.power_metrics.ch2_voltage, 2) + "V " + - String(lastMeasurement.variant.power_metrics.ch2_current, 0) + "mA"); + if (m.has_ch2_voltage || m.has_ch2_current) { + drawLine("Ch2", m.ch2_voltage, m.ch2_current); } - if (lastMeasurement.variant.power_metrics.has_ch3_voltage || lastMeasurement.variant.power_metrics.has_ch3_current) { - display->drawString(x, y += _fontHeight(FONT_SMALL), - "Ch3: " + String(lastMeasurement.variant.power_metrics.ch3_voltage, 2) + "V " + - String(lastMeasurement.variant.power_metrics.ch3_current, 0) + "mA"); + if (m.has_ch3_voltage || m.has_ch3_current) { + drawLine("Ch3", m.ch3_voltage, m.ch3_current); } } From eebf174735471c80d8198765573c18ccbbe9a969 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 5 May 2025 21:27:49 -0400 Subject: [PATCH 085/265] Update Screen.cpp --- src/graphics/Screen.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index ad121610a..d4b6436fd 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -158,7 +158,7 @@ static bool haveGlyphs(const char *str) // LOG_DEBUG("haveGlyphs=%d", have); return have; } -bool hasUnreadMessage = false; +extern bool hasUnreadMessage; /** * Draw the icon with extra info printed around the corners */ @@ -978,7 +978,6 @@ bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int validCached = true; return validCached; } -// ********************************* // **************************** // * Text Message Screen * From 3596ea20bcd555f8b9d44fbaa8103915cc9e4963 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 6 May 2025 00:04:47 -0400 Subject: [PATCH 086/265] Navigation bar peek on bottom --- src/graphics/Screen.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index d4b6436fd..129ba82de 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -38,6 +38,7 @@ along with this program. If not, see . #include "gps/GeoCoord.h" #include "gps/RTC.h" #include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" #include "graphics/images.h" #include "input/ScanAndSelect.h" #include "input/TouchScreenImpl1.h" @@ -52,7 +53,6 @@ along with this program. If not, see . #include "modules/WaypointModule.h" #include "sleep.h" #include "target_specific.h" -#include "graphics/SharedUIDisplay.h" #if HAS_WIFI && !defined(ARCH_PORTDUINO) #include "mesh/wifi/WiFiAPClient.h" @@ -2953,7 +2953,7 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) } static int8_t lastFrameIndex = -1; static uint32_t lastFrameChangeTime = 0; -constexpr uint32_t ICON_DISPLAY_DURATION_MS = 1250; +constexpr uint32_t ICON_DISPLAY_DURATION_MS = 1000; // Bottom navigation icons void drawCustomFrameIcons(OLEDDisplay *display, OLEDDisplayUiState *state) @@ -2966,10 +2966,6 @@ void drawCustomFrameIcons(OLEDDisplay *display, OLEDDisplayUiState *state) lastFrameChangeTime = millis(); } - // Only show bar briefly after switching frames - if (millis() - lastFrameChangeTime > ICON_DISPLAY_DURATION_MS) - return; - const bool useBigIcons = (SCREEN_WIDTH > 128); const int iconSize = useBigIcons ? 16 : 8; const int spacing = useBigIcons ? 8 : 4; @@ -2981,7 +2977,12 @@ void drawCustomFrameIcons(OLEDDisplay *display, OLEDDisplayUiState *state) const int totalWidth = totalIcons * iconSize + (totalIcons - 1) * spacing; const int xStart = (SCREEN_WIDTH - totalWidth) / 2; - const int y = SCREEN_HEIGHT - iconSize - 1; + + // Only show bar briefly after switching frames + int y = SCREEN_HEIGHT - iconSize - 1; + if (millis() - lastFrameChangeTime > ICON_DISPLAY_DURATION_MS) { + y = SCREEN_HEIGHT; + } // Pre-calculate bounding rect const int rectX = xStart - 2 - bigOffset; From 7bc473ed994c8f17d40bde6dbc3f64c9bfa46889 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 6 May 2025 01:07:29 -0400 Subject: [PATCH 087/265] Dismiss Memory and wifi screen --- src/graphics/Screen.cpp | 41 +++++++++++++++++++++++++++-------------- src/graphics/Screen.h | 24 ++++++++++++++++-------- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 129ba82de..81ab9258b 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -3537,8 +3537,11 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = drawLoRaFocused; indicatorIcons.push_back(icon_radio); - normalFrames[numframes++] = drawMemoryScreen; - indicatorIcons.push_back(icon_memory); + if (!dismissedFrames.memory) { + fsi.positions.memory = numframes; + normalFrames[numframes++] = drawMemoryScreen; + indicatorIcons.push_back(icon_memory); + } // then all the nodes // We only show a few nodes in our scrolling list - because meshes with many nodes would have too many screens @@ -3557,13 +3560,13 @@ void Screen::setFrames(FrameFocus focus) // fsi.positions.settings = numframes; // normalFrames[numframes++] = &Screen::drawDebugInfoSettingsTrampoline; - fsi.positions.wifi = numframes; -#if HAS_WIFI && !defined(ARCH_PORTDUINO) - if (isWifiAvailable()) { + #if HAS_WIFI && !defined(ARCH_PORTDUINO) + if (!dismissedFrames.wifi && isWifiAvailable()) { + fsi.positions.wifi = numframes; normalFrames[numframes++] = &Screen::drawDebugInfoWiFiTrampoline; indicatorIcons.push_back(icon_wifi); } -#endif + #endif fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE this->frameCount = numframes; // βœ… Save frame count for use in custom overlay @@ -3627,26 +3630,36 @@ void Screen::dismissCurrentFrame() uint8_t currentFrame = ui->getUiState()->currentFrame; bool dismissed = false; - // Only dismiss if the text message frame is currently valid and visible - if (framesetInfo.positions.textMessage != 255 && currentFrame == framesetInfo.positions.textMessage && - devicestate.has_rx_text_message) { + if (currentFrame == framesetInfo.positions.textMessage && devicestate.has_rx_text_message) { LOG_INFO("Dismiss Text Message"); devicestate.has_rx_text_message = false; - memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); // βœ… clear message + memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); + dismissedFrames.textMessage = true; dismissed = true; } - else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { LOG_DEBUG("Dismiss Waypoint"); devicestate.has_rx_waypoint = false; + dismissedFrames.waypoint = true; + dismissed = true; + } + else if (currentFrame == framesetInfo.positions.wifi) { + LOG_DEBUG("Dismiss WiFi Screen"); + dismissedFrames.wifi = true; + dismissed = true; + } + else if (currentFrame == framesetInfo.positions.memory) { + LOG_INFO("Dismiss Memory"); + dismissedFrames.memory = true; dismissed = true; } - // If we did make changes to dismiss, we now need to regenerate the frameset - if (dismissed) - setFrames(); + if (dismissed) { + setFrames(FOCUS_DEFAULT); // You could also use FOCUS_PRESERVE + } } + void Screen::handleStartFirmwareUpdateScreen() { LOG_DEBUG("Show firmware screen"); diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 8d101108f..2c8eff244 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -601,19 +601,27 @@ class Screen : public concurrency::OSThread // - Used to dismiss the currently shown frame (txt; waypoint) by CardKB combo struct FramesetInfo { struct FramePositions { - uint8_t fault = 0; - uint8_t textMessage = 0; - uint8_t waypoint = 0; - uint8_t focusedModule = 0; - uint8_t log = 0; - uint8_t settings = 0; - uint8_t wifi = 0; - uint8_t deviceFocused = 0; + uint8_t fault = 255; + uint8_t textMessage = 255; + uint8_t waypoint = 255; + uint8_t focusedModule = 255; + uint8_t log = 255; + uint8_t settings = 255; + uint8_t wifi = 255; + uint8_t deviceFocused = 255; + uint8_t memory = 255; } positions; uint8_t frameCount = 0; } framesetInfo; + struct DismissedFrames { + bool textMessage = false; + bool waypoint = false; + bool wifi = false; + bool memory = false; + } dismissedFrames; + // Which frame we want to be displayed, after we regen the frameset by calling setFrames enum FrameFocus : uint8_t { FOCUS_DEFAULT, // No specific frame From 9a57a774f6bb5a28e952649e5015e564c771a17a Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 6 May 2025 01:54:09 -0400 Subject: [PATCH 088/265] More detailed IAQ Levels for hazardous environments --- .../Telemetry/EnvironmentTelemetry.cpp | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index fd2d3b219..9c0245e34 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -398,13 +398,19 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt entries.push_back("Hum: " + String(m.relative_humidity, 0) + "%"); if (m.barometric_pressure != 0) entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa"); - if (m.iaq != 0) { - String aqi = "IAQ: " + String(m.iaq); - if (m.iaq < 50) aqi += " (Good)"; - else if (m.iaq < 100) aqi += " (Moderate)"; - else aqi += " (!)"; - entries.push_back(aqi); - } + if (m.iaq != 0) { + String aqi = "IAQ: " + String(m.iaq); + + if (m.iaq <= 50) aqi += " (Good)"; + else if (m.iaq <= 100) aqi += " (Moderate)"; + else if (m.iaq <= 150) aqi += " (Poor)"; + else if (m.iaq <= 200) aqi += " (Unhealthy)"; + else if (m.iaq <= 250) aqi += " (Very Unhealthy)"; + else if (m.iaq <= 350) aqi += " (Hazardous)"; + else aqi += " (Extreme)"; + + entries.push_back(aqi); + } if (m.voltage != 0 || m.current != 0) entries.push_back(String(m.voltage, 1) + "V / " + String(m.current, 0) + "mA"); if (m.lux != 0) From bc1cc0081f81b485c47e227ee61e46bcbf8a8637 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 7 May 2025 00:54:11 -0400 Subject: [PATCH 089/265] Added IAQ alert and new Overlay Alert Banner function --- src/graphics/Screen.cpp | 51 ++++++++++++++++++- src/graphics/Screen.h | 2 + .../Telemetry/EnvironmentTelemetry.cpp | 42 ++++++++++----- 3 files changed, 82 insertions(+), 13 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 81ab9258b..609f7a51c 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -84,6 +84,8 @@ namespace graphics // A text message frame + debug frame + all the node infos FrameCallback *normalFrames; static uint32_t targetFramerate = IDLE_FRAMERATE; +static String alertBannerMessage; +static uint32_t alertBannerUntil = 0; uint32_t logo_timeout = 5000; // 4 seconds for EACH logo @@ -296,6 +298,53 @@ static void drawWelcomeScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i #endif } +// ============================== +// Overlay Alert Banner Renderer +// ============================== +// Displays a temporary centered banner message (e.g., warning, status, etc.) +// The banner appears in the center of the screen and disappears after the specified duration + +// Called to trigger a banner with custom message and duration +void Screen::showOverlayBanner(const String &message, uint32_t durationMs) +{ + // Store the message and set the expiration timestamp + alertBannerMessage = message; + alertBannerUntil = millis() + durationMs; +} + +// Draws the overlay banner on screen, if still within display duration +static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + // Exit if no message is active or duration has passed + if (alertBannerMessage.length() == 0 || millis() > alertBannerUntil) return; + + // === Layout Configuration === + constexpr uint16_t padding = 5; // Padding around the text + constexpr uint8_t imprecision = 3; // Pixel jitter to reduce burn-in on E-Ink + + // Setup font and alignment + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === Measure and position the box === + uint16_t textWidth = display->getStringWidth(alertBannerMessage.c_str(), alertBannerMessage.length(), true); + uint16_t boxWidth = padding * 2 + textWidth; + uint16_t boxHeight = FONT_HEIGHT_SMALL + padding * 2; + + int16_t boxLeft = (display->width() / 2) - (boxWidth / 2) + random(-imprecision, imprecision + 1); + int16_t boxTop = (display->height() / 2) - (boxHeight / 2) + random(-imprecision, imprecision + 1); + + // === Draw background box === + display->setColor(BLACK); + display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Slightly oversized box + display->setColor(WHITE); + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border + + // === Draw the text (twice for faux bold) === + display->drawString(boxLeft + padding, boxTop + padding, alertBannerMessage); + display->drawString(boxLeft + padding + 1, boxTop + padding, alertBannerMessage); // Faux bold effect +} + // draw overlay in bottom right corner of screen to show when notifications are muted or modifier key is active static void drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) { @@ -3576,7 +3625,7 @@ void Screen::setFrames(FrameFocus focus) ui->disableAllIndicators(); // Add function overlay here. This can show when notifications muted, modifier key is active etc - static OverlayCallback overlays[] = {drawFunctionOverlay, drawCustomFrameIcons}; + static OverlayCallback overlays[] = {drawFunctionOverlay, drawCustomFrameIcons, drawAlertBannerOverlay}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 2c8eff244..9db389296 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -261,6 +261,8 @@ class Screen : public concurrency::OSThread enqueueCmd(cmd); } + void showOverlayBanner(const String &message, uint32_t durationMs = 3000); + void startFirmwareUpdateScreen() { ScreenCmd cmd; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 9c0245e34..d159f47be 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -19,6 +19,8 @@ #include #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" +#include "buzz.h" +#include "modules/ExternalNotificationModule.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL // Sensors @@ -398,19 +400,35 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt entries.push_back("Hum: " + String(m.relative_humidity, 0) + "%"); if (m.barometric_pressure != 0) entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa"); - if (m.iaq != 0) { - String aqi = "IAQ: " + String(m.iaq); - - if (m.iaq <= 50) aqi += " (Good)"; - else if (m.iaq <= 100) aqi += " (Moderate)"; - else if (m.iaq <= 150) aqi += " (Poor)"; - else if (m.iaq <= 200) aqi += " (Unhealthy)"; - else if (m.iaq <= 250) aqi += " (Very Unhealthy)"; - else if (m.iaq <= 350) aqi += " (Hazardous)"; - else aqi += " (Extreme)"; - - entries.push_back(aqi); + if (m.iaq != 0) { + String aqi = "IAQ: " + String(m.iaq); + + if (m.iaq <= 50) aqi += " (Good)"; + else if (m.iaq <= 100) aqi += " (Moderate)"; + else if (m.iaq <= 150) aqi += " (Poor)"; + else if (m.iaq <= 200) aqi += " (Unhealthy)"; + else if (m.iaq <= 250) aqi += " (Very Unhealthy)"; + else if (m.iaq <= 350) aqi += " (Hazardous)"; + else aqi += " (Extreme)"; + + entries.push_back(aqi); + + // === IAQ alert logic === + static uint32_t lastAlertTime = 0; + uint32_t now = millis(); + + bool isOwnTelemetry = lastMeasurementPacket->from == nodeDB->getNodeNum(); + bool isIAQAlert = m.iaq > 100 && (now - lastAlertTime > 60000); + + if (isOwnTelemetry && isIAQAlert) { + LOG_INFO("drawFrame: IAQ %d (own) β€” showing banner", m.iaq); + screen->showOverlayBanner("Unhealthy IAQ Levels", 3000); // Always show banner + if (moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) { + playLongBeep(); // Only buzz if not muted + } + lastAlertTime = now; } + } if (m.voltage != 0 || m.current != 0) entries.push_back(String(m.voltage, 1) + "V / " + String(m.current, 0) + "mA"); if (m.lux != 0) From 0a61ea41372bbcce889d7467282462b887c58932 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 7 May 2025 01:05:14 -0400 Subject: [PATCH 090/265] Ad-hoc Position banner added --- src/ButtonThread.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp index 04200a7df..a7d594a77 100644 --- a/src/ButtonThread.cpp +++ b/src/ButtonThread.cpp @@ -219,11 +219,17 @@ int32_t ButtonThread::runOnce() case BUTTON_EVENT_DOUBLE_PRESSED: { LOG_BUTTON("Double press!"); -#ifdef ELECROW_ThinkNode_M1 + + #ifdef ELECROW_ThinkNode_M1 digitalWrite(PIN_EINK_EN, digitalRead(PIN_EINK_EN) == LOW); break; -#endif + #endif + + // Send GPS position immediately sendAdHocPosition(); + + // Show temporary on-screen confirmation banner for 3 seconds + screen->showOverlayBanner("Ad-hoc Position Sent", 3000); break; } From 4e8ae7b1085724002051aaea5e49d8e579823071 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 7 May 2025 02:49:11 -0400 Subject: [PATCH 091/265] Removed Jitter --- src/ButtonThread.cpp | 2 +- src/graphics/Screen.cpp | 8 +++----- src/modules/Telemetry/EnvironmentTelemetry.cpp | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp index a7d594a77..a6840e5a1 100644 --- a/src/ButtonThread.cpp +++ b/src/ButtonThread.cpp @@ -229,7 +229,7 @@ int32_t ButtonThread::runOnce() sendAdHocPosition(); // Show temporary on-screen confirmation banner for 3 seconds - screen->showOverlayBanner("Ad-hoc Position Sent", 3000); + screen->showOverlayBanner("Ad-hoc Ping Sent", 3000); break; } diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 609f7a51c..ee44d77cc 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -297,7 +297,6 @@ static void drawWelcomeScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i esp_task_wdt_reset(); #endif } - // ============================== // Overlay Alert Banner Renderer // ============================== @@ -319,8 +318,7 @@ static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *sta if (alertBannerMessage.length() == 0 || millis() > alertBannerUntil) return; // === Layout Configuration === - constexpr uint16_t padding = 5; // Padding around the text - constexpr uint8_t imprecision = 3; // Pixel jitter to reduce burn-in on E-Ink + constexpr uint16_t padding = 5; // Padding around the text // Setup font and alignment display->setFont(FONT_SMALL); @@ -331,8 +329,8 @@ static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *sta uint16_t boxWidth = padding * 2 + textWidth; uint16_t boxHeight = FONT_HEIGHT_SMALL + padding * 2; - int16_t boxLeft = (display->width() / 2) - (boxWidth / 2) + random(-imprecision, imprecision + 1); - int16_t boxTop = (display->height() / 2) - (boxHeight / 2) + random(-imprecision, imprecision + 1); + int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); + int16_t boxTop = (display->height() / 2) - (boxHeight / 2); // === Draw background box === display->setColor(BLACK); diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index d159f47be..c4581c6a1 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -418,7 +418,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt uint32_t now = millis(); bool isOwnTelemetry = lastMeasurementPacket->from == nodeDB->getNodeNum(); - bool isIAQAlert = m.iaq > 100 && (now - lastAlertTime > 60000); + bool isIAQAlert = m.iaq > 200 && (now - lastAlertTime > 60000); if (isOwnTelemetry && isIAQAlert) { LOG_INFO("drawFrame: IAQ %d (own) β€” showing banner", m.iaq); From 581021031c1ecf786472858edbff72a13ab5ed80 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 7 May 2025 18:44:18 -0400 Subject: [PATCH 092/265] Update SharedUIDisplay.cpp --- src/graphics/SharedUIDisplay.cpp | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index b6a0a4d46..4b37a1a8c 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -1,13 +1,14 @@ #include "graphics/SharedUIDisplay.h" +#include "RTC.h" #include "graphics/ScreenFonts.h" #include "main.h" -#include "power.h" #include "meshtastic/config.pb.h" -#include "RTC.h" +#include "power.h" #include #include -namespace graphics { +namespace graphics +{ // === Shared External State === bool hasUnreadMessage = false; @@ -82,9 +83,9 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) } else { int batteryX = 1; int batteryY = HEADER_OFFSET_Y + 1; - #ifdef USE_EINK +#ifdef USE_EINK batteryY += 2; - #endif +#endif display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v); if (isCharging && isBoltVisibleShared) { display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v); @@ -125,7 +126,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) if (config.display.use_12h_clock) { bool isPM = hour >= 12; hour %= 12; - if (hour == 0) hour = 12; + if (hour == 0) + hour = 12; snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); } @@ -141,11 +143,11 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) if (isMailIconVisible) { if (useHorizontalBattery) { int iconW = 16, iconH = 12; - int iconX = timeX - iconW - 3; + int iconX = timeX - iconW - 4; int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; - display->drawRect(iconX, iconY, iconW, iconH); - display->drawLine(iconX + 1, iconY + 1, iconX + iconW / 2, iconY + iconH - 2); - display->drawLine(iconX + iconW - 2, iconY + 1, iconX + iconW / 2, iconY + iconH - 2); + display->drawRect(iconX, iconY, iconW + 1, iconH); + display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); + display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); } else { int iconX = timeX - mail_width; int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; @@ -155,7 +157,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) } display->drawString(timeX, textY, timeStr); - if (isBold) display->drawString(timeX - 1, textY, timeStr); + if (isBold) + display->drawString(timeX - 1, textY, timeStr); } display->setColor(WHITE); From 14752caee5857f7e5ef5f55ec626859d0088a881 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 8 May 2025 22:00:57 -0400 Subject: [PATCH 093/265] Notification banners implemented --- src/ButtonThread.cpp | 5 +- src/graphics/Screen.cpp | 54 +++++++++++++---- src/main.cpp | 5 +- src/modules/AdminModule.cpp | 2 +- src/modules/CannedMessageModule.cpp | 58 ++++++++++--------- .../Telemetry/EnvironmentTelemetry.cpp | 39 ++++++++----- src/shutdown.h | 2 +- 7 files changed, 109 insertions(+), 56 deletions(-) diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp index a6840e5a1..c9b4be8a7 100644 --- a/src/ButtonThread.cpp +++ b/src/ButtonThread.cpp @@ -286,9 +286,12 @@ int32_t ButtonThread::runOnce() case BUTTON_EVENT_LONG_PRESSED: { LOG_BUTTON("Long press!"); powerFSM.trigger(EVENT_PRESS); + if (screen) { - screen->startAlert("Shutting down..."); + // Show shutdown message as a temporary overlay banner + screen->showOverlayBanner("Shutting Down..."); // Display for 3 seconds } + playBeep(); break; } diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index ee44d77cc..0029ed787 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -308,26 +308,43 @@ void Screen::showOverlayBanner(const String &message, uint32_t durationMs) { // Store the message and set the expiration timestamp alertBannerMessage = message; - alertBannerUntil = millis() + durationMs; + alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; } // Draws the overlay banner on screen, if still within display duration static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) { // Exit if no message is active or duration has passed - if (alertBannerMessage.length() == 0 || millis() > alertBannerUntil) return; + if (alertBannerMessage.length() == 0 || (alertBannerUntil != 0 && millis() > alertBannerUntil)) return; // === Layout Configuration === - constexpr uint16_t padding = 5; // Padding around the text + constexpr uint16_t padding = 5; // Padding around text inside the box + constexpr uint8_t lineSpacing = 1; // Extra space between lines // Setup font and alignment display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setTextAlignment(TEXT_ALIGN_LEFT); // We will manually center per line - // === Measure and position the box === - uint16_t textWidth = display->getStringWidth(alertBannerMessage.c_str(), alertBannerMessage.length(), true); - uint16_t boxWidth = padding * 2 + textWidth; - uint16_t boxHeight = FONT_HEIGHT_SMALL + padding * 2; + // === Split the message into lines (supports multi-line banners) === + std::vector lines; + int start = 0, newlineIdx; + while ((newlineIdx = alertBannerMessage.indexOf('\n', start)) != -1) { + lines.push_back(alertBannerMessage.substring(start, newlineIdx)); + start = newlineIdx + 1; + } + lines.push_back(alertBannerMessage.substring(start)); + + // === Measure text dimensions === + uint16_t maxWidth = 0; + std::vector lineWidths; + for (const auto& line : lines) { + uint16_t w = display->getStringWidth(line.c_str(), line.length(), true); + lineWidths.push_back(w); + if (w > maxWidth) maxWidth = w; + } + + uint16_t boxWidth = padding * 2 + maxWidth; + uint16_t boxHeight = padding * 2 + lines.size() * FONT_HEIGHT_SMALL + (lines.size() - 1) * lineSpacing; int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); int16_t boxTop = (display->height() / 2) - (boxHeight / 2); @@ -338,9 +355,16 @@ static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *sta display->setColor(WHITE); display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border - // === Draw the text (twice for faux bold) === - display->drawString(boxLeft + padding, boxTop + padding, alertBannerMessage); - display->drawString(boxLeft + padding + 1, boxTop + padding, alertBannerMessage); // Faux bold effect + // === Draw each line centered in the box === + int16_t lineY = boxTop + padding; + for (size_t i = 0; i < lines.size(); ++i) { + int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; + + display->drawString(textX, lineY, lines[i]); + display->drawString(textX + 1, lineY, lines[i]); // Faux bold + + lineY += FONT_HEIGHT_SMALL + lineSpacing; + } } // draw overlay in bottom right corner of screen to show when notifications are muted or modifier key is active @@ -4199,11 +4223,17 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) setFrames(FOCUS_PRESERVE); // Stay on same frame, silently add/remove frames } else { // Incoming message - // setFrames(FOCUS_TEXTMESSAGE); // Focus on the new message devicestate.has_rx_text_message = true; // Needed to include the message frame hasUnreadMessage = true; // Enables mail icon in the header setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view forceDisplay(); // Forces screen redraw (this works in your codebase) + + // === Show banner: "New Message" followed by name on second line === + const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); + if (node && node->has_user && node->user.long_name[0]) { + String name = String(node->user.long_name); + screen->showOverlayBanner("New Message\nfrom " + name, 3000); // Multiline banner + } } } diff --git a/src/main.cpp b/src/main.cpp index 9ef944e65..651f1ff16 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1226,9 +1226,12 @@ void setup() LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset"); config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; nodeDB->saveToDisk(SEGMENT_CONFIG); + if (!rIf->reconfigure()) { LOG_WARN("Reconfigure failed, rebooting"); - screen->startAlert("Rebooting..."); + if (screen) { + screen->showOverlayBanner("Rebooting..."); + } rebootAtMsec = millis() + 5000; } } diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 88109bc78..fcc70e39f 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -1096,7 +1096,7 @@ void AdminModule::handleGetDeviceUIConfig(const meshtastic_MeshPacket &req) void AdminModule::reboot(int32_t seconds) { LOG_INFO("Reboot in %d seconds", seconds); - screen->startAlert("Rebooting..."); + screen->showOverlayBanner("Rebooting...", 0); // stays on screen rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000); } diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 4700f122e..5e0645f24 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -463,52 +463,57 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) break; // mute (switch off/toggle) external notifications on fn+m case INPUT_BROKER_MSG_MUTE_TOGGLE: - if (moduleConfig.external_notification.enabled == true) { - if (externalNotificationModule->getMute()) { - externalNotificationModule->setMute(false); - showTemporaryMessage("Notifications \nEnabled"); - if (screen) - screen->removeFunctionSymbol("M"); // remove the mute symbol from the bottom right corner - } else { - externalNotificationModule->stopNow(); // this will turn off all GPIO and sounds and idle the loop - externalNotificationModule->setMute(true); - showTemporaryMessage("Notifications \nDisabled"); - if (screen) - screen->setFunctionSymbol("M"); // add the mute symbol to the bottom right corner + if (moduleConfig.external_notification.enabled == true) { + if (externalNotificationModule->getMute()) { + externalNotificationModule->setMute(false); + if (screen) { + screen->removeFunctionSymbol("M"); + screen->showOverlayBanner("Notifications\nEnabled", 3000); + } + } else { + externalNotificationModule->stopNow(); + externalNotificationModule->setMute(true); + if (screen) { + screen->setFunctionSymbol("M"); + screen->showOverlayBanner("Notifications\nDisabled", 3000); } } - break; - case INPUT_BROKER_MSG_GPS_TOGGLE: // toggle GPS like triple press does -#if !MESHTASTIC_EXCLUDE_GPS + } + break; + + case INPUT_BROKER_MSG_GPS_TOGGLE: + #if !MESHTASTIC_EXCLUDE_GPS if (gps != nullptr) { gps->toggleGpsMode(); } - if (screen) + if (screen) { screen->forceDisplay(); - showTemporaryMessage("GPS Toggled"); -#endif + screen->showOverlayBanner("GPS Toggled", 3000); + } + #endif break; - case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: // toggle Bluetooth on/off + + case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: if (config.bluetooth.enabled == true) { config.bluetooth.enabled = false; LOG_INFO("User toggled Bluetooth"); nodeDB->saveToDisk(); disableBluetooth(); - showTemporaryMessage("Bluetooth OFF"); - } else if (config.bluetooth.enabled == false) { + if (screen) screen->showOverlayBanner("Bluetooth OFF", 3000); + } else { config.bluetooth.enabled = true; LOG_INFO("User toggled Bluetooth"); nodeDB->saveToDisk(); rebootAtMsec = millis() + 2000; - showTemporaryMessage("Bluetooth ON\nReboot"); + if (screen) screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000); } break; - case INPUT_BROKER_MSG_SEND_PING: // fn+space send network ping like double press does + case INPUT_BROKER_MSG_SEND_PING: service->refreshLocalMeshNode(); if (service->trySendPosition(NODENUM_BROADCAST, true)) { - showTemporaryMessage("Position \nUpdate Sent"); + if (screen) screen->showOverlayBanner("Position\nUpdate Sent", 3000); } else { - showTemporaryMessage("Node Info \nUpdate Sent"); + if (screen) screen->showOverlayBanner("Node Info\nUpdate Sent", 3000); } break; case INPUT_BROKER_MSG_DISMISS_FRAME: // fn+del: dismiss screen frames like text or waypoint @@ -869,14 +874,13 @@ int32_t CannedMessageModule::runOnce() // handle fn+s for shutdown case INPUT_BROKER_MSG_SHUTDOWN: if (screen) - screen->startAlert("Shutting down..."); shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; break; // and fn+r for reboot case INPUT_BROKER_MSG_REBOOT: if (screen) - screen->startAlert("Rebooting..."); + screen->showOverlayBanner("Rebooting...", 0); // stays on screen rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; break; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index c4581c6a1..d08cdb764 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -402,14 +402,24 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa"); if (m.iaq != 0) { String aqi = "IAQ: " + String(m.iaq); + const char *bannerMsg = nullptr; // Default: no banner - if (m.iaq <= 50) aqi += " (Good)"; - else if (m.iaq <= 100) aqi += " (Moderate)"; - else if (m.iaq <= 150) aqi += " (Poor)"; - else if (m.iaq <= 200) aqi += " (Unhealthy)"; - else if (m.iaq <= 250) aqi += " (Very Unhealthy)"; - else if (m.iaq <= 350) aqi += " (Hazardous)"; - else aqi += " (Extreme)"; + if (m.iaq <= 25) aqi += " (Excellent)"; + else if (m.iaq <= 50) aqi += " (Good)"; + else if (m.iaq <= 100) aqi += " (Moderate)"; + else if (m.iaq <= 150) aqi += " (Poor)"; + else if (m.iaq <= 200) { + aqi += " (Unhealthy)"; + bannerMsg = "Unhealthy IAQ"; + } + else if (m.iaq <= 300) { + aqi += " (Very Unhealthy)"; + bannerMsg = "Very Unhealthy IAQ"; + } + else { + aqi += " (Hazardous)"; + bannerMsg = "Hazardous IAQ"; + } entries.push_back(aqi); @@ -418,14 +428,17 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt uint32_t now = millis(); bool isOwnTelemetry = lastMeasurementPacket->from == nodeDB->getNodeNum(); - bool isIAQAlert = m.iaq > 200 && (now - lastAlertTime > 60000); + bool isCooldownOver = (now - lastAlertTime > 60000); - if (isOwnTelemetry && isIAQAlert) { - LOG_INFO("drawFrame: IAQ %d (own) β€” showing banner", m.iaq); - screen->showOverlayBanner("Unhealthy IAQ Levels", 3000); // Always show banner - if (moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) { - playLongBeep(); // Only buzz if not muted + if (isOwnTelemetry && bannerMsg && isCooldownOver) { + LOG_INFO("drawFrame: IAQ %d (own) β€” showing banner: %s", m.iaq, bannerMsg); + screen->showOverlayBanner(bannerMsg, 3000); + + // Only buzz if IAQ is over 200 + if (m.iaq > 200 && moduleConfig.external_notification.enabled && !externalNotificationModule->getMute()) { + playLongBeep(); } + lastAlertTime = now; } } diff --git a/src/shutdown.h b/src/shutdown.h index f02cb7964..d585ddb30 100644 --- a/src/shutdown.h +++ b/src/shutdown.h @@ -42,7 +42,7 @@ void powerCommandsCheck() #if defined(ARCH_ESP32) || defined(ARCH_NRF52) if (shutdownAtMsec) { - screen->startAlert("Shutting down..."); + screen->showOverlayBanner("Shutting Down...", 0); // stays on screen } #endif From 2a7059c86ebea89dabe625c9b87fc8f9f3ca0e6c Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 8 May 2025 23:12:05 -0400 Subject: [PATCH 094/265] Alert Message banner --- src/graphics/Screen.cpp | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 0029ed787..2a83c4653 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -4226,20 +4226,38 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) devicestate.has_rx_text_message = true; // Needed to include the message frame hasUnreadMessage = true; // Enables mail icon in the header setFrames(FOCUS_PRESERVE); // Refresh frame list without switching view - forceDisplay(); // Forces screen redraw (this works in your codebase) + forceDisplay(); // Forces screen redraw - // === Show banner: "New Message" followed by name on second line === + // === Prepare banner content === const meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(packet->from); - if (node && node->has_user && node->user.long_name[0]) { - String name = String(node->user.long_name); - screen->showOverlayBanner("New Message\nfrom " + name, 3000); // Multiline banner + const char *longName = (node && node->has_user) ? node->user.long_name : nullptr; + + const char *msgRaw = reinterpret_cast(packet->decoded.payload.bytes); + String msg = String(msgRaw); + msg.trim(); // Remove leading/trailing whitespace/newlines + + String banner; + + // Match bell character or exact alert text + if (msg == "\x07" || msg.indexOf("Alert Bell Character") != -1) { + banner = "Alert Received"; + } else { + banner = "New Message"; } + + if (longName && longName[0]) { + banner += "\nfrom "; + banner += longName; + } + + screen->showOverlayBanner(banner, 3000); } } return 0; } + // Triggered by MeshModules int Screen::handleUIFrameEvent(const UIFrameEvent *event) { From b66d3d71574b7654f796a37250ba60b805c0412a Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 9 May 2025 11:02:31 -0400 Subject: [PATCH 095/265] Bell Icon added --- src/graphics/Screen.cpp | 56 +++++++++++++++++++++++++---------------- src/graphics/images.h | 5 ++++ 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 2a83c4653..4cff46b9d 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -315,11 +315,15 @@ void Screen::showOverlayBanner(const String &message, uint32_t durationMs) static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) { // Exit if no message is active or duration has passed - if (alertBannerMessage.length() == 0 || (alertBannerUntil != 0 && millis() > alertBannerUntil)) return; + if (alertBannerMessage.length() == 0 || (alertBannerUntil != 0 && millis() > alertBannerUntil)) + return; // === Layout Configuration === - constexpr uint16_t padding = 5; // Padding around text inside the box - constexpr uint8_t lineSpacing = 1; // Extra space between lines + constexpr uint16_t padding = 5; // Padding around text inside the box + constexpr uint8_t lineSpacing = 1; // Extra space between lines + + // Search the mesage to determine if we need the bell added + bool needs_bell = (alertBannerMessage.indexOf("Alert Received") != -1); // Setup font and alignment display->setFont(FONT_SMALL); @@ -337,31 +341,43 @@ static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *sta // === Measure text dimensions === uint16_t maxWidth = 0; std::vector lineWidths; - for (const auto& line : lines) { + for (const auto &line : lines) { uint16_t w = display->getStringWidth(line.c_str(), line.length(), true); lineWidths.push_back(w); - if (w > maxWidth) maxWidth = w; + if (w > maxWidth) + maxWidth = w; } uint16_t boxWidth = padding * 2 + maxWidth; + if (needs_bell && boxWidth < 78) + boxWidth += 20; + uint16_t boxHeight = padding * 2 + lines.size() * FONT_HEIGHT_SMALL + (lines.size() - 1) * lineSpacing; int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); - int16_t boxTop = (display->height() / 2) - (boxHeight / 2); + int16_t boxTop = (display->height() / 2) - (boxHeight / 2); // === Draw background box === display->setColor(BLACK); display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Slightly oversized box display->setColor(WHITE); - display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border // === Draw each line centered in the box === int16_t lineY = boxTop + padding; for (size_t i = 0; i < lines.size(); ++i) { int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; + uint16_t line_width = display->getStringWidth(lines[i].c_str(), lines[i].length(), true); - display->drawString(textX, lineY, lines[i]); - display->drawString(textX + 1, lineY, lines[i]); // Faux bold + if (needs_bell && i == 0) { + int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2; + display->drawXbm(textX - 10, bellY, 8, 8, bell_alert); + display->drawXbm(textX + line_width + 2, bellY, 8, 8, bell_alert); + } + + display->drawString(textX, lineY, lines[i]); + if (SCREEN_WIDTH > 128) + display->drawString(textX + 1, lineY, lines[i]); // Faux bold lineY += FONT_HEIGHT_SMALL + lineSpacing; } @@ -3631,13 +3647,13 @@ void Screen::setFrames(FrameFocus focus) // fsi.positions.settings = numframes; // normalFrames[numframes++] = &Screen::drawDebugInfoSettingsTrampoline; - #if HAS_WIFI && !defined(ARCH_PORTDUINO) +#if HAS_WIFI && !defined(ARCH_PORTDUINO) if (!dismissedFrames.wifi && isWifiAvailable()) { fsi.positions.wifi = numframes; normalFrames[numframes++] = &Screen::drawDebugInfoWiFiTrampoline; indicatorIcons.push_back(icon_wifi); } - #endif +#endif fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE this->frameCount = numframes; // βœ… Save frame count for use in custom overlay @@ -3707,19 +3723,16 @@ void Screen::dismissCurrentFrame() memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); dismissedFrames.textMessage = true; dismissed = true; - } - else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { + } else if (currentFrame == framesetInfo.positions.waypoint && devicestate.has_rx_waypoint) { LOG_DEBUG("Dismiss Waypoint"); devicestate.has_rx_waypoint = false; dismissedFrames.waypoint = true; dismissed = true; - } - else if (currentFrame == framesetInfo.positions.wifi) { + } else if (currentFrame == framesetInfo.positions.wifi) { LOG_DEBUG("Dismiss WiFi Screen"); dismissedFrames.wifi = true; dismissed = true; - } - else if (currentFrame == framesetInfo.positions.memory) { + } else if (currentFrame == framesetInfo.positions.memory) { LOG_INFO("Dismiss Memory"); dismissedFrames.memory = true; dismissed = true; @@ -3730,7 +3743,6 @@ void Screen::dismissCurrentFrame() } } - void Screen::handleStartFirmwareUpdateScreen() { LOG_DEBUG("Show firmware screen"); @@ -4234,12 +4246,13 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) const char *msgRaw = reinterpret_cast(packet->decoded.payload.bytes); String msg = String(msgRaw); - msg.trim(); // Remove leading/trailing whitespace/newlines + msg.trim(); // Remove leading/trailing whitespace/newlines String banner; // Match bell character or exact alert text - if (msg == "\x07" || msg.indexOf("Alert Bell Character") != -1) { + // if (msg == "\x07" || msg.indexOf("Alert Bell Character") != -1) { + if (msg.indexOf("\x07") != -1) { banner = "Alert Received"; } else { banner = "New Message"; @@ -4250,14 +4263,13 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) banner += longName; } - screen->showOverlayBanner(banner, 3000); + screen->showOverlayBanner(banner, 30000); } } return 0; } - // Triggered by MeshModules int Screen::handleUIFrameEvent(const UIFrameEvent *event) { diff --git a/src/graphics/images.h b/src/graphics/images.h index 3f7986048..ffc556a4f 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -389,6 +389,11 @@ const uint8_t icon_module[] PROGMEM = { 0b00111100, // β–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘ 0b00011000 // β–‘β–‘β–‘β–ˆβ–ˆβ–‘β–‘β–‘ }; + +// Bell icon for Alert Message +const unsigned char bell_alert[] PROGMEM = {0b00011000, 0b00100100, 0b00100100, 0b01000010, + 0b01000010, 0b01000010, 0b11111111, 0b00011000}; + #endif #include "img/icon.xbm" From ae88ec96f7d3da8e506b8c7fd4940b738fe7228e Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 9 May 2025 12:06:21 -0400 Subject: [PATCH 096/265] Extra padding for tft alert banner and BaseUI on welcome screen --- src/graphics/Screen.cpp | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 4cff46b9d..d6fe1eada 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -166,33 +166,41 @@ extern bool hasUnreadMessage; */ static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - // draw an xbm image. - // Please note that everything that should be transitioned - // needs to be drawn relative to x and y + // draw centered icon + int iconX = x + (SCREEN_WIDTH - icon_width) / 2; + int iconY = y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2; - // draw centered icon left to right and centered above the one line of app text - display->drawXbm(x + (SCREEN_WIDTH - icon_width) / 2, y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2, - icon_width, icon_height, icon_bits); + display->drawXbm(iconX, iconY, icon_width, icon_height, icon_bits); + // Draw centered label "BaseUI" just below the icon + display->setFont(FONT_SMALL); + const char *label = "BaseUI"; + int labelY = iconY + icon_height; + labelY += (SCREEN_WIDTH > 128) ? 2 : -2; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(x + SCREEN_WIDTH / 2, labelY, label); + + // Draw app title display->setFont(FONT_MEDIUM); display->setTextAlignment(TEXT_ALIGN_LEFT); const char *title = "meshtastic.org"; display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); - display->setFont(FONT_SMALL); // Draw region in upper left + display->setFont(FONT_SMALL); if (upperMsg) display->drawString(x + 0, y + 0, upperMsg); // Draw version and short name in upper right char buf[25]; snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); - display->setTextAlignment(TEXT_ALIGN_RIGHT); display->drawString(x + SCREEN_WIDTH, y + 0, buf); + screen->forceDisplay(); - display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code + // Restore default alignment + display->setTextAlignment(TEXT_ALIGN_LEFT); } #ifdef USERPREFS_OEM_TEXT @@ -349,8 +357,8 @@ static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *sta } uint16_t boxWidth = padding * 2 + maxWidth; - if (needs_bell && boxWidth < 78) - boxWidth += 20; + if (needs_bell && boxWidth < (SCREEN_WIDTH > 128) ? 106 : 78) + boxWidth += (SCREEN_WIDTH > 128) ? 26 : 20; uint16_t boxHeight = padding * 2 + lines.size() * FONT_HEIGHT_SMALL + (lines.size() - 1) * lineSpacing; @@ -4263,7 +4271,7 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) banner += longName; } - screen->showOverlayBanner(banner, 30000); + screen->showOverlayBanner(banner, 3000); } } From 212963156b82c69267ead7645fbb30d0c39b22c7 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 9 May 2025 12:36:21 -0400 Subject: [PATCH 097/265] Fixed logic check for minimum pop-up size --- src/graphics/Screen.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index d6fe1eada..50cfe9d26 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -347,6 +347,7 @@ static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *sta lines.push_back(alertBannerMessage.substring(start)); // === Measure text dimensions === + uint16_t minWidth = (SCREEN_WIDTH > 128) ? 106 : 78; uint16_t maxWidth = 0; std::vector lineWidths; for (const auto &line : lines) { @@ -357,7 +358,7 @@ static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *sta } uint16_t boxWidth = padding * 2 + maxWidth; - if (needs_bell && boxWidth < (SCREEN_WIDTH > 128) ? 106 : 78) + if (needs_bell && boxWidth < minWidth) boxWidth += (SCREEN_WIDTH > 128) ? 26 : 20; uint16_t boxHeight = padding * 2 + lines.size() * FONT_HEIGHT_SMALL + (lines.size() - 1) * lineSpacing; From 71ba6fa9ce5fc96d9c6c061f290fb4a02688e693 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 9 May 2025 14:49:19 -0400 Subject: [PATCH 098/265] Update Screen.cpp --- src/graphics/Screen.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 50cfe9d26..b215f85f7 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -169,6 +169,7 @@ static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDispl // draw centered icon int iconX = x + (SCREEN_WIDTH - icon_width) / 2; int iconY = y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2; + iconY -= (SCREEN_WIDTH > 128) ? 0 : 4; display->drawXbm(iconX, iconY, icon_width, icon_height, icon_bits); From a1d859bf4cfcbeda9dc006035879db3ac251ed2a Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 9 May 2025 21:19:50 -0400 Subject: [PATCH 099/265] Base UI Logo change --- src/graphics/Screen.cpp | 91 ++++++++++++++++++++++++++++++----------- 1 file changed, 67 insertions(+), 24 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index b215f85f7..298b835b4 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1,9 +1,11 @@ /* +BaseUI +Developed by: +Ronald Garcia (aka HarukiToreda) -SSD1306 - Screen module - -Copyright (C) 2018 by Xose PΓ©rez - +In collaboration with: +- JasonP: Graphics icon work, UI adjustments, code optimizations, enhancements, and testing +- TonyG (aka Topho) – Project management, structural planning, and testing This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -166,41 +168,82 @@ extern bool hasUnreadMessage; */ static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - // draw centered icon - int iconX = x + (SCREEN_WIDTH - icon_width) / 2; - int iconY = y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2; - iconY -= (SCREEN_WIDTH > 128) ? 0 : 4; - - display->drawXbm(iconX, iconY, icon_width, icon_height, icon_bits); - - // Draw centered label "BaseUI" just below the icon - display->setFont(FONT_SMALL); const char *label = "BaseUI"; - int labelY = iconY + icon_height; - labelY += (SCREEN_WIDTH > 128) ? 2 : -2; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(x + SCREEN_WIDTH / 2, labelY, label); + display->setFont(FONT_SMALL); + int textWidth = display->getStringWidth(label); + int r = 3; // corner radius - // Draw app title + if (SCREEN_WIDTH > 128) { + // === ORIGINAL WIDE SCREEN LAYOUT (unchanged) === + int padding = 4; + int boxWidth = max(icon_width, textWidth) + padding * 2; + int boxHeight = icon_height + FONT_HEIGHT_SMALL + padding * 3; + int boxX = x + (SCREEN_WIDTH - boxWidth) / 2; + int boxY = y + (SCREEN_HEIGHT - boxHeight) / 2; + + display->setColor(WHITE); + display->fillRect(boxX + r, boxY, boxWidth - 2 * r, boxHeight); + display->fillRect(boxX, boxY + r, boxWidth, boxHeight - 2 * r); + display->fillCircle(boxX + r, boxY + r, r); + display->fillCircle(boxX + boxWidth - r - 1, boxY + r, r); + display->fillCircle(boxX + r, boxY + boxHeight - r - 1, r); + display->fillCircle(boxX + boxWidth - r - 1, boxY + boxHeight - r - 1, r); + + display->setColor(BLACK); + int iconX = boxX + (boxWidth - icon_width) / 2; + int iconY = boxY + padding; + display->drawXbm(iconX, iconY, icon_width, icon_height, icon_bits); + + int labelY = iconY + icon_height + padding; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(x + SCREEN_WIDTH / 2, labelY, label); + display->drawString(x + SCREEN_WIDTH / 2 + 1, labelY, label); // faux bold + + } else { + // === TIGHT SMALL SCREEN LAYOUT === + int iconY = y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2; + iconY -= 4; + + int labelY = iconY + icon_height - 2; + + int boxWidth = max(icon_width, textWidth) + 4; + int boxX = x + (SCREEN_WIDTH - boxWidth) / 2; + int boxY = iconY - 1; + int boxBottom = labelY + FONT_HEIGHT_SMALL - 2; + int boxHeight = boxBottom - boxY; + + display->setColor(WHITE); + display->fillRect(boxX + r, boxY, boxWidth - 2 * r, boxHeight); + display->fillRect(boxX, boxY + r, boxWidth, boxHeight - 2 * r); + display->fillCircle(boxX + r, boxY + r, r); + display->fillCircle(boxX + boxWidth - r - 1, boxY + r, r); + display->fillCircle(boxX + r, boxY + boxHeight - r - 1, r); + display->fillCircle(boxX + boxWidth - r - 1, boxY + boxHeight - r - 1, r); + + display->setColor(BLACK); + int iconX = boxX + (boxWidth - icon_width) / 2; + display->drawXbm(iconX, iconY, icon_width, icon_height, icon_bits); + + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(x + SCREEN_WIDTH / 2, labelY, label); + } + + // === Footer and headers (shared) === display->setFont(FONT_MEDIUM); + display->setColor(WHITE); display->setTextAlignment(TEXT_ALIGN_LEFT); const char *title = "meshtastic.org"; display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); - // Draw region in upper left display->setFont(FONT_SMALL); - if (upperMsg) - display->drawString(x + 0, y + 0, upperMsg); + if (upperMsg) display->drawString(x + 0, y + 0, upperMsg); - // Draw version and short name in upper right char buf[25]; snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); display->setTextAlignment(TEXT_ALIGN_RIGHT); display->drawString(x + SCREEN_WIDTH, y + 0, buf); screen->forceDisplay(); - - // Restore default alignment display->setTextAlignment(TEXT_ALIGN_LEFT); } From dbc0122bbd3b980e22bf3ed3da2bed4911f944ff Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 9 May 2025 23:31:56 -0400 Subject: [PATCH 100/265] Adjustments and alignments to IconScreen --- src/graphics/Screen.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 298b835b4..17ef5861f 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1,11 +1,10 @@ /* BaseUI -Developed by: -Ronald Garcia (aka HarukiToreda) -In collaboration with: -- JasonP: Graphics icon work, UI adjustments, code optimizations, enhancements, and testing -- TonyG (aka Topho) – Project management, structural planning, and testing +Developed and Maintained By: +- Ronald Garcia (HarukiToreda) – Lead development and implementation. +- JasonP (aka Xaositek) – Screen layout and icon design, UI improvements and testing. +- TonyG (aka Tropho) – Project management, structural planning, and testing This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -178,12 +177,12 @@ static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDispl int padding = 4; int boxWidth = max(icon_width, textWidth) + padding * 2; int boxHeight = icon_height + FONT_HEIGHT_SMALL + padding * 3; - int boxX = x + (SCREEN_WIDTH - boxWidth) / 2; + int boxX = x - 1 + (SCREEN_WIDTH - boxWidth) / 2; int boxY = y + (SCREEN_HEIGHT - boxHeight) / 2; display->setColor(WHITE); display->fillRect(boxX + r, boxY, boxWidth - 2 * r, boxHeight); - display->fillRect(boxX, boxY + r, boxWidth, boxHeight - 2 * r); + display->fillRect(boxX, boxY + r, boxWidth - 1, boxHeight - 2 * r); display->fillCircle(boxX + r, boxY + r, r); display->fillCircle(boxX + boxWidth - r - 1, boxY + r, r); display->fillCircle(boxX + r, boxY + boxHeight - r - 1, r); @@ -196,8 +195,8 @@ static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDispl int labelY = iconY + icon_height + padding; display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(x + SCREEN_WIDTH / 2, labelY, label); - display->drawString(x + SCREEN_WIDTH / 2 + 1, labelY, label); // faux bold + display->drawString(x + SCREEN_WIDTH / 2 - 3, labelY, label); + display->drawString(x + SCREEN_WIDTH / 2 - 2, labelY, label); // faux bold } else { // === TIGHT SMALL SCREEN LAYOUT === @@ -214,7 +213,7 @@ static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDispl display->setColor(WHITE); display->fillRect(boxX + r, boxY, boxWidth - 2 * r, boxHeight); - display->fillRect(boxX, boxY + r, boxWidth, boxHeight - 2 * r); + display->fillRect(boxX, boxY + r, boxWidth - 1, boxHeight - 2 * r); display->fillCircle(boxX + r, boxY + r, r); display->fillCircle(boxX + boxWidth - r - 1, boxY + r, r); display->fillCircle(boxX + r, boxY + boxHeight - r - 1, r); @@ -236,7 +235,8 @@ static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDispl display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); display->setFont(FONT_SMALL); - if (upperMsg) display->drawString(x + 0, y + 0, upperMsg); + if (upperMsg) + display->drawString(x + 0, y + 0, upperMsg); char buf[25]; snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); From 9c4dae3bf601274d6840475e026e411c9d8e471f Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 10 May 2025 00:23:13 -0400 Subject: [PATCH 101/265] Inline Emote and text feature --- src/graphics/Screen.cpp | 49 +++++++++++++++++++++++++++++++++-------- src/graphics/images.h | 2 ++ 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 17ef5861f..14b9ee423 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1119,6 +1119,43 @@ bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int return validCached; } +struct Emote { + const char *code; + const uint8_t *bitmap; + int width, height; +}; + +void drawStringWithEmotes(OLEDDisplay* display, int x, int y, const std::string& line, const Emote* emotes, int emoteCount) +{ + int cursorX = x; + const int fontHeight = FONT_HEIGHT_SMALL; + const int fontMidline = y + (fontHeight / 2); + + for (size_t i = 0; i < line.length();) { + bool matched = false; + + for (int e = 0; e < emoteCount; ++e) { + size_t emojiLen = strlen(emotes[e].code); + if (line.compare(i, emojiLen, emotes[e].code) == 0) { + // Vertically center + nudge upward for better alignment + int iconY = fontMidline - (emotes[e].height / 2) - 1; + display->drawXbm(cursorX, iconY, emotes[e].width, emotes[e].height, emotes[e].bitmap); + cursorX += emotes[e].width + 1; + i += emojiLen; + matched = true; + break; + } + } + + if (!matched) { + char c[2] = {line[i], '\0'}; + display->drawString(cursorX, y, c); + cursorX += display->getStringWidth(c); + ++i; + } + } +} + // **************************** // * Text Message Screen * // **************************** @@ -1182,13 +1219,6 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 bounceY = (bounceY + 1) % (bounceRange * 2); } - // === Emote rendering === - struct Emote { - const char *code; - const uint8_t *bitmap; - int width, height; - }; - const Emote emotes[] = {{"\U0001F44D", thumbup, thumbs_width, thumbs_height}, {"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, {"\U0001F60A", smiley, smiley_width, smiley_height}, @@ -1217,7 +1247,8 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 {"\U0001F495", heart, heart_width, heart_height}, {"\U0001F496", heart, heart_width, heart_height}, {"\U0001F497", heart, heart_width, heart_height}, - {"\U0001F498", heart, heart_width, heart_height}}; + {"\U0001F498", heart, heart_width, heart_height}, + {"\U0001F514", bell_alert, bell_alert_width, bell_alert_height}}; for (const Emote &e : emotes) { if (strcmp(msg, e.code) == 0) { @@ -1336,7 +1367,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 display->drawString(x + 4, lineY, lines[i].c_str()); display->setColor(WHITE); } else { - display->drawString(x, lineY, lines[i].c_str()); + drawStringWithEmotes(display, x, lineY, lines[i], emotes, sizeof(emotes)/sizeof(Emote)); } } } diff --git a/src/graphics/images.h b/src/graphics/images.h index ffc556a4f..6394217f9 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -391,6 +391,8 @@ const uint8_t icon_module[] PROGMEM = { }; // Bell icon for Alert Message +#define bell_alert_width 8 +#define bell_alert_height 8 const unsigned char bell_alert[] PROGMEM = {0b00011000, 0b00100100, 0b00100100, 0b01000010, 0b01000010, 0b01000010, 0b11111111, 0b00011000}; From 6bc9986f22fa4b60d0a287a483b8ec4e48e94694 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 10 May 2025 01:10:20 -0400 Subject: [PATCH 102/265] Spacing added in text to allow bigger emotes. --- src/graphics/Screen.cpp | 57 ++++++++++++++++++++++++++++++++++------- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 14b9ee423..858736d35 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1129,16 +1129,38 @@ void drawStringWithEmotes(OLEDDisplay* display, int x, int y, const std::string& { int cursorX = x; const int fontHeight = FONT_HEIGHT_SMALL; - const int fontMidline = y + (fontHeight / 2); + // === Step 1: Find tallest emote in the line === + int maxIconHeight = 0; + + for (size_t i = 0; i < line.length();) { + for (int e = 0; e < emoteCount; ++e) { + size_t emojiLen = strlen(emotes[e].code); + if (line.compare(i, emojiLen, emotes[e].code) == 0) { + if (emotes[e].height > maxIconHeight) + maxIconHeight = emotes[e].height; + i += emojiLen; + goto next_char; + } + } + i++; // move to next char if no emote match + next_char:; + } + + // === Step 2: Calculate vertical shift to center line === + int lineHeight = std::max(fontHeight, maxIconHeight); + int baselineOffset = (lineHeight - fontHeight) / 2; + int fontY = y + baselineOffset; + int fontMidline = fontY + fontHeight / 2; + + // === Step 3: Render text and icons centered in line === for (size_t i = 0; i < line.length();) { bool matched = false; for (int e = 0; e < emoteCount; ++e) { size_t emojiLen = strlen(emotes[e].code); if (line.compare(i, emojiLen, emotes[e].code) == 0) { - // Vertically center + nudge upward for better alignment - int iconY = fontMidline - (emotes[e].height / 2) - 1; + int iconY = fontMidline - emotes[e].height / 2 - 1; // slight nudge up display->drawXbm(cursorX, iconY, emotes[e].width, emotes[e].height, emotes[e].bitmap); cursorX += emotes[e].width + 1; i += emojiLen; @@ -1149,7 +1171,7 @@ void drawStringWithEmotes(OLEDDisplay* display, int x, int y, const std::string& if (!matched) { char c[2] = {line[i], '\0'}; - display->drawString(cursorX, y, c); + display->drawString(cursorX, fontY, c); cursorX += display->getStringWidth(c); ++i; } @@ -1310,9 +1332,24 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 lines.push_back(line); // === Scrolling logic === - const float rowHeight = FONT_HEIGHT_SMALL - 1; - const int totalHeight = lines.size() * rowHeight; - const int scrollStop = std::max(0, totalHeight - usableHeight); + std::vector rowHeights; + + for (const auto& line : lines) { + int maxHeight = FONT_HEIGHT_SMALL; + for (const Emote &e : emotes) { + if (line.find(e.code) != std::string::npos) { + if (e.height > maxHeight) + maxHeight = e.height; + } + } + rowHeights.push_back(maxHeight); + } + int totalHeight = 0; + for (size_t i = 1; i < rowHeights.size(); ++i) { + totalHeight += rowHeights[i]; + } + int usableScrollHeight = usableHeight - rowHeights[0]; // remove header height + int scrollStop = std::max(0, totalHeight - usableScrollHeight); static float scrollY = 0.0f; static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0; @@ -1357,8 +1394,10 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // === Render visible lines === for (size_t i = 0; i < lines.size(); ++i) { - int lineY = static_cast(i * rowHeight + yOffset); - if (lineY > -rowHeight && lineY < scrollBottom) { + int lineY = yOffset; + for (size_t j = 0; j < i; ++j) + lineY += rowHeights[j]; + if (lineY > -rowHeights[i] && lineY < scrollBottom) { if (i == 0 && isInverted) { drawRoundedHighlight(display, x, lineY, SCREEN_WIDTH, FONT_HEIGHT_SMALL - 1, cornerRadius); display->setColor(BLACK); From 48dc44ea8f45d3f8758bda19d233c9d40f2d0ded Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 10 May 2025 17:29:18 -0400 Subject: [PATCH 103/265] Fix for degree sign type characters on text message screen --- src/graphics/Screen.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 858736d35..f4bfb8eba 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1170,10 +1170,17 @@ void drawStringWithEmotes(OLEDDisplay* display, int x, int y, const std::string& } if (!matched) { - char c[2] = {line[i], '\0'}; - display->drawString(cursorX, fontY, c); - cursorX += display->getStringWidth(c); - ++i; + uint8_t c = static_cast(line[i]); + int charLen = 1; + + if ((c & 0xE0) == 0xC0) charLen = 2; // 2-byte UTF-8 + else if ((c & 0xF0) == 0xE0) charLen = 3; // 3-byte UTF-8 + else if ((c & 0xF8) == 0xF0) charLen = 4; // 4-byte UTF-8 + + std::string utf8char = line.substr(i, charLen); + display->drawString(cursorX, fontY, utf8char.c_str()); + cursorX += display->getStringWidth(utf8char.c_str()); + i += charLen; } } } From 6542c7bb470538dd025e29fc56a9ac3a4824f329 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 10 May 2025 23:05:08 -0400 Subject: [PATCH 104/265] Degree sign fix --- src/graphics/Screen.cpp | 74 ++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index f4bfb8eba..226c70581 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1131,56 +1131,71 @@ void drawStringWithEmotes(OLEDDisplay* display, int x, int y, const std::string& const int fontHeight = FONT_HEIGHT_SMALL; // === Step 1: Find tallest emote in the line === - int maxIconHeight = 0; - + int maxIconHeight = fontHeight; for (size_t i = 0; i < line.length();) { + bool matched = false; for (int e = 0; e < emoteCount; ++e) { size_t emojiLen = strlen(emotes[e].code); if (line.compare(i, emojiLen, emotes[e].code) == 0) { if (emotes[e].height > maxIconHeight) maxIconHeight = emotes[e].height; i += emojiLen; - goto next_char; + matched = true; + break; } } - i++; // move to next char if no emote match - next_char:; + if (!matched) { + uint8_t c = static_cast(line[i]); + if ((c & 0xE0) == 0xC0) i += 2; + else if ((c & 0xF0) == 0xE0) i += 3; + else if ((c & 0xF8) == 0xF0) i += 4; + else i += 1; + } } - // === Step 2: Calculate vertical shift to center line === + // === Step 2: Baseline alignment === int lineHeight = std::max(fontHeight, maxIconHeight); int baselineOffset = (lineHeight - fontHeight) / 2; int fontY = y + baselineOffset; int fontMidline = fontY + fontHeight / 2; - // === Step 3: Render text and icons centered in line === - for (size_t i = 0; i < line.length();) { - bool matched = false; + // === Step 3: Render line in segments === + size_t i = 0; + while (i < line.length()) { + // Look ahead for the next emote match + size_t nextEmotePos = std::string::npos; + const Emote* matchedEmote = nullptr; + size_t emojiLen = 0; for (int e = 0; e < emoteCount; ++e) { - size_t emojiLen = strlen(emotes[e].code); - if (line.compare(i, emojiLen, emotes[e].code) == 0) { - int iconY = fontMidline - emotes[e].height / 2 - 1; // slight nudge up - display->drawXbm(cursorX, iconY, emotes[e].width, emotes[e].height, emotes[e].bitmap); - cursorX += emotes[e].width + 1; - i += emojiLen; - matched = true; - break; + size_t pos = line.find(emotes[e].code, i); + if (pos != std::string::npos && (nextEmotePos == std::string::npos || pos < nextEmotePos)) { + nextEmotePos = pos; + matchedEmote = &emotes[e]; + emojiLen = strlen(emotes[e].code); } } - if (!matched) { - uint8_t c = static_cast(line[i]); - int charLen = 1; + // Render normal text segment up to the emote + if (nextEmotePos != std::string::npos && nextEmotePos > i) { + std::string textChunk = line.substr(i, nextEmotePos - i); + display->drawString(cursorX, fontY, textChunk.c_str()); + cursorX += display->getStringWidth(textChunk.c_str()); + i = nextEmotePos; + } - if ((c & 0xE0) == 0xC0) charLen = 2; // 2-byte UTF-8 - else if ((c & 0xF0) == 0xE0) charLen = 3; // 3-byte UTF-8 - else if ((c & 0xF8) == 0xF0) charLen = 4; // 4-byte UTF-8 - - std::string utf8char = line.substr(i, charLen); - display->drawString(cursorX, fontY, utf8char.c_str()); - cursorX += display->getStringWidth(utf8char.c_str()); - i += charLen; + // Render the emote (if found) + if (matchedEmote) { + int iconY = fontMidline - matchedEmote->height / 2 - 1; + display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap); + cursorX += matchedEmote->width + 1; + i += emojiLen; + } else { + // No more emotes β€” render the rest of the line + std::string remaining = line.substr(i); + display->drawString(cursorX, fontY, remaining.c_str()); + cursorX += display->getStringWidth(remaining.c_str()); + break; } } } @@ -1956,8 +1971,7 @@ struct NodeEntry { enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 }; static NodeListMode currentMode = MODE_LAST_HEARD; -static unsigned long lastModeSwitchTime = 0; -static int scrollIndex = 0; + static int scrollIndex = 0; // Use dynamic timing based on mode unsigned long getModeCycleIntervalMs() From 9fc208df5f277c4319806b9a57da749ea35df15d Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 11 May 2025 00:02:40 -0400 Subject: [PATCH 105/265] Bold effect on text in ** Like this ** --- src/graphics/Screen.cpp | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 226c70581..85c622917 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1161,7 +1161,16 @@ void drawStringWithEmotes(OLEDDisplay* display, int x, int y, const std::string& // === Step 3: Render line in segments === size_t i = 0; + bool inBold = false; + while (i < line.length()) { + // Check for ** start/end for faux bold + if (line.compare(i, 2, "**") == 0) { + inBold = !inBold; + i += 2; + continue; + } + // Look ahead for the next emote match size_t nextEmotePos = std::string::npos; const Emote* matchedEmote = nullptr; @@ -1176,16 +1185,24 @@ void drawStringWithEmotes(OLEDDisplay* display, int x, int y, const std::string& } } - // Render normal text segment up to the emote - if (nextEmotePos != std::string::npos && nextEmotePos > i) { - std::string textChunk = line.substr(i, nextEmotePos - i); + // Render normal text segment up to the emote or bold toggle + size_t nextControl = std::min(nextEmotePos, line.find("**", i)); + if (nextControl == std::string::npos) nextControl = line.length(); + + if (nextControl > i) { + std::string textChunk = line.substr(i, nextControl - i); + if (inBold) { + // Faux bold: draw twice, offset by 1px + display->drawString(cursorX + 1, fontY, textChunk.c_str()); + } display->drawString(cursorX, fontY, textChunk.c_str()); cursorX += display->getStringWidth(textChunk.c_str()); - i = nextEmotePos; + i = nextControl; + continue; } // Render the emote (if found) - if (matchedEmote) { + if (matchedEmote && i == nextEmotePos) { int iconY = fontMidline - matchedEmote->height / 2 - 1; display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap); cursorX += matchedEmote->width + 1; @@ -1193,6 +1210,9 @@ void drawStringWithEmotes(OLEDDisplay* display, int x, int y, const std::string& } else { // No more emotes β€” render the rest of the line std::string remaining = line.substr(i); + if (inBold) { + display->drawString(cursorX + 1, fontY, remaining.c_str()); + } display->drawString(cursorX, fontY, remaining.c_str()); cursorX += display->getStringWidth(remaining.c_str()); break; From 4b770ceade261806be76852e7d8811006c4db353 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 11 May 2025 01:43:56 -0400 Subject: [PATCH 106/265] Mute symbol on Header --- src/graphics/SharedUIDisplay.cpp | 109 +++++++++++------- src/graphics/SharedUIDisplay.h | 1 + src/modules/CannedMessageModule.cpp | 28 +++-- .../Telemetry/EnvironmentTelemetry.cpp | 3 +- 4 files changed, 85 insertions(+), 56 deletions(-) diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 4b37a1a8c..63edcc19e 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -12,6 +12,7 @@ namespace graphics // === Shared External State === bool hasUnreadMessage = false; +bool isMuted = false; // === Internal State === bool isBoltVisibleShared = true; @@ -24,13 +25,16 @@ uint32_t lastMailBlink = 0; // ********************************* void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r) { - display->fillRect(x + r, y, w - 2 * r, h); - display->fillRect(x, y + r, r, h - 2 * r); - display->fillRect(x + w - r, y + r, r, h - 2 * r); - display->fillCircle(x + r + 1, y + r, r); - display->fillCircle(x + w - r - 1, y + r, r); - display->fillCircle(x + r + 1, y + h - r - 1, r); - display->fillCircle(x + w - r - 1, y + h - r - 1, r); + // Draw the center and side rectangles + display->fillRect(x + r, y, w - 2 * r, h); // center bar + display->fillRect(x, y + r, r, h - 2 * r); // left edge + display->fillRect(x + w - r, y + r, r, h - 2 * r); // right edge + + // Draw the rounded corners using filled circles + display->fillCircle(x + r + 1, y + r, r); // top-left + display->fillCircle(x + w - r - 1, y + r, r); // top-right + display->fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left + display->fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right } // ************************* @@ -52,13 +56,17 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) const int screenW = display->getWidth(); const int screenH = display->getHeight(); + // === Draw background highlight if inverted === if (isInverted) { drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2); display->setColor(BLACK); } + // === Get battery charge percentage and charging status === int chargePercent = powerStatus->getBatteryChargePercent(); bool isCharging = powerStatus->getIsCharging() == meshtastic::OptionalBool::OptTrue; + + // === Animate lightning bolt blinking if charging === uint32_t now = millis(); if (isCharging && now - lastBlinkShared > 500) { isBoltVisibleShared = !isBoltVisibleShared; @@ -68,8 +76,9 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) bool useHorizontalBattery = (screenW > 128 && screenW > screenH); const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - // === Battery Icons === + // === Draw battery icon === if (useHorizontalBattery) { + // Wide screen battery layout int batteryX = 2; int batteryY = HEADER_OFFSET_Y + 2; display->drawXbm(batteryX, batteryY, 29, 15, batteryBitmap_h); @@ -81,10 +90,11 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 13); } } else { + // Tall screen battery layout int batteryX = 1; int batteryY = HEADER_OFFSET_Y + 1; #ifdef USE_EINK - batteryY += 2; + batteryY += 2; // Extra spacing on E-Ink #endif display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v); if (isCharging && isBoltVisibleShared) { @@ -114,53 +124,72 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) display->drawString(percentX + chargeNumWidth, textY, "%"); } - // === Time and Mail Icon === + // === Handle time display and alignment === uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); + char timeStr[10] = ""; + int alignX; + if (rtc_sec > 0) { + // Format time string (12h or 24h) long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; int hour = hms / SEC_PER_HOUR; int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - char timeStr[10]; snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); if (config.display.use_12h_clock) { bool isPM = hour >= 12; hour %= 12; - if (hour == 0) - hour = 12; + if (hour == 0) hour = 12; snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); } int timeStrWidth = display->getStringWidth(timeStr); - int timeX = screenW - xOffset - timeStrWidth + 4; - - if (hasUnreadMessage) { - if (now - lastMailBlink > 500) { - isMailIconVisible = !isMailIconVisible; - lastMailBlink = now; - } - - if (isMailIconVisible) { - if (useHorizontalBattery) { - int iconW = 16, iconH = 12; - int iconX = timeX - iconW - 4; - int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; - display->drawRect(iconX, iconY, iconW + 1, iconH); - display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); - display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); - } else { - int iconX = timeX - mail_width; - int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - display->drawXbm(iconX, iconY, mail_width, mail_height, mail); - } - } - } - - display->drawString(timeX, textY, timeStr); - if (isBold) - display->drawString(timeX - 1, textY, timeStr); + alignX = screenW - xOffset - timeStrWidth + 4; + } else { + // If time is not valid, reserve space for alignment anyway + int fallbackWidth = display->getStringWidth("12:34"); + alignX = screenW - xOffset - fallbackWidth + 4; } + // === Determine if mail icon should blink === + bool showMail = false; + if (hasUnreadMessage) { + if (now - lastMailBlink > 500) { + isMailIconVisible = !isMailIconVisible; + lastMailBlink = now; + } + showMail = isMailIconVisible; + } + + // === Draw Mail or Mute icon in the top-right corner === + if (showMail) { + if (useHorizontalBattery) { + int iconW = 16, iconH = 12; + int iconX = screenW - xOffset - iconW; + int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; + display->drawRect(iconX, iconY, iconW + 1, iconH); + display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); + display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); + } else { + int iconX = screenW - xOffset - mail_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + display->drawXbm(iconX, iconY, mail_width, mail_height, mail); + } + } else if (isMuted) { + const char* muteStr = "M"; + int mWidth = display->getStringWidth(muteStr); + int mX = screenW - xOffset - mWidth; + display->drawString(mX, textY, muteStr); + if (isBold) + display->drawString(mX + 1, textY, muteStr); + } else if (rtc_sec > 0) { + // Only draw the time if nothing else is shown + display->drawString(alignX, textY, timeStr); + if (isBold) + display->drawString(alignX - 1, textY, timeStr); + } + + // === Reset color back to white for following content === display->setColor(WHITE); } diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index 99508efde..3454d3c54 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -27,6 +27,7 @@ namespace graphics { // Shared state (declare inside namespace) extern bool hasUnreadMessage; +extern bool isMuted; // Rounded highlight (used for inverted headers) void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r); diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 5e0645f24..a91163482 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -14,6 +14,7 @@ #include "input/ScanAndSelect.h" #include "mesh/generated/meshtastic/cannedmessages.pb.h" #include "modules/AdminModule.h" +#include "graphics/SharedUIDisplay.h" #include "main.h" // for cardkb_found #include "modules/ExternalNotificationModule.h" // for buzzer control @@ -35,6 +36,7 @@ #define INACTIVATE_AFTER_MS 20000 extern ScanI2C::DeviceAddress cardkb_found; +extern bool graphics::isMuted; static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto"; @@ -463,23 +465,19 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) break; // mute (switch off/toggle) external notifications on fn+m case INPUT_BROKER_MSG_MUTE_TOGGLE: - if (moduleConfig.external_notification.enabled == true) { - if (externalNotificationModule->getMute()) { - externalNotificationModule->setMute(false); - if (screen) { - screen->removeFunctionSymbol("M"); - screen->showOverlayBanner("Notifications\nEnabled", 3000); - } - } else { - externalNotificationModule->stopNow(); - externalNotificationModule->setMute(true); - if (screen) { - screen->setFunctionSymbol("M"); - screen->showOverlayBanner("Notifications\nDisabled", 3000); + if (moduleConfig.external_notification.enabled == true) { + if (externalNotificationModule->getMute()) { + externalNotificationModule->setMute(false); + graphics::isMuted = false; + if (screen) screen->showOverlayBanner("Notifications\nEnabled", 3000); + } else { + externalNotificationModule->stopNow(); + externalNotificationModule->setMute(true); + graphics::isMuted = true; + if (screen) screen->showOverlayBanner("Notifications\nDisabled", 3000); } } - } - break; + break; case INPUT_BROKER_MSG_GPS_TOGGLE: #if !MESHTASTIC_EXCLUDE_GPS diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index d08cdb764..8b1c636ac 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -330,7 +330,8 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt // === Draw Title (Centered under header) === const int highlightHeight = FONT_HEIGHT_SMALL - 1; const int titleY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - const char *titleStr = "Environment"; + const char *titleStr = (SCREEN_WIDTH > 128) ? "Environment" : "Env."; + const int centerX = x + SCREEN_WIDTH / 2; // Use black text on white background if in inverted mode From d9bfed242c405524313e6d834f6e281bcf533a53 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 11 May 2025 02:07:40 -0400 Subject: [PATCH 107/265] Fixed bug making time dissapear when mute icon shows --- src/graphics/SharedUIDisplay.cpp | 160 +++++++++++++++++++------------ 1 file changed, 99 insertions(+), 61 deletions(-) diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 63edcc19e..7a89860dc 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -56,18 +56,19 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) const int screenW = display->getWidth(); const int screenH = display->getHeight(); - // === Draw background highlight if inverted === + // === Inverted Header Background === if (isInverted) { drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2); display->setColor(BLACK); } - // === Get battery charge percentage and charging status === + // === Battery State === int chargePercent = powerStatus->getBatteryChargePercent(); bool isCharging = powerStatus->getIsCharging() == meshtastic::OptionalBool::OptTrue; - - // === Animate lightning bolt blinking if charging === + static uint32_t lastBlinkShared = 0; + static bool isBoltVisibleShared = true; uint32_t now = millis(); + if (isCharging && now - lastBlinkShared > 500) { isBoltVisibleShared = !isBoltVisibleShared; lastBlinkShared = now; @@ -76,30 +77,28 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) bool useHorizontalBattery = (screenW > 128 && screenW > screenH); const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - // === Draw battery icon === + // === Battery Icons === if (useHorizontalBattery) { - // Wide screen battery layout int batteryX = 2; int batteryY = HEADER_OFFSET_Y + 2; display->drawXbm(batteryX, batteryY, 29, 15, batteryBitmap_h); - if (isCharging && isBoltVisibleShared) { + if (isCharging && isBoltVisibleShared) display->drawXbm(batteryX + 9, batteryY + 1, 9, 13, lightning_bolt_h); - } else { + else { display->drawXbm(batteryX + 8, batteryY, 12, 15, batteryBitmap_sidegaps_h); int fillWidth = 24 * chargePercent / 100; display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 13); } } else { - // Tall screen battery layout int batteryX = 1; int batteryY = HEADER_OFFSET_Y + 1; -#ifdef USE_EINK - batteryY += 2; // Extra spacing on E-Ink -#endif + #ifdef USE_EINK + batteryY += 2; + #endif display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v); - if (isCharging && isBoltVisibleShared) { + if (isCharging && isBoltVisibleShared) display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v); - } else { + else { display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v); int fillHeight = 8 * chargePercent / 100; int fillY = batteryY - fillHeight; @@ -107,7 +106,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) } } - // === Battery % Text === + // === Battery % Display === char chargeStr[4]; snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent); int chargeNumWidth = display->getStringWidth(chargeStr); @@ -124,18 +123,19 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) display->drawString(percentX + chargeNumWidth, textY, "%"); } - // === Handle time display and alignment === + // === Time and Right-aligned Icons === uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); - char timeStr[10] = ""; - int alignX; + char timeStr[10] = "--:--"; // Fallback display + int timeStrWidth = display->getStringWidth("12:34"); // Default alignment + int timeX = screenW - xOffset - timeStrWidth + 4; if (rtc_sec > 0) { - // Format time string (12h or 24h) + // === Build Time String === long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY; int hour = hms / SEC_PER_HOUR; int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute); + if (config.display.use_12h_clock) { bool isPM = hour >= 12; hour %= 12; @@ -143,54 +143,92 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); } - int timeStrWidth = display->getStringWidth(timeStr); - alignX = screenW - xOffset - timeStrWidth + 4; + timeStrWidth = display->getStringWidth(timeStr); + timeX = screenW - xOffset - timeStrWidth + 4; + + // === Show Mail or Mute Icon to the Left of Time === + int iconRightEdge = timeX - 2; + + static bool isMailIconVisible = true; + static uint32_t lastMailBlink = 0; + bool showMail = false; + + if (hasUnreadMessage) { + if (now - lastMailBlink > 500) { + isMailIconVisible = !isMailIconVisible; + lastMailBlink = now; + } + showMail = isMailIconVisible; + } + + if (showMail) { + if (useHorizontalBattery) { + int iconW = 16, iconH = 12; + int iconX = iconRightEdge - iconW; + int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; + display->drawRect(iconX, iconY, iconW + 1, iconH); + display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); + display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); + } else { + int iconX = iconRightEdge - mail_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + display->drawXbm(iconX, iconY, mail_width, mail_height, mail); + } + } else if (isMuted) { + const char* muteStr = "M"; + int mWidth = display->getStringWidth(muteStr); + int mX = iconRightEdge - mWidth; + display->drawString(mX, textY, muteStr); + if (isBold) + display->drawString(mX + 1, textY, muteStr); + } + + // === Draw Time === + display->drawString(timeX, textY, timeStr); + if (isBold) + display->drawString(timeX - 1, textY, timeStr); + } else { - // If time is not valid, reserve space for alignment anyway - int fallbackWidth = display->getStringWidth("12:34"); - alignX = screenW - xOffset - fallbackWidth + 4; - } + // === No Time Available: Mail/Mute Icon Moves to Far Right === + int iconRightEdge = screenW - xOffset; - // === Determine if mail icon should blink === - bool showMail = false; - if (hasUnreadMessage) { - if (now - lastMailBlink > 500) { - isMailIconVisible = !isMailIconVisible; - lastMailBlink = now; + static bool isMailIconVisible = true; + static uint32_t lastMailBlink = 0; + bool showMail = false; + + if (hasUnreadMessage) { + if (now - lastMailBlink > 500) { + isMailIconVisible = !isMailIconVisible; + lastMailBlink = now; + } + showMail = isMailIconVisible; } - showMail = isMailIconVisible; - } - // === Draw Mail or Mute icon in the top-right corner === - if (showMail) { - if (useHorizontalBattery) { - int iconW = 16, iconH = 12; - int iconX = screenW - xOffset - iconW; - int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; - display->drawRect(iconX, iconY, iconW + 1, iconH); - display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); - display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); - } else { - int iconX = screenW - xOffset - mail_width; - int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - display->drawXbm(iconX, iconY, mail_width, mail_height, mail); + if (showMail) { + if (useHorizontalBattery) { + int iconW = 16, iconH = 12; + int iconX = iconRightEdge - iconW; + int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; + display->drawRect(iconX, iconY, iconW + 1, iconH); + display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); + display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); + } else { + int iconX = iconRightEdge - mail_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + display->drawXbm(iconX, iconY, mail_width, mail_height, mail); + } + } else if (isMuted) { + const char* muteStr = "M"; + int mWidth = display->getStringWidth(muteStr); + int mX = iconRightEdge - mWidth; + display->drawString(mX, textY, muteStr); + if (isBold) + display->drawString(mX + 1, textY, muteStr); } - } else if (isMuted) { - const char* muteStr = "M"; - int mWidth = display->getStringWidth(muteStr); - int mX = screenW - xOffset - mWidth; - display->drawString(mX, textY, muteStr); - if (isBold) - display->drawString(mX + 1, textY, muteStr); - } else if (rtc_sec > 0) { - // Only draw the time if nothing else is shown - display->drawString(alignX, textY, timeStr); - if (isBold) - display->drawString(alignX - 1, textY, timeStr); } - // === Reset color back to white for following content === - display->setColor(WHITE); + display->setColor(WHITE); // Reset for other UI } + } // namespace graphics From 2f4f2b1202444e517b35f2e60b601d74362b4c74 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 11 May 2025 12:15:44 -0400 Subject: [PATCH 108/265] Bug fix where freetext screen activates when alert banner shows --- src/graphics/Screen.cpp | 7 ++++-- src/graphics/Screen.h | 3 +++ src/modules/CannedMessageModule.cpp | 36 ++++++++++++++++++++++++++--- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 85c622917..5bf55362a 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -71,6 +71,9 @@ along with this program. If not, see . using namespace meshtastic; /** @todo remove */ +String alertBannerMessage = ""; +uint32_t alertBannerUntil = 0; + namespace graphics { @@ -3826,8 +3829,8 @@ void Screen::setFrames(FrameFocus focus) ui->setFrames(normalFrames, numframes); ui->disableAllIndicators(); - // Add function overlay here. This can show when notifications muted, modifier key is active etc - static OverlayCallback overlays[] = {drawFunctionOverlay, drawCustomFrameIcons, drawAlertBannerOverlay}; + // Add overlays: frame icons and alert banner) + static OverlayCallback overlays[] = {drawCustomFrameIcons, drawAlertBannerOverlay}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 9db389296..005c0fa02 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -703,4 +703,7 @@ class Screen : public concurrency::OSThread } // namespace graphics +extern String alertBannerMessage; +extern uint32_t alertBannerUntil; + #endif \ No newline at end of file diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index a91163482..4246f000c 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -202,6 +202,28 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) // source at all) return 0; } + + // === Toggle Destination Selector with Tab === + if (event->kbchar == 0x09) { + if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { + // Exit selection + this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; + this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + } else { + // Enter selection + this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; + this->destIndex = 0; + this->scrollIndex = 0; + this->runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + } + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->notifyObservers(&e); + screen->forceDisplay(); + return 0; + } + if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) { return 0; // Ignore input while sending } @@ -435,9 +457,17 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) validEvent = true; } if (event->inputEvent == static_cast(ANYKEY)) { - // when inactive, this will switch to the freetext mode - if ((this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || - (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED)) { + // Prevent entering freetext mode while overlay banner is showing + extern String alertBannerMessage; + extern uint32_t alertBannerUntil; + if (alertBannerMessage.length() > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil)) { + return 0; + } + + if ((this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || + this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || + this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) && + (event->kbchar >= 32 && event->kbchar <= 126)) { this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; } From 7293e542ecdecbae7563b9e9c2b46c3b81e99300 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 11 May 2025 23:24:05 -0400 Subject: [PATCH 109/265] GPS toggle now tells if it's on or off --- src/ButtonThread.cpp | 21 +++++++++----- src/modules/CannedMessageModule.cpp | 44 ++++++++++++++++------------- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp index c9b4be8a7..991f55cdb 100644 --- a/src/ButtonThread.cpp +++ b/src/ButtonThread.cpp @@ -237,14 +237,21 @@ int32_t ButtonThread::runOnce() LOG_BUTTON("Mulitipress! %hux", multipressClickCount); switch (multipressClickCount) { #if HAS_GPS && !defined(ELECROW_ThinkNode_M1) - // 3 clicks: toggle GPS - case 3: - if (!config.device.disable_triple_click && (gps != nullptr)) { - gps->toggleGpsMode(); - if (screen) - screen->forceDisplay(true); // Force a new UI frame, then force an EInk update + // 3 clicks: toggle GPS + case 3: + if (!config.device.disable_triple_click && (gps != nullptr)) { + gps->toggleGpsMode(); + + const char* statusMsg = (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) + ? "GPS Enabled" + : "GPS Disabled"; + + if (screen) { + screen->forceDisplay(true); // Force a new UI frame, then force an EInk update + screen->showOverlayBanner(statusMsg, 3000); } - break; + } + break; #elif defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2) case 3: LOG_INFO("3 clicks: toggle buzzer"); diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 4246f000c..b9d616c3a 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -510,17 +510,19 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) break; case INPUT_BROKER_MSG_GPS_TOGGLE: - #if !MESHTASTIC_EXCLUDE_GPS +#if !MESHTASTIC_EXCLUDE_GPS if (gps != nullptr) { gps->toggleGpsMode(); + const char* statusMsg = (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) + ? "GPS Enabled" + : "GPS Disabled"; + if (screen) { + screen->forceDisplay(); + screen->showOverlayBanner(statusMsg, 3000); + } } - if (screen) { - screen->forceDisplay(); - screen->showOverlayBanner("GPS Toggled", 3000); - } - #endif +#endif break; - case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: if (config.bluetooth.enabled == true) { config.bluetooth.enabled = false; @@ -544,6 +546,21 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) if (screen) screen->showOverlayBanner("Node Info\nUpdate Sent", 3000); } break; + case INPUT_BROKER_MSG_SHUTDOWN: + if (screen) + screen->showOverlayBanner("Shutting down..."); + shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + validEvent = true; + break; + + case INPUT_BROKER_MSG_REBOOT: + if (screen) + screen->showOverlayBanner("Rebooting...", 0); // stays on screen + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + validEvent = true; + break; case INPUT_BROKER_MSG_DISMISS_FRAME: // fn+del: dismiss screen frames like text or waypoint // Avoid opening the canned message screen frame // We're only handling the keypress here by convention, this has nothing to do with canned messages @@ -899,19 +916,6 @@ int32_t CannedMessageModule::runOnce() case INPUT_BROKER_MSG_RIGHT: // already handled above break; - // handle fn+s for shutdown - case INPUT_BROKER_MSG_SHUTDOWN: - if (screen) - shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - break; - // and fn+r for reboot - case INPUT_BROKER_MSG_REBOOT: - if (screen) - screen->showOverlayBanner("Rebooting...", 0); // stays on screen - rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - break; default: if (this->highlight != 0x00) { break; From c40fdc9a431aa556ea9351a29b42fb80a3a8ab9f Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 12 May 2025 13:58:13 -0400 Subject: [PATCH 110/265] Adjustments to GPS screen --- src/graphics/Screen.cpp | 146 +++++++++++++++++++--------------------- 1 file changed, 70 insertions(+), 76 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 5bf55362a..ce341a0cb 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1128,7 +1128,7 @@ struct Emote { int width, height; }; -void drawStringWithEmotes(OLEDDisplay* display, int x, int y, const std::string& line, const Emote* emotes, int emoteCount) +void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount) { int cursorX = x; const int fontHeight = FONT_HEIGHT_SMALL; @@ -1149,10 +1149,14 @@ void drawStringWithEmotes(OLEDDisplay* display, int x, int y, const std::string& } if (!matched) { uint8_t c = static_cast(line[i]); - if ((c & 0xE0) == 0xC0) i += 2; - else if ((c & 0xF0) == 0xE0) i += 3; - else if ((c & 0xF8) == 0xF0) i += 4; - else i += 1; + if ((c & 0xE0) == 0xC0) + i += 2; + else if ((c & 0xF0) == 0xE0) + i += 3; + else if ((c & 0xF8) == 0xF0) + i += 4; + else + i += 1; } } @@ -1176,7 +1180,7 @@ void drawStringWithEmotes(OLEDDisplay* display, int x, int y, const std::string& // Look ahead for the next emote match size_t nextEmotePos = std::string::npos; - const Emote* matchedEmote = nullptr; + const Emote *matchedEmote = nullptr; size_t emojiLen = 0; for (int e = 0; e < emoteCount; ++e) { @@ -1190,7 +1194,8 @@ void drawStringWithEmotes(OLEDDisplay* display, int x, int y, const std::string& // Render normal text segment up to the emote or bold toggle size_t nextControl = std::min(nextEmotePos, line.find("**", i)); - if (nextControl == std::string::npos) nextControl = line.length(); + if (nextControl == std::string::npos) + nextControl = line.length(); if (nextControl > i) { std::string textChunk = line.substr(i, nextControl - i); @@ -1379,7 +1384,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // === Scrolling logic === std::vector rowHeights; - for (const auto& line : lines) { + for (const auto &line : lines) { int maxHeight = FONT_HEIGHT_SMALL; for (const Emote &e : emotes) { if (line.find(e.code) != std::string::npos) { @@ -1451,7 +1456,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 display->drawString(x + 4, lineY, lines[i].c_str()); display->setColor(WHITE); } else { - drawStringWithEmotes(display, x, lineY, lines[i], emotes, sizeof(emotes)/sizeof(Emote)); + drawStringWithEmotes(display, x, lineY, lines[i], emotes, sizeof(emotes) / sizeof(Emote)); } } } @@ -1994,7 +1999,7 @@ struct NodeEntry { enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 }; static NodeListMode currentMode = MODE_LAST_HEARD; - static int scrollIndex = 0; +static int scrollIndex = 0; // Use dynamic timing based on mode unsigned long getModeCycleIntervalMs() @@ -2561,43 +2566,13 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i // === Content below header === - // === First Row: Region / Channel Utilization and GPS === + // === First Row: Region / Channel Utilization and Uptime === bool origBold = config.display.heading_bold; config.display.heading_bold = false; // Display Region and Channel Utilization - config.display.heading_bold = false; - drawNodes(display, x + 1, compactFirstLine + 3, nodeStatus, -1, false, "online"); -#if HAS_GPS - auto number_of_satellites = gpsStatus->getNumSatellites(); - int gps_rightchar_offset = (SCREEN_WIDTH > 128) ? -51 : -46; - if (number_of_satellites < 10) { - gps_rightchar_offset += (SCREEN_WIDTH > 128) ? 8 : 6; - } - if (!gpsStatus || !gpsStatus->getIsConnected()) { - gps_rightchar_offset = (SCREEN_WIDTH > 128) ? -20 : 2; - } - - if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - String displayLine = ""; - if (config.position.fixed_position) { - gps_rightchar_offset = (SCREEN_WIDTH > 128) ? -80 : -50; - displayLine = "Fixed GPS"; - } else { - gps_rightchar_offset = (SCREEN_WIDTH > 128) ? -58 : -38; - displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; - } - display->drawString(SCREEN_WIDTH + gps_rightchar_offset, compactFirstLine, displayLine); - } else { - drawGPS(display, SCREEN_WIDTH + gps_rightchar_offset, compactFirstLine + 3, gpsStatus); - } -#endif - - config.display.heading_bold = origBold; - - // === Second Row: Uptime and Voltage === uint32_t uptime = millis() / 1000; char uptimeStr[6]; uint32_t minutes = uptime / 60, hours = minutes / 60, days = hours / 24; @@ -2618,7 +2593,28 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i char uptimeFullStr[16]; snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); - display->drawString(x, compactSecondLine, uptimeFullStr); + display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeFullStr), compactFirstLine, uptimeFullStr); + + config.display.heading_bold = origBold; + + // === Second Row: Satellites and Voltage === + config.display.heading_bold = false; + +#if HAS_GPS + auto number_of_satellites = gpsStatus->getNumSatellites(); + + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + String displayLine = ""; + if (config.position.fixed_position) { + displayLine = "Fixed GPS"; + } else { + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + } + display->drawString(0, compactSecondLine, displayLine); + } else { + drawGPS(display, 0, compactSecondLine + 3, gpsStatus); + } +#endif char batStr[20]; if (powerStatus->getHasBattery()) { @@ -2630,6 +2626,8 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i display->drawString(x + SCREEN_WIDTH - display->getStringWidth("USB"), compactSecondLine, String("USB")); } + config.display.heading_bold = origBold; + // === Third Row: LongName Centered === // Blank @@ -2799,30 +2797,22 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat #if HAS_GPS bool origBold = config.display.heading_bold; config.display.heading_bold = false; - if (config.position.fixed_position) { - display->drawString(x, compactFirstLine, "Sat:"); - if (SCREEN_WIDTH > 128) { - drawGPS(display, x + 32, compactFirstLine + 3, gpsStatus); + + auto number_of_satellites = gpsStatus->getNumSatellites(); + String Satelite_String = "Sat:"; + display->drawString(0, compactFirstLine, Satelite_String); + String displayLine = ""; + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + if (config.position.fixed_position) { + displayLine = "Fixed GPS"; } else { - drawGPS(display, x + 23, compactFirstLine + 3, gpsStatus); - } - } else if (!gpsStatus || !gpsStatus->getIsConnected()) { - String displayLine = - config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; - display->drawString(x, compactFirstLine, "Sat:"); - if (SCREEN_WIDTH > 128) { - display->drawString(x + 32, compactFirstLine, displayLine); - } else { - display->drawString(x + 23, compactFirstLine, displayLine); + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } + display->drawString(display->getStringWidth(Satelite_String) + 3, compactFirstLine, displayLine); } else { - display->drawString(x, compactFirstLine, "Sat:"); - if (SCREEN_WIDTH > 128) { - drawGPS(display, x + 32, compactFirstLine + 3, gpsStatus); - } else { - drawGPS(display, x + 23, compactFirstLine + 3, gpsStatus); - } + drawGPS(display, display->getStringWidth(Satelite_String) + 3, compactFirstLine + 3, gpsStatus); } + config.display.heading_bold = origBold; // === Update GeoCoord === @@ -2841,22 +2831,26 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat validHeading = !isnan(heading); } - // === Second Row: Altitude === - String displayLine; - displayLine = "Alt: " + String(geoCoord.getAltitude()) + "m"; - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) - displayLine = "Alt: " + String(geoCoord.getAltitude() * METERS_TO_FEET) + "ft"; - display->drawString(x, compactSecondLine, displayLine); + // If GPS is off, no need to display these parts + if (displayLine != "GPS off" && displayLine != "No GPS") { - // === Third Row: Latitude === - char latStr[32]; - snprintf(latStr, sizeof(latStr), "Lat: %.5f", geoCoord.getLatitude() * 1e-7); - display->drawString(x, compactThirdLine, latStr); + // === Second Row: Altitude === + String displayLine; + displayLine = "Alt: " + String(geoCoord.getAltitude()) + "m"; + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) + displayLine = "Alt: " + String(geoCoord.getAltitude() * METERS_TO_FEET) + "ft"; + display->drawString(x, compactSecondLine, displayLine); - // === Fifth Row: Longitude === - char lonStr[32]; - snprintf(lonStr, sizeof(lonStr), "Lon: %.5f", geoCoord.getLongitude() * 1e-7); - display->drawString(x, compactFourthLine, lonStr); + // === Third Row: Latitude === + char latStr[32]; + snprintf(latStr, sizeof(latStr), "Lat: %.5f", geoCoord.getLatitude() * 1e-7); + display->drawString(x, compactThirdLine, latStr); + + // === Fourth Row: Longitude === + char lonStr[32]; + snprintf(lonStr, sizeof(lonStr), "Lon: %.5f", geoCoord.getLongitude() * 1e-7); + display->drawString(x, compactFourthLine, lonStr); + } // === Draw Compass if heading is valid === if (validHeading) { From fe25e5efd5686b6f7da17a4b4296170aec95b630 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 13 May 2025 00:25:55 -0400 Subject: [PATCH 111/265] Mute icon and bell emote added --- src/graphics/SharedUIDisplay.cpp | 36 +++++++++++++------------------- src/graphics/images.h | 29 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 7a89860dc..855b7bfbc 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -31,10 +31,10 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, display->fillRect(x + w - r, y + r, r, h - 2 * r); // right edge // Draw the rounded corners using filled circles - display->fillCircle(x + r + 1, y + r, r); // top-left - display->fillCircle(x + w - r - 1, y + r, r); // top-right - display->fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left - display->fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right + display->fillCircle(x + r + 1, y + r, r); // top-left + display->fillCircle(x + w - r - 1, y + r, r); // top-right + display->fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left + display->fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right } // ************************* @@ -92,9 +92,9 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) } else { int batteryX = 1; int batteryY = HEADER_OFFSET_Y + 1; - #ifdef USE_EINK +#ifdef USE_EINK batteryY += 2; - #endif +#endif display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v); if (isCharging && isBoltVisibleShared) display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v); @@ -125,7 +125,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) // === Time and Right-aligned Icons === uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); - char timeStr[10] = "--:--"; // Fallback display + char timeStr[10] = "--:--"; // Fallback display int timeStrWidth = display->getStringWidth("12:34"); // Default alignment int timeX = screenW - xOffset - timeStrWidth + 4; @@ -139,7 +139,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) if (config.display.use_12h_clock) { bool isPM = hour >= 12; hour %= 12; - if (hour == 0) hour = 12; + if (hour == 0) + hour = 12; snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a"); } @@ -175,12 +176,9 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) display->drawXbm(iconX, iconY, mail_width, mail_height, mail); } } else if (isMuted) { - const char* muteStr = "M"; - int mWidth = display->getStringWidth(muteStr); - int mX = iconRightEdge - mWidth; - display->drawString(mX, textY, muteStr); - if (isBold) - display->drawString(mX + 1, textY, muteStr); + int iconX = iconRightEdge - mute_symbol_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol); } // === Draw Time === @@ -218,17 +216,13 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) display->drawXbm(iconX, iconY, mail_width, mail_height, mail); } } else if (isMuted) { - const char* muteStr = "M"; - int mWidth = display->getStringWidth(muteStr); - int mX = iconRightEdge - mWidth; - display->drawString(mX, textY, muteStr); - if (isBold) - display->drawString(mX + 1, textY, muteStr); + int iconX = iconRightEdge - mute_symbol_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + display->drawXbm(iconX, iconY, mute_symbol_width, bell_alert_height, bell_alert); } } display->setColor(WHITE); // Reset for other UI } - } // namespace graphics diff --git a/src/graphics/images.h b/src/graphics/images.h index 6394217f9..b4847b5dc 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -244,6 +244,22 @@ static unsigned char poo[] PROGMEM = { 0x01, 0x00, 0x1F, 0x20, 0x03, 0xE0, 0x03, 0x20, 0x07, 0x7E, 0x04, 0x30, 0xFE, 0x0F, 0xFC, 0x1F, 0xF0, 0x00, 0xF0, 0x0F, }; +#define bell_icon_width 30 +#define bell_icon_height 30 +static unsigned char bell_icon[] PROGMEM = { + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11110000, + 0b00000011, 0b00000000, 0b00000000, 0b11111100, 0b00001111, 0b00000000, 0b00000000, 0b00001111, 0b00111100, 0b00000000, + 0b00000000, 0b00000011, 0b00110000, 0b00000000, 0b10000000, 0b00000001, 0b01100000, 0b00000000, 0b11000000, 0b00000000, + 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, + 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, + 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, + 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000, 0b10000000, 0b00000000, 0b01100000, 0b00000000, + 0b10000000, 0b00000001, 0b01110000, 0b00000000, 0b10000000, 0b00000011, 0b00110000, 0b00000000, 0b00000000, 0b00000011, + 0b00011000, 0b00000000, 0b00000000, 0b00000110, 0b11110000, 0b11111111, 0b11111111, 0b00000011, 0b00000000, 0b00001100, + 0b00001100, 0b00000000, 0b00000000, 0b00011000, 0b00000110, 0b00000000, 0b00000000, 0b11111000, 0b00000111, 0b00000000, + 0b00000000, 0b11100000, 0b00000001, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}; + #define mail_width 10 #define mail_height 7 static const unsigned char mail[] PROGMEM = { @@ -390,6 +406,19 @@ const uint8_t icon_module[] PROGMEM = { 0b00011000 // β–‘β–‘β–‘β–ˆβ–ˆβ–‘β–‘β–‘ }; +#define mute_symbol_width 8 +#define mute_symbol_height 8 +const uint8_t mute_symbol[] PROGMEM = { + 0b00011001, // β–ˆ + 0b00100110, // β–ˆ + 0b00100100, // β–ˆβ–ˆβ–ˆβ–ˆ + 0b01001010, // β–ˆ β–ˆ β–ˆ + 0b01010010, // β–ˆ β–ˆ β–ˆ + 0b01100010, // β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ + 0b11111111, // β–ˆ β–ˆ + 0b10011000, // β–ˆ +}; + // Bell icon for Alert Message #define bell_alert_width 8 #define bell_alert_height 8 From efb3f85cd06d5cc9840bf3cbad8d86751d86e833 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 13 May 2025 00:49:59 -0400 Subject: [PATCH 112/265] Fix for emote and mute icon --- src/graphics/Screen.cpp | 2 +- src/graphics/SharedUIDisplay.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index ce341a0cb..622191e17 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1320,7 +1320,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 {"\U0001F496", heart, heart_width, heart_height}, {"\U0001F497", heart, heart_width, heart_height}, {"\U0001F498", heart, heart_width, heart_height}, - {"\U0001F514", bell_alert, bell_alert_width, bell_alert_height}}; + {"\U0001F514", bell_icon, bell_icon_width, bell_icon_height}}; for (const Emote &e : emotes) { if (strcmp(msg, e.code) == 0) { diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 855b7bfbc..bd768f4d8 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -218,7 +218,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) } else if (isMuted) { int iconX = iconRightEdge - mute_symbol_width; int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - display->drawXbm(iconX, iconY, mute_symbol_width, bell_alert_height, bell_alert); + display->drawXbm(iconX, iconY, mute_symbol_width, bell_alert_height, mute_symbol); } } From 9cba2e7b7f35662d584464b384efbebdf4e2b848 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 13 May 2025 10:00:34 -0400 Subject: [PATCH 113/265] Screen logo, mute icon, and screen wake on new node --- src/graphics/Screen.cpp | 20 ++++++++++---------- src/graphics/SharedUIDisplay.cpp | 26 ++++++++++++++++++++------ src/graphics/images.h | 8 ++++++++ src/modules/NodeInfoModule.cpp | 7 ------- 4 files changed, 38 insertions(+), 23 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 622191e17..d935881ae 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -3,8 +3,8 @@ BaseUI Developed and Maintained By: - Ronald Garcia (HarukiToreda) – Lead development and implementation. -- JasonP (aka Xaositek) – Screen layout and icon design, UI improvements and testing. -- TonyG (aka Tropho) – Project management, structural planning, and testing +- JasonP (Xaositek) – Screen layout and icon design, UI improvements and testing. +- TonyG (Tropho) – Project management, structural planning, and testing This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -178,22 +178,22 @@ static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDispl if (SCREEN_WIDTH > 128) { // === ORIGINAL WIDE SCREEN LAYOUT (unchanged) === int padding = 4; - int boxWidth = max(icon_width, textWidth) + padding * 2; - int boxHeight = icon_height + FONT_HEIGHT_SMALL + padding * 3; + int boxWidth = max(icon_width, textWidth) + (padding * 2) + 16; + int boxHeight = icon_height + FONT_HEIGHT_SMALL + (padding * 3) - 8; int boxX = x - 1 + (SCREEN_WIDTH - boxWidth) / 2; - int boxY = y + (SCREEN_HEIGHT - boxHeight) / 2; + int boxY = y - 6 + (SCREEN_HEIGHT - boxHeight) / 2; display->setColor(WHITE); display->fillRect(boxX + r, boxY, boxWidth - 2 * r, boxHeight); display->fillRect(boxX, boxY + r, boxWidth - 1, boxHeight - 2 * r); - display->fillCircle(boxX + r, boxY + r, r); - display->fillCircle(boxX + boxWidth - r - 1, boxY + r, r); - display->fillCircle(boxX + r, boxY + boxHeight - r - 1, r); - display->fillCircle(boxX + boxWidth - r - 1, boxY + boxHeight - r - 1, r); + display->fillCircle(boxX + r, boxY + r, r); // Upper Left + display->fillCircle(boxX + boxWidth - r - 1, boxY + r, r); // Upper Right + display->fillCircle(boxX + r, boxY + boxHeight - r - 1, r); // Lower Left + display->fillCircle(boxX + boxWidth - r - 1, boxY + boxHeight - r - 1, r); // Lower Right display->setColor(BLACK); int iconX = boxX + (boxWidth - icon_width) / 2; - int iconY = boxY + padding; + int iconY = boxY + padding - 2; display->drawXbm(iconX, iconY, icon_width, icon_height, icon_bits); int labelY = iconY + icon_height + padding; diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index bd768f4d8..39c2f3fec 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -56,6 +56,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) const int screenW = display->getWidth(); const int screenH = display->getHeight(); + const bool useBigIcons = (screenW > 128); + // === Inverted Header Background === if (isInverted) { drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2); @@ -176,9 +178,15 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) display->drawXbm(iconX, iconY, mail_width, mail_height, mail); } } else if (isMuted) { - int iconX = iconRightEdge - mute_symbol_width; - int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol); + if (useBigIcons) { + int iconX = iconRightEdge - mute_symbol_big_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; + display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); + } else { + int iconX = iconRightEdge - mute_symbol_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol); + } } // === Draw Time === @@ -216,9 +224,15 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) display->drawXbm(iconX, iconY, mail_width, mail_height, mail); } } else if (isMuted) { - int iconX = iconRightEdge - mute_symbol_width; - int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; - display->drawXbm(iconX, iconY, mute_symbol_width, bell_alert_height, mute_symbol); + if (useBigIcons) { + int iconX = iconRightEdge - mute_symbol_big_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; + display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); + } else { + int iconX = iconRightEdge - mute_symbol_width; + int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol); + } } } diff --git a/src/graphics/images.h b/src/graphics/images.h index b4847b5dc..cdd0c3502 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -419,6 +419,14 @@ const uint8_t mute_symbol[] PROGMEM = { 0b10011000, // β–ˆ }; +#define mute_symbol_big_width 16 +#define mute_symbol_big_height 16 +const uint8_t mute_symbol_big[] PROGMEM = {0b00000001, 0b00000000, 0b11000010, 0b00000011, 0b00110100, 0b00001100, 0b00011000, + 0b00001000, 0b00011000, 0b00010000, 0b00101000, 0b00010000, 0b01001000, 0b00010000, + 0b10001000, 0b00010000, 0b00001000, 0b00010001, 0b00001000, 0b00010010, 0b00001000, + 0b00010100, 0b00000100, 0b00101000, 0b11111100, 0b00111111, 0b01000000, 0b00100010, + 0b10000000, 0b01000001, 0b00000000, 0b10000000}; + // Bell icon for Alert Message #define bell_alert_width 8 #define bell_alert_height 8 diff --git a/src/modules/NodeInfoModule.cpp b/src/modules/NodeInfoModule.cpp index ce4a6bd06..b80624887 100644 --- a/src/modules/NodeInfoModule.cpp +++ b/src/modules/NodeInfoModule.cpp @@ -18,13 +18,6 @@ bool NodeInfoModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes bool wasBroadcast = isBroadcast(mp.to); - // Show new nodes on LCD screen - if (wasBroadcast) { - String lcd = String("Joined: ") + p.long_name + "\n"; - if (screen) - screen->print(lcd.c_str()); - } - // if user has changed while packet was not for us, inform phone if (hasChanged && !wasBroadcast && !isToUs(&mp)) service->sendToPhone(packetPool.allocCopy(mp)); From ace63eee8d5710b82fbeb42fcbb01f9525e52c5c Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 13 May 2025 14:15:56 -0400 Subject: [PATCH 114/265] Fix for dismissing channel frame when replied. --- src/graphics/Screen.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index d935881ae..5b97a5f80 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -4387,13 +4387,18 @@ int Screen::handleStatusUpdate(const meshtastic::Status *arg) return 0; } -// Handles when message is received would jump to text message frame. +// Handles when message is received; will jump to text message frame. int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) { if (showingNormalScreen) { if (packet->from == 0) { - // Outgoing message - setFrames(FOCUS_PRESERVE); // Stay on same frame, silently add/remove frames + // Outgoing message (likely sent from phone) + devicestate.has_rx_text_message = false; + memset(&devicestate.rx_text_message, 0, sizeof(devicestate.rx_text_message)); + dismissedFrames.textMessage = true; + hasUnreadMessage = false; // Clear unread state when user replies + + setFrames(FOCUS_PRESERVE); // Stay on same frame, silently update frame list } else { // Incoming message devicestate.has_rx_text_message = true; // Needed to include the message frame @@ -4412,7 +4417,6 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) String banner; // Match bell character or exact alert text - // if (msg == "\x07" || msg.indexOf("Alert Bell Character") != -1) { if (msg.indexOf("\x07") != -1) { banner = "Alert Received"; } else { From 18d11d28d482a411ad19e51ad81225cec5f60d77 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 15 May 2025 12:14:24 -0400 Subject: [PATCH 115/265] Line added to non-inverted header --- src/graphics/Screen.cpp | 4 ++-- src/graphics/SharedUIDisplay.cpp | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 5b97a5f80..8b201da27 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1504,7 +1504,7 @@ static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const NodeStat !defined(DISPLAY_FORCE_SMALL_FONTS) display->drawFastImage(x, y + 3, 8, 8, imgUser); #else - display->drawFastImage(x, y, 8, 8, imgUser); + display->drawFastImage(x, y + 1, 8, 8, imgUser); #endif display->drawString(x + 10, y - 2, usersString); int string_offset = (SCREEN_WIDTH > 128) ? 2 : 1; @@ -2571,7 +2571,7 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i config.display.heading_bold = false; // Display Region and Channel Utilization - drawNodes(display, x + 1, compactFirstLine + 3, nodeStatus, -1, false, "online"); + drawNodes(display, x + 1, compactFirstLine + 2, nodeStatus, -1, false, "online"); uint32_t uptime = millis() / 1000; char uptimeStr[6]; diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 39c2f3fec..080ca66f4 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -62,6 +62,12 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) if (isInverted) { drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2); display->setColor(BLACK); + } else { + if (screenW > 128) { + display->drawLine(0, 20, screenW, 20); + } else { + display->drawLine(0, 14, screenW, 14); + } } // === Battery State === From 512183c39fb5ef06caa58a496e712a1383c7531a Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 15 May 2025 12:15:03 -0400 Subject: [PATCH 116/265] Line added to text message screen --- src/graphics/Screen.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 8b201da27..ea17801c6 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1334,6 +1334,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 display->setColor(WHITE); } else { display->drawString(x, 0, headerStr); + if (SCREEN_WIDTH > 128) { + display->drawLine(0, 20, SCREEN_WIDTH, 20); + } else { + display->drawLine(0, 14, SCREEN_WIDTH, 14); + } } // Center the emote below header + apply bounce @@ -1441,6 +1446,13 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 int scrollOffset = static_cast(scrollY); int yOffset = -scrollOffset; + if (!isInverted) { + if (SCREEN_WIDTH > 128) { + display->drawLine(0, yOffset + 20, SCREEN_WIDTH, yOffset + 20); + } else { + display->drawLine(0, yOffset + 14, SCREEN_WIDTH, yOffset + 14); + } + } // === Render visible lines === for (size_t i = 0; i < lines.size(); ++i) { From a8294b983ab4f4a295f0c9eecea253b5ae9516c5 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 16 May 2025 02:14:50 -0400 Subject: [PATCH 117/265] Fix for Magnetic compass. --- src/motion/ICM20948Sensor.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/motion/ICM20948Sensor.cpp b/src/motion/ICM20948Sensor.cpp index d03633124..7f296a2d8 100755 --- a/src/motion/ICM20948Sensor.cpp +++ b/src/motion/ICM20948Sensor.cpp @@ -190,12 +190,18 @@ bool ICM20948Singleton::init(ScanI2C::FoundDevice device) #endif // startup -#ifdef Wire1 - ICM_20948_Status_e status = - begin(device.address.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire, device.address.address == ICM20948_ADDR ? 1 : 0); -#else - ICM_20948_Status_e status = begin(Wire, device.address.address == ICM20948_ADDR ? 1 : 0); -#endif + #if defined(WIRE_INTERFACES_COUNT) && (WIRE_INTERFACES_COUNT > 1) + TwoWire &bus = (device.address.port == ScanI2C::I2CPort::WIRE1 ? Wire1 : Wire); + #else + TwoWire &bus = Wire; // fallback if only one I2C interface + #endif + + bool bAddr = (device.address.address == 0x69); + delay(100); + + LOG_DEBUG("ICM20948 begin on addr 0x%02X (port=%d, bAddr=%d)", device.address.address, device.address.port, bAddr); + + ICM_20948_Status_e status = begin(bus, bAddr); if (status != ICM_20948_Stat_Ok) { LOG_DEBUG("ICM20948 init begin - %s", statusString()); return false; From 61d918751e3e0b47f159e42a84cd9ee051c1df8c Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sat, 17 May 2025 15:07:46 -0500 Subject: [PATCH 118/265] Unify the native display config between legacy display and MUI --- src/graphics/TFTDisplay.cpp | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 14787baff..cb3c9c395 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -666,6 +666,8 @@ class LGFX : public lgfx::LGFX_Device public: LGFX(void) { + int _width = 0; + int _height = 0; if (settingsMap[displayPanel] == st7789) _panel_instance = new lgfx::Panel_ST7789; else if (settingsMap[displayPanel] == st7735) @@ -686,7 +688,13 @@ class LGFX : public lgfx::LGFX_Device _panel_instance = new lgfx::Panel_NULL; LOG_ERROR("Unknown display panel configured!"); } - + if (settingsMap[displayRotate]) { + _width = settingsMap[displayHeight]; + _height = settingsMap[displayWidth]; + } else { + _width = settingsMap[displayWidth]; + _height = settingsMap[displayHeight]; + } auto buscfg = _bus_instance.config(); buscfg.spi_mode = 0; buscfg.spi_host = settingsMap[displayspidev]; @@ -700,11 +708,17 @@ class LGFX : public lgfx::LGFX_Device LOG_DEBUG("Height: %d, Width: %d ", settingsMap[displayHeight], settingsMap[displayWidth]); cfg.pin_cs = settingsMap[displayCS]; // Pin number where CS is connected (-1 = disable) cfg.pin_rst = settingsMap[displayReset]; - cfg.panel_width = settingsMap[displayWidth]; // actual displayable width - cfg.panel_height = settingsMap[displayHeight]; // actual displayable height + cfg.panel_width = _width; // actual displayable width + cfg.panel_height = _height; // actual displayable height cfg.offset_x = settingsMap[displayOffsetX]; // Panel offset amount in X direction cfg.offset_y = settingsMap[displayOffsetY]; // Panel offset amount in Y direction - cfg.offset_rotation = settingsMap[displayOffsetRotate]; // Rotation direction value offset 0~7 (4~7 is mirrored) + if (settingsMap[displayOffsetRotate] = 3) { + cfg.offset_rotation = 0; + } else if (settingsMap[displayOffsetRotate] = 7) { + cfg.offset_rotation = 4; + } else { + cfg.offset_rotation = settingsMap[displayOffsetRotate] + 1; // Rotation direction value offset 0~7 (4~7 is mirrored) + } cfg.invert = settingsMap[displayInvert]; // Set to true if the light/darkness of the panel is reversed _panel_instance->config(cfg); @@ -977,11 +991,7 @@ TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY g backlightEnable = p; #if ARCH_PORTDUINO - if (settingsMap[displayRotate]) { - setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayHeight], settingsMap[configNames::displayWidth]); - } else { - setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayWidth], settingsMap[configNames::displayHeight]); - } + setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayWidth], settingsMap[configNames::displayHeight]); #elif defined(SCREEN_ROTATE) setGeometry(GEOMETRY_RAWMODE, TFT_HEIGHT, TFT_WIDTH); From e7352cada47df82ca5df33ca3f99f29dbbe41d76 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sat, 17 May 2025 16:24:53 -0500 Subject: [PATCH 119/265] First attempt at honoring config.display.displaymode --- src/graphics/Screen.cpp | 32 ++++++++++++++++------------- src/graphics/TFTDisplay.cpp | 16 ++++++--------- src/main.cpp | 14 ++++++------- src/modules/CannedMessageModule.cpp | 2 -- src/modules/CannedMessageModule.h | 2 -- src/modules/Modules.cpp | 8 +++++--- variants/portduino/platformio.ini | 5 ++--- 7 files changed, 37 insertions(+), 42 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 61999ee79..d38aadbdb 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1557,15 +1557,17 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O #elif defined(USE_ST7567) dispdev = new ST7567Wire(address.address, -1, -1, geometry, (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); -#elif ARCH_PORTDUINO && !HAS_TFT - if (settingsMap[displayPanel] != no_screen) { - LOG_DEBUG("Make TFTDisplay!"); - dispdev = new TFTDisplay(address.address, -1, -1, geometry, - (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); - } else { - dispdev = new AutoOLEDWire(address.address, -1, -1, geometry, - (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); - isAUTOOled = true; +#elif ARCH_PORTDUINO + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + if (settingsMap[displayPanel] != no_screen) { + LOG_DEBUG("Make TFTDisplay!"); + dispdev = new TFTDisplay(address.address, -1, -1, geometry, + (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + } else { + dispdev = new AutoOLEDWire(address.address, -1, -1, geometry, + (address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE); + isAUTOOled = true; + } } #else dispdev = new AutoOLEDWire(address.address, -1, -1, geometry, @@ -1789,11 +1791,13 @@ void Screen::setup() #endif serialSinceMsec = millis(); -#if ARCH_PORTDUINO && !HAS_TFT - if (settingsMap[touchscreenModule]) { - touchScreenImpl1 = - new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); - touchScreenImpl1->init(); +#if ARCH_PORTDUINO + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + if (settingsMap[touchscreenModule]) { + touchScreenImpl1 = + new TouchScreenImpl1(dispdev->getWidth(), dispdev->getHeight(), static_cast(dispdev)->getTouch); + touchScreenImpl1->init(); + } } #elif HAS_TOUCHSCREEN touchScreenImpl1 = diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index cb3c9c395..30a61fb2e 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -653,7 +653,7 @@ static LGFX *tft = nullptr; #include // Graphics and font library for ILI9342 driver chip static TFT_eSPI *tft = nullptr; // Invoke library, pins defined in User_Setup.h -#elif ARCH_PORTDUINO && HAS_SCREEN != 0 && !HAS_TFT +#elif ARCH_PORTDUINO #include // Graphics and font library for ST7735 driver chip class LGFX : public lgfx::LGFX_Device @@ -708,17 +708,11 @@ class LGFX : public lgfx::LGFX_Device LOG_DEBUG("Height: %d, Width: %d ", settingsMap[displayHeight], settingsMap[displayWidth]); cfg.pin_cs = settingsMap[displayCS]; // Pin number where CS is connected (-1 = disable) cfg.pin_rst = settingsMap[displayReset]; - cfg.panel_width = _width; // actual displayable width - cfg.panel_height = _height; // actual displayable height + cfg.panel_width = _width; // actual displayable width + cfg.panel_height = _height; // actual displayable height cfg.offset_x = settingsMap[displayOffsetX]; // Panel offset amount in X direction cfg.offset_y = settingsMap[displayOffsetY]; // Panel offset amount in Y direction - if (settingsMap[displayOffsetRotate] = 3) { - cfg.offset_rotation = 0; - } else if (settingsMap[displayOffsetRotate] = 7) { - cfg.offset_rotation = 4; - } else { - cfg.offset_rotation = settingsMap[displayOffsetRotate] + 1; // Rotation direction value offset 0~7 (4~7 is mirrored) - } + cfg.offset_rotation = settingsMap[displayOffsetRotate]; // Rotation direction value offset 0~7 (4~7 is mirrored) cfg.invert = settingsMap[displayInvert]; // Set to true if the light/darkness of the panel is reversed _panel_instance->config(cfg); @@ -1179,6 +1173,8 @@ bool TFTDisplay::connect() tft->setRotation(1); // T-Deck has the TFT in landscape #elif defined(T_WATCH_S3) || defined(SENSECAP_INDICATOR) tft->setRotation(2); // T-Watch S3 left-handed orientation +#elif ARCH_PORTDUINO + tft->setRotation(0); #else tft->setRotation(3); // Orient horizontal and wide underneath the silkscreen name label #endif diff --git a/src/main.cpp b/src/main.cpp index 1e46d9db1..66fa8db05 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -372,11 +372,9 @@ void setup() SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0); #endif -#if !HAS_TFT meshtastic_Config_DisplayConfig_OledType screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; OLEDDISPLAY_GEOMETRY screen_geometry = GEOMETRY_128_64; -#endif #ifdef USE_SEGGER auto mode = false ? SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL : SEGGER_RTT_MODE_NO_BLOCK_TRIM; @@ -540,7 +538,9 @@ void setup() #endif #if HAS_TFT - tftSetup(); + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + tftSetup(); + } #endif // Currently only the tbeam has a PMU @@ -605,7 +605,6 @@ void setup() } #endif -#if !HAS_TFT auto screenInfo = i2cScanner->firstScreen(); screen_found = screenInfo.type != ScanI2C::DeviceType::NONE ? screenInfo.address : ScanI2C::ADDRESS_NONE; @@ -623,7 +622,6 @@ void setup() screen_model = meshtastic_Config_DisplayConfig_OledType::meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; } } -#endif #define UPDATE_FROM_SCANNER(FIND_FN) @@ -791,11 +789,9 @@ void setup() else playStartMelody(); -#if !HAS_TFT // fixed screen override? if (config.display.oled != meshtastic_Config_DisplayConfig_OledType_OLED_AUTO) screen_model = config.display.oled; -#endif #if defined(USE_SH1107) screen_model = meshtastic_Config_DisplayConfig_OledType_OLED_SH1107; // set dimension of 128x128 @@ -865,7 +861,9 @@ void setup() // Initialize the screen first so we can show the logo while we start up everything else. #if HAS_SCREEN - screen = new graphics::Screen(screen_found, screen_model, screen_geometry); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + screen = new graphics::Screen(screen_found, screen_model, screen_geometry); + } #endif // setup TZ prior to time actions. #if !MESHTASTIC_EXCLUDE_TZ diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index c16c0e4b3..eb6e92124 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -983,7 +983,6 @@ bool CannedMessageModule::interceptingKeyboardInput() } } -#if !HAS_TFT void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { char buffer[50]; @@ -1146,7 +1145,6 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st } } } -#endif //! HAS_TFT ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &mp) { diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index a91933a0f..8b2b728f3 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -118,9 +118,7 @@ class CannedMessageModule : public SinglePortModule, public ObservableshouldDraw(); } virtual Observable *getUIFrameObservable() override { return this; } virtual bool interceptingKeyboardInput() override; -#if !HAS_TFT virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; -#endif virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) override; diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index fac2ca976..f5e815765 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -183,9 +183,11 @@ void setupModules() aSerialKeyboardImpl->init(); #endif // INPUTBROKER_MATRIX_TYPE #endif // HAS_BUTTON -#if ARCH_PORTDUINO && !HAS_TFT - aLinuxInputImpl = new LinuxInputImpl(); - aLinuxInputImpl->init(); +#if ARCH_PORTDUINO + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + aLinuxInputImpl = new LinuxInputImpl(); + aLinuxInputImpl->init(); + } #endif #if HAS_TRACKBALL && !MESHTASTIC_EXCLUDE_INPUTBROKER trackballInterruptImpl1 = new TrackballInterruptImpl1(); diff --git a/variants/portduino/platformio.ini b/variants/portduino/platformio.ini index fe89ad6e6..4f1d25f02 100644 --- a/variants/portduino/platformio.ini +++ b/variants/portduino/platformio.ini @@ -26,7 +26,8 @@ build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunctio -D RAM_SIZE=16384 -D USE_X11=1 -D HAS_TFT=1 - -D HAS_SCREEN=0 + -D HAS_SCREEN=1 + -D LV_CACHE_DEF_SIZE=6291456 -D LV_BUILD_TEST=0 -D LV_USE_LIBINPUT=1 @@ -41,7 +42,6 @@ build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunctio !pkg-config --libs openssl --silence-errors || : build_src_filter = ${native_base.build_src_filter} - - [env:native-fb] extends = native_base @@ -72,7 +72,6 @@ build_flags = ${native_base.build_flags} -Os -ffunction-sections -fdata-sections !pkg-config --libs openssl --silence-errors || : build_src_filter = ${native_base.build_src_filter} - - [env:native-tft-debug] extends = native_base From afc5d1fdeb721f21c2250a0b73cdef5269c9d85f Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sat, 17 May 2025 18:25:22 -0500 Subject: [PATCH 120/265] Get MUI and legacy screen co-existing on Portduino --- src/PowerFSM.cpp | 6 ++++-- src/main.cpp | 19 ++++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index dbe4796cf..7e7494046 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -198,7 +198,8 @@ static void powerEnter() LOG_INFO("Loss of power in Powered"); powerFSM.trigger(EVENT_POWER_DISCONNECTED); } else { - screen->setOn(true); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR ) + screen->setOn(true); setBluetoothEnable(true); // within enter() the function getState() returns the state we came from @@ -221,7 +222,8 @@ static void powerIdle() static void powerExit() { - screen->setOn(true); + if (screen) + screen->setOn(true); setBluetoothEnable(true); // Mothballed: print change of power-state to device screen diff --git a/src/main.cpp b/src/main.cpp index 66fa8db05..fb9799a43 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -537,12 +537,6 @@ void setup() digitalWrite(AQ_SET_PIN, HIGH); #endif -#if HAS_TFT - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { - tftSetup(); - } -#endif - // Currently only the tbeam has a PMU // PMU initialization needs to be placed before i2c scanning power = new Power(); @@ -767,6 +761,12 @@ void setup() // but we need to do this after main cpu init (esp32setup), because we need the random seed set nodeDB = new NodeDB; +#if HAS_TFT + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + tftSetup(); + } +#endif + // If we're taking on the repeater role, use NextHopRouter and turn off 3V3_S rail because peripherals are not needed if (config.device.role == meshtastic_Config_DeviceConfig_Role_REPEATER) { router = new NextHopRouter(); @@ -955,7 +955,7 @@ void setup() defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) screen->setup(); #elif defined(ARCH_PORTDUINO) - if (screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) { + if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) && config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { screen->setup(); } #else @@ -963,8 +963,9 @@ void setup() screen->setup(); #endif #endif - - screen->print("Started...\n"); + if (screen) { + screen->print("Started...\n"); + } #ifdef PIN_PWR_DELAY_MS // This may be required to give the peripherals time to power up. From 212005bfe9f226d320378543816c83a2df0396e6 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sat, 17 May 2025 20:07:33 -0500 Subject: [PATCH 121/265] More checking for null screen --- src/PowerFSM.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index 7e7494046..02b637d3c 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -172,15 +172,18 @@ static void nbEnter() static void darkEnter() { setBluetoothEnable(true); - screen->setOn(false); + if (screen) + screen->setOn(false); } static void serialEnter() { LOG_DEBUG("State: SERIAL"); setBluetoothEnable(false); - screen->setOn(true); - screen->print("Serial connected\n"); + if (screen) { + screen->setOn(true); + screen->print("Serial connected\n"); + } } static void serialExit() From f1440a27d75d8ec71765c798dc345cecf51498ba Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sat, 17 May 2025 22:03:23 -0500 Subject: [PATCH 122/265] Check for null screen --- src/Power.cpp | 3 +- src/PowerFSM.cpp | 19 +++++++---- src/input/ScanAndSelect.cpp | 19 ++++++----- src/main.cpp | 3 +- src/mesh/http/ContentHandler.cpp | 1 + src/mesh/http/WebServer.cpp | 3 +- src/modules/AdminModule.cpp | 9 +++-- src/modules/RemoteHardwareModule.cpp | 3 +- src/modules/ReplyModule.cpp | 4 +-- src/motion/BMX160Sensor.cpp | 13 +++++--- src/motion/ICM20948Sensor.cpp | 13 +++++--- src/nimble/NimbleBluetooth.cpp | 49 +++++++++++++++------------- src/shutdown.h | 2 +- src/sleep.cpp | 4 +-- 14 files changed, 85 insertions(+), 60 deletions(-) diff --git a/src/Power.cpp b/src/Power.cpp index a9ed6360e..12b1a0ff2 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -853,7 +853,8 @@ int32_t Power::runOnce() #ifndef T_WATCH_S3 // FIXME - why is this triggering on the T-Watch S3? if (PMU->isPekeyLongPressIrq()) { LOG_DEBUG("PEK long button press"); - screen->setOn(false); + if (screen) + screen->setOn(false); } #endif diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index 02b637d3c..0263fed5f 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -82,7 +82,8 @@ static uint32_t secsSlept; static void lsEnter() { LOG_INFO("lsEnter begin, ls_secs=%u", config.power.ls_secs); - screen->setOn(false); + if (screen) + screen->setOn(false); secsSlept = 0; // How long have we been sleeping this time // LOG_INFO("lsEnter end"); @@ -160,7 +161,8 @@ static void lsExit() static void nbEnter() { LOG_DEBUG("State: NB"); - screen->setOn(false); + if (screen) + screen->setOn(false); #ifdef ARCH_ESP32 // Only ESP32 should turn off bluetooth setBluetoothEnable(false); @@ -190,7 +192,8 @@ static void serialExit() { // Turn bluetooth back on when we leave serial stream API setBluetoothEnable(true); - screen->print("Serial disconnected\n"); + if (screen) + screen->print("Serial disconnected\n"); } static void powerEnter() @@ -201,7 +204,7 @@ static void powerEnter() LOG_INFO("Loss of power in Powered"); powerFSM.trigger(EVENT_POWER_DISCONNECTED); } else { - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR ) + if (screen) screen->setOn(true); setBluetoothEnable(true); // within enter() the function getState() returns the state we came from @@ -226,7 +229,7 @@ static void powerIdle() static void powerExit() { if (screen) - screen->setOn(true); + screen->setOn(true); setBluetoothEnable(true); // Mothballed: print change of power-state to device screen @@ -237,7 +240,8 @@ static void powerExit() static void onEnter() { LOG_DEBUG("State: ON"); - screen->setOn(true); + if (screen) + screen->setOn(true); setBluetoothEnable(true); } @@ -251,7 +255,8 @@ static void onIdle() static void screenPress() { - screen->onPress(); + if (screen) + screen->onPress(); } static void bootEnter() diff --git a/src/input/ScanAndSelect.cpp b/src/input/ScanAndSelect.cpp index 1262f99b4..306f96f0d 100644 --- a/src/input/ScanAndSelect.cpp +++ b/src/input/ScanAndSelect.cpp @@ -84,7 +84,8 @@ int32_t ScanAndSelectInput::runOnce() // Dismiss the alert screen several seconds after it appears if (!Throttle::isWithinTimespanMs(alertingSinceMs, durationAlertMs)) { alertingNoMessage = false; - screen->endAlert(); + if (screen) + screen->endAlert(); } } @@ -183,13 +184,15 @@ void ScanAndSelectInput::alertNoMessage() alertingSinceMs = millis(); // Graphics code: the alert frame to show on screen - screen->startAlert([](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { - display->setTextAlignment(TEXT_ALIGN_CENTER_BOTH); - display->setFont(FONT_SMALL); - int16_t textX = display->getWidth() / 2; - int16_t textY = display->getHeight() / 2; - display->drawString(textX + x, textY + y, "No Canned Messages"); - }); + if (screen) { + screen->startAlert([](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + display->setTextAlignment(TEXT_ALIGN_CENTER_BOTH); + display->setFont(FONT_SMALL); + int16_t textX = display->getWidth() / 2; + int16_t textY = display->getHeight() / 2; + display->drawString(textX + x, textY + y, "No Canned Messages"); + }); + } } // Remove the canned message frame from screen diff --git a/src/main.cpp b/src/main.cpp index fb9799a43..0a08dec69 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1227,7 +1227,8 @@ void setup() nodeDB->saveToDisk(SEGMENT_CONFIG); if (!rIf->reconfigure()) { LOG_WARN("Reconfigure failed, rebooting"); - screen->startAlert("Rebooting..."); + if (screen) + screen->startAlert("Rebooting..."); rebootAtMsec = millis() + 5000; } } diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index 5841fe478..c1d5a8dbe 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -903,6 +903,7 @@ void handleBlinkLED(HTTPRequest *req, HTTPResponse *res) } } else { #if HAS_SCREEN + if (screen) screen->blink(); #endif } diff --git a/src/mesh/http/WebServer.cpp b/src/mesh/http/WebServer.cpp index 5f6ad9eb3..bf170de59 100644 --- a/src/mesh/http/WebServer.cpp +++ b/src/mesh/http/WebServer.cpp @@ -154,7 +154,8 @@ void createSSLCert() esp_task_wdt_reset(); #if HAS_SCREEN if (millis() / 1000 >= 3) { - screen->setSSLFrames(); + if (screen) + screen->setSSLFrames(); } #endif } diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 3ff4fa74d..5cf5d778d 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -200,14 +200,16 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta #if defined(ARCH_ESP32) #if !MESHTASTIC_EXCLUDE_BLUETOOTH if (!BleOta::getOtaAppVersion().isEmpty()) { - screen->startFirmwareUpdateScreen(); + if (screen) + screen->startFirmwareUpdateScreen(); BleOta::switchToOtaApp(); LOG_INFO("Rebooting to BLE OTA"); } #endif #if !MESHTASTIC_EXCLUDE_WIFI if (WiFiOTA::trySwitchToOTA()) { - screen->startFirmwareUpdateScreen(); + if (screen) + screen->startFirmwareUpdateScreen(); WiFiOTA::saveConfig(&config.network); LOG_INFO("Rebooting to WiFi OTA"); } @@ -1111,7 +1113,8 @@ void AdminModule::handleGetDeviceUIConfig(const meshtastic_MeshPacket &req) void AdminModule::reboot(int32_t seconds) { LOG_INFO("Reboot in %d seconds", seconds); - screen->startAlert("Rebooting..."); + if (screen) + screen->startAlert("Rebooting..."); rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000); } diff --git a/src/modules/RemoteHardwareModule.cpp b/src/modules/RemoteHardwareModule.cpp index 9bc8512b6..c49c57eda 100644 --- a/src/modules/RemoteHardwareModule.cpp +++ b/src/modules/RemoteHardwareModule.cpp @@ -84,7 +84,8 @@ bool RemoteHardwareModule::handleReceivedProtobuf(const meshtastic_MeshPacket &r switch (p.type) { case meshtastic_HardwareMessage_Type_WRITE_GPIOS: { // Print notification to LCD screen - screen->print("Write GPIOs\n"); + if (screen) + screen->print("Write GPIOs\n"); pinModes(p.gpio_mask, OUTPUT, availablePins); for (uint8_t i = 0; i < NUM_GPIOS; i++) { diff --git a/src/modules/ReplyModule.cpp b/src/modules/ReplyModule.cpp index c4f63c6b1..37cbd30d8 100644 --- a/src/modules/ReplyModule.cpp +++ b/src/modules/ReplyModule.cpp @@ -14,8 +14,8 @@ meshtastic_MeshPacket *ReplyModule::allocReply() // The incoming message is in p.payload LOG_INFO("Received message from=0x%0x, id=%d, msg=%.*s", req.from, req.id, p.payload.size, p.payload.bytes); #endif - - screen->print("Send reply\n"); + if (screen) + screen->print("Send reply\n"); const char *replyStr = "Message Received"; auto reply = allocDataPacket(); // Allocate a packet for sending diff --git a/src/motion/BMX160Sensor.cpp b/src/motion/BMX160Sensor.cpp index a3909ea3a..003ee850c 100755 --- a/src/motion/BMX160Sensor.cpp +++ b/src/motion/BMX160Sensor.cpp @@ -37,7 +37,8 @@ int32_t BMX160Sensor::runOnce() if (!showingScreen) { powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration showingScreen = true; - screen->startAlert((FrameCallback)drawFrameCalibration); + if (screen) + screen->startAlert((FrameCallback)drawFrameCalibration); } if (magAccel.x > highestX) @@ -58,7 +59,8 @@ int32_t BMX160Sensor::runOnce() doCalibration = false; endCalibrationAt = 0; showingScreen = false; - screen->endAlert(); + if (screen) + screen->endAlert(); } // LOG_DEBUG("BMX160 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX, @@ -103,8 +105,8 @@ int32_t BMX160Sensor::runOnce() heading += 270; break; } - - screen->setHeading(heading); + if (screen) + screen->setHeading(heading); #endif return MOTION_SENSOR_CHECK_INTERVAL_MS; @@ -118,7 +120,8 @@ void BMX160Sensor::calibrate(uint16_t forSeconds) doCalibration = true; uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided endCalibrationAt = millis() + calibrateFor; - screen->setEndCalibration(endCalibrationAt); + if (screen) + screen->setEndCalibration(endCalibrationAt); #endif } diff --git a/src/motion/ICM20948Sensor.cpp b/src/motion/ICM20948Sensor.cpp index d03633124..a4c82c6ec 100755 --- a/src/motion/ICM20948Sensor.cpp +++ b/src/motion/ICM20948Sensor.cpp @@ -60,7 +60,8 @@ int32_t ICM20948Sensor::runOnce() if (!showingScreen) { powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration showingScreen = true; - screen->startAlert((FrameCallback)drawFrameCalibration); + if (screen) + screen->startAlert((FrameCallback)drawFrameCalibration); } if (magX > highestX) @@ -81,7 +82,8 @@ int32_t ICM20948Sensor::runOnce() doCalibration = false; endCalibrationAt = 0; showingScreen = false; - screen->endAlert(); + if (screen) + screen->endAlert(); } // LOG_DEBUG("ICM20948 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX, @@ -124,8 +126,8 @@ int32_t ICM20948Sensor::runOnce() heading += 270; break; } - - screen->setHeading(heading); + if (screen) + screen->setHeading(heading); #endif // Wake on motion using polling - this is not as efficient as using hardware interrupt pin (see above) @@ -159,7 +161,8 @@ void ICM20948Sensor::calibrate(uint16_t forSeconds) doCalibration = true; uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided endCalibrationAt = millis() + calibrateFor; - screen->setEndCalibration(endCalibrationAt); + if (screen) + screen->setEndCalibration(endCalibrationAt); #endif } // ---------------------------------------------------------------------- diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 009439f25..80787092d 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -94,31 +94,33 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks bluetoothStatus->updateStatus(new meshtastic::BluetoothStatus(std::to_string(passkey))); #if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus - screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { - char btPIN[16] = "888888"; - snprintf(btPIN, sizeof(btPIN), "%06u", passkey); - int x_offset = display->width() / 2; - int y_offset = display->height() <= 80 ? 0 : 32; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(x_offset + x, y_offset + y, "Bluetooth"); + if (screen) { + screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void { + char btPIN[16] = "888888"; + snprintf(btPIN, sizeof(btPIN), "%06u", passkey); + int x_offset = display->width() / 2; + int y_offset = display->height() <= 80 ? 0 : 32; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, y_offset + y, "Bluetooth"); - display->setFont(FONT_SMALL); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; - display->drawString(x_offset + x, y_offset + y, "Enter this code"); + display->setFont(FONT_SMALL); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5; + display->drawString(x_offset + x, y_offset + y, "Enter this code"); - display->setFont(FONT_LARGE); - String displayPin(btPIN); - String pin = displayPin.substring(0, 3) + " " + displayPin.substring(3, 6); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; - display->drawString(x_offset + x, y_offset + y, pin); + display->setFont(FONT_LARGE); + String displayPin(btPIN); + String pin = displayPin.substring(0, 3) + " " + displayPin.substring(3, 6); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; + display->drawString(x_offset + x, y_offset + y, pin); - display->setFont(FONT_SMALL); - String deviceName = "Name: "; - deviceName.concat(getDeviceName()); - y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; - display->drawString(x_offset + x, y_offset + y, deviceName); - }); + display->setFont(FONT_SMALL); + String deviceName = "Name: "; + deviceName.concat(getDeviceName()); + y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; + display->drawString(x_offset + x, y_offset + y, deviceName); + }); + } #endif passkeyShowing = true; @@ -134,7 +136,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks // Todo: migrate this display code back into Screen class, and observe bluetoothStatus if (passkeyShowing) { passkeyShowing = false; - screen->endAlert(); + if (screen) + screen->endAlert(); } } diff --git a/src/shutdown.h b/src/shutdown.h index f02cb7964..63066c988 100644 --- a/src/shutdown.h +++ b/src/shutdown.h @@ -41,7 +41,7 @@ void powerCommandsCheck() } #if defined(ARCH_ESP32) || defined(ARCH_NRF52) - if (shutdownAtMsec) { + if (shutdownAtMsec && screen) { screen->startAlert("Shutting down..."); } #endif diff --git a/src/sleep.cpp b/src/sleep.cpp index 8ffb08b04..2fe4c71cd 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -221,8 +221,8 @@ void doDeepSleep(uint32_t msecToWake, bool skipPreflight = false, bool skipSaveN #endif powerMon->setState(meshtastic_PowerMon_State_CPU_DeepSleep); - - screen->doDeepSleep(); // datasheet says this will draw only 10ua + if (screen) + screen->doDeepSleep(); // datasheet says this will draw only 10ua if (!skipSaveNodeDb) { nodeDB->saveToDisk(); From 61ebce524161f11bb53107324c8c0fcf607df17c Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 18 May 2025 19:46:48 -0500 Subject: [PATCH 123/265] Get t-deck working with both UIs for tft build --- src/main.cpp | 10 +++-- src/modules/Modules.cpp | 63 ++++++++++++++++++------------- variants/portduino/platformio.ini | 2 +- variants/t-deck/platformio.ini | 3 +- variants/t-deck/variant.h | 5 +-- 5 files changed, 46 insertions(+), 37 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 0a08dec69..de6d175f8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -953,9 +953,11 @@ void setup() // the current region name) #if defined(ST7701_CS) || defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || \ defined(ST7789_CS) || defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) - screen->setup(); + if (screen) + screen->setup(); #elif defined(ARCH_PORTDUINO) - if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) && config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + if ((screen_found.port != ScanI2C::I2CPort::NO_I2C || settingsMap[displayPanel]) && + config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { screen->setup(); } #else @@ -964,8 +966,8 @@ void setup() #endif #endif if (screen) { - screen->print("Started...\n"); - } + screen->print("Started...\n"); + } #ifdef PIN_PWR_DELAY_MS // This may be required to give the peripherals time to power up. diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index f5e815765..f0dafa332 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -104,7 +104,9 @@ void setupModules() { if (config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER) { #if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER - inputBroker = new InputBroker(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + inputBroker = new InputBroker(); + } #endif #if !MESHTASTIC_EXCLUDE_ADMIN adminModule = new AdminModule(); @@ -152,36 +154,40 @@ void setupModules() // Example: Put your module here // new ReplyModule(); #if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER - rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); - if (!rotaryEncoderInterruptImpl1->init()) { - delete rotaryEncoderInterruptImpl1; - rotaryEncoderInterruptImpl1 = nullptr; - } - upDownInterruptImpl1 = new UpDownInterruptImpl1(); - if (!upDownInterruptImpl1->init()) { - delete upDownInterruptImpl1; - upDownInterruptImpl1 = nullptr; - } + + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + rotaryEncoderInterruptImpl1 = new RotaryEncoderInterruptImpl1(); + if (!rotaryEncoderInterruptImpl1->init()) { + delete rotaryEncoderInterruptImpl1; + rotaryEncoderInterruptImpl1 = nullptr; + } + upDownInterruptImpl1 = new UpDownInterruptImpl1(); + if (!upDownInterruptImpl1->init()) { + delete upDownInterruptImpl1; + upDownInterruptImpl1 = nullptr; + } #if HAS_SCREEN - // In order to have the user button dismiss the canned message frame, this class lightly interacts with the Screen class - scanAndSelectInput = new ScanAndSelectInput(); - if (!scanAndSelectInput->init()) { - delete scanAndSelectInput; - scanAndSelectInput = nullptr; - } + // In order to have the user button dismiss the canned message frame, this class lightly interacts with the Screen + // class + scanAndSelectInput = new ScanAndSelectInput(); + if (!scanAndSelectInput->init()) { + delete scanAndSelectInput; + scanAndSelectInput = nullptr; + } #endif - cardKbI2cImpl = new CardKbI2cImpl(); - cardKbI2cImpl->init(); + cardKbI2cImpl = new CardKbI2cImpl(); + cardKbI2cImpl->init(); #ifdef INPUTBROKER_MATRIX_TYPE - kbMatrixImpl = new KbMatrixImpl(); - kbMatrixImpl->init(); + kbMatrixImpl = new KbMatrixImpl(); + kbMatrixImpl->init(); #endif // INPUTBROKER_MATRIX_TYPE #ifdef INPUTBROKER_SERIAL_TYPE - aSerialKeyboardImpl = new SerialKeyboardImpl(); - aSerialKeyboardImpl->init(); + aSerialKeyboardImpl = new SerialKeyboardImpl(); + aSerialKeyboardImpl->init(); #endif // INPUTBROKER_MATRIX_TYPE + } #endif // HAS_BUTTON #if ARCH_PORTDUINO if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { @@ -190,14 +196,19 @@ void setupModules() } #endif #if HAS_TRACKBALL && !MESHTASTIC_EXCLUDE_INPUTBROKER - trackballInterruptImpl1 = new TrackballInterruptImpl1(); - trackballInterruptImpl1->init(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + trackballInterruptImpl1 = new TrackballInterruptImpl1(); + trackballInterruptImpl1->init(); + } #endif + LOG_DEBUG("location5"); #ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE expressLRSFiveWayInput = new ExpressLRSFiveWay(); #endif #if HAS_SCREEN && !MESHTASTIC_EXCLUDE_CANNEDMESSAGES - cannedMessageModule = new CannedMessageModule(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + cannedMessageModule = new CannedMessageModule(); + } #endif #if ARCH_PORTDUINO new HostMetricsModule(); diff --git a/variants/portduino/platformio.ini b/variants/portduino/platformio.ini index 4f1d25f02..6da827508 100644 --- a/variants/portduino/platformio.ini +++ b/variants/portduino/platformio.ini @@ -56,7 +56,7 @@ build_flags = ${native_base.build_flags} -Os -ffunction-sections -fdata-sections -D USE_FRAMEBUFFER=1 -D LV_COLOR_DEPTH=32 -D HAS_TFT=1 - -D HAS_SCREEN=0 + -D HAS_SCREEN=1 -D LV_BUILD_TEST=0 -D LV_USE_LOG=0 -D LV_USE_EVDEV=1 diff --git a/variants/t-deck/platformio.ini b/variants/t-deck/platformio.ini index 14fbee6cf..48d2eef89 100644 --- a/variants/t-deck/platformio.ini +++ b/variants/t-deck/platformio.ini @@ -26,7 +26,6 @@ build_flags = ${env:t-deck.build_flags} -D CONFIG_DISABLE_HAL_LOCKS=1 ; "feels" to be a bit more stable without locks -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 - -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 -D MESHTASTIC_EXCLUDE_WEBSERVER=1 -D MESHTASTIC_EXCLUDE_SERIAL=1 -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 @@ -39,7 +38,7 @@ build_flags = -D INPUTDRIVER_ENCODER_BTN=0 -D INPUTDRIVER_BUTTON_TYPE=0 -D HAS_SDCARD - -D HAS_SCREEN=0 + -D HAS_SCREEN=1 -D HAS_TFT=1 -D USE_I2S_BUZZER -D RAM_SIZE=5120 diff --git a/variants/t-deck/variant.h b/variants/t-deck/variant.h index a21c786b3..d63e235b0 100644 --- a/variants/t-deck/variant.h +++ b/variants/t-deck/variant.h @@ -1,6 +1,5 @@ #define TFT_CS 12 -#ifndef HAS_TFT // for TFT-UI the definitions are in device-ui #define BUTTON_PIN 0 // ST7789 TFT LCD @@ -24,7 +23,6 @@ #define SCREEN_ROTATE #define SCREEN_TRANSITION_FRAMERATE 5 #define BRIGHTNESS_DEFAULT 130 // Medium Low Brightness -#endif #define HAS_TOUCHSCREEN 1 #define SCREEN_TOUCH_INT 16 @@ -34,10 +32,9 @@ #define USE_POWERSAVE #define SLEEP_TIME 120 -#ifndef HAS_TFT #define BUTTON_PIN 0 // #define BUTTON_NEED_PULLUP -#endif + #define GPS_DEFAULT_NOT_PRESENT 1 #define GPS_RX_PIN 44 #define GPS_TX_PIN 43 From 7411dd4f0ca0d7c942424e802ec189646728776b Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 18 May 2025 19:57:49 -0500 Subject: [PATCH 124/265] guard against a few more null screen pointers --- src/main.cpp | 2 +- src/modules/WaypointModule.cpp | 4 ++++ src/motion/MotionSensor.cpp | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index de6d175f8..ad01dbc0e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -961,7 +961,7 @@ void setup() screen->setup(); } #else - if (screen_found.port != ScanI2C::I2CPort::NO_I2C) + if (screen_found.port != ScanI2C::I2CPort::NO_I2C && screen) screen->setup(); #endif #endif diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp index 479a973c2..1feab9402 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -48,6 +48,8 @@ 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) return false; @@ -79,6 +81,8 @@ 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) + return; // Prepare to draw display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); diff --git a/src/motion/MotionSensor.cpp b/src/motion/MotionSensor.cpp index 56738d355..840d64277 100755 --- a/src/motion/MotionSensor.cpp +++ b/src/motion/MotionSensor.cpp @@ -34,6 +34,8 @@ ScanI2C::I2CPort MotionSensor::devicePort() #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + if (screen == nullptr) + return; // int x_offset = display->width() / 2; // int y_offset = display->height() <= 80 ? 0 : 32; display->setTextAlignment(TEXT_ALIGN_LEFT); From d7697e6decd5bf025d9f06ebbfa35c9fb1cbda2a Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 18 May 2025 19:58:37 -0500 Subject: [PATCH 125/265] Trunk --- src/PowerFSM.cpp | 6 +++--- src/mesh/http/ContentHandler.cpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index 0263fed5f..333f73610 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -204,8 +204,8 @@ static void powerEnter() LOG_INFO("Loss of power in Powered"); powerFSM.trigger(EVENT_POWER_DISCONNECTED); } else { - if (screen) - screen->setOn(true); + if (screen) + screen->setOn(true); setBluetoothEnable(true); // within enter() the function getState() returns the state we came from @@ -229,7 +229,7 @@ static void powerIdle() static void powerExit() { if (screen) - screen->setOn(true); + screen->setOn(true); setBluetoothEnable(true); // Mothballed: print change of power-state to device screen diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index c1d5a8dbe..42ebb8417 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -903,8 +903,8 @@ void handleBlinkLED(HTTPRequest *req, HTTPResponse *res) } } else { #if HAS_SCREEN - if (screen) - screen->blink(); + if (screen) + screen->blink(); #endif } From 584ac8bdc9e9858986fe5eb450ccd76e9b4d01ca Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 18 May 2025 20:03:42 -0500 Subject: [PATCH 126/265] Default to MUI display for devices that support it --- src/mesh/NodeDB.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 4b1a6d64d..7e274e67e 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -499,6 +499,9 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) true; // FIXME: maybe false in the future, and setting region to enable it. (unset region forces it off) config.lora.override_duty_cycle = false; config.lora.config_ok_to_mqtt = false; +#if HAS_TFT // For the devices that support MUI, default to that + config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR; +#endif #ifdef USERPREFS_CONFIG_LORA_REGION config.lora.region = USERPREFS_CONFIG_LORA_REGION; #else From 660a7058c8301acd263115453675a1cc444bb55a Mon Sep 17 00:00:00 2001 From: mverch67 Date: Sat, 17 May 2025 22:33:03 +0200 Subject: [PATCH 127/265] fix rotation --- src/graphics/TFTDisplay.cpp | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 30a61fb2e..9d9b9ae1d 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -666,8 +666,6 @@ class LGFX : public lgfx::LGFX_Device public: LGFX(void) { - int _width = 0; - int _height = 0; if (settingsMap[displayPanel] == st7789) _panel_instance = new lgfx::Panel_ST7789; else if (settingsMap[displayPanel] == st7735) @@ -688,13 +686,7 @@ class LGFX : public lgfx::LGFX_Device _panel_instance = new lgfx::Panel_NULL; LOG_ERROR("Unknown display panel configured!"); } - if (settingsMap[displayRotate]) { - _width = settingsMap[displayHeight]; - _height = settingsMap[displayWidth]; - } else { - _width = settingsMap[displayWidth]; - _height = settingsMap[displayHeight]; - } + auto buscfg = _bus_instance.config(); buscfg.spi_mode = 0; buscfg.spi_host = settingsMap[displayspidev]; @@ -705,11 +697,16 @@ class LGFX : public lgfx::LGFX_Device _panel_instance->setBus(&_bus_instance); // set the bus on the panel. auto cfg = _panel_instance->config(); // Gets a structure for display panel settings. - LOG_DEBUG("Height: %d, Width: %d ", settingsMap[displayHeight], settingsMap[displayWidth]); + LOG_DEBUG("Width: %d, Height: %d", settingsMap[displayWidth], settingsMap[displayHeight]); cfg.pin_cs = settingsMap[displayCS]; // Pin number where CS is connected (-1 = disable) cfg.pin_rst = settingsMap[displayReset]; - cfg.panel_width = _width; // actual displayable width - cfg.panel_height = _height; // actual displayable height + if (settingsMap[displayRotate]) { + cfg.panel_width = settingsMap[displayHeight]; // actual displayable width + cfg.panel_height = settingsMap[displayWidth]; // actual displayable height + } else { + cfg.panel_width = settingsMap[displayWidth]; // actual displayable width + cfg.panel_height = settingsMap[displayHeight]; // actual displayable height + } cfg.offset_x = settingsMap[displayOffsetX]; // Panel offset amount in X direction cfg.offset_y = settingsMap[displayOffsetY]; // Panel offset amount in Y direction cfg.offset_rotation = settingsMap[displayOffsetRotate]; // Rotation direction value offset 0~7 (4~7 is mirrored) @@ -985,7 +982,11 @@ TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY g backlightEnable = p; #if ARCH_PORTDUINO - setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayWidth], settingsMap[configNames::displayHeight]); + if (settingsMap[displayRotate]) { + setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayWidth], settingsMap[configNames::displayHeight]); + } else { + setGeometry(GEOMETRY_RAWMODE, settingsMap[configNames::displayHeight], settingsMap[configNames::displayWidth]); + } #elif defined(SCREEN_ROTATE) setGeometry(GEOMETRY_RAWMODE, TFT_HEIGHT, TFT_WIDTH); From 286376e46ae8e2f3f779f49ed7650600c036bd47 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 18 May 2025 22:58:25 -0500 Subject: [PATCH 128/265] Seeed indicator working with unified tft --- variants/seeed-sensecap-indicator/platformio.ini | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/variants/seeed-sensecap-indicator/platformio.ini b/variants/seeed-sensecap-indicator/platformio.ini index b643288a6..56c77113a 100644 --- a/variants/seeed-sensecap-indicator/platformio.ini +++ b/variants/seeed-sensecap-indicator/platformio.ini @@ -37,8 +37,6 @@ upload_speed = 460800 build_flags = ${env:seeed-sensecap-indicator.build_flags} -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 - -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 - -D MESHTASTIC_EXCLUDE_SCREEN=1 -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 -D MESHTASTIC_EXCLUDE_WEBSERVER=1 -D MESHTASTIC_EXCLUDE_SERIAL=1 @@ -46,7 +44,7 @@ build_flags = -D INPUTDRIVER_BUTTON_TYPE=38 -D HAS_TELEMETRY=0 -D CONFIG_DISABLE_HAL_LOCKS=1 - -D HAS_SCREEN=0 + -D HAS_SCREEN=1 -D HAS_TFT=1 -D DISPLAY_SET_RESOLUTION -D RAM_SIZE=4096 From 9aff65e3133645b6459e2d4adf2eee3f2a5a79f2 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 18 May 2025 23:01:46 -0500 Subject: [PATCH 129/265] One of the classic blunders --- src/mesh/NodeDB.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 7e274e67e..41c4a2b19 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -500,7 +500,7 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) config.lora.override_duty_cycle = false; config.lora.config_ok_to_mqtt = false; #if HAS_TFT // For the devices that support MUI, default to that - config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR; + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; #endif #ifdef USERPREFS_CONFIG_LORA_REGION config.lora.region = USERPREFS_CONFIG_LORA_REGION; From 7f0afcdae2e9f693d437b6af500fe14bbd3924fc Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 18 May 2025 23:18:08 -0500 Subject: [PATCH 130/265] Enable cannedMessages for t-deck --- variants/t-deck/platformio.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/variants/t-deck/platformio.ini b/variants/t-deck/platformio.ini index 48d2eef89..a45a9708e 100644 --- a/variants/t-deck/platformio.ini +++ b/variants/t-deck/platformio.ini @@ -25,7 +25,6 @@ extends = env:t-deck build_flags = ${env:t-deck.build_flags} -D CONFIG_DISABLE_HAL_LOCKS=1 ; "feels" to be a bit more stable without locks - -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 -D MESHTASTIC_EXCLUDE_WEBSERVER=1 -D MESHTASTIC_EXCLUDE_SERIAL=1 -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 From aca515917058253f34c9914b362a6a88cffa5ed6 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 19 May 2025 19:43:28 -0400 Subject: [PATCH 131/265] Adding date to GPS screen --- src/graphics/Screen.cpp | 68 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 7a8d63baa..e0d08b2b0 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -125,6 +125,63 @@ static bool heartbeat = false; #include "graphics/ScreenFonts.h" #include + +// Start Functions to write date/time to the screen +#include // Only needed if you're using std::string elsewhere + +bool isLeapYear(int year) { + return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); +} + +const int daysInMonth[] = { 31,28,31,30,31,30,31,31,30,31,30,31 }; + +// Fills the buffer with a formatted date/time string and returns pixel width +int formatDateTime(char* buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay* display, bool includeTime) { + int sec = rtc_sec % 60; + rtc_sec /= 60; + int min = rtc_sec % 60; + rtc_sec /= 60; + int hour = rtc_sec % 24; + rtc_sec /= 24; + + int year = 1970; + while (true) { + int daysInYear = isLeapYear(year) ? 366 : 365; + if (rtc_sec >= daysInYear) { + rtc_sec -= daysInYear; + year++; + } else { + break; + } + } + + int month = 0; + while (month < 12) { + int dim = daysInMonth[month]; + if (month == 1 && isLeapYear(year)) dim++; + if (rtc_sec >= dim) { + rtc_sec -= dim; + month++; + } else { + break; + } + } + + int day = rtc_sec + 1; + + if (includeTime) { + snprintf(buf, bufSize, "%04d-%02d-%02d %02d:%02d:%02d", year, month + 1, day, hour, min, sec); + } else { + snprintf(buf, bufSize, "%04d-%02d-%02d", year, month + 1, day); + } + + return display->getStringWidth(buf); +} + +// Usage: int stringWidth = formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display); +// End Functions to write date/time to the screen + + void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display) { for (int row = 0; row < height; row++) { @@ -2862,6 +2919,17 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat char lonStr[32]; snprintf(lonStr, sizeof(lonStr), "Lon: %.5f", geoCoord.getLongitude() * 1e-7); display->drawString(x, compactFourthLine, lonStr); + + if(SCREEN_HEIGHT > 64){ + // === Fifth Row: Date === + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); + char datetimeStr[25]; + bool showTime = false; // set to true for full datetime + formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime); + char fullLine[40]; + snprintf(fullLine, sizeof(fullLine), "Date: %s", datetimeStr); + display->drawString(0, compactFifthLine, fullLine); + } } // === Draw Compass if heading is valid === From 6db585051e15c160618783a8609b67b6076e0f48 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 19 May 2025 19:56:32 -0400 Subject: [PATCH 132/265] Save on reboot and shutdown --- src/modules/CannedMessageModule.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index b9d616c3a..90150af37 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -550,6 +550,7 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) if (screen) screen->showOverlayBanner("Shutting down..."); shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; + nodeDB->saveToDisk(); this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; validEvent = true; break; @@ -557,6 +558,7 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) case INPUT_BROKER_MSG_REBOOT: if (screen) screen->showOverlayBanner("Rebooting...", 0); // stays on screen + nodeDB->saveToDisk(); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; validEvent = true; From 021f872507e0a57da95dff75b45ff37951b0bce4 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Mon, 19 May 2025 21:53:58 -0500 Subject: [PATCH 133/265] Gate more modules behind config.display.displaymode --- src/mesh/eth/ethClient.cpp | 4 +++- src/mesh/wifi/WiFiAPClient.cpp | 8 ++++++-- src/modules/Modules.cpp | 4 +++- variants/seeed-sensecap-indicator/platformio.ini | 5 ----- variants/t-deck/platformio.ini | 3 --- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp index 70c6e3fe4..9c92a6c27 100644 --- a/src/mesh/eth/ethClient.cpp +++ b/src/mesh/eth/ethClient.cpp @@ -64,7 +64,9 @@ static int32_t reconnectETH() } #if !MESHTASTIC_EXCLUDE_SOCKETAPI - initApiServer(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + initApiServer(); + } #endif ethStartupComplete = true; diff --git a/src/mesh/wifi/WiFiAPClient.cpp b/src/mesh/wifi/WiFiAPClient.cpp index 789f8ac44..ad7127d6e 100644 --- a/src/mesh/wifi/WiFiAPClient.cpp +++ b/src/mesh/wifi/WiFiAPClient.cpp @@ -124,10 +124,14 @@ static void onNetworkConnected() } #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WEBSERVER - initWebServer(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + initWebServer(); + } #endif #if !MESHTASTIC_EXCLUDE_SOCKETAPI - initApiServer(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + initApiServer(); + } #endif APStartupComplete = true; } diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index f0dafa332..0ecdadf4c 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -234,7 +234,9 @@ void setupModules() #if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \ !defined(CONFIG_IDF_TARGET_ESP32C3) #if !MESHTASTIC_EXCLUDE_SERIAL - new SerialModule(); + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + new SerialModule(); + } #endif #endif #ifdef ARCH_ESP32 diff --git a/variants/seeed-sensecap-indicator/platformio.ini b/variants/seeed-sensecap-indicator/platformio.ini index 56c77113a..f2495c340 100644 --- a/variants/seeed-sensecap-indicator/platformio.ini +++ b/variants/seeed-sensecap-indicator/platformio.ini @@ -36,11 +36,6 @@ upload_speed = 460800 build_flags = ${env:seeed-sensecap-indicator.build_flags} - -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 - -D MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR=1 - -D MESHTASTIC_EXCLUDE_WEBSERVER=1 - -D MESHTASTIC_EXCLUDE_SERIAL=1 - -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 -D INPUTDRIVER_BUTTON_TYPE=38 -D HAS_TELEMETRY=0 -D CONFIG_DISABLE_HAL_LOCKS=1 diff --git a/variants/t-deck/platformio.ini b/variants/t-deck/platformio.ini index a45a9708e..707e94166 100644 --- a/variants/t-deck/platformio.ini +++ b/variants/t-deck/platformio.ini @@ -25,9 +25,6 @@ extends = env:t-deck build_flags = ${env:t-deck.build_flags} -D CONFIG_DISABLE_HAL_LOCKS=1 ; "feels" to be a bit more stable without locks - -D MESHTASTIC_EXCLUDE_WEBSERVER=1 - -D MESHTASTIC_EXCLUDE_SERIAL=1 - -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 -D INPUTDRIVER_I2C_KBD_TYPE=0x55 -D INPUTDRIVER_ENCODER_TYPE=3 -D INPUTDRIVER_ENCODER_LEFT=1 From a4ef8bf5f0c6641596b34513c65339a9e6839ccb Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Mon, 19 May 2025 22:01:17 -0500 Subject: [PATCH 134/265] Unify tft for unphone --- variants/unphone/platformio.ini | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/variants/unphone/platformio.ini b/variants/unphone/platformio.ini index 399d65b03..2f1546d6f 100644 --- a/variants/unphone/platformio.ini +++ b/variants/unphone/platformio.ini @@ -36,13 +36,8 @@ extends = env:unphone build_flags = ${env:unphone.build_flags} -D CONFIG_DISABLE_HAL_LOCKS=1 - -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 - -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 - -D MESHTASTIC_EXCLUDE_WEBSERVER=1 - -D MESHTASTIC_EXCLUDE_SERIAL=1 - -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 -D INPUTDRIVER_BUTTON_TYPE=21 - -D HAS_SCREEN=0 + -D HAS_SCREEN=1 -D HAS_TFT=1 -D HAS_SDCARD -D DISPLAY_SET_RESOLUTION From 25b2a75dfee80c8194aa4b242dbe2c8cbf84d0a0 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 20 May 2025 22:11:43 -0400 Subject: [PATCH 135/265] Lora Screen Refactored --- src/graphics/Screen.cpp | 82 ++++++++++++++++++++++++++++++----------- 1 file changed, 60 insertions(+), 22 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index e0d08b2b0..c5a8f33b9 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -46,6 +46,7 @@ along with this program. If not, see . #include "main.h" #include "mesh-pb-constants.h" #include "mesh/Channels.h" +#include "RadioLibInterface.h" #include "mesh/generated/meshtastic/deviceonly.pb.h" #include "meshUtils.h" #include "modules/AdminModule.h" @@ -2697,18 +2698,49 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i config.display.heading_bold = origBold; - // === Third Row: LongName Centered === - // Blank - // === Fourth Row: LongName Centered === + // Crafting all the data first so we can use it + int textWidth = 0; + int nameX = 0; + int yOffset = (SCREEN_WIDTH > 128) ? 0 : 7; + const char *longName = nullptr; meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { - const char *longName = ourNode->user.long_name; - int textWidth = display->getStringWidth(longName); - int nameX = (SCREEN_WIDTH - textWidth) / 2; - int yOffset = (SCREEN_WIDTH > 128) ? 0 : 7; - display->drawString(nameX, compactFourthLine - yOffset, longName); + longName = ourNode->user.long_name; } + uint8_t dmac[6]; + char shortnameble[35]; + getMacAddr(dmac); + snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); + snprintf(shortnameble, sizeof(shortnameble), "%s", haveGlyphs(owner.short_name) ? owner.short_name : ""); + + char combinedName[50]; + snprintf(combinedName, sizeof(combinedName), "%s (%s)", longName, shortnameble); + if(SCREEN_WIDTH - (display->getStringWidth(longName) + display->getStringWidth(shortnameble)) > 10){ + // === Third Row: combinedName Centered === + size_t len = strlen(combinedName); + if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) { + combinedName[len - 3] = '\0'; // Remove the last three characters + } + textWidth = display->getStringWidth(combinedName); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, compactThirdLine + yOffset, combinedName); + } else { + // === Third Row: LongName Centered === + textWidth = display->getStringWidth(longName); + nameX = (SCREEN_WIDTH - textWidth) / 2; + yOffset = (strcmp(shortnameble, "") == 0) ? 1 : 0; + if(yOffset == 1){ + yOffset = (SCREEN_WIDTH > 128) ? 0 : 7; + } + display->drawString(nameX, compactThirdLine + yOffset, longName); + + // === Fourth Row: ShortName Centered === + textWidth = display->getStringWidth(shortnameble); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, compactFourthLine, shortnameble); + } + } // **************************** @@ -2751,29 +2783,35 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int const char *region = myRegion ? myRegion->name : NULL; snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); int textWidth = display->getStringWidth(regionradiopreset); - int nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(SCREEN_WIDTH - textWidth, compactFirstLine, regionradiopreset); - // === Second Row: Channel Utilization === - // Get our hardware ID + // === Second Row: BLE Name === uint8_t dmac[6]; + char shortnameble[35]; getMacAddr(dmac); snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); - - char shortnameble[35]; - snprintf(shortnameble, sizeof(shortnameble), "%s_%s", haveGlyphs(owner.short_name) ? owner.short_name : "", ourId); + snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", ourId); textWidth = display->getStringWidth(shortnameble); - nameX = (SCREEN_WIDTH - textWidth) / 2; + int nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, compactSecondLine, shortnameble); - // === Third Row: Node longName === - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { - const char *longName = ourNode->user.long_name; - textWidth = display->getStringWidth(longName); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, compactThirdLine, longName); + // === Third Row: Frequency / ChanNum === + char frequencyslot[35]; + char freqStr[16]; + float freq = RadioLibInterface::instance->getFreq(); + snprintf(freqStr, sizeof(freqStr), "%.3f", freq); + if(config.lora.channel_num == 0){ + snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %s", freqStr); + } else { + snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Chan: %s (%d)", freqStr, config.lora.channel_num); } + size_t len = strlen(frequencyslot); + if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { + frequencyslot[len - 4] = '\0'; // Remove the last three characters + } + textWidth = display->getStringWidth(frequencyslot); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, compactThirdLine, frequencyslot); // === Fourth Row: Channel Utilization === const char *chUtil = "ChUtil:"; From f1d4e5fa482b2fb1b6996d00611b8a8711e4fff2 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 20 May 2025 22:40:53 -0400 Subject: [PATCH 136/265] Save on shutdown added to buttonthead --- src/ButtonThread.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp index 3eeb76905..300b40242 100644 --- a/src/ButtonThread.cpp +++ b/src/ButtonThread.cpp @@ -310,6 +310,7 @@ int32_t ButtonThread::runOnce() playShutdownMelody(); delay(3000); power->shutdown(); + nodeDB->saveToDisk(); break; } From 93aa88129c6741f2f7bacf10ffa38d37d8767282 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Tue, 20 May 2025 23:08:32 -0400 Subject: [PATCH 137/265] Re-arranged Lora Screen --- src/graphics/Screen.cpp | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index c5a8f33b9..a7ae21354 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2773,27 +2773,26 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int display->setColor(WHITE); display->setTextAlignment(TEXT_ALIGN_LEFT); - // === First Row: Region / Radio Preset === + // === First Row: Region / BLE Name === drawNodes(display, x, compactFirstLine + 3, nodeStatus, 0, true); - auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); - - // Display Region and Radio Preset - char regionradiopreset[25]; - const char *region = myRegion ? myRegion->name : NULL; - snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); - int textWidth = display->getStringWidth(regionradiopreset); - display->drawString(SCREEN_WIDTH - textWidth, compactFirstLine, regionradiopreset); - - // === Second Row: BLE Name === uint8_t dmac[6]; char shortnameble[35]; getMacAddr(dmac); snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", ourId); - textWidth = display->getStringWidth(shortnameble); - int nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, compactSecondLine, shortnameble); + int textWidth = display->getStringWidth(shortnameble); + int nameX = (SCREEN_WIDTH - textWidth); + display->drawString(nameX, compactFirstLine, shortnameble); + + // === Second Row: Radio Preset === + auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); + char regionradiopreset[25]; + const char *region = myRegion ? myRegion->name : NULL; + snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); + textWidth = display->getStringWidth(regionradiopreset); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, compactSecondLine, regionradiopreset); // === Third Row: Frequency / ChanNum === char frequencyslot[35]; From 53d58017906d8325cb182c56524e57376d0f55dd Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 21 May 2025 14:33:39 -0400 Subject: [PATCH 138/265] Compiling error fixes --- src/graphics/Screen.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index a7ae21354..bbe0a8f48 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -148,7 +148,7 @@ int formatDateTime(char* buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay* dis int year = 1970; while (true) { int daysInYear = isLeapYear(year) ? 366 : 365; - if (rtc_sec >= daysInYear) { + if (rtc_sec >= (uint32_t)daysInYear) { rtc_sec -= daysInYear; year++; } else { @@ -160,7 +160,7 @@ int formatDateTime(char* buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay* dis while (month < 12) { int dim = daysInMonth[month]; if (month == 1 && isLeapYear(year)) dim++; - if (rtc_sec >= dim) { + if (rtc_sec >= (uint32_t)dim) { rtc_sec -= dim; month++; } else { From b35fb886e4a7f7ef534750cfb95759538b243b7e Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 21 May 2025 21:13:46 -0400 Subject: [PATCH 139/265] Node selection optimization for encoder and fix for ACK messages. --- src/graphics/images.h | 15 + src/modules/CannedMessageModule.cpp | 551 +++++++++++++++------------- src/modules/CannedMessageModule.h | 4 + 3 files changed, 320 insertions(+), 250 deletions(-) diff --git a/src/graphics/images.h b/src/graphics/images.h index cdd0c3502..db61cb055 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -433,6 +433,21 @@ const uint8_t mute_symbol_big[] PROGMEM = {0b00000001, 0b00000000, 0b11000010, 0 const unsigned char bell_alert[] PROGMEM = {0b00011000, 0b00100100, 0b00100100, 0b01000010, 0b01000010, 0b01000010, 0b11111111, 0b00011000}; + +#define key_symbol_width 8 +#define key_symbol_height 8 +const uint8_t key_symbol[] PROGMEM = { + 0b00000000, + 0b00000000, + 0b00000110, + 0b11111001, + 0b10101001, + 0b10000110, + 0b00000000, + 0b00000000 +}; + + #endif #include "img/icon.xbm" diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 90150af37..ee1186989 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -13,9 +13,9 @@ #include "detect/ScanI2C.h" #include "input/ScanAndSelect.h" #include "mesh/generated/meshtastic/cannedmessages.pb.h" +#include "graphics/images.h" #include "modules/AdminModule.h" #include "graphics/SharedUIDisplay.h" - #include "main.h" // for cardkb_found #include "modules/ExternalNotificationModule.h" // for buzzer control #if !MESHTASTIC_EXCLUDE_GPS @@ -70,6 +70,9 @@ CannedMessageModule::CannedMessageModule() } } +bool hasKeyForNode(const meshtastic_NodeInfoLite* node) { + return node && node->has_user && node->user.public_key.size > 0; +} /** * @brief Items in array this->messages will be set to be pointing on the right * starting points of the string this->messageStore @@ -125,22 +128,32 @@ int CannedMessageModule::splitConfiguredMessages() } void CannedMessageModule::resetSearch() { LOG_INFO("Resetting search, restoring full destination list"); - updateFilteredNodes(); // Reload all nodes and channels + + int previousDestIndex = destIndex; + + searchQuery = ""; + updateFilteredNodes(); + + // Adjust scrollIndex so previousDestIndex is still visible + int totalEntries = activeChannelIndices.size() + filteredNodes.size(); + this->visibleRows = (displayHeight - FONT_HEIGHT_SMALL * 2) / FONT_HEIGHT_SMALL; + if (this->visibleRows < 1) this->visibleRows = 1; + int maxScrollIndex = std::max(0, totalEntries - visibleRows); + scrollIndex = std::min(std::max(previousDestIndex - (visibleRows / 2), 0), maxScrollIndex); + + lastUpdateMillis = millis(); requestFocus(); } void CannedMessageModule::updateFilteredNodes() { - static size_t lastNumMeshNodes = 0; // Track the last known node count - static String lastSearchQuery = ""; // Track last search query + static size_t lastNumMeshNodes = 0; + static String lastSearchQuery = ""; size_t numMeshNodes = nodeDB->getNumMeshNodes(); - - // If the number of nodes has changed, force an update bool nodesChanged = (numMeshNodes != lastNumMeshNodes); lastNumMeshNodes = numMeshNodes; - // Also check if search query changed + // Early exit if nothing changed if (searchQuery == lastSearchQuery && !nodesChanged) return; - lastSearchQuery = searchQuery; needsUpdate = false; @@ -148,43 +161,50 @@ void CannedMessageModule::updateFilteredNodes() { this->activeChannelIndices.clear(); NodeNum myNodeNum = nodeDB->getNodeNum(); + String lowerSearchQuery = searchQuery; + lowerSearchQuery.toLowerCase(); - for (size_t i = 0; i < numMeshNodes; i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + // Preallocate space to reduce reallocation + this->filteredNodes.reserve(numMeshNodes); + + for (size_t i = 0; i < numMeshNodes; ++i) { + meshtastic_NodeInfoLite* node = nodeDB->getMeshNodeByIndex(i); if (!node || node->num == myNodeNum) continue; - String nodeName = node->user.long_name; - String lowerNodeName = nodeName; - String lowerSearchQuery = searchQuery; + const String& nodeName = node->user.long_name; - lowerNodeName.toLowerCase(); - lowerSearchQuery.toLowerCase(); - - if (searchQuery.length() == 0 || lowerNodeName.indexOf(lowerSearchQuery) != -1) { + if (searchQuery.length() == 0) { this->filteredNodes.push_back({node, sinceLastSeen(node)}); + } else { + // Avoid unnecessary lowercase conversion if already matched + String lowerNodeName = nodeName; + lowerNodeName.toLowerCase(); + + if (lowerNodeName.indexOf(lowerSearchQuery) != -1) { + this->filteredNodes.push_back({node, sinceLastSeen(node)}); + } } } // Populate active channels - this->activeChannelIndices.clear(); std::vector seenChannels; - for (uint8_t i = 0; i < channels.getNumChannels(); i++) { - String channelName = channels.getName(i); - if (channelName.length() > 0 && std::find(seenChannels.begin(), seenChannels.end(), channelName) == seenChannels.end()) { + seenChannels.reserve(channels.getNumChannels()); + for (uint8_t i = 0; i < channels.getNumChannels(); ++i) { + String name = channels.getName(i); + if (name.length() > 0 && std::find(seenChannels.begin(), seenChannels.end(), name) == seenChannels.end()) { this->activeChannelIndices.push_back(i); - seenChannels.push_back(channelName); + seenChannels.push_back(name); } } - // Sort nodes by favorite status and last seen time - std::sort(this->filteredNodes.begin(), this->filteredNodes.end(), [](const NodeEntry &a, const NodeEntry &b) { - if (a.node->is_favorite != b.node->is_favorite) { - return a.node->is_favorite > b.node->is_favorite; // Favorited nodes first - } - return a.lastHeard < b.lastHeard; // Otherwise, sort by last heard (oldest first) + // Sort by favorite, then last heard + std::sort(this->filteredNodes.begin(), this->filteredNodes.end(), [](const NodeEntry& a, const NodeEntry& b) { + if (a.node->is_favorite != b.node->is_favorite) + return a.node->is_favorite > b.node->is_favorite; + return a.lastHeard < b.lastHeard; }); - - // πŸ”Ή If nodes have changed, refresh the screen + scrollIndex = 0; // Show first result at the top + destIndex = 0; // Highlight the first entry if (nodesChanged) { LOG_INFO("Nodes changed, forcing UI refresh."); screen->forceDisplay(); @@ -229,88 +249,66 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) } static int lastDestIndex = -1; // Cache the last index bool selectionChanged = false; // Track if UI needs redrawing - bool isUp = event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP); bool isDown = event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { if (event->kbchar >= 32 && event->kbchar <= 126) { this->searchQuery += event->kbchar; + needsUpdate = true; + runOnce(); // <=== Force filtering immediately return 0; } size_t numMeshNodes = this->filteredNodes.size(); int totalEntries = numMeshNodes + this->activeChannelIndices.size(); - int columns = 2; - int totalRows = (totalEntries + columns - 1) / columns; + int columns = 1; + int totalRows = totalEntries; // one entry per row now int maxScrollIndex = std::max(0, totalRows - this->visibleRows); scrollIndex = std::max(0, std::min(scrollIndex, maxScrollIndex)); if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK)) { if (this->searchQuery.length() > 0) { this->searchQuery.remove(this->searchQuery.length() - 1); + needsUpdate = true; + runOnce(); // <=== Ensure filter updates after backspace } if (this->searchQuery.length() == 0) { resetSearch(); // Function to restore all destinations + needsUpdate = false; } return 0; } - bool needsRedraw = false; - // πŸ”Ό UP Navigation in Node Selection if (isUp) { - if ((this->destIndex / columns) <= scrollIndex) { - if (scrollIndex > 0) { - scrollIndex--; - needsRedraw = true; + if (this->destIndex > 0) { + this->destIndex--; + if ((this->destIndex / columns) < scrollIndex) { + scrollIndex = this->destIndex / columns; + shouldRedraw = true; + } else if ((this->destIndex / columns) >= (scrollIndex + visibleRows)) { + scrollIndex = (this->destIndex / columns) - visibleRows + 1; + shouldRedraw = true; + } else { + shouldRedraw = true; // βœ… allow redraw only once below } - } else if (this->destIndex >= columns) { - this->destIndex -= columns; } } // πŸ”½ DOWN Navigation in Node Selection if (isDown) { - if ((this->destIndex / columns) >= (scrollIndex + this->visibleRows - 1)) { - if (scrollIndex < maxScrollIndex) { - scrollIndex++; - needsRedraw = true; + if (this->destIndex + 1 < totalEntries) { + this->destIndex++; + if ((this->destIndex / columns) >= (scrollIndex + visibleRows)) { + scrollIndex = (this->destIndex / columns) - visibleRows + 1; + shouldRedraw = true; + } else { + shouldRedraw = true; } - } else if (this->destIndex + columns < totalEntries) { - this->destIndex += columns; } } - - // β—€ LEFT Navigation (Wrap to previous row OR last row) - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) { - if (this->destIndex % columns == 0) { - if (this->destIndex >= columns) { - this->destIndex = this->destIndex - columns + (columns - 1); - } else { - int lastRowStart = ((totalEntries - 1) / columns) * columns; - this->destIndex = std::min(lastRowStart + (columns - 1), totalEntries - 1); - } - } else { - this->destIndex--; - } - } - - // β–Ά RIGHT Navigation (Wrap to next row OR first row) - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) { - int nextIndex = this->destIndex + 1; - if ((this->destIndex + 1) % columns == 0 || nextIndex >= totalEntries) { - if (this->destIndex + columns < totalEntries) { - this->destIndex = this->destIndex + columns - (columns - 1); - } else { - this->destIndex = 0; - } - } else { - this->destIndex++; - } - } - if (this->destSelect != CANNED_MESSAGE_DESTINATION_TYPE_NODE) { if (isUp && this->messagesCount > 0) { this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; @@ -322,8 +320,9 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) } } // Only refresh UI when needed - if (needsRedraw) { + if (shouldRedraw) { screen->forceDisplay(); + shouldRedraw = false; } if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) { if (this->destIndex < static_cast(this->activeChannelIndices.size())) { @@ -690,17 +689,24 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha // Prevents the canned message module from regenerating the screen's frameset at unexpected times, // or raising a UIFrameEvent before another module has the chance this->waitingForAck = true; - + this->lastSentNode = dest; LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); service->sendToMesh( p, RX_SRC_LOCAL, true); // send to mesh, cc to phone. Even if there's no phone connected, this stores the message to match ACKs } - +unsigned long lastUpdateMillis = 0; int32_t CannedMessageModule::runOnce() { - updateFilteredNodes(); + #define NODE_UPDATE_IDLE_MS 100 + #define NODE_UPDATE_ACTIVE_MS 80 + + unsigned long updateThreshold = (searchQuery.length() > 0) ? NODE_UPDATE_ACTIVE_MS : NODE_UPDATE_IDLE_MS; + if (needsUpdate && millis() - lastUpdateMillis > updateThreshold) { + updateFilteredNodes(); + lastUpdateMillis = millis(); + } if (((!moduleConfig.canned_message.enabled) && !CANNED_MESSAGE_MODULE_ENABLE) || (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { temporaryMessage = ""; @@ -953,7 +959,10 @@ int32_t CannedMessageModule::runOnce() this->notifyObservers(&e); return INACTIVATE_AFTER_MS; } - + if (shouldRedraw) { + screen->forceDisplay(); + shouldRedraw = false; + } return INT32_MAX; } @@ -1249,7 +1258,6 @@ bool CannedMessageModule::interceptingKeyboardInput() return true; } } - #if !HAS_TFT void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { @@ -1258,233 +1266,255 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); + // === Draw temporary message if available === if (temporaryMessage.length() != 0) { requestFocus(); // Tell Screen::setFrames to move to our module's frame LOG_DEBUG("Draw temporary message: %s", temporaryMessage.c_str()); display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_MEDIUM); display->drawString(display->getWidth() / 2 + x, 0 + y + 12, temporaryMessage); - } else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) { - requestFocus(); // Tell Screen::setFrames to move to our module's frame - EINK_ADD_FRAMEFLAG(display, COSMETIC); // Clean after this popup. Layout makes ghosting particularly obvious - display->setTextAlignment(TEXT_ALIGN_CENTER); + return; + } -#ifdef USE_EINK - display->setFont(FONT_SMALL); // No chunky text -#else - display->setFont(FONT_MEDIUM); // Chunky text -#endif - - String displayString; - display->setTextAlignment(TEXT_ALIGN_CENTER); - if (this->ack) { - displayString = "Delivered to\n%s"; - } else { - displayString = "Delivery failed\nto %s"; - } - display->drawStringf(display->getWidth() / 2 + x, 0 + y + 12, buffer, displayString, - cannedMessageModule->getNodeName(this->incoming)); - - display->setFont(FONT_SMALL); - - String snrString = "Last Rx SNR: %f"; - String rssiString = "Last Rx RSSI: %d"; - - // Don't bother drawing snr and rssi for tiny displays - if (display->getHeight() > 100) { - - // Original implementation used constants of y = 100 and y = 130. Shrink this if screen is *slightly* small - int16_t snrY = 100; - int16_t rssiY = 130; - - // If dislay is *slighly* too small for the original consants, squish up a bit - if (display->getHeight() < rssiY + FONT_HEIGHT_SMALL) { - snrY = display->getHeight() - ((1.5) * FONT_HEIGHT_SMALL); - rssiY = display->getHeight() - ((2.5) * FONT_HEIGHT_SMALL); - } - - if (this->ack) { - display->drawStringf(display->getWidth() / 2 + x, snrY + y, buffer, snrString, this->lastRxSnr); - display->drawStringf(display->getWidth() / 2 + x, rssiY + y, buffer, rssiString, this->lastRxRssi); - } - } - } else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) { - // E-Ink: clean the screen *after* this pop-up - EINK_ADD_FRAMEFLAG(display, COSMETIC); - - requestFocus(); // Tell Screen::setFrames to move to our module's frame - -#ifdef USE_EINK - display->setFont(FONT_SMALL); // No chunky text -#else - display->setFont(FONT_MEDIUM); // Chunky text -#endif - - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(display->getWidth() / 2 + x, 0 + y + 12, "Sending..."); - } else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawString(10 + x, 0 + y + FONT_HEIGHT_SMALL, "Canned Message\nModule disabled."); - } else if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { + // === Destination Selection === + if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { requestFocus(); - updateFilteredNodes(); - display->clear(); + display->setColor(WHITE); // Always draw cleanly display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); + // === Header === int titleY = 2; String titleText = "Select Destination"; titleText += searchQuery.length() > 0 ? " [" + searchQuery + "]" : " [ ]"; - display->drawString(display->getWidth() / 2 - display->getStringWidth(titleText) / 2, titleY, titleText); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(display->getWidth() / 2, titleY, titleText); + display->setTextAlignment(TEXT_ALIGN_LEFT); - int rowYOffset = titleY + FONT_HEIGHT_SMALL; // Adjusted for search box spacing + // === List Items === + int rowYOffset = titleY + (FONT_HEIGHT_SMALL - 4); int numActiveChannels = this->activeChannelIndices.size(); int totalEntries = numActiveChannels + this->filteredNodes.size(); - int columns = 2; - this->visibleRows = (display->getHeight() - (titleY + FONT_HEIGHT_SMALL)) / FONT_HEIGHT_SMALL; + int columns = 1; + this->visibleRows = (display->getHeight() - (titleY + FONT_HEIGHT_SMALL)) / (FONT_HEIGHT_SMALL - 4); if (this->visibleRows < 1) this->visibleRows = 1; - // Ensure scrolling within bounds + // === Clamp scrolling === if (scrollIndex > totalEntries / columns) scrollIndex = totalEntries / columns; if (scrollIndex < 0) scrollIndex = 0; for (int row = 0; row < visibleRows; row++) { - int itemIndex = (scrollIndex + row) * columns; - for (int col = 0; col < columns; col++) { - if (itemIndex >= totalEntries) break; + int itemIndex = scrollIndex + row; + if (itemIndex >= totalEntries) break; - int xOffset = col * (display->getWidth() / columns); - int yOffset = row * FONT_HEIGHT_SMALL + rowYOffset; - String entryText; + int xOffset = 0; + int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset; + String entryText; - // Draw Channels First - if (itemIndex < numActiveChannels) { - uint8_t channelIndex = this->activeChannelIndices[itemIndex]; - entryText = String("@") + String(channels.getName(channelIndex)); - } - // Then Draw Nodes - else { - int nodeIndex = itemIndex - numActiveChannels; - if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { - meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; - entryText = node ? (node->is_favorite ? "* " + String(node->user.long_name) : String(node->user.long_name)) : "?"; + // Draw Channels First + if (itemIndex < numActiveChannels) { + uint8_t channelIndex = this->activeChannelIndices[itemIndex]; + entryText = String("@") + String(channels.getName(channelIndex)); + } + // Then Draw Nodes + else { + int nodeIndex = itemIndex - numActiveChannels; + if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { + meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; + if (node) { + entryText = node->is_favorite ? "* " + String(node->user.long_name) : String(node->user.long_name); + bool hasKey = hasKeyForNode(node); } } + } - // Prevent Empty Names - if (entryText.length() == 0 || entryText == "Unknown") entryText = "?"; + if (entryText.length() == 0 || entryText == "Unknown") entryText = "?"; - // Trim if Too Long - while (display->getStringWidth(entryText + "-") > (display->getWidth() / columns - 4)) { - entryText = entryText.substring(0, entryText.length() - 1); + // === Highlight background (if selected) === + if (itemIndex == destIndex) { + int scrollPadding = 8; // Reserve space for scrollbar + display->fillRect(0, yOffset + 2, display->getWidth() - scrollPadding, FONT_HEIGHT_SMALL - 5); + display->setColor(BLACK); + } + + // === Draw entry text === + display->drawString(xOffset + 2, yOffset, entryText); + display->setColor(WHITE); + + // === Draw key icon (after highlight) === + if (itemIndex >= numActiveChannels) { + int nodeIndex = itemIndex - numActiveChannels; + if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { + meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; + if (node && hasKeyForNode(node)) { + int iconX = display->getWidth() - key_symbol_width - 15; + int iconY = yOffset + (FONT_HEIGHT_SMALL - key_symbol_height) / 2; + + if (itemIndex == destIndex) { + display->setColor(INVERSE); + } else { + display->setColor(WHITE); + } + display->drawXbm(iconX, iconY, key_symbol_width, key_symbol_height, key_symbol); + } } - - // Highlight Selection - if (itemIndex == destIndex) { - display->fillRect(xOffset, yOffset, display->getStringWidth(entryText) + 4, FONT_HEIGHT_SMALL + 2); - display->setColor(BLACK); - } - display->drawString(xOffset + 2, yOffset, entryText); - display->setColor(WHITE); - itemIndex++; } } - if (totalEntries > visibleRows * columns) { - display->drawRect(display->getWidth() - 6, rowYOffset, 4, visibleRows * FONT_HEIGHT_SMALL); - int totalPages = (totalEntries + columns - 1) / columns; - int scrollHeight = (visibleRows * FONT_HEIGHT_SMALL * visibleRows) / (totalPages); - int scrollPos = rowYOffset + ((visibleRows * FONT_HEIGHT_SMALL) * scrollIndex) / totalPages; - display->fillRect(display->getWidth() - 6, scrollPos, 4, scrollHeight); + + // Scrollbar + if (totalEntries > visibleRows) { + int scrollbarHeight = visibleRows * (FONT_HEIGHT_SMALL - 4); + int totalScrollable = totalEntries; + int scrollTrackX = display->getWidth() - 6; + display->drawRect(scrollTrackX, rowYOffset, 4, scrollbarHeight); + int scrollHeight = (scrollbarHeight * visibleRows) / totalScrollable; + int scrollPos = rowYOffset + (scrollbarHeight * scrollIndex) / totalScrollable; + display->fillRect(scrollTrackX, scrollPos, 4, scrollHeight); } - screen->forceDisplay(); - } else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - requestFocus(); // Tell Screen::setFrames to move to our module's frame + return; + } + + // === ACK/NACK Screen === + if (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) { + requestFocus(); + EINK_ADD_FRAMEFLAG(display, COSMETIC); + display->setTextAlignment(TEXT_ALIGN_CENTER); +#ifdef USE_EINK + display->setFont(FONT_SMALL); +#else + display->setFont(FONT_MEDIUM); +#endif + if (this->ack) { + if (this->lastSentNode == NODENUM_BROADCAST) { + snprintf(buffer, sizeof(buffer), "Relayed to %s", channels.getName(this->channel)); + } else { + snprintf(buffer, sizeof(buffer), "%s\nto %s", + this->lastAckWasRelayed ? "Delivered (Relayed)" : "Delivered (Direct)", + getNodeName(this->incoming)); + } + } else { + snprintf(buffer, sizeof(buffer), "Delivery failed\nto %s", getNodeName(this->incoming)); + } + display->drawString(display->getWidth() / 2 + x, 0 + y + 12, buffer); + display->setFont(FONT_SMALL); + + // SNR/RSSI + if (display->getHeight() > 100) { + int16_t snrY = 100; + int16_t rssiY = 130; + if (display->getHeight() < rssiY + FONT_HEIGHT_SMALL) { + snrY = display->getHeight() - ((1.5) * FONT_HEIGHT_SMALL); + rssiY = display->getHeight() - ((2.5) * FONT_HEIGHT_SMALL); + } + if (this->ack) { + display->drawStringf(display->getWidth() / 2 + x, snrY + y, buffer, "Last Rx SNR: %f", this->lastRxSnr); + display->drawStringf(display->getWidth() / 2 + x, rssiY + y, buffer, "Last Rx RSSI: %d", this->lastRxRssi); + } + } + return; + } + + // === Sending Screen === + if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) { + EINK_ADD_FRAMEFLAG(display, COSMETIC); + requestFocus(); +#ifdef USE_EINK + display->setFont(FONT_SMALL); +#else + display->setFont(FONT_MEDIUM); +#endif + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(display->getWidth() / 2 + x, 0 + y + 12, "Sending..."); + return; + } + + // === Disabled Screen === + if (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawString(10 + x, 0 + y + FONT_HEIGHT_SMALL, "Canned Message\nModule disabled."); + return; + } + + // === Free Text Input Screen === + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + requestFocus(); #if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) EInkDynamicDisplay* einkDisplay = static_cast(display); - einkDisplay->enableUnlimitedFastMode(); // Enable unlimited fast refresh while typing + einkDisplay->enableUnlimitedFastMode(); #endif - #if defined(USE_VIRTUAL_KEYBOARD) drawKeyboard(display, state, 0, 0); #else - display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); + if (this->destSelect != CANNED_MESSAGE_DESTINATION_TYPE_NONE) { display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); display->setColor(BLACK); } + switch (this->destSelect) { case CANNED_MESSAGE_DESTINATION_TYPE_NODE: - display->drawStringf(1 + x, 0 + y, buffer, "To: >%s<@%s", cannedMessageModule->getNodeName(this->dest), - channels.getName(this->channel)); - LOG_INFO("Displaying recipient: Node=%s (ID=%d)", cannedMessageModule->getNodeName(this->dest), this->dest); - display->drawStringf(0 + x, 0 + y, buffer, "To: >%s<@%s", cannedMessageModule->getNodeName(this->dest), - channels.getName(this->channel)); + display->drawStringf(0 + x, 0 + y, buffer, "To: >%s<@%s", getNodeName(this->dest), channels.getName(this->channel)); break; case CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL: - display->drawStringf(1 + x, 0 + y, buffer, "To: %s@>%s<", cannedMessageModule->getNodeName(this->dest), - channels.getName(this->channel)); - display->drawStringf(0 + x, 0 + y, buffer, "To: %s@>%s<", cannedMessageModule->getNodeName(this->dest), - channels.getName(this->channel)); + display->drawStringf(0 + x, 0 + y, buffer, "To: %s@>%s<", getNodeName(this->dest), channels.getName(this->channel)); break; default: if (display->getWidth() > 128) { - display->drawStringf(0 + x, 0 + y, buffer, "To: %s@%s", cannedMessageModule->getNodeName(this->dest), - channels.getName(this->channel)); + display->drawStringf(0 + x, 0 + y, buffer, "To: %s@%s", getNodeName(this->dest), channels.getName(this->channel)); } else { - display->drawStringf(0 + x, 0 + y, buffer, "To: %.5s@%.5s", cannedMessageModule->getNodeName(this->dest), - channels.getName(this->channel)); + display->drawStringf(0 + x, 0 + y, buffer, "To: %.5s@%.5s", getNodeName(this->dest), channels.getName(this->channel)); } break; } - // used chars right aligned, only when not editing the destination + if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) { - uint16_t charsLeft = - meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0); + uint16_t charsLeft = meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0); snprintf(buffer, sizeof(buffer), "%d left", charsLeft); display->drawString(x + display->getWidth() - display->getStringWidth(buffer), y + 0, buffer); } + display->setColor(WHITE); - display->drawStringMaxWidth( - 0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), - cannedMessageModule->drawWithCursor(cannedMessageModule->freetext, cannedMessageModule->cursor)); + display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), + drawWithCursor(this->freetext, this->cursor)); #endif - } else { - if (this->messagesCount > 0) { - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawStringf(0 + x, 0 + y, buffer, "To: %s", cannedMessageModule->getNodeName(this->dest)); - int lines = (display->getHeight() / FONT_HEIGHT_SMALL) - 1; - if (lines == 3) { - display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, cannedMessageModule->getCurrentMessage()); - display->setColor(WHITE); - if (this->messagesCount > 1) { - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL, cannedMessageModule->getPrevMessage()); - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 3, cannedMessageModule->getNextMessage()); - } - } else { - int topMsg = (messagesCount > lines && currentMessageIndex >= lines - 1) ? currentMessageIndex - lines + 2 : 0; - for (int i = 0; i < std::min(messagesCount, lines); i++) { - if (i == currentMessageIndex - topMsg) { + return; + } + + // === Canned Messages List === + if (this->messagesCount > 0) { + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawStringf(0 + x, 0 + y, buffer, "To: %s", getNodeName(this->dest)); + int lines = (display->getHeight() / FONT_HEIGHT_SMALL) - 1; + + if (lines == 3) { + display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); + display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, getCurrentMessage()); + display->setColor(WHITE); + + if (this->messagesCount > 1) { + display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL, getPrevMessage()); + display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 3, getNextMessage()); + } + } else { + int topMsg = (messagesCount > lines && currentMessageIndex >= lines - 1) ? currentMessageIndex - lines + 2 : 0; + for (int i = 0; i < std::min(messagesCount, lines); i++) { + if (i == currentMessageIndex - topMsg) { #ifdef USE_EINK - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), ">"); - display->drawString(12 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), - cannedMessageModule->getCurrentMessage()); + display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), ">"); + display->drawString(12 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), getCurrentMessage()); #else - display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), x + display->getWidth(), - y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), cannedMessageModule->getCurrentMessage()); - display->setColor(WHITE); + display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), x + display->getWidth(), y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); + display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), getCurrentMessage()); + display->setColor(WHITE); #endif - } else if (messagesCount > 1) { // Only draw others if there are multiple messages - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), - cannedMessageModule->getMessageByIndex(topMsg + i)); - } + } else { + display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), getMessageByIndex(topMsg + i)); } } } @@ -1495,17 +1525,38 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &mp) { if (mp.decoded.portnum == meshtastic_PortNum_ROUTING_APP && waitingForAck) { - // look for a request_id if (mp.decoded.request_id != 0) { UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen - requestFocus(); // Tell Screen::setFrames that our module's frame should be shown, even if not "first" in the frameset + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + requestFocus(); this->runState = CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED; - this->incoming = service->getNodenumFromRequestId(mp.decoded.request_id); + + // Decode the Routing payload to check for errors meshtastic_Routing decoded = meshtastic_Routing_init_default; pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_Routing_fields, &decoded); - this->ack = decoded.error_reason == meshtastic_Routing_Error_NONE; - waitingForAck = false; // No longer want routing packets + + // === Relay Detection === + uint8_t relayByte = mp.relay_node; + uint8_t senderLastByte = mp.from & 0xFF; + this->lastAckWasRelayed = (relayByte != senderLastByte); + + // === Accept ACK if no error AND: + // - Broadcast (allow any ACK) + // - OR matches exact destination + bool isAck = (decoded.error_reason == meshtastic_Routing_Error_NONE); + bool isFromDest = (mp.from == this->lastSentNode); + bool isBroadcast = (this->lastSentNode == NODENUM_BROADCAST); + + this->ack = isAck && (isBroadcast || isFromDest); + + // === Set .incoming to the node who ACK'd (even if it was broadcast) + if (isBroadcast && mp.from != nodeDB->getNodeNum()) { + this->incoming = mp.from; + } else { + this->incoming = this->lastSentNode; + } + + waitingForAck = false; this->notifyObservers(&e); // run the next time 2 seconds later setIntervalFromNow(2000); diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index f044d4e85..a0d4da1ec 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -61,6 +61,8 @@ class CannedMessageModule : public SinglePortModule, public Observable activeChannelIndices; + bool shouldRedraw = false; + unsigned long lastUpdateMillis = 0; public: CannedMessageModule(); @@ -164,8 +166,10 @@ class CannedMessageModule : public SinglePortModule, public Observable Date: Thu, 22 May 2025 00:14:28 -0400 Subject: [PATCH 140/265] Added destination change on Cannedmessage screen and dismiss text message frame on reply. --- src/modules/CannedMessageModule.cpp | 87 ++++++++++++++--------------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index ee1186989..dcc825c00 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -69,7 +69,7 @@ CannedMessageModule::CannedMessageModule() disable(); } } - +static bool returnToCannedList = false; bool hasKeyForNode(const meshtastic_NodeInfoLite* node) { return node && node->has_user && node->user.public_key.size > 0; } @@ -116,16 +116,19 @@ int CannedMessageModule::splitConfiguredMessages() } i += 1; } + + // Add "[Select Destination]" as the final message if (strlen(this->messages[messageIndex - 1]) > 0) { - // We have a last message. - LOG_DEBUG("CannedMessage %d is: '%s'", messageIndex - 1, this->messages[messageIndex - 1]); + this->messages[messageIndex - 1] = (char*)"[Select Destination]"; this->messagesCount = messageIndex; } else { - this->messagesCount = messageIndex - 1; + this->messages[messageIndex - 1] = (char*)"[Select Destination]"; + this->messagesCount = messageIndex; } return this->messagesCount; } + void CannedMessageModule::resetSearch() { LOG_INFO("Resetting search, restoring full destination list"); @@ -340,7 +343,8 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) } // βœ… Now correctly switches to FreeText screen with selected node/channel - this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + this->runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; + returnToCannedList = false; this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; screen->forceDisplay(); return 0; @@ -381,6 +385,15 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) } if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) { + if (strcmp(this->messages[this->currentMessageIndex], "[Select Destination]") == 0) { + returnToCannedList = true; + this->runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; + this->destIndex = 0; + this->scrollIndex = 0; + screen->forceDisplay(); + return 0; + } #if defined(USE_VIRTUAL_KEYBOARD) if (this->currentMessageIndex == 0) { this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; @@ -689,13 +702,22 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha // Prevents the canned message module from regenerating the screen's frameset at unexpected times, // or raising a UIFrameEvent before another module has the chance this->waitingForAck = true; - this->lastSentNode = dest; + LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); service->sendToMesh( p, RX_SRC_LOCAL, true); // send to mesh, cc to phone. Even if there's no phone connected, this stores the message to match ACKs + + // === Simulate local message to dismiss the message frame after sending === + // This mimics what happens when replying from the phone to clear unread state and UI + if (screen) { + meshtastic_MeshPacket simulatedPacket = {}; + simulatedPacket.from = 0; // Outgoing message (from local device) + screen->handleTextMessage(&simulatedPacket); // Calls logic to clear unread and dismiss frame + } } + unsigned long lastUpdateMillis = 0; int32_t CannedMessageModule::runOnce() { @@ -752,6 +774,10 @@ int32_t CannedMessageModule::runOnce() this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; } } else { + if (strcmp(this->messages[this->currentMessageIndex], "[Select Destination]") == 0) { + this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + return INT32_MAX; + } if ((this->messagesCount > this->currentMessageIndex) && (strlen(this->messages[this->currentMessageIndex]) > 0)) { if (strcmp(this->messages[this->currentMessageIndex], "~") == 0) { powerFSM.trigger(EVENT_PRESS); @@ -1384,18 +1410,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st #else display->setFont(FONT_MEDIUM); #endif - if (this->ack) { - if (this->lastSentNode == NODENUM_BROADCAST) { - snprintf(buffer, sizeof(buffer), "Relayed to %s", channels.getName(this->channel)); - } else { - snprintf(buffer, sizeof(buffer), "%s\nto %s", - this->lastAckWasRelayed ? "Delivered (Relayed)" : "Delivered (Direct)", - getNodeName(this->incoming)); - } - } else { - snprintf(buffer, sizeof(buffer), "Delivery failed\nto %s", getNodeName(this->incoming)); - } - display->drawString(display->getWidth() / 2 + x, 0 + y + 12, buffer); + String displayString = this->ack ? "Delivered to\n%s" : "Delivery failed\nto %s"; + display->drawStringf(display->getWidth() / 2 + x, 0 + y + 12, buffer, displayString, getNodeName(this->incoming)); display->setFont(FONT_SMALL); // SNR/RSSI @@ -1520,43 +1536,22 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st } } } -#endif //! HAS_TFT +#endif ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &mp) { if (mp.decoded.portnum == meshtastic_PortNum_ROUTING_APP && waitingForAck) { + // look for a request_id if (mp.decoded.request_id != 0) { UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - requestFocus(); + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + requestFocus(); // Tell Screen::setFrames that our module's frame should be shown, even if not "first" in the frameset this->runState = CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED; - - // Decode the Routing payload to check for errors + this->incoming = service->getNodenumFromRequestId(mp.decoded.request_id); meshtastic_Routing decoded = meshtastic_Routing_init_default; pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_Routing_fields, &decoded); - - // === Relay Detection === - uint8_t relayByte = mp.relay_node; - uint8_t senderLastByte = mp.from & 0xFF; - this->lastAckWasRelayed = (relayByte != senderLastByte); - - // === Accept ACK if no error AND: - // - Broadcast (allow any ACK) - // - OR matches exact destination - bool isAck = (decoded.error_reason == meshtastic_Routing_Error_NONE); - bool isFromDest = (mp.from == this->lastSentNode); - bool isBroadcast = (this->lastSentNode == NODENUM_BROADCAST); - - this->ack = isAck && (isBroadcast || isFromDest); - - // === Set .incoming to the node who ACK'd (even if it was broadcast) - if (isBroadcast && mp.from != nodeDB->getNodeNum()) { - this->incoming = mp.from; - } else { - this->incoming = this->lastSentNode; - } - - waitingForAck = false; + this->ack = decoded.error_reason == meshtastic_Routing_Error_NONE; + waitingForAck = false; // No longer want routing packets this->notifyObservers(&e); // run the next time 2 seconds later setIntervalFromNow(2000); From 351dff14e8030b946e0f66a64869d6748a05b241 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 22 May 2025 00:25:54 -0400 Subject: [PATCH 141/265] No more blinking icons for Eink --- src/graphics/SharedUIDisplay.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 080ca66f4..f655882c0 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -77,10 +77,12 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) static bool isBoltVisibleShared = true; uint32_t now = millis(); +#ifndef USE_EINK if (isCharging && now - lastBlinkShared > 500) { isBoltVisibleShared = !isBoltVisibleShared; lastBlinkShared = now; } +#endif bool useHorizontalBattery = (screenW > 128 && screenW > screenH); const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; @@ -162,6 +164,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) static uint32_t lastMailBlink = 0; bool showMail = false; +#ifndef USE_EINK if (hasUnreadMessage) { if (now - lastMailBlink > 500) { isMailIconVisible = !isMailIconVisible; @@ -169,6 +172,11 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) } showMail = isMailIconVisible; } +#else + if (hasUnreadMessage) { + showMail = true; + } +#endif if (showMail) { if (useHorizontalBattery) { From 9d9fb2d74cdb79c47555c475c2a35b78781866e2 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 22 May 2025 01:09:11 -0400 Subject: [PATCH 142/265] Navigation bar should no longer hide on Eink displays --- src/graphics/Screen.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index bbe0a8f48..578a709f0 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -3352,11 +3352,15 @@ void drawCustomFrameIcons(OLEDDisplay *display, OLEDDisplayUiState *state) const int totalWidth = totalIcons * iconSize + (totalIcons - 1) * spacing; const int xStart = (SCREEN_WIDTH - totalWidth) / 2; - // Only show bar briefly after switching frames + // Only show bar briefly after switching frames (unless on E-Ink) +#if defined(USE_EINK) + int y = SCREEN_HEIGHT - iconSize - 1; +#else int y = SCREEN_HEIGHT - iconSize - 1; if (millis() - lastFrameChangeTime > ICON_DISPLAY_DURATION_MS) { y = SCREEN_HEIGHT; } +#endif // Pre-calculate bounding rect const int rectX = xStart - 2 - bigOffset; From d98612a2ca43c0892a8785933e0baafd388b04e6 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 22 May 2025 01:42:08 -0400 Subject: [PATCH 143/265] Added select as the rotary encoder button press --- src/input/InputBroker.h | 2 ++ src/modules/CannedMessageModule.cpp | 24 +++++++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index db7524bb0..b993d8c1b 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -19,6 +19,8 @@ #define INPUT_BROKER_MSG_FN_SYMBOL_ON 0xf1 #define INPUT_BROKER_MSG_FN_SYMBOL_OFF 0xf2 #define INPUT_BROKER_MSG_BLUETOOTH_TOGGLE 0xAA +#define INPUT_BROKER_MSG_SELECT 0x0D // Enter key / rotary encoder click + typedef struct _InputEvent { const char *source; diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index dcc825c00..25e9ac3b8 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -251,9 +251,23 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) return 0; // Ignore input while sending } static int lastDestIndex = -1; // Cache the last index - bool selectionChanged = false; // Track if UI needs redrawing - bool isUp = event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP); - bool isDown = event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); + bool isUp = false; + bool isDown = false; + bool isSelect = false; + + // Accept both inputEvent and kbchar from rotary encoder or CardKB + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP) || + event->kbchar == INPUT_BROKER_MSG_UP) { + isUp = true; + } + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN) || + event->kbchar == INPUT_BROKER_MSG_DOWN) { + isDown = true; + } + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT) || + event->kbchar == INPUT_BROKER_MSG_SELECT) { + isSelect = true; + } if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { if (event->kbchar >= 32 && event->kbchar <= 126) { @@ -327,7 +341,7 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) screen->forceDisplay(); shouldRedraw = false; } - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) { + if (isSelect) { if (this->destIndex < static_cast(this->activeChannelIndices.size())) { this->dest = NODENUM_BROADCAST; this->channel = this->activeChannelIndices[this->destIndex]; @@ -383,7 +397,7 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) } } } - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) { + if (isSelect) { if (strcmp(this->messages[this->currentMessageIndex], "[Select Destination]") == 0) { returnToCannedList = true; From c59f16db42cba0259b63d79860b14235c709d6e9 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 22 May 2025 13:33:24 -0400 Subject: [PATCH 144/265] Eink adjustment --- src/graphics/Screen.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 578a709f0..615ea4bff 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2240,15 +2240,20 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t int totalEntries = nodeList.size(); int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; + #ifdef USE_EINK + totalRowsAvailable -= 1; + #endif int visibleNodeRows = totalRowsAvailable; + int totalColumns = 2; - int startIndex = scrollIndex * visibleNodeRows * 2; - int endIndex = std::min(startIndex + visibleNodeRows * 2, totalEntries); + 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) { int xPos = x + (col * columnWidth); @@ -2263,11 +2268,13 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); yOffset += rowYOffset; shownCount++; + rowCount++; - if (y + yOffset > display->getHeight() - FONT_HEIGHT_SMALL) { + if (rowCount >= totalRowsAvailable) { yOffset = 0; + rowCount = 0; col++; - if (col > 1) + if (col > (totalColumns - 1)) break; } } From 9e3cf441a1b6c33c36fca56776603f0dc3125d9f Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 22 May 2025 23:50:41 -0400 Subject: [PATCH 145/265] Encoder fix --- src/modules/CannedMessageModule.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 25e9ac3b8..fcfbf890a 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -270,7 +270,10 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) } if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { - if (event->kbchar >= 32 && event->kbchar <= 126) { + //Fix rotary encoder registering as character instead of navigation + if (isUp || isDown || isSelect) { + // Already handled below β€” skip character input + } else if (event->kbchar >= 32 && event->kbchar <= 126) { this->searchQuery += event->kbchar; needsUpdate = true; runOnce(); // <=== Force filtering immediately From 4a55f6468aac5284f2bbeaa3b9d34d8c2b97aa40 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 23 May 2025 02:57:20 -0400 Subject: [PATCH 146/265] Emote code cleanup --- src/graphics/Screen.cpp | 64 +++-------- src/graphics/emotes.cpp | 227 ++++++++++++++++++++++++++++++++++++++++ src/graphics/emotes.h | 85 +++++++++++++++ src/graphics/images.h | 195 ---------------------------------- 4 files changed, 329 insertions(+), 242 deletions(-) create mode 100644 src/graphics/emotes.cpp create mode 100644 src/graphics/emotes.h diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 615ea4bff..aa35ed049 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -41,6 +41,7 @@ along with this program. If not, see . #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" +#include "graphics/emotes.h" #include "input/ScanAndSelect.h" #include "input/TouchScreenImpl1.h" #include "main.h" @@ -56,6 +57,11 @@ along with this program. If not, see . #include "sleep.h" #include "target_specific.h" + +using graphics::Emote; +using graphics::emotes; +using graphics::numEmotes; + #if HAS_WIFI && !defined(ARCH_PORTDUINO) #include "mesh/wifi/WiFiAPClient.h" #endif @@ -1180,12 +1186,6 @@ bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int return validCached; } -struct Emote { - const char *code; - const uint8_t *bitmap; - int width, height; -}; - void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount) { int cursorX = x; @@ -1196,8 +1196,8 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string for (size_t i = 0; i < line.length();) { bool matched = false; for (int e = 0; e < emoteCount; ++e) { - size_t emojiLen = strlen(emotes[e].code); - if (line.compare(i, emojiLen, emotes[e].code) == 0) { + size_t emojiLen = strlen(emotes[e].label); + if (line.compare(i, emojiLen, emotes[e].label) == 0) { if (emotes[e].height > maxIconHeight) maxIconHeight = emotes[e].height; i += emojiLen; @@ -1242,11 +1242,11 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string size_t emojiLen = 0; for (int e = 0; e < emoteCount; ++e) { - size_t pos = line.find(emotes[e].code, i); + size_t pos = line.find(emotes[e].label, i); if (pos != std::string::npos && (nextEmotePos == std::string::npos || pos < nextEmotePos)) { nextEmotePos = pos; matchedEmote = &emotes[e]; - emojiLen = strlen(emotes[e].code); + emojiLen = strlen(emotes[e].label); } } @@ -1348,40 +1348,9 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 lastBounceTime = now; bounceY = (bounceY + 1) % (bounceRange * 2); } - - const Emote emotes[] = {{"\U0001F44D", thumbup, thumbs_width, thumbs_height}, - {"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, - {"\U0001F60A", smiley, smiley_width, smiley_height}, - {"\U0001F600", smiley, smiley_width, smiley_height}, - {"\U0001F642", smiley, smiley_width, smiley_height}, - {"\U0001F609", smiley, smiley_width, smiley_height}, - {"\U0001F601", smiley, smiley_width, smiley_height}, - {"❓", question, question_width, question_height}, - {"‼️", bang, bang_width, bang_height}, - {"\U0001F4A9", poo, poo_width, poo_height}, - {"\U0001F923", haha, haha_width, haha_height}, - {"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height}, - {"\U0001F920", cowboy, cowboy_width, cowboy_height}, - {"\U0001F42D", deadmau5, deadmau5_width, deadmau5_height}, - {"β˜€οΈ", sun, sun_width, sun_height}, - {"\xE2\x98\x80\xEF\xB8\x8F", sun, sun_width, sun_height}, - {"β˜”", rain, rain_width, rain_height}, - {"\u2614", rain, rain_width, rain_height}, - {"☁️", cloud, cloud_width, cloud_height}, - {"🌫️", fog, fog_width, fog_height}, - {"\U0001F608", devil, devil_width, devil_height}, - {"β™₯️", heart, heart_width, heart_height}, - {"\U0001F9E1", heart, heart_width, heart_height}, - {"\U00002763", heart, heart_width, heart_height}, - {"\U00002764", heart, heart_width, heart_height}, - {"\U0001F495", heart, heart_width, heart_height}, - {"\U0001F496", heart, heart_width, heart_height}, - {"\U0001F497", heart, heart_width, heart_height}, - {"\U0001F498", heart, heart_width, heart_height}, - {"\U0001F514", bell_icon, bell_icon_width, bell_icon_height}}; - - for (const Emote &e : emotes) { - if (strcmp(msg, e.code) == 0) { + for (int i = 0; i < numEmotes; ++i) { + const Emote &e = emotes[i]; + if (strcmp(msg, e.label) == 0){ // Draw the header if (isInverted) { drawRoundedHighlight(display, x, 0, SCREEN_WIDTH, FONT_HEIGHT_SMALL - 1, cornerRadius); @@ -1449,8 +1418,9 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 for (const auto &line : lines) { int maxHeight = FONT_HEIGHT_SMALL; - for (const Emote &e : emotes) { - if (line.find(e.code) != std::string::npos) { + for (int i = 0; i < numEmotes; ++i) { + const Emote &e = emotes[i]; + if (line.find(e.label) != std::string::npos) { if (e.height > maxHeight) maxHeight = e.height; } @@ -1526,7 +1496,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 display->drawString(x + 4, lineY, lines[i].c_str()); display->setColor(WHITE); } else { - drawStringWithEmotes(display, x, lineY, lines[i], emotes, sizeof(emotes) / sizeof(Emote)); + drawStringWithEmotes(display, x, lineY, lines[i], emotes, numEmotes); } } } diff --git a/src/graphics/emotes.cpp b/src/graphics/emotes.cpp new file mode 100644 index 000000000..38a9f2915 --- /dev/null +++ b/src/graphics/emotes.cpp @@ -0,0 +1,227 @@ +#include "emotes.h" + +namespace graphics { + +// Always define Emote list and count +const Emote emotes[] = { +#ifndef EXCLUDE_EMOJI + // --- Thumbs --- + {"\U0001F44D", thumbup, thumbs_width, thumbs_height}, // πŸ‘ Thumbs Up + {"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, // πŸ‘Ž Thumbs Down + + // --- Smileys (Multiple Unicode Aliases) --- + {"\U0001F60A", smiley, smiley_width, smiley_height}, // 😊 Smiling Face with Smiling Eyes + {"\U0001F600", smiley, smiley_width, smiley_height}, // πŸ˜€ Grinning Face + {"\U0001F642", smiley, smiley_width, smiley_height}, // πŸ™‚ Slightly Smiling Face + {"\U0001F609", smiley, smiley_width, smiley_height}, // πŸ˜‰ Winking Face + {"\U0001F601", smiley, smiley_width, smiley_height}, // 😁 Grinning Face with Smiling Eyes + + // --- Question/Alert --- + {"\u2753", question, question_width, question_height}, // ❓ Question Mark + {"\u203C\uFE0F", bang, bang_width, bang_height}, // ‼️ Double Exclamation Mark + + // --- Laughing Faces --- + {"\U0001F602", haha, haha_width, haha_height}, // πŸ˜‚ Face with Tears of Joy + {"\U0001F923", haha, haha_width, haha_height}, // 🀣 Rolling on the Floor Laughing + {"\U0001F606", haha, haha_width, haha_height}, // πŸ˜† Smiling with Open Mouth and Closed Eyes + {"\U0001F605", haha, haha_width, haha_height}, // πŸ˜… Smiling with Sweat + {"\U0001F604", haha, haha_width, haha_height}, // πŸ˜„ Grinning Face with Smiling Eyes + + // --- Gestures and People --- + {"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height},// πŸ‘‹ Waving Hand + {"\U0001F920", cowboy, cowboy_width, cowboy_height}, // 🀠 Cowboy Hat Face + {"\U0001F3A7", deadmau5, deadmau5_width, deadmau5_height}, // 🎧 Headphones + + // --- Weather --- + {"\u2600", sun, sun_width, sun_height}, // β˜€ Sun (without variation selector) + {"\u2600\uFE0F", sun, sun_width, sun_height}, // β˜€οΈ Sun (with variation selector) + {"\U0001F327\uFE0F", rain, rain_width, rain_height}, // 🌧️ Cloud with Rain + {"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // ☁️ Cloud + {"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // 🌫️ Fog + + // --- Misc Faces --- + {"\U0001F608", devil, devil_width, devil_height}, // 😈 Smiling Face with Horns + + // --- Hearts (Multiple Unicode Aliases) --- + {"\u2764\uFE0F", heart, heart_width, heart_height}, // ❀️ Red Heart + {"\U0001F9E1", heart, heart_width, heart_height}, // 🧑 Orange Heart + {"\U00002763", heart, heart_width, heart_height}, // ❣ Heart Exclamation + {"\U00002764", heart, heart_width, heart_height}, // ❀ Red Heart (legacy) + {"\U0001F495", heart, heart_width, heart_height}, // πŸ’• Two Hearts + {"\U0001F496", heart, heart_width, heart_height}, // πŸ’– Sparkling Heart + {"\U0001F497", heart, heart_width, heart_height}, // πŸ’— Growing Heart + {"\U0001F498", heart, heart_width, heart_height}, // πŸ’˜ Heart with Arrow + + // --- Objects --- + {"\U0001F4A9", poo, poo_width, poo_height}, // πŸ’© Pile of Poo + {"\U0001F514", bell_icon, bell_icon_width, bell_icon_height} // πŸ”” Bell +#endif +}; + +const int numEmotes = sizeof(emotes) / sizeof(emotes[0]); + +#ifndef EXCLUDE_EMOJI +const unsigned char thumbup[] PROGMEM = { + 0x00, 0x1C, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x80, 0x09, 0x00, 0x00, + 0xC0, 0x08, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, + 0x0C, 0xCE, 0x7F, 0x00, 0x04, 0x20, 0x80, 0x00, 0x02, 0x20, 0x80, 0x00, 0x02, 0x60, 0xC0, 0x00, 0x01, 0xF8, 0xFF, 0x01, + 0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0x18, 0x80, 0x00, + 0x02, 0x30, 0xC0, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x38, 0x20, 0x10, 0x00, 0xE0, 0xCF, 0x1F, 0x00, +}; + +const unsigned char thumbdown[] PROGMEM = { + 0xE0, 0xCF, 0x1F, 0x00, 0x38, 0x20, 0x10, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x02, 0x30, 0xC0, 0x00, + 0x01, 0x18, 0x80, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01, + 0x01, 0xF8, 0xFF, 0x01, 0x02, 0x60, 0xC0, 0x00, 0x02, 0x20, 0x80, 0x00, 0x04, 0x20, 0x80, 0x00, 0x0C, 0xCE, 0x7F, 0x00, + 0x18, 0x02, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0xC0, 0x08, 0x00, 0x00, + 0x80, 0x09, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, +}; + +const unsigned char smiley[] PROGMEM = { + 0x00, 0xfe, 0x0f, 0x00, 0x80, 0x01, 0x30, 0x00, 0x40, 0x00, 0xc0, 0x00, 0x20, 0x00, 0x00, 0x01, 0x10, 0x00, 0x00, 0x02, + 0x08, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x10, 0x02, 0x0e, 0x0e, 0x10, 0x02, 0x09, 0x12, 0x10, + 0x01, 0x09, 0x12, 0x20, 0x01, 0x0f, 0x1e, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, + 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x81, 0x00, 0x20, 0x20, + 0x82, 0x00, 0x20, 0x10, 0x02, 0x01, 0x10, 0x10, 0x04, 0x02, 0x08, 0x08, 0x04, 0xfc, 0x07, 0x08, 0x08, 0x00, 0x00, 0x04, + 0x10, 0x00, 0x00, 0x02, 0x20, 0x00, 0x00, 0x01, 0x40, 0x00, 0xc0, 0x00, 0x80, 0x01, 0x30, 0x00, 0x00, 0xfe, 0x0f, 0x00 +}; + +const unsigned char question[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x07, 0x00, + 0xE0, 0xC3, 0x0F, 0x00, 0xF0, 0x81, 0x0F, 0x00, 0xF0, 0x01, 0x0F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x80, 0x0F, 0x00, + 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x7C, 0x00, 0x00, + 0x00, 0x3C, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +const unsigned char bang[] PROGMEM = { + 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x07, 0xF8, 0x3F, 0xFF, 0x07, 0xF8, 0x3F, + 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, + 0xFE, 0x03, 0xF0, 0x1F, 0xFE, 0x03, 0xF0, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, + 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0xC0, 0x03, 0xFC, 0x03, 0xF0, 0x0F, 0xFE, 0x03, 0xF0, 0x1F, + 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xF8, 0x01, 0xE0, 0x07, +}; + +const unsigned char haha[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, + 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x1F, 0x3E, 0x00, 0x80, 0x03, 0x70, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0xC0, 0x00, 0xC2, 0x00, + 0x60, 0x00, 0x03, 0x00, 0x60, 0x00, 0xC1, 0x1F, 0x60, 0x80, 0x8F, 0x31, 0x30, 0x0E, 0x80, 0x31, 0x30, 0x10, 0x30, 0x1F, + 0x30, 0x08, 0x58, 0x00, 0x30, 0x04, 0x6C, 0x03, 0x60, 0x00, 0xF3, 0x01, 0x60, 0xC0, 0xFC, 0x01, 0x80, 0x38, 0xBF, 0x01, + 0xE0, 0xC5, 0xDF, 0x00, 0xB0, 0xF9, 0xEF, 0x00, 0x30, 0xF1, 0x73, 0x00, 0xB0, 0x1D, 0x3E, 0x00, 0xF0, 0xFD, 0x0F, 0x00, + 0xE0, 0xE0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +const unsigned char wave_icon[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xC0, 0x00, + 0x00, 0x0C, 0x9C, 0x01, 0x80, 0x17, 0x20, 0x01, 0x80, 0x26, 0x46, 0x02, 0x80, 0x44, 0x88, 0x02, 0xC0, 0x89, 0x8A, 0x02, + 0x40, 0x93, 0x8B, 0x02, 0x40, 0x26, 0x13, 0x00, 0x80, 0x44, 0x16, 0x00, 0xC0, 0x89, 0x24, 0x00, 0x40, 0x93, 0x60, 0x00, + 0x40, 0x26, 0x40, 0x00, 0x80, 0x0C, 0x80, 0x00, 0x00, 0x09, 0x80, 0x00, 0x00, 0x02, 0x80, 0x00, 0x40, 0x06, 0x80, 0x00, + 0x50, 0x0C, 0x80, 0x00, 0x50, 0x08, 0x40, 0x00, 0x90, 0x10, 0x20, 0x00, 0xB0, 0x21, 0x10, 0x00, 0x20, 0x47, 0x18, 0x00, + 0x40, 0x80, 0x0F, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +const unsigned char cowboy[] PROGMEM = { + 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x3C, 0xFE, 0x1F, 0x0F, + 0xFE, 0xFE, 0xDF, 0x1F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, + 0x3E, 0xC0, 0x00, 0x1F, 0x1E, 0x00, 0x00, 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x08, 0x0E, 0x1C, 0x04, 0x00, 0x0E, 0x1C, 0x00, + 0x04, 0x0E, 0x1C, 0x08, 0x04, 0x0E, 0x1C, 0x08, 0x04, 0x04, 0x08, 0x08, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x08, + 0x8C, 0x07, 0x70, 0x0C, 0x88, 0xFC, 0x4F, 0x04, 0x88, 0x01, 0x40, 0x04, 0x90, 0xFF, 0x7F, 0x02, 0x30, 0x03, 0x30, 0x03, + 0x60, 0x0E, 0x9C, 0x01, 0xC0, 0xF8, 0xC7, 0x00, 0x80, 0x01, 0x60, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0xF8, 0x07, 0x00, +}; + +const unsigned char deadmau5[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x07, 0x00, + 0x00, 0xFC, 0x03, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0x00, + 0xE0, 0xFF, 0xFF, 0x01, 0xF0, 0xFF, 0x7F, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0xF8, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x07, + 0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, 0x00, + 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0xFC, + 0x0F, 0xFF, 0x7F, 0x00, 0xC0, 0xFF, 0x1F, 0xF8, 0x0F, 0xFC, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0xF8, 0x1F, 0xFC, 0x1F, 0x00, + 0x00, 0xFF, 0x0F, 0xFC, 0x3F, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x1F, 0xFF, 0xFF, 0xFE, 0x01, 0x00, 0x00, 0x00, 0xFC, 0xFF, + 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, + 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xC0, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x80, 0x07, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +const unsigned char sun[] PROGMEM = { + 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x30, 0xC0, 0x00, 0x03, + 0x70, 0x00, 0x80, 0x03, 0xF0, 0x00, 0xC0, 0x03, 0xF0, 0xF8, 0xC7, 0x03, 0xE0, 0xFC, 0xCF, 0x01, 0x00, 0xFE, 0x1F, 0x00, + 0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x8E, 0xFF, 0x7F, 0x1C, 0x9F, 0xFF, 0x7F, 0x3E, + 0x9F, 0xFF, 0x7F, 0x3E, 0x8E, 0xFF, 0x7F, 0x1C, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00, + 0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0xC0, 0xF9, 0xE7, 0x00, 0xE0, 0x01, 0xE0, 0x01, 0xF0, 0x01, 0xE0, 0x03, + 0xF0, 0xC0, 0xC0, 0x03, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, +}; + +const unsigned char rain[] PROGMEM = { + 0xC0, 0x0F, 0xC0, 0x00, 0x40, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x03, 0x38, 0x00, + 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10, 0x03, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x20, + 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0x30, 0x02, 0x00, + 0x00, 0x10, 0x06, 0x00, 0x00, 0x08, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x01, 0x80, 0x00, 0x01, 0x00, + 0xC0, 0xC0, 0x81, 0x03, 0xA0, 0x60, 0xC1, 0x03, 0x90, 0x20, 0x41, 0x01, 0xF0, 0xE0, 0xC0, 0x01, 0x60, 0x4C, + 0x98, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0x0B, 0x12, 0x00, 0x00, 0x09, 0x1A, 0x00, 0x00, 0x06, 0x0E, 0x00, +}; + +const unsigned char cloud[] PROGMEM = { + 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x70, 0x30, 0x00, 0x00, 0x10, 0x60, 0x00, 0x80, 0x1F, 0x40, 0x00, + 0xC0, 0x0F, 0xC0, 0x00, 0xC0, 0x00, 0x80, 0x00, 0x60, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x01, + 0x20, 0x00, 0x00, 0x07, 0x38, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x08, 0x06, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10, + 0x02, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, + 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x10, + 0x02, 0x00, 0x00, 0x10, 0x06, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03, +}; + +const unsigned char fog[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3C, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01, + 0x00, 0x38, 0x00, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0xFF, 0x83, 0xFF, 0x01, 0x03, 0xFF, 0x81, 0x01, 0x00, 0x7C, 0x00, 0x00, + 0xF8, 0x00, 0x3E, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01, 0x00, 0x38, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +const unsigned char devil[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x10, 0x03, 0xC0, 0x01, 0x38, 0x07, 0x7C, 0x0F, 0x38, 0x1F, 0x03, 0x30, 0x1E, + 0xFE, 0x01, 0xE0, 0x1F, 0x7E, 0x00, 0x80, 0x1F, 0x3C, 0x00, 0x00, 0x0F, 0x1C, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x06, + 0x08, 0x00, 0x00, 0x04, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x0E, 0x1C, 0x0C, + 0x0C, 0x18, 0x06, 0x0C, 0x0C, 0x1C, 0x06, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x0C, 0x06, 0x0C, + 0x08, 0x00, 0x00, 0x06, 0x18, 0x02, 0x10, 0x06, 0x10, 0x0C, 0x0C, 0x03, 0x30, 0xF8, 0x07, 0x03, 0x60, 0xE0, 0x80, 0x01, + 0xC0, 0x00, 0xC0, 0x00, 0x80, 0x01, 0x70, 0x00, 0x00, 0x06, 0x1C, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +const unsigned char heart[] PROGMEM = { + 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xF0, 0x00, 0xF8, 0x0F, 0xFC, 0x07, 0xFC, 0x1F, 0x06, 0x0E, 0xFE, 0x3F, 0x03, 0x18, + 0xFE, 0xFF, 0x7F, 0x10, 0xFF, 0xFF, 0xFF, 0x31, 0xFF, 0xFF, 0xFF, 0x33, 0xFF, 0xFF, 0xFF, 0x37, 0xFF, 0xFF, 0xFF, 0x37, + 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFE, 0xFF, 0xFF, 0x1F, 0xFE, 0xFF, 0xFF, 0x1F, + 0xFC, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x0F, 0xF8, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0x03, + 0xE0, 0xFF, 0xFF, 0x01, 0xC0, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFE, 0x1F, 0x00, + 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x00, 0x00, +}; + +const unsigned char poo[] PROGMEM = { + 0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xEC, 0x01, 0x00, 0x00, 0x8C, 0x07, 0x00, 0x00, 0x0C, 0x06, 0x00, + 0x00, 0x24, 0x0C, 0x00, 0x00, 0x34, 0x08, 0x00, 0x00, 0x1F, 0x08, 0x00, 0xC0, 0x0F, 0x08, 0x00, 0xC0, 0x00, 0x3C, 0x00, + 0x60, 0x00, 0x7C, 0x00, 0x60, 0x00, 0xC6, 0x00, 0x20, 0x00, 0xCB, 0x00, 0xA0, 0xC7, 0xFF, 0x00, 0xE0, 0x7F, 0xF7, 0x00, + 0xF0, 0x18, 0xE3, 0x03, 0x78, 0x18, 0x41, 0x03, 0x6C, 0x9B, 0x5D, 0x06, 0x64, 0x9B, 0x5D, 0x04, 0x44, 0x1A, 0x41, 0x04, + 0x4C, 0xD8, 0x63, 0x06, 0xF8, 0xFC, 0x36, 0x06, 0xFE, 0x0F, 0x9C, 0x1F, 0x07, 0x03, 0xC0, 0x30, 0x03, 0x00, 0x78, 0x20, + 0x01, 0x00, 0x1F, 0x20, 0x03, 0xE0, 0x03, 0x20, 0x07, 0x7E, 0x04, 0x30, 0xFE, 0x0F, 0xFC, 0x1F, 0xF0, 0x00, 0xF0, 0x0F, +}; + +const unsigned char bell_icon[] PROGMEM = { + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11110000, + 0b00000011, 0b00000000, 0b00000000, 0b11111100, 0b00001111, 0b00000000, 0b00000000, 0b00001111, 0b00111100, 0b00000000, + 0b00000000, 0b00000011, 0b00110000, 0b00000000, 0b10000000, 0b00000001, 0b01100000, 0b00000000, 0b11000000, 0b00000000, + 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, + 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, + 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, + 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000, 0b10000000, 0b00000000, 0b01100000, 0b00000000, + 0b10000000, 0b00000001, 0b01110000, 0b00000000, 0b10000000, 0b00000011, 0b00110000, 0b00000000, 0b00000000, 0b00000011, + 0b00011000, 0b00000000, 0b00000000, 0b00000110, 0b11110000, 0b11111111, 0b11111111, 0b00000011, 0b00000000, 0b00001100, + 0b00001100, 0b00000000, 0b00000000, 0b00011000, 0b00000110, 0b00000000, 0b00000000, 0b11111000, 0b00000111, 0b00000000, + 0b00000000, 0b11100000, 0b00000001, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000 +}; +#endif + +} // namespace graphics + diff --git a/src/graphics/emotes.h b/src/graphics/emotes.h new file mode 100644 index 000000000..5204d870b --- /dev/null +++ b/src/graphics/emotes.h @@ -0,0 +1,85 @@ +#pragma once +#include + +namespace graphics { + +// === Emote List === +struct Emote { + const char *label; + const unsigned char *bitmap; + int width; + int height; +}; + +extern const Emote emotes[/* numEmotes */]; +extern const int numEmotes; + +#ifndef EXCLUDE_EMOJI +// === Emote Bitmaps === +#define thumbs_height 25 +#define thumbs_width 25 +extern const unsigned char thumbup[] PROGMEM; +extern const unsigned char thumbdown[] PROGMEM; + +#define smiley_height 30 +#define smiley_width 30 +extern const unsigned char smiley[] PROGMEM; + +#define question_height 25 +#define question_width 25 +extern const unsigned char question[] PROGMEM; + +#define bang_height 30 +#define bang_width 30 +extern const unsigned char bang[] PROGMEM; + +#define haha_height 30 +#define haha_width 30 +extern const unsigned char haha[] PROGMEM; + +#define wave_icon_height 30 +#define wave_icon_width 30 +extern const unsigned char wave_icon[] PROGMEM; + +#define cowboy_height 30 +#define cowboy_width 30 +extern const unsigned char cowboy[] PROGMEM; + +#define deadmau5_height 30 +#define deadmau5_width 60 +extern const unsigned char deadmau5[] PROGMEM; + +#define sun_height 30 +#define sun_width 30 +extern const unsigned char sun[] PROGMEM; + +#define rain_height 30 +#define rain_width 30 +extern const unsigned char rain[] PROGMEM; + +#define cloud_height 30 +#define cloud_width 30 +extern const unsigned char cloud[] PROGMEM; + +#define fog_height 25 +#define fog_width 25 +extern const unsigned char fog[] PROGMEM; + +#define devil_height 30 +#define devil_width 30 +extern const unsigned char devil[] PROGMEM; + +#define heart_height 30 +#define heart_width 30 +extern const unsigned char heart[] PROGMEM; + +#define poo_height 30 +#define poo_width 30 +extern const unsigned char poo[] PROGMEM; + +#define bell_icon_width 30 +#define bell_icon_height 30 +extern const unsigned char bell_icon[] PROGMEM; +#endif // EXCLUDE_EMOJI + +} // namespace graphics diff --git a/src/graphics/images.h b/src/graphics/images.h index db61cb055..e2f000256 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -68,198 +68,6 @@ const unsigned char batteryBitmap_sidegaps_v[] PROGMEM = {0b10000010, 0b10000010 // Lightning Bolt const unsigned char lightning_bolt_v[] PROGMEM = {0b00000100, 0b00000110, 0b00011111, 0b00001100, 0b00000100}; -#ifndef EXCLUDE_EMOJI -#define thumbs_height 25 -#define thumbs_width 25 -static unsigned char thumbup[] PROGMEM = { - 0x00, 0x1C, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x80, 0x09, 0x00, 0x00, - 0xC0, 0x08, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, - 0x0C, 0xCE, 0x7F, 0x00, 0x04, 0x20, 0x80, 0x00, 0x02, 0x20, 0x80, 0x00, 0x02, 0x60, 0xC0, 0x00, 0x01, 0xF8, 0xFF, 0x01, - 0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0x18, 0x80, 0x00, - 0x02, 0x30, 0xC0, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x38, 0x20, 0x10, 0x00, 0xE0, 0xCF, 0x1F, 0x00, -}; - -static unsigned char thumbdown[] PROGMEM = { - 0xE0, 0xCF, 0x1F, 0x00, 0x38, 0x20, 0x10, 0x00, 0x0C, 0x20, 0x30, 0x00, 0x06, 0xE0, 0x3F, 0x00, 0x02, 0x30, 0xC0, 0x00, - 0x01, 0x18, 0x80, 0x00, 0x01, 0x10, 0x80, 0x00, 0x01, 0xF8, 0xFF, 0x00, 0x01, 0x08, 0x00, 0x01, 0x01, 0x08, 0x00, 0x01, - 0x01, 0xF8, 0xFF, 0x01, 0x02, 0x60, 0xC0, 0x00, 0x02, 0x20, 0x80, 0x00, 0x04, 0x20, 0x80, 0x00, 0x0C, 0xCE, 0x7F, 0x00, - 0x18, 0x02, 0x00, 0x00, 0x10, 0x04, 0x00, 0x00, 0x20, 0x04, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0xC0, 0x08, 0x00, 0x00, - 0x80, 0x09, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, -}; - -#define smiley_height 30 -#define smiley_width 30 -static unsigned char smiley[] PROGMEM = { - 0x00, 0xfe, 0x0f, 0x00, 0x80, 0x01, 0x30, 0x00, 0x40, 0x00, 0xc0, 0x00, 0x20, 0x00, 0x00, 0x01, 0x10, 0x00, 0x00, 0x02, - 0x08, 0x00, 0x00, 0x04, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x10, 0x02, 0x0e, 0x0e, 0x10, 0x02, 0x09, 0x12, 0x10, - 0x01, 0x09, 0x12, 0x20, 0x01, 0x0f, 0x1e, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, - 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x81, 0x00, 0x20, 0x20, - 0x82, 0x00, 0x20, 0x10, 0x02, 0x01, 0x10, 0x10, 0x04, 0x02, 0x08, 0x08, 0x04, 0xfc, 0x07, 0x08, 0x08, 0x00, 0x00, 0x04, - 0x10, 0x00, 0x00, 0x02, 0x20, 0x00, 0x00, 0x01, 0x40, 0x00, 0xc0, 0x00, 0x80, 0x01, 0x30, 0x00, 0x00, 0xfe, 0x0f, 0x00}; - -#define question_height 25 -#define question_width 25 -static unsigned char question[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x07, 0x00, - 0xE0, 0xC3, 0x0F, 0x00, 0xF0, 0x81, 0x0F, 0x00, 0xF0, 0x01, 0x0F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x80, 0x0F, 0x00, - 0x00, 0xC0, 0x0F, 0x00, 0x00, 0xE0, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xF8, 0x01, 0x00, 0x00, 0x7C, 0x00, 0x00, - 0x00, 0x3C, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -}; - -#define bang_height 30 -#define bang_width 30 -static unsigned char bang[] PROGMEM = { - 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x0F, 0xFC, 0x3F, 0xFF, 0x07, 0xF8, 0x3F, 0xFF, 0x07, 0xF8, 0x3F, - 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, - 0xFE, 0x03, 0xF0, 0x1F, 0xFE, 0x03, 0xF0, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, - 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x03, 0xF0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F, 0xFC, 0x01, 0xE0, 0x0F, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0xC0, 0x03, 0xFC, 0x03, 0xF0, 0x0F, 0xFE, 0x03, 0xF0, 0x1F, - 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFE, 0x07, 0xF8, 0x1F, 0xFC, 0x03, 0xF0, 0x0F, 0xF8, 0x01, 0xE0, 0x07, -}; - -#define haha_height 30 -#define haha_width 30 -static unsigned char haha[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, - 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x1F, 0x3E, 0x00, 0x80, 0x03, 0x70, 0x00, 0xC0, 0x01, 0xE0, 0x00, 0xC0, 0x00, 0xC2, 0x00, - 0x60, 0x00, 0x03, 0x00, 0x60, 0x00, 0xC1, 0x1F, 0x60, 0x80, 0x8F, 0x31, 0x30, 0x0E, 0x80, 0x31, 0x30, 0x10, 0x30, 0x1F, - 0x30, 0x08, 0x58, 0x00, 0x30, 0x04, 0x6C, 0x03, 0x60, 0x00, 0xF3, 0x01, 0x60, 0xC0, 0xFC, 0x01, 0x80, 0x38, 0xBF, 0x01, - 0xE0, 0xC5, 0xDF, 0x00, 0xB0, 0xF9, 0xEF, 0x00, 0x30, 0xF1, 0x73, 0x00, 0xB0, 0x1D, 0x3E, 0x00, 0xF0, 0xFD, 0x0F, 0x00, - 0xE0, 0xE0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -}; - -#define wave_icon_height 30 -#define wave_icon_width 30 -static unsigned char wave_icon[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0xC0, 0x00, - 0x00, 0x0C, 0x9C, 0x01, 0x80, 0x17, 0x20, 0x01, 0x80, 0x26, 0x46, 0x02, 0x80, 0x44, 0x88, 0x02, 0xC0, 0x89, 0x8A, 0x02, - 0x40, 0x93, 0x8B, 0x02, 0x40, 0x26, 0x13, 0x00, 0x80, 0x44, 0x16, 0x00, 0xC0, 0x89, 0x24, 0x00, 0x40, 0x93, 0x60, 0x00, - 0x40, 0x26, 0x40, 0x00, 0x80, 0x0C, 0x80, 0x00, 0x00, 0x09, 0x80, 0x00, 0x00, 0x02, 0x80, 0x00, 0x40, 0x06, 0x80, 0x00, - 0x50, 0x0C, 0x80, 0x00, 0x50, 0x08, 0x40, 0x00, 0x90, 0x10, 0x20, 0x00, 0xB0, 0x21, 0x10, 0x00, 0x20, 0x47, 0x18, 0x00, - 0x40, 0x80, 0x0F, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -}; - -#define cowboy_height 30 -#define cowboy_width 30 -static unsigned char cowboy[] PROGMEM = { - 0x00, 0xF0, 0x03, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x3C, 0xFE, 0x1F, 0x0F, - 0xFE, 0xFE, 0xDF, 0x1F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, - 0x3E, 0xC0, 0x00, 0x1F, 0x1E, 0x00, 0x00, 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x08, 0x0E, 0x1C, 0x04, 0x00, 0x0E, 0x1C, 0x00, - 0x04, 0x0E, 0x1C, 0x08, 0x04, 0x0E, 0x1C, 0x08, 0x04, 0x04, 0x08, 0x08, 0x04, 0x00, 0x00, 0x08, 0x04, 0x00, 0x00, 0x08, - 0x8C, 0x07, 0x70, 0x0C, 0x88, 0xFC, 0x4F, 0x04, 0x88, 0x01, 0x40, 0x04, 0x90, 0xFF, 0x7F, 0x02, 0x30, 0x03, 0x30, 0x03, - 0x60, 0x0E, 0x9C, 0x01, 0xC0, 0xF8, 0xC7, 0x00, 0x80, 0x01, 0x60, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0xF8, 0x07, 0x00, -}; - -#define deadmau5_height 30 -#define deadmau5_width 60 -static unsigned char deadmau5[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x07, 0x00, - 0x00, 0xFC, 0x03, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0x00, 0xC0, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0x00, - 0xE0, 0xFF, 0xFF, 0x01, 0xF0, 0xFF, 0x7F, 0x00, 0xF0, 0xFF, 0xFF, 0x03, 0xF8, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x07, - 0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0xFE, 0xFF, 0xFF, 0x00, - 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xF0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0xE0, 0xFF, 0x3F, 0xFC, - 0x0F, 0xFF, 0x7F, 0x00, 0xC0, 0xFF, 0x1F, 0xF8, 0x0F, 0xFC, 0x3F, 0x00, 0x80, 0xFF, 0x0F, 0xF8, 0x1F, 0xFC, 0x1F, 0x00, - 0x00, 0xFF, 0x0F, 0xFC, 0x3F, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x1F, 0xFF, 0xFF, 0xFE, 0x01, 0x00, 0x00, 0x00, 0xFC, 0xFF, - 0xFF, 0x07, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, - 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xC0, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x80, 0x07, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -}; - -#define sun_width 30 -#define sun_height 30 -static unsigned char sun[] PROGMEM = { - 0x00, 0xC0, 0x00, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x30, 0xC0, 0x00, 0x03, - 0x70, 0x00, 0x80, 0x03, 0xF0, 0x00, 0xC0, 0x03, 0xF0, 0xF8, 0xC7, 0x03, 0xE0, 0xFC, 0xCF, 0x01, 0x00, 0xFE, 0x1F, 0x00, - 0x00, 0xFF, 0x3F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x8E, 0xFF, 0x7F, 0x1C, 0x9F, 0xFF, 0x7F, 0x3E, - 0x9F, 0xFF, 0x7F, 0x3E, 0x8E, 0xFF, 0x7F, 0x1C, 0x80, 0xFF, 0x7F, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00, - 0x00, 0xFE, 0x1F, 0x00, 0x00, 0xFC, 0x0F, 0x00, 0xC0, 0xF9, 0xE7, 0x00, 0xE0, 0x01, 0xE0, 0x01, 0xF0, 0x01, 0xE0, 0x03, - 0xF0, 0xC0, 0xC0, 0x03, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0xC0, 0x00, 0x00, -}; - -#define rain_width 30 -#define rain_height 30 -static unsigned char rain[] PROGMEM = { - 0xC0, 0x0F, 0xC0, 0x00, 0x40, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x03, 0x38, 0x00, - 0x00, 0x0E, 0x0C, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10, 0x03, 0x00, 0x00, 0x30, 0x01, 0x00, 0x00, 0x20, - 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x03, 0x00, 0x00, 0x30, 0x02, 0x00, - 0x00, 0x10, 0x06, 0x00, 0x00, 0x08, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x01, 0x80, 0x00, 0x01, 0x00, - 0xC0, 0xC0, 0x81, 0x03, 0xA0, 0x60, 0xC1, 0x03, 0x90, 0x20, 0x41, 0x01, 0xF0, 0xE0, 0xC0, 0x01, 0x60, 0x4C, - 0x98, 0x00, 0x00, 0x0E, 0x1C, 0x00, 0x00, 0x0B, 0x12, 0x00, 0x00, 0x09, 0x1A, 0x00, 0x00, 0x06, 0x0E, 0x00, -}; - -#define cloud_height 30 -#define cloud_width 30 -static unsigned char cloud[] PROGMEM = { - 0x00, 0x80, 0x07, 0x00, 0x00, 0xE0, 0x1F, 0x00, 0x00, 0x70, 0x30, 0x00, 0x00, 0x10, 0x60, 0x00, 0x80, 0x1F, 0x40, 0x00, - 0xC0, 0x0F, 0xC0, 0x00, 0xC0, 0x00, 0x80, 0x00, 0x60, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x00, 0x20, 0x00, 0x80, 0x01, - 0x20, 0x00, 0x00, 0x07, 0x38, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x08, 0x06, 0x00, 0x00, 0x18, 0x02, 0x00, 0x00, 0x10, - 0x02, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, - 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x30, 0x03, 0x00, 0x00, 0x10, - 0x02, 0x00, 0x00, 0x10, 0x06, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x0C, 0xFC, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03, -}; - -#define fog_height 25 -#define fog_width 25 -static unsigned char fog[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x3C, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01, - 0x00, 0x38, 0x00, 0x00, 0xFC, 0x00, 0x7E, 0x00, 0xFF, 0x83, 0xFF, 0x01, 0x03, 0xFF, 0x81, 0x01, 0x00, 0x7C, 0x00, 0x00, - 0xF8, 0x00, 0x3E, 0x00, 0xFE, 0x01, 0xFF, 0x00, 0x87, 0xC7, 0xC3, 0x01, 0x03, 0xFE, 0x80, 0x01, 0x00, 0x38, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -}; - -#define devil_height 30 -#define devil_width 30 -static unsigned char devil[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x10, 0x03, 0xC0, 0x01, 0x38, 0x07, 0x7C, 0x0F, 0x38, 0x1F, 0x03, 0x30, 0x1E, - 0xFE, 0x01, 0xE0, 0x1F, 0x7E, 0x00, 0x80, 0x1F, 0x3C, 0x00, 0x00, 0x0F, 0x1C, 0x00, 0x00, 0x0E, 0x18, 0x00, 0x00, 0x06, - 0x08, 0x00, 0x00, 0x04, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x0E, 0x1C, 0x0C, - 0x0C, 0x18, 0x06, 0x0C, 0x0C, 0x1C, 0x06, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x1C, 0x0E, 0x0C, 0x0C, 0x0C, 0x06, 0x0C, - 0x08, 0x00, 0x00, 0x06, 0x18, 0x02, 0x10, 0x06, 0x10, 0x0C, 0x0C, 0x03, 0x30, 0xF8, 0x07, 0x03, 0x60, 0xE0, 0x80, 0x01, - 0xC0, 0x00, 0xC0, 0x00, 0x80, 0x01, 0x70, 0x00, 0x00, 0x06, 0x1C, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, -}; - -#define heart_height 30 -#define heart_width 30 -static unsigned char heart[] PROGMEM = { - 0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xF0, 0x00, 0xF8, 0x0F, 0xFC, 0x07, 0xFC, 0x1F, 0x06, 0x0E, 0xFE, 0x3F, 0x03, 0x18, - 0xFE, 0xFF, 0x7F, 0x10, 0xFF, 0xFF, 0xFF, 0x31, 0xFF, 0xFF, 0xFF, 0x33, 0xFF, 0xFF, 0xFF, 0x37, 0xFF, 0xFF, 0xFF, 0x37, - 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFF, 0xFF, 0xFF, 0x3F, 0xFE, 0xFF, 0xFF, 0x1F, 0xFE, 0xFF, 0xFF, 0x1F, - 0xFC, 0xFF, 0xFF, 0x0F, 0xFC, 0xFF, 0xFF, 0x0F, 0xF8, 0xFF, 0xFF, 0x07, 0xF0, 0xFF, 0xFF, 0x03, 0xF0, 0xFF, 0xFF, 0x03, - 0xE0, 0xFF, 0xFF, 0x01, 0xC0, 0xFF, 0xFF, 0x00, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0xFF, 0x3F, 0x00, 0x00, 0xFE, 0x1F, 0x00, - 0x00, 0xFC, 0x0F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0x00, 0xF0, 0x03, 0x00, 0x00, 0xE0, 0x01, 0x00, 0x00, 0x80, 0x00, 0x00, -}; - -#define poo_width 30 -#define poo_height 30 -static unsigned char poo[] PROGMEM = { - 0x00, 0x1C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0x00, 0xEC, 0x01, 0x00, 0x00, 0x8C, 0x07, 0x00, 0x00, 0x0C, 0x06, 0x00, - 0x00, 0x24, 0x0C, 0x00, 0x00, 0x34, 0x08, 0x00, 0x00, 0x1F, 0x08, 0x00, 0xC0, 0x0F, 0x08, 0x00, 0xC0, 0x00, 0x3C, 0x00, - 0x60, 0x00, 0x7C, 0x00, 0x60, 0x00, 0xC6, 0x00, 0x20, 0x00, 0xCB, 0x00, 0xA0, 0xC7, 0xFF, 0x00, 0xE0, 0x7F, 0xF7, 0x00, - 0xF0, 0x18, 0xE3, 0x03, 0x78, 0x18, 0x41, 0x03, 0x6C, 0x9B, 0x5D, 0x06, 0x64, 0x9B, 0x5D, 0x04, 0x44, 0x1A, 0x41, 0x04, - 0x4C, 0xD8, 0x63, 0x06, 0xF8, 0xFC, 0x36, 0x06, 0xFE, 0x0F, 0x9C, 0x1F, 0x07, 0x03, 0xC0, 0x30, 0x03, 0x00, 0x78, 0x20, - 0x01, 0x00, 0x1F, 0x20, 0x03, 0xE0, 0x03, 0x20, 0x07, 0x7E, 0x04, 0x30, 0xFE, 0x0F, 0xFC, 0x1F, 0xF0, 0x00, 0xF0, 0x0F, -}; - -#define bell_icon_width 30 -#define bell_icon_height 30 -static unsigned char bell_icon[] PROGMEM = { - 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11110000, - 0b00000011, 0b00000000, 0b00000000, 0b11111100, 0b00001111, 0b00000000, 0b00000000, 0b00001111, 0b00111100, 0b00000000, - 0b00000000, 0b00000011, 0b00110000, 0b00000000, 0b10000000, 0b00000001, 0b01100000, 0b00000000, 0b11000000, 0b00000000, - 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, - 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, - 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, - 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000, 0b10000000, 0b00000000, 0b01100000, 0b00000000, - 0b10000000, 0b00000001, 0b01110000, 0b00000000, 0b10000000, 0b00000011, 0b00110000, 0b00000000, 0b00000000, 0b00000011, - 0b00011000, 0b00000000, 0b00000000, 0b00000110, 0b11110000, 0b11111111, 0b11111111, 0b00000011, 0b00000000, 0b00001100, - 0b00001100, 0b00000000, 0b00000000, 0b00011000, 0b00000110, 0b00000000, 0b00000000, 0b11111000, 0b00000111, 0b00000000, - 0b00000000, 0b11100000, 0b00000001, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}; - #define mail_width 10 #define mail_height 7 static const unsigned char mail[] PROGMEM = { @@ -447,7 +255,4 @@ const uint8_t key_symbol[] PROGMEM = { 0b00000000 }; - -#endif - #include "img/icon.xbm" From 10f1567b96844889a6ac064381f6476e94053c23 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 23 May 2025 21:08:25 -0400 Subject: [PATCH 147/265] Bluetooth dissabled indicator --- src/graphics/Screen.cpp | 9 +++++++-- src/graphics/images.h | 13 +++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index aa35ed049..2f332ceda 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2675,8 +2675,7 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i config.display.heading_bold = origBold; - - // Crafting all the data first so we can use it + // === Third & Fourth Rows: Node Identity === int textWidth = 0; int nameX = 0; int yOffset = (SCREEN_WIDTH > 128) ? 0 : 7; @@ -2718,6 +2717,12 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i display->drawString(nameX, compactFourthLine, shortnameble); } + // === Fifth Row: Bluetooth Off Icon === + if (!config.bluetooth.enabled) { + const int iconX = 2; // Left aligned + const int iconY = compactFifthLine; + display->drawXbm(iconX, iconY, placeholder_width, placeholder_height, placeholder); + } } // **************************** diff --git a/src/graphics/images.h b/src/graphics/images.h index e2f000256..ef9f8adc9 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -255,4 +255,17 @@ const uint8_t key_symbol[] PROGMEM = { 0b00000000 }; +#define placeholder_width 8 +#define placeholder_height 8 +const uint8_t placeholder[] PROGMEM = { + 0b11111111, + 0b11111111, + 0b11111111, + 0b11111111, + 0b11111111, + 0b11111111, + 0b11111111, + 0b11111111 +}; + #include "img/icon.xbm" From 06a65bd80e5881ff2993b796406482ba5853ad71 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 24 May 2025 03:45:00 -0400 Subject: [PATCH 148/265] Nodeinfo screens for favorites feature --- src/graphics/Screen.cpp | 219 +++++++++++++++++------------------- src/graphics/Screen.h | 26 ++--- src/graphics/images.h | 13 +++ src/modules/AdminModule.cpp | 2 + 4 files changed, 131 insertions(+), 129 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 2f332ceda..219d0d836 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1863,97 +1863,100 @@ uint16_t Screen::getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) // ********************* static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + static std::vector favoritedNodes; + static int prevFrame = -1; + + if (state->currentFrame != prevFrame) { + prevFrame = state->currentFrame; + + favoritedNodes.clear(); + size_t total = nodeDB->getNumMeshNodes(); + for (size_t i = 0; i < total; i++) { + meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + if (!n || n->num == nodeDB->getNodeNum()) continue; + if (n->is_favorite) favoritedNodes.push_back(n); + } + + // Sort favorites by node number to keep consistent order + std::sort(favoritedNodes.begin(), favoritedNodes.end(), [](meshtastic_NodeInfoLite *a, meshtastic_NodeInfoLite *b) { + return a->num < b->num; + }); + } + + if (favoritedNodes.empty()) return; + + int nodeIndex = state->currentFrame - (screen->frameCount - favoritedNodes.size()); + if (nodeIndex < 0 || nodeIndex >= (int)favoritedNodes.size()) return; + + meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; + if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) return; + display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); // === Header === graphics::drawCommonHeader(display, x, y); - // === Reset color in case inverted mode left it BLACK === - display->setColor(WHITE); - - // === Advance to next favorite node when frame changes === - if (state->currentFrame != prevFrame) { - prevFrame = state->currentFrame; - - int attempts = 0; - int total = nodeDB->getNumMeshNodes(); - do { - nodeIndex = (nodeIndex + 1) % total; - meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(nodeIndex); - if (n && n->is_favorite && n->num != nodeDB->getNodeNum()) { - break; - } - } while (++attempts < total); - } - - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(nodeIndex); - if (!node || !node->is_favorite || node->num == nodeDB->getNodeNum()) - return; - - // === Draw Title (centered safe short name or ID) === - static char titleBuf[20]; - const char *titleStr = nullptr; - - bool valid = node->has_user && strlen(node->user.short_name) > 0; - if (valid) { - for (size_t i = 0; i < strlen(node->user.short_name); i++) { - uint8_t c = (uint8_t)node->user.short_name[i]; - if (c < 32 || c > 126) { - valid = false; - break; - } - } - } - - if (valid) { - titleStr = node->user.short_name; - } else { - snprintf(titleBuf, sizeof(titleBuf), "%04X", (uint16_t)(node->num & 0xFFFF)); - titleStr = titleBuf; - } - - const int centerX = x + SCREEN_WIDTH / 2; + // === Title: Short Name centered in header row === const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int headerOffsetY = 2; - const int titleY = y + headerOffsetY + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int centerX = x + SCREEN_WIDTH / 2; + const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; + + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) + display->setColor(BLACK); display->setTextAlignment(TEXT_ALIGN_CENTER); - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->setColor(BLACK); - } - display->drawString(centerX, titleY, titleStr); - if (config.display.heading_bold) { - display->drawString(centerX + 1, titleY, titleStr); - } + display->setFont(FONT_SMALL); + display->drawString(centerX, textY, shortName); + if (config.display.heading_bold) + display->drawString(centerX + 1, textY, shortName); + display->setColor(WHITE); display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); - // === First Row: Last Heard === - static char lastStr[20]; - screen->getTimeAgoStr(sinceLastSeen(node), lastStr, sizeof(lastStr)); - display->drawString(x, compactFirstLine, lastStr); + const char *username = node->has_user ? node->user.long_name : "Unknown Name"; - // === Second Row: Signal / Hops === static char signalStr[20]; - if (node->hops_away > 0) { - snprintf(signalStr, sizeof(signalStr), "Hops Away: %d", node->hops_away); - } else { + if (node->hops_away > 0) + snprintf(signalStr, sizeof(signalStr), "Hops: %d", node->hops_away); + else snprintf(signalStr, sizeof(signalStr), "Signal: %d%%", clamp((int)((node->snr + 10) * 5), 0, 100)); - } - display->drawString(x, compactSecondLine, signalStr); - // === Third Row: Distance and Bearing === + static char seenStr[20]; + uint32_t seconds = sinceLastSeen(node); + if (seconds == 0 || seconds == UINT32_MAX) { + snprintf(seenStr, sizeof(seenStr), "Heard: ?"); + } else { + uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; + snprintf(seenStr, sizeof(seenStr), (days > 365 ? "Heard: ?" : "Heard: %d%c ago"), + (days ? days + : hours ? hours + : minutes), + (days ? 'd' + : hours ? 'h' + : 'm')); + } + static char distStr[20]; - strncpy(distStr, "? km ?Β°", sizeof(distStr)); - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - strncpy(distStr, "? mi ?Β°", sizeof(distStr)); - } + strncpy(distStr, + (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "? mi ?Β°" : "? km ?Β°", + sizeof(distStr)); + // === First Row: Long Name === + display->drawString(x, compactFirstLine, username); + + // === Second Row: Last Seen === + display->drawString(x, compactSecondLine, seenStr); + + // === Third Row: Signal Strength or Hops === + display->drawString(x, compactThirdLine, signalStr); + + // === Fourth Row: Distance/Bearing === + display->drawString(x, compactFourthLine, distStr); + + // === Compass Rendering (resized like CompassAndLocation screen) === meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - - // === Match GPS screen compass position === const int16_t topY = compactFirstLine; const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); const int16_t usableHeight = bottomY - topY - 5; @@ -1966,56 +1969,38 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; bool hasNodeHeading = false; - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { - const meshtastic_PositionLite &op = ourNode->position; - float myHeading = screen->hasHeading() ? radians(screen->getHeading()) - : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { + const auto &op = ourNode->position; + float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 + : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); screen->drawCompassNorth(display, compassX, compassY, myHeading); if (nodeDB->hasValidPosition(node)) { hasNodeHeading = true; - const meshtastic_PositionLite &p = node->position; + const auto &p = node->position; + float d = GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), + DegD(op.latitude_i), DegD(op.longitude_i)); + float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), + DegD(p.latitude_i), DegD(p.longitude_i)); + if (!config.display.compass_north_top) bearing -= myHeading; - float d = - GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearing); - float bearingToOther = - GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); - - if (!config.display.compass_north_top) - bearingToOther -= myHeading; - - screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); - - float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther; - bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI; - - 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); - } else { - if (d < 2000) - snprintf(distStr, sizeof(distStr), "%.0fm %.0fΒ°", d, bearingToOtherDegrees); - else - snprintf(distStr, sizeof(distStr), "%.1fkm %.0fΒ°", d / 1000, bearingToOtherDegrees); - } + float bearingDeg = fmodf((bearing < 0 ? bearing + 2 * PI : bearing) * 180 / PI, 360.0f); + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) + snprintf(distStr, sizeof(distStr), d < 2 * MILES_TO_FEET ? "%.0fft %.0fΒ°" : "%.1fmi %.0fΒ°", + d * METERS_TO_FEET / (d < 2 * MILES_TO_FEET ? 1 : MILES_TO_FEET), bearingDeg); + else + snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0fΒ°" : "%.1fkm %.0fΒ°", + d / (d < 2000 ? 1 : 1000), bearingDeg); } } - display->drawString(x, compactThirdLine, distStr); - - if (!hasNodeHeading) { + if (!hasNodeHeading) display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?"); - } display->drawCircle(compassX, compassY, compassRadius); - - // === Final reset to WHITE to ensure clean state for next frame === - display->setColor(WHITE); } // Combined dynamic node list frame cycling through LastHeard, HopSignal, and Distance modes @@ -3859,7 +3844,7 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = screen->digitalWatchFace ? &Screen::drawDigitalClockFrame : &Screen::drawAnalogClockFrame; #endif - // βœ… Declare this early so it’s available in FOCUS_PRESERVE block + // Declare this early so it’s available in FOCUS_PRESERVE block bool willInsertTextMessage = shouldDrawMessage(&devicestate.rx_text_message); if (willInsertTextMessage) { @@ -3903,11 +3888,13 @@ void Screen::setFrames(FrameFocus focus) indicatorIcons.push_back(icon_memory); } - // then all the nodes - // We only show a few nodes in our scrolling list - because meshes with many nodes would have too many screens - // size_t numToShow = min(numMeshNodes, 4U); - // for (size_t i = 0; i < numToShow; i++) - // normalFrames[numframes++] = drawNodeInfo; + for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { + meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { + normalFrames[numframes++] = drawNodeInfo; + indicatorIcons.push_back(icon_node); + } + } // then the debug info diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 005c0fa02..b92cdbc91 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -184,6 +184,19 @@ class Screen : public concurrency::OSThread size_t frameCount = 0; // Total number of active frames ~Screen(); + // Which frame we want to be displayed, after we regen the frameset by calling setFrames + enum FrameFocus : uint8_t { + FOCUS_DEFAULT, // No specific frame + FOCUS_PRESERVE, // Return to the previous frame + FOCUS_FAULT, + FOCUS_TEXTMESSAGE, + FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus + }; + + // Regenerate the normal set of frames, focusing a specific frame if requested + // Call when a frame should be added / removed, or custom frames should be cleared + void setFrames(FrameFocus focus = FOCUS_DEFAULT); + std::vector indicatorIcons; // Per-frame custom icon pointers Screen(const Screen &) = delete; Screen &operator=(const Screen &) = delete; @@ -624,19 +637,6 @@ class Screen : public concurrency::OSThread bool memory = false; } dismissedFrames; - // Which frame we want to be displayed, after we regen the frameset by calling setFrames - enum FrameFocus : uint8_t { - FOCUS_DEFAULT, // No specific frame - FOCUS_PRESERVE, // Return to the previous frame - FOCUS_FAULT, - FOCUS_TEXTMESSAGE, - FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus - }; - - // Regenerate the normal set of frames, focusing a specific frame if requested - // Call when a frame should be added / removed, or custom frames should be cleared - void setFrames(FrameFocus focus = FOCUS_DEFAULT); - /// Try to start drawing ASAP void setFastFramerate(); diff --git a/src/graphics/images.h b/src/graphics/images.h index ef9f8adc9..9e421dba9 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -268,4 +268,17 @@ const uint8_t placeholder[] PROGMEM = { 0b11111111 }; +#define icon_node_width 8 +#define icon_node_height 8 +static const uint8_t icon_node[] PROGMEM = { + 0x10, // # + 0x10, // # ← antenna + 0x10, // # + 0xFE, // ####### ← device top + 0x82, // # # + 0xAA, // # # # # ← body with pattern + 0x92, // # # # + 0xFE // ####### ← device base +}; + #include "img/icon.xbm" diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index cf322d4ff..b4ebf314a 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -297,6 +297,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta if (node != NULL) { node->is_favorite = true; saveChanges(SEGMENT_NODEDATABASE, false); + if (screen) screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens } break; } @@ -306,6 +307,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta if (node != NULL) { node->is_favorite = false; saveChanges(SEGMENT_NODEDATABASE, false); + if (screen) screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens } break; } From 8f717d58e7b4489d6a37d96a7d47a241759d8a36 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 24 May 2025 03:52:49 -0400 Subject: [PATCH 149/265] Scroll navigation bar to next page when too long --- src/graphics/Screen.cpp | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 219d0d836..98b13a57a 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -3296,8 +3296,7 @@ static int8_t lastFrameIndex = -1; static uint32_t lastFrameChangeTime = 0; constexpr uint32_t ICON_DISPLAY_DURATION_MS = 1000; -// Bottom navigation icons -void drawCustomFrameIcons(OLEDDisplay *display, OLEDDisplayUiState *state) +void NavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) { int currentFrame = state->currentFrame; @@ -3313,10 +3312,15 @@ void drawCustomFrameIcons(OLEDDisplay *display, OLEDDisplayUiState *state) const int bigOffset = useBigIcons ? 1 : 0; const size_t totalIcons = screen->indicatorIcons.size(); - if (totalIcons == 0) - return; + if (totalIcons == 0) return; - const int totalWidth = totalIcons * iconSize + (totalIcons - 1) * spacing; + const int iconsPerPage = (SCREEN_WIDTH + spacing) / (iconSize + spacing); + const int totalPages = (totalIcons + iconsPerPage - 1) / iconsPerPage; + const int currentPage = currentFrame / iconsPerPage; + const int pageStart = currentPage * iconsPerPage; + const int pageEnd = min(pageStart + iconsPerPage, totalIcons); + + const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing; const int xStart = (SCREEN_WIDTH - totalWidth) / 2; // Only show bar briefly after switching frames (unless on E-Ink) @@ -3340,10 +3344,10 @@ void drawCustomFrameIcons(OLEDDisplay *display, OLEDDisplayUiState *state) display->setColor(WHITE); display->drawRect(rectX, y - 2, rectWidth, rectHeight); - // Icon drawing loop - for (size_t i = 0; i < totalIcons; ++i) { + // Icon drawing loop for the current page + for (size_t i = pageStart; i < pageEnd; ++i) { const uint8_t *icon = screen->indicatorIcons[i]; - const int x = xStart + i * (iconSize + spacing); + const int x = xStart + (i - pageStart) * (iconSize + spacing); const bool isActive = (i == static_cast(currentFrame)); if (isActive) { @@ -3405,7 +3409,7 @@ void Screen::setup() // === Set custom overlay callbacks === static OverlayCallback overlays[] = { drawFunctionOverlay, // For mute/buzzer modifiers etc. - drawCustomFrameIcons // Custom indicator icons for each frame + NavigationBar // Custom indicator icons for each frame }; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); @@ -3923,7 +3927,7 @@ void Screen::setFrames(FrameFocus focus) ui->disableAllIndicators(); // Add overlays: frame icons and alert banner) - static OverlayCallback overlays[] = {drawCustomFrameIcons, drawAlertBannerOverlay}; + static OverlayCallback overlays[] = {NavigationBar, drawAlertBannerOverlay}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list From 295ed2a8cfecc10330f65f21035f1376470eee3e Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 24 May 2025 04:09:35 -0400 Subject: [PATCH 150/265] Increased cycling time for NodeInfo screens to 3 secs --- src/graphics/Screen.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 98b13a57a..1baa45327 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2030,7 +2030,7 @@ static int scrollIndex = 0; unsigned long getModeCycleIntervalMs() { // return (currentMode == MODE_DISTANCE) ? 3000 : 2000; - return 2000; + return 3000; } // h! Calculates bearing between two lat/lon points (used for compass) @@ -3294,7 +3294,7 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) } static int8_t lastFrameIndex = -1; static uint32_t lastFrameChangeTime = 0; -constexpr uint32_t ICON_DISPLAY_DURATION_MS = 1000; +constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000; void NavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) { @@ -3314,11 +3314,11 @@ void NavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) const size_t totalIcons = screen->indicatorIcons.size(); if (totalIcons == 0) return; - const int iconsPerPage = (SCREEN_WIDTH + spacing) / (iconSize + spacing); - const int totalPages = (totalIcons + iconsPerPage - 1) / iconsPerPage; - const int currentPage = currentFrame / iconsPerPage; - const int pageStart = currentPage * iconsPerPage; - const int pageEnd = min(pageStart + iconsPerPage, totalIcons); + const size_t iconsPerPage = (SCREEN_WIDTH + spacing) / (iconSize + spacing); + const size_t totalPages = (totalIcons + iconsPerPage - 1) / iconsPerPage; + const size_t currentPage = currentFrame / iconsPerPage; + const size_t pageStart = currentPage * iconsPerPage; + const size_t pageEnd = min(pageStart + iconsPerPage, totalIcons); const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing; const int xStart = (SCREEN_WIDTH - totalWidth) / 2; From 13421b32b514fa8c6b921472745095a32b79c6b5 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 24 May 2025 04:12:15 -0400 Subject: [PATCH 151/265] Inverted on default header --- src/graphics/SharedUIDisplay.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index f655882c0..c99f3ad20 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -50,7 +50,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) const int xOffset = 4; const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); + const bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); const bool isBold = config.display.heading_bold; const int screenW = display->getWidth(); From b5f74cead1a240f02facd4f33adc08b32e5e1917 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 24 May 2025 04:37:10 -0400 Subject: [PATCH 152/265] Fixed invertion to tittles --- src/graphics/Screen.cpp | 27 ++++++++++++------- .../Telemetry/EnvironmentTelemetry.cpp | 2 +- src/modules/Telemetry/PowerTelemetry.cpp | 2 +- src/modules/WaypointModule.cpp | 4 +-- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 1baa45327..bfba0cc72 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1306,13 +1306,20 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 const int textWidth = SCREEN_WIDTH; const int cornerRadius = 2; - bool isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); + bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); bool isBold = config.display.heading_bold; // === Header Construction === meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); char headerStr[80]; - const char *sender = (node && node->has_user) ? node->user.short_name : "???"; + const char *sender = "???"; + if (node && node->has_user) { + if (SCREEN_WIDTH >= 200 && strlen(node->user.long_name) > 0) { + sender = node->user.long_name; + } else { + sender = node->user.short_name; + } + } uint32_t seconds = sinceReceived(&mp), minutes = seconds / 60, hours = minutes / 60, days = hours / 24; uint8_t timestampHours, timestampMinutes; int32_t daysAgo; @@ -1902,7 +1909,7 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ const int centerX = x + SCREEN_WIDTH / 2; const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) display->setColor(BLACK); display->setTextAlignment(TEXT_ALIGN_CENTER); @@ -2176,7 +2183,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_CENTER); - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) display->setColor(BLACK); display->drawString(centerX, textY, title); @@ -2728,7 +2735,7 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int const char *titleStr = (SCREEN_WIDTH > 128) ? "LoRa Info" : "LoRa"; const int centerX = x + SCREEN_WIDTH / 2; - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { display->setColor(BLACK); } @@ -2854,7 +2861,7 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat const char *titleStr = "GPS"; const int centerX = x + SCREEN_WIDTH / 2; - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { display->setColor(BLACK); } @@ -2991,7 +2998,7 @@ static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, in const char *titleStr = (SCREEN_WIDTH > 128) ? "Memory" : "Mem"; const int centerX = x + SCREEN_WIDTH / 2; - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { display->setColor(BLACK); } @@ -4167,7 +4174,7 @@ void DebugInfo::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // The coordinates define the left starting point of the text display->setTextAlignment(TEXT_ALIGN_LEFT); - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); display->setColor(BLACK); } @@ -4281,7 +4288,7 @@ void DebugInfo::drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, i // The coordinates define the left starting point of the text display->setTextAlignment(TEXT_ALIGN_LEFT); - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); display->setColor(BLACK); } @@ -4361,7 +4368,7 @@ void DebugInfo::drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *stat // The coordinates define the left starting point of the text display->setTextAlignment(TEXT_ALIGN_LEFT); - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); display->setColor(BLACK); } diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 8c33d171e..49969ffc5 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -335,7 +335,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt const int centerX = x + SCREEN_WIDTH / 2; // Use black text on white background if in inverted mode - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) display->setColor(BLACK); display->setTextAlignment(TEXT_ALIGN_CENTER); diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index 5190a5947..b99e5f91f 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -120,7 +120,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s const char *titleStr = (SCREEN_WIDTH > 128) ? "Power Telem." : "Power"; const int centerX = x + SCREEN_WIDTH / 2; - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { display->setColor(BLACK); } diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp index 479a973c2..3692ac2f8 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -85,7 +85,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, // Handle inverted display // Unsure of expected behavior: for now, copy drawNodeInfo - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); // Decode the waypoint @@ -180,7 +180,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, // 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) { + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { display->setColor(BLACK); } From bf21481bef142d73dc7dbbac6630c7d71a8022a1 Mon Sep 17 00:00:00 2001 From: mverch67 Date: Sat, 24 May 2025 18:01:47 +0200 Subject: [PATCH 153/265] fix compilation errors (for targets which have no telemetry) --- src/modules/Modules.cpp | 1 + src/modules/Telemetry/AirQualityTelemetry.cpp | 2 +- src/modules/Telemetry/EnvironmentTelemetry.cpp | 3 ++- src/power.h | 2 +- variants/seeed-sensecap-indicator/platformio.ini | 1 - 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 0ecdadf4c..bbb20045e 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -62,6 +62,7 @@ #include "modules/Telemetry/AirQualityTelemetry.h" #include "modules/Telemetry/EnvironmentTelemetry.h" #include "modules/Telemetry/HealthTelemetry.h" +#include "modules/Telemetry/Sensor/TelemetrySensor.h" #endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_POWER_TELEMETRY #include "modules/Telemetry/PowerTelemetry.h" diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index fafb28699..2472b95b1 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h") +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && __has_include("Adafruit_PM25AQI.h") #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "AirQualityTelemetry.h" diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 56f9d7433..57fd0e149 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -1,6 +1,6 @@ #include "configuration.h" -#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR +#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR #include "../mesh/generated/meshtastic/telemetry.pb.h" #include "Default.h" @@ -19,6 +19,7 @@ #include #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL + // Sensors #include "Sensor/CGRadSensSensor.h" diff --git a/src/power.h b/src/power.h index d7fa7f8a9..33a356d92 100644 --- a/src/power.h +++ b/src/power.h @@ -78,8 +78,8 @@ extern NullSensor ina3221Sensor; #endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && !defined(ARCH_STM32WL) -#include "modules/Telemetry/Sensor/MAX17048Sensor.h" #if __has_include() +#include "modules/Telemetry/Sensor/MAX17048Sensor.h" extern MAX17048Sensor max17048Sensor; #else extern NullSensor max17048Sensor; diff --git a/variants/seeed-sensecap-indicator/platformio.ini b/variants/seeed-sensecap-indicator/platformio.ini index f2495c340..2187ebd8a 100644 --- a/variants/seeed-sensecap-indicator/platformio.ini +++ b/variants/seeed-sensecap-indicator/platformio.ini @@ -37,7 +37,6 @@ upload_speed = 460800 build_flags = ${env:seeed-sensecap-indicator.build_flags} -D INPUTDRIVER_BUTTON_TYPE=38 - -D HAS_TELEMETRY=0 -D CONFIG_DISABLE_HAL_LOCKS=1 -D HAS_SCREEN=1 -D HAS_TFT=1 From 61de338aa2164831d83baf0b772e452b0ca50e9d Mon Sep 17 00:00:00 2001 From: mverch67 Date: Sat, 24 May 2025 18:02:13 +0200 Subject: [PATCH 154/265] unphone --- variants/unphone/platformio.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/variants/unphone/platformio.ini b/variants/unphone/platformio.ini index 2f1546d6f..ef0f62b60 100644 --- a/variants/unphone/platformio.ini +++ b/variants/unphone/platformio.ini @@ -43,9 +43,9 @@ build_flags = -D DISPLAY_SET_RESOLUTION -D RAM_SIZE=6144 -D LV_CACHE_DEF_SIZE=2097152 - -D LV_LVGL_H_INCLUDE_SIMPLE - -D LV_CONF_INCLUDE_SIMPLE - -D LV_COMP_CONF_INCLUDE_SIMPLE + -D LV_LVGL_H_INCLUDE_SIMPLE + -D LV_CONF_INCLUDE_SIMPLE + -D LV_COMP_CONF_INCLUDE_SIMPLE -D LV_BUILD_TEST=0 -D LV_USE_SYSMON=0 -D LV_USE_PROFILER=0 From 6a22bb51876c149752952926ef087c3cf037a39f Mon Sep 17 00:00:00 2001 From: mverch67 Date: Sat, 24 May 2025 18:02:28 +0200 Subject: [PATCH 155/265] PICOmputer --- variants/picomputer-s3/platformio.ini | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/variants/picomputer-s3/platformio.ini b/variants/picomputer-s3/platformio.ini index df2d0dfdc..b861b5496 100644 --- a/variants/picomputer-s3/platformio.ini +++ b/variants/picomputer-s3/platformio.ini @@ -26,21 +26,15 @@ extends = env:picomputer-s3 build_flags = ${env:picomputer-s3.build_flags} - -D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1 - -D MESHTASTIC_EXCLUDE_INPUTBROKER=1 - -D MESHTASTIC_EXCLUDE_BLUETOOTH=1 - -D MESHTASTIC_EXCLUDE_WEBSERVER=1 - -D MESHTASTIC_EXCLUDE_SERIAL=1 - -D MESHTASTIC_EXCLUDE_SOCKETAPI=1 -D INPUTDRIVER_MATRIX_TYPE=1 -D USE_PIN_BUZZER=PIN_BUZZER -D USE_SX127x - -D HAS_SCREEN=0 + -D HAS_SCREEN=1 -D HAS_TFT=1 - -D RAM_SIZE=1024 - -D LV_LVGL_H_INCLUDE_SIMPLE - -D LV_CONF_INCLUDE_SIMPLE - -D LV_COMP_CONF_INCLUDE_SIMPLE + -D RAM_SIZE=1560 + -D LV_LVGL_H_INCLUDE_SIMPLE + -D LV_CONF_INCLUDE_SIMPLE + -D LV_COMP_CONF_INCLUDE_SIMPLE -D LV_USE_SYSMON=0 -D LV_USE_PROFILER=0 -D LV_USE_PERF_MONITOR=0 @@ -51,7 +45,7 @@ build_flags = -D LGFX_DRIVER=LGFX_PICOMPUTER_S3 -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_PICOMPUTER_S3.h\" -D VIEW_320x240 -; -D USE_DOUBLE_BUFFER +; -D USE_DOUBLE_BUFFER -D USE_PACKET_API lib_deps = From bed25406c177b60d993eb49991d35b2b89bc73a0 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 24 May 2025 14:16:28 -0400 Subject: [PATCH 156/265] Facelist to cannedmessage screen --- src/modules/CannedMessageModule.cpp | 76 ++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index fcfbf890a..f867ca7e2 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -1516,41 +1516,69 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st return; } - // === Canned Messages List === +// === Canned Messages List === if (this->messagesCount > 0) { display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringf(0 + x, 0 + y, buffer, "To: %s", getNodeName(this->dest)); - int lines = (display->getHeight() / FONT_HEIGHT_SMALL) - 1; - if (lines == 3) { - display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, getCurrentMessage()); - display->setColor(WHITE); + const int rowSpacing = FONT_HEIGHT_SMALL - 4; + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + const int boxYOffset = (highlightHeight - FONT_HEIGHT_SMALL) / 2; - if (this->messagesCount > 1) { - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL, getPrevMessage()); - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 3, getNextMessage()); + // Draw header (To: ...) + switch (this->destSelect) { + case CANNED_MESSAGE_DESTINATION_TYPE_NODE: + display->drawStringf(x + 0, y + 0, buffer, "To: >%s<@%s", getNodeName(this->dest), channels.getName(this->channel)); + break; + case CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL: + display->drawStringf(x + 0, y + 0, buffer, "To: %s@>%s<", getNodeName(this->dest), channels.getName(this->channel)); + break; + default: + if (display->getWidth() > 128) { + display->drawStringf(x + 0, y + 0, buffer, "To: %s@%s", getNodeName(this->dest), channels.getName(this->channel)); + } else { + display->drawStringf(x + 0, y + 0, buffer, "To: %.5s@%.5s", getNodeName(this->dest), channels.getName(this->channel)); } - } else { - int topMsg = (messagesCount > lines && currentMessageIndex >= lines - 1) ? currentMessageIndex - lines + 2 : 0; - for (int i = 0; i < std::min(messagesCount, lines); i++) { - if (i == currentMessageIndex - topMsg) { + break; + } + + // Shift message list upward by 3 pixels to reduce spacing between header and first message + const int listYOffset = y + FONT_HEIGHT_SMALL - 3; + const int visibleRows = (display->getHeight() - listYOffset) / rowSpacing; + + int topMsg = (messagesCount > visibleRows && currentMessageIndex >= visibleRows - 1) + ? currentMessageIndex - visibleRows + 2 + : 0; + + for (int i = 0; i < std::min(messagesCount, visibleRows); i++) { + int lineY = listYOffset + rowSpacing * i; + const char* msg = getMessageByIndex(topMsg + i); + + if ((topMsg + i) == currentMessageIndex) { #ifdef USE_EINK - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), ">"); - display->drawString(12 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), getCurrentMessage()); + display->drawString(x + 0, lineY, ">"); + display->drawString(x + 12, lineY, msg); #else - display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), getCurrentMessage()); - display->setColor(WHITE); + int scrollPadding = 8; + display->fillRect(x + 0, lineY + 2, display->getWidth() - scrollPadding, FONT_HEIGHT_SMALL - 5); + display->setColor(BLACK); + display->drawString(x + 2, lineY, msg); + display->setColor(WHITE); #endif - } else { - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), getMessageByIndex(topMsg + i)); - } + } else { + display->drawString(x + 0, lineY, msg); } } + + // Scrollbar + if (messagesCount > visibleRows) { + int scrollHeight = display->getHeight() - listYOffset; + int scrollTrackX = display->getWidth() - 6; + display->drawRect(scrollTrackX, listYOffset, 4, scrollHeight); + int barHeight = (scrollHeight * visibleRows) / messagesCount; + int scrollPos = listYOffset + (scrollHeight * topMsg) / messagesCount; + display->fillRect(scrollTrackX, scrollPos, 4, barHeight); + } } } #endif From b2663d4b1970f3dc295ee51943b17d4fc8f6e6d9 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 24 May 2025 20:38:47 -0400 Subject: [PATCH 157/265] Fixed Ack and Nack messages --- src/modules/CannedMessageModule.cpp | 150 ++++++++++++++++++---------- src/modules/CannedMessageModule.h | 3 +- 2 files changed, 101 insertions(+), 52 deletions(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index f867ca7e2..9c5628f14 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -703,35 +703,43 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies) { + // === Prepare packet === meshtastic_MeshPacket *p = allocDataPacket(); p->to = dest; p->channel = channel; p->want_ack = true; + + // Save destination for ACK/NACK UI fallback + this->lastSentNode = dest; + this->incoming = dest; + + // Copy message payload p->decoded.payload.size = strlen(message); memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); - if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { - p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character - p->decoded.payload.bytes[p->decoded.payload.size + 1] = '\0'; // Bell character - p->decoded.payload.size++; + + // Optionally add bell character + if (moduleConfig.canned_message.send_bell && + p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) + { + p->decoded.payload.bytes[p->decoded.payload.size++] = 7; // Bell + p->decoded.payload.bytes[p->decoded.payload.size] = '\0'; // Null-terminate } - // Only receive routing messages when expecting ACK for a canned message - // Prevents the canned message module from regenerating the screen's frameset at unexpected times, - // or raising a UIFrameEvent before another module has the chance + // Mark as waiting for ACK to trigger ACK/NACK screen this->waitingForAck = true; - LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); + // Log outgoing message + LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", + p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); - service->sendToMesh( - p, RX_SRC_LOCAL, - true); // send to mesh, cc to phone. Even if there's no phone connected, this stores the message to match ACKs + // Send to mesh and phone (even if no phone connected, to track ACKs) + service->sendToMesh(p, RX_SRC_LOCAL, true); - // === Simulate local message to dismiss the message frame after sending === - // This mimics what happens when replying from the phone to clear unread state and UI + // === Simulate local message to clear unread UI === if (screen) { meshtastic_MeshPacket simulatedPacket = {}; - simulatedPacket.from = 0; // Outgoing message (from local device) - screen->handleTextMessage(&simulatedPacket); // Calls logic to clear unread and dismiss frame + simulatedPacket.from = 0; // Local device + screen->handleTextMessage(&simulatedPacket); } } @@ -1028,16 +1036,16 @@ const char *CannedMessageModule::getMessageByIndex(int index) const char *CannedMessageModule::getNodeName(NodeNum node) { - if (node == NODENUM_BROADCAST) { - return "Broadcast"; - } else { - meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); - if (info != NULL) { - return info->user.long_name; - } else { - return "Unknown"; - } + if (node == NODENUM_BROADCAST) return "Broadcast"; + + meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); + if (info && info->has_user && strlen(info->user.long_name) > 0) { + return info->user.long_name; } + + static char fallback[12]; + snprintf(fallback, sizeof(fallback), "0x%08x", node); + return fallback; } bool CannedMessageModule::shouldDraw() @@ -1422,28 +1430,48 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st requestFocus(); EINK_ADD_FRAMEFLAG(display, COSMETIC); display->setTextAlignment(TEXT_ALIGN_CENTER); -#ifdef USE_EINK - display->setFont(FONT_SMALL); -#else - display->setFont(FONT_MEDIUM); -#endif - String displayString = this->ack ? "Delivered to\n%s" : "Delivery failed\nto %s"; - display->drawStringf(display->getWidth() / 2 + x, 0 + y + 12, buffer, displayString, getNodeName(this->incoming)); - display->setFont(FONT_SMALL); - // SNR/RSSI - if (display->getHeight() > 100) { - int16_t snrY = 100; - int16_t rssiY = 130; - if (display->getHeight() < rssiY + FONT_HEIGHT_SMALL) { - snrY = display->getHeight() - ((1.5) * FONT_HEIGHT_SMALL); - rssiY = display->getHeight() - ((2.5) * FONT_HEIGHT_SMALL); - } - if (this->ack) { - display->drawStringf(display->getWidth() / 2 + x, snrY + y, buffer, "Last Rx SNR: %f", this->lastRxSnr); - display->drawStringf(display->getWidth() / 2 + x, rssiY + y, buffer, "Last Rx RSSI: %d", this->lastRxRssi); + #ifdef USE_EINK + display->setFont(FONT_SMALL); + int yOffset = y + 10; + #else + display->setFont(FONT_MEDIUM); + int yOffset = y + 10; + #endif + + // --- Delivery Status Message --- + if (this->ack) { + if (this->lastSentNode == NODENUM_BROADCAST) { + snprintf(buffer, sizeof(buffer), "Broadcast Sent to\n%s", channels.getName(this->channel)); + } else if (this->lastAckHopLimit > this->lastAckHopStart) { + snprintf(buffer, sizeof(buffer), "Delivered (%d hops)\nto %s", + this->lastAckHopLimit - this->lastAckHopStart, + getNodeName(this->incoming)); + } else { + snprintf(buffer, sizeof(buffer), "Delivered\nto %s", getNodeName(this->incoming)); } + } else { + snprintf(buffer, sizeof(buffer), "Delivery failed\nto %s", getNodeName(this->incoming)); } + + // Draw delivery message and compute y-offset after text height + int lineCount = 1; + for (const char *ptr = buffer; *ptr; ptr++) { + if (*ptr == '\n') lineCount++; + } + + display->drawString(display->getWidth() / 2 + x, yOffset, buffer); + yOffset += lineCount * FONT_HEIGHT_MEDIUM; // only 1 line gap, no extra padding + + #ifndef USE_EINK + // --- SNR + RSSI Compact Line --- + if (this->ack) { + display->setFont(FONT_SMALL); + snprintf(buffer, sizeof(buffer), "SNR: %.1f dB RSSI: %d", this->lastRxSnr, this->lastRxRssi); + display->drawString(display->getWidth() / 2 + x, yOffset, buffer); + } + #endif + return; } @@ -1586,20 +1614,40 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &mp) { if (mp.decoded.portnum == meshtastic_PortNum_ROUTING_APP && waitingForAck) { - // look for a request_id if (mp.decoded.request_id != 0) { + // Trigger screen refresh for ACK/NACK feedback UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen - requestFocus(); // Tell Screen::setFrames that our module's frame should be shown, even if not "first" in the frameset + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + requestFocus(); this->runState = CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED; - this->incoming = service->getNodenumFromRequestId(mp.decoded.request_id); + + // Decode the routing response meshtastic_Routing decoded = meshtastic_Routing_init_default; pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_Routing_fields, &decoded); - this->ack = decoded.error_reason == meshtastic_Routing_Error_NONE; - waitingForAck = false; // No longer want routing packets + + // Track hop metadata + this->lastAckWasRelayed = (mp.hop_limit != mp.hop_start); + this->lastAckHopStart = mp.hop_start; + this->lastAckHopLimit = mp.hop_limit; + + // Determine ACK status + bool isAck = (decoded.error_reason == meshtastic_Routing_Error_NONE); + bool isFromDest = (mp.from == this->lastSentNode); + bool isBroadcast = (this->lastSentNode == NODENUM_BROADCAST); + + // Identify the responding node + if (isBroadcast && mp.from != nodeDB->getNodeNum()) { + this->incoming = mp.from; // Relayed by another node + } else { + this->incoming = this->lastSentNode; // Direct reply + } + + // Final ACK confirmation logic + this->ack = isAck && (isBroadcast || isFromDest); + + waitingForAck = false; this->notifyObservers(&e); - // run the next time 2 seconds later - setIntervalFromNow(2000); + setIntervalFromNow(3000); // Time to show ACK/NACK screen } } diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index a0d4da1ec..dcdeba971 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -63,7 +63,8 @@ class CannedMessageModule : public SinglePortModule, public Observable activeChannelIndices; bool shouldRedraw = false; unsigned long lastUpdateMillis = 0; - + uint8_t lastAckHopStart = 0; + uint8_t lastAckHopLimit = 0; public: CannedMessageModule(); const char *getCurrentMessage(); From e974a58d180c29d645293fc9e06af40ed75ef931 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sat, 24 May 2025 20:46:33 -0400 Subject: [PATCH 158/265] Cannedmessagemodule.h cleanup --- src/modules/CannedMessageModule.h | 183 +++++++++++++++--------------- 1 file changed, 93 insertions(+), 90 deletions(-) diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index dcdeba971..9a3f610c0 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -3,6 +3,10 @@ #include "ProtobufModule.h" #include "input/InputBroker.h" +// ============================ +// Enums & Defines +// ============================ + enum cannedMessageModuleRunState { CANNED_MESSAGE_RUN_STATE_DISABLED, CANNED_MESSAGE_RUN_STATE_INACTIVE, @@ -25,6 +29,17 @@ enum cannedMessageDestinationType { enum CannedMessageModuleIconType { shift, backspace, space, enter }; +#define CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT 50 +#define CANNED_MESSAGE_MODULE_MESSAGES_SIZE 800 + +#ifndef CANNED_MESSAGE_MODULE_ENABLE +#define CANNED_MESSAGE_MODULE_ENABLE 0 +#endif + +// ============================ +// Data Structures +// ============================ + struct Letter { String character; float width; @@ -34,90 +49,57 @@ struct Letter { int rectHeight; }; -#define CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT 50 -/** - * Sum of CannedMessageModuleConfig part sizes. - */ -#define CANNED_MESSAGE_MODULE_MESSAGES_SIZE 800 - -#ifndef CANNED_MESSAGE_MODULE_ENABLE -#define CANNED_MESSAGE_MODULE_ENABLE 0 -#endif - struct NodeEntry { meshtastic_NodeInfoLite *node; uint32_t lastHeard; }; -class CannedMessageModule : public SinglePortModule, public Observable, private concurrency::OSThread -{ - CallbackObserver inputObserver = - CallbackObserver(this, &CannedMessageModule::handleInputEvent); - private: - int displayHeight = 64; // Default to a common value, update dynamically - int destIndex = 0; // Tracks currently selected node/channel in selection mode - int scrollIndex = 0; // Tracks scrolling position in node selection grid - int visibleRows = 0; - bool needsUpdate = true; - String searchQuery; - std::vector activeChannelIndices; - bool shouldRedraw = false; - unsigned long lastUpdateMillis = 0; - uint8_t lastAckHopStart = 0; - uint8_t lastAckHopLimit = 0; - public: +// ============================ +// Main Class +// ============================ + +class CannedMessageModule : public SinglePortModule, + public Observable, + private concurrency::OSThread { +public: CannedMessageModule(); + + // === Message navigation === const char *getCurrentMessage(); const char *getPrevMessage(); const char *getNextMessage(); const char *getMessageByIndex(int index); const char *getNodeName(NodeNum node); + + // === State/UI === bool shouldDraw(); bool hasMessages(); - // void eventUp(); - // void eventDown(); - // void eventSelect(); + void showTemporaryMessage(const String &message); + void resetSearch(); + void updateFilteredNodes(); + String drawWithCursor(String text, int cursor); + // === Admin Handlers === void handleGetCannedMessageModuleMessages(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response); void handleSetCannedMessageModuleMessages(const char *from_msg); - void showTemporaryMessage(const String &message); - void resetSearch(); - void updateFilteredNodes(); - std::vector filteredNodes; - String nodeSelectionInput; - String drawWithCursor(String text, int cursor); - #ifdef RAK14014 cannedMessageModuleRunState getRunState() const { return runState; } #endif - /* - -Override the wantPacket method. We need the Routing Messages to look for ACKs. - */ - virtual bool wantPacket(const meshtastic_MeshPacket *p) override - { - if (p->rx_rssi != 0) { - this->lastRxRssi = p->rx_rssi; - } - - if (p->rx_snr > 0) { - this->lastRxSnr = p->rx_snr; - } - - switch (p->decoded.portnum) { - case meshtastic_PortNum_ROUTING_APP: - return waitingForAck; - default: - return false; - } + // === Packet Interest Filter === + virtual bool wantPacket(const meshtastic_MeshPacket *p) override { + if (p->rx_rssi != 0) lastRxRssi = p->rx_rssi; + if (p->rx_snr > 0) lastRxSnr = p->rx_snr; + return (p->decoded.portnum == meshtastic_PortNum_ROUTING_APP) ? waitingForAck : false; } - protected: +protected: + // === Thread Entry Point === virtual int32_t runOnce() override; + // === Transmission === void sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies); - int splitConfiguredMessages(); int getNextIndex(); int getPrevIndex(); @@ -125,60 +107,81 @@ class CannedMessageModule : public SinglePortModule, public ObservableshouldDraw(); } + virtual bool wantUIFrame() override { return shouldDraw(); } virtual Observable *getUIFrameObservable() override { return this; } virtual bool interceptingKeyboardInput() override; #if !HAS_TFT virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) override; #endif - virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, - meshtastic_AdminMessage *request, - meshtastic_AdminMessage *response) override; + virtual AdminMessageHandleResult handleAdminMessageForModule( + const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; - /** 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 - */ virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override; void loadProtoForModule(); bool saveProtoForModule(); - void installDefaultCannedMessageModuleConfig(); - int currentMessageIndex = -1; - cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - char payload = 0x00; - unsigned int cursor = 0; - String freetext = ""; // Text Buffer for Freetext Editor - NodeNum dest = NODENUM_BROADCAST; - ChannelIndex channel = 0; - cannedMessageDestinationType destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; - uint8_t numChannels = 0; - ChannelIndex indexChannels[MAX_NUM_CHANNELS] = {0}; - NodeNum incoming = NODENUM_BROADCAST; - NodeNum lastSentNode = 0; // Tracks who the message was sent to (for ACK screen) - bool ack = false; // True means ACK, false means NAK (error_reason != NONE) - bool waitingForAck = false; // Are currently interested in routing packets? - bool lastAckWasRelayed = false; - float lastRxSnr = 0; - int32_t lastRxRssi = 0; +private: + // === Input Observers === + CallbackObserver inputObserver = + CallbackObserver(this, &CannedMessageModule::handleInputEvent); + // === Display and UI === + int displayHeight = 64; + int destIndex = 0; + int scrollIndex = 0; + int visibleRows = 0; + bool needsUpdate = true; + bool shouldRedraw = false; + unsigned long lastUpdateMillis = 0; + String searchQuery; + String nodeSelectionInput; + String freetext; + String temporaryMessage; + + // === Message Storage === char messageStore[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1]; char *messages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT]; int messagesCount = 0; + int currentMessageIndex = -1; + + // === Routing & Acknowledgment === + NodeNum dest = NODENUM_BROADCAST; // Destination node for outgoing messages (default: broadcast) + NodeNum incoming = NODENUM_BROADCAST; // Source node from which last ACK/NACK was received + 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 + uint8_t numChannels = 0; // Total number of channels available for selection + ChannelIndex indexChannels[MAX_NUM_CHANNELS] = {0}; // Cached channel indices available for this node + + 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) + + // === State Tracking === + cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + cannedMessageDestinationType destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; + char highlight = 0x00; + char payload = 0x00; + unsigned int cursor = 0; unsigned long lastTouchMillis = 0; - String temporaryMessage; + std::vector activeChannelIndices; + std::vector filteredNodes; + #if defined(USE_VIRTUAL_KEYBOARD) Letter keyboard[2][4][10] = {{{{"Q", 20, 0, 0, 0, 0}, @@ -252,4 +255,4 @@ class CannedMessageModule : public SinglePortModule, public Observable Date: Sun, 25 May 2025 17:11:12 -0500 Subject: [PATCH 159/265] Trigger a reboot when moving between Display Types --- src/modules/AdminModule.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 5cf5d778d..0af5225b0 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -622,8 +622,12 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c) config.has_display = true; if (config.display.screen_on_secs == c.payload_variant.display.screen_on_secs && config.display.flip_screen == c.payload_variant.display.flip_screen && - config.display.oled == c.payload_variant.display.oled) { + config.display.oled == c.payload_variant.display.oled && + config.display.displaymode == c.payload_variant.display.displaymode) { requiresReboot = false; + } else if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR && + c.payload_variant.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { + config.bluetooth.enabled = false; } #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR if (config.display.wake_on_tap_or_motion == false && c.payload_variant.display.wake_on_tap_or_motion == true && From 7c4714afbba3e6c9a4b284ac3f9b0584c1d2f97f Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 26 May 2025 01:35:34 -0400 Subject: [PATCH 160/265] Unified navigation inputs for cannedmessages --- .vscode/settings.json | 3 +- src/ButtonThread.cpp | 17 +- src/ButtonThread.h | 1 + src/graphics/Screen.cpp | 7 - src/input/InputBroker.h | 3 +- src/modules/CannedMessageModule.cpp | 957 +++++++++++++++------------- src/modules/CannedMessageModule.h | 14 +- 7 files changed, 552 insertions(+), 450 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index d37702476..878d4976e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -62,6 +62,7 @@ "stdexcept": "cpp", "streambuf": "cpp", "cinttypes": "cpp", - "typeinfo": "cpp" + "typeinfo": "cpp", + "*.xbm": "cpp" } } diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp index 300b40242..1dca83033 100644 --- a/src/ButtonThread.cpp +++ b/src/ButtonThread.cpp @@ -10,8 +10,9 @@ #include "buzz.h" #include "main.h" #include "modules/ExternalNotificationModule.h" +#include "modules/CannedMessageModule.h" #include "power.h" -#include "sleep.h" +#include "sleep.h" #ifdef ARCH_PORTDUINO #include "platform/portduino/PortduinoGlue.h" #endif @@ -26,6 +27,7 @@ using namespace concurrency; ButtonThread *buttonThread; // Declared extern in header +extern CannedMessageModule* cannedMessageModule; volatile ButtonThread::ButtonEventType ButtonThread::btnEvent = ButtonThread::BUTTON_EVENT_NONE; #if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN) @@ -118,6 +120,17 @@ ButtonThread::ButtonThread() : OSThread("Button") void ButtonThread::switchPage() { + // Prevent screen switch if CannedMessageModule is focused and intercepting input +#if HAS_SCREEN + extern CannedMessageModule* cannedMessageModule; + + if (cannedMessageModule && cannedMessageModule->isInterceptingAndFocused()) { + LOG_DEBUG("User button ignored during canned message input"); + return; // Skip screen change + } +#endif + + // Default behavior if not blocked #ifdef BUTTON_PIN #if !defined(USERPREFS_BUTTON_PIN) if (((config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN) != @@ -135,8 +148,8 @@ void ButtonThread::switchPage() powerFSM.trigger(EVENT_PRESS); } #endif - #endif + #if defined(ARCH_PORTDUINO) if ((settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) && (settingsMap[user] != moduleConfig.canned_message.inputbroker_pin_press) || diff --git a/src/ButtonThread.h b/src/ButtonThread.h index 3af700dd0..22ead4156 100644 --- a/src/ButtonThread.h +++ b/src/ButtonThread.h @@ -40,6 +40,7 @@ class ButtonThread : public concurrency::OSThread bool isBuzzing() { return buzzer_flag; } void setScreenFlag(bool flag) { screen_flag = flag; } bool getScreenFlag() { return screen_flag; } + bool isInterceptingAndFocused(); // Disconnect and reconnect interrupts for light sleep #ifdef ARCH_ESP32 diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index bfba0cc72..71047ace3 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1754,7 +1754,6 @@ float Screen::estimatedHeading(double lat, double lon) /// We will skip one node - the one for us, so we just blindly loop over all /// nodes -static size_t nodeIndex; static int8_t prevFrame = -1; // Draw the arrow pointing to a node's location @@ -2640,8 +2639,6 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i config.display.heading_bold = false; #if HAS_GPS - auto number_of_satellites = gpsStatus->getNumSatellites(); - if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { String displayLine = ""; if (config.position.fixed_position) { @@ -2878,7 +2875,6 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat bool origBold = config.display.heading_bold; config.display.heading_bold = false; - auto number_of_satellites = gpsStatus->getNumSatellites(); String Satelite_String = "Sat:"; display->drawString(0, compactFirstLine, Satelite_String); String displayLine = ""; @@ -3322,7 +3318,6 @@ void NavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) if (totalIcons == 0) return; const size_t iconsPerPage = (SCREEN_WIDTH + spacing) / (iconSize + spacing); - const size_t totalPages = (totalIcons + iconsPerPage - 1) / iconsPerPage; const size_t currentPage = currentFrame / iconsPerPage; const size_t pageStart = currentPage * iconsPerPage; const size_t pageEnd = min(pageStart + iconsPerPage, totalIcons); @@ -3654,8 +3649,6 @@ int32_t Screen::runOnce() // Switch to a low framerate (to save CPU) when we are not in transition // but we should only call setTargetFPS when framestate changes, because // otherwise that breaks animations. - // === Auto-hide indicator icons unless in transition === - OLEDDisplayUiState *state = ui->getUiState(); if (targetFramerate != IDLE_FRAMERATE && ui->getUiState()->frameState == FIXED) { // oldFrameState = ui->getUiState()->frameState; diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index b993d8c1b..72084dad3 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -19,8 +19,7 @@ #define INPUT_BROKER_MSG_FN_SYMBOL_ON 0xf1 #define INPUT_BROKER_MSG_FN_SYMBOL_OFF 0xf2 #define INPUT_BROKER_MSG_BLUETOOTH_TOGGLE 0xAA -#define INPUT_BROKER_MSG_SELECT 0x0D // Enter key / rotary encoder click - +#define INPUT_BROKER_MSG_TAB 0x09 typedef struct _InputEvent { const char *source; diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 9c5628f14..78ca741f5 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -79,7 +79,7 @@ bool hasKeyForNode(const meshtastic_NodeInfoLite* node) { * * @return int Returns the number of messages found. */ -// FIXME: This is just one set of messages now + int CannedMessageModule::splitConfiguredMessages() { int messageIndex = 0; @@ -88,43 +88,43 @@ int CannedMessageModule::splitConfiguredMessages() String canned_messages = cannedMessageModuleConfig.messages; #if defined(USE_VIRTUAL_KEYBOARD) + // Add a "Free Text" entry at the top if using a virtual keyboard String separator = canned_messages.length() ? "|" : ""; - canned_messages = "[---- Free Text ----]" + separator + canned_messages; #endif - // collect all the message parts + // Copy all message parts into the buffer strncpy(this->messageStore, canned_messages.c_str(), sizeof(this->messageStore)); - // The first message points to the beginning of the store. + // First message points to start of buffer this->messages[messageIndex++] = this->messageStore; int upTo = strlen(this->messageStore) - 1; + // Walk buffer, splitting on '|' while (i < upTo) { if (this->messageStore[i] == '|') { - // Message ending found, replace it with string-end character. - this->messageStore[i] = '\0'; + this->messageStore[i] = '\0'; // End previous message - // hit our max messages, bail + // Stop if we've hit max message slots if (messageIndex >= CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT) { this->messagesCount = messageIndex; return this->messagesCount; } - // Next message starts after pipe (|) just found. + // Point to the next message start this->messages[messageIndex++] = (this->messageStore + i + 1); } i += 1; } - // Add "[Select Destination]" as the final message - if (strlen(this->messages[messageIndex - 1]) > 0) { - this->messages[messageIndex - 1] = (char*)"[Select Destination]"; - this->messagesCount = messageIndex; - } else { - this->messages[messageIndex - 1] = (char*)"[Select Destination]"; - this->messagesCount = messageIndex; - } + // Always add "[Select Destination]" next-to-last + this->messages[messageIndex++] = (char*)"[Select Destination]"; + + // === Add [Exit] as the final entry in the list === + this->messages[messageIndex++] = (char*)"[Exit]"; + + // Record how many messages there are + this->messagesCount = messageIndex; return this->messagesCount; } @@ -214,359 +214,536 @@ void CannedMessageModule::updateFilteredNodes() { } } -int CannedMessageModule::handleInputEvent(const InputEvent *event) -{ - if ((strlen(moduleConfig.canned_message.allow_input_source) > 0) && - (strcasecmp(moduleConfig.canned_message.allow_input_source, event->source) != 0) && - (strcasecmp(moduleConfig.canned_message.allow_input_source, "_any") != 0)) { - // Event source is not accepted. - // Event only accepted if source matches the configured one, or - // the configured one is "_any" (or if there is no configured - // source at all) +// Main input handler for the Canned Message UI. +// This function dispatches all key/button/touch input events relevant to canned messaging, +// and routes them to the appropriate handler or updates state as needed. + +int CannedMessageModule::handleInputEvent(const InputEvent* event) { + // Only allow input from the permitted source (usually "kb" or "_any") + if (!isInputSourceAllowed(event)) return 0; + + // --- TAB key: Used for switching between destination selection and freetext entry. + if (event->kbchar == 0x09) { // 0x09 == Tab key + if (handleTabSwitch(event)) return 0; + } + + // --- System/global commands: Brightness, Fn key, Bluetooth, GPS, etc. + if (handleSystemCommandInput(event)) return 0; + + // --- In INACTIVE state, Enter/Select acts like "Right" to advance frame. + if (runState == CANNED_MESSAGE_RUN_STATE_INACTIVE && + (event->inputEvent == meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) + { + // Mutate the event to look like a RIGHT arrow press, which will move to the next frame + const_cast(event)->inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT); + const_cast(event)->kbchar = INPUT_BROKER_MSG_RIGHT; + return 0; // Let the screen/frame navigation code handle it + } + + // --- Any printable character (except Tab or Enter) opens FreeText input from inactive, active, or disabled + if ((runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || + runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || + runState == CANNED_MESSAGE_RUN_STATE_DISABLED) && + (event->kbchar >= 32 && event->kbchar <= 126)) // Printable ASCII + { + // Skip Tab (0x09) and Enter since those have special functions above + if (event->kbchar != 0x09 && !isSelectEvent(event)) { + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + // DO NOT return here! Let this key event continue into the FreeText handler below. + } + } + + // Block all input when in the middle of sending a message + if (runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) return 0; + + // Cancel/Back while in FreeText mode exits back to inactive (clears draft) + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL) && + runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) + { + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + freetext = ""; + cursor = 0; + payload = 0; + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + screen->forceDisplay(); + return 1; // Handled + } + + // --- Normalize directional/select events for sub-UIs (node select, message select, etc.) + bool isUp = isUpEvent(event); + bool isDown = isDownEvent(event); + bool isSelect = isSelectEvent(event); + + // --- If currently selecting a destination node, handle navigation within that UI + if (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE && + runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) + { + return handleDestinationSelectionInput(event, isUp, isDown, isSelect); + } + + // --- Handle navigation in canned message list UI (up/down/select) + if (handleMessageSelectorInput(event, isUp, isDown, isSelect)) return 0; + + // --- Handle actual text entry or special input in FreeText mode + if (handleFreeTextInput(event)) return 0; + + // --- Matrix keypad mode (used for hardware with a matrix input) + if (event->inputEvent == static_cast(MATRIXKEY)) { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + payload = MATRIXKEY; + currentMessageIndex = event->kbchar - 1; + lastTouchMillis = millis(); + requestFocus(); return 0; } - // === Toggle Destination Selector with Tab === - if (event->kbchar == 0x09) { - if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { - // Exit selection - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; - this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - } else { - // Enter selection - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; - this->destIndex = 0; - this->scrollIndex = 0; - this->runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + // Default: Not handled, let other input layers process this event if needed + return 0; +} + +bool CannedMessageModule::isInputSourceAllowed(const InputEvent* event) { + return strlen(moduleConfig.canned_message.allow_input_source) == 0 || + strcasecmp(moduleConfig.canned_message.allow_input_source, event->source) == 0 || + strcasecmp(moduleConfig.canned_message.allow_input_source, "_any") == 0; +} + +bool CannedMessageModule::isUpEvent(const InputEvent* event) { + return event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP) || + event->kbchar == INPUT_BROKER_MSG_UP; +} + +bool CannedMessageModule::isDownEvent(const InputEvent* event) { + return event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN) || + event->kbchar == INPUT_BROKER_MSG_DOWN; +} + +bool CannedMessageModule::isSelectEvent(const InputEvent* event) { + return event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT); +} + +bool CannedMessageModule::handleTabSwitch(const InputEvent* event) { + if (event->kbchar != 0x09) return false; + + destSelect = (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) + ? CANNED_MESSAGE_DESTINATION_TYPE_NONE + : CANNED_MESSAGE_DESTINATION_TYPE_NODE; + runState = (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) + ? CANNED_MESSAGE_RUN_STATE_FREETEXT + : CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + + destIndex = 0; + scrollIndex = 0; + // RESTORE THIS! + if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) + updateFilteredNodes(); + + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + screen->forceDisplay(); + return true; +} + +int CannedMessageModule::handleDestinationSelectionInput(const InputEvent* event, bool isUp, bool isDown, bool isSelect) { + static bool shouldRedraw = false; + + // Handle character input for search + if (!isUp && !isDown && !isSelect && event->kbchar >= 32 && event->kbchar <= 126) { + this->searchQuery += event->kbchar; + needsUpdate = true; + runOnce(); // update filter immediately + return 0; + } + + size_t numMeshNodes = filteredNodes.size(); + int totalEntries = numMeshNodes + activeChannelIndices.size(); + int columns = 1; + int totalRows = totalEntries; + int maxScrollIndex = std::max(0, totalRows - visibleRows); + scrollIndex = clamp(scrollIndex, 0, maxScrollIndex); + + // Handle backspace + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK)) { + if (searchQuery.length() > 0) { + searchQuery.remove(searchQuery.length() - 1); + needsUpdate = true; + runOnce(); } + if (searchQuery.length() == 0) { + resetSearch(); + needsUpdate = false; + } + return 0; + } + + // UP + if (isUp && destIndex > 0) { + destIndex--; + if ((destIndex / columns) < scrollIndex) + scrollIndex = destIndex / columns; + else if ((destIndex / columns) >= (scrollIndex + visibleRows)) + scrollIndex = (destIndex / columns) - visibleRows + 1; + + shouldRedraw = true; + } + + // DOWN + if (isDown && destIndex + 1 < totalEntries) { + destIndex++; + if ((destIndex / columns) >= (scrollIndex + visibleRows)) + scrollIndex = (destIndex / columns) - visibleRows + 1; + + shouldRedraw = true; + } + + if (shouldRedraw) { + screen->forceDisplay(); + shouldRedraw = false; + } + + // SELECT + if (isSelect) { + if (destIndex < static_cast(activeChannelIndices.size())) { + dest = NODENUM_BROADCAST; + channel = activeChannelIndices[destIndex]; + } else { + int nodeIndex = destIndex - static_cast(activeChannelIndices.size()); + if (nodeIndex >= 0 && nodeIndex < static_cast(filteredNodes.size())) { + meshtastic_NodeInfoLite* selectedNode = filteredNodes[nodeIndex].node; + if (selectedNode) { + dest = selectedNode->num; + channel = selectedNode->channel; + } + } + } + + runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; + returnToCannedList = false; + destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; + screen->forceDisplay(); + return 0; + } + + // CANCEL + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) { + destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + searchQuery = ""; UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->notifyObservers(&e); + notifyObservers(&e); screen->forceDisplay(); return 0; } - if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) { - return 0; // Ignore input while sending - } - static int lastDestIndex = -1; // Cache the last index - bool isUp = false; - bool isDown = false; - bool isSelect = false; + return 0; +} - // Accept both inputEvent and kbchar from rotary encoder or CardKB - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP) || - event->kbchar == INPUT_BROKER_MSG_UP) { - isUp = true; - } - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN) || - event->kbchar == INPUT_BROKER_MSG_DOWN) { - isDown = true; - } - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT) || - event->kbchar == INPUT_BROKER_MSG_SELECT) { - isSelect = true; +bool CannedMessageModule::handleMessageSelectorInput(const InputEvent* event, bool isUp, bool isDown, bool isSelect) { + if (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) return false; + + // === Handle Cancel key: go inactive, clear UI state === + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) { + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + freetext = ""; + cursor = 0; + payload = 0; + currentMessageIndex = -1; + + // Notify UI that we want to redraw/close this screen + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + screen->forceDisplay(); + return true; } - if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { - //Fix rotary encoder registering as character instead of navigation - if (isUp || isDown || isSelect) { - // Already handled below β€” skip character input - } else if (event->kbchar >= 32 && event->kbchar <= 126) { - this->searchQuery += event->kbchar; - needsUpdate = true; - runOnce(); // <=== Force filtering immediately - return 0; - } + bool handled = false; - size_t numMeshNodes = this->filteredNodes.size(); - int totalEntries = numMeshNodes + this->activeChannelIndices.size(); - int columns = 1; - int totalRows = totalEntries; // one entry per row now - int maxScrollIndex = std::max(0, totalRows - this->visibleRows); - scrollIndex = std::max(0, std::min(scrollIndex, maxScrollIndex)); + // Handle up/down navigation + if (isUp && messagesCount > 0) { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; + handled = true; + } else if (isDown && messagesCount > 0) { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; + handled = true; + } else if (isSelect) { + const char* current = messages[currentMessageIndex]; - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK)) { - if (this->searchQuery.length() > 0) { - this->searchQuery.remove(this->searchQuery.length() - 1); - needsUpdate = true; - runOnce(); // <=== Ensure filter updates after backspace - } - if (this->searchQuery.length() == 0) { - resetSearch(); // Function to restore all destinations - needsUpdate = false; - } - return 0; - } - - // πŸ”Ό UP Navigation in Node Selection - if (isUp) { - if (this->destIndex > 0) { - this->destIndex--; - if ((this->destIndex / columns) < scrollIndex) { - scrollIndex = this->destIndex / columns; - shouldRedraw = true; - } else if ((this->destIndex / columns) >= (scrollIndex + visibleRows)) { - scrollIndex = (this->destIndex / columns) - visibleRows + 1; - shouldRedraw = true; - } else { - shouldRedraw = true; // βœ… allow redraw only once below - } - } - } - - // πŸ”½ DOWN Navigation in Node Selection - if (isDown) { - if (this->destIndex + 1 < totalEntries) { - this->destIndex++; - if ((this->destIndex / columns) >= (scrollIndex + visibleRows)) { - scrollIndex = (this->destIndex / columns) - visibleRows + 1; - shouldRedraw = true; - } else { - shouldRedraw = true; - } - } - } - - if (this->destSelect != CANNED_MESSAGE_DESTINATION_TYPE_NODE) { - if (isUp && this->messagesCount > 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; - return 0; - } - if (isDown && this->messagesCount > 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; - return 0; - } - } - // Only refresh UI when needed - if (shouldRedraw) { + // === [Select Destination] triggers destination selection UI === + if (strcmp(current, "[Select Destination]") == 0) { + returnToCannedList = true; + runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; + destIndex = 0; + scrollIndex = 0; + updateFilteredNodes(); // Make sure list is fresh screen->forceDisplay(); - shouldRedraw = false; + return true; } - if (isSelect) { - if (this->destIndex < static_cast(this->activeChannelIndices.size())) { - this->dest = NODENUM_BROADCAST; - this->channel = this->activeChannelIndices[this->destIndex]; - } else { - int nodeIndex = this->destIndex - static_cast(this->activeChannelIndices.size()); - if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { - meshtastic_NodeInfoLite *selectedNode = this->filteredNodes[nodeIndex].node; - if (selectedNode) { - this->dest = selectedNode->num; - this->channel = selectedNode->channel; - } - } - } - - // βœ… Now correctly switches to FreeText screen with selected node/channel - this->runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; - returnToCannedList = false; - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; - screen->forceDisplay(); - return 0; - } - - // Handle Cancel (ESC) - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) { - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; // Ensure return to main screen - this->searchQuery = ""; - + + // === [Exit] returns to the main/inactive screen === + if (strcmp(current, "[Exit]") == 0) { + // Set runState to inactive so we return to main UI + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + currentMessageIndex = -1; + + // Notify UI to regenerate frame set and redraw UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->notifyObservers(&e); - + notifyObservers(&e); screen->forceDisplay(); - return 0; // πŸš€ Prevents input from affecting canned messages + return true; } - - return 0; // πŸš€ FINAL EARLY EXIT: Stops the function from continuing into canned message handling - } - // If we reach here, we are NOT in Select Destination mode. - // The remaining logic is for canned message handling. - bool validEvent = false; - if (this->destSelect != CANNED_MESSAGE_DESTINATION_TYPE_NODE) { - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP)) { - if (this->messagesCount > 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP; - validEvent = true; - } - } - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN)) { - if (this->messagesCount > 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; - validEvent = true; - } - } - } - if (isSelect) { - if (strcmp(this->messages[this->currentMessageIndex], "[Select Destination]") == 0) { - returnToCannedList = true; - this->runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; - this->destIndex = 0; - this->scrollIndex = 0; - screen->forceDisplay(); - return 0; - } + // === [Free Text] triggers the free text input (virtual keyboard) === #if defined(USE_VIRTUAL_KEYBOARD) - if (this->currentMessageIndex == 0) { - this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - - requestFocus(); // Tell Screen::setFrames to move to our module's frame, next time it runs + if (currentMessageIndex == 0) { + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + requestFocus(); UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen - this->notifyObservers(&e); - - return 0; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return true; } #endif - // when inactive, call the onebutton shortpress instead. Activate Module only on up/down - if ((this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED)) { + // Normal canned message selection + if (runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || runState == CANNED_MESSAGE_RUN_STATE_DISABLED) { powerFSM.trigger(EVENT_PRESS); } else { - this->payload = this->runState; - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; - validEvent = true; + payload = runState; + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + handled = true; } } - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) { - // If in Node Selection Mode, exit and return to FreeText Mode - if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { - updateFilteredNodes(); // Ensure the filtered node list is refreshed before selecting - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; - this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - return 0; - } - - // Default behavior for Cancel in other modes - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen - this->currentMessageIndex = -1; -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD) - this->freetext = ""; // clear freetext - this->cursor = 0; - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; -#endif - - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - this->notifyObservers(&e); - screen->forceDisplay(); // Ensure the UI updates properly - return 0; + if (handled) { + requestFocus(); + if (runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) + setIntervalFromNow(0); + else + runOnce(); } - if ((event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK)) || - (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) || - (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT))) { + return handled; +} + +bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) { #if defined(USE_VIRTUAL_KEYBOARD) - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) { - this->payload = INPUT_BROKER_MSG_LEFT; - } else if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) { - this->payload = INPUT_BROKER_MSG_RIGHT; - } -#else - // tweak for left/right events generated via trackball/touch with empty kbchar - if (!event->kbchar) { - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) { - this->payload = INPUT_BROKER_MSG_LEFT; - } else if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) { - this->payload = INPUT_BROKER_MSG_RIGHT; - } - } else { - // pass the pressed key - this->payload = event->kbchar; - } + if (runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) return false; + + String keyTapped = keyForCoordinates(event->touchX, event->touchY); + bool valid = false; + + if (keyTapped == "⇧") { + highlight = -1; + payload = 0x00; + shift = !shift; + valid = true; + } else if (keyTapped == "⌫") { +#ifndef RAK14014 + highlight = keyTapped[0]; +#endif + payload = 0x08; + shift = false; + valid = true; + } else if (keyTapped == "123" || keyTapped == "ABC") { + highlight = -1; + payload = 0x00; + charSet = (charSet == 0 ? 1 : 0); + valid = true; + } else if (keyTapped == " ") { +#ifndef RAK14014 + highlight = keyTapped[0]; +#endif + payload = keyTapped[0]; + shift = false; + valid = true; + } + // Touch enter/submit + else if (keyTapped == "↡") { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; // Send the message! + payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; + currentMessageIndex = -1; + shift = false; + valid = true; + } else if (!keyTapped.isEmpty()) { +#ifndef RAK14014 + highlight = keyTapped[0]; +#endif + payload = shift ? keyTapped[0] : std::tolower(keyTapped[0]); + shift = false; + valid = true; + } + + if (valid) { + lastTouchMillis = millis(); + return true; + } #endif - this->lastTouchMillis = millis(); - validEvent = true; + bool isSelect = isSelectEvent(event); + + if (runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && isSelect) { + if (dest == 0) dest = NODENUM_BROADCAST; + + // Defensive: If channel isn't valid, pick the first available channel + if (channel < 0 || channel >= channels.getNumChannels()) channel = 0; + + payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; + currentMessageIndex = -1; + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; + lastTouchMillis = millis(); + return true; } - if (event->inputEvent == static_cast(ANYKEY)) { - // Prevent entering freetext mode while overlay banner is showing - extern String alertBannerMessage; - extern uint32_t alertBannerUntil; - if (alertBannerMessage.length() > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil)) { - return 0; - } - if ((this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || - this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || - this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) && - (event->kbchar >= 32 && event->kbchar <= 126)) { - this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - } + // Backspace + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK)) { + payload = 0x08; + lastTouchMillis = millis(); + runOnce(); + return true; + } - validEvent = false; // If key is normal than it will be set to true. + // Cancel (dismiss freetext screen) + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) { + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + screen->forceDisplay(); + return true; + } - // Run modifier key code below, (doesnt inturrupt typing or reset to start screen page) - switch (event->kbchar) { - case INPUT_BROKER_MSG_BRIGHTNESS_UP: // make screen brighter - if (screen) - screen->increaseBrightness(); + // Tab (switch destination) + if (event->kbchar == INPUT_BROKER_MSG_TAB) { + return handleTabSwitch(event); // Reuse tab logic + } + + // Printable ASCII (add char to draft) + if (event->kbchar >= 32 && event->kbchar <= 126) { + payload = event->kbchar; + lastTouchMillis = millis(); + runOnce(); + return true; + } + + return false; +} + +bool CannedMessageModule::handleSystemCommandInput(const InputEvent* event) { + // Only respond to "ANYKEY" events + if (event->inputEvent != static_cast(ANYKEY)) return false; + + // In FreeText, printable keys should go to FreeText input, not here + if (runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && + event->kbchar >= 32 && event->kbchar <= 126) { + return false; // Let handleFreeTextInput() process it + } + + // Suppress all system input if an alert banner is showing + extern String alertBannerMessage; + extern uint32_t alertBannerUntil; + if (alertBannerMessage.length() > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil)) { + return true; + } + + // Printable character in inactive/active/disabled: switch to FreeText (but let handleFreeTextInput actually handle key) + if ((runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || + runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || + runState == CANNED_MESSAGE_RUN_STATE_DISABLED) && + (event->kbchar >= 32 && event->kbchar <= 126)) + { + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + // Let FreeText input handler process the key itself + return false; + } + + bool valid = false; + + switch (event->kbchar) { + // Fn key symbols + case INPUT_BROKER_MSG_FN_SYMBOL_ON: + if (screen) screen->setFunctionSymbol("Fn"); + break; + case INPUT_BROKER_MSG_FN_SYMBOL_OFF: + if (screen) screen->removeFunctionSymbol("Fn"); + break; + + // Screen/system toggles + case INPUT_BROKER_MSG_BRIGHTNESS_UP: + if (screen) screen->increaseBrightness(); LOG_DEBUG("Increase Screen Brightness"); break; - case INPUT_BROKER_MSG_BRIGHTNESS_DOWN: // make screen dimmer - if (screen) - screen->decreaseBrightness(); + case INPUT_BROKER_MSG_BRIGHTNESS_DOWN: + if (screen) screen->decreaseBrightness(); LOG_DEBUG("Decrease Screen Brightness"); break; - case INPUT_BROKER_MSG_FN_SYMBOL_ON: // draw modifier (function) symbol - if (screen) - screen->setFunctionSymbol("Fn"); - break; - case INPUT_BROKER_MSG_FN_SYMBOL_OFF: // remove modifier (function) symbol - if (screen) - screen->removeFunctionSymbol("Fn"); - break; - // mute (switch off/toggle) external notifications on fn+m case INPUT_BROKER_MSG_MUTE_TOGGLE: - if (moduleConfig.external_notification.enabled == true) { - if (externalNotificationModule->getMute()) { - externalNotificationModule->setMute(false); - graphics::isMuted = false; - if (screen) screen->showOverlayBanner("Notifications\nEnabled", 3000); - } else { + if (moduleConfig.external_notification.enabled && externalNotificationModule) { + bool isMuted = externalNotificationModule->getMute(); + externalNotificationModule->setMute(!isMuted); + graphics::isMuted = !isMuted; + if (!isMuted) externalNotificationModule->stopNow(); - externalNotificationModule->setMute(true); - graphics::isMuted = true; - if (screen) screen->showOverlayBanner("Notifications\nDisabled", 3000); - } + if (screen) + screen->showOverlayBanner(isMuted ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000); } break; - case INPUT_BROKER_MSG_GPS_TOGGLE: -#if !MESHTASTIC_EXCLUDE_GPS - if (gps != nullptr) { - gps->toggleGpsMode(); - const char* statusMsg = (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) - ? "GPS Enabled" - : "GPS Disabled"; - if (screen) { - screen->forceDisplay(); - screen->showOverlayBanner(statusMsg, 3000); - } - } -#endif - break; + // Bluetooth toggle case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: - if (config.bluetooth.enabled == true) { - config.bluetooth.enabled = false; - LOG_INFO("User toggled Bluetooth"); - nodeDB->saveToDisk(); + config.bluetooth.enabled = !config.bluetooth.enabled; + LOG_INFO("User toggled Bluetooth"); + nodeDB->saveToDisk(); +#if defined(ARDUINO_ARCH_NRF52) + if (!config.bluetooth.enabled) { + disableBluetooth(); + if (screen) screen->showOverlayBanner("Bluetooth OFF\nRebooting", 3000); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 2000; + } else { + if (screen) screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + } +#else + if (!config.bluetooth.enabled) { disableBluetooth(); if (screen) screen->showOverlayBanner("Bluetooth OFF", 3000); } else { - config.bluetooth.enabled = true; - LOG_INFO("User toggled Bluetooth"); - nodeDB->saveToDisk(); - rebootAtMsec = millis() + 2000; if (screen) screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; } +#endif break; + + // GPS toggle + case INPUT_BROKER_MSG_GPS_TOGGLE: +#if !MESHTASTIC_EXCLUDE_GPS + if (gps) { + gps->toggleGpsMode(); + const char* msg = (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) + ? "GPS Enabled" : "GPS Disabled"; + if (screen) { + screen->forceDisplay(); + screen->showOverlayBanner(msg, 3000); + } + } +#endif + break; + + // Mesh ping case INPUT_BROKER_MSG_SEND_PING: service->refreshLocalMeshNode(); if (service->trySendPosition(NODENUM_BROADCAST, true)) { @@ -575,130 +752,36 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) if (screen) screen->showOverlayBanner("Node Info\nUpdate Sent", 3000); } break; + + // Power control case INPUT_BROKER_MSG_SHUTDOWN: - if (screen) - screen->showOverlayBanner("Shutting down..."); + if (screen) screen->showOverlayBanner("Shutting down..."); shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; nodeDB->saveToDisk(); - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - validEvent = true; + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + valid = true; break; - case INPUT_BROKER_MSG_REBOOT: - if (screen) - screen->showOverlayBanner("Rebooting...", 0); // stays on screen + if (screen) screen->showOverlayBanner("Rebooting...", 0); nodeDB->saveToDisk(); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - validEvent = true; + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + valid = true; break; - case INPUT_BROKER_MSG_DISMISS_FRAME: // fn+del: dismiss screen frames like text or waypoint - // Avoid opening the canned message screen frame - // We're only handling the keypress here by convention, this has nothing to do with canned messages - this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - // Attempt to close whatever frame is currently shown on display - screen->dismissCurrentFrame(); - return 0; + case INPUT_BROKER_MSG_DISMISS_FRAME: + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + if (screen) screen->dismissCurrentFrame(); + return true; + + // Default: store last key and let other input handlers process if needed default: - // pass the pressed key - // LOG_DEBUG("Canned message ANYKEY (%x)", event->kbchar); - this->payload = event->kbchar; - this->lastTouchMillis = millis(); - validEvent = true; + payload = event->kbchar; + lastTouchMillis = millis(); + valid = true; break; - } - if (screen && (event->kbchar != INPUT_BROKER_MSG_FN_SYMBOL_ON)) { - screen->removeFunctionSymbol("Fn"); // remove modifier (function) symbol - } } -#if defined(USE_VIRTUAL_KEYBOARD) - if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - String keyTapped = keyForCoordinates(event->touchX, event->touchY); - - if (keyTapped == "⇧") { - this->highlight = -1; - - this->payload = 0x00; - - validEvent = true; - - this->shift = !this->shift; - } else if (keyTapped == "⌫") { -#ifndef RAK14014 - this->highlight = keyTapped[0]; -#endif - - this->payload = 0x08; - - validEvent = true; - - this->shift = false; - } else if (keyTapped == "123" || keyTapped == "ABC") { - this->highlight = -1; - - this->payload = 0x00; - - this->charSet = this->charSet == 0 ? 1 : 0; - - validEvent = true; - } else if (keyTapped == " ") { -#ifndef RAK14014 - this->highlight = keyTapped[0]; -#endif - - this->payload = keyTapped[0]; - - validEvent = true; - - this->shift = false; - } else if (keyTapped == "↡") { - this->highlight = 0x00; - - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; - - this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; - - this->currentMessageIndex = event->kbchar - 1; - - validEvent = true; - - this->shift = false; - } else if (keyTapped != "") { -#ifndef RAK14014 - this->highlight = keyTapped[0]; -#endif - - this->payload = this->shift ? keyTapped[0] : std::tolower(keyTapped[0]); - - validEvent = true; - - this->shift = false; - } - } -#endif - - if (event->inputEvent == static_cast(MATRIXKEY)) { - // this will send the text immediately on matrix press - this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; - this->payload = MATRIXKEY; - this->currentMessageIndex = event->kbchar - 1; - this->lastTouchMillis = millis(); - validEvent = true; - } - - if (validEvent) { - requestFocus(); // Tell Screen::setFrames to move to our module's frame, next time it runs - - // Let runOnce to be called immediately. - if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { - setIntervalFromNow(0); // on fast keypresses, this isn't fast enough. - } else { - runOnce(); - } - } - - return 0; + return valid; } void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies) @@ -742,7 +825,7 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha screen->handleTextMessage(&simulatedPacket); } } - +bool validEvent = false; unsigned long lastUpdateMillis = 0; int32_t CannedMessageModule::runOnce() { @@ -754,19 +837,22 @@ int32_t CannedMessageModule::runOnce() updateFilteredNodes(); lastUpdateMillis = millis(); } + // Prevent message list activity when selecting destination + if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + return INACTIVATE_AFTER_MS; + } + if (((!moduleConfig.canned_message.enabled) && !CANNED_MESSAGE_MODULE_ENABLE) || (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { temporaryMessage = ""; return INT32_MAX; } - // LOG_DEBUG("Check status"); UIFrameEvent e; if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) || - (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE)) { - // TODO: might have some feedback of sending state + (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; temporaryMessage = ""; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; this->freetext = ""; // clear freetext this->cursor = 0; @@ -779,7 +865,7 @@ int32_t CannedMessageModule::runOnce() } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { // Reset module - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; this->freetext = ""; // clear freetext this->cursor = 0; @@ -952,7 +1038,7 @@ int32_t CannedMessageModule::runOnce() this->cursor--; } break; - case 0x09: // Tab key (Switch to Destination Selection Mode) + case INPUT_BROKER_MSG_TAB: // Tab key (Switch to Destination Selection Mode) { if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) { // Enter selection screen @@ -996,8 +1082,6 @@ int32_t CannedMessageModule::runOnce() } break; } - if (screen) - screen->removeFunctionSymbol("Fn"); } this->lastTouchMillis = millis(); @@ -1092,7 +1176,7 @@ void CannedMessageModule::showTemporaryMessage(const String &message) UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen notifyObservers(&e); - runState = CANNED_MESSAGE_RUN_STATE_MESSAGE; + runState = CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION; // run this loop again in 2 seconds, next iteration will clear the display setIntervalFromNow(2000); } @@ -1374,7 +1458,6 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; if (node) { entryText = node->is_favorite ? "* " + String(node->user.long_name) : String(node->user.long_name); - bool hasKey = hasKeyForNode(node); } } } @@ -1550,8 +1633,6 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st display->setFont(FONT_SMALL); const int rowSpacing = FONT_HEIGHT_SMALL - 4; - const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int boxYOffset = (highlightHeight - FONT_HEIGHT_SMALL) / 2; // Draw header (To: ...) switch (this->destSelect) { @@ -1760,3 +1841,7 @@ String CannedMessageModule::drawWithCursor(String text, int cursor) } #endif + +bool CannedMessageModule::isInterceptingAndFocused() { + return this->interceptingKeyboardInput(); +} \ No newline at end of file diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index 9a3f610c0..7ba537a10 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -11,13 +11,13 @@ enum cannedMessageModuleRunState { CANNED_MESSAGE_RUN_STATE_DISABLED, CANNED_MESSAGE_RUN_STATE_INACTIVE, CANNED_MESSAGE_RUN_STATE_ACTIVE, - CANNED_MESSAGE_RUN_STATE_FREETEXT, CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE, CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED, - CANNED_MESSAGE_RUN_STATE_MESSAGE, CANNED_MESSAGE_RUN_STATE_ACTION_SELECT, CANNED_MESSAGE_RUN_STATE_ACTION_UP, CANNED_MESSAGE_RUN_STATE_ACTION_DOWN, + CANNED_MESSAGE_RUN_STATE_FREETEXT, + CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION, CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION }; @@ -77,6 +77,7 @@ public: void showTemporaryMessage(const String &message); void resetSearch(); void updateFilteredNodes(); + bool isInterceptingAndFocused(); String drawWithCursor(String text, int cursor); // === Admin Handlers === @@ -182,6 +183,15 @@ private: std::vector activeChannelIndices; std::vector filteredNodes; + bool isInputSourceAllowed(const InputEvent *event); + bool isUpEvent(const InputEvent *event); + bool isDownEvent(const InputEvent *event); + bool isSelectEvent(const InputEvent *event); + bool handleTabSwitch(const InputEvent *event); + int handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect); + bool handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect); + bool handleFreeTextInput(const InputEvent *event); + bool handleSystemCommandInput(const InputEvent *event); #if defined(USE_VIRTUAL_KEYBOARD) Letter keyboard[2][4][10] = {{{{"Q", 20, 0, 0, 0, 0}, From dbfa703cfa08733c0daf767ac2569cfc6459f2aa Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 26 May 2025 03:00:38 -0400 Subject: [PATCH 161/265] Virtual keyboard fix --- src/graphics/images.h | 2 ++ src/modules/CannedMessageModule.cpp | 6 +++--- src/modules/CannedMessageModule.h | 8 +++++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/graphics/images.h b/src/graphics/images.h index 9e421dba9..a49646c06 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -281,4 +281,6 @@ static const uint8_t icon_node[] PROGMEM = { 0xFE // ####### ← device base }; + #include "img/icon.xbm" +static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning"); diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 78ca741f5..bbc1d13c9 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -596,7 +596,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) { if (dest == 0) dest = NODENUM_BROADCAST; // Defensive: If channel isn't valid, pick the first available channel - if (channel < 0 || channel >= channels.getNumChannels()) channel = 0; + if (channel >= channels.getNumChannels()) channel = 0; payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; currentMessageIndex = -1; @@ -1028,7 +1028,7 @@ int32_t CannedMessageModule::runOnce() switch (this->payload) { // code below all trigger the freetext window (where you type to send a message) or reset the // display back to the default window case 0x08: // backspace - if (this->freetext.length() > 0 && this->highlight == 0x00) { + if (this->freetext.length() > 0 && this->key_highlight == 0x00) { if (this->cursor == this->freetext.length()) { this->freetext = this->freetext.substring(0, this->freetext.length() - 1); } else { @@ -1062,7 +1062,7 @@ int32_t CannedMessageModule::runOnce() // already handled above break; default: - if (this->highlight != 0x00) { + if (this->key_highlight != 0x00) { break; } diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index 7ba537a10..c8728fd8b 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -150,6 +150,13 @@ private: String freetext; String temporaryMessage; +#if defined(USE_VIRTUAL_KEYBOARD) + bool shift = false; // True if Shift (caps/alt) is active + int charSet = 0; // 0 = alpha keyboard, 1 = numeric/symbol keyboard + int highlight = -1; // Highlighted key for UI feedback +#endif + char key_highlight = 0x00; + // === Message Storage === char messageStore[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1]; char *messages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT]; @@ -176,7 +183,6 @@ private: // === State Tracking === cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; cannedMessageDestinationType destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; - char highlight = 0x00; char payload = 0x00; unsigned int cursor = 0; unsigned long lastTouchMillis = 0; From 3108de207ec3df2ac4f38fff016804493fdb9802 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 26 May 2025 03:23:08 -0400 Subject: [PATCH 162/265] bug fix --- src/modules/CannedMessageModule.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index bbc1d13c9..52156f7f9 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -575,7 +575,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) { currentMessageIndex = -1; shift = false; valid = true; - } else if (!keyTapped.isEmpty()) { + } else if (keyTapped.length() > 0) { #ifndef RAK14014 highlight = keyTapped[0]; #endif From 3741b78a327884f656ebd8b974912c59cfe19e10 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 26 May 2025 04:37:41 -0400 Subject: [PATCH 163/265] Revert "bug fix" This reverts commit 3108de207ec3df2ac4f38fff016804493fdb9802. --- src/modules/CannedMessageModule.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 52156f7f9..bbc1d13c9 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -575,7 +575,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) { currentMessageIndex = -1; shift = false; valid = true; - } else if (keyTapped.length() > 0) { + } else if (!keyTapped.isEmpty()) { #ifndef RAK14014 highlight = keyTapped[0]; #endif From 6aff2179d1ace44ae9c3b998b561420fd3f76dba Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 26 May 2025 04:37:58 -0400 Subject: [PATCH 164/265] Revert "Virtual keyboard fix" This reverts commit dbfa703cfa08733c0daf767ac2569cfc6459f2aa. --- src/graphics/images.h | 2 -- src/modules/CannedMessageModule.cpp | 6 +++--- src/modules/CannedMessageModule.h | 8 +------- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/graphics/images.h b/src/graphics/images.h index a49646c06..9e421dba9 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -281,6 +281,4 @@ static const uint8_t icon_node[] PROGMEM = { 0xFE // ####### ← device base }; - #include "img/icon.xbm" -static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning"); diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index bbc1d13c9..78ca741f5 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -596,7 +596,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) { if (dest == 0) dest = NODENUM_BROADCAST; // Defensive: If channel isn't valid, pick the first available channel - if (channel >= channels.getNumChannels()) channel = 0; + if (channel < 0 || channel >= channels.getNumChannels()) channel = 0; payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; currentMessageIndex = -1; @@ -1028,7 +1028,7 @@ int32_t CannedMessageModule::runOnce() switch (this->payload) { // code below all trigger the freetext window (where you type to send a message) or reset the // display back to the default window case 0x08: // backspace - if (this->freetext.length() > 0 && this->key_highlight == 0x00) { + if (this->freetext.length() > 0 && this->highlight == 0x00) { if (this->cursor == this->freetext.length()) { this->freetext = this->freetext.substring(0, this->freetext.length() - 1); } else { @@ -1062,7 +1062,7 @@ int32_t CannedMessageModule::runOnce() // already handled above break; default: - if (this->key_highlight != 0x00) { + if (this->highlight != 0x00) { break; } diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index c8728fd8b..7ba537a10 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -150,13 +150,6 @@ private: String freetext; String temporaryMessage; -#if defined(USE_VIRTUAL_KEYBOARD) - bool shift = false; // True if Shift (caps/alt) is active - int charSet = 0; // 0 = alpha keyboard, 1 = numeric/symbol keyboard - int highlight = -1; // Highlighted key for UI feedback -#endif - char key_highlight = 0x00; - // === Message Storage === char messageStore[CANNED_MESSAGE_MODULE_MESSAGES_SIZE + 1]; char *messages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT]; @@ -183,6 +176,7 @@ private: // === State Tracking === cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; cannedMessageDestinationType destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; + char highlight = 0x00; char payload = 0x00; unsigned int cursor = 0; unsigned long lastTouchMillis = 0; From 4935e91454ea4876caae606dcf987634a63667ac Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 26 May 2025 18:01:16 -0400 Subject: [PATCH 165/265] More cannedmessage cleanup --- src/modules/CannedMessageModule.cpp | 166 +++++++++++----------------- src/modules/CannedMessageModule.h | 4 +- 2 files changed, 64 insertions(+), 106 deletions(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 78ca741f5..84211a488 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -217,20 +217,33 @@ void CannedMessageModule::updateFilteredNodes() { // Main input handler for the Canned Message UI. // This function dispatches all key/button/touch input events relevant to canned messaging, // and routes them to the appropriate handler or updates state as needed. - int CannedMessageModule::handleInputEvent(const InputEvent* event) { // Only allow input from the permitted source (usually "kb" or "_any") if (!isInputSourceAllowed(event)) return 0; - // --- TAB key: Used for switching between destination selection and freetext entry. - if (event->kbchar == 0x09) { // 0x09 == Tab key + // TAB: Switch between destination and canned messages (or trigger tab logic) + if (event->kbchar == INPUT_BROKER_MSG_TAB) { if (handleTabSwitch(event)) return 0; } - // --- System/global commands: Brightness, Fn key, Bluetooth, GPS, etc. + // === Printable key in INACTIVE, ACTIVE, or DISABLED opens FreeText === + if ((runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || + runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || + runState == CANNED_MESSAGE_RUN_STATE_DISABLED) && + (event->kbchar >= 32 && event->kbchar <= 126)) + { + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + // DO NOT return here! Let this key event continue into FreeText input logic below. + } + + // === System/global commands: Brightness, Fn, Bluetooth, GPS, Power, etc === if (handleSystemCommandInput(event)) return 0; - // --- In INACTIVE state, Enter/Select acts like "Right" to advance frame. + // === In INACTIVE state, Enter/Select acts like "Right" to advance frame. === if (runState == CANNED_MESSAGE_RUN_STATE_INACTIVE && (event->inputEvent == meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) { @@ -240,33 +253,14 @@ int CannedMessageModule::handleInputEvent(const InputEvent* event) { return 0; // Let the screen/frame navigation code handle it } - // --- Any printable character (except Tab or Enter) opens FreeText input from inactive, active, or disabled - if ((runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || - runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || - runState == CANNED_MESSAGE_RUN_STATE_DISABLED) && - (event->kbchar >= 32 && event->kbchar <= 126)) // Printable ASCII - { - // Skip Tab (0x09) and Enter since those have special functions above - if (event->kbchar != 0x09 && !isSelectEvent(event)) { - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - // DO NOT return here! Let this key event continue into the FreeText handler below. - } - } - - // Block all input when in the middle of sending a message + // === Block input when sending === if (runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) return 0; - // Cancel/Back while in FreeText mode exits back to inactive (clears draft) + // === Cancel/Back while in FreeText mode: exit to inactive and clear draft === if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL) && runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - freetext = ""; - cursor = 0; payload = 0; UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; @@ -275,25 +269,27 @@ int CannedMessageModule::handleInputEvent(const InputEvent* event) { return 1; // Handled } - // --- Normalize directional/select events for sub-UIs (node select, message select, etc.) + // --- Normalize directional/select events for sub-UIs (node select, message select, etc.) --- bool isUp = isUpEvent(event); bool isDown = isDownEvent(event); bool isSelect = isSelectEvent(event); - // --- If currently selecting a destination node, handle navigation within that UI - if (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE && - runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) - { - return handleDestinationSelectionInput(event, isUp, isDown, isSelect); + // === FreeText input mode === + if (runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + if (handleFreeTextInput(event)) return 1; // Consumed by freetext, do NOT send to search + return 0; } - // --- Handle navigation in canned message list UI (up/down/select) + // === Node/Destination Selection input mode === + if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + if (handleDestinationSelectionInput(event, isUp, isDown, isSelect)) return 1; // Consumed by search, do NOT send to freetext + return 0; + } + + // === Handle navigation in canned message list UI (up/down/select) === if (handleMessageSelectorInput(event, isUp, isDown, isSelect)) return 0; - // --- Handle actual text entry or special input in FreeText mode - if (handleFreeTextInput(event)) return 0; - - // --- Matrix keypad mode (used for hardware with a matrix input) + // === Matrix keypad mode (used for hardware with a matrix input) === if (event->inputEvent == static_cast(MATRIXKEY)) { runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; payload = MATRIXKEY; @@ -303,7 +299,7 @@ int CannedMessageModule::handleInputEvent(const InputEvent* event) { return 0; } - // Default: Not handled, let other input layers process this event if needed + // === Default: Not handled, let other input layers process this event if needed === return 0; } @@ -314,15 +310,11 @@ bool CannedMessageModule::isInputSourceAllowed(const InputEvent* event) { } bool CannedMessageModule::isUpEvent(const InputEvent* event) { - return event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP) || - event->kbchar == INPUT_BROKER_MSG_UP; + return event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP); } - bool CannedMessageModule::isDownEvent(const InputEvent* event) { - return event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN) || - event->kbchar == INPUT_BROKER_MSG_DOWN; + return event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); } - bool CannedMessageModule::isSelectEvent(const InputEvent* event) { return event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT); } @@ -354,7 +346,7 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent* event static bool shouldRedraw = false; // Handle character input for search - if (!isUp && !isDown && !isSelect && event->kbchar >= 32 && event->kbchar <= 126) { + if (event->kbchar >= 32 && event->kbchar <= 126) { this->searchQuery += event->kbchar; needsUpdate = true; runOnce(); // update filter immediately @@ -640,57 +632,35 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) { } bool CannedMessageModule::handleSystemCommandInput(const InputEvent* event) { - // Only respond to "ANYKEY" events + // Only respond to "ANYKEY" events for system keys if (event->inputEvent != static_cast(ANYKEY)) return false; - // In FreeText, printable keys should go to FreeText input, not here - if (runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && - event->kbchar >= 32 && event->kbchar <= 126) { - return false; // Let handleFreeTextInput() process it - } - - // Suppress all system input if an alert banner is showing + // Block ALL input if an alert banner is active extern String alertBannerMessage; extern uint32_t alertBannerUntil; if (alertBannerMessage.length() > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil)) { return true; } - // Printable character in inactive/active/disabled: switch to FreeText (but let handleFreeTextInput actually handle key) - if ((runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || - runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || - runState == CANNED_MESSAGE_RUN_STATE_DISABLED) && - (event->kbchar >= 32 && event->kbchar <= 126)) - { - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - // Let FreeText input handler process the key itself - return false; - } - - bool valid = false; - + // System commands (all others fall through to return false) switch (event->kbchar) { // Fn key symbols case INPUT_BROKER_MSG_FN_SYMBOL_ON: if (screen) screen->setFunctionSymbol("Fn"); - break; + return true; case INPUT_BROKER_MSG_FN_SYMBOL_OFF: if (screen) screen->removeFunctionSymbol("Fn"); - break; - - // Screen/system toggles + return true; + // Brightness case INPUT_BROKER_MSG_BRIGHTNESS_UP: if (screen) screen->increaseBrightness(); LOG_DEBUG("Increase Screen Brightness"); - break; + return true; case INPUT_BROKER_MSG_BRIGHTNESS_DOWN: if (screen) screen->decreaseBrightness(); LOG_DEBUG("Decrease Screen Brightness"); - break; + return true; + // Mute case INPUT_BROKER_MSG_MUTE_TOGGLE: if (moduleConfig.external_notification.enabled && externalNotificationModule) { bool isMuted = externalNotificationModule->getMute(); @@ -701,14 +671,13 @@ bool CannedMessageModule::handleSystemCommandInput(const InputEvent* event) { if (screen) screen->showOverlayBanner(isMuted ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000); } - break; - - // Bluetooth toggle + return true; + // Bluetooth case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: config.bluetooth.enabled = !config.bluetooth.enabled; LOG_INFO("User toggled Bluetooth"); nodeDB->saveToDisk(); -#if defined(ARDUINO_ARCH_NRF52) + #if defined(ARDUINO_ARCH_NRF52) if (!config.bluetooth.enabled) { disableBluetooth(); if (screen) screen->showOverlayBanner("Bluetooth OFF\nRebooting", 3000); @@ -717,7 +686,7 @@ bool CannedMessageModule::handleSystemCommandInput(const InputEvent* event) { if (screen) screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; } -#else + #else if (!config.bluetooth.enabled) { disableBluetooth(); if (screen) screen->showOverlayBanner("Bluetooth OFF", 3000); @@ -725,24 +694,22 @@ bool CannedMessageModule::handleSystemCommandInput(const InputEvent* event) { if (screen) screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; } -#endif - break; - - // GPS toggle + #endif + return true; + // GPS case INPUT_BROKER_MSG_GPS_TOGGLE: -#if !MESHTASTIC_EXCLUDE_GPS + #if !MESHTASTIC_EXCLUDE_GPS if (gps) { gps->toggleGpsMode(); const char* msg = (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) - ? "GPS Enabled" : "GPS Disabled"; + ? "GPS Enabled" : "GPS Disabled"; if (screen) { screen->forceDisplay(); screen->showOverlayBanner(msg, 3000); } } -#endif - break; - + #endif + return true; // Mesh ping case INPUT_BROKER_MSG_SEND_PING: service->refreshLocalMeshNode(); @@ -751,37 +718,28 @@ bool CannedMessageModule::handleSystemCommandInput(const InputEvent* event) { } else { if (screen) screen->showOverlayBanner("Node Info\nUpdate Sent", 3000); } - break; - + return true; // Power control case INPUT_BROKER_MSG_SHUTDOWN: if (screen) screen->showOverlayBanner("Shutting down..."); shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; nodeDB->saveToDisk(); runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - valid = true; - break; + return true; case INPUT_BROKER_MSG_REBOOT: if (screen) screen->showOverlayBanner("Rebooting...", 0); nodeDB->saveToDisk(); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - valid = true; - break; + return true; case INPUT_BROKER_MSG_DISMISS_FRAME: runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; if (screen) screen->dismissCurrentFrame(); return true; - - // Default: store last key and let other input handlers process if needed + // Not a system command, let other handlers process it default: - payload = event->kbchar; - lastTouchMillis = millis(); - valid = true; - break; + return false; } - - return valid; } void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies) diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index 7ba537a10..112ae8260 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -16,9 +16,9 @@ enum cannedMessageModuleRunState { CANNED_MESSAGE_RUN_STATE_ACTION_SELECT, CANNED_MESSAGE_RUN_STATE_ACTION_UP, CANNED_MESSAGE_RUN_STATE_ACTION_DOWN, + CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION, CANNED_MESSAGE_RUN_STATE_FREETEXT, - CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION, - CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION + CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION }; enum cannedMessageDestinationType { From 4fdc5f094ada9330a4310acd31f71f1ddd2ed401 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 26 May 2025 18:46:15 -0400 Subject: [PATCH 166/265] More cleanup --- src/modules/CannedMessageModule.cpp | 139 +++++++++++++--------------- src/modules/CannedMessageModule.h | 1 + 2 files changed, 67 insertions(+), 73 deletions(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 84211a488..f13254cf9 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -214,92 +214,85 @@ void CannedMessageModule::updateFilteredNodes() { } } -// Main input handler for the Canned Message UI. -// This function dispatches all key/button/touch input events relevant to canned messaging, -// and routes them to the appropriate handler or updates state as needed. +// Returns true if character input is currently allowed (used for search/freetext states) +bool CannedMessageModule::isCharInputAllowed() const { + return runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || + runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; +} +/** + * Main input event dispatcher for CannedMessageModule. + * Routes keyboard/button/touch input to the correct handler based on the current runState. + * Only one handler (per state) processes each event, eliminating redundancy. + */ int CannedMessageModule::handleInputEvent(const InputEvent* event) { - // Only allow input from the permitted source (usually "kb" or "_any") + // Allow input only from configured source (hardware/software filter) if (!isInputSourceAllowed(event)) return 0; - // TAB: Switch between destination and canned messages (or trigger tab logic) - if (event->kbchar == INPUT_BROKER_MSG_TAB) { - if (handleTabSwitch(event)) return 0; - } + // Global/system commands always processed (brightness, BT, GPS, shutdown, etc.) + if (handleSystemCommandInput(event)) return 1; - // === Printable key in INACTIVE, ACTIVE, or DISABLED opens FreeText === - if ((runState == CANNED_MESSAGE_RUN_STATE_INACTIVE || - runState == CANNED_MESSAGE_RUN_STATE_ACTIVE || - runState == CANNED_MESSAGE_RUN_STATE_DISABLED) && - (event->kbchar >= 32 && event->kbchar <= 126)) - { - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - // DO NOT return here! Let this key event continue into FreeText input logic below. - } + // Tab key: Always allow switching between canned/destination screens + if (event->kbchar == INPUT_BROKER_MSG_TAB && handleTabSwitch(event)) return 1; - // === System/global commands: Brightness, Fn, Bluetooth, GPS, Power, etc === - if (handleSystemCommandInput(event)) return 0; - - // === In INACTIVE state, Enter/Select acts like "Right" to advance frame. === - if (runState == CANNED_MESSAGE_RUN_STATE_INACTIVE && - (event->inputEvent == meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) - { - // Mutate the event to look like a RIGHT arrow press, which will move to the next frame - const_cast(event)->inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT); - const_cast(event)->kbchar = INPUT_BROKER_MSG_RIGHT; - return 0; // Let the screen/frame navigation code handle it - } - - // === Block input when sending === - if (runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) return 0; - - // === Cancel/Back while in FreeText mode: exit to inactive and clear draft === - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL) && - runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) - { - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - payload = 0; - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - screen->forceDisplay(); - return 1; // Handled - } - - // --- Normalize directional/select events for sub-UIs (node select, message select, etc.) --- - bool isUp = isUpEvent(event); - bool isDown = isDownEvent(event); - bool isSelect = isSelectEvent(event); - - // === FreeText input mode === - if (runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - if (handleFreeTextInput(event)) return 1; // Consumed by freetext, do NOT send to search - return 0; - } - - // === Node/Destination Selection input mode === - if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { - if (handleDestinationSelectionInput(event, isUp, isDown, isSelect)) return 1; // Consumed by search, do NOT send to freetext - return 0; - } - - // === Handle navigation in canned message list UI (up/down/select) === - if (handleMessageSelectorInput(event, isUp, isDown, isSelect)) return 0; - - // === Matrix keypad mode (used for hardware with a matrix input) === + // Matrix keypad: If matrix key, trigger action select for canned message if (event->inputEvent == static_cast(MATRIXKEY)) { runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; payload = MATRIXKEY; currentMessageIndex = event->kbchar - 1; lastTouchMillis = millis(); requestFocus(); - return 0; + return 1; } - // === Default: Not handled, let other input layers process this event if needed === + // Always normalize navigation/select buttons for further handlers + bool isUp = isUpEvent(event); + bool isDown = isDownEvent(event); + bool isSelect = isSelectEvent(event); + + // Route event to handler for current UI state (no double-handling) + switch (runState) { + // Node/Channel destination selection mode: Handles character search, arrows, select, cancel, backspace + case CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION: + return handleDestinationSelectionInput(event, isUp, isDown, isSelect); // All allowed input for this state + + // Free text input mode: Handles character input, cancel, backspace, select, etc. + case CANNED_MESSAGE_RUN_STATE_FREETEXT: + return handleFreeTextInput(event); // All allowed input for this state + + // If sending, block all input except global/system (handled above) + case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE: + return 1; // Swallow all + + // If inactive: allow select to advance frame, or char to open free text input + case CANNED_MESSAGE_RUN_STATE_INACTIVE: + if (isSelect) { + // Remap select to right (frame advance), let screen navigation handle it + const_cast(event)->inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT); + const_cast(event)->kbchar = INPUT_BROKER_MSG_RIGHT; + return 0; + } + // Printable char (ASCII) opens free text compose; then let the handler process the event + if (event->kbchar >= 32 && event->kbchar <= 126) { + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + // Immediately process the input in the new state (freetext) + return handleFreeTextInput(event); + } + break; + + // (Other states can be added here as needed) + default: + break; + } + + // If no state handler above processed the event, let the message selector try to handle it + // (Handles up/down/select on canned message list, exit/return) + if (handleMessageSelectorInput(event, isUp, isDown, isSelect)) return 1; + + // Default: event not handled by canned message system, allow others to process return 0; } diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index 112ae8260..7511bab31 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -78,6 +78,7 @@ public: void resetSearch(); void updateFilteredNodes(); bool isInterceptingAndFocused(); + bool isCharInputAllowed() const; String drawWithCursor(String text, int cursor); // === Admin Handlers === From 7c9ec46d8fc0ff337298932df6dcf049e957cdb1 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 26 May 2025 18:53:14 -0400 Subject: [PATCH 167/265] move destination option to top of canned message screen --- src/modules/CannedMessageModule.cpp | 45 ++++++++++++++++++----------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index f13254cf9..7de50f8ad 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -96,35 +96,46 @@ int CannedMessageModule::splitConfiguredMessages() // Copy all message parts into the buffer strncpy(this->messageStore, canned_messages.c_str(), sizeof(this->messageStore)); - // First message points to start of buffer - this->messages[messageIndex++] = this->messageStore; + // Temporary array to allow for insertion + const char* tempMessages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT + 3] = {0}; + int tempCount = 0; + + // First message always starts at buffer start + tempMessages[tempCount++] = this->messageStore; int upTo = strlen(this->messageStore) - 1; // Walk buffer, splitting on '|' while (i < upTo) { if (this->messageStore[i] == '|') { this->messageStore[i] = '\0'; // End previous message - - // Stop if we've hit max message slots - if (messageIndex >= CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT) { - this->messagesCount = messageIndex; - return this->messagesCount; - } - - // Point to the next message start - this->messages[messageIndex++] = (this->messageStore + i + 1); + if (tempCount >= CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT) + break; + tempMessages[tempCount++] = (this->messageStore + i + 1); } i += 1; } - // Always add "[Select Destination]" next-to-last - this->messages[messageIndex++] = (char*)"[Select Destination]"; + // Insert "[Select Destination]" after Free Text if present, otherwise at the top +#if defined(USE_VIRTUAL_KEYBOARD) + // Insert at position 1 (after Free Text) + for (int j = tempCount; j > 1; j--) tempMessages[j] = tempMessages[j - 1]; + tempMessages[1] = "[Select Destination]"; + tempCount++; +#else + // Insert at position 0 (top) + for (int j = tempCount; j > 0; j--) tempMessages[j] = tempMessages[j - 1]; + tempMessages[0] = "[Select Destination]"; + tempCount++; +#endif - // === Add [Exit] as the final entry in the list === - this->messages[messageIndex++] = (char*)"[Exit]"; + // Add [Exit] as the last entry + tempMessages[tempCount++] = "[Exit]"; - // Record how many messages there are - this->messagesCount = messageIndex; + // Copy to the member array + for (int k = 0; k < tempCount; ++k) { + this->messages[k] = (char*)tempMessages[k]; + } + this->messagesCount = tempCount; return this->messagesCount; } From aaf9f4011fadc87352538ee0545eadc14858c31d Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 26 May 2025 23:45:28 -0400 Subject: [PATCH 168/265] Bluetooth dissable icon --- src/graphics/Screen.cpp | 7 ++++--- src/graphics/images.h | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 71047ace3..0e88a40b3 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2708,9 +2708,10 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i // === Fifth Row: Bluetooth Off Icon === if (!config.bluetooth.enabled) { - const int iconX = 2; // Left aligned - const int iconY = compactFifthLine; - display->drawXbm(iconX, iconY, placeholder_width, placeholder_height, placeholder); + const int iconX = 0; // Left aligned + const int iconY = compactFifthLine + ((SCREEN_WIDTH > 128) ? 42 : 2); + display->drawXbm(iconX, iconY, bluetoothdisabled_width, bluetoothdisabled_height, bluetoothdisabled); + display->drawLine(iconX, iconY, iconX + 9, iconY + 5); } } diff --git a/src/graphics/images.h b/src/graphics/images.h index 9e421dba9..549d7714a 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -281,4 +281,18 @@ static const uint8_t icon_node[] PROGMEM = { 0xFE // ####### ← device base }; +#define bluetoothdisabled_width 8 +#define bluetoothdisabled_height 8 +const uint8_t bluetoothdisabled[] PROGMEM = { + 0b11101100, + 0b01010100, + 0b01001100, + 0b01010100, + 0b01001100, + 0b00000000, + 0b00000000, + 0b00000000 +}; + #include "img/icon.xbm" +static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning"); \ No newline at end of file From bdd03bc8532246d87b2324ad711e311eaa8989fc Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Mon, 26 May 2025 23:45:53 -0400 Subject: [PATCH 169/265] Fix to but that takes you to message selection screen --- src/modules/CannedMessageModule.cpp | 109 +++++++++++++++------------- 1 file changed, 59 insertions(+), 50 deletions(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 7de50f8ad..bab932c13 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -533,64 +533,72 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent* event, bo } bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) { -#if defined(USE_VIRTUAL_KEYBOARD) + // Always process only if in FREETEXT mode if (runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) return false; - String keyTapped = keyForCoordinates(event->touchX, event->touchY); - bool valid = false; +#if defined(USE_VIRTUAL_KEYBOARD) + // Touch input (virtual keyboard) handling + // Only handle if touch coordinates present (CardKB won't set these) + if (event->touchX != 0 || event->touchY != 0) { + String keyTapped = keyForCoordinates(event->touchX, event->touchY); + bool valid = false; - if (keyTapped == "⇧") { - highlight = -1; - payload = 0x00; - shift = !shift; - valid = true; - } else if (keyTapped == "⌫") { -#ifndef RAK14014 - highlight = keyTapped[0]; -#endif - payload = 0x08; - shift = false; - valid = true; - } else if (keyTapped == "123" || keyTapped == "ABC") { - highlight = -1; - payload = 0x00; - charSet = (charSet == 0 ? 1 : 0); - valid = true; - } else if (keyTapped == " ") { -#ifndef RAK14014 - highlight = keyTapped[0]; -#endif - payload = keyTapped[0]; - shift = false; - valid = true; - } - // Touch enter/submit - else if (keyTapped == "↡") { - runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; // Send the message! - payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; - currentMessageIndex = -1; - shift = false; - valid = true; - } else if (!keyTapped.isEmpty()) { -#ifndef RAK14014 - highlight = keyTapped[0]; -#endif - payload = shift ? keyTapped[0] : std::tolower(keyTapped[0]); - shift = false; - valid = true; + if (keyTapped == "⇧") { + highlight = -1; + payload = 0x00; + shift = !shift; + valid = true; + } else if (keyTapped == "⌫") { + #ifndef RAK14014 + highlight = keyTapped[0]; + #endif + payload = 0x08; + shift = false; + valid = true; + } else if (keyTapped == "123" || keyTapped == "ABC") { + highlight = -1; + payload = 0x00; + charSet = (charSet == 0 ? 1 : 0); + valid = true; + } else if (keyTapped == " ") { + #ifndef RAK14014 + highlight = keyTapped[0]; + #endif + payload = keyTapped[0]; + shift = false; + valid = true; + } + // Touch enter/submit + else if (keyTapped == "↡") { + runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; // Send the message! + payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; + currentMessageIndex = -1; + shift = false; + valid = true; + } else if (!keyTapped.isEmpty()) { + #ifndef RAK14014 + highlight = keyTapped[0]; + #endif + payload = shift ? keyTapped[0] : std::tolower(keyTapped[0]); + shift = false; + valid = true; + } + + if (valid) { + lastTouchMillis = millis(); + return true; // STOP: We handled a VKB touch + } } +#endif // USE_VIRTUAL_KEYBOARD - if (valid) { - lastTouchMillis = millis(); - return true; - } -#endif + // ---- All hardware keys fall through to here (CardKB, physical, etc.) ---- + // Confirm select (Enter) bool isSelect = isSelectEvent(event); - - if (runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && isSelect) { + if (isSelect) { + LOG_DEBUG("[SELECT] handleFreeTextInput: runState=%d, dest=%u, channel=%d, freetext='%s'", + (int)runState, dest, channel, freetext.c_str()); if (dest == 0) dest = NODENUM_BROADCAST; - // Defensive: If channel isn't valid, pick the first available channel if (channel < 0 || channel >= channels.getNumChannels()) channel = 0; @@ -598,6 +606,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) { currentMessageIndex = -1; runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; lastTouchMillis = millis(); + runOnce(); return true; } From 03b59ae39aa42f72e331cc250c4ba2e9dc4118c0 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Tue, 27 May 2025 17:18:18 -0500 Subject: [PATCH 170/265] Key verification implementation (#6892) * Very rough start on key verification routine * KeyVerification mostly working * Actually working * Properly hide KeyVerification behind PKI enabled defines * Change to avoid admin.admin * Update src/modules/KeyVerificationModule.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Ben Meadors Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/modules/KeyVerificationModule.cpp | 306 ++++++++++++++++++++++++++ src/modules/KeyVerificationModule.h | 64 ++++++ src/modules/Modules.cpp | 7 +- 3 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 src/modules/KeyVerificationModule.cpp create mode 100644 src/modules/KeyVerificationModule.h diff --git a/src/modules/KeyVerificationModule.cpp b/src/modules/KeyVerificationModule.cpp new file mode 100644 index 000000000..062d79b0f --- /dev/null +++ b/src/modules/KeyVerificationModule.cpp @@ -0,0 +1,306 @@ +#if !MESHTASTIC_EXCLUDE_PKI +#include "KeyVerificationModule.h" +#include "MeshService.h" +#include "RTC.h" +#include "main.h" +#include "modules/AdminModule.h" +#include + +KeyVerificationModule *keyVerificationModule; + +KeyVerificationModule::KeyVerificationModule() + : ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) +{ + ourPortNum = meshtastic_PortNum_KEY_VERIFICATION_APP; +} + +AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) +{ + updateState(); + if (request->which_payload_variant == meshtastic_AdminMessage_key_verification_tag && mp.from == 0) { + LOG_WARN("Handling Key Verification Admin Message type %u", request->key_verification.message_type); + + if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_INITIATE_VERIFICATION && + currentState == KEY_VERIFICATION_IDLE) { + sendInitialRequest(request->key_verification.remote_nodenum); + + } else if (request->key_verification.message_type == + meshtastic_KeyVerificationAdmin_MessageType_PROVIDE_SECURITY_NUMBER && + request->key_verification.has_security_number && currentState == KEY_VERIFICATION_SENDER_AWAITING_NUMBER && + request->key_verification.nonce == currentNonce) { + processSecurityNumber(request->key_verification.security_number); + + } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY && + request->key_verification.nonce == currentNonce) { + auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); + remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; + resetToIdle(); + } else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) { + resetToIdle(); + } + return AdminMessageHandleResult::HANDLED; + } + return AdminMessageHandleResult::NOT_HANDLED; +} + +bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r) +{ + updateState(); + if (mp.pki_encrypted == false) + return false; + if (mp.from != currentRemoteNode) // because the inital connection request is handled in allocReply() + return false; + if (currentState == KEY_VERIFICATION_IDLE) { + return false; // if we're idle, the only acceptable message is an init, which should be handled by allocReply() + + } else if (currentState == KEY_VERIFICATION_SENDER_HAS_INITIATED && r->nonce == currentNonce && r->hash2.size == 32 && + r->hash1.size == 0) { + memcpy(hash2, r->hash2.bytes, 32); + if (screen) + screen->startAlert("Enter Security Number"); // TODO: replace with actual prompt in BaseUI + + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + sprintf(cn->message, "Enter Security Number for Key Verification"); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_request_tag; + cn->payload_variant.key_verification_number_request.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_number_request.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + sizeof(cn->payload_variant.key_verification_number_request.remote_longname)); + service->sendClientNotification(cn); + LOG_INFO("Received hash2"); + currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER; + return true; + + } else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && r->hash1.size == 32 && r->nonce == currentNonce) { + if (memcmp(hash1, r->hash1.bytes, 32) == 0) { + memset(message, 0, sizeof(message)); + sprintf(message, "Verification: \n"); + generateVerificationCode(message + 15); + LOG_INFO("Hash1 matches!"); + if (screen) { + screen->endAlert(); + screen->startAlert(message); + } + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + sprintf(cn->message, "Final confirmation for incoming manual key verification %s", message); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; + cn->payload_variant.key_verification_final.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + sizeof(cn->payload_variant.key_verification_final.remote_longname)); + cn->payload_variant.key_verification_final.isSender = false; + service->sendClientNotification(cn); + + currentState = KEY_VERIFICATION_RECEIVER_AWAITING_USER; + return true; + } + } + return false; +} + +bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode) +{ + LOG_DEBUG("keyVerification start"); + // generate nonce + updateState(); + if (currentState != KEY_VERIFICATION_IDLE) { + return false; + } + currentNonce = random(); + currentNonceTimestamp = getTime(); + currentRemoteNode = remoteNode; + meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero; + KeyVerification.nonce = currentNonce; + KeyVerification.hash2.size = 0; + KeyVerification.hash1.size = 0; + meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification); + p->to = remoteNode; + p->channel = 0; + p->pki_encrypted = true; + p->decoded.want_response = true; + p->priority = meshtastic_MeshPacket_Priority_HIGH; + service->sendToMesh(p, RX_SRC_LOCAL, true); + + currentState = KEY_VERIFICATION_SENDER_HAS_INITIATED; + return true; +} + +meshtastic_MeshPacket *KeyVerificationModule::allocReply() +{ + SHA256 hash; + NodeNum ourNodeNum = nodeDB->getNodeNum(); + updateState(); + if (currentState != KEY_VERIFICATION_IDLE) { // TODO: cooldown period + LOG_WARN("Key Verification requested, but already in a request"); + return nullptr; + } else if (!currentRequest->pki_encrypted) { + LOG_WARN("Key Verification requested, but not in a PKI packet"); + return nullptr; + } + currentState = KEY_VERIFICATION_RECEIVER_AWAITING_HASH1; + + auto req = *currentRequest; + const auto &p = req.decoded; + meshtastic_KeyVerification scratch; + meshtastic_KeyVerification response; + meshtastic_MeshPacket *responsePacket = nullptr; + pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_KeyVerification_msg, &scratch); + + currentNonce = scratch.nonce; + response.nonce = scratch.nonce; + currentRemoteNode = req.from; + currentNonceTimestamp = getTime(); + currentSecurityNumber = random(1, 999999); + + // generate hash1 + hash.reset(); + hash.update(¤tSecurityNumber, sizeof(currentSecurityNumber)); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(¤tRemoteNode, sizeof(currentRemoteNode)); + hash.update(&ourNodeNum, sizeof(ourNodeNum)); + hash.update(currentRequest->public_key.bytes, currentRequest->public_key.size); + hash.update(owner.public_key.bytes, owner.public_key.size); + hash.finalize(hash1, 32); + + // generate hash2 + hash.reset(); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(hash1, 32); + hash.finalize(hash2, 32); + response.hash1.size = 0; + response.hash2.size = 32; + memcpy(response.hash2.bytes, hash2, 32); + + responsePacket = allocDataProtobuf(response); + + responsePacket->pki_encrypted = true; + if (screen) { + snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000); + screen->startAlert(message); + LOG_WARN("%s", message); + } + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + sprintf(cn->message, "Incoming Key Verification.\nSecurity Number\n%03u %03u", currentSecurityNumber / 1000, + currentSecurityNumber % 1000); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_inform_tag; + cn->payload_variant.key_verification_number_inform.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_number_inform.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + sizeof(cn->payload_variant.key_verification_number_inform.remote_longname)); + cn->payload_variant.key_verification_number_inform.security_number = currentSecurityNumber; + service->sendClientNotification(cn); + LOG_WARN("Security Number %04u", currentSecurityNumber); + return responsePacket; +} + +void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber) +{ + SHA256 hash; + NodeNum ourNodeNum = nodeDB->getNodeNum(); + uint8_t scratch_hash[32] = {0}; + LOG_WARN("received security number: %u", incomingNumber); + meshtastic_NodeInfoLite *remoteNodePtr = nullptr; + remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode); + if (remoteNodePtr == nullptr || !remoteNodePtr->has_user || remoteNodePtr->user.public_key.size != 32) { + currentState = KEY_VERIFICATION_IDLE; + return; // should we throw an error here? + } + LOG_WARN("hashing "); + // calculate hash1 + hash.reset(); + hash.update(&incomingNumber, sizeof(incomingNumber)); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(&ourNodeNum, sizeof(ourNodeNum)); + hash.update(¤tRemoteNode, sizeof(currentRemoteNode)); + hash.update(owner.public_key.bytes, owner.public_key.size); + + hash.update(remoteNodePtr->user.public_key.bytes, remoteNodePtr->user.public_key.size); + hash.finalize(hash1, 32); + + hash.reset(); + hash.update(¤tNonce, sizeof(currentNonce)); + hash.update(hash1, 32); + hash.finalize(scratch_hash, 32); + + if (memcmp(scratch_hash, hash2, 32) != 0) { + LOG_WARN("Hash2 did not match"); + return; // should probably throw an error of some sort + } + currentSecurityNumber = incomingNumber; + + meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero; + KeyVerification.nonce = currentNonce; + KeyVerification.hash2.size = 0; + KeyVerification.hash1.size = 32; + memcpy(KeyVerification.hash1.bytes, hash1, 32); + meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification); + p->to = currentRemoteNode; + p->channel = 0; + p->pki_encrypted = true; + p->decoded.want_response = true; + p->priority = meshtastic_MeshPacket_Priority_HIGH; + service->sendToMesh(p, RX_SRC_LOCAL, true); + currentState = KEY_VERIFICATION_SENDER_AWAITING_USER; + memset(message, 0, sizeof(message)); + sprintf(message, "Verification: \n"); + generateVerificationCode(message + 15); // send the toPhone packet + if (screen) { + screen->endAlert(); + screen->startAlert(message); + } + meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); + cn->level = meshtastic_LogRecord_Level_WARNING; + sprintf(cn->message, "Final confirmation for outgoing manual key verification %s", message); + cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; + cn->payload_variant.key_verification_final.nonce = currentNonce; + strncpy(cn->payload_variant.key_verification_final.remote_longname, // should really check for nulls, etc + nodeDB->getMeshNode(currentRemoteNode)->user.long_name, + sizeof(cn->payload_variant.key_verification_final.remote_longname)); + cn->payload_variant.key_verification_final.isSender = true; + service->sendClientNotification(cn); + LOG_INFO(message); + + return; +} + +void KeyVerificationModule::updateState() +{ + if (currentState != KEY_VERIFICATION_IDLE) { + // check for the 30 second timeout + if (currentNonceTimestamp < getTime() - 30) { + resetToIdle(); + } + } +} + +void KeyVerificationModule::resetToIdle() +{ + memset(hash1, 0, 32); + memset(hash2, 0, 32); + currentNonce = 0; + currentNonceTimestamp = 0; + currentSecurityNumber = 0; + currentRemoteNode = 0; + currentState = KEY_VERIFICATION_IDLE; + if (screen) + screen->endAlert(); +} + +void KeyVerificationModule::generateVerificationCode(char *readableCode) +{ + for (int i = 0; i < 4; i++) { + // drop the two highest significance bits, then encode as a base64 + readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary. + } + readableCode[4] = ' '; + for (int i = 5; i < 9; i++) { + // drop the two highest significance bits, then encode as a base64 + readableCode[i] = (hash1[i] >> 2) + 48; // not a standardized base64, but workable and avoids having a dictionary. + } +} +#endif \ No newline at end of file diff --git a/src/modules/KeyVerificationModule.h b/src/modules/KeyVerificationModule.h new file mode 100644 index 000000000..3dcec9ace --- /dev/null +++ b/src/modules/KeyVerificationModule.h @@ -0,0 +1,64 @@ +#pragma once + +#include "ProtobufModule.h" +#include "SinglePortModule.h" + +enum KeyVerificationState { + KEY_VERIFICATION_IDLE, + KEY_VERIFICATION_SENDER_HAS_INITIATED, + KEY_VERIFICATION_SENDER_AWAITING_NUMBER, + KEY_VERIFICATION_SENDER_AWAITING_USER, + KEY_VERIFICATION_RECEIVER_AWAITING_USER, + KEY_VERIFICATION_RECEIVER_AWAITING_HASH1, +}; + +class KeyVerificationModule : public ProtobufModule //, private concurrency::OSThread // +{ + // CallbackObserver nodeStatusObserver = + // CallbackObserver(this, &KeyVerificationModule::handleStatusUpdate); + + public: + KeyVerificationModule(); + /* : concurrency::OSThread("KeyVerification"), + ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) + { + nodeStatusObserver.observe(&nodeStatus->onNewStatus); + setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent + }*/ + virtual bool wantUIFrame() { return false; }; + bool sendInitialRequest(NodeNum remoteNode); + + protected: + /* Called to handle a particular incoming message + @return true if you've guaranteed you've handled this message and no other handlers should be considered for it + */ + virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *p); + // virtual meshtastic_MeshPacket *allocReply() override; + + // rather than add to the craziness that is the admin module, just handle those requests here. + virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, + meshtastic_AdminMessage *request, + meshtastic_AdminMessage *response) override; + /* + * Send our Telemetry into the mesh + */ + bool sendMetrics(); + virtual meshtastic_MeshPacket *allocReply() override; + + private: + uint64_t currentNonce = 0; + uint32_t currentNonceTimestamp = 0; + NodeNum currentRemoteNode = 0; + uint32_t currentSecurityNumber = 0; + KeyVerificationState currentState = KEY_VERIFICATION_IDLE; + uint8_t hash1[32] = {0}; // + uint8_t hash2[32] = {0}; // + char message[26] = {0}; + + void processSecurityNumber(uint32_t); + void updateState(); // check the timeouts and maybe reset the state to idle + void resetToIdle(); // Zero out module state + void generateVerificationCode(char *); // fills char with the user readable verification code +}; + +extern KeyVerificationModule *keyVerificationModule; \ No newline at end of file diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index bbb20045e..b3b73c44f 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -12,6 +12,9 @@ #endif #include "input/kbMatrixImpl.h" #endif +#if !MESHTASTIC_EXCLUDE_PKI +#include "KeyVerificationModule.h" +#endif #if !MESHTASTIC_EXCLUDE_ADMIN #include "modules/AdminModule.h" #endif @@ -136,7 +139,9 @@ void setupModules() #if !MESHTASTIC_EXCLUDE_ATAK atakPluginModule = new AtakPluginModule(); #endif - +#if !MESHTASTIC_EXCLUDE_PKI + keyVerificationModule = new KeyVerificationModule(); +#endif #if !MESHTASTIC_EXCLUDE_DROPZONE dropzoneModule = new DropzoneModule(); #endif From 051e7331f2b93bee989a8aa286e16bae078ff112 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 28 May 2025 03:04:19 -0400 Subject: [PATCH 171/265] Fix for shutdown banner --- src/modules/CannedMessageModule.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index bab932c13..9e1720c93 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -735,8 +735,8 @@ bool CannedMessageModule::handleSystemCommandInput(const InputEvent* event) { // Power control case INPUT_BROKER_MSG_SHUTDOWN: if (screen) screen->showOverlayBanner("Shutting down..."); - shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; nodeDB->saveToDisk(); + shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; return true; case INPUT_BROKER_MSG_REBOOT: From fda6de2f51dd2a31867e49bd67740ac05c5ce44f Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 28 May 2025 21:41:44 -0400 Subject: [PATCH 172/265] Favorite Node Info screens --- src/graphics/Screen.cpp | 257 +++++++++++++++++++++++---------- src/graphics/SharedUIDisplay.h | 7 + 2 files changed, 186 insertions(+), 78 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 0e88a40b3..5396cd1ed 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1864,33 +1864,35 @@ uint16_t Screen::getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) return diam - 20; }; -// ********************* -// * Node Info * -// ********************* +// ********************** +// * Favorite Node Info * +// ********************** static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + // --- Cache favorite nodes for the current frame only, to save computation --- static std::vector favoritedNodes; static int prevFrame = -1; + // --- Only rebuild favorites list if we're on a new frame --- if (state->currentFrame != prevFrame) { prevFrame = state->currentFrame; - favoritedNodes.clear(); size_t total = nodeDB->getNumMeshNodes(); for (size_t i = 0; i < total; i++) { meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + // Skip nulls and ourself if (!n || n->num == nodeDB->getNodeNum()) continue; if (n->is_favorite) favoritedNodes.push_back(n); } - - // Sort favorites by node number to keep consistent order - std::sort(favoritedNodes.begin(), favoritedNodes.end(), [](meshtastic_NodeInfoLite *a, meshtastic_NodeInfoLite *b) { - return a->num < b->num; - }); + // Keep a stable, consistent display order + std::sort(favoritedNodes.begin(), favoritedNodes.end(), + [](meshtastic_NodeInfoLite *a, meshtastic_NodeInfoLite *b) { + return a->num < b->num; + }); } - if (favoritedNodes.empty()) return; + // --- Only display if index is valid --- int nodeIndex = state->currentFrame - (screen->frameCount - favoritedNodes.size()); if (nodeIndex < 0 || nodeIndex >= (int)favoritedNodes.size()) return; @@ -1899,18 +1901,16 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ display->clear(); - // === Header === + // === Draw battery/time/mail header (common across screens) === graphics::drawCommonHeader(display, x, y); - // === Title: Short Name centered in header row === + // === Draw the short node name centered at the top, with bold shadow if set === const int highlightHeight = FONT_HEIGHT_SMALL - 1; const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; const int centerX = x + SCREEN_WIDTH / 2; const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) display->setColor(BLACK); - display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_SMALL); display->drawString(centerX, textY, shortName); @@ -1921,52 +1921,159 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - const char *username = node->has_user ? node->user.long_name : "Unknown Name"; + // ===== DYNAMIC ROW STACKING WITH YOUR MACROS ===== + // 1. Each potential info row has a macro-defined Y position (not regular increments!). + // 2. Each row is only shown if it has valid data. + // 3. Each row "moves up" if previous are empty, so there are never any blank rows. + // 4. The first line is ALWAYS at your macro position; subsequent lines use the next available macro slot. - static char signalStr[20]; - if (node->hops_away > 0) - snprintf(signalStr, sizeof(signalStr), "Hops: %d", node->hops_away); - else - snprintf(signalStr, sizeof(signalStr), "Signal: %d%%", clamp((int)((node->snr + 10) * 5), 0, 100)); + // List of available macro Y positions in order, from top to bottom. + const int yPositions[5] = { + moreCompactFirstLine, + moreCompactSecondLine, + moreCompactThirdLine, + moreCompactFourthLine, + moreCompactFifthLine + }; + int line = 0; // which slot to use next - static char seenStr[20]; - uint32_t seconds = sinceLastSeen(node); - if (seconds == 0 || seconds == UINT32_MAX) { - snprintf(seenStr, sizeof(seenStr), "Heard: ?"); - } else { - uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; - snprintf(seenStr, sizeof(seenStr), (days > 365 ? "Heard: ?" : "Heard: %d%c ago"), - (days ? days - : hours ? hours - : minutes), - (days ? 'd' - : hours ? 'h' - : 'm')); + // === 1. Long Name (always try to show first) === + const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; + if (username && line < 5) { + // Print node's long name (e.g. "Backpack Node") + display->drawString(x, yPositions[line++], username); } - static char distStr[20]; - strncpy(distStr, - (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "? mi ?Β°" : "? km ?Β°", - sizeof(distStr)); + // === 2. Signal and Hops (combined on one line, if available) === + // If both are present: "Signal: 97% [2hops]" + // If only one: show only that one + char signalHopsStr[32] = ""; + bool haveSignal = false; + int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100); - // === First Row: Long Name === - display->drawString(x, compactFirstLine, username); + // Use shorter label if screen is narrow (<= 128 px) + const char *signalLabel = (display->getWidth() > 128) ? "Signal" : "Sig"; - // === Second Row: Last Seen === - display->drawString(x, compactSecondLine, seenStr); + // --- Build the Signal/Hops line --- + // If SNR looks reasonable, show signal + if ((int)((node->snr + 10) * 5) >= 0 && node->snr > -100) { + snprintf(signalHopsStr, sizeof(signalHopsStr), "%s: %d%%", signalLabel, percentSignal); + haveSignal = true; + } + // If hops is valid (>0), show right after signal + if (node->hops_away > 0) { + size_t len = strlen(signalHopsStr); + // Decide between "1 Hop" and "N Hops" + if (haveSignal) { + snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops")); + } else { + snprintf(signalHopsStr, sizeof(signalHopsStr), "[%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops")); + } + } + if (signalHopsStr[0] && line < 5) { + display->drawString(x, yPositions[line++], signalHopsStr); + } - // === Third Row: Signal Strength or Hops === - display->drawString(x, compactThirdLine, signalStr); + // === 3. Heard (last seen, skip if node never seen) === + char seenStr[20] = ""; + uint32_t seconds = sinceLastSeen(node); + if (seconds != 0 && seconds != UINT32_MAX) { + uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; + // Format as "Heard: Xm ago", "Heard: Xh ago", or "Heard: Xd ago" + snprintf(seenStr, sizeof(seenStr), + (days > 365 ? "Heard: ?" : "Heard: %d%c ago"), + (days ? days : hours ? hours : minutes), + (days ? 'd' : hours ? 'h' : 'm')); + } + if (seenStr[0] && line < 5) { + display->drawString(x, yPositions[line++], seenStr); + } - // === Fourth Row: Distance/Bearing === - display->drawString(x, compactFourthLine, distStr); + // === 4. Uptime (only show if metric is present) === + char uptimeStr[32] = ""; + if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { + uint32_t uptime = node->device_metrics.uptime_seconds; + uint32_t days = uptime / 86400; + uint32_t hours = (uptime % 86400) / 3600; + uint32_t mins = (uptime % 3600) / 60; + // Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m" + if (days) + snprintf(uptimeStr, sizeof(uptimeStr), "Uptime: %ud %uh", days, hours); + else if (hours) + snprintf(uptimeStr, sizeof(uptimeStr), "Uptime: %uh %um", hours, mins); + else + snprintf(uptimeStr, sizeof(uptimeStr), "Uptime: %um", mins); + } + if (uptimeStr[0] && line < 5) { + display->drawString(x, yPositions[line++], uptimeStr); + } - // === Compass Rendering (resized like CompassAndLocation screen) === + // === 5. Distance (only if both nodes have GPS position) === meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + char distStr[24] = ""; // Make buffer big enough for any string + bool haveDistance = false; + + if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { + double lat1 = ourNode->position.latitude_i * 1e-7; + double lon1 = ourNode->position.longitude_i * 1e-7; + double lat2 = node->position.latitude_i * 1e-7; + double lon2 = node->position.longitude_i * 1e-7; + double earthRadiusKm = 6371.0; + double dLat = (lat2 - lat1) * DEG_TO_RAD; + double dLon = (lon2 - lon1) * DEG_TO_RAD; + double a = + sin(dLat / 2) * sin(dLat / 2) + + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * + sin(dLon / 2) * sin(dLon / 2); + double c = 2 * atan2(sqrt(a), sqrt(1 - a)); + double distanceKm = earthRadiusKm * c; + + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + double miles = distanceKm * 0.621371; + if (miles < 0.1) { + int feet = (int)(miles * 5280); + if (feet > 0 && feet < 1000) { + snprintf(distStr, sizeof(distStr), "Distance: %dft", feet); + haveDistance = true; + } else if (feet >= 1000) { + snprintf(distStr, sizeof(distStr), "Distance: ΒΌmi"); + haveDistance = true; + } + } else { + int roundedMiles = (int)(miles + 0.5); + if (roundedMiles > 0 && roundedMiles < 1000) { + snprintf(distStr, sizeof(distStr), "Distance: %dmi", roundedMiles); + haveDistance = true; + } + } + } else { + if (distanceKm < 1.0) { + int meters = (int)(distanceKm * 1000); + if (meters > 0 && meters < 1000) { + snprintf(distStr, sizeof(distStr), "Distance: %dm", meters); + haveDistance = true; + } else if (meters >= 1000) { + snprintf(distStr, sizeof(distStr), "Distance: 1km"); + haveDistance = true; + } + } else { + int km = (int)(distanceKm + 0.5); + if (km > 0 && km < 1000) { + snprintf(distStr, sizeof(distStr), "Distance: %dkm", km); + haveDistance = true; + } + } + } + } + // Only display if we actually have a value! + if (haveDistance && distStr[0] && line < 5) { + display->drawString(x, yPositions[line++], distStr); + } + + // --- Compass Rendering (only show if valid heading/bearing) --- const int16_t topY = compactFirstLine; const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); const int16_t usableHeight = bottomY - topY - 5; - int16_t compassRadius = usableHeight / 2; if (compassRadius < 8) compassRadius = 8; @@ -1974,41 +2081,35 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; - bool hasNodeHeading = false; - - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) { - const auto &op = ourNode->position; - float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 - : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - screen->drawCompassNorth(display, compassX, compassY, myHeading); - - if (nodeDB->hasValidPosition(node)) { - hasNodeHeading = true; - const auto &p = node->position; - float d = GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), - DegD(op.latitude_i), DegD(op.longitude_i)); - float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), - DegD(p.latitude_i), DegD(p.longitude_i)); - if (!config.display.compass_north_top) bearing -= myHeading; - - screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearing); - - float bearingDeg = fmodf((bearing < 0 ? bearing + 2 * PI : bearing) * 180 / PI, 360.0f); - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) - snprintf(distStr, sizeof(distStr), d < 2 * MILES_TO_FEET ? "%.0fft %.0fΒ°" : "%.1fmi %.0fΒ°", - d * METERS_TO_FEET / (d < 2 * MILES_TO_FEET ? 1 : MILES_TO_FEET), bearingDeg); - else - snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0fΒ°" : "%.1fkm %.0fΒ°", - d / (d < 2000 ? 1 : 1000), bearingDeg); - } + // Determine if we have valid compass info + bool showCompass = false; + if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { + showCompass = true; } - if (!hasNodeHeading) - display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?"); + if (showCompass) { + // Draw north + const auto &op = ourNode->position; + float myHeading = screen->hasHeading() + ? screen->getHeading() * PI / 180 + : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + screen->drawCompassNorth(display, compassX, compassY, myHeading); - display->drawCircle(compassX, compassY, compassRadius); + // Draw node-to-node bearing + const auto &p = node->position; + float d = GeoCoord::latLongToMeter( + DegD(p.latitude_i), DegD(p.longitude_i), + DegD(op.latitude_i), DegD(op.longitude_i)); + float bearing = GeoCoord::bearing( + DegD(op.latitude_i), DegD(op.longitude_i), + DegD(p.latitude_i), DegD(p.longitude_i)); + if (!config.display.compass_north_top) bearing -= myHeading; + screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearing); + + display->drawCircle(compassX, compassY, compassRadius); + } + // (Else, show nothing) } - // Combined dynamic node list frame cycling through LastHeard, HopSignal, and Distance modes // Uses a single frame and changes data every few seconds (E-Ink variant is separate) diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index 3454d3c54..8d85ff764 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -21,6 +21,13 @@ namespace graphics { #define standardThirdLine (FONT_HEIGHT_SMALL + 1) * 3 #define standardFourthLine (FONT_HEIGHT_SMALL + 1) * 4 +// More Compact line layout +#define moreCompactFirstLine compactFirstLine +#define moreCompactSecondLine (moreCompactFirstLine + (FONT_HEIGHT_SMALL - 5)) +#define moreCompactThirdLine (moreCompactSecondLine + (FONT_HEIGHT_SMALL - 5)) +#define moreCompactFourthLine (moreCompactThirdLine + (FONT_HEIGHT_SMALL - 5)) +#define moreCompactFifthLine (moreCompactFourthLine + (FONT_HEIGHT_SMALL - 5)) + // Quick screen access #define SCREEN_WIDTH display->getWidth() #define SCREEN_HEIGHT display->getHeight() From a66f381a5d842c9c59e8c75b9a9fcd6b3306869b Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Wed, 28 May 2025 23:22:11 -0400 Subject: [PATCH 173/265] Compact lines for Device focus screen --- src/graphics/Screen.cpp | 70 ++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 5396cd1ed..0a6e8f374 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2705,12 +2705,19 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i // === Content below header === + // Determine if we need to show 4 or 5 rows on the screen + int rows = 4; + if (!config.bluetooth.enabled) { + rows = 5; + } + + // === First Row: Region / Channel Utilization and Uptime === bool origBold = config.display.heading_bold; config.display.heading_bold = false; // Display Region and Channel Utilization - drawNodes(display, x + 1, compactFirstLine + 2, nodeStatus, -1, false, "online"); + drawNodes(display, x + 1, ((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)) + 2, nodeStatus, -1, false, "online"); uint32_t uptime = millis() / 1000; char uptimeStr[6]; @@ -2732,7 +2739,7 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i char uptimeFullStr[16]; snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); - display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeFullStr), compactFirstLine, uptimeFullStr); + display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeFullStr), ((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)), uptimeFullStr); config.display.heading_bold = origBold; @@ -2747,9 +2754,9 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i } else { displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } - display->drawString(0, compactSecondLine, displayLine); + display->drawString(0, ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), displayLine); } else { - drawGPS(display, 0, compactSecondLine + 3, gpsStatus); + drawGPS(display, 0, ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)) + 3, gpsStatus); } #endif @@ -2758,13 +2765,18 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i int batV = powerStatus->getBatteryVoltageMv() / 1000; int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; snprintf(batStr, sizeof(batStr), "%01d.%02dV", batV, batCv); - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(batStr), compactSecondLine, batStr); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(batStr), ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), batStr); } else { - display->drawString(x + SCREEN_WIDTH - display->getStringWidth("USB"), compactSecondLine, String("USB")); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth("USB"), ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), String("USB")); } config.display.heading_bold = origBold; + // === Third Row: Bluetooth Off (Only If Actually Off) === + if (!config.bluetooth.enabled) { + display->drawString(0, ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactThirdLine : moreCompactThirdLine)), "BT off"); + } + // === Third & Fourth Rows: Node Identity === int textWidth = 0; int nameX = 0; @@ -2783,36 +2795,26 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i char combinedName[50]; snprintf(combinedName, sizeof(combinedName), "%s (%s)", longName, shortnameble); if(SCREEN_WIDTH - (display->getStringWidth(longName) + display->getStringWidth(shortnameble)) > 10){ - // === Third Row: combinedName Centered === size_t len = strlen(combinedName); if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) { combinedName[len - 3] = '\0'; // Remove the last three characters } textWidth = display->getStringWidth(combinedName); nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, compactThirdLine + yOffset, combinedName); + display->drawString(nameX, ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine)) + yOffset, combinedName); } else { - // === Third Row: LongName Centered === textWidth = display->getStringWidth(longName); nameX = (SCREEN_WIDTH - textWidth) / 2; yOffset = (strcmp(shortnameble, "") == 0) ? 1 : 0; if(yOffset == 1){ yOffset = (SCREEN_WIDTH > 128) ? 0 : 7; } - display->drawString(nameX, compactThirdLine + yOffset, longName); + display->drawString(nameX, ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine)) + yOffset, longName); // === Fourth Row: ShortName Centered === textWidth = display->getStringWidth(shortnameble); nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, compactFourthLine, shortnameble); - } - - // === Fifth Row: Bluetooth Off Icon === - if (!config.bluetooth.enabled) { - const int iconX = 0; // Left aligned - const int iconY = compactFifthLine + ((SCREEN_WIDTH > 128) ? 42 : 2); - display->drawXbm(iconX, iconY, bluetoothdisabled_width, bluetoothdisabled_height, bluetoothdisabled); - display->drawLine(iconX, iconY, iconX + 9, iconY + 5); + display->drawString(nameX, ((rows == 4) ? compactFourthLine : ((SCREEN_HEIGHT > 64) ? compactFifthLine : moreCompactFifthLine)), shortnameble); } } @@ -2978,7 +2980,7 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat config.display.heading_bold = false; String Satelite_String = "Sat:"; - display->drawString(0, compactFirstLine, Satelite_String); + display->drawString(0, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), Satelite_String); String displayLine = ""; if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { if (config.position.fixed_position) { @@ -2986,9 +2988,9 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat } else { displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } - display->drawString(display->getStringWidth(Satelite_String) + 3, compactFirstLine, displayLine); + display->drawString(display->getStringWidth(Satelite_String) + 3, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), displayLine); } else { - drawGPS(display, display->getStringWidth(Satelite_String) + 3, compactFirstLine + 3, gpsStatus); + drawGPS(display, display->getStringWidth(Satelite_String) + 3, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine) + 3, gpsStatus); } config.display.heading_bold = origBold; @@ -3017,28 +3019,26 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat displayLine = "Alt: " + String(geoCoord.getAltitude()) + "m"; if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) displayLine = "Alt: " + String(geoCoord.getAltitude() * METERS_TO_FEET) + "ft"; - display->drawString(x, compactSecondLine, displayLine); + display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine), displayLine); // === Third Row: Latitude === char latStr[32]; snprintf(latStr, sizeof(latStr), "Lat: %.5f", geoCoord.getLatitude() * 1e-7); - display->drawString(x, compactThirdLine, latStr); + display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactThirdLine : moreCompactThirdLine), latStr); // === Fourth Row: Longitude === char lonStr[32]; snprintf(lonStr, sizeof(lonStr), "Lon: %.5f", geoCoord.getLongitude() * 1e-7); - display->drawString(x, compactFourthLine, lonStr); + display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine), lonStr); - if(SCREEN_HEIGHT > 64){ - // === Fifth Row: Date === - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); - char datetimeStr[25]; - bool showTime = false; // set to true for full datetime - formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime); - char fullLine[40]; - snprintf(fullLine, sizeof(fullLine), "Date: %s", datetimeStr); - display->drawString(0, compactFifthLine, fullLine); - } + // === Fifth Row: Date === + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); + char datetimeStr[25]; + bool showTime = false; // set to true for full datetime + formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime); + char fullLine[40]; + snprintf(fullLine, sizeof(fullLine), "Date: %s", datetimeStr); + display->drawString(0, ((SCREEN_HEIGHT > 64) ? compactFifthLine : moreCompactFifthLine), fullLine); } // === Draw Compass if heading is valid === From ac0547ca3e51d7e84509e55805423e2fbd8af38f Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 29 May 2025 01:44:06 -0400 Subject: [PATCH 174/265] Support for Comapass on square and portrait screens --- src/graphics/Screen.cpp | 120 +++++++++++++++++++++++++++------------- 1 file changed, 83 insertions(+), 37 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 0a6e8f374..75b9b818d 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2070,46 +2070,92 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ display->drawString(x, yPositions[line++], distStr); } - // --- Compass Rendering (only show if valid heading/bearing) --- - const int16_t topY = compactFirstLine; - const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); - const int16_t usableHeight = bottomY - topY - 5; - int16_t compassRadius = usableHeight / 2; - if (compassRadius < 8) - compassRadius = 8; - const int16_t compassDiam = compassRadius * 2; - const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; - const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + // --- Compass Rendering: landscape (wide) screens use the original side-aligned logic --- + if (SCREEN_WIDTH > SCREEN_HEIGHT) { + bool showCompass = false; + if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { + showCompass = true; + } + if (showCompass) { + const int16_t topY = compactFirstLine; + const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); + const int16_t usableHeight = bottomY - topY - 5; + int16_t compassRadius = usableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + const int16_t compassDiam = compassRadius * 2; + const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; + const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; - // Determine if we have valid compass info - bool showCompass = false; - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { - showCompass = true; + const auto &op = ourNode->position; + float myHeading = screen->hasHeading() + ? screen->getHeading() * PI / 180 + : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + screen->drawCompassNorth(display, compassX, compassY, myHeading); + + const auto &p = node->position; + float d = GeoCoord::latLongToMeter( + DegD(p.latitude_i), DegD(p.longitude_i), + DegD(op.latitude_i), DegD(op.longitude_i)); + float bearing = GeoCoord::bearing( + DegD(op.latitude_i), DegD(op.longitude_i), + DegD(p.latitude_i), DegD(p.longitude_i)); + if (!config.display.compass_north_top) bearing -= myHeading; + screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearing); + + display->drawCircle(compassX, compassY, compassRadius); + } + // else show nothing + } else { + // Portrait or square: put compass at the bottom and centered, scaled to fit available space + bool showCompass = false; + if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { + showCompass = true; + } + if (showCompass) { + int yBelowContent = (line > 0 && line <= 5) ? (yPositions[line - 1] + FONT_HEIGHT_SMALL + 2) : moreCompactFirstLine; + const int margin = 4; + // --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) ----------- + #if defined(USE_EINK) + const int iconSize = (SCREEN_WIDTH > 128) ? 16 : 8; + const int navBarHeight = iconSize + 6; + #else + const int navBarHeight = 0; + #endif + int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; + // --------- END PATCH FOR EINK NAV BAR ----------- + + if (availableHeight < FONT_HEIGHT_SMALL * 2) return; + + int compassRadius = availableHeight / 2; + if (compassRadius < 8) compassRadius = 8; + if (compassRadius * 2 > SCREEN_WIDTH - 16) compassRadius = (SCREEN_WIDTH - 16) / 2; + + int compassX = x + SCREEN_WIDTH / 2; + int compassY = yBelowContent + availableHeight / 2; + + const auto &op = ourNode->position; + float myHeading = screen->hasHeading() + ? screen->getHeading() * PI / 180 + : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + screen->drawCompassNorth(display, compassX, compassY, myHeading); + + const auto &p = node->position; + float d = GeoCoord::latLongToMeter( + DegD(p.latitude_i), DegD(p.longitude_i), + DegD(op.latitude_i), DegD(op.longitude_i)); + float bearing = GeoCoord::bearing( + DegD(op.latitude_i), DegD(op.longitude_i), + DegD(p.latitude_i), DegD(p.longitude_i)); + if (!config.display.compass_north_top) bearing -= myHeading; + screen->drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); + + display->drawCircle(compassX, compassY, compassRadius); + } + // else show nothing } - - if (showCompass) { - // Draw north - const auto &op = ourNode->position; - float myHeading = screen->hasHeading() - ? screen->getHeading() * PI / 180 - : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - screen->drawCompassNorth(display, compassX, compassY, myHeading); - - // Draw node-to-node bearing - const auto &p = node->position; - float d = GeoCoord::latLongToMeter( - DegD(p.latitude_i), DegD(p.longitude_i), - DegD(op.latitude_i), DegD(op.longitude_i)); - float bearing = GeoCoord::bearing( - DegD(op.latitude_i), DegD(op.longitude_i), - DegD(p.latitude_i), DegD(p.longitude_i)); - if (!config.display.compass_north_top) bearing -= myHeading; - screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearing); - - display->drawCircle(compassX, compassY, compassRadius); - } - // (Else, show nothing) } + // Combined dynamic node list frame cycling through LastHeard, HopSignal, and Distance modes // Uses a single frame and changes data every few seconds (E-Ink variant is separate) From 69a08cb69d2a0422407dd9f2e722711cd7a061ce Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 29 May 2025 01:57:59 -0400 Subject: [PATCH 175/265] GPS compass now supports portrait and square displays --- src/graphics/Screen.cpp | 92 +++++++++++++++++++++++++++++------------ 1 file changed, 66 insertions(+), 26 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 75b9b818d..066ea2b41 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -3089,37 +3089,77 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat // === Draw Compass if heading is valid === if (validHeading) { - const int16_t topY = compactFirstLine; - const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height - const int16_t usableHeight = bottomY - topY - 5; + // --- Compass Rendering: landscape (wide) screens use original side-aligned logic --- + if (SCREEN_WIDTH > SCREEN_HEIGHT) { + const int16_t topY = compactFirstLine; + const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height + const int16_t usableHeight = bottomY - topY - 5; - int16_t compassRadius = usableHeight / 2; - if (compassRadius < 8) - compassRadius = 8; - const int16_t compassDiam = compassRadius * 2; - const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; + int16_t compassRadius = usableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + const int16_t compassDiam = compassRadius * 2; + const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; - // Center vertically and nudge down slightly to keep "N" clear of header - const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + // Center vertically and nudge down slightly to keep "N" clear of header + const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; - // Draw compass - screen->drawNodeHeading(display, compassX, compassY, compassDiam, -heading); - display->drawCircle(compassX, compassY, compassRadius); + screen->drawNodeHeading(display, compassX, compassY, compassDiam, -heading); + display->drawCircle(compassX, compassY, compassRadius); - // "N" label - float northAngle = -heading; - float radius = compassRadius; - int16_t nX = compassX + (radius - 1) * sin(northAngle); - int16_t nY = compassY - (radius - 1) * cos(northAngle); - int16_t nLabelWidth = display->getStringWidth("N") + 2; - int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; + // "N" label + float northAngle = -heading; + float radius = compassRadius; + int16_t nX = compassX + (radius - 1) * sin(northAngle); + int16_t nY = compassY - (radius - 1) * cos(northAngle); + int16_t nLabelWidth = display->getStringWidth("N") + 2; + int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; - display->setColor(BLACK); - display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); - display->setColor(WHITE); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + display->setColor(BLACK); + display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); + display->setColor(WHITE); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + } else { + // Portrait or square: put compass at the bottom and centered, scaled to fit available space + // For E-Ink screens, account for navigation bar at the bottom! + int yBelowContent = ((SCREEN_HEIGHT > 64) ? compactFifthLine : moreCompactFifthLine) + FONT_HEIGHT_SMALL + 2; + const int margin = 4; + int availableHeight = +#if defined(USE_EINK) + SCREEN_HEIGHT - yBelowContent - 24; // Leave extra space for nav bar on E-Ink +#else + SCREEN_HEIGHT - yBelowContent - margin; +#endif + + if (availableHeight < FONT_HEIGHT_SMALL * 2) return; + + int compassRadius = availableHeight / 2; + if (compassRadius < 8) compassRadius = 8; + if (compassRadius * 2 > SCREEN_WIDTH - 16) compassRadius = (SCREEN_WIDTH - 16) / 2; + + int compassX = x + SCREEN_WIDTH / 2; + int compassY = yBelowContent + availableHeight / 2; + + screen->drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading); + display->drawCircle(compassX, compassY, compassRadius); + + // "N" label + float northAngle = -heading; + float radius = compassRadius; + int16_t nX = compassX + (radius - 1) * sin(northAngle); + int16_t nY = compassY - (radius - 1) * cos(northAngle); + int16_t nLabelWidth = display->getStringWidth("N") + 2; + int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; + + display->setColor(BLACK); + display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); + display->setColor(WHITE); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + } } #endif } From 303006e1df9f1ff5b589d5e467ab4bbc1213050b Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 29 May 2025 02:31:09 -0400 Subject: [PATCH 176/265] Indents indents indents --- src/graphics/Screen.cpp | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 066ea2b41..8cc768399 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1945,14 +1945,14 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ } // === 2. Signal and Hops (combined on one line, if available) === - // If both are present: "Signal: 97% [2hops]" + // If both are present: "Sig: 97% [2hops]" // If only one: show only that one char signalHopsStr[32] = ""; bool haveSignal = false; int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100); - // Use shorter label if screen is narrow (<= 128 px) - const char *signalLabel = (display->getWidth() > 128) ? "Signal" : "Sig"; + // Always use "Sig" for the label + const char *signalLabel = " Sig"; // --- Build the Signal/Hops line --- // If SNR looks reasonable, show signal @@ -1981,7 +1981,7 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; // Format as "Heard: Xm ago", "Heard: Xh ago", or "Heard: Xd ago" snprintf(seenStr, sizeof(seenStr), - (days > 365 ? "Heard: ?" : "Heard: %d%c ago"), + (days > 365 ? " Heard: ?" : " Heard: %d%c ago"), (days ? days : hours ? hours : minutes), (days ? 'd' : hours ? 'h' : 'm')); } @@ -1998,11 +1998,11 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ uint32_t mins = (uptime % 3600) / 60; // Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m" if (days) - snprintf(uptimeStr, sizeof(uptimeStr), "Uptime: %ud %uh", days, hours); + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %ud %uh", days, hours); else if (hours) - snprintf(uptimeStr, sizeof(uptimeStr), "Uptime: %uh %um", hours, mins); + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %uh %um", hours, mins); else - snprintf(uptimeStr, sizeof(uptimeStr), "Uptime: %um", mins); + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %um", mins); } if (uptimeStr[0] && line < 5) { display->drawString(x, yPositions[line++], uptimeStr); @@ -2033,16 +2033,16 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ if (miles < 0.1) { int feet = (int)(miles * 5280); if (feet > 0 && feet < 1000) { - snprintf(distStr, sizeof(distStr), "Distance: %dft", feet); + snprintf(distStr, sizeof(distStr), " Distance: %dft", feet); haveDistance = true; } else if (feet >= 1000) { - snprintf(distStr, sizeof(distStr), "Distance: ΒΌmi"); + snprintf(distStr, sizeof(distStr), " Distance: ΒΌmi"); haveDistance = true; } } else { int roundedMiles = (int)(miles + 0.5); if (roundedMiles > 0 && roundedMiles < 1000) { - snprintf(distStr, sizeof(distStr), "Distance: %dmi", roundedMiles); + snprintf(distStr, sizeof(distStr), " Distance: %dmi", roundedMiles); haveDistance = true; } } @@ -2050,16 +2050,16 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ if (distanceKm < 1.0) { int meters = (int)(distanceKm * 1000); if (meters > 0 && meters < 1000) { - snprintf(distStr, sizeof(distStr), "Distance: %dm", meters); + snprintf(distStr, sizeof(distStr), " Distance: %dm", meters); haveDistance = true; } else if (meters >= 1000) { - snprintf(distStr, sizeof(distStr), "Distance: 1km"); + snprintf(distStr, sizeof(distStr), " Distance: 1km"); haveDistance = true; } } else { int km = (int)(distanceKm + 0.5); if (km > 0 && km < 1000) { - snprintf(distStr, sizeof(distStr), "Distance: %dkm", km); + snprintf(distStr, sizeof(distStr), " Distance: %dkm", km); haveDistance = true; } } @@ -3062,19 +3062,19 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat // === Second Row: Altitude === String displayLine; - displayLine = "Alt: " + String(geoCoord.getAltitude()) + "m"; + displayLine = " Alt: " + String(geoCoord.getAltitude()) + "m"; if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) - displayLine = "Alt: " + String(geoCoord.getAltitude() * METERS_TO_FEET) + "ft"; + displayLine = " Alt: " + String(geoCoord.getAltitude() * METERS_TO_FEET) + "ft"; display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine), displayLine); // === Third Row: Latitude === char latStr[32]; - snprintf(latStr, sizeof(latStr), "Lat: %.5f", geoCoord.getLatitude() * 1e-7); + snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7); display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactThirdLine : moreCompactThirdLine), latStr); // === Fourth Row: Longitude === char lonStr[32]; - snprintf(lonStr, sizeof(lonStr), "Lon: %.5f", geoCoord.getLongitude() * 1e-7); + snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7); display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine), lonStr); // === Fifth Row: Date === @@ -3083,7 +3083,7 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat bool showTime = false; // set to true for full datetime formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime); char fullLine[40]; - snprintf(fullLine, sizeof(fullLine), "Date: %s", datetimeStr); + snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr); display->drawString(0, ((SCREEN_HEIGHT > 64) ? compactFifthLine : moreCompactFifthLine), fullLine); } From 69058002d720c3bf9a0aa1b15423af890f0c9624 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 29 May 2025 11:34:22 -0400 Subject: [PATCH 177/265] Eink Cycling code moving frames --- src/graphics/Screen.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 8cc768399..af75873c7 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -3495,6 +3495,8 @@ void NavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) if (currentFrame != lastFrameIndex) { lastFrameIndex = currentFrame; lastFrameChangeTime = millis(); + + EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); } const bool useBigIcons = (SCREEN_WIDTH > 128); @@ -3516,6 +3518,8 @@ void NavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) // Only show bar briefly after switching frames (unless on E-Ink) #if defined(USE_EINK) int y = SCREEN_HEIGHT - iconSize - 1; + + EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); #else int y = SCREEN_HEIGHT - iconSize - 1; if (millis() - lastFrameChangeTime > ICON_DISPLAY_DURATION_MS) { From 699e1a15b3cbd9a005e64b4fa7a78890ce94a11b Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 29 May 2025 13:48:30 -0400 Subject: [PATCH 178/265] Fix horizontal battery for EInk and adjust favorite node notation on node lists --- src/graphics/Screen.cpp | 43 ++++++++++++++++++++++++-------- src/graphics/SharedUIDisplay.cpp | 2 +- src/graphics/images.h | 13 ++++++++++ 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index af75873c7..595a597a4 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2230,8 +2230,6 @@ String getSafeNodeName(meshtastic_NodeInfoLite *node) nodeName = String(idStr); } } - if (node->is_favorite) - nodeName = "*" + nodeName; return nodeName; } @@ -2425,7 +2423,14 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawString(x, y, nodeName); + display->drawString(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nodeName); + if (node->is_favorite){ + if(SCREEN_WIDTH > 128){ + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } + } int rightEdge = x + columnWidth - timeOffset; int textWidth = display->getStringWidth(timeStr); @@ -2450,7 +2455,15 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x, y, nameMaxWidth, nodeName); + + display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nameMaxWidth, nodeName); + if (node->is_favorite){ + if(SCREEN_WIDTH > 128){ + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } + } char hopStr[6] = ""; if (node->has_hops_away && node->hops_away > 0) @@ -2536,7 +2549,14 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x, y, nameMaxWidth, nodeName); + display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nameMaxWidth, nodeName); + if (node->is_favorite){ + if(SCREEN_WIDTH > 128){ + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } + } if (strlen(distStr) > 0) { int offset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) @@ -2660,7 +2680,14 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x, y, nameMaxWidth, nodeName); + display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nameMaxWidth, nodeName); + if (node->is_favorite){ + if(SCREEN_WIDTH > 128){ + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } + } } void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, double userLat, double userLon) @@ -3495,8 +3522,6 @@ void NavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) if (currentFrame != lastFrameIndex) { lastFrameIndex = currentFrame; lastFrameChangeTime = millis(); - - EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); } const bool useBigIcons = (SCREEN_WIDTH > 128); @@ -3518,8 +3543,6 @@ void NavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) // Only show bar briefly after switching frames (unless on E-Ink) #if defined(USE_EINK) int y = SCREEN_HEIGHT - iconSize - 1; - - EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); #else int y = SCREEN_HEIGHT - iconSize - 1; if (millis() - lastFrameChangeTime > ICON_DISPLAY_DURATION_MS) { diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index c99f3ad20..594b2dcf5 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -84,7 +84,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) } #endif - bool useHorizontalBattery = (screenW > 128 && screenW > screenH); + bool useHorizontalBattery = (screenW > 128 && screenW >= screenH); const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2; // === Battery Icons === diff --git a/src/graphics/images.h b/src/graphics/images.h index 549d7714a..bd6e19a17 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -294,5 +294,18 @@ const uint8_t bluetoothdisabled[] PROGMEM = { 0b00000000 }; +#define smallbulletpoint_width 8 +#define smallbulletpoint_height 8 +const uint8_t smallbulletpoint[] PROGMEM = { + 0b00000011, + 0b00000011, + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000 +}; + #include "img/icon.xbm" static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning"); \ No newline at end of file From 53f2f615b22aa27165f1c5751c0121903c261eed Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 30 May 2025 00:56:19 -0400 Subject: [PATCH 179/265] Tdeck fixes and cursor support added --- src/modules/CannedMessageModule.cpp | 160 ++++++++++++++-------------- 1 file changed, 78 insertions(+), 82 deletions(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 9e1720c93..0759da668 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -82,7 +82,6 @@ bool hasKeyForNode(const meshtastic_NodeInfoLite* node) { int CannedMessageModule::splitConfiguredMessages() { - int messageIndex = 0; int i = 0; String canned_messages = cannedMessageModuleConfig.messages; @@ -272,17 +271,47 @@ int CannedMessageModule::handleInputEvent(const InputEvent* event) { // If sending, block all input except global/system (handled above) case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE: - return 1; // Swallow all + return 1; - // If inactive: allow select to advance frame, or char to open free text input case CANNED_MESSAGE_RUN_STATE_INACTIVE: if (isSelect) { - // Remap select to right (frame advance), let screen navigation handle it - const_cast(event)->inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT); - const_cast(event)->kbchar = INPUT_BROKER_MSG_RIGHT; - return 0; + // When inactive, call the onebutton shortpress instead. Activate module only on up/down + powerFSM.trigger(EVENT_PRESS); + return 1; // Let caller know we handled it } - // Printable char (ASCII) opens free text compose; then let the handler process the event + // Let LEFT/RIGHT pass through so frame navigation works + if ( + event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT) || + event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT) + ) { + break; + } + // Handle UP/DOWN: activate canned message list! + if ( + event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP) || + event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN) + ) { + // Always select the first real canned message on activation + int firstRealMsgIdx = 0; + for (int i = 0; i < messagesCount; ++i) { + if (strcmp(messages[i], "[Select Destination]") != 0 && + strcmp(messages[i], "[Exit]") != 0 && + strcmp(messages[i], "[---- Free Text ----]") != 0) { + firstRealMsgIdx = i; + break; + } + } + currentMessageIndex = firstRealMsgIdx; + + // This triggers the canned message list + runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return 1; + } + // Printable char (ASCII) opens free text compose if (event->kbchar >= 32 && event->kbchar <= 126) { runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; requestFocus(); @@ -349,8 +378,11 @@ bool CannedMessageModule::handleTabSwitch(const InputEvent* event) { int CannedMessageModule::handleDestinationSelectionInput(const InputEvent* event, bool isUp, bool isDown, bool isSelect) { static bool shouldRedraw = false; - // Handle character input for search - if (event->kbchar >= 32 && event->kbchar <= 126) { + if (event->kbchar >= 32 && event->kbchar <= 126 && + !isUp && !isDown && + event->inputEvent != static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT) && + event->inputEvent != static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT) && + event->inputEvent != static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) { this->searchQuery += event->kbchar; needsUpdate = true; runOnce(); // update filter immediately @@ -600,7 +632,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) { (int)runState, dest, channel, freetext.c_str()); if (dest == 0) dest = NODENUM_BROADCAST; // Defensive: If channel isn't valid, pick the first available channel - if (channel < 0 || channel >= channels.getNumChannels()) channel = 0; + if (channel >= channels.getNumChannels()) channel = 0; payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; currentMessageIndex = -1; @@ -618,6 +650,21 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) { return true; } + // Move cursor left + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) { + payload = INPUT_BROKER_MSG_LEFT; + lastTouchMillis = millis(); + runOnce(); + return true; + } + // Move cursor right + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) { + payload = INPUT_BROKER_MSG_RIGHT; + lastTouchMillis = millis(); + runOnce(); + return true; + } + // Cancel (dismiss freetext screen) if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) { runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; @@ -865,11 +912,7 @@ int32_t CannedMessageModule::runOnce() powerFSM.trigger(EVENT_PRESS); return INT32_MAX; } else { -#if defined(USE_VIRTUAL_KEYBOARD) - sendText(this->dest, indexChannels[this->channel], this->messages[this->currentMessageIndex], true); -#else sendText(this->dest, this->channel, this->messages[this->currentMessageIndex], true); -#endif } this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; } else { @@ -888,9 +931,20 @@ int32_t CannedMessageModule::runOnce() this->notifyObservers(&e); return 2000; - } else if ((this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) && (this->currentMessageIndex == -1)) { - this->currentMessageIndex = 0; - LOG_DEBUG("First touch (%d):%s", this->currentMessageIndex, this->getCurrentMessage()); + } + // === Always highlight the first real canned message when entering the message list === + else if ((this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) && (this->currentMessageIndex == -1)) { + // Find first actual canned message (not a special action entry) + int firstRealMsgIdx = 0; + for (int i = 0; i < this->messagesCount; ++i) { + if (strcmp(this->messages[i], "[Select Destination]") != 0 && + strcmp(this->messages[i], "[Exit]") != 0 && + strcmp(this->messages[i], "[---- Free Text ----]") != 0) { + firstRealMsgIdx = i; + break; + } + } + this->currentMessageIndex = firstRealMsgIdx; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) { @@ -922,73 +976,15 @@ int32_t CannedMessageModule::runOnce() } else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { switch (this->payload) { case INPUT_BROKER_MSG_LEFT: - if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { - size_t numMeshNodes = nodeDB->getNumMeshNodes(); - if (this->dest == NODENUM_BROADCAST) { - this->dest = nodeDB->getNodeNum(); - } - for (unsigned int i = 0; i < numMeshNodes; i++) { - if (nodeDB->getMeshNodeByIndex(i)->num == this->dest) { - this->dest = - (i > 0) ? nodeDB->getMeshNodeByIndex(i - 1)->num : nodeDB->getMeshNodeByIndex(numMeshNodes - 1)->num; - break; - } - } - if (this->dest == nodeDB->getNodeNum()) { - this->dest = NODENUM_BROADCAST; - } - } else if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL) { - for (unsigned int i = 0; i < channels.getNumChannels(); i++) { - if ((channels.getByIndex(i).role == meshtastic_Channel_Role_SECONDARY) || - (channels.getByIndex(i).role == meshtastic_Channel_Role_PRIMARY)) { - indexChannels[numChannels] = i; - numChannels++; - } - } - if (this->channel == 0) { - this->channel = numChannels - 1; - } else { - this->channel--; - } - } else { - if (this->cursor > 0) { - this->cursor--; - } + // Only handle cursor movement in freetext + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor > 0) { + this->cursor--; } break; case INPUT_BROKER_MSG_RIGHT: - if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { - size_t numMeshNodes = nodeDB->getNumMeshNodes(); - if (this->dest == NODENUM_BROADCAST) { - this->dest = nodeDB->getNodeNum(); - } - for (unsigned int i = 0; i < numMeshNodes; i++) { - if (nodeDB->getMeshNodeByIndex(i)->num == this->dest) { - this->dest = - (i < numMeshNodes - 1) ? nodeDB->getMeshNodeByIndex(i + 1)->num : nodeDB->getMeshNodeByIndex(0)->num; - break; - } - } - if (this->dest == nodeDB->getNodeNum()) { - this->dest = NODENUM_BROADCAST; - } - } else if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL) { - for (unsigned int i = 0; i < channels.getNumChannels(); i++) { - if ((channels.getByIndex(i).role == meshtastic_Channel_Role_SECONDARY) || - (channels.getByIndex(i).role == meshtastic_Channel_Role_PRIMARY)) { - indexChannels[numChannels] = i; - numChannels++; - } - } - if (this->channel == numChannels - 1) { - this->channel = 0; - } else { - this->channel++; - } - } else { - if (this->cursor < this->freetext.length()) { - this->cursor++; - } + // Only handle cursor movement in freetext + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor < this->freetext.length()) { + this->cursor++; } break; default: From ea9c71ecd9fa3ae7e6e686237722bbaf5fc3eefc Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 30 May 2025 02:11:04 -0400 Subject: [PATCH 180/265] Unified header for sending. --- src/modules/CannedMessageModule.cpp | 55 +++++++++++------------------ src/modules/CannedMessageModule.h | 4 +-- 2 files changed, 21 insertions(+), 38 deletions(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 0759da668..0e75bd360 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -138,6 +138,21 @@ int CannedMessageModule::splitConfiguredMessages() return this->messagesCount; } +void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char* buffer) { + if (display->getWidth() > 128) { + if (this->dest == NODENUM_BROADCAST) { + display->drawStringf(x, y, buffer, "To: Broadcast@%s", channels.getName(this->channel)); + } else { + display->drawStringf(x, y, buffer, "To: %s@%s", getNodeName(this->dest), channels.getName(this->channel)); + } + } else { + if (this->dest == NODENUM_BROADCAST) { + display->drawStringf(x, y, buffer, "To: Broadc@%.5s", channels.getName(this->channel)); + } else { + display->drawStringf(x, y, buffer, "To: %.5s@%.5s", getNodeName(this->dest), channels.getName(this->channel)); + } + } +} void CannedMessageModule::resetSearch() { LOG_INFO("Resetting search, restoring full destination list"); @@ -1560,33 +1575,17 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - if (this->destSelect != CANNED_MESSAGE_DESTINATION_TYPE_NONE) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - } - - switch (this->destSelect) { - case CANNED_MESSAGE_DESTINATION_TYPE_NODE: - display->drawStringf(0 + x, 0 + y, buffer, "To: >%s<@%s", getNodeName(this->dest), channels.getName(this->channel)); - break; - case CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL: - display->drawStringf(0 + x, 0 + y, buffer, "To: %s@>%s<", getNodeName(this->dest), channels.getName(this->channel)); - break; - default: - if (display->getWidth() > 128) { - display->drawStringf(0 + x, 0 + y, buffer, "To: %s@%s", getNodeName(this->dest), channels.getName(this->channel)); - } else { - display->drawStringf(0 + x, 0 + y, buffer, "To: %.5s@%.5s", getNodeName(this->dest), channels.getName(this->channel)); - } - break; - } + // --- Draw node/channel header at the top --- + drawHeader(display, x, y, buffer); + // --- Char count right-aligned --- if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) { uint16_t charsLeft = meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0); snprintf(buffer, sizeof(buffer), "%d left", charsLeft); display->drawString(x + display->getWidth() - display->getStringWidth(buffer), y + 0, buffer); } + // --- Draw Free Text input, shifted down --- display->setColor(WHITE); display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), drawWithCursor(this->freetext, this->cursor)); @@ -1602,21 +1601,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st const int rowSpacing = FONT_HEIGHT_SMALL - 4; // Draw header (To: ...) - switch (this->destSelect) { - case CANNED_MESSAGE_DESTINATION_TYPE_NODE: - display->drawStringf(x + 0, y + 0, buffer, "To: >%s<@%s", getNodeName(this->dest), channels.getName(this->channel)); - break; - case CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL: - display->drawStringf(x + 0, y + 0, buffer, "To: %s@>%s<", getNodeName(this->dest), channels.getName(this->channel)); - break; - default: - if (display->getWidth() > 128) { - display->drawStringf(x + 0, y + 0, buffer, "To: %s@%s", getNodeName(this->dest), channels.getName(this->channel)); - } else { - display->drawStringf(x + 0, y + 0, buffer, "To: %.5s@%.5s", getNodeName(this->dest), channels.getName(this->channel)); - } - break; - } + drawHeader(display, x, y, buffer); // Shift message list upward by 3 pixels to reduce spacing between header and first message const int listYOffset = y + FONT_HEIGHT_SMALL - 3; diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index 7511bab31..2b524692b 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -24,7 +24,6 @@ enum cannedMessageModuleRunState { enum cannedMessageDestinationType { CANNED_MESSAGE_DESTINATION_TYPE_NONE, CANNED_MESSAGE_DESTINATION_TYPE_NODE, - CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL }; enum CannedMessageModuleIconType { shift, backspace, space, enter }; @@ -102,6 +101,7 @@ protected: // === Transmission === void sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies); + void drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char* buffer); int splitConfiguredMessages(); int getNextIndex(); int getPrevIndex(); @@ -162,8 +162,6 @@ private: NodeNum incoming = NODENUM_BROADCAST; // Source node from which last ACK/NACK was received 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 - uint8_t numChannels = 0; // Total number of channels available for selection - ChannelIndex indexChannels[MAX_NUM_CHANNELS] = {0}; // Cached channel indices available for this node 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 From 43aa906017afd54a8f69a2bf291cd8543984a4f3 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Fri, 30 May 2025 09:31:13 -0500 Subject: [PATCH 181/265] trunk --- src/ButtonThread.cpp | 48 +- src/graphics/Screen.cpp | 226 ++++--- src/graphics/Screen.h | 2 +- src/graphics/SharedUIDisplay.h | 13 +- src/graphics/emotes.cpp | 72 ++- src/graphics/emotes.h | 3 +- src/graphics/images.h | 55 +- src/main.cpp | 2 +- src/modules/AdminModule.cpp | 8 +- src/modules/CannedMessageModule.cpp | 582 ++++++++++-------- src/modules/CannedMessageModule.h | 44 +- .../Telemetry/EnvironmentTelemetry.cpp | 58 +- src/modules/Telemetry/PowerTelemetry.cpp | 9 +- src/shutdown.h | 2 +- 14 files changed, 580 insertions(+), 544 deletions(-) diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp index 1dca83033..3680bb6b8 100644 --- a/src/ButtonThread.cpp +++ b/src/ButtonThread.cpp @@ -9,10 +9,10 @@ #include "RadioLibInterface.h" #include "buzz.h" #include "main.h" -#include "modules/ExternalNotificationModule.h" #include "modules/CannedMessageModule.h" +#include "modules/ExternalNotificationModule.h" #include "power.h" -#include "sleep.h" +#include "sleep.h" #ifdef ARCH_PORTDUINO #include "platform/portduino/PortduinoGlue.h" #endif @@ -27,7 +27,7 @@ using namespace concurrency; ButtonThread *buttonThread; // Declared extern in header -extern CannedMessageModule* cannedMessageModule; +extern CannedMessageModule *cannedMessageModule; volatile ButtonThread::ButtonEventType ButtonThread::btnEvent = ButtonThread::BUTTON_EVENT_NONE; #if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN) @@ -122,7 +122,7 @@ void ButtonThread::switchPage() { // Prevent screen switch if CannedMessageModule is focused and intercepting input #if HAS_SCREEN - extern CannedMessageModule* cannedMessageModule; + extern CannedMessageModule *cannedMessageModule; if (cannedMessageModule && cannedMessageModule->isInterceptingAndFocused()) { LOG_DEBUG("User button ignored during canned message input"); @@ -232,15 +232,15 @@ int32_t ButtonThread::runOnce() case BUTTON_EVENT_DOUBLE_PRESSED: { LOG_BUTTON("Double press!"); - - #ifdef ELECROW_ThinkNode_M1 + +#ifdef ELECROW_ThinkNode_M1 digitalWrite(PIN_EINK_EN, digitalRead(PIN_EINK_EN) == LOW); break; - #endif - +#endif + // Send GPS position immediately sendAdHocPosition(); - + // Show temporary on-screen confirmation banner for 3 seconds screen->showOverlayBanner("Ad-hoc Ping Sent", 3000); break; @@ -250,21 +250,21 @@ int32_t ButtonThread::runOnce() LOG_BUTTON("Mulitipress! %hux", multipressClickCount); switch (multipressClickCount) { #if HAS_GPS && !defined(ELECROW_ThinkNode_M1) - // 3 clicks: toggle GPS - case 3: - if (!config.device.disable_triple_click && (gps != nullptr)) { - gps->toggleGpsMode(); + // 3 clicks: toggle GPS + case 3: + if (!config.device.disable_triple_click && (gps != nullptr)) { + gps->toggleGpsMode(); - const char* statusMsg = (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) - ? "GPS Enabled" - : "GPS Disabled"; + const char *statusMsg = (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) + ? "GPS Enabled" + : "GPS Disabled"; - if (screen) { - screen->forceDisplay(true); // Force a new UI frame, then force an EInk update - screen->showOverlayBanner(statusMsg, 3000); + if (screen) { + screen->forceDisplay(true); // Force a new UI frame, then force an EInk update + screen->showOverlayBanner(statusMsg, 3000); + } } - } - break; + break; #elif defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2) case 3: LOG_INFO("3 clicks: toggle buzzer"); @@ -306,12 +306,12 @@ int32_t ButtonThread::runOnce() case BUTTON_EVENT_LONG_PRESSED: { LOG_BUTTON("Long press!"); powerFSM.trigger(EVENT_PRESS); - + if (screen) { // Show shutdown message as a temporary overlay banner - screen->showOverlayBanner("Shutting Down..."); // Display for 3 seconds + screen->showOverlayBanner("Shutting Down..."); // Display for 3 seconds } - + playBeep(); break; } diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 235398177..9e38f46b1 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -35,19 +35,19 @@ along with this program. If not, see . #include "FSCommon.h" #include "MeshService.h" #include "NodeDB.h" +#include "RadioLibInterface.h" #include "error.h" #include "gps/GeoCoord.h" #include "gps/RTC.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" -#include "graphics/images.h" #include "graphics/emotes.h" +#include "graphics/images.h" #include "input/ScanAndSelect.h" #include "input/TouchScreenImpl1.h" #include "main.h" #include "mesh-pb-constants.h" #include "mesh/Channels.h" -#include "RadioLibInterface.h" #include "mesh/generated/meshtastic/deviceonly.pb.h" #include "meshUtils.h" #include "modules/AdminModule.h" @@ -57,7 +57,6 @@ along with this program. If not, see . #include "sleep.h" #include "target_specific.h" - using graphics::Emote; using graphics::emotes; using graphics::numEmotes; @@ -132,18 +131,19 @@ static bool heartbeat = false; #include "graphics/ScreenFonts.h" #include - // Start Functions to write date/time to the screen -#include // Only needed if you're using std::string elsewhere +#include // Only needed if you're using std::string elsewhere -bool isLeapYear(int year) { +bool isLeapYear(int year) +{ return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); } -const int daysInMonth[] = { 31,28,31,30,31,30,31,31,30,31,30,31 }; +const int daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // Fills the buffer with a formatted date/time string and returns pixel width -int formatDateTime(char* buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay* display, bool includeTime) { +int formatDateTime(char *buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay *display, bool includeTime) +{ int sec = rtc_sec % 60; rtc_sec /= 60; int min = rtc_sec % 60; @@ -165,7 +165,8 @@ int formatDateTime(char* buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay* dis int month = 0; while (month < 12) { int dim = daysInMonth[month]; - if (month == 1 && isLeapYear(year)) dim++; + if (month == 1 && isLeapYear(year)) + dim++; if (rtc_sec >= (uint32_t)dim) { rtc_sec -= dim; month++; @@ -188,7 +189,6 @@ int formatDateTime(char* buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay* dis // Usage: int stringWidth = formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display); // End Functions to write date/time to the screen - void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display) { for (int row = 0; row < height; row++) { @@ -1357,7 +1357,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 } for (int i = 0; i < numEmotes; ++i) { const Emote &e = emotes[i]; - if (strcmp(msg, e.label) == 0){ + if (strcmp(msg, e.label) == 0) { // Draw the header if (isInverted) { drawRoundedHighlight(display, x, 0, SCREEN_WIDTH, FONT_HEIGHT_SMALL - 1, cornerRadius); @@ -1881,23 +1881,26 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ for (size_t i = 0; i < total; i++) { meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); // Skip nulls and ourself - if (!n || n->num == nodeDB->getNodeNum()) continue; - if (n->is_favorite) favoritedNodes.push_back(n); + if (!n || n->num == nodeDB->getNodeNum()) + continue; + if (n->is_favorite) + favoritedNodes.push_back(n); } // Keep a stable, consistent display order std::sort(favoritedNodes.begin(), favoritedNodes.end(), - [](meshtastic_NodeInfoLite *a, meshtastic_NodeInfoLite *b) { - return a->num < b->num; - }); + [](meshtastic_NodeInfoLite *a, meshtastic_NodeInfoLite *b) { return a->num < b->num; }); } - if (favoritedNodes.empty()) return; + if (favoritedNodes.empty()) + return; // --- Only display if index is valid --- int nodeIndex = state->currentFrame - (screen->frameCount - favoritedNodes.size()); - if (nodeIndex < 0 || nodeIndex >= (int)favoritedNodes.size()) return; + if (nodeIndex < 0 || nodeIndex >= (int)favoritedNodes.size()) + return; meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; - if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) return; + if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) + return; display->clear(); @@ -1928,13 +1931,8 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ // 4. The first line is ALWAYS at your macro position; subsequent lines use the next available macro slot. // List of available macro Y positions in order, from top to bottom. - const int yPositions[5] = { - moreCompactFirstLine, - moreCompactSecondLine, - moreCompactThirdLine, - moreCompactFourthLine, - moreCompactFifthLine - }; + const int yPositions[5] = {moreCompactFirstLine, moreCompactSecondLine, moreCompactThirdLine, moreCompactFourthLine, + moreCompactFifthLine}; int line = 0; // which slot to use next // === 1. Long Name (always try to show first) === @@ -1965,7 +1963,8 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ size_t len = strlen(signalHopsStr); // Decide between "1 Hop" and "N Hops" if (haveSignal) { - snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops")); + snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away, + (node->hops_away == 1 ? "Hop" : "Hops")); } else { snprintf(signalHopsStr, sizeof(signalHopsStr), "[%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops")); } @@ -1980,10 +1979,13 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ if (seconds != 0 && seconds != UINT32_MAX) { uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; // Format as "Heard: Xm ago", "Heard: Xh ago", or "Heard: Xd ago" - snprintf(seenStr, sizeof(seenStr), - (days > 365 ? " Heard: ?" : " Heard: %d%c ago"), - (days ? days : hours ? hours : minutes), - (days ? 'd' : hours ? 'h' : 'm')); + snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard: ?" : " Heard: %d%c ago"), + (days ? days + : hours ? hours + : minutes), + (days ? 'd' + : hours ? 'h' + : 'm')); } if (seenStr[0] && line < 5) { display->drawString(x, yPositions[line++], seenStr); @@ -2010,7 +2012,7 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ // === 5. Distance (only if both nodes have GPS position) === meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - char distStr[24] = ""; // Make buffer big enough for any string + char distStr[24] = ""; // Make buffer big enough for any string bool haveDistance = false; if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { @@ -2022,9 +2024,7 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ double dLat = (lat2 - lat1) * DEG_TO_RAD; double dLon = (lon2 - lon1) * DEG_TO_RAD; double a = - sin(dLat / 2) * sin(dLat / 2) + - cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * - sin(dLon / 2) * sin(dLon / 2); + sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); double c = 2 * atan2(sqrt(a), sqrt(1 - a)); double distanceKm = earthRadiusKm * c; @@ -2088,19 +2088,16 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; const auto &op = ourNode->position; - float myHeading = screen->hasHeading() - ? screen->getHeading() * PI / 180 - : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 + : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); screen->drawCompassNorth(display, compassX, compassY, myHeading); const auto &p = node->position; - float d = GeoCoord::latLongToMeter( - DegD(p.latitude_i), DegD(p.longitude_i), - DegD(op.latitude_i), DegD(op.longitude_i)); - float bearing = GeoCoord::bearing( - DegD(op.latitude_i), DegD(op.longitude_i), - DegD(p.latitude_i), DegD(p.longitude_i)); - if (!config.display.compass_north_top) bearing -= myHeading; + float d = + GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); + if (!config.display.compass_north_top) + bearing -= myHeading; screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearing); display->drawCircle(compassX, compassY, compassRadius); @@ -2115,39 +2112,39 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ if (showCompass) { int yBelowContent = (line > 0 && line <= 5) ? (yPositions[line - 1] + FONT_HEIGHT_SMALL + 2) : moreCompactFirstLine; const int margin = 4; - // --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) ----------- - #if defined(USE_EINK) - const int iconSize = (SCREEN_WIDTH > 128) ? 16 : 8; - const int navBarHeight = iconSize + 6; - #else - const int navBarHeight = 0; - #endif +// --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) ----------- +#if defined(USE_EINK) + const int iconSize = (SCREEN_WIDTH > 128) ? 16 : 8; + const int navBarHeight = iconSize + 6; +#else + const int navBarHeight = 0; +#endif int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; // --------- END PATCH FOR EINK NAV BAR ----------- - if (availableHeight < FONT_HEIGHT_SMALL * 2) return; + if (availableHeight < FONT_HEIGHT_SMALL * 2) + return; int compassRadius = availableHeight / 2; - if (compassRadius < 8) compassRadius = 8; - if (compassRadius * 2 > SCREEN_WIDTH - 16) compassRadius = (SCREEN_WIDTH - 16) / 2; + if (compassRadius < 8) + compassRadius = 8; + if (compassRadius * 2 > SCREEN_WIDTH - 16) + compassRadius = (SCREEN_WIDTH - 16) / 2; int compassX = x + SCREEN_WIDTH / 2; int compassY = yBelowContent + availableHeight / 2; const auto &op = ourNode->position; - float myHeading = screen->hasHeading() - ? screen->getHeading() * PI / 180 - : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 + : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); screen->drawCompassNorth(display, compassX, compassY, myHeading); const auto &p = node->position; - float d = GeoCoord::latLongToMeter( - DegD(p.latitude_i), DegD(p.longitude_i), - DegD(op.latitude_i), DegD(op.longitude_i)); - float bearing = GeoCoord::bearing( - DegD(op.latitude_i), DegD(op.longitude_i), - DegD(p.latitude_i), DegD(p.longitude_i)); - if (!config.display.compass_north_top) bearing -= myHeading; + float d = + GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); + if (!config.display.compass_north_top) + bearing -= myHeading; screen->drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); display->drawCircle(compassX, compassY, compassRadius); @@ -2346,9 +2343,9 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t int totalEntries = nodeList.size(); int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; - #ifdef USE_EINK - totalRowsAvailable -= 1; - #endif +#ifdef USE_EINK + totalRowsAvailable -= 1; +#endif int visibleNodeRows = totalRowsAvailable; int totalColumns = 2; @@ -2424,8 +2421,8 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); display->drawString(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nodeName); - if (node->is_favorite){ - if(SCREEN_WIDTH > 128){ + if (node->is_favorite) { + if (SCREEN_WIDTH > 128) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); @@ -2455,10 +2452,10 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - + display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nameMaxWidth, nodeName); - if (node->is_favorite){ - if(SCREEN_WIDTH > 128){ + if (node->is_favorite) { + if (SCREEN_WIDTH > 128) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); @@ -2550,8 +2547,8 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nameMaxWidth, nodeName); - if (node->is_favorite){ - if(SCREEN_WIDTH > 128){ + if (node->is_favorite) { + if (SCREEN_WIDTH > 128) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); @@ -2681,8 +2678,8 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nameMaxWidth, nodeName); - if (node->is_favorite){ - if(SCREEN_WIDTH > 128){ + if (node->is_favorite) { + if (SCREEN_WIDTH > 128) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); } else { display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); @@ -2784,13 +2781,14 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i rows = 5; } - // === First Row: Region / Channel Utilization and Uptime === bool origBold = config.display.heading_bold; config.display.heading_bold = false; // Display Region and Channel Utilization - drawNodes(display, x + 1, ((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)) + 2, nodeStatus, -1, false, "online"); + drawNodes(display, x + 1, + ((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)) + 2, nodeStatus, + -1, false, "online"); uint32_t uptime = millis() / 1000; char uptimeStr[6]; @@ -2812,7 +2810,9 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i char uptimeFullStr[16]; snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); - display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeFullStr), ((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)), uptimeFullStr); + display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeFullStr), + ((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)), + uptimeFullStr); config.display.heading_bold = origBold; @@ -2827,9 +2827,13 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i } else { displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } - display->drawString(0, ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), displayLine); + display->drawString( + 0, ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), + displayLine); } else { - drawGPS(display, 0, ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)) + 3, gpsStatus); + drawGPS(display, 0, + ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)) + 3, + gpsStatus); } #endif @@ -2838,16 +2842,22 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i int batV = powerStatus->getBatteryVoltageMv() / 1000; int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; snprintf(batStr, sizeof(batStr), "%01d.%02dV", batV, batCv); - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(batStr), ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), batStr); + display->drawString( + x + SCREEN_WIDTH - display->getStringWidth(batStr), + ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), batStr); } else { - display->drawString(x + SCREEN_WIDTH - display->getStringWidth("USB"), ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), String("USB")); + display->drawString( + x + SCREEN_WIDTH - display->getStringWidth("USB"), + ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), + String("USB")); } config.display.heading_bold = origBold; // === Third Row: Bluetooth Off (Only If Actually Off) === if (!config.bluetooth.enabled) { - display->drawString(0, ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactThirdLine : moreCompactThirdLine)), "BT off"); + display->drawString( + 0, ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactThirdLine : moreCompactThirdLine)), "BT off"); } // === Third & Fourth Rows: Node Identity === @@ -2867,27 +2877,35 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i char combinedName[50]; snprintf(combinedName, sizeof(combinedName), "%s (%s)", longName, shortnameble); - if(SCREEN_WIDTH - (display->getStringWidth(longName) + display->getStringWidth(shortnameble)) > 10){ + if (SCREEN_WIDTH - (display->getStringWidth(longName) + display->getStringWidth(shortnameble)) > 10) { size_t len = strlen(combinedName); if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) { - combinedName[len - 3] = '\0'; // Remove the last three characters + combinedName[len - 3] = '\0'; // Remove the last three characters } textWidth = display->getStringWidth(combinedName); nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine)) + yOffset, combinedName); + display->drawString( + nameX, + ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine)) + yOffset, + combinedName); } else { textWidth = display->getStringWidth(longName); nameX = (SCREEN_WIDTH - textWidth) / 2; yOffset = (strcmp(shortnameble, "") == 0) ? 1 : 0; - if(yOffset == 1){ + if (yOffset == 1) { yOffset = (SCREEN_WIDTH > 128) ? 0 : 7; } - display->drawString(nameX, ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine)) + yOffset, longName); + display->drawString( + nameX, + ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine)) + yOffset, + longName); // === Fourth Row: ShortName Centered === textWidth = display->getStringWidth(shortnameble); nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, ((rows == 4) ? compactFourthLine : ((SCREEN_HEIGHT > 64) ? compactFifthLine : moreCompactFifthLine)), shortnameble); + display->drawString(nameX, + ((rows == 4) ? compactFourthLine : ((SCREEN_HEIGHT > 64) ? compactFifthLine : moreCompactFifthLine)), + shortnameble); } } @@ -2947,14 +2965,14 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int char freqStr[16]; float freq = RadioLibInterface::instance->getFreq(); snprintf(freqStr, sizeof(freqStr), "%.3f", freq); - if(config.lora.channel_num == 0){ + if (config.lora.channel_num == 0) { snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %s", freqStr); } else { snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Chan: %s (%d)", freqStr, config.lora.channel_num); } size_t len = strlen(frequencyslot); if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { - frequencyslot[len - 4] = '\0'; // Remove the last three characters + frequencyslot[len - 4] = '\0'; // Remove the last three characters } textWidth = display->getStringWidth(frequencyslot); nameX = (SCREEN_WIDTH - textWidth) / 2; @@ -3061,9 +3079,11 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat } else { displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } - display->drawString(display->getStringWidth(Satelite_String) + 3, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), displayLine); + display->drawString(display->getStringWidth(Satelite_String) + 3, + ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), displayLine); } else { - drawGPS(display, display->getStringWidth(Satelite_String) + 3, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine) + 3, gpsStatus); + drawGPS(display, display->getStringWidth(Satelite_String) + 3, + ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine) + 3, gpsStatus); } config.display.heading_bold = origBold; @@ -3107,7 +3127,7 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat // === Fifth Row: Date === uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); char datetimeStr[25]; - bool showTime = false; // set to true for full datetime + bool showTime = false; // set to true for full datetime formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime); char fullLine[40]; snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr); @@ -3160,11 +3180,14 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat SCREEN_HEIGHT - yBelowContent - margin; #endif - if (availableHeight < FONT_HEIGHT_SMALL * 2) return; + if (availableHeight < FONT_HEIGHT_SMALL * 2) + return; int compassRadius = availableHeight / 2; - if (compassRadius < 8) compassRadius = 8; - if (compassRadius * 2 > SCREEN_WIDTH - 16) compassRadius = (SCREEN_WIDTH - 16) / 2; + if (compassRadius < 8) + compassRadius = 8; + if (compassRadius * 2 > SCREEN_WIDTH - 16) + compassRadius = (SCREEN_WIDTH - 16) / 2; int compassX = x + SCREEN_WIDTH / 2; int compassY = yBelowContent + availableHeight / 2; @@ -3532,7 +3555,8 @@ void NavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) const int bigOffset = useBigIcons ? 1 : 0; const size_t totalIcons = screen->indicatorIcons.size(); - if (totalIcons == 0) return; + if (totalIcons == 0) + return; const size_t iconsPerPage = (SCREEN_WIDTH + spacing) / (iconSize + spacing); const size_t currentPage = currentFrame / iconsPerPage; @@ -3628,7 +3652,7 @@ void Screen::setup() // === Set custom overlay callbacks === static OverlayCallback overlays[] = { drawFunctionOverlay, // For mute/buzzer modifiers etc. - NavigationBar // Custom indicator icons for each frame + NavigationBar // Custom indicator icons for each frame }; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index b92cdbc91..e348e7571 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -275,7 +275,7 @@ class Screen : public concurrency::OSThread } void showOverlayBanner(const String &message, uint32_t durationMs = 3000); - + void startFirmwareUpdateScreen() { ScreenCmd cmd; diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index 8d85ff764..1a8f5fb23 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -2,7 +2,8 @@ #include -namespace graphics { +namespace graphics +{ // ======================= // Shared UI Helpers @@ -22,11 +23,11 @@ namespace graphics { #define standardFourthLine (FONT_HEIGHT_SMALL + 1) * 4 // More Compact line layout -#define moreCompactFirstLine compactFirstLine -#define moreCompactSecondLine (moreCompactFirstLine + (FONT_HEIGHT_SMALL - 5)) -#define moreCompactThirdLine (moreCompactSecondLine + (FONT_HEIGHT_SMALL - 5)) -#define moreCompactFourthLine (moreCompactThirdLine + (FONT_HEIGHT_SMALL - 5)) -#define moreCompactFifthLine (moreCompactFourthLine + (FONT_HEIGHT_SMALL - 5)) +#define moreCompactFirstLine compactFirstLine +#define moreCompactSecondLine (moreCompactFirstLine + (FONT_HEIGHT_SMALL - 5)) +#define moreCompactThirdLine (moreCompactSecondLine + (FONT_HEIGHT_SMALL - 5)) +#define moreCompactFourthLine (moreCompactThirdLine + (FONT_HEIGHT_SMALL - 5)) +#define moreCompactFifthLine (moreCompactFourthLine + (FONT_HEIGHT_SMALL - 5)) // Quick screen access #define SCREEN_WIDTH display->getWidth() diff --git a/src/graphics/emotes.cpp b/src/graphics/emotes.cpp index 38a9f2915..205d5c660 100644 --- a/src/graphics/emotes.cpp +++ b/src/graphics/emotes.cpp @@ -1,56 +1,57 @@ #include "emotes.h" -namespace graphics { +namespace graphics +{ // Always define Emote list and count const Emote emotes[] = { #ifndef EXCLUDE_EMOJI // --- Thumbs --- - {"\U0001F44D", thumbup, thumbs_width, thumbs_height}, // πŸ‘ Thumbs Up - {"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, // πŸ‘Ž Thumbs Down + {"\U0001F44D", thumbup, thumbs_width, thumbs_height}, // πŸ‘ Thumbs Up + {"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, // πŸ‘Ž Thumbs Down // --- Smileys (Multiple Unicode Aliases) --- - {"\U0001F60A", smiley, smiley_width, smiley_height}, // 😊 Smiling Face with Smiling Eyes - {"\U0001F600", smiley, smiley_width, smiley_height}, // πŸ˜€ Grinning Face - {"\U0001F642", smiley, smiley_width, smiley_height}, // πŸ™‚ Slightly Smiling Face - {"\U0001F609", smiley, smiley_width, smiley_height}, // πŸ˜‰ Winking Face - {"\U0001F601", smiley, smiley_width, smiley_height}, // 😁 Grinning Face with Smiling Eyes + {"\U0001F60A", smiley, smiley_width, smiley_height}, // 😊 Smiling Face with Smiling Eyes + {"\U0001F600", smiley, smiley_width, smiley_height}, // πŸ˜€ Grinning Face + {"\U0001F642", smiley, smiley_width, smiley_height}, // πŸ™‚ Slightly Smiling Face + {"\U0001F609", smiley, smiley_width, smiley_height}, // πŸ˜‰ Winking Face + {"\U0001F601", smiley, smiley_width, smiley_height}, // 😁 Grinning Face with Smiling Eyes // --- Question/Alert --- - {"\u2753", question, question_width, question_height}, // ❓ Question Mark - {"\u203C\uFE0F", bang, bang_width, bang_height}, // ‼️ Double Exclamation Mark + {"\u2753", question, question_width, question_height}, // ❓ Question Mark + {"\u203C\uFE0F", bang, bang_width, bang_height}, // ‼️ Double Exclamation Mark // --- Laughing Faces --- - {"\U0001F602", haha, haha_width, haha_height}, // πŸ˜‚ Face with Tears of Joy - {"\U0001F923", haha, haha_width, haha_height}, // 🀣 Rolling on the Floor Laughing - {"\U0001F606", haha, haha_width, haha_height}, // πŸ˜† Smiling with Open Mouth and Closed Eyes - {"\U0001F605", haha, haha_width, haha_height}, // πŸ˜… Smiling with Sweat - {"\U0001F604", haha, haha_width, haha_height}, // πŸ˜„ Grinning Face with Smiling Eyes + {"\U0001F602", haha, haha_width, haha_height}, // πŸ˜‚ Face with Tears of Joy + {"\U0001F923", haha, haha_width, haha_height}, // 🀣 Rolling on the Floor Laughing + {"\U0001F606", haha, haha_width, haha_height}, // πŸ˜† Smiling with Open Mouth and Closed Eyes + {"\U0001F605", haha, haha_width, haha_height}, // πŸ˜… Smiling with Sweat + {"\U0001F604", haha, haha_width, haha_height}, // πŸ˜„ Grinning Face with Smiling Eyes // --- Gestures and People --- - {"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height},// πŸ‘‹ Waving Hand - {"\U0001F920", cowboy, cowboy_width, cowboy_height}, // 🀠 Cowboy Hat Face - {"\U0001F3A7", deadmau5, deadmau5_width, deadmau5_height}, // 🎧 Headphones + {"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height}, // πŸ‘‹ Waving Hand + {"\U0001F920", cowboy, cowboy_width, cowboy_height}, // 🀠 Cowboy Hat Face + {"\U0001F3A7", deadmau5, deadmau5_width, deadmau5_height}, // 🎧 Headphones // --- Weather --- - {"\u2600", sun, sun_width, sun_height}, // β˜€ Sun (without variation selector) - {"\u2600\uFE0F", sun, sun_width, sun_height}, // β˜€οΈ Sun (with variation selector) - {"\U0001F327\uFE0F", rain, rain_width, rain_height}, // 🌧️ Cloud with Rain - {"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // ☁️ Cloud - {"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // 🌫️ Fog + {"\u2600", sun, sun_width, sun_height}, // β˜€ Sun (without variation selector) + {"\u2600\uFE0F", sun, sun_width, sun_height}, // β˜€οΈ Sun (with variation selector) + {"\U0001F327\uFE0F", rain, rain_width, rain_height}, // 🌧️ Cloud with Rain + {"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // ☁️ Cloud + {"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // 🌫️ Fog // --- Misc Faces --- - {"\U0001F608", devil, devil_width, devil_height}, // 😈 Smiling Face with Horns + {"\U0001F608", devil, devil_width, devil_height}, // 😈 Smiling Face with Horns // --- Hearts (Multiple Unicode Aliases) --- - {"\u2764\uFE0F", heart, heart_width, heart_height}, // ❀️ Red Heart - {"\U0001F9E1", heart, heart_width, heart_height}, // 🧑 Orange Heart - {"\U00002763", heart, heart_width, heart_height}, // ❣ Heart Exclamation - {"\U00002764", heart, heart_width, heart_height}, // ❀ Red Heart (legacy) - {"\U0001F495", heart, heart_width, heart_height}, // πŸ’• Two Hearts - {"\U0001F496", heart, heart_width, heart_height}, // πŸ’– Sparkling Heart - {"\U0001F497", heart, heart_width, heart_height}, // πŸ’— Growing Heart - {"\U0001F498", heart, heart_width, heart_height}, // πŸ’˜ Heart with Arrow + {"\u2764\uFE0F", heart, heart_width, heart_height}, // ❀️ Red Heart + {"\U0001F9E1", heart, heart_width, heart_height}, // 🧑 Orange Heart + {"\U00002763", heart, heart_width, heart_height}, // ❣ Heart Exclamation + {"\U00002764", heart, heart_width, heart_height}, // ❀ Red Heart (legacy) + {"\U0001F495", heart, heart_width, heart_height}, // πŸ’• Two Hearts + {"\U0001F496", heart, heart_width, heart_height}, // πŸ’– Sparkling Heart + {"\U0001F497", heart, heart_width, heart_height}, // πŸ’— Growing Heart + {"\U0001F498", heart, heart_width, heart_height}, // πŸ’˜ Heart with Arrow // --- Objects --- {"\U0001F4A9", poo, poo_width, poo_height}, // πŸ’© Pile of Poo @@ -83,8 +84,7 @@ const unsigned char smiley[] PROGMEM = { 0x01, 0x09, 0x12, 0x20, 0x01, 0x0f, 0x1e, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x81, 0x00, 0x20, 0x20, 0x82, 0x00, 0x20, 0x10, 0x02, 0x01, 0x10, 0x10, 0x04, 0x02, 0x08, 0x08, 0x04, 0xfc, 0x07, 0x08, 0x08, 0x00, 0x00, 0x04, - 0x10, 0x00, 0x00, 0x02, 0x20, 0x00, 0x00, 0x01, 0x40, 0x00, 0xc0, 0x00, 0x80, 0x01, 0x30, 0x00, 0x00, 0xfe, 0x0f, 0x00 -}; + 0x10, 0x00, 0x00, 0x02, 0x20, 0x00, 0x00, 0x01, 0x40, 0x00, 0xc0, 0x00, 0x80, 0x01, 0x30, 0x00, 0x00, 0xfe, 0x0f, 0x00}; const unsigned char question[] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x07, 0x00, @@ -219,9 +219,7 @@ const unsigned char bell_icon[] PROGMEM = { 0b00011000, 0b00000000, 0b00000000, 0b00000110, 0b11110000, 0b11111111, 0b11111111, 0b00000011, 0b00000000, 0b00001100, 0b00001100, 0b00000000, 0b00000000, 0b00011000, 0b00000110, 0b00000000, 0b00000000, 0b11111000, 0b00000111, 0b00000000, 0b00000000, 0b11100000, 0b00000001, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, - 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000 -}; + 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}; #endif } // namespace graphics - diff --git a/src/graphics/emotes.h b/src/graphics/emotes.h index 5204d870b..5640ac04a 100644 --- a/src/graphics/emotes.h +++ b/src/graphics/emotes.h @@ -1,7 +1,8 @@ #pragma once #include -namespace graphics { +namespace graphics +{ // === Emote List === struct Emote { diff --git a/src/graphics/images.h b/src/graphics/images.h index bd6e19a17..f11acc084 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -241,71 +241,38 @@ const uint8_t mute_symbol_big[] PROGMEM = {0b00000001, 0b00000000, 0b11000010, 0 const unsigned char bell_alert[] PROGMEM = {0b00011000, 0b00100100, 0b00100100, 0b01000010, 0b01000010, 0b01000010, 0b11111111, 0b00011000}; - #define key_symbol_width 8 #define key_symbol_height 8 -const uint8_t key_symbol[] PROGMEM = { - 0b00000000, - 0b00000000, - 0b00000110, - 0b11111001, - 0b10101001, - 0b10000110, - 0b00000000, - 0b00000000 -}; +const uint8_t key_symbol[] PROGMEM = {0b00000000, 0b00000000, 0b00000110, 0b11111001, + 0b10101001, 0b10000110, 0b00000000, 0b00000000}; #define placeholder_width 8 #define placeholder_height 8 -const uint8_t placeholder[] PROGMEM = { - 0b11111111, - 0b11111111, - 0b11111111, - 0b11111111, - 0b11111111, - 0b11111111, - 0b11111111, - 0b11111111 -}; +const uint8_t placeholder[] PROGMEM = {0b11111111, 0b11111111, 0b11111111, 0b11111111, + 0b11111111, 0b11111111, 0b11111111, 0b11111111}; #define icon_node_width 8 #define icon_node_height 8 static const uint8_t icon_node[] PROGMEM = { - 0x10, // # + 0x10, // # 0x10, // # ← antenna 0x10, // # 0xFE, // ####### ← device top - 0x82, // # # + 0x82, // # # 0xAA, // # # # # ← body with pattern - 0x92, // # # # + 0x92, // # # # 0xFE // ####### ← device base }; #define bluetoothdisabled_width 8 #define bluetoothdisabled_height 8 -const uint8_t bluetoothdisabled[] PROGMEM = { - 0b11101100, - 0b01010100, - 0b01001100, - 0b01010100, - 0b01001100, - 0b00000000, - 0b00000000, - 0b00000000 -}; +const uint8_t bluetoothdisabled[] PROGMEM = {0b11101100, 0b01010100, 0b01001100, 0b01010100, + 0b01001100, 0b00000000, 0b00000000, 0b00000000}; #define smallbulletpoint_width 8 #define smallbulletpoint_height 8 -const uint8_t smallbulletpoint[] PROGMEM = { - 0b00000011, - 0b00000011, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000 -}; +const uint8_t smallbulletpoint[] PROGMEM = {0b00000011, 0b00000011, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b00000000, 0b00000000}; #include "img/icon.xbm" static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning"); \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index a85853688..7cbf77c83 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1228,7 +1228,7 @@ void setup() LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset"); config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET; nodeDB->saveToDisk(SEGMENT_CONFIG); - + if (!rIf->reconfigure()) { LOG_WARN("Reconfigure failed, rebooting"); screen->startAlert("Rebooting..."); diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 5df61386b..d22c648ca 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -299,7 +299,8 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta if (node != NULL) { node->is_favorite = true; saveChanges(SEGMENT_NODEDATABASE, false); - if (screen) screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens + if (screen) + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens } break; } @@ -309,7 +310,8 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta if (node != NULL) { node->is_favorite = false; saveChanges(SEGMENT_NODEDATABASE, false); - if (screen) screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens + if (screen) + screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens } break; } @@ -1120,7 +1122,7 @@ void AdminModule::reboot(int32_t seconds) { LOG_INFO("Reboot in %d seconds", seconds); if (screen) - screen->showOverlayBanner("Rebooting...", 0); // stays on screen + screen->showOverlayBanner("Rebooting...", 0); // stays on screen rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000); } diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 5f0bb5448..eb3758c30 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -11,12 +11,12 @@ #include "PowerFSM.h" // needed for button bypass #include "SPILock.h" #include "detect/ScanI2C.h" -#include "input/ScanAndSelect.h" -#include "mesh/generated/meshtastic/cannedmessages.pb.h" -#include "graphics/images.h" -#include "modules/AdminModule.h" #include "graphics/SharedUIDisplay.h" -#include "main.h" // for cardkb_found +#include "graphics/images.h" +#include "input/ScanAndSelect.h" +#include "main.h" // for cardkb_found +#include "mesh/generated/meshtastic/cannedmessages.pb.h" +#include "modules/AdminModule.h" #include "modules/ExternalNotificationModule.h" // for buzzer control #if !MESHTASTIC_EXCLUDE_GPS #include "GPS.h" @@ -70,7 +70,8 @@ CannedMessageModule::CannedMessageModule() } } static bool returnToCannedList = false; -bool hasKeyForNode(const meshtastic_NodeInfoLite* node) { +bool hasKeyForNode(const meshtastic_NodeInfoLite *node) +{ return node && node->has_user && node->user.public_key.size > 0; } /** @@ -96,7 +97,7 @@ int CannedMessageModule::splitConfiguredMessages() strncpy(this->messageStore, canned_messages.c_str(), sizeof(this->messageStore)); // Temporary array to allow for insertion - const char* tempMessages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT + 3] = {0}; + const char *tempMessages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT + 3] = {0}; int tempCount = 0; // First message always starts at buffer start @@ -117,12 +118,14 @@ int CannedMessageModule::splitConfiguredMessages() // Insert "[Select Destination]" after Free Text if present, otherwise at the top #if defined(USE_VIRTUAL_KEYBOARD) // Insert at position 1 (after Free Text) - for (int j = tempCount; j > 1; j--) tempMessages[j] = tempMessages[j - 1]; + for (int j = tempCount; j > 1; j--) + tempMessages[j] = tempMessages[j - 1]; tempMessages[1] = "[Select Destination]"; tempCount++; #else // Insert at position 0 (top) - for (int j = tempCount; j > 0; j--) tempMessages[j] = tempMessages[j - 1]; + for (int j = tempCount; j > 0; j--) + tempMessages[j] = tempMessages[j - 1]; tempMessages[0] = "[Select Destination]"; tempCount++; #endif @@ -132,13 +135,14 @@ int CannedMessageModule::splitConfiguredMessages() // Copy to the member array for (int k = 0; k < tempCount; ++k) { - this->messages[k] = (char*)tempMessages[k]; + this->messages[k] = (char *)tempMessages[k]; } this->messagesCount = tempCount; return this->messagesCount; } -void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char* buffer) { +void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer) +{ if (display->getWidth() > 128) { if (this->dest == NODENUM_BROADCAST) { display->drawStringf(x, y, buffer, "To: Broadcast@%s", channels.getName(this->channel)); @@ -154,7 +158,8 @@ void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, } } -void CannedMessageModule::resetSearch() { +void CannedMessageModule::resetSearch() +{ LOG_INFO("Resetting search, restoring full destination list"); int previousDestIndex = destIndex; @@ -165,14 +170,16 @@ void CannedMessageModule::resetSearch() { // Adjust scrollIndex so previousDestIndex is still visible int totalEntries = activeChannelIndices.size() + filteredNodes.size(); this->visibleRows = (displayHeight - FONT_HEIGHT_SMALL * 2) / FONT_HEIGHT_SMALL; - if (this->visibleRows < 1) this->visibleRows = 1; + if (this->visibleRows < 1) + this->visibleRows = 1; int maxScrollIndex = std::max(0, totalEntries - visibleRows); scrollIndex = std::min(std::max(previousDestIndex - (visibleRows / 2), 0), maxScrollIndex); lastUpdateMillis = millis(); requestFocus(); } -void CannedMessageModule::updateFilteredNodes() { +void CannedMessageModule::updateFilteredNodes() +{ static size_t lastNumMeshNodes = 0; static String lastSearchQuery = ""; @@ -181,7 +188,8 @@ void CannedMessageModule::updateFilteredNodes() { lastNumMeshNodes = numMeshNodes; // Early exit if nothing changed - if (searchQuery == lastSearchQuery && !nodesChanged) return; + if (searchQuery == lastSearchQuery && !nodesChanged) + return; lastSearchQuery = searchQuery; needsUpdate = false; @@ -196,10 +204,11 @@ void CannedMessageModule::updateFilteredNodes() { this->filteredNodes.reserve(numMeshNodes); for (size_t i = 0; i < numMeshNodes; ++i) { - meshtastic_NodeInfoLite* node = nodeDB->getMeshNodeByIndex(i); - if (!node || node->num == myNodeNum) continue; + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + if (!node || node->num == myNodeNum) + continue; - const String& nodeName = node->user.long_name; + const String &nodeName = node->user.long_name; if (searchQuery.length() == 0) { this->filteredNodes.push_back({node, sinceLastSeen(node)}); @@ -226,13 +235,13 @@ void CannedMessageModule::updateFilteredNodes() { } // Sort by favorite, then last heard - std::sort(this->filteredNodes.begin(), this->filteredNodes.end(), [](const NodeEntry& a, const NodeEntry& b) { + std::sort(this->filteredNodes.begin(), this->filteredNodes.end(), [](const NodeEntry &a, const NodeEntry &b) { if (a.node->is_favorite != b.node->is_favorite) return a.node->is_favorite > b.node->is_favorite; return a.lastHeard < b.lastHeard; }); - scrollIndex = 0; // Show first result at the top - destIndex = 0; // Highlight the first entry + scrollIndex = 0; // Show first result at the top + destIndex = 0; // Highlight the first entry if (nodesChanged) { LOG_INFO("Nodes changed, forcing UI refresh."); screen->forceDisplay(); @@ -240,24 +249,28 @@ void CannedMessageModule::updateFilteredNodes() { } // Returns true if character input is currently allowed (used for search/freetext states) -bool CannedMessageModule::isCharInputAllowed() const { - return runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || - runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; +bool CannedMessageModule::isCharInputAllowed() const +{ + return runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; } /** * Main input event dispatcher for CannedMessageModule. * Routes keyboard/button/touch input to the correct handler based on the current runState. * Only one handler (per state) processes each event, eliminating redundancy. */ -int CannedMessageModule::handleInputEvent(const InputEvent* event) { +int CannedMessageModule::handleInputEvent(const InputEvent *event) +{ // Allow input only from configured source (hardware/software filter) - if (!isInputSourceAllowed(event)) return 0; + if (!isInputSourceAllowed(event)) + return 0; // Global/system commands always processed (brightness, BT, GPS, shutdown, etc.) - if (handleSystemCommandInput(event)) return 1; + if (handleSystemCommandInput(event)) + return 1; // Tab key: Always allow switching between canned/destination screens - if (event->kbchar == INPUT_BROKER_MSG_TAB && handleTabSwitch(event)) return 1; + if (event->kbchar == INPUT_BROKER_MSG_TAB && handleTabSwitch(event)) + return 1; // Matrix keypad: If matrix key, trigger action select for canned message if (event->inputEvent == static_cast(MATRIXKEY)) { @@ -276,106 +289,106 @@ int CannedMessageModule::handleInputEvent(const InputEvent* event) { // Route event to handler for current UI state (no double-handling) switch (runState) { - // Node/Channel destination selection mode: Handles character search, arrows, select, cancel, backspace - case CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION: - return handleDestinationSelectionInput(event, isUp, isDown, isSelect); // All allowed input for this state + // Node/Channel destination selection mode: Handles character search, arrows, select, cancel, backspace + case CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION: + return handleDestinationSelectionInput(event, isUp, isDown, isSelect); // All allowed input for this state - // Free text input mode: Handles character input, cancel, backspace, select, etc. - case CANNED_MESSAGE_RUN_STATE_FREETEXT: - return handleFreeTextInput(event); // All allowed input for this state + // Free text input mode: Handles character input, cancel, backspace, select, etc. + case CANNED_MESSAGE_RUN_STATE_FREETEXT: + return handleFreeTextInput(event); // All allowed input for this state - // If sending, block all input except global/system (handled above) - case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE: - return 1; + // If sending, block all input except global/system (handled above) + case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE: + return 1; - case CANNED_MESSAGE_RUN_STATE_INACTIVE: - if (isSelect) { - // When inactive, call the onebutton shortpress instead. Activate module only on up/down - powerFSM.trigger(EVENT_PRESS); - return 1; // Let caller know we handled it - } - // Let LEFT/RIGHT pass through so frame navigation works - if ( - event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT) || - event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT) - ) { - break; - } - // Handle UP/DOWN: activate canned message list! - if ( - event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP) || - event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN) - ) { - // Always select the first real canned message on activation - int firstRealMsgIdx = 0; - for (int i = 0; i < messagesCount; ++i) { - if (strcmp(messages[i], "[Select Destination]") != 0 && - strcmp(messages[i], "[Exit]") != 0 && - strcmp(messages[i], "[---- Free Text ----]") != 0) { - firstRealMsgIdx = i; - break; - } + case CANNED_MESSAGE_RUN_STATE_INACTIVE: + if (isSelect) { + // When inactive, call the onebutton shortpress instead. Activate module only on up/down + powerFSM.trigger(EVENT_PRESS); + return 1; // Let caller know we handled it + } + // Let LEFT/RIGHT pass through so frame navigation works + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT) || + event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) { + break; + } + // Handle UP/DOWN: activate canned message list! + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP) || + event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN)) { + // Always select the first real canned message on activation + int firstRealMsgIdx = 0; + for (int i = 0; i < messagesCount; ++i) { + if (strcmp(messages[i], "[Select Destination]") != 0 && strcmp(messages[i], "[Exit]") != 0 && + strcmp(messages[i], "[---- Free Text ----]") != 0) { + firstRealMsgIdx = i; + break; } - currentMessageIndex = firstRealMsgIdx; - - // This triggers the canned message list - runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - return 1; } - // Printable char (ASCII) opens free text compose - if (event->kbchar >= 32 && event->kbchar <= 126) { - runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; - requestFocus(); - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - notifyObservers(&e); - // Immediately process the input in the new state (freetext) - return handleFreeTextInput(event); - } - break; + currentMessageIndex = firstRealMsgIdx; - // (Other states can be added here as needed) - default: - break; + // This triggers the canned message list + runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + return 1; + } + // Printable char (ASCII) opens free text compose + if (event->kbchar >= 32 && event->kbchar <= 126) { + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + requestFocus(); + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + notifyObservers(&e); + // Immediately process the input in the new state (freetext) + return handleFreeTextInput(event); + } + break; + + // (Other states can be added here as needed) + default: + break; } // If no state handler above processed the event, let the message selector try to handle it // (Handles up/down/select on canned message list, exit/return) - if (handleMessageSelectorInput(event, isUp, isDown, isSelect)) return 1; + if (handleMessageSelectorInput(event, isUp, isDown, isSelect)) + return 1; // Default: event not handled by canned message system, allow others to process return 0; } -bool CannedMessageModule::isInputSourceAllowed(const InputEvent* event) { +bool CannedMessageModule::isInputSourceAllowed(const InputEvent *event) +{ return strlen(moduleConfig.canned_message.allow_input_source) == 0 || strcasecmp(moduleConfig.canned_message.allow_input_source, event->source) == 0 || strcasecmp(moduleConfig.canned_message.allow_input_source, "_any") == 0; } -bool CannedMessageModule::isUpEvent(const InputEvent* event) { +bool CannedMessageModule::isUpEvent(const InputEvent *event) +{ return event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP); } -bool CannedMessageModule::isDownEvent(const InputEvent* event) { +bool CannedMessageModule::isDownEvent(const InputEvent *event) +{ return event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); } -bool CannedMessageModule::isSelectEvent(const InputEvent* event) { +bool CannedMessageModule::isSelectEvent(const InputEvent *event) +{ return event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT); } -bool CannedMessageModule::handleTabSwitch(const InputEvent* event) { - if (event->kbchar != 0x09) return false; +bool CannedMessageModule::handleTabSwitch(const InputEvent *event) +{ + if (event->kbchar != 0x09) + return false; - destSelect = (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) - ? CANNED_MESSAGE_DESTINATION_TYPE_NONE - : CANNED_MESSAGE_DESTINATION_TYPE_NODE; - runState = (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) - ? CANNED_MESSAGE_RUN_STATE_FREETEXT - : CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + destSelect = (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) ? CANNED_MESSAGE_DESTINATION_TYPE_NONE + : CANNED_MESSAGE_DESTINATION_TYPE_NODE; + runState = (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) ? CANNED_MESSAGE_RUN_STATE_FREETEXT + : CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; destIndex = 0; scrollIndex = 0; @@ -390,11 +403,11 @@ bool CannedMessageModule::handleTabSwitch(const InputEvent* event) { return true; } -int CannedMessageModule::handleDestinationSelectionInput(const InputEvent* event, bool isUp, bool isDown, bool isSelect) { +int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) +{ static bool shouldRedraw = false; - if (event->kbchar >= 32 && event->kbchar <= 126 && - !isUp && !isDown && + if (event->kbchar >= 32 && event->kbchar <= 126 && !isUp && !isDown && event->inputEvent != static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT) && event->inputEvent != static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT) && event->inputEvent != static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) { @@ -458,7 +471,7 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent* event } else { int nodeIndex = destIndex - static_cast(activeChannelIndices.size()); if (nodeIndex >= 0 && nodeIndex < static_cast(filteredNodes.size())) { - meshtastic_NodeInfoLite* selectedNode = filteredNodes[nodeIndex].node; + meshtastic_NodeInfoLite *selectedNode = filteredNodes[nodeIndex].node; if (selectedNode) { dest = selectedNode->num; channel = selectedNode->channel; @@ -489,8 +502,10 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent* event return 0; } -bool CannedMessageModule::handleMessageSelectorInput(const InputEvent* event, bool isUp, bool isDown, bool isSelect) { - if (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) return false; +bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) +{ + if (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) + return false; // === Handle Cancel key: go inactive, clear UI state === if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) { @@ -518,7 +533,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent* event, bo runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN; handled = true; } else if (isSelect) { - const char* current = messages[currentMessageIndex]; + const char *current = messages[currentMessageIndex]; // === [Select Destination] triggers destination selection UI === if (strcmp(current, "[Select Destination]") == 0) { @@ -579,9 +594,11 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent* event, bo return handled; } -bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) { +bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) +{ // Always process only if in FREETEXT mode - if (runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) return false; + if (runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) + return false; #if defined(USE_VIRTUAL_KEYBOARD) // Touch input (virtual keyboard) handling @@ -596,9 +613,9 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) { shift = !shift; valid = true; } else if (keyTapped == "⌫") { - #ifndef RAK14014 +#ifndef RAK14014 highlight = keyTapped[0]; - #endif +#endif payload = 0x08; shift = false; valid = true; @@ -608,13 +625,13 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) { charSet = (charSet == 0 ? 1 : 0); valid = true; } else if (keyTapped == " ") { - #ifndef RAK14014 +#ifndef RAK14014 highlight = keyTapped[0]; - #endif +#endif payload = keyTapped[0]; shift = false; valid = true; - } + } // Touch enter/submit else if (keyTapped == "↡") { runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; // Send the message! @@ -623,9 +640,9 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) { shift = false; valid = true; } else if (!keyTapped.isEmpty()) { - #ifndef RAK14014 +#ifndef RAK14014 highlight = keyTapped[0]; - #endif +#endif payload = shift ? keyTapped[0] : std::tolower(keyTapped[0]); shift = false; valid = true; @@ -643,11 +660,13 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) { // Confirm select (Enter) bool isSelect = isSelectEvent(event); if (isSelect) { - LOG_DEBUG("[SELECT] handleFreeTextInput: runState=%d, dest=%u, channel=%d, freetext='%s'", - (int)runState, dest, channel, freetext.c_str()); - if (dest == 0) dest = NODENUM_BROADCAST; + LOG_DEBUG("[SELECT] handleFreeTextInput: runState=%d, dest=%u, channel=%d, freetext='%s'", (int)runState, dest, channel, + freetext.c_str()); + if (dest == 0) + dest = NODENUM_BROADCAST; // Defensive: If channel isn't valid, pick the first available channel - if (channel >= channels.getNumChannels()) channel = 0; + if (channel >= channels.getNumChannels()) + channel = 0; payload = CANNED_MESSAGE_RUN_STATE_FREETEXT; currentMessageIndex = -1; @@ -706,9 +725,11 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) { return false; } -bool CannedMessageModule::handleSystemCommandInput(const InputEvent* event) { +bool CannedMessageModule::handleSystemCommandInput(const InputEvent *event) +{ // Only respond to "ANYKEY" events for system keys - if (event->inputEvent != static_cast(ANYKEY)) return false; + if (event->inputEvent != static_cast(ANYKEY)) + return false; // Block ALL input if an alert banner is active extern String alertBannerMessage; @@ -719,101 +740,114 @@ bool CannedMessageModule::handleSystemCommandInput(const InputEvent* event) { // System commands (all others fall through to return false) switch (event->kbchar) { - // Fn key symbols - case INPUT_BROKER_MSG_FN_SYMBOL_ON: - if (screen) screen->setFunctionSymbol("Fn"); - return true; - case INPUT_BROKER_MSG_FN_SYMBOL_OFF: - if (screen) screen->removeFunctionSymbol("Fn"); - return true; - // Brightness - case INPUT_BROKER_MSG_BRIGHTNESS_UP: - if (screen) screen->increaseBrightness(); - LOG_DEBUG("Increase Screen Brightness"); - return true; - case INPUT_BROKER_MSG_BRIGHTNESS_DOWN: - if (screen) screen->decreaseBrightness(); - LOG_DEBUG("Decrease Screen Brightness"); - return true; - // Mute - case INPUT_BROKER_MSG_MUTE_TOGGLE: - if (moduleConfig.external_notification.enabled && externalNotificationModule) { - bool isMuted = externalNotificationModule->getMute(); - externalNotificationModule->setMute(!isMuted); - graphics::isMuted = !isMuted; - if (!isMuted) - externalNotificationModule->stopNow(); - if (screen) - screen->showOverlayBanner(isMuted ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000); - } - return true; - // Bluetooth - case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: - config.bluetooth.enabled = !config.bluetooth.enabled; - LOG_INFO("User toggled Bluetooth"); - nodeDB->saveToDisk(); - #if defined(ARDUINO_ARCH_NRF52) - if (!config.bluetooth.enabled) { - disableBluetooth(); - if (screen) screen->showOverlayBanner("Bluetooth OFF\nRebooting", 3000); - rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 2000; - } else { - if (screen) screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000); - rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; - } - #else - if (!config.bluetooth.enabled) { - disableBluetooth(); - if (screen) screen->showOverlayBanner("Bluetooth OFF", 3000); - } else { - if (screen) screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000); - rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; - } - #endif - return true; - // GPS - case INPUT_BROKER_MSG_GPS_TOGGLE: - #if !MESHTASTIC_EXCLUDE_GPS - if (gps) { - gps->toggleGpsMode(); - const char* msg = (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) - ? "GPS Enabled" : "GPS Disabled"; - if (screen) { - screen->forceDisplay(); - screen->showOverlayBanner(msg, 3000); - } - } - #endif - return true; - // Mesh ping - case INPUT_BROKER_MSG_SEND_PING: - service->refreshLocalMeshNode(); - if (service->trySendPosition(NODENUM_BROADCAST, true)) { - if (screen) screen->showOverlayBanner("Position\nUpdate Sent", 3000); - } else { - if (screen) screen->showOverlayBanner("Node Info\nUpdate Sent", 3000); - } - return true; - // Power control - case INPUT_BROKER_MSG_SHUTDOWN: - if (screen) screen->showOverlayBanner("Shutting down..."); - nodeDB->saveToDisk(); - shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - return true; - case INPUT_BROKER_MSG_REBOOT: - if (screen) screen->showOverlayBanner("Rebooting...", 0); - nodeDB->saveToDisk(); + // Fn key symbols + case INPUT_BROKER_MSG_FN_SYMBOL_ON: + if (screen) + screen->setFunctionSymbol("Fn"); + return true; + case INPUT_BROKER_MSG_FN_SYMBOL_OFF: + if (screen) + screen->removeFunctionSymbol("Fn"); + return true; + // Brightness + case INPUT_BROKER_MSG_BRIGHTNESS_UP: + if (screen) + screen->increaseBrightness(); + LOG_DEBUG("Increase Screen Brightness"); + return true; + case INPUT_BROKER_MSG_BRIGHTNESS_DOWN: + if (screen) + screen->decreaseBrightness(); + LOG_DEBUG("Decrease Screen Brightness"); + return true; + // Mute + case INPUT_BROKER_MSG_MUTE_TOGGLE: + if (moduleConfig.external_notification.enabled && externalNotificationModule) { + bool isMuted = externalNotificationModule->getMute(); + externalNotificationModule->setMute(!isMuted); + graphics::isMuted = !isMuted; + if (!isMuted) + externalNotificationModule->stopNow(); + if (screen) + screen->showOverlayBanner(isMuted ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000); + } + return true; + // Bluetooth + case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: + config.bluetooth.enabled = !config.bluetooth.enabled; + LOG_INFO("User toggled Bluetooth"); + nodeDB->saveToDisk(); +#if defined(ARDUINO_ARCH_NRF52) + if (!config.bluetooth.enabled) { + disableBluetooth(); + if (screen) + screen->showOverlayBanner("Bluetooth OFF\nRebooting", 3000); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 2000; + } else { + if (screen) + screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000); rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - return true; - case INPUT_BROKER_MSG_DISMISS_FRAME: - runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; - if (screen) screen->dismissCurrentFrame(); - return true; - // Not a system command, let other handlers process it - default: - return false; + } +#else + if (!config.bluetooth.enabled) { + disableBluetooth(); + if (screen) + screen->showOverlayBanner("Bluetooth OFF", 3000); + } else { + if (screen) + screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + } +#endif + return true; + // GPS + case INPUT_BROKER_MSG_GPS_TOGGLE: +#if !MESHTASTIC_EXCLUDE_GPS + if (gps) { + gps->toggleGpsMode(); + const char *msg = + (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) ? "GPS Enabled" : "GPS Disabled"; + if (screen) { + screen->forceDisplay(); + screen->showOverlayBanner(msg, 3000); + } + } +#endif + return true; + // Mesh ping + case INPUT_BROKER_MSG_SEND_PING: + service->refreshLocalMeshNode(); + if (service->trySendPosition(NODENUM_BROADCAST, true)) { + if (screen) + screen->showOverlayBanner("Position\nUpdate Sent", 3000); + } else { + if (screen) + screen->showOverlayBanner("Node Info\nUpdate Sent", 3000); + } + return true; + // Power control + case INPUT_BROKER_MSG_SHUTDOWN: + if (screen) + screen->showOverlayBanner("Shutting down..."); + nodeDB->saveToDisk(); + shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000; + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + return true; + case INPUT_BROKER_MSG_REBOOT: + if (screen) + screen->showOverlayBanner("Rebooting...", 0); + nodeDB->saveToDisk(); + rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000; + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + return true; + case INPUT_BROKER_MSG_DISMISS_FRAME: + runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; + if (screen) + screen->dismissCurrentFrame(); + return true; + // Not a system command, let other handlers process it + default: + return false; } } @@ -834,9 +868,7 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); // Optionally add bell character - if (moduleConfig.canned_message.send_bell && - p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) - { + if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) { p->decoded.payload.bytes[p->decoded.payload.size++] = 7; // Bell p->decoded.payload.bytes[p->decoded.payload.size] = '\0'; // Null-terminate } @@ -845,8 +877,7 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha this->waitingForAck = true; // Log outgoing message - LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", - p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); + LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes); // Send to mesh and phone (even if no phone connected, to track ACKs) service->sendToMesh(p, RX_SRC_LOCAL, true); @@ -862,8 +893,8 @@ bool validEvent = false; unsigned long lastUpdateMillis = 0; int32_t CannedMessageModule::runOnce() { - #define NODE_UPDATE_IDLE_MS 100 - #define NODE_UPDATE_ACTIVE_MS 80 +#define NODE_UPDATE_IDLE_MS 100 +#define NODE_UPDATE_ACTIVE_MS 80 unsigned long updateThreshold = (searchQuery.length() > 0) ? NODE_UPDATE_ACTIVE_MS : NODE_UPDATE_IDLE_MS; if (needsUpdate && millis() - lastUpdateMillis > updateThreshold) { @@ -882,7 +913,8 @@ int32_t CannedMessageModule::runOnce() } UIFrameEvent e; if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) || - (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { + (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || + (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) { this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; temporaryMessage = ""; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; @@ -952,8 +984,7 @@ int32_t CannedMessageModule::runOnce() // Find first actual canned message (not a special action entry) int firstRealMsgIdx = 0; for (int i = 0; i < this->messagesCount; ++i) { - if (strcmp(this->messages[i], "[Select Destination]") != 0 && - strcmp(this->messages[i], "[Exit]") != 0 && + if (strcmp(this->messages[i], "[Select Destination]") != 0 && strcmp(this->messages[i], "[Exit]") != 0 && strcmp(this->messages[i], "[---- Free Text ----]") != 0) { firstRealMsgIdx = i; break; @@ -1021,24 +1052,23 @@ int32_t CannedMessageModule::runOnce() } break; case INPUT_BROKER_MSG_TAB: // Tab key (Switch to Destination Selection Mode) - { - if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) { - // Enter selection screen - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; - this->destIndex = 0; // Reset to first node/channel - this->scrollIndex = 0; // Reset scrolling - this->runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; - - // Ensure UI updates correctly - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->notifyObservers(&e); - } - - // If already inside the selection screen, do nothing (prevent exiting) - return 0; + { + if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) { + // Enter selection screen + this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; + this->destIndex = 0; // Reset to first node/channel + this->scrollIndex = 0; // Reset scrolling + this->runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + + // Ensure UI updates correctly + UIFrameEvent e; + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + this->notifyObservers(&e); } - break; + + // If already inside the selection screen, do nothing (prevent exiting) + return 0; + } break; case INPUT_BROKER_MSG_LEFT: case INPUT_BROKER_MSG_RIGHT: // already handled above @@ -1102,7 +1132,8 @@ const char *CannedMessageModule::getMessageByIndex(int index) const char *CannedMessageModule::getNodeName(NodeNum node) { - if (node == NODENUM_BROADCAST) return "Broadcast"; + if (node == NODENUM_BROADCAST) + return "Broadcast"; meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node); if (info && info->has_user && strlen(info->user.long_name) > 0) { @@ -1378,7 +1409,7 @@ bool CannedMessageModule::interceptingKeyboardInput() void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - this->displayHeight = display->getHeight(); // Store display height for later use + this->displayHeight = display->getHeight(); // Store display height for later use char buffer[50]; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); @@ -1394,7 +1425,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st } // === Destination Selection === - if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { + if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || + this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { requestFocus(); display->setColor(WHITE); // Always draw cleanly display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -1414,15 +1446,19 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st int totalEntries = numActiveChannels + this->filteredNodes.size(); int columns = 1; this->visibleRows = (display->getHeight() - (titleY + FONT_HEIGHT_SMALL)) / (FONT_HEIGHT_SMALL - 4); - if (this->visibleRows < 1) this->visibleRows = 1; + if (this->visibleRows < 1) + this->visibleRows = 1; // === Clamp scrolling === - if (scrollIndex > totalEntries / columns) scrollIndex = totalEntries / columns; - if (scrollIndex < 0) scrollIndex = 0; + if (scrollIndex > totalEntries / columns) + scrollIndex = totalEntries / columns; + if (scrollIndex < 0) + scrollIndex = 0; for (int row = 0; row < visibleRows; row++) { int itemIndex = scrollIndex + row; - if (itemIndex >= totalEntries) break; + if (itemIndex >= totalEntries) + break; int xOffset = 0; int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset; @@ -1444,7 +1480,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st } } - if (entryText.length() == 0 || entryText == "Unknown") entryText = "?"; + if (entryText.length() == 0 || entryText == "Unknown") + entryText = "?"; // === Highlight background (if selected) === if (itemIndex == destIndex) { @@ -1496,22 +1533,21 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st EINK_ADD_FRAMEFLAG(display, COSMETIC); display->setTextAlignment(TEXT_ALIGN_CENTER); - #ifdef USE_EINK +#ifdef USE_EINK display->setFont(FONT_SMALL); int yOffset = y + 10; - #else +#else display->setFont(FONT_MEDIUM); int yOffset = y + 10; - #endif +#endif // --- Delivery Status Message --- if (this->ack) { if (this->lastSentNode == NODENUM_BROADCAST) { snprintf(buffer, sizeof(buffer), "Broadcast Sent to\n%s", channels.getName(this->channel)); } else if (this->lastAckHopLimit > this->lastAckHopStart) { - snprintf(buffer, sizeof(buffer), "Delivered (%d hops)\nto %s", - this->lastAckHopLimit - this->lastAckHopStart, - getNodeName(this->incoming)); + snprintf(buffer, sizeof(buffer), "Delivered (%d hops)\nto %s", this->lastAckHopLimit - this->lastAckHopStart, + getNodeName(this->incoming)); } else { snprintf(buffer, sizeof(buffer), "Delivered\nto %s", getNodeName(this->incoming)); } @@ -1522,20 +1558,21 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st // Draw delivery message and compute y-offset after text height int lineCount = 1; for (const char *ptr = buffer; *ptr; ptr++) { - if (*ptr == '\n') lineCount++; + if (*ptr == '\n') + lineCount++; } display->drawString(display->getWidth() / 2 + x, yOffset, buffer); yOffset += lineCount * FONT_HEIGHT_MEDIUM; // only 1 line gap, no extra padding - #ifndef USE_EINK +#ifndef USE_EINK // --- SNR + RSSI Compact Line --- if (this->ack) { display->setFont(FONT_SMALL); snprintf(buffer, sizeof(buffer), "SNR: %.1f dB RSSI: %d", this->lastRxSnr, this->lastRxRssi); display->drawString(display->getWidth() / 2 + x, yOffset, buffer); } - #endif +#endif return; } @@ -1566,7 +1603,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { requestFocus(); #if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY) - EInkDynamicDisplay* einkDisplay = static_cast(display); + EInkDynamicDisplay *einkDisplay = static_cast(display); einkDisplay->enableUnlimitedFastMode(); #endif #if defined(USE_VIRTUAL_KEYBOARD) @@ -1575,25 +1612,26 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - // --- Draw node/channel header at the top --- - drawHeader(display, x, y, buffer); + // --- Draw node/channel header at the top --- + drawHeader(display, x, y, buffer); // --- Char count right-aligned --- if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) { - uint16_t charsLeft = meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0); + uint16_t charsLeft = + meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0); snprintf(buffer, sizeof(buffer), "%d left", charsLeft); display->drawString(x + display->getWidth() - display->getStringWidth(buffer), y + 0, buffer); } - // --- Draw Free Text input, shifted down --- + // --- Draw Free Text input, shifted down --- display->setColor(WHITE); display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), - drawWithCursor(this->freetext, this->cursor)); + drawWithCursor(this->freetext, this->cursor)); #endif return; } -// === Canned Messages List === + // === Canned Messages List === if (this->messagesCount > 0) { display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); @@ -1607,13 +1645,12 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st const int listYOffset = y + FONT_HEIGHT_SMALL - 3; const int visibleRows = (display->getHeight() - listYOffset) / rowSpacing; - int topMsg = (messagesCount > visibleRows && currentMessageIndex >= visibleRows - 1) - ? currentMessageIndex - visibleRows + 2 - : 0; + int topMsg = + (messagesCount > visibleRows && currentMessageIndex >= visibleRows - 1) ? currentMessageIndex - visibleRows + 2 : 0; for (int i = 0; i < std::min(messagesCount, visibleRows); i++) { int lineY = listYOffset + rowSpacing * i; - const char* msg = getMessageByIndex(topMsg + i); + const char *msg = getMessageByIndex(topMsg + i); if ((topMsg + i) == currentMessageIndex) { #ifdef USE_EINK @@ -1793,6 +1830,7 @@ String CannedMessageModule::drawWithCursor(String text, int cursor) #endif -bool CannedMessageModule::isInterceptingAndFocused() { +bool CannedMessageModule::isInterceptingAndFocused() +{ return this->interceptingKeyboardInput(); } \ No newline at end of file diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index 6caf7c9cf..563e8b17c 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -57,10 +57,9 @@ struct NodeEntry { // Main Class // ============================ -class CannedMessageModule : public SinglePortModule, - public Observable, - private concurrency::OSThread { -public: +class CannedMessageModule : public SinglePortModule, public Observable, private concurrency::OSThread +{ + public: CannedMessageModule(); // === Message navigation === @@ -89,19 +88,22 @@ public: #endif // === Packet Interest Filter === - virtual bool wantPacket(const meshtastic_MeshPacket *p) override { - if (p->rx_rssi != 0) lastRxRssi = p->rx_rssi; - if (p->rx_snr > 0) lastRxSnr = p->rx_snr; + virtual bool wantPacket(const meshtastic_MeshPacket *p) override + { + if (p->rx_rssi != 0) + lastRxRssi = p->rx_rssi; + if (p->rx_snr > 0) + lastRxSnr = p->rx_snr; return (p->decoded.portnum == meshtastic_PortNum_ROUTING_APP) ? waitingForAck : false; } -protected: + protected: // === Thread Entry Point === virtual int32_t runOnce() override; // === Transmission === void sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies); - void drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char* buffer); + void drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer); int splitConfiguredMessages(); int getNextIndex(); int getPrevIndex(); @@ -130,7 +132,7 @@ protected: bool saveProtoForModule(); void installDefaultCannedMessageModuleConfig(); -private: + private: // === Input Observers === CallbackObserver inputObserver = CallbackObserver(this, &CannedMessageModule::handleInputEvent); @@ -155,19 +157,19 @@ private: int currentMessageIndex = -1; // === Routing & Acknowledgment === - NodeNum dest = NODENUM_BROADCAST; // Destination node for outgoing messages (default: broadcast) - NodeNum incoming = NODENUM_BROADCAST; // Source node from which last ACK/NACK was received - 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 + NodeNum dest = NODENUM_BROADCAST; // Destination node for outgoing messages (default: broadcast) + NodeNum incoming = NODENUM_BROADCAST; // Source node from which last ACK/NACK was received + 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 + 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) + 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) // === State Tracking === cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 74621015a..ea796fca6 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -11,16 +11,16 @@ #include "RTC.h" #include "Router.h" #include "UnitConversions.h" +#include "buzz.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" #include "main.h" +#include "modules/ExternalNotificationModule.h" #include "power.h" #include "sleep.h" #include "target_specific.h" #include #include -#include "graphics/SharedUIDisplay.h" -#include "graphics/images.h" -#include "buzz.h" -#include "modules/ExternalNotificationModule.h" #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL @@ -30,8 +30,9 @@ #include "Sensor/RCWL9620Sensor.h" #include "Sensor/nullSensor.h" -namespace graphics { - extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y); +namespace graphics +{ +extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y); } #if __has_include() #include "Sensor/AHT10.h" @@ -358,9 +359,9 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt display->setColor(BLACK); display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(centerX, titleY, titleStr); // Centered title + display->drawString(centerX, titleY, titleStr); // Centered title if (config.display.heading_bold) - display->drawString(centerX + 1, titleY, titleStr); // Bold effect via 1px offset + display->drawString(centerX + 1, titleY, titleStr); // Bold effect via 1px offset // Restore text color & alignment display->setColor(WHITE); @@ -387,9 +388,8 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt const auto &m = telemetry.variant.environment_metrics; // Check if any telemetry field has valid data - bool hasAny = m.has_temperature || m.has_relative_humidity || m.barometric_pressure != 0 || - m.iaq != 0 || m.voltage != 0 || m.current != 0 || m.lux != 0 || - m.white_lux != 0 || m.weight != 0 || m.distance != 0 || m.radiation != 0; + bool hasAny = m.has_temperature || m.has_relative_humidity || m.barometric_pressure != 0 || m.iaq != 0 || m.voltage != 0 || + m.current != 0 || m.lux != 0 || m.white_lux != 0 || m.weight != 0 || m.distance != 0 || m.radiation != 0; if (!hasAny) { display->drawString(x, currentY, "No Telemetry"); @@ -399,21 +399,21 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt // === First line: Show sender name + time since received (left), and first metric (right) === const char *sender = getSenderShortName(*lastMeasurementPacket); uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket); - String agoStr = (agoSecs > 864000) ? "?" : - (agoSecs > 3600) ? String(agoSecs / 3600) + "h" : - (agoSecs > 60) ? String(agoSecs / 60) + "m" : - String(agoSecs) + "s"; + String agoStr = (agoSecs > 864000) ? "?" + : (agoSecs > 3600) ? String(agoSecs / 3600) + "h" + : (agoSecs > 60) ? String(agoSecs / 60) + "m" + : String(agoSecs) + "s"; String leftStr = String(sender) + " (" + agoStr + ")"; - display->drawString(x, currentY, leftStr); // Left side: who and when + display->drawString(x, currentY, leftStr); // Left side: who and when // === Collect sensor readings as label strings (no icons) === std::vector entries; if (m.has_temperature) { String tempStr = moduleConfig.telemetry.environment_display_fahrenheit - ? "Tmp: " + String(UnitConversions::CelsiusToFahrenheit(m.temperature), 1) + "Β°F" - : "Tmp: " + String(m.temperature, 1) + "Β°C"; + ? "Tmp: " + String(UnitConversions::CelsiusToFahrenheit(m.temperature), 1) + "Β°F" + : "Tmp: " + String(m.temperature, 1) + "Β°C"; entries.push_back(tempStr); } if (m.has_relative_humidity) @@ -422,21 +422,23 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa"); if (m.iaq != 0) { String aqi = "IAQ: " + String(m.iaq); - const char *bannerMsg = nullptr; // Default: no banner + const char *bannerMsg = nullptr; // Default: no banner - if (m.iaq <= 25) aqi += " (Excellent)"; - else if (m.iaq <= 50) aqi += " (Good)"; - else if (m.iaq <= 100) aqi += " (Moderate)"; - else if (m.iaq <= 150) aqi += " (Poor)"; + if (m.iaq <= 25) + aqi += " (Excellent)"; + else if (m.iaq <= 50) + aqi += " (Good)"; + else if (m.iaq <= 100) + aqi += " (Moderate)"; + else if (m.iaq <= 150) + aqi += " (Poor)"; else if (m.iaq <= 200) { aqi += " (Unhealthy)"; bannerMsg = "Unhealthy IAQ"; - } - else if (m.iaq <= 300) { + } else if (m.iaq <= 300) { aqi += " (Very Unhealthy)"; bannerMsg = "Very Unhealthy IAQ"; - } - else { + } else { aqi += " (Hazardous)"; bannerMsg = "Hazardous IAQ"; } @@ -480,7 +482,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt String valueStr = entries.front(); int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr); display->drawString(rightX, currentY, valueStr); - entries.erase(entries.begin()); // Remove from queue + entries.erase(entries.begin()); // Remove from queue } // === Advance to next line for remaining telemetry entries === diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index b99e5f91f..e42d718a5 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -10,11 +10,11 @@ #include "PowerTelemetry.h" #include "RTC.h" #include "Router.h" +#include "graphics/SharedUIDisplay.h" #include "main.h" #include "power.h" #include "sleep.h" #include "target_specific.h" -#include "graphics/SharedUIDisplay.h" #define FAILED_STATE_SENSOR_READ_MULTIPLIER 10 #define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true @@ -22,8 +22,9 @@ #include "graphics/ScreenFonts.h" #include -namespace graphics { - extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y); +namespace graphics +{ +extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y); } int32_t PowerTelemetryModule::runOnce() @@ -112,7 +113,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - graphics::drawCommonHeader(display, x, y); // Shared UI header + graphics::drawCommonHeader(display, x, y); // Shared UI header // === Draw title (aligned with header baseline) === const int highlightHeight = FONT_HEIGHT_SMALL - 1; diff --git a/src/shutdown.h b/src/shutdown.h index cc6b0e680..998944677 100644 --- a/src/shutdown.h +++ b/src/shutdown.h @@ -42,7 +42,7 @@ void powerCommandsCheck() #if defined(ARCH_ESP32) || defined(ARCH_NRF52) if (shutdownAtMsec && screen) { - screen->showOverlayBanner("Shutting Down...", 0); // stays on screen + screen->showOverlayBanner("Shutting Down...", 0); // stays on screen } #endif From 749c3ca53cfbf7b8cdba441ad6b2fb48b7e54c71 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Fri, 30 May 2025 09:51:29 -0500 Subject: [PATCH 182/265] Add missed merge line --- src/main.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 7cbf77c83..fafa3812b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1231,7 +1231,9 @@ void setup() if (!rIf->reconfigure()) { LOG_WARN("Reconfigure failed, rebooting"); - screen->startAlert("Rebooting..."); + if (screen) { + screen->showOverlayBanner("Rebooting..."); + } rebootAtMsec = millis() + 5000; } } From 218f5bdbf32e0ff599a55791b5fb39bd720f3f7c Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Fri, 30 May 2025 11:14:14 -0500 Subject: [PATCH 183/265] Move keyVerification messages to showOverlayBanner --- src/modules/KeyVerificationModule.cpp | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/modules/KeyVerificationModule.cpp b/src/modules/KeyVerificationModule.cpp index 062d79b0f..e0f415408 100644 --- a/src/modules/KeyVerificationModule.cpp +++ b/src/modules/KeyVerificationModule.cpp @@ -59,7 +59,7 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket & r->hash1.size == 0) { memcpy(hash2, r->hash2.bytes, 32); if (screen) - screen->startAlert("Enter Security Number"); // TODO: replace with actual prompt in BaseUI + screen->showOverlayBanner("Enter Security Number", 15000); meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_WARNING; @@ -81,8 +81,7 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket & generateVerificationCode(message + 15); LOG_INFO("Hash1 matches!"); if (screen) { - screen->endAlert(); - screen->startAlert(message); + screen->showOverlayBanner(message, 15000); } meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_WARNING; @@ -180,7 +179,7 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply() responsePacket->pki_encrypted = true; if (screen) { snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000); - screen->startAlert(message); + screen->showOverlayBanner(message, 15000); LOG_WARN("%s", message); } meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); @@ -250,8 +249,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber) sprintf(message, "Verification: \n"); generateVerificationCode(message + 15); // send the toPhone packet if (screen) { - screen->endAlert(); - screen->startAlert(message); + screen->showOverlayBanner(message, 15000); } meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); cn->level = meshtastic_LogRecord_Level_WARNING; @@ -287,8 +285,6 @@ void KeyVerificationModule::resetToIdle() currentSecurityNumber = 0; currentRemoteNode = 0; currentState = KEY_VERIFICATION_IDLE; - if (screen) - screen->endAlert(); } void KeyVerificationModule::generateVerificationCode(char *readableCode) From 3df894106bf9a31c2e36f2e6ac9817fd8b9e4664 Mon Sep 17 00:00:00 2001 From: Jason P Date: Fri, 30 May 2025 11:34:45 -0500 Subject: [PATCH 184/265] Update alignments and spacing (#6924) -Update alignment on node list -Adjust signal bar baseline --- src/graphics/Screen.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 9e38f46b1..9c018ad70 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -2420,7 +2420,7 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawString(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nodeName); + display->drawString(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nodeName); if (node->is_favorite) { if (SCREEN_WIDTH > 128) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); @@ -2444,8 +2444,8 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int int nameMaxWidth = columnWidth - 25; int barsOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 16 : 20) // Offset for Wide Screens (Left Column:Right Column) : (isLeftCol ? 15 : 19); // Offset for Narrow Screens (Left Column:Right Column) - int hopOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 22 : 28) // Offset for Wide Screens (Left Column:Right Column) - : (isLeftCol ? 18 : 20); // Offset for Narrow Screens (Left Column:Right Column) + int hopOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 17 : 25) // Offset for Wide Screens (Left Column:Right Column) + : (isLeftCol ? 13 : 17); // Offset for Narrow Screens (Left Column:Right Column) int barsXOffset = columnWidth - barsOffset; String nodeName = getSafeNodeName(node); @@ -2453,7 +2453,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nameMaxWidth, nodeName); + display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName); if (node->is_favorite) { if (SCREEN_WIDTH > 128) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); @@ -2475,7 +2475,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; int barWidth = 2; int barStartX = x + barsXOffset; - int barStartY = y + (FONT_HEIGHT_SMALL / 2) + 2; + int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2; for (int b = 0; b < 4; b++) { if (b < bars) { @@ -2546,7 +2546,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nameMaxWidth, nodeName); + display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName); if (node->is_favorite) { if (SCREEN_WIDTH > 128) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); @@ -2677,7 +2677,7 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nameMaxWidth, nodeName); + display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName); if (node->is_favorite) { if (SCREEN_WIDTH > 128) { drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); From 0edccf5b86e0742ec5e7595370154775609198e5 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sat, 31 May 2025 06:21:55 -0500 Subject: [PATCH 185/265] Do position broadcast on secondary channel if disabled on primary (#6920) * Do default position broadcast on secondary channel if disabled on primary * Consolidate the ifs * For Loops, how do they work? --- src/modules/PositionModule.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp index 0b1bdcc46..88bcdb016 100644 --- a/src/modules/PositionModule.cpp +++ b/src/modules/PositionModule.cpp @@ -328,7 +328,13 @@ void PositionModule::sendOurPosition() // If we changed channels, ask everyone else for their latest info LOG_INFO("Send pos@%x:6 to mesh (wantReplies=%d)", localPosition.timestamp, requestReplies); - sendOurPosition(NODENUM_BROADCAST, requestReplies); + for (uint8_t channelNum = 0; channelNum < 8; channelNum++) { + if (channels.getByIndex(channelNum).settings.has_module_settings && + channels.getByIndex(channelNum).settings.module_settings.position_precision != 0) { + sendOurPosition(NODENUM_BROADCAST, requestReplies, channelNum); + return; + } + } } void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t channel) @@ -340,11 +346,6 @@ void PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t cha // Set's the class precision value for this particular packet if (channels.getByIndex(channel).settings.has_module_settings) { precision = channels.getByIndex(channel).settings.module_settings.position_precision; - } else if (channels.getByIndex(channel).role == meshtastic_Channel_Role_PRIMARY) { - // backwards compatibility for Primary channels created before position_precision was set by default - precision = 13; - } else { - precision = 0; } meshtastic_MeshPacket *p = allocPositionPacket(); From 77e7235ce5d116ef1442bbf8d288a0e577a2f4d3 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 31 May 2025 19:31:45 -0500 Subject: [PATCH 186/265] Screen refactor / move to renderers (#6932) * WIP Screen.cpp refactoring * WIP * Notification and time * Draw nodes and device focused * Namespacing and more moved methods * Move EInk ones * Eink fixes * Remove useless wrapper functions * Update alignments and spacing * Update src/graphics/draw/NotificationRenderer.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fully qualify * Move drawfunctionoverlay * Put the imperial back * CompassRenderer methods * Moar * Another * Fixed compassarrow renderer * Draw columns * Name cleanup --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/graphics/Screen.cpp | 3127 +------------------- src/graphics/Screen.h | 29 +- src/graphics/ScreenGlobals.cpp | 6 + src/graphics/draw/ClockRenderer.h | 40 + src/graphics/draw/CompassRenderer.cpp | 132 + src/graphics/draw/CompassRenderer.h | 36 + src/graphics/draw/DebugRenderer.cpp | 671 +++++ src/graphics/draw/DebugRenderer.h | 38 + src/graphics/draw/DrawRenderers.h | 38 + src/graphics/draw/MessageRenderer.cpp | 448 +++ src/graphics/draw/MessageRenderer.h | 18 + src/graphics/draw/NodeListRenderer.cpp | 877 ++++++ src/graphics/draw/NodeListRenderer.h | 71 + src/graphics/draw/NotificationRenderer.cpp | 177 ++ src/graphics/draw/NotificationRenderer.h | 24 + src/graphics/draw/UIRenderer.cpp | 1254 ++++++++ src/graphics/draw/UIRenderer.h | 92 + src/modules/WaypointModule.cpp | 11 +- src/motion/MotionSensor.cpp | 5 +- 19 files changed, 3982 insertions(+), 3112 deletions(-) create mode 100644 src/graphics/ScreenGlobals.cpp create mode 100644 src/graphics/draw/ClockRenderer.h create mode 100644 src/graphics/draw/CompassRenderer.cpp create mode 100644 src/graphics/draw/CompassRenderer.h create mode 100644 src/graphics/draw/DebugRenderer.cpp create mode 100644 src/graphics/draw/DebugRenderer.h create mode 100644 src/graphics/draw/DrawRenderers.h create mode 100644 src/graphics/draw/MessageRenderer.cpp create mode 100644 src/graphics/draw/MessageRenderer.h create mode 100644 src/graphics/draw/NodeListRenderer.cpp create mode 100644 src/graphics/draw/NodeListRenderer.h create mode 100644 src/graphics/draw/NotificationRenderer.cpp create mode 100644 src/graphics/draw/NotificationRenderer.h create mode 100644 src/graphics/draw/UIRenderer.cpp create mode 100644 src/graphics/draw/UIRenderer.h diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 9c018ad70..22842adaf 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -28,6 +28,11 @@ along with this program. If not, see . #include #include "DisplayFormatters.h" +#include "draw/DebugRenderer.h" +#include "draw/MessageRenderer.h" +#include "draw/NodeListRenderer.h" +#include "draw/NotificationRenderer.h" +#include "draw/UIRenderer.h" #if !MESHTASTIC_EXCLUDE_GPS #include "GPS.h" #endif @@ -66,8 +71,6 @@ using graphics::numEmotes; #endif #ifdef ARCH_ESP32 -#include "esp_task_wdt.h" -#include "modules/StoreForwardModule.h" #endif #if ARCH_PORTDUINO @@ -99,8 +102,6 @@ static uint32_t alertBannerUntil = 0; uint32_t logo_timeout = 5000; // 4 seconds for EACH logo -uint32_t hours_in_month = 730; - // This image definition is here instead of images.h because it's modified dynamically by the drawBattery function uint8_t imgBattery[16] = {0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xE7, 0x3C}; @@ -111,13 +112,15 @@ uint32_t dopThresholds[5] = {2000, 1000, 500, 200, 100}; // we'll need to hold onto pointers for the modules that can draw a frame. std::vector moduleFrames; -// Stores the last 4 of our hardware ID, to make finding the device for pairing easier -static char ourId[5]; - -// vector where symbols (string) are displayed in bottom corner of display. +// Global variables for screen function overlay symbols std::vector functionSymbol; -// string displayed in bottom right corner of display. Created from elements in functionSymbol vector -std::string functionSymbolString = ""; +std::string functionSymbolString; + +// Stores the last 4 of our hardware ID, to make finding the device for pairing easier +// FIXME: Needs refactoring and getMacAddr needs to be moved to a utility class +extern "C" { +char ourId[5]; +} #if HAS_GPS // GeoCoord object for the screen @@ -131,291 +134,11 @@ static bool heartbeat = false; #include "graphics/ScreenFonts.h" #include -// Start Functions to write date/time to the screen -#include // Only needed if you're using std::string elsewhere - -bool isLeapYear(int year) -{ - return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); -} - -const int daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - -// Fills the buffer with a formatted date/time string and returns pixel width -int formatDateTime(char *buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay *display, bool includeTime) -{ - int sec = rtc_sec % 60; - rtc_sec /= 60; - int min = rtc_sec % 60; - rtc_sec /= 60; - int hour = rtc_sec % 24; - rtc_sec /= 24; - - int year = 1970; - while (true) { - int daysInYear = isLeapYear(year) ? 366 : 365; - if (rtc_sec >= (uint32_t)daysInYear) { - rtc_sec -= daysInYear; - year++; - } else { - break; - } - } - - int month = 0; - while (month < 12) { - int dim = daysInMonth[month]; - if (month == 1 && isLeapYear(year)) - dim++; - if (rtc_sec >= (uint32_t)dim) { - rtc_sec -= dim; - month++; - } else { - break; - } - } - - int day = rtc_sec + 1; - - if (includeTime) { - snprintf(buf, bufSize, "%04d-%02d-%02d %02d:%02d:%02d", year, month + 1, day, hour, min, sec); - } else { - snprintf(buf, bufSize, "%04d-%02d-%02d", year, month + 1, day); - } - - return display->getStringWidth(buf); -} - // Usage: int stringWidth = formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display); // End Functions to write date/time to the screen -void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display) -{ - for (int row = 0; row < height; row++) { - uint8_t rowMask = (1 << row); - for (int col = 0; col < width; col++) { - uint8_t colData = pgm_read_byte(&bitmapXBM[col]); - if (colData & rowMask) { - // Note: rows become X, columns become Y after transpose - display->fillRect(x + row * 2, y + col * 2, 2, 2); - } - } - } -} - -#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) - -// Check if the display can render a string (detect special chars; emoji) -static bool haveGlyphs(const char *str) -{ -#if defined(OLED_PL) || defined(OLED_UA) || defined(OLED_RU) || defined(OLED_CS) - // Don't want to make any assumptions about custom language support - return true; -#endif - - // Check each character with the lookup function for the OLED library - // We're not really meant to use this directly.. - bool have = true; - for (uint16_t i = 0; i < strlen(str); i++) { - uint8_t result = Screen::customFontTableLookup((uint8_t)str[i]); - // If font doesn't support a character, it is substituted for ΒΏ - if (result == 191 && (uint8_t)str[i] != 191) { - have = false; - break; - } - } - - // LOG_DEBUG("haveGlyphs=%d", have); - return have; -} extern bool hasUnreadMessage; -/** - * Draw the icon with extra info printed around the corners - */ -static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - const char *label = "BaseUI"; - display->setFont(FONT_SMALL); - int textWidth = display->getStringWidth(label); - int r = 3; // corner radius - if (SCREEN_WIDTH > 128) { - // === ORIGINAL WIDE SCREEN LAYOUT (unchanged) === - int padding = 4; - int boxWidth = max(icon_width, textWidth) + (padding * 2) + 16; - int boxHeight = icon_height + FONT_HEIGHT_SMALL + (padding * 3) - 8; - int boxX = x - 1 + (SCREEN_WIDTH - boxWidth) / 2; - int boxY = y - 6 + (SCREEN_HEIGHT - boxHeight) / 2; - - display->setColor(WHITE); - display->fillRect(boxX + r, boxY, boxWidth - 2 * r, boxHeight); - display->fillRect(boxX, boxY + r, boxWidth - 1, boxHeight - 2 * r); - display->fillCircle(boxX + r, boxY + r, r); // Upper Left - display->fillCircle(boxX + boxWidth - r - 1, boxY + r, r); // Upper Right - display->fillCircle(boxX + r, boxY + boxHeight - r - 1, r); // Lower Left - display->fillCircle(boxX + boxWidth - r - 1, boxY + boxHeight - r - 1, r); // Lower Right - - display->setColor(BLACK); - int iconX = boxX + (boxWidth - icon_width) / 2; - int iconY = boxY + padding - 2; - display->drawXbm(iconX, iconY, icon_width, icon_height, icon_bits); - - int labelY = iconY + icon_height + padding; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(x + SCREEN_WIDTH / 2 - 3, labelY, label); - display->drawString(x + SCREEN_WIDTH / 2 - 2, labelY, label); // faux bold - - } else { - // === TIGHT SMALL SCREEN LAYOUT === - int iconY = y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2; - iconY -= 4; - - int labelY = iconY + icon_height - 2; - - int boxWidth = max(icon_width, textWidth) + 4; - int boxX = x + (SCREEN_WIDTH - boxWidth) / 2; - int boxY = iconY - 1; - int boxBottom = labelY + FONT_HEIGHT_SMALL - 2; - int boxHeight = boxBottom - boxY; - - display->setColor(WHITE); - display->fillRect(boxX + r, boxY, boxWidth - 2 * r, boxHeight); - display->fillRect(boxX, boxY + r, boxWidth - 1, boxHeight - 2 * r); - display->fillCircle(boxX + r, boxY + r, r); - display->fillCircle(boxX + boxWidth - r - 1, boxY + r, r); - display->fillCircle(boxX + r, boxY + boxHeight - r - 1, r); - display->fillCircle(boxX + boxWidth - r - 1, boxY + boxHeight - r - 1, r); - - display->setColor(BLACK); - int iconX = boxX + (boxWidth - icon_width) / 2; - display->drawXbm(iconX, iconY, icon_width, icon_height, icon_bits); - - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(x + SCREEN_WIDTH / 2, labelY, label); - } - - // === Footer and headers (shared) === - display->setFont(FONT_MEDIUM); - display->setColor(WHITE); - display->setTextAlignment(TEXT_ALIGN_LEFT); - const char *title = "meshtastic.org"; - display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); - - display->setFont(FONT_SMALL); - if (upperMsg) - display->drawString(x + 0, y + 0, upperMsg); - - char buf[25]; - snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(x + SCREEN_WIDTH, y + 0, buf); - - screen->forceDisplay(); - display->setTextAlignment(TEXT_ALIGN_LEFT); -} - -#ifdef USERPREFS_OEM_TEXT - -static void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA; - display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, - y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, - USERPREFS_OEM_IMAGE_HEIGHT, xbm); - - switch (USERPREFS_OEM_FONT_SIZE) { - case 0: - display->setFont(FONT_SMALL); - break; - case 2: - display->setFont(FONT_LARGE); - break; - default: - display->setFont(FONT_MEDIUM); - break; - } - - display->setTextAlignment(TEXT_ALIGN_LEFT); - const char *title = USERPREFS_OEM_TEXT; - display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); - display->setFont(FONT_SMALL); - - // Draw region in upper left - if (upperMsg) - display->drawString(x + 0, y + 0, upperMsg); - - // Draw version and shortname in upper right - char buf[25]; - snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); - - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(x + SCREEN_WIDTH, y + 0, buf); - screen->forceDisplay(); - - display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code -} - -static void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - // Draw region in upper left - const char *region = myRegion ? myRegion->name : NULL; - drawOEMIconScreen(region, display, state, x, y); -} - -#endif - -void Screen::drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *message) -{ - uint16_t x_offset = display->width() / 2; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(x_offset + x, 26 + y, message); -} - -// Used on boot when a certificate is being created -static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_SMALL); - display->drawString(64 + x, y, "Creating SSL certificate"); - -#ifdef ARCH_ESP32 - yield(); - esp_task_wdt_reset(); -#endif - - display->setFont(FONT_SMALL); - if ((millis() / 1000) % 2) { - display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . ."); - } else { - display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . "); - } -} - -// Used when booting without a region set -static void drawWelcomeScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(64 + x, y, "//\\ E S H T /\\ S T / C"); - display->drawString(64 + x, y + FONT_HEIGHT_SMALL, getDeviceName()); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - if ((millis() / 10000) % 2) { - display->drawString(x, y + FONT_HEIGHT_SMALL * 2 - 3, "Set the region using the"); - display->drawString(x, y + FONT_HEIGHT_SMALL * 3 - 3, "Meshtastic Android, iOS,"); - display->drawString(x, y + FONT_HEIGHT_SMALL * 4 - 3, "Web or CLI clients."); - } else { - display->drawString(x, y + FONT_HEIGHT_SMALL * 2 - 3, "Visit meshtastic.org"); - display->drawString(x, y + FONT_HEIGHT_SMALL * 3 - 3, "for more information."); - display->drawString(x, y + FONT_HEIGHT_SMALL * 4 - 3, ""); - } - -#ifdef ARCH_ESP32 - yield(); - esp_task_wdt_reset(); -#endif -} // ============================== // Overlay Alert Banner Renderer // ============================== @@ -430,160 +153,6 @@ void Screen::showOverlayBanner(const String &message, uint32_t durationMs) alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; } -// Draws the overlay banner on screen, if still within display duration -static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) -{ - // Exit if no message is active or duration has passed - if (alertBannerMessage.length() == 0 || (alertBannerUntil != 0 && millis() > alertBannerUntil)) - return; - - // === Layout Configuration === - constexpr uint16_t padding = 5; // Padding around text inside the box - constexpr uint8_t lineSpacing = 1; // Extra space between lines - - // Search the mesage to determine if we need the bell added - bool needs_bell = (alertBannerMessage.indexOf("Alert Received") != -1); - - // Setup font and alignment - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); // We will manually center per line - - // === Split the message into lines (supports multi-line banners) === - std::vector lines; - int start = 0, newlineIdx; - while ((newlineIdx = alertBannerMessage.indexOf('\n', start)) != -1) { - lines.push_back(alertBannerMessage.substring(start, newlineIdx)); - start = newlineIdx + 1; - } - lines.push_back(alertBannerMessage.substring(start)); - - // === Measure text dimensions === - uint16_t minWidth = (SCREEN_WIDTH > 128) ? 106 : 78; - uint16_t maxWidth = 0; - std::vector lineWidths; - for (const auto &line : lines) { - uint16_t w = display->getStringWidth(line.c_str(), line.length(), true); - lineWidths.push_back(w); - if (w > maxWidth) - maxWidth = w; - } - - uint16_t boxWidth = padding * 2 + maxWidth; - if (needs_bell && boxWidth < minWidth) - boxWidth += (SCREEN_WIDTH > 128) ? 26 : 20; - - uint16_t boxHeight = padding * 2 + lines.size() * FONT_HEIGHT_SMALL + (lines.size() - 1) * lineSpacing; - - int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); - int16_t boxTop = (display->height() / 2) - (boxHeight / 2); - - // === Draw background box === - display->setColor(BLACK); - display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Slightly oversized box - display->setColor(WHITE); - display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border - - // === Draw each line centered in the box === - int16_t lineY = boxTop + padding; - for (size_t i = 0; i < lines.size(); ++i) { - int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; - uint16_t line_width = display->getStringWidth(lines[i].c_str(), lines[i].length(), true); - - if (needs_bell && i == 0) { - int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2; - display->drawXbm(textX - 10, bellY, 8, 8, bell_alert); - display->drawXbm(textX + line_width + 2, bellY, 8, 8, bell_alert); - } - - display->drawString(textX, lineY, lines[i]); - if (SCREEN_WIDTH > 128) - display->drawString(textX + 1, lineY, lines[i]); // Faux bold - - lineY += FONT_HEIGHT_SMALL + lineSpacing; - } -} - -// draw overlay in bottom right corner of screen to show when notifications are muted or modifier key is active -static void drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) -{ - // LOG_DEBUG("Draw function overlay"); - if (functionSymbol.begin() != functionSymbol.end()) { - char buf[64]; - display->setFont(FONT_SMALL); - snprintf(buf, sizeof(buf), "%s", functionSymbolString.c_str()); - display->drawString(SCREEN_WIDTH - display->getStringWidth(buf), SCREEN_HEIGHT - FONT_HEIGHT_SMALL, buf); - } -} - -#ifdef USE_EINK -/// Used on eink displays while in deep sleep -static void drawDeepSleepScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - - // Next frame should use full-refresh, and block while running, else device will sleep before async callback - EINK_ADD_FRAMEFLAG(display, COSMETIC); - EINK_ADD_FRAMEFLAG(display, BLOCKING); - - LOG_DEBUG("Draw deep sleep screen"); - - // Display displayStr on the screen - drawIconScreen("Sleeping", display, state, x, y); -} - -/// Used on eink displays when screen updates are paused -static void drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) -{ - LOG_DEBUG("Draw screensaver overlay"); - - EINK_ADD_FRAMEFLAG(display, COSMETIC); // Take the opportunity for a full-refresh - - // Config - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - const char *pauseText = "Screen Paused"; - const char *idText = owner.short_name; - const bool useId = haveGlyphs(idText); // This bool is used to hide the idText box if we can't render the short name - constexpr uint16_t padding = 5; - constexpr uint8_t dividerGap = 1; - constexpr uint8_t imprecision = 5; // How far the box origins can drift from center. Combat burn-in. - - // Dimensions - const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); // "true": handle utf8 chars - const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText)); - const uint16_t boxWidth = padding + (useId ? idTextWidth + padding + padding : 0) + pauseTextWidth + padding; - const uint16_t boxHeight = padding + FONT_HEIGHT_SMALL + padding; - - // Position - const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2) + random(-imprecision, imprecision + 1); - // const int16_t boxRight = boxLeft + boxWidth - 1; - const int16_t boxTop = (display->height() / 2) - (boxHeight / 2 + random(-imprecision, imprecision + 1)); - const int16_t boxBottom = boxTop + boxHeight - 1; - const int16_t idTextLeft = boxLeft + padding; - const int16_t idTextTop = boxTop + padding; - const int16_t pauseTextLeft = boxLeft + (useId ? padding + idTextWidth + padding : 0) + padding; - const int16_t pauseTextTop = boxTop + padding; - const int16_t dividerX = boxLeft + padding + idTextWidth + padding; - const int16_t dividerTop = boxTop + 1 + dividerGap; - const int16_t dividerBottom = boxBottom - 1 - dividerGap; - - // Draw: box - display->setColor(EINK_WHITE); - display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Clear a slightly oversized area for the box - display->setColor(EINK_BLACK); - display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); - - // Draw: Text - if (useId) - display->drawString(idTextLeft, idTextTop, idText); - display->drawString(pauseTextLeft, pauseTextTop, pauseText); - display->drawString(pauseTextLeft + 1, pauseTextTop, pauseText); // Faux bold - - // Draw: divider - if (useId) - display->drawLine(dividerX, dividerTop, dividerX, dividerBottom); -} -#endif - static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { uint8_t module_frame; @@ -606,78 +175,12 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int pi.drawFrame(display, state, x, y); } -static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(64 + x, y, "Updating"); - - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawStringMaxWidth(0 + x, 2 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), - "Please be patient and do not power off."); -} - -/// Draw the last text message we received -static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_MEDIUM); - - char tempBuf[24]; - snprintf(tempBuf, sizeof(tempBuf), "Critical fault #%d", error_code); - display->drawString(0 + x, 0 + y, tempBuf); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawString(0 + x, FONT_HEIGHT_MEDIUM + y, "For help, please visit \nmeshtastic.org"); -} - // Ignore messages originating from phone (from the current node 0x0) unless range test or store and forward module are enabled static bool shouldDrawMessage(const meshtastic_MeshPacket *packet) { return packet->from != 0 && !moduleConfig.store_forward.enabled; } -// Draw power bars or a charging indicator on an image of a battery, determined by battery charge voltage or percentage. -static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer, const PowerStatus *powerStatus) -{ - static const uint8_t powerBar[3] = {0x81, 0xBD, 0xBD}; - static const uint8_t lightning[8] = {0xA1, 0xA1, 0xA5, 0xAD, 0xB5, 0xA5, 0x85, 0x85}; - - // Clear the bar area inside the battery image - for (int i = 1; i < 14; i++) { - imgBuffer[i] = 0x81; - } - - // Fill with lightning or power bars - if (powerStatus->getIsCharging()) { - memcpy(imgBuffer + 3, lightning, 8); - } else { - for (int i = 0; i < 4; i++) { - if (powerStatus->getBatteryChargePercent() >= 25 * i) - memcpy(imgBuffer + 1 + (i * 3), powerBar, 3); - } - } - - // Slightly more conservative scaling based on screen width - int scale = 1; - - if (SCREEN_WIDTH >= 200) - scale = 2; - if (SCREEN_WIDTH >= 300) - scale = 2; // Do NOT go higher than 2 - - // Draw scaled battery image (16 columns Γ— 8 rows) - for (int col = 0; col < 16; col++) { - uint8_t colBits = imgBuffer[col]; - for (int row = 0; row < 8; row++) { - if (colBits & (1 << row)) { - display->fillRect(x + col * scale, y + row * scale, scale, scale); - } - } - } -} - #if defined(DISPLAY_CLOCK_FRAME) void Screen::drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale) @@ -1186,542 +689,6 @@ bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int return validCached; } -void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount) -{ - int cursorX = x; - const int fontHeight = FONT_HEIGHT_SMALL; - - // === Step 1: Find tallest emote in the line === - int maxIconHeight = fontHeight; - for (size_t i = 0; i < line.length();) { - bool matched = false; - for (int e = 0; e < emoteCount; ++e) { - size_t emojiLen = strlen(emotes[e].label); - if (line.compare(i, emojiLen, emotes[e].label) == 0) { - if (emotes[e].height > maxIconHeight) - maxIconHeight = emotes[e].height; - i += emojiLen; - matched = true; - break; - } - } - if (!matched) { - uint8_t c = static_cast(line[i]); - if ((c & 0xE0) == 0xC0) - i += 2; - else if ((c & 0xF0) == 0xE0) - i += 3; - else if ((c & 0xF8) == 0xF0) - i += 4; - else - i += 1; - } - } - - // === Step 2: Baseline alignment === - int lineHeight = std::max(fontHeight, maxIconHeight); - int baselineOffset = (lineHeight - fontHeight) / 2; - int fontY = y + baselineOffset; - int fontMidline = fontY + fontHeight / 2; - - // === Step 3: Render line in segments === - size_t i = 0; - bool inBold = false; - - while (i < line.length()) { - // Check for ** start/end for faux bold - if (line.compare(i, 2, "**") == 0) { - inBold = !inBold; - i += 2; - continue; - } - - // Look ahead for the next emote match - size_t nextEmotePos = std::string::npos; - const Emote *matchedEmote = nullptr; - size_t emojiLen = 0; - - for (int e = 0; e < emoteCount; ++e) { - size_t pos = line.find(emotes[e].label, i); - if (pos != std::string::npos && (nextEmotePos == std::string::npos || pos < nextEmotePos)) { - nextEmotePos = pos; - matchedEmote = &emotes[e]; - emojiLen = strlen(emotes[e].label); - } - } - - // Render normal text segment up to the emote or bold toggle - size_t nextControl = std::min(nextEmotePos, line.find("**", i)); - if (nextControl == std::string::npos) - nextControl = line.length(); - - if (nextControl > i) { - std::string textChunk = line.substr(i, nextControl - i); - if (inBold) { - // Faux bold: draw twice, offset by 1px - display->drawString(cursorX + 1, fontY, textChunk.c_str()); - } - display->drawString(cursorX, fontY, textChunk.c_str()); - cursorX += display->getStringWidth(textChunk.c_str()); - i = nextControl; - continue; - } - - // Render the emote (if found) - if (matchedEmote && i == nextEmotePos) { - int iconY = fontMidline - matchedEmote->height / 2 - 1; - display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap); - cursorX += matchedEmote->width + 1; - i += emojiLen; - } else { - // No more emotes β€” render the rest of the line - std::string remaining = line.substr(i); - if (inBold) { - display->drawString(cursorX + 1, fontY, remaining.c_str()); - } - display->drawString(cursorX, fontY, remaining.c_str()); - cursorX += display->getStringWidth(remaining.c_str()); - break; - } - } -} - -// **************************** -// * Text Message Screen * -// **************************** -void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - // Clear the unread message indicator when viewing the message - hasUnreadMessage = false; - - const meshtastic_MeshPacket &mp = devicestate.rx_text_message; - const char *msg = reinterpret_cast(mp.decoded.payload.bytes); - - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - - const int navHeight = FONT_HEIGHT_SMALL; - const int scrollBottom = SCREEN_HEIGHT - navHeight; - const int usableHeight = scrollBottom; - const int textWidth = SCREEN_WIDTH; - const int cornerRadius = 2; - - bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); - bool isBold = config.display.heading_bold; - - // === Header Construction === - meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); - char headerStr[80]; - const char *sender = "???"; - if (node && node->has_user) { - if (SCREEN_WIDTH >= 200 && strlen(node->user.long_name) > 0) { - sender = node->user.long_name; - } else { - sender = node->user.short_name; - } - } - uint32_t seconds = sinceReceived(&mp), minutes = seconds / 60, hours = minutes / 60, days = hours / 24; - uint8_t timestampHours, timestampMinutes; - int32_t daysAgo; - bool useTimestamp = deltaToTimestamp(seconds, ×tampHours, ×tampMinutes, &daysAgo); - - if (useTimestamp && minutes >= 15 && daysAgo == 0) { - std::string prefix = (daysAgo == 1 && SCREEN_WIDTH >= 200) ? "Yesterday" : "At"; - if (config.display.use_12h_clock) { - bool isPM = timestampHours >= 12; - timestampHours = timestampHours % 12; - if (timestampHours == 0) - timestampHours = 12; - snprintf(headerStr, sizeof(headerStr), "%s %d:%02d%s from %s", prefix.c_str(), timestampHours, timestampMinutes, - isPM ? "p" : "a", sender); - } else { - snprintf(headerStr, sizeof(headerStr), "%s %d:%02d from %s", prefix.c_str(), timestampHours, timestampMinutes, - sender); - } - } else { - snprintf(headerStr, sizeof(headerStr), "%s ago from %s", screen->drawTimeDelta(days, hours, minutes, seconds).c_str(), - sender); - } - -#ifndef EXCLUDE_EMOJI - // === Bounce animation setup === - static uint32_t lastBounceTime = 0; - static int bounceY = 0; - const int bounceRange = 2; // Max pixels to bounce up/down - const int bounceInterval = 60; // How quickly to change bounce direction (ms) - - uint32_t now = millis(); - if (now - lastBounceTime >= bounceInterval) { - lastBounceTime = now; - bounceY = (bounceY + 1) % (bounceRange * 2); - } - for (int i = 0; i < numEmotes; ++i) { - const Emote &e = emotes[i]; - if (strcmp(msg, e.label) == 0) { - // Draw the header - if (isInverted) { - drawRoundedHighlight(display, x, 0, SCREEN_WIDTH, FONT_HEIGHT_SMALL - 1, cornerRadius); - display->setColor(BLACK); - display->drawString(x + 3, 0, headerStr); - if (isBold) - display->drawString(x + 4, 0, headerStr); - display->setColor(WHITE); - } else { - display->drawString(x, 0, headerStr); - if (SCREEN_WIDTH > 128) { - display->drawLine(0, 20, SCREEN_WIDTH, 20); - } else { - display->drawLine(0, 14, SCREEN_WIDTH, 14); - } - } - - // Center the emote below header + apply bounce - int remainingHeight = SCREEN_HEIGHT - FONT_HEIGHT_SMALL - navHeight; - int emoteY = FONT_HEIGHT_SMALL + (remainingHeight - e.height) / 2 + bounceY - bounceRange; - display->drawXbm((SCREEN_WIDTH - e.width) / 2, emoteY, e.width, e.height, e.bitmap); - return; - } - } -#endif - - // === Word-wrap and build line list === - char messageBuf[237]; - snprintf(messageBuf, sizeof(messageBuf), "%s", msg); - - std::vector lines; - lines.push_back(std::string(headerStr)); // Header line is always first - - std::string line, word; - for (int i = 0; messageBuf[i]; ++i) { - char ch = messageBuf[i]; - if (ch == '\n') { - if (!word.empty()) - line += word; - if (!line.empty()) - lines.push_back(line); - line.clear(); - word.clear(); - } else if (ch == ' ') { - line += word + ' '; - word.clear(); - } else { - word += ch; - std::string test = line + word; - if (display->getStringWidth(test.c_str()) > textWidth + 4) { - if (!line.empty()) - lines.push_back(line); - line = word; - word.clear(); - } - } - } - if (!word.empty()) - line += word; - if (!line.empty()) - lines.push_back(line); - - // === Scrolling logic === - std::vector rowHeights; - - for (const auto &line : lines) { - int maxHeight = FONT_HEIGHT_SMALL; - for (int i = 0; i < numEmotes; ++i) { - const Emote &e = emotes[i]; - if (line.find(e.label) != std::string::npos) { - if (e.height > maxHeight) - maxHeight = e.height; - } - } - rowHeights.push_back(maxHeight); - } - int totalHeight = 0; - for (size_t i = 1; i < rowHeights.size(); ++i) { - totalHeight += rowHeights[i]; - } - int usableScrollHeight = usableHeight - rowHeights[0]; // remove header height - int scrollStop = std::max(0, totalHeight - usableScrollHeight); - - static float scrollY = 0.0f; - static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0; - static bool waitingToReset = false, scrollStarted = false; - - // === Smooth scrolling adjustment === - // You can tweak this divisor to change how smooth it scrolls. - // Lower = smoother, but can feel slow. - float delta = (now - lastTime) / 400.0f; - lastTime = now; - - const float scrollSpeed = 2.0f; // pixels per second - - // Delay scrolling start by 2 seconds - if (scrollStartDelay == 0) - scrollStartDelay = now; - if (!scrollStarted && now - scrollStartDelay > 2000) - scrollStarted = true; - - if (totalHeight > usableHeight) { - if (scrollStarted) { - if (!waitingToReset) { - scrollY += delta * scrollSpeed; - if (scrollY >= scrollStop) { - scrollY = scrollStop; - waitingToReset = true; - pauseStart = lastTime; - } - } else if (lastTime - pauseStart > 3000) { - scrollY = 0; - waitingToReset = false; - scrollStarted = false; - scrollStartDelay = lastTime; - } - } - } else { - scrollY = 0; - } - - int scrollOffset = static_cast(scrollY); - int yOffset = -scrollOffset; - if (!isInverted) { - if (SCREEN_WIDTH > 128) { - display->drawLine(0, yOffset + 20, SCREEN_WIDTH, yOffset + 20); - } else { - display->drawLine(0, yOffset + 14, SCREEN_WIDTH, yOffset + 14); - } - } - - // === Render visible lines === - for (size_t i = 0; i < lines.size(); ++i) { - int lineY = yOffset; - for (size_t j = 0; j < i; ++j) - lineY += rowHeights[j]; - if (lineY > -rowHeights[i] && lineY < scrollBottom) { - if (i == 0 && isInverted) { - drawRoundedHighlight(display, x, lineY, SCREEN_WIDTH, FONT_HEIGHT_SMALL - 1, cornerRadius); - display->setColor(BLACK); - display->drawString(x + 3, lineY, lines[i].c_str()); - if (isBold) - display->drawString(x + 4, lineY, lines[i].c_str()); - display->setColor(WHITE); - } else { - drawStringWithEmotes(display, x, lineY, lines[i], emotes, numEmotes); - } - } - } -} - -/// Draw a series of fields in a column, wrapping to multiple columns if needed -void Screen::drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields) -{ - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); - - const char **f = fields; - int xo = x, yo = y; - while (*f) { - display->drawString(xo, yo, *f); - if ((display->getColor() == BLACK) && config.display.heading_bold) - display->drawString(xo + 1, yo, *f); - - display->setColor(WHITE); - yo += FONT_HEIGHT_SMALL; - if (yo > SCREEN_HEIGHT - FONT_HEIGHT_SMALL) { - xo += SCREEN_WIDTH / 2; - yo = 0; - } - f++; - } -} - -// Draw nodes status -static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const NodeStatus *nodeStatus, int node_offset = 0, - bool show_total = true, String additional_words = "") -{ - char usersString[20]; - int nodes_online = (nodeStatus->getNumOnline() > 0) ? nodeStatus->getNumOnline() + node_offset : 0; - - snprintf(usersString, sizeof(usersString), "%d", nodes_online); - - if (show_total) { - int nodes_total = (nodeStatus->getNumTotal() > 0) ? nodeStatus->getNumTotal() + node_offset : 0; - snprintf(usersString, sizeof(usersString), "%d/%d", nodes_online, nodes_total); - } - -#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \ - !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x, y + 3, 8, 8, imgUser); -#else - display->drawFastImage(x, y + 1, 8, 8, imgUser); -#endif - display->drawString(x + 10, y - 2, usersString); - int string_offset = (SCREEN_WIDTH > 128) ? 2 : 1; - if (additional_words != "") { - display->drawString(x + 10 + display->getStringWidth(usersString) + string_offset, y - 2, additional_words); - if (config.display.heading_bold) - display->drawString(x + 11 + display->getStringWidth(usersString) + string_offset, y - 2, additional_words); - } -} -#if HAS_GPS -// Draw GPS status summary -static void drawGPS(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) -{ - if (config.position.fixed_position) { - // GPS coordinates are currently fixed - display->drawString(x - 1, y - 2, "Fixed GPS"); - if (config.display.heading_bold) - display->drawString(x, y - 2, "Fixed GPS"); - return; - } - if (!gps->getIsConnected()) { - display->drawString(x, y - 2, "No GPS"); - if (config.display.heading_bold) - display->drawString(x + 1, y - 2, "No GPS"); - return; - } - // Adjust position if we’re going to draw too wide - int maxDrawWidth = 6; // Position icon - - if (!gps->getHasLock()) { - maxDrawWidth += display->getStringWidth("No sats") + 2; // icon + text + buffer - } else { - maxDrawWidth += (5 * 2) + 8 + display->getStringWidth("99") + 2; // bars + sat icon + text + buffer - } - - if (x + maxDrawWidth > SCREEN_WIDTH) { - x = SCREEN_WIDTH - maxDrawWidth; - if (x < 0) - x = 0; // Clamp to screen - } - - display->drawFastImage(x, y, 6, 8, gps->getHasLock() ? imgPositionSolid : imgPositionEmpty); - if (!gps->getHasLock()) { - // Draw "No sats" to the right of the icon with slightly more gap - int textX = x + 9; // 6 (icon) + 3px spacing - display->drawString(textX, y - 3, "No sats"); - if (config.display.heading_bold) - display->drawString(textX + 1, y - 3, "No sats"); - return; - } else { - char satsString[3]; - uint8_t bar[2] = {0}; - - // Draw DOP signal bars - for (int i = 0; i < 5; i++) { - if (gps->getDOP() <= dopThresholds[i]) - bar[0] = ~((1 << (5 - i)) - 1); - else - bar[0] = 0b10000000; - - display->drawFastImage(x + 9 + (i * 2), y, 2, 8, bar); - } - - // Draw satellite image - display->drawFastImage(x + 24, y, 8, 8, imgSatellite); - - // Draw the number of satellites - snprintf(satsString, sizeof(satsString), "%u", gps->getNumSatellites()); - int textX = x + 34; - display->drawString(textX, y - 2, satsString); - if (config.display.heading_bold) - display->drawString(textX + 1, y - 2, satsString); - } -} - -// Draw status when GPS is disabled or not present -static void drawGPSpowerstat(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) -{ - String displayLine; - int pos; - if (y < FONT_HEIGHT_SMALL) { // Line 1: use short string - displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; - pos = SCREEN_WIDTH - display->getStringWidth(displayLine); - } else { - displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "GPS not present" - : "GPS is disabled"; - pos = (SCREEN_WIDTH - display->getStringWidth(displayLine)) / 2; - } - display->drawString(x + pos, y, displayLine); -} - -static void drawGPSAltitude(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) -{ - String displayLine = ""; - if (!gps->getIsConnected() && !config.position.fixed_position) { - // displayLine = "No GPS Module"; - // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } else if (!gps->getHasLock() && !config.position.fixed_position) { - // displayLine = "No GPS Lock"; - // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } else { - geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); - displayLine = "Altitude: " + String(geoCoord.getAltitude()) + "m"; - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) - displayLine = "Altitude: " + String(geoCoord.getAltitude() * METERS_TO_FEET) + "ft"; - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } -} - -// Draw GPS status coordinates -static void drawGPScoordinates(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) -{ - auto gpsFormat = config.display.gps_format; - String displayLine = ""; - - if (!gps->getIsConnected() && !config.position.fixed_position) { - displayLine = "No GPS present"; - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } else if (!gps->getHasLock() && !config.position.fixed_position) { - displayLine = "No GPS Lock"; - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); - } else { - - geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); - - if (gpsFormat != meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) { - char coordinateLine[22]; - if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees - snprintf(coordinateLine, sizeof(coordinateLine), "%f %f", geoCoord.getLatitude() * 1e-7, - geoCoord.getLongitude() * 1e-7); - } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator - snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %06u %07u", geoCoord.getUTMZone(), geoCoord.getUTMBand(), - geoCoord.getUTMEasting(), geoCoord.getUTMNorthing()); - } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System - snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %1c%1c %05u %05u", geoCoord.getMGRSZone(), - geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k(), - geoCoord.getMGRSEasting(), geoCoord.getMGRSNorthing()); - } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC) { // Open Location Code - geoCoord.getOLCCode(coordinateLine); - } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference - if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') // OSGR is only valid around the UK region - snprintf(coordinateLine, sizeof(coordinateLine), "%s", "Out of Boundary"); - else - snprintf(coordinateLine, sizeof(coordinateLine), "%1c%1c %05u %05u", geoCoord.getOSGRE100k(), - geoCoord.getOSGRN100k(), geoCoord.getOSGREasting(), geoCoord.getOSGRNorthing()); - } - - // If fixed position, display text "Fixed GPS" alternating with the coordinates. - if (config.position.fixed_position) { - if ((millis() / 10000) % 2) { - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(coordinateLine))) / 2, y, coordinateLine); - } else { - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth("Fixed GPS"))) / 2, y, "Fixed GPS"); - } - } else { - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(coordinateLine))) / 2, y, coordinateLine); - } - } else { - char latLine[22]; - char lonLine[22]; - snprintf(latLine, sizeof(latLine), "%2iΒ° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), geoCoord.getDMSLatMin(), - geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP()); - snprintf(lonLine, sizeof(lonLine), "%3iΒ° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), geoCoord.getDMSLonMin(), - geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP()); - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(latLine))) / 2, y - FONT_HEIGHT_SMALL * 1, latLine); - display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(lonLine))) / 2, y, lonLine); - } - } -} -#endif /** * Given a recent lat/lon return a guess of the heading the user is walking on. * @@ -1756,35 +723,6 @@ float Screen::estimatedHeading(double lat, double lon) /// nodes static int8_t prevFrame = -1; -// Draw the arrow pointing to a node's location -void Screen::drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) -{ - Point tip(0.0f, 0.5f), tail(0.0f, -0.35f); // pointing up initially - float arrowOffsetX = 0.14f, arrowOffsetY = 1.0f; - Point leftArrow(tip.x - arrowOffsetX, tip.y - arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y - arrowOffsetY); - - Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow}; - - for (int i = 0; i < 4; i++) { - arrowPoints[i]->rotate(headingRadian); - arrowPoints[i]->scale(compassDiam * 0.6); - arrowPoints[i]->translate(compassX, compassY); - } - /* Old arrow - display->drawLine(tip.x, tip.y, tail.x, tail.y); - display->drawLine(leftArrow.x, leftArrow.y, tip.x, tip.y); - display->drawLine(rightArrow.x, rightArrow.y, tip.x, tip.y); - display->drawLine(leftArrow.x, leftArrow.y, tail.x, tail.y); - display->drawLine(rightArrow.x, rightArrow.y, tail.x, tail.y); - */ -#ifdef USE_EINK - display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); -#else - display->fillTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); -#endif - display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y); -} - // Get a string representation of the time passed since something happened void Screen::getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength) { @@ -1809,1568 +747,15 @@ void Screen::getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength) else if (agoSecs < 120 * 60) // last 2 hrs snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / 60); // Only show hours ago if it's been less than 6 months. Otherwise, we may have bad data. - else if ((agoSecs / 60 / 60) < (hours_in_month * 6)) + else if ((agoSecs / 60 / 60) < (HOURS_IN_MONTH * 6)) snprintf(timeStr, maxLength, "%u hours ago", agoSecs / 60 / 60); else snprintf(timeStr, maxLength, "unknown age"); } -void Screen::drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading) -{ - Serial.print("🧭 [Main Compass] Raw Heading (deg): "); - Serial.println(myHeading * RAD_TO_DEG); - - // If north is supposed to be at the top of the compass we want rotation to be +0 - if (config.display.compass_north_top) - myHeading = -0; - /* N sign points currently not deleted*/ - Point N1(-0.04f, 0.65f), N2(0.04f, 0.65f); // N sign points (N1-N4) - Point N3(-0.04f, 0.55f), N4(0.04f, 0.55f); - Point NC1(0.00f, 0.50f); // north circle center point - Point *rosePoints[] = {&N1, &N2, &N3, &N4, &NC1}; - - uint16_t compassDiam = Screen::getCompassDiam(SCREEN_WIDTH, SCREEN_HEIGHT); - - for (int i = 0; i < 5; i++) { - // North on compass will be negative of heading - rosePoints[i]->rotate(-myHeading); - rosePoints[i]->scale(compassDiam); - rosePoints[i]->translate(compassX, compassY); - } -} - -uint16_t Screen::getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) -{ - uint16_t diam = 0; - uint16_t offset = 0; - - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) - offset = FONT_HEIGHT_SMALL; - - // get the smaller of the 2 dimensions and subtract 20 - if (displayWidth > (displayHeight - offset)) { - diam = displayHeight - offset; - // if 2/3 of the other size would be smaller, use that - if (diam > (displayWidth * 2 / 3)) { - diam = displayWidth * 2 / 3; - } - } else { - diam = displayWidth; - if (diam > ((displayHeight - offset) * 2 / 3)) { - diam = (displayHeight - offset) * 2 / 3; - } - } - - return diam - 20; -}; - -// ********************** -// * Favorite Node Info * -// ********************** -static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - // --- Cache favorite nodes for the current frame only, to save computation --- - static std::vector favoritedNodes; - static int prevFrame = -1; - - // --- Only rebuild favorites list if we're on a new frame --- - if (state->currentFrame != prevFrame) { - prevFrame = state->currentFrame; - favoritedNodes.clear(); - size_t total = nodeDB->getNumMeshNodes(); - for (size_t i = 0; i < total; i++) { - meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); - // Skip nulls and ourself - if (!n || n->num == nodeDB->getNodeNum()) - continue; - if (n->is_favorite) - favoritedNodes.push_back(n); - } - // Keep a stable, consistent display order - std::sort(favoritedNodes.begin(), favoritedNodes.end(), - [](meshtastic_NodeInfoLite *a, meshtastic_NodeInfoLite *b) { return a->num < b->num; }); - } - if (favoritedNodes.empty()) - return; - - // --- Only display if index is valid --- - int nodeIndex = state->currentFrame - (screen->frameCount - favoritedNodes.size()); - if (nodeIndex < 0 || nodeIndex >= (int)favoritedNodes.size()) - return; - - meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; - if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) - return; - - display->clear(); - - // === Draw battery/time/mail header (common across screens) === - graphics::drawCommonHeader(display, x, y); - - // === Draw the short node name centered at the top, with bold shadow if set === - const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - const int centerX = x + SCREEN_WIDTH / 2; - const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) - display->setColor(BLACK); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_SMALL); - display->drawString(centerX, textY, shortName); - if (config.display.heading_bold) - display->drawString(centerX + 1, textY, shortName); - - display->setColor(WHITE); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - - // ===== DYNAMIC ROW STACKING WITH YOUR MACROS ===== - // 1. Each potential info row has a macro-defined Y position (not regular increments!). - // 2. Each row is only shown if it has valid data. - // 3. Each row "moves up" if previous are empty, so there are never any blank rows. - // 4. The first line is ALWAYS at your macro position; subsequent lines use the next available macro slot. - - // List of available macro Y positions in order, from top to bottom. - const int yPositions[5] = {moreCompactFirstLine, moreCompactSecondLine, moreCompactThirdLine, moreCompactFourthLine, - moreCompactFifthLine}; - int line = 0; // which slot to use next - - // === 1. Long Name (always try to show first) === - const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; - if (username && line < 5) { - // Print node's long name (e.g. "Backpack Node") - display->drawString(x, yPositions[line++], username); - } - - // === 2. Signal and Hops (combined on one line, if available) === - // If both are present: "Sig: 97% [2hops]" - // If only one: show only that one - char signalHopsStr[32] = ""; - bool haveSignal = false; - int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100); - - // Always use "Sig" for the label - const char *signalLabel = " Sig"; - - // --- Build the Signal/Hops line --- - // If SNR looks reasonable, show signal - if ((int)((node->snr + 10) * 5) >= 0 && node->snr > -100) { - snprintf(signalHopsStr, sizeof(signalHopsStr), "%s: %d%%", signalLabel, percentSignal); - haveSignal = true; - } - // If hops is valid (>0), show right after signal - if (node->hops_away > 0) { - size_t len = strlen(signalHopsStr); - // Decide between "1 Hop" and "N Hops" - if (haveSignal) { - snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away, - (node->hops_away == 1 ? "Hop" : "Hops")); - } else { - snprintf(signalHopsStr, sizeof(signalHopsStr), "[%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops")); - } - } - if (signalHopsStr[0] && line < 5) { - display->drawString(x, yPositions[line++], signalHopsStr); - } - - // === 3. Heard (last seen, skip if node never seen) === - char seenStr[20] = ""; - uint32_t seconds = sinceLastSeen(node); - if (seconds != 0 && seconds != UINT32_MAX) { - uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; - // Format as "Heard: Xm ago", "Heard: Xh ago", or "Heard: Xd ago" - snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard: ?" : " Heard: %d%c ago"), - (days ? days - : hours ? hours - : minutes), - (days ? 'd' - : hours ? 'h' - : 'm')); - } - if (seenStr[0] && line < 5) { - display->drawString(x, yPositions[line++], seenStr); - } - - // === 4. Uptime (only show if metric is present) === - char uptimeStr[32] = ""; - if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { - uint32_t uptime = node->device_metrics.uptime_seconds; - uint32_t days = uptime / 86400; - uint32_t hours = (uptime % 86400) / 3600; - uint32_t mins = (uptime % 3600) / 60; - // Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m" - if (days) - snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %ud %uh", days, hours); - else if (hours) - snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %uh %um", hours, mins); - else - snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %um", mins); - } - if (uptimeStr[0] && line < 5) { - display->drawString(x, yPositions[line++], uptimeStr); - } - - // === 5. Distance (only if both nodes have GPS position) === - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - char distStr[24] = ""; // Make buffer big enough for any string - bool haveDistance = false; - - if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { - double lat1 = ourNode->position.latitude_i * 1e-7; - double lon1 = ourNode->position.longitude_i * 1e-7; - double lat2 = node->position.latitude_i * 1e-7; - double lon2 = node->position.longitude_i * 1e-7; - double earthRadiusKm = 6371.0; - double dLat = (lat2 - lat1) * DEG_TO_RAD; - double dLon = (lon2 - lon1) * DEG_TO_RAD; - double a = - sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); - double c = 2 * atan2(sqrt(a), sqrt(1 - a)); - double distanceKm = earthRadiusKm * c; - - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - double miles = distanceKm * 0.621371; - if (miles < 0.1) { - int feet = (int)(miles * 5280); - if (feet > 0 && feet < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dft", feet); - haveDistance = true; - } else if (feet >= 1000) { - snprintf(distStr, sizeof(distStr), " Distance: ΒΌmi"); - haveDistance = true; - } - } else { - int roundedMiles = (int)(miles + 0.5); - if (roundedMiles > 0 && roundedMiles < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dmi", roundedMiles); - haveDistance = true; - } - } - } else { - if (distanceKm < 1.0) { - int meters = (int)(distanceKm * 1000); - if (meters > 0 && meters < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dm", meters); - haveDistance = true; - } else if (meters >= 1000) { - snprintf(distStr, sizeof(distStr), " Distance: 1km"); - haveDistance = true; - } - } else { - int km = (int)(distanceKm + 0.5); - if (km > 0 && km < 1000) { - snprintf(distStr, sizeof(distStr), " Distance: %dkm", km); - haveDistance = true; - } - } - } - } - // Only display if we actually have a value! - if (haveDistance && distStr[0] && line < 5) { - display->drawString(x, yPositions[line++], distStr); - } - - // --- Compass Rendering: landscape (wide) screens use the original side-aligned logic --- - if (SCREEN_WIDTH > SCREEN_HEIGHT) { - bool showCompass = false; - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { - showCompass = true; - } - if (showCompass) { - const int16_t topY = compactFirstLine; - const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); - const int16_t usableHeight = bottomY - topY - 5; - int16_t compassRadius = usableHeight / 2; - if (compassRadius < 8) - compassRadius = 8; - const int16_t compassDiam = compassRadius * 2; - const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; - const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; - - const auto &op = ourNode->position; - float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 - : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - screen->drawCompassNorth(display, compassX, compassY, myHeading); - - const auto &p = node->position; - float d = - GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); - if (!config.display.compass_north_top) - bearing -= myHeading; - screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearing); - - display->drawCircle(compassX, compassY, compassRadius); - } - // else show nothing - } else { - // Portrait or square: put compass at the bottom and centered, scaled to fit available space - bool showCompass = false; - if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { - showCompass = true; - } - if (showCompass) { - int yBelowContent = (line > 0 && line <= 5) ? (yPositions[line - 1] + FONT_HEIGHT_SMALL + 2) : moreCompactFirstLine; - const int margin = 4; -// --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) ----------- -#if defined(USE_EINK) - const int iconSize = (SCREEN_WIDTH > 128) ? 16 : 8; - const int navBarHeight = iconSize + 6; -#else - const int navBarHeight = 0; -#endif - int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; - // --------- END PATCH FOR EINK NAV BAR ----------- - - if (availableHeight < FONT_HEIGHT_SMALL * 2) - return; - - int compassRadius = availableHeight / 2; - if (compassRadius < 8) - compassRadius = 8; - if (compassRadius * 2 > SCREEN_WIDTH - 16) - compassRadius = (SCREEN_WIDTH - 16) / 2; - - int compassX = x + SCREEN_WIDTH / 2; - int compassY = yBelowContent + availableHeight / 2; - - const auto &op = ourNode->position; - float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 - : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - screen->drawCompassNorth(display, compassX, compassY, myHeading); - - const auto &p = node->position; - float d = - GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); - if (!config.display.compass_north_top) - bearing -= myHeading; - screen->drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); - - display->drawCircle(compassX, compassY, compassRadius); - } - // else show nothing - } -} - // Combined dynamic node list frame cycling through LastHeard, HopSignal, and Distance modes // Uses a single frame and changes data every few seconds (E-Ink variant is separate) -// ============================= -// Shared Types and Structures -// ============================= -typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); -typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double); - -struct NodeEntry { - meshtastic_NodeInfoLite *node; - uint32_t lastHeard; - float cachedDistance = -1.0f; // Only used in distance mode -}; - -// ============================= -// Shared Enums and Timing Logic -// ============================= -enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 }; - -static NodeListMode currentMode = MODE_LAST_HEARD; -static int scrollIndex = 0; - -// Use dynamic timing based on mode -unsigned long getModeCycleIntervalMs() -{ - // return (currentMode == MODE_DISTANCE) ? 3000 : 2000; - return 3000; -} - -// h! Calculates bearing between two lat/lon points (used for compass) -float calculateBearing(double lat1, double lon1, double lat2, double lon2) -{ - double dLon = (lon2 - lon1) * DEG_TO_RAD; - lat1 = lat1 * DEG_TO_RAD; - lat2 = lat2 * DEG_TO_RAD; - - double y = sin(dLon) * cos(lat2); - double x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon); - double initialBearing = atan2(y, x); - - return fmod((initialBearing * RAD_TO_DEG + 360), 360); // Normalize to 0-360Β° -} - -int calculateMaxScroll(int totalEntries, int visibleRows) -{ - int totalRows = (totalEntries + 1) / 2; - return std::max(0, totalRows - visibleRows); -} - -// ============================= -// Node Sorting and Scroll Helpers -// ============================= -String getSafeNodeName(meshtastic_NodeInfoLite *node) -{ - String nodeName = "?"; - if (node->has_user && strlen(node->user.short_name) > 0) { - bool valid = true; - const char *name = node->user.short_name; - for (size_t i = 0; i < strlen(name); i++) { - uint8_t c = (uint8_t)name[i]; - if (c < 32 || c > 126) { - valid = false; - break; - } - } - if (valid) { - nodeName = name; - } else { - char idStr[6]; - snprintf(idStr, sizeof(idStr), "%04X", (uint16_t)(node->num & 0xFFFF)); - nodeName = String(idStr); - } - } - return nodeName; -} - -void retrieveAndSortNodes(std::vector &nodeList) -{ - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - bool hasValidSelf = nodeDB->hasValidPosition(ourNode); - - size_t numNodes = nodeDB->getNumMeshNodes(); - for (size_t i = 0; i < numNodes; i++) { - meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); - if (!node || node->num == nodeDB->getNodeNum()) - continue; - - NodeEntry entry; - entry.node = node; - entry.lastHeard = sinceLastSeen(node); - entry.cachedDistance = -1.0f; - - // Pre-calculate distance if we're about to render distance screen - if (currentMode == MODE_DISTANCE && hasValidSelf && nodeDB->hasValidPosition(node)) { - float lat1 = ourNode->position.latitude_i * 1e-7f; - float lon1 = ourNode->position.longitude_i * 1e-7f; - float lat2 = node->position.latitude_i * 1e-7f; - float lon2 = node->position.longitude_i * 1e-7f; - - float dLat = (lat2 - lat1) * DEG_TO_RAD; - float dLon = (lon2 - lon1) * DEG_TO_RAD; - float a = - sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); - float c = 2 * atan2(sqrt(a), sqrt(1 - a)); - entry.cachedDistance = 6371.0f * c; // Earth radius in km - } - - nodeList.push_back(entry); - } - - std::sort(nodeList.begin(), nodeList.end(), [](const NodeEntry &a, const NodeEntry &b) { - bool aFav = a.node->is_favorite; - bool bFav = b.node->is_favorite; - if (aFav != bFav) - return aFav > bFav; - if (a.lastHeard == 0 || a.lastHeard == UINT32_MAX) - return false; - if (b.lastHeard == 0 || b.lastHeard == UINT32_MAX) - return true; - return a.lastHeard < b.lastHeard; - }); -} - -void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) -{ - int columnWidth = display->getWidth() / 2; - int separatorX = x + columnWidth - 2; - display->drawLine(separatorX, yStart, separatorX, yEnd); -} - -void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollIndex, int columns, int scrollStartY) -{ - const int rowHeight = FONT_HEIGHT_SMALL - 3; - const int totalVisualRows = (totalEntries + columns - 1) / columns; - if (totalVisualRows <= visibleNodeRows) - return; - const int scrollAreaHeight = visibleNodeRows * rowHeight; - const int scrollbarX = display->getWidth() - 6; - const int scrollbarWidth = 4; - const int scrollBarHeight = (scrollAreaHeight * visibleNodeRows) / totalVisualRows; - const int scrollBarY = scrollStartY + (scrollAreaHeight * scrollIndex) / totalVisualRows; - display->drawRect(scrollbarX, scrollStartY, scrollbarWidth, scrollAreaHeight); - display->fillRect(scrollbarX, scrollBarY, scrollbarWidth, scrollBarHeight); -} - -// ============================= -// Shared Node List Screen Logic -// ============================= -void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, - EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0, - double lon = 0) -{ - const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; - const int rowYOffset = FONT_HEIGHT_SMALL - 3; - - int columnWidth = display->getWidth() / 2; - - display->clear(); - - // === Draw the battery/time header === - graphics::drawCommonHeader(display, x, y); - - // === Manually draw the centered title within the header === - const int highlightHeight = COMMON_HEADER_HEIGHT; - const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - const int centerX = x + SCREEN_WIDTH / 2; - - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) - display->setColor(BLACK); - - display->drawString(centerX, textY, title); - if (config.display.heading_bold) - display->drawString(centerX + 1, textY, title); - - display->setColor(WHITE); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - // === Space below header === - y += COMMON_HEADER_HEIGHT; - - // === Fetch and display sorted node list === - std::vector nodeList; - retrieveAndSortNodes(nodeList); - - int totalEntries = nodeList.size(); - int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; -#ifdef USE_EINK - totalRowsAvailable -= 1; -#endif - int visibleNodeRows = totalRowsAvailable; - int totalColumns = 2; - - 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) { - int xPos = x + (col * columnWidth); - int yPos = y + yOffset; - renderer(display, nodeList[i].node, xPos, yPos, columnWidth); - - // βœ… Actually render the compass arrow - if (extras) { - extras(display, nodeList[i].node, xPos, yPos, columnWidth, heading, lat, lon); - } - - lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); - yOffset += rowYOffset; - shownCount++; - rowCount++; - - if (rowCount >= totalRowsAvailable) { - yOffset = 0; - rowCount = 0; - col++; - if (col > (totalColumns - 1)) - break; - } - } - - // === Draw column separator - if (shownCount > 0) { - const int firstNodeY = y + 3; - drawColumnSeparator(display, x, firstNodeY, lastNodeY); - } - - const int scrollStartY = y + 3; - drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY); -} - -// ============================= -// Shared Dynamic Entry Renderers -// ============================= -void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) -{ - bool isLeftCol = (x < SCREEN_WIDTH / 2); - int timeOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) - : (isLeftCol ? 3 : 7); // Offset for Narrow Screens (Left Column:Right Column) - - String nodeName = getSafeNodeName(node); - - char timeStr[10]; - uint32_t seconds = sinceLastSeen(node); - if (seconds == 0 || seconds == UINT32_MAX) { - snprintf(timeStr, sizeof(timeStr), "?"); - } else { - uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; - snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"), - (days ? days - : hours ? hours - : minutes), - (days ? 'd' - : hours ? 'h' - : 'm')); - } - - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawString(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nodeName); - if (node->is_favorite) { - if (SCREEN_WIDTH > 128) { - drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); - } else { - display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); - } - } - - int rightEdge = x + columnWidth - timeOffset; - int textWidth = display->getStringWidth(timeStr); - display->drawString(rightEdge - textWidth, y, timeStr); -} - -// **************************** -// * Hops / Signal Screen * -// **************************** -void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) -{ - bool isLeftCol = (x < SCREEN_WIDTH / 2); - - int nameMaxWidth = columnWidth - 25; - int barsOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 16 : 20) // Offset for Wide Screens (Left Column:Right Column) - : (isLeftCol ? 15 : 19); // Offset for Narrow Screens (Left Column:Right Column) - int hopOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 17 : 25) // Offset for Wide Screens (Left Column:Right Column) - : (isLeftCol ? 13 : 17); // Offset for Narrow Screens (Left Column:Right Column) - int barsXOffset = columnWidth - barsOffset; - - String nodeName = getSafeNodeName(node); - - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - - display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName); - if (node->is_favorite) { - if (SCREEN_WIDTH > 128) { - drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); - } else { - display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); - } - } - - char hopStr[6] = ""; - if (node->has_hops_away && node->hops_away > 0) - snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away); - - if (hopStr[0] != '\0') { - int rightEdge = x + columnWidth - hopOffset; - int textWidth = display->getStringWidth(hopStr); - display->drawString(rightEdge - textWidth, y, hopStr); - } - - int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; - int barWidth = 2; - int barStartX = x + barsXOffset; - int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2; - - for (int b = 0; b < 4; b++) { - if (b < bars) { - int height = (b * 2); - display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height); - } - } -} - -// ************************** -// * Distance Screen * -// ************************** -void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) -{ - bool isLeftCol = (x < SCREEN_WIDTH / 2); - int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); - - String nodeName = getSafeNodeName(node); - char distStr[10] = ""; - - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { - double lat1 = ourNode->position.latitude_i * 1e-7; - double lon1 = ourNode->position.longitude_i * 1e-7; - double lat2 = node->position.latitude_i * 1e-7; - double lon2 = node->position.longitude_i * 1e-7; - - double earthRadiusKm = 6371.0; - double dLat = (lat2 - lat1) * DEG_TO_RAD; - double dLon = (lon2 - lon1) * DEG_TO_RAD; - - double a = - sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); - double c = 2 * atan2(sqrt(a), sqrt(1 - a)); - double distanceKm = earthRadiusKm * c; - - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - double miles = distanceKm * 0.621371; - if (miles < 0.1) { - int feet = (int)(miles * 5280); - if (feet < 1000) - snprintf(distStr, sizeof(distStr), "%dft", feet); - else - snprintf(distStr, sizeof(distStr), "ΒΌmi"); // 4-char max - } else { - int roundedMiles = (int)(miles + 0.5); - if (roundedMiles < 1000) - snprintf(distStr, sizeof(distStr), "%dmi", roundedMiles); - else - snprintf(distStr, sizeof(distStr), "999"); // Max display cap - } - } else { - if (distanceKm < 1.0) { - int meters = (int)(distanceKm * 1000); - if (meters < 1000) - snprintf(distStr, sizeof(distStr), "%dm", meters); - else - snprintf(distStr, sizeof(distStr), "1k"); - } else { - int km = (int)(distanceKm + 0.5); - if (km < 1000) - snprintf(distStr, sizeof(distStr), "%dk", km); - else - snprintf(distStr, sizeof(distStr), "999"); - } - } - } - - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName); - if (node->is_favorite) { - if (SCREEN_WIDTH > 128) { - drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); - } else { - display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); - } - } - - if (strlen(distStr) > 0) { - int offset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) - : (isLeftCol ? 5 : 8); // Offset for Narrow Screens (Left Column:Right Column) - int rightEdge = x + columnWidth - offset; - int textWidth = display->getStringWidth(distStr); - display->drawString(rightEdge - textWidth, y, distStr); - } -} - -// ============================= -// Dynamic Unified Entry Renderer -// ============================= -void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) -{ - switch (currentMode) { - 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; // Silences warning for MODE_COUNT or unexpected values - } -} - -const char *getCurrentModeTitle(int screenWidth) -{ - switch (currentMode) { - case MODE_LAST_HEARD: - return "Node List"; - case MODE_HOP_SIGNAL: - return (screenWidth > 128) ? "Hops|Signals" : "Hop|Sig"; - case MODE_DISTANCE: - return "Distances"; - default: - return "Nodes"; - } -} - -// ============================= -// OLED/TFT Version (cycles every few seconds) -// ============================= -#ifndef USE_EINK -static void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - // Static variables to track mode and duration - static NodeListMode lastRenderedMode = MODE_COUNT; - static unsigned long modeStartTime = 0; - - unsigned long now = millis(); - - // On very first call (on boot or state enter) - if (lastRenderedMode == MODE_COUNT) { - currentMode = MODE_LAST_HEARD; - modeStartTime = now; - } - - // Time to switch to next mode? - if (now - modeStartTime >= getModeCycleIntervalMs()) { - currentMode = static_cast((currentMode + 1) % MODE_COUNT); - modeStartTime = now; - } - - // Render screen based on currentMode - const char *title = getCurrentModeTitle(display->getWidth()); - drawNodeListScreen(display, state, x, y, title, drawEntryDynamic); - - // Track the last mode to avoid reinitializing modeStartTime - lastRenderedMode = currentMode; -} -#endif - -// ============================= -// E-Ink Version (mode set once per boot) -// ============================= -#ifdef USE_EINK -static void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - if (state->ticksSinceLastStateSwitch == 0) { - currentMode = MODE_LAST_HEARD; - } - const char *title = getCurrentModeTitle(display->getWidth()); - drawNodeListScreen(display, state, x, y, title, drawEntryDynamic); -} -#endif - -// Add these below (still inside #ifdef USE_EINK if you prefer): -#ifdef USE_EINK -static void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - const char *title = "Node List"; - drawNodeListScreen(display, state, x, y, title, drawEntryLastHeard); -} - -static void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - const char *title = (display->getWidth() > 128) ? "Hops|Signals" : "Hop|Sig"; - drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal); -} - -static void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - const char *title = "Distances"; - drawNodeListScreen(display, state, x, y, title, drawNodeDistance); -} -#endif -// Helper function: Draw a single node entry for Node List (Modified for Compass Screen) -void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) -{ - bool isLeftCol = (x < SCREEN_WIDTH / 2); - - // Adjust max text width depending on column and screen width - int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); - - String nodeName = getSafeNodeName(node); - - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName); - if (node->is_favorite) { - if (SCREEN_WIDTH > 128) { - drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); - } else { - display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); - } - } -} -void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, - double userLat, double userLon) -{ - if (!nodeDB->hasValidPosition(node)) - return; - - bool isLeftCol = (x < SCREEN_WIDTH / 2); - int arrowXOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); - - int centerX = x + columnWidth - arrowXOffset; - int centerY = y + FONT_HEIGHT_SMALL / 2; - - double nodeLat = node->position.latitude_i * 1e-7; - double nodeLon = node->position.longitude_i * 1e-7; - float bearingToNode = calculateBearing(userLat, userLon, nodeLat, nodeLon); - float relativeBearing = fmod((bearingToNode - myHeading + 360), 360); - float angle = relativeBearing * DEG_TO_RAD; - - // Shrink size by 2px - int size = FONT_HEIGHT_SMALL - 5; - float halfSize = size / 2.0; - - // Point of the arrow - int tipX = centerX + halfSize * cos(angle); - int tipY = centerY - halfSize * sin(angle); - - float baseAngle = radians(35); - float sideLen = halfSize * 0.95; - float notchInset = halfSize * 0.35; - - // Left and right corners - int leftX = centerX + sideLen * cos(angle + PI - baseAngle); - int leftY = centerY - sideLen * sin(angle + PI - baseAngle); - - int rightX = centerX + sideLen * cos(angle + PI + baseAngle); - int rightY = centerY - sideLen * sin(angle + PI + baseAngle); - - // Center notch (cut-in) - int notchX = centerX - notchInset * cos(angle); - int notchY = centerY + notchInset * sin(angle); - - // Draw the chevron-style arrowhead - display->fillTriangle(tipX, tipY, leftX, leftY, notchX, notchY); - display->fillTriangle(tipX, tipY, notchX, notchY, rightX, rightY); -} - -// Public screen entry for compass -static void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - float heading = 0; - bool validHeading = false; - double lat = 0; - double lon = 0; - -#if HAS_GPS - geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()), - int32_t(gpsStatus->getAltitude())); - lat = geoCoord.getLatitude() * 1e-7; - lon = geoCoord.getLongitude() * 1e-7; - - if (screen->hasHeading()) { - heading = screen->getHeading(); // degrees - validHeading = true; - } else { - heading = screen->estimatedHeading(lat, lon); - validHeading = !isnan(heading); - } -#endif - - if (!validHeading) - return; - - drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon); -} - -// **************************** -// * Device Focused Screen * -// **************************** -static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - - // === Header === - graphics::drawCommonHeader(display, x, y); - - // === Content below header === - - // Determine if we need to show 4 or 5 rows on the screen - int rows = 4; - if (!config.bluetooth.enabled) { - rows = 5; - } - - // === First Row: Region / Channel Utilization and Uptime === - bool origBold = config.display.heading_bold; - config.display.heading_bold = false; - - // Display Region and Channel Utilization - drawNodes(display, x + 1, - ((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)) + 2, nodeStatus, - -1, false, "online"); - - uint32_t uptime = millis() / 1000; - char uptimeStr[6]; - uint32_t minutes = uptime / 60, hours = minutes / 60, days = hours / 24; - - if (days > 365) { - snprintf(uptimeStr, sizeof(uptimeStr), "?"); - } else { - snprintf(uptimeStr, sizeof(uptimeStr), "%d%c", - days ? days - : hours ? hours - : minutes ? minutes - : (int)uptime, - days ? 'd' - : hours ? 'h' - : minutes ? 'm' - : 's'); - } - - char uptimeFullStr[16]; - snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); - display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeFullStr), - ((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)), - uptimeFullStr); - - config.display.heading_bold = origBold; - - // === Second Row: Satellites and Voltage === - config.display.heading_bold = false; - -#if HAS_GPS - if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - String displayLine = ""; - if (config.position.fixed_position) { - displayLine = "Fixed GPS"; - } else { - displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; - } - display->drawString( - 0, ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), - displayLine); - } else { - drawGPS(display, 0, - ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)) + 3, - gpsStatus); - } -#endif - - char batStr[20]; - if (powerStatus->getHasBattery()) { - int batV = powerStatus->getBatteryVoltageMv() / 1000; - int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; - snprintf(batStr, sizeof(batStr), "%01d.%02dV", batV, batCv); - display->drawString( - x + SCREEN_WIDTH - display->getStringWidth(batStr), - ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), batStr); - } else { - display->drawString( - x + SCREEN_WIDTH - display->getStringWidth("USB"), - ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), - String("USB")); - } - - config.display.heading_bold = origBold; - - // === Third Row: Bluetooth Off (Only If Actually Off) === - if (!config.bluetooth.enabled) { - display->drawString( - 0, ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactThirdLine : moreCompactThirdLine)), "BT off"); - } - - // === Third & Fourth Rows: Node Identity === - int textWidth = 0; - int nameX = 0; - int yOffset = (SCREEN_WIDTH > 128) ? 0 : 7; - const char *longName = nullptr; - meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); - if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { - longName = ourNode->user.long_name; - } - uint8_t dmac[6]; - char shortnameble[35]; - getMacAddr(dmac); - snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); - snprintf(shortnameble, sizeof(shortnameble), "%s", haveGlyphs(owner.short_name) ? owner.short_name : ""); - - char combinedName[50]; - snprintf(combinedName, sizeof(combinedName), "%s (%s)", longName, shortnameble); - if (SCREEN_WIDTH - (display->getStringWidth(longName) + display->getStringWidth(shortnameble)) > 10) { - size_t len = strlen(combinedName); - if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) { - combinedName[len - 3] = '\0'; // Remove the last three characters - } - textWidth = display->getStringWidth(combinedName); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString( - nameX, - ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine)) + yOffset, - combinedName); - } else { - textWidth = display->getStringWidth(longName); - nameX = (SCREEN_WIDTH - textWidth) / 2; - yOffset = (strcmp(shortnameble, "") == 0) ? 1 : 0; - if (yOffset == 1) { - yOffset = (SCREEN_WIDTH > 128) ? 0 : 7; - } - display->drawString( - nameX, - ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine)) + yOffset, - longName); - - // === Fourth Row: ShortName Centered === - textWidth = display->getStringWidth(shortnameble); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, - ((rows == 4) ? compactFourthLine : ((SCREEN_HEIGHT > 64) ? compactFifthLine : moreCompactFifthLine)), - shortnameble); - } -} - -// **************************** -// * LoRa Focused Screen * -// **************************** -static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - - // === Header === - graphics::drawCommonHeader(display, x, y); - - // === Draw title (aligned with header baseline) === - const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - const char *titleStr = (SCREEN_WIDTH > 128) ? "LoRa Info" : "LoRa"; - const int centerX = x + SCREEN_WIDTH / 2; - - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->setColor(BLACK); - } - - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(centerX, textY, titleStr); - if (config.display.heading_bold) { - display->drawString(centerX + 1, textY, titleStr); - } - display->setColor(WHITE); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - // === First Row: Region / BLE Name === - drawNodes(display, x, compactFirstLine + 3, nodeStatus, 0, true); - - uint8_t dmac[6]; - char shortnameble[35]; - getMacAddr(dmac); - snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); - snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", ourId); - int textWidth = display->getStringWidth(shortnameble); - int nameX = (SCREEN_WIDTH - textWidth); - display->drawString(nameX, compactFirstLine, shortnameble); - - // === Second Row: Radio Preset === - auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); - char regionradiopreset[25]; - const char *region = myRegion ? myRegion->name : NULL; - snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); - textWidth = display->getStringWidth(regionradiopreset); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, compactSecondLine, regionradiopreset); - - // === Third Row: Frequency / ChanNum === - char frequencyslot[35]; - char freqStr[16]; - float freq = RadioLibInterface::instance->getFreq(); - snprintf(freqStr, sizeof(freqStr), "%.3f", freq); - if (config.lora.channel_num == 0) { - snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %s", freqStr); - } else { - snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Chan: %s (%d)", freqStr, config.lora.channel_num); - } - size_t len = strlen(frequencyslot); - if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { - frequencyslot[len - 4] = '\0'; // Remove the last three characters - } - textWidth = display->getStringWidth(frequencyslot); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, compactThirdLine, frequencyslot); - - // === Fourth Row: Channel Utilization === - const char *chUtil = "ChUtil:"; - char chUtilPercentage[10]; - snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); - - int chUtil_x = (SCREEN_WIDTH > 128) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; - int chUtil_y = compactFourthLine + 3; - - int chutil_bar_width = (SCREEN_WIDTH > 128) ? 100 : 50; - int chutil_bar_height = (SCREEN_WIDTH > 128) ? 12 : 7; - int extraoffset = (SCREEN_WIDTH > 128) ? 6 : 3; - int chutil_percent = airTime->channelUtilizationPercent(); - - int centerofscreen = SCREEN_WIDTH / 2; - int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; - int starting_position = centerofscreen - total_line_content_width; - - display->drawString(starting_position, compactFourthLine, chUtil); - - // Force 56% or higher to show a full 100% bar, text would still show related percent. - if (chutil_percent >= 61) { - chutil_percent = 100; - } - - // Weighting for nonlinear segments - float milestone1 = 25; - float milestone2 = 40; - float weight1 = 0.45; // Weight for 0–25% - float weight2 = 0.35; // Weight for 25–40% - float weight3 = 0.20; // Weight for 40–100% - float totalWeight = weight1 + weight2 + weight3; - - int seg1 = chutil_bar_width * (weight1 / totalWeight); - int seg2 = chutil_bar_width * (weight2 / totalWeight); - int seg3 = chutil_bar_width * (weight3 / totalWeight); - - int fillRight = 0; - - if (chutil_percent <= milestone1) { - fillRight = (seg1 * (chutil_percent / milestone1)); - } else if (chutil_percent <= milestone2) { - fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); - } else { - fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); - } - - // Draw outline - display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); - - // Fill progress - if (fillRight > 0) { - display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); - } - - display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, compactFourthLine, chUtilPercentage); -} - -// **************************** -// * My Position Screen * -// **************************** -static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->clear(); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - - // === Header === - graphics::drawCommonHeader(display, x, y); - - // === Draw title === - const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - const char *titleStr = "GPS"; - const int centerX = x + SCREEN_WIDTH / 2; - - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->setColor(BLACK); - } - - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(centerX, textY, titleStr); - if (config.display.heading_bold) { - display->drawString(centerX + 1, textY, titleStr); - } - display->setColor(WHITE); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - // === First Row: My Location === -#if HAS_GPS - bool origBold = config.display.heading_bold; - config.display.heading_bold = false; - - String Satelite_String = "Sat:"; - display->drawString(0, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), Satelite_String); - String displayLine = ""; - if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - if (config.position.fixed_position) { - displayLine = "Fixed GPS"; - } else { - displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; - } - display->drawString(display->getStringWidth(Satelite_String) + 3, - ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), displayLine); - } else { - drawGPS(display, display->getStringWidth(Satelite_String) + 3, - ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine) + 3, gpsStatus); - } - - config.display.heading_bold = origBold; - - // === Update GeoCoord === - geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()), - int32_t(gpsStatus->getAltitude())); - - // === Determine Compass Heading === - float heading; - bool validHeading = false; - - if (screen->hasHeading()) { - heading = radians(screen->getHeading()); - validHeading = true; - } else { - heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7); - validHeading = !isnan(heading); - } - - // If GPS is off, no need to display these parts - if (displayLine != "GPS off" && displayLine != "No GPS") { - - // === Second Row: Altitude === - String displayLine; - displayLine = " Alt: " + String(geoCoord.getAltitude()) + "m"; - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) - displayLine = " Alt: " + String(geoCoord.getAltitude() * METERS_TO_FEET) + "ft"; - display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine), displayLine); - - // === Third Row: Latitude === - char latStr[32]; - snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7); - display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactThirdLine : moreCompactThirdLine), latStr); - - // === Fourth Row: Longitude === - char lonStr[32]; - snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7); - display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine), lonStr); - - // === Fifth Row: Date === - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); - char datetimeStr[25]; - bool showTime = false; // set to true for full datetime - formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime); - char fullLine[40]; - snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr); - display->drawString(0, ((SCREEN_HEIGHT > 64) ? compactFifthLine : moreCompactFifthLine), fullLine); - } - - // === Draw Compass if heading is valid === - if (validHeading) { - // --- Compass Rendering: landscape (wide) screens use original side-aligned logic --- - if (SCREEN_WIDTH > SCREEN_HEIGHT) { - const int16_t topY = compactFirstLine; - const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height - const int16_t usableHeight = bottomY - topY - 5; - - int16_t compassRadius = usableHeight / 2; - if (compassRadius < 8) - compassRadius = 8; - const int16_t compassDiam = compassRadius * 2; - const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; - - // Center vertically and nudge down slightly to keep "N" clear of header - const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; - - screen->drawNodeHeading(display, compassX, compassY, compassDiam, -heading); - display->drawCircle(compassX, compassY, compassRadius); - - // "N" label - float northAngle = -heading; - float radius = compassRadius; - int16_t nX = compassX + (radius - 1) * sin(northAngle); - int16_t nY = compassY - (radius - 1) * cos(northAngle); - int16_t nLabelWidth = display->getStringWidth("N") + 2; - int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; - - display->setColor(BLACK); - display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); - display->setColor(WHITE); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); - } else { - // Portrait or square: put compass at the bottom and centered, scaled to fit available space - // For E-Ink screens, account for navigation bar at the bottom! - int yBelowContent = ((SCREEN_HEIGHT > 64) ? compactFifthLine : moreCompactFifthLine) + FONT_HEIGHT_SMALL + 2; - const int margin = 4; - int availableHeight = -#if defined(USE_EINK) - SCREEN_HEIGHT - yBelowContent - 24; // Leave extra space for nav bar on E-Ink -#else - SCREEN_HEIGHT - yBelowContent - margin; -#endif - - if (availableHeight < FONT_HEIGHT_SMALL * 2) - return; - - int compassRadius = availableHeight / 2; - if (compassRadius < 8) - compassRadius = 8; - if (compassRadius * 2 > SCREEN_WIDTH - 16) - compassRadius = (SCREEN_WIDTH - 16) / 2; - - int compassX = x + SCREEN_WIDTH / 2; - int compassY = yBelowContent + availableHeight / 2; - - screen->drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading); - display->drawCircle(compassX, compassY, compassRadius); - - // "N" label - float northAngle = -heading; - float radius = compassRadius; - int16_t nX = compassX + (radius - 1) * sin(northAngle); - int16_t nY = compassY - (radius - 1) * cos(northAngle); - int16_t nLabelWidth = display->getStringWidth("N") + 2; - int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; - - display->setColor(BLACK); - display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); - display->setColor(WHITE); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); - } - } -#endif -} - -// **************************** -// * Memory Screen * -// **************************** -static void drawMemoryScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->clear(); - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - // === Header === - graphics::drawCommonHeader(display, x, y); - - // === Draw title === - const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - const char *titleStr = (SCREEN_WIDTH > 128) ? "Memory" : "Mem"; - const int centerX = x + SCREEN_WIDTH / 2; - - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->setColor(BLACK); - } - - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(centerX, textY, titleStr); - if (config.display.heading_bold) { - display->drawString(centerX + 1, textY, titleStr); - } - display->setColor(WHITE); - - // === Layout === - int contentY = y + FONT_HEIGHT_SMALL; - const int rowYOffset = FONT_HEIGHT_SMALL - 3; - const int barHeight = 6; - const int labelX = x; - const int barsOffset = (SCREEN_WIDTH > 128) ? 24 : 0; - const int barX = x + 40 + barsOffset; - - int rowY = contentY; - - // === Heap delta tracking (disabled) === - /* - static uint32_t previousHeapFree = 0; - static int32_t totalHeapDelta = 0; - static int deltaChangeCount = 0; - */ - - auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { - if (total == 0) - return; - - int percent = (used * 100) / total; - - char combinedStr[24]; - if (SCREEN_WIDTH > 128) { - snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %lu/%luKB", (percent > 80) ? "! " : "", percent, used / 1024, - total / 1024); - } else { - snprintf(combinedStr, sizeof(combinedStr), "%s%3d%%", (percent > 80) ? "! " : "", percent); - } - - int textWidth = display->getStringWidth(combinedStr); - int adjustedBarWidth = SCREEN_WIDTH - barX - textWidth - 6; - if (adjustedBarWidth < 10) - adjustedBarWidth = 10; - - int fillWidth = (used * adjustedBarWidth) / total; - - // Label - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawString(labelX, rowY, label); - - // Bar - int barY = rowY + (FONT_HEIGHT_SMALL - barHeight) / 2; - display->setColor(WHITE); - display->drawRect(barX, barY, adjustedBarWidth, barHeight); - - display->fillRect(barX, barY, fillWidth, barHeight); - display->setColor(WHITE); - - // Value string - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(SCREEN_WIDTH - 2, rowY, combinedStr); - - rowY += rowYOffset; - - // === Heap delta display (disabled) === - /* - if (isHeap && previousHeapFree > 0) { - int32_t delta = (int32_t)(memGet.getFreeHeap() - previousHeapFree); - if (delta != 0) { - totalHeapDelta += delta; - deltaChangeCount++; - - char deltaStr[16]; - snprintf(deltaStr, sizeof(deltaStr), "%ld", delta); - - int deltaX = centerX - display->getStringWidth(deltaStr) / 2 - 8; - int deltaY = rowY + 1; - - // Triangle - if (delta > 0) { - display->drawLine(deltaX, deltaY + 6, deltaX + 3, deltaY); - display->drawLine(deltaX + 3, deltaY, deltaX + 6, deltaY + 6); - display->drawLine(deltaX, deltaY + 6, deltaX + 6, deltaY + 6); - } else { - display->drawLine(deltaX, deltaY, deltaX + 3, deltaY + 6); - display->drawLine(deltaX + 3, deltaY + 6, deltaX + 6, deltaY); - display->drawLine(deltaX, deltaY, deltaX + 6, deltaY); - } - - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(centerX + 6, deltaY, deltaStr); - rowY += rowYOffset; - } - } - - if (isHeap) { - previousHeapFree = memGet.getFreeHeap(); - } - */ - }; - - // === Memory values === - uint32_t heapUsed = memGet.getHeapSize() - memGet.getFreeHeap(); - uint32_t heapTotal = memGet.getHeapSize(); - - uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram(); - uint32_t psramTotal = memGet.getPsramSize(); - - uint32_t flashUsed = 0, flashTotal = 0; -#ifdef ESP32 - flashUsed = FSCom.usedBytes(); - flashTotal = FSCom.totalBytes(); -#endif - - uint32_t sdUsed = 0, sdTotal = 0; - bool hasSD = false; - /* - #ifdef HAS_SDCARD - hasSD = SD.cardType() != CARD_NONE; - if (hasSD) { - sdUsed = SD.usedBytes(); - sdTotal = SD.totalBytes(); - } - #endif - */ - // === Draw memory rows - drawUsageRow("Heap:", heapUsed, heapTotal, true); - drawUsageRow("PSRAM:", psramUsed, psramTotal); -#ifdef ESP32 - if (flashTotal > 0) - drawUsageRow("Flash:", flashUsed, flashTotal); -#endif - if (hasSD && sdTotal > 0) - drawUsageRow("SD:", sdUsed, sdTotal); -} - #if defined(ESP_PLATFORM) && defined(USE_ST7789) SPIClass SPI1(HSPI); #endif @@ -3440,7 +825,7 @@ Screen::~Screen() void Screen::doDeepSleep() { #ifdef USE_EINK - setOn(false, drawDeepSleepScreen); + setOn(false, graphics::UIRenderer::drawDeepSleepFrame); #ifdef PIN_EINK_EN digitalWrite(PIN_EINK_EN, LOW); // power off backlight #endif @@ -3535,87 +920,6 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver) screenOn = on; } } -static int8_t lastFrameIndex = -1; -static uint32_t lastFrameChangeTime = 0; -constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000; - -void NavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) -{ - int currentFrame = state->currentFrame; - - // Detect frame change and record time - if (currentFrame != lastFrameIndex) { - lastFrameIndex = currentFrame; - lastFrameChangeTime = millis(); - } - - const bool useBigIcons = (SCREEN_WIDTH > 128); - const int iconSize = useBigIcons ? 16 : 8; - const int spacing = useBigIcons ? 8 : 4; - const int bigOffset = useBigIcons ? 1 : 0; - - const size_t totalIcons = screen->indicatorIcons.size(); - if (totalIcons == 0) - return; - - const size_t iconsPerPage = (SCREEN_WIDTH + spacing) / (iconSize + spacing); - const size_t currentPage = currentFrame / iconsPerPage; - const size_t pageStart = currentPage * iconsPerPage; - const size_t pageEnd = min(pageStart + iconsPerPage, totalIcons); - - const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing; - const int xStart = (SCREEN_WIDTH - totalWidth) / 2; - - // Only show bar briefly after switching frames (unless on E-Ink) -#if defined(USE_EINK) - int y = SCREEN_HEIGHT - iconSize - 1; -#else - int y = SCREEN_HEIGHT - iconSize - 1; - if (millis() - lastFrameChangeTime > ICON_DISPLAY_DURATION_MS) { - y = SCREEN_HEIGHT; - } -#endif - - // Pre-calculate bounding rect - const int rectX = xStart - 2 - bigOffset; - const int rectWidth = totalWidth + 4 + (bigOffset * 2); - const int rectHeight = iconSize + 6; - - // Clear background and draw border - display->setColor(BLACK); - display->fillRect(rectX + 1, y - 2, rectWidth - 2, rectHeight - 2); - display->setColor(WHITE); - display->drawRect(rectX, y - 2, rectWidth, rectHeight); - - // Icon drawing loop for the current page - for (size_t i = pageStart; i < pageEnd; ++i) { - const uint8_t *icon = screen->indicatorIcons[i]; - const int x = xStart + (i - pageStart) * (iconSize + spacing); - const bool isActive = (i == static_cast(currentFrame)); - - if (isActive) { - display->setColor(WHITE); - display->fillRect(x - 2, y - 2, iconSize + 4, iconSize + 4); - display->setColor(BLACK); - } - - if (useBigIcons) { - drawScaledXBitmap16x16(x, y, 8, 8, icon, display); - } else { - display->drawXbm(x, y, iconSize, iconSize, icon); - } - - if (isActive) { - display->setColor(WHITE); - } - } - - // Knock the corners off the square - display->setColor(BLACK); - display->drawRect(rectX, y - 2, 1, 1); - display->drawRect(rectX + rectWidth - 1, y - 2, 1, 1); - display->setColor(WHITE); -} void Screen::setup() { @@ -3651,8 +955,8 @@ void Screen::setup() // === Set custom overlay callbacks === static OverlayCallback overlays[] = { - drawFunctionOverlay, // For mute/buzzer modifiers etc. - NavigationBar // Custom indicator icons for each frame + graphics::UIRenderer::drawFunctionOverlay, // For mute/buzzer modifiers etc. + graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame }; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); @@ -3668,12 +972,12 @@ void Screen::setup() alertFrames[0] = [this](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { #ifdef ARCH_ESP32 if (wakeCause == ESP_SLEEP_WAKEUP_TIMER || wakeCause == ESP_SLEEP_WAKEUP_EXT1) - drawFrameText(display, state, x, y, "Resuming..."); + graphics::UIRenderer::drawFrameText(display, state, x, y, "Resuming..."); else #endif { const char *region = myRegion ? myRegion->name : nullptr; - drawIconScreen(region, display, state, x, y); + graphics::UIRenderer::drawIconScreen(region, display, state, x, y); } }; ui->setFrames(alertFrames, 1); @@ -3810,7 +1114,7 @@ int32_t Screen::runOnce() if (showingOEMBootScreen && (millis() > ((logo_timeout / 2) + serialSinceMsec))) { LOG_INFO("Switch to OEM screen..."); // Change frames. - static FrameCallback bootOEMFrames[] = {drawOEMBootScreen}; + static FrameCallback bootOEMFrames[] = {graphics::UIRenderer::drawOEMBootScreen}; static const int bootOEMFrameCount = sizeof(bootOEMFrames) / sizeof(bootOEMFrames[0]); ui->setFrames(bootOEMFrames, bootOEMFrameCount); ui->update(); @@ -3926,31 +1230,13 @@ int32_t Screen::runOnce() return (1000 / targetFramerate); } -void Screen::drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - Screen *screen2 = reinterpret_cast(state->userData); - screen2->debugInfo.drawFrame(display, state, x, y); -} - -void Screen::drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - Screen *screen2 = reinterpret_cast(state->userData); - screen2->debugInfo.drawFrameSettings(display, state, x, y); -} - -void Screen::drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - Screen *screen2 = reinterpret_cast(state->userData); - screen2->debugInfo.drawFrameWiFi(display, state, x, y); -} - /* show a message that the SSL cert is being built * it is expected that this will be used during the boot phase */ void Screen::setSSLFrames() { if (address_found.address) { // LOG_DEBUG("Show SSL frames"); - static FrameCallback sslFrames[] = {drawSSLScreen}; + static FrameCallback sslFrames[] = {graphics::NotificationRenderer::NotificationRenderer::drawSSLScreen}; ui->setFrames(sslFrames, 1); ui->update(); } @@ -3962,7 +1248,7 @@ void Screen::setWelcomeFrames() { if (address_found.address) { // LOG_DEBUG("Show Welcome frames"); - static FrameCallback frames[] = {drawWelcomeScreen}; + static FrameCallback frames[] = {graphics::NotificationRenderer::NotificationRenderer::drawWelcomeScreen}; setFrameImmediateDraw(frames); } } @@ -3989,7 +1275,7 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver) // Else, display the usual "overlay" screensaver else { - screensaverOverlay = drawScreensaverOverlay; + screensaverOverlay = graphics::UIRenderer::drawScreensaverOverlay; ui->setOverlays(&screensaverOverlay, 1); } @@ -4081,7 +1367,7 @@ void Screen::setFrames(FrameFocus focus) // If we have a critical fault, show it first fsi.positions.fault = numframes; if (error_code) { - normalFrames[numframes++] = drawCriticalFaultFrame; + normalFrames[numframes++] = graphics::NotificationRenderer::NotificationRenderer::drawCriticalFaultFrame; indicatorIcons.push_back(icon_error); focus = FOCUS_FAULT; // Change our "focus" parameter, to ensure we show the fault frame } @@ -4095,49 +1381,49 @@ void Screen::setFrames(FrameFocus focus) if (willInsertTextMessage) { fsi.positions.textMessage = numframes; - normalFrames[numframes++] = drawTextMessageFrame; + normalFrames[numframes++] = graphics::MessageRenderer::drawTextMessageFrame; indicatorIcons.push_back(icon_mail); } - normalFrames[numframes++] = drawDeviceFocused; + normalFrames[numframes++] = graphics::UIRenderer::drawDeviceFocused; indicatorIcons.push_back(icon_home); #ifndef USE_EINK - normalFrames[numframes++] = drawDynamicNodeListScreen; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen; indicatorIcons.push_back(icon_nodes); #endif // Show detailed node views only on E-Ink builds #ifdef USE_EINK - normalFrames[numframes++] = drawLastHeardScreen; + normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen; indicatorIcons.push_back(icon_nodes); - normalFrames[numframes++] = drawHopSignalScreen; + normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen; indicatorIcons.push_back(icon_signal); - normalFrames[numframes++] = drawDistanceScreen; + normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen; indicatorIcons.push_back(icon_distance); #endif - normalFrames[numframes++] = drawNodeListWithCompasses; + normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; indicatorIcons.push_back(icon_list); - normalFrames[numframes++] = drawCompassAndLocationScreen; + normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; indicatorIcons.push_back(icon_compass); - normalFrames[numframes++] = drawLoRaFocused; + normalFrames[numframes++] = graphics::DebugRenderer::drawLoRaFocused; indicatorIcons.push_back(icon_radio); if (!dismissedFrames.memory) { fsi.positions.memory = numframes; - normalFrames[numframes++] = drawMemoryScreen; + normalFrames[numframes++] = graphics::DebugRenderer::drawMemoryUsage; indicatorIcons.push_back(icon_memory); } for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { - normalFrames[numframes++] = drawNodeInfo; + normalFrames[numframes++] = graphics::UIRenderer::drawNodeInfo; indicatorIcons.push_back(icon_node); } } @@ -4147,16 +1433,16 @@ void Screen::setFrames(FrameFocus focus) // Since frames are basic function pointers, we have to use a helper to // call a method on debugInfo object. // fsi.positions.log = numframes; - // normalFrames[numframes++] = &Screen::drawDebugInfoTrampoline; + // normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoTrampoline; // call a method on debugInfoScreen object (for more details) // fsi.positions.settings = numframes; - // normalFrames[numframes++] = &Screen::drawDebugInfoSettingsTrampoline; + // normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoSettingsTrampoline; #if HAS_WIFI && !defined(ARCH_PORTDUINO) if (!dismissedFrames.wifi && isWifiAvailable()) { fsi.positions.wifi = numframes; - normalFrames[numframes++] = &Screen::drawDebugInfoWiFiTrampoline; + normalFrames[numframes++] = graphics::DebugRenderer::drawDebugInfoWiFiTrampoline; indicatorIcons.push_back(icon_wifi); } #endif @@ -4169,7 +1455,8 @@ void Screen::setFrames(FrameFocus focus) ui->disableAllIndicators(); // Add overlays: frame icons and alert banner) - static OverlayCallback overlays[] = {NavigationBar, drawAlertBannerOverlay}; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, + graphics::NotificationRenderer::NotificationRenderer::drawAlertBannerOverlay}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list @@ -4255,7 +1542,7 @@ void Screen::handleStartFirmwareUpdateScreen() showingNormalScreen = false; EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame - static FrameCallback frames[] = {drawFrameFirmware}; + static FrameCallback frames[] = {graphics::NotificationRenderer::NotificationRenderer::drawFrameFirmware}; setFrameImmediateDraw(frames); } @@ -4322,23 +1609,6 @@ void Screen::removeFunctionSymbol(std::string sym) setFastFramerate(); } -std::string Screen::drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds) -{ - std::string uptime; - - if (days > (hours_in_month * 6)) - uptime = "?"; - else if (days >= 2) - uptime = std::to_string(days) + "d"; - else if (hours >= 2) - uptime = std::to_string(hours) + "h"; - else if (minutes >= 1) - uptime = std::to_string(minutes) + "m"; - else - uptime = std::to_string(seconds) + "s"; - return uptime; -} - void Screen::handlePrint(const char *text) { // the string passed into us probably has a newline, but that would confuse the logging system @@ -4402,321 +1672,6 @@ void Screen::setFastFramerate() runASAP = true; } -void DebugInfo::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setFont(FONT_SMALL); - - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); - - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - } - - char channelStr[20]; - { - concurrency::LockGuard guard(&lock); - snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); - } - - // Display power status - if (powerStatus->getHasBattery()) { - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - drawBattery(display, x, y + 2, imgBattery, powerStatus); - } else { - drawBattery(display, x + 1, y + 3, imgBattery, powerStatus); - } - } else if (powerStatus->knowsUSB()) { - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - display->drawFastImage(x, y + 2, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower); - } else { - display->drawFastImage(x + 1, y + 3, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower); - } - } - // Display nodes status - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus); - } else { - drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 3, nodeStatus); - } -#if HAS_GPS - // Display GPS status - if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - drawGPSpowerstat(display, x, y + 2, gpsStatus); - } else { - if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { - drawGPS(display, x + (SCREEN_WIDTH * 0.63), y + 2, gpsStatus); - } else { - drawGPS(display, x + (SCREEN_WIDTH * 0.63), y + 3, gpsStatus); - } - } -#endif - display->setColor(WHITE); - // Draw the channel name - display->drawString(x, y + FONT_HEIGHT_SMALL, channelStr); - // Draw our hardware ID to assist with bluetooth pairing. Either prefix with Info or S&F Logo - if (moduleConfig.store_forward.enabled) { -#ifdef ARCH_ESP32 - if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat, - (storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit -#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || ARCH_PORTDUINO) && \ - !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, - imgQuestionL1); - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, - imgQuestionL2); -#else - display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, - imgQuestion); -#endif - } else { -#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \ - !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(ourId), y + 3 + FONT_HEIGHT_SMALL, 16, 8, - imgSFL1); - display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(ourId), y + 11 + FONT_HEIGHT_SMALL, 16, 8, - imgSFL2); -#else - display->drawFastImage(x + SCREEN_WIDTH - 13 - display->getStringWidth(ourId), y + 2 + FONT_HEIGHT_SMALL, 11, 8, - imgSF); -#endif - } -#endif - } else { - // TODO: Raspberry Pi supports more than just the one screen size -#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ - defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || ARCH_PORTDUINO) && \ - !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, - imgInfoL1); - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, - imgInfoL2); -#else - display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, imgInfo); -#endif - } - - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(ourId), y + FONT_HEIGHT_SMALL, ourId); - - // Draw any log messages - display->drawLogBuffer(x, y + (FONT_HEIGHT_SMALL * 2)); - - /* Display a heartbeat pixel that blinks every time the frame is redrawn */ -#ifdef SHOW_REDRAWS - if (heartbeat) - display->setPixel(0, 0); - heartbeat = !heartbeat; -#endif -} - -// Jm -void DebugInfo::drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ -#if HAS_WIFI && !defined(ARCH_PORTDUINO) - const char *wifiName = config.network.wifi_ssid; - - display->setFont(FONT_SMALL); - - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); - - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - } - - if (WiFi.status() != WL_CONNECTED) { - display->drawString(x, y, String("WiFi: Not Connected")); - if (config.display.heading_bold) - display->drawString(x + 1, y, String("WiFi: Not Connected")); - } else { - display->drawString(x, y, String("WiFi: Connected")); - if (config.display.heading_bold) - display->drawString(x + 1, y, String("WiFi: Connected")); - - display->drawString(x + SCREEN_WIDTH - display->getStringWidth("RSSI " + String(WiFi.RSSI())), y, - "RSSI " + String(WiFi.RSSI())); - if (config.display.heading_bold) { - display->drawString(x + SCREEN_WIDTH - display->getStringWidth("RSSI " + String(WiFi.RSSI())) - 1, y, - "RSSI " + String(WiFi.RSSI())); - } - } - - display->setColor(WHITE); - - /* - - WL_CONNECTED: assigned when connected to a WiFi network; - - WL_NO_SSID_AVAIL: assigned when no SSID are available; - - WL_CONNECT_FAILED: assigned when the connection fails for all the attempts; - - WL_CONNECTION_LOST: assigned when the connection is lost; - - WL_DISCONNECTED: assigned when disconnected from a network; - - WL_IDLE_STATUS: it is a temporary status assigned when WiFi.begin() is called and remains active until the number of - attempts expires (resulting in WL_CONNECT_FAILED) or a connection is established (resulting in WL_CONNECTED); - - WL_SCAN_COMPLETED: assigned when the scan networks is completed; - - WL_NO_SHIELD: assigned when no WiFi shield is present; - - */ - if (WiFi.status() == WL_CONNECTED) { - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "IP: " + String(WiFi.localIP().toString().c_str())); - } else if (WiFi.status() == WL_NO_SSID_AVAIL) { - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "SSID Not Found"); - } else if (WiFi.status() == WL_CONNECTION_LOST) { - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Connection Lost"); - } else if (WiFi.status() == WL_CONNECT_FAILED) { - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Connection Failed"); - } else if (WiFi.status() == WL_IDLE_STATUS) { - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Idle ... Reconnecting"); - } -#ifdef ARCH_ESP32 - else { - // Codes: - // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, - WiFi.disconnectReasonName(static_cast(getWifiDisconnectReason()))); - } -#else - else { - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Unkown status: " + String(WiFi.status())); - } -#endif - - display->drawString(x, y + FONT_HEIGHT_SMALL * 2, "SSID: " + String(wifiName)); - - display->drawString(x, y + FONT_HEIGHT_SMALL * 3, "http://meshtastic.local"); - - /* Display a heartbeat pixel that blinks every time the frame is redrawn */ -#ifdef SHOW_REDRAWS - if (heartbeat) - display->setPixel(0, 0); - heartbeat = !heartbeat; -#endif -#endif -} - -void DebugInfo::drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setFont(FONT_SMALL); - - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); - - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - } - - char batStr[20]; - if (powerStatus->getHasBattery()) { - int batV = powerStatus->getBatteryVoltageMv() / 1000; - int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; - - snprintf(batStr, sizeof(batStr), "B %01d.%02dV %3d%% %c%c", batV, batCv, powerStatus->getBatteryChargePercent(), - powerStatus->getIsCharging() ? '+' : ' ', powerStatus->getHasUSB() ? 'U' : ' '); - - // Line 1 - display->drawString(x, y, batStr); - if (config.display.heading_bold) - display->drawString(x + 1, y, batStr); - } else { - // Line 1 - display->drawString(x, y, String("USB")); - if (config.display.heading_bold) - display->drawString(x + 1, y, String("USB")); - } - - // auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, true); - - // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode), y, mode); - // if (config.display.heading_bold) - // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode) - 1, y, mode); - - uint32_t currentMillis = millis(); - uint32_t seconds = currentMillis / 1000; - uint32_t minutes = seconds / 60; - uint32_t hours = minutes / 60; - uint32_t days = hours / 24; - // currentMillis %= 1000; - // seconds %= 60; - // minutes %= 60; - // hours %= 24; - - // Show uptime as days, hours, minutes OR seconds - std::string uptime = screen->drawTimeDelta(days, hours, minutes, seconds); - - // Line 1 (Still) - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); - if (config.display.heading_bold) - display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); - - display->setColor(WHITE); - - // Setup string to assemble analogClock string - std::string analogClock = ""; - - uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone - if (rtc_sec > 0) { - long hms = rtc_sec % SEC_PER_DAY; - // hms += tz.tz_dsttime * SEC_PER_HOUR; - // hms -= tz.tz_minuteswest * SEC_PER_MIN; - // mod `hms` to ensure in positive range of [0...SEC_PER_DAY) - hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; - - // Tear apart hms into h:m:s - int hour = hms / SEC_PER_HOUR; - int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; - int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN - - char timebuf[12]; - - if (config.display.use_12h_clock) { - std::string meridiem = "am"; - if (hour >= 12) { - if (hour > 12) - hour -= 12; - meridiem = "pm"; - } - if (hour == 00) { - hour = 12; - } - snprintf(timebuf, sizeof(timebuf), "%d:%02d:%02d%s", hour, min, sec, meridiem.c_str()); - } else { - snprintf(timebuf, sizeof(timebuf), "%02d:%02d:%02d", hour, min, sec); - } - analogClock += timebuf; - } - - // Line 2 - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, analogClock.c_str()); - - // Display Channel Utilization - char chUtil[13]; - snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), y + FONT_HEIGHT_SMALL * 1, chUtil); - -#if HAS_GPS - if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - // Line 3 - if (config.display.gps_format != - meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude - drawGPSAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); - - // Line 4 - drawGPScoordinates(display, x, y + FONT_HEIGHT_SMALL * 3, gpsStatus); - } else { - drawGPSpowerstat(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); - } -#endif -/* Display a heartbeat pixel that blinks every time the frame is redrawn */ -#ifdef SHOW_REDRAWS - if (heartbeat) - display->setPixel(0, 0); - heartbeat = !heartbeat; -#endif -} - int Screen::handleStatusUpdate(const meshtastic::Status *arg) { // LOG_DEBUG("Screen got status update %d", arg->getStatusType()); diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index e348e7571..29e5df226 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -5,6 +5,10 @@ #include "detect/ScanI2C.h" #include "mesh/generated/meshtastic/config.pb.h" #include +#include +#include + +#define getStringCenteredX(s) ((SCREEN_WIDTH - display->getStringWidth(s)) / 2) #if !HAS_SCREEN #include "power.h" @@ -64,6 +68,7 @@ class Screen #include "mesh/MeshModule.h" #include "power.h" #include +#include // 0 to 255, though particular variants might define different defaults #ifndef BRIGHTNESS_DEFAULT @@ -228,21 +233,11 @@ class Screen : public concurrency::OSThread void blink(); - void drawFrameText(OLEDDisplay *, OLEDDisplayUiState *, int16_t, int16_t, const char *); - void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength); // Draw north - void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading); - - static uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight); - float estimatedHeading(double lat, double lon); - void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian); - - void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); - /// Handle button press, trackball or swipe action) void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); } void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); } @@ -322,9 +317,6 @@ class Screen : public concurrency::OSThread } } - /// generates a very brief time delta display - std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds); - /// Overrides the default utf8 character conversion, to replace empty space with question marks static char customFontTableLookup(const uint8_t ch) { @@ -643,13 +635,6 @@ class Screen : public concurrency::OSThread // Sets frame up for immediate drawing void setFrameImmediateDraw(FrameCallback *drawFrames); - /// Called when debug screen is to be drawn, calls through to debugInfo.drawFrame. - static void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - - static void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - - static void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - #if defined(DISPLAY_CLOCK_FRAME) static void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); @@ -706,4 +691,8 @@ class Screen : public concurrency::OSThread extern String alertBannerMessage; extern uint32_t alertBannerUntil; +// Extern declarations for function symbols used in UIRenderer +extern std::vector functionSymbol; +extern std::string functionSymbolString; + #endif \ No newline at end of file diff --git a/src/graphics/ScreenGlobals.cpp b/src/graphics/ScreenGlobals.cpp new file mode 100644 index 000000000..bc139faaf --- /dev/null +++ b/src/graphics/ScreenGlobals.cpp @@ -0,0 +1,6 @@ +#include +#include + +// Global variables for screen function overlay +std::vector functionSymbol; +std::string functionSymbolString; diff --git a/src/graphics/draw/ClockRenderer.h b/src/graphics/draw/ClockRenderer.h new file mode 100644 index 000000000..23ad79d39 --- /dev/null +++ b/src/graphics/draw/ClockRenderer.h @@ -0,0 +1,40 @@ +#pragma once + +#include "graphics/Screen.h" +#include +#include + +namespace graphics +{ + +/// Forward declarations +class Screen; + +/** + * @brief Clock drawing functions + * + * Contains all functions related to drawing analog and digital clocks, + * segmented displays, and time-related UI elements. + */ +namespace ClockRenderer +{ +// Clock frame functions +void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// Segmented display functions +void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale = 1); +void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale = 1); +void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height); +void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height); + +// UI elements for clock displays +void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1); +void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y); + +// Utility functions +bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo); + +} // namespace ClockRenderer + +} // namespace graphics diff --git a/src/graphics/draw/CompassRenderer.cpp b/src/graphics/draw/CompassRenderer.cpp new file mode 100644 index 000000000..b55dd5141 --- /dev/null +++ b/src/graphics/draw/CompassRenderer.cpp @@ -0,0 +1,132 @@ +#include "CompassRenderer.h" +#include "NodeDB.h" +#include "UIRenderer.h" +#include "configuration.h" +#include "gps/GeoCoord.h" +#include "graphics/ScreenFonts.h" +#include + +namespace graphics +{ +namespace CompassRenderer +{ + +// Point helper class for compass calculations +struct Point { + float x, y; + Point(float x, float y) : x(x), y(y) {} + + void rotate(float angle) + { + float cos_a = cos(angle); + float sin_a = sin(angle); + float new_x = x * cos_a - y * sin_a; + float new_y = x * sin_a + y * cos_a; + x = new_x; + y = new_y; + } + + void scale(float factor) + { + x *= factor; + y *= factor; + } + + void translate(float dx, float dy) + { + x += dx; + y += dy; + } +}; + +void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading) +{ + // Show the compass heading (not implemented in original) + // This could draw a "N" indicator or north arrow + // For now, we'll draw a simple north indicator + const float radius = 8.0f; + Point north(0, -radius); + north.rotate(-myHeading); + north.translate(compassX, compassY); + + // Draw a small "N" or north indicator + display->drawCircle(north.x, north.y, 2); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(north.x, north.y - 3, "N"); +} + +void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) +{ + Point tip(0.0f, 0.5f), tail(0.0f, -0.35f); // pointing up initially + float arrowOffsetX = 0.14f, arrowOffsetY = 1.0f; + Point leftArrow(tip.x - arrowOffsetX, tip.y - arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y - arrowOffsetY); + + Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow}; + + for (int i = 0; i < 4; i++) { + arrowPoints[i]->rotate(headingRadian); + arrowPoints[i]->scale(compassDiam * 0.6); + arrowPoints[i]->translate(compassX, compassY); + } + +#ifdef USE_EINK + display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); +#else + display->fillTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); +#endif + display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y); +} + +void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing) +{ + float radians = bearing * DEG_TO_RAD; + + Point tip(0, -size / 2); + Point left(-size / 4, size / 4); + Point right(size / 4, size / 4); + + tip.rotate(radians); + left.rotate(radians); + right.rotate(radians); + + tip.translate(x, y); + left.translate(x, y); + right.translate(x, y); + + display->drawTriangle(tip.x, tip.y, left.x, left.y, right.x, right.y); +} + +float estimatedHeading(double lat, double lon) +{ + // Simple magnetic declination estimation + // This is a very basic implementation - the original might be more sophisticated + return 0.0f; // Return 0 for now, indicating no heading available +} + +uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) +{ + // Calculate appropriate compass diameter based on display size + uint16_t minDimension = (displayWidth < displayHeight) ? displayWidth : displayHeight; + uint16_t maxDiam = minDimension / 3; // Use 1/3 of the smaller dimension + + // Ensure minimum and maximum bounds + if (maxDiam < 16) + maxDiam = 16; + if (maxDiam > 64) + maxDiam = 64; + + return maxDiam; +} + +float calculateBearing(double lat1, double lon1, double lat2, double lon2) +{ + double dLon = (lon2 - lon1) * DEG_TO_RAD; + double y = sin(dLon) * cos(lat2 * DEG_TO_RAD); + double x = cos(lat1 * DEG_TO_RAD) * sin(lat2 * DEG_TO_RAD) - sin(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * cos(dLon); + double bearing = atan2(y, x) * RAD_TO_DEG; + return fmod(bearing + 360.0, 360.0); +} + +} // namespace CompassRenderer +} // namespace graphics diff --git a/src/graphics/draw/CompassRenderer.h b/src/graphics/draw/CompassRenderer.h new file mode 100644 index 000000000..2f7197084 --- /dev/null +++ b/src/graphics/draw/CompassRenderer.h @@ -0,0 +1,36 @@ +#pragma once + +#include "graphics/Screen.h" +#include "mesh/generated/meshtastic/mesh.pb.h" +#include +#include + +namespace graphics +{ + +/// Forward declarations +class Screen; + +/** + * @brief Compass and navigation drawing functions + * + * Contains all functions related to drawing compass elements, headings, + * navigation arrows, and location-based UI components. + */ +namespace CompassRenderer +{ +// Compass drawing functions +void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading); +void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian); +void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing); + +// Navigation and location functions +float estimatedHeading(double lat, double lon); +uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight); + +// Utility functions for bearing calculations +float calculateBearing(double lat1, double lon1, double lat2, double lon2); + +} // namespace CompassRenderer + +} // namespace graphics diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp new file mode 100644 index 000000000..167fd602d --- /dev/null +++ b/src/graphics/draw/DebugRenderer.cpp @@ -0,0 +1,671 @@ +#include "DebugRenderer.h" +#include "../Screen.h" +#include "FSCommon.h" +#include "NodeDB.h" +#include "Throttle.h" +#include "UIRenderer.h" +#include "airtime.h" +#include "configuration.h" +#include "gps/RTC.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" +#include "main.h" +#include "mesh/Channels.h" +#include "mesh/generated/meshtastic/deviceonly.pb.h" +#include "sleep.h" + +#if HAS_WIFI && !defined(ARCH_PORTDUINO) +#include "mesh/wifi/WiFiAPClient.h" +#include +#ifdef ARCH_ESP32 +#include "mesh/wifi/WiFiAPClient.h" +#endif +#endif + +#ifdef ARCH_ESP32 +#include "modules/StoreForwardModule.h" +#endif +#include +#include +#include + +using namespace meshtastic; + +// Battery icon array +static uint8_t imgBattery[16] = {0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xE7, 0x3C}; + +// External variables +extern graphics::Screen *screen; +extern PowerStatus *powerStatus; +extern NodeStatus *nodeStatus; +extern GPSStatus *gpsStatus; +extern Channels channels; +extern "C" { +extern char ourId[5]; +} +extern AirTime *airTime; + +// External functions from Screen.cpp +extern bool heartbeat; + +#ifdef ARCH_ESP32 +extern StoreForwardModule *storeForwardModule; +#endif + +namespace graphics +{ +namespace DebugRenderer +{ + +void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setFont(FONT_SMALL); + + // The coordinates define the left starting point of the text + display->setTextAlignment(TEXT_ALIGN_LEFT); + + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); + } + + char channelStr[20]; + snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex())); + + // Display power status + if (powerStatus->getHasBattery()) { + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + UIRenderer::drawBattery(display, x, y + 2, imgBattery, powerStatus); + } else { + UIRenderer::drawBattery(display, x + 1, y + 3, imgBattery, powerStatus); + } + } else if (powerStatus->knowsUSB()) { + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + display->drawFastImage(x, y + 2, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower); + } else { + display->drawFastImage(x + 1, y + 3, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower); + } + } + // Display nodes status + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus); + } else { + UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 3, nodeStatus); + } +#if HAS_GPS + // Display GPS status + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + UIRenderer::drawGpsPowerStatus(display, x, y + 2, gpsStatus); + } else { + if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { + UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 2, gpsStatus); + } else { + UIRenderer::drawGps(display, x + (SCREEN_WIDTH * 0.63), y + 3, gpsStatus); + } + } +#endif + display->setColor(WHITE); + // Draw the channel name + display->drawString(x, y + FONT_HEIGHT_SMALL, channelStr); + // Draw our hardware ID to assist with bluetooth pairing. Either prefix with Info or S&F Logo + if (moduleConfig.store_forward.enabled) { +#ifdef ARCH_ESP32 + if (!Throttle::isWithinTimespanMs(storeForwardModule->lastHeartbeat, + (storeForwardModule->heartbeatInterval * 1200))) { // no heartbeat, overlap a bit +#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ + !defined(DISPLAY_FORCE_SMALL_FONTS) + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, + imgQuestionL1); + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, + imgQuestionL2); +#else + display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, + imgQuestion); +#endif + } else { +#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \ + !defined(DISPLAY_FORCE_SMALL_FONTS) + display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(ourId), y + 3 + FONT_HEIGHT_SMALL, 16, 8, + imgSFL1); + display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(ourId), y + 11 + FONT_HEIGHT_SMALL, 16, 8, + imgSFL2); +#else + display->drawFastImage(x + SCREEN_WIDTH - 13 - display->getStringWidth(ourId), y + 2 + FONT_HEIGHT_SMALL, 11, 8, + imgSF); +#endif + } +#endif + } else { + // TODO: Raspberry Pi supports more than just the one screen size +#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ + !defined(DISPLAY_FORCE_SMALL_FONTS) + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, + imgInfoL1); + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, + imgInfoL2); +#else + display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, imgInfo); +#endif + } + + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(ourId), y + FONT_HEIGHT_SMALL, ourId); + + // Draw any log messages + display->drawLogBuffer(x, y + (FONT_HEIGHT_SMALL * 2)); + + /* Display a heartbeat pixel that blinks every time the frame is redrawn */ +#ifdef SHOW_REDRAWS + if (heartbeat) + display->setPixel(0, 0); + heartbeat = !heartbeat; +#endif +} + +void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ +#if HAS_WIFI && !defined(ARCH_PORTDUINO) + const char *wifiName = config.network.wifi_ssid; + + display->setFont(FONT_SMALL); + + // The coordinates define the left starting point of the text + display->setTextAlignment(TEXT_ALIGN_LEFT); + + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); + } + + if (WiFi.status() != WL_CONNECTED) { + display->drawString(x, y, String("WiFi: Not Connected")); + if (config.display.heading_bold) + display->drawString(x + 1, y, String("WiFi: Not Connected")); + } else { + display->drawString(x, y, String("WiFi: Connected")); + if (config.display.heading_bold) + display->drawString(x + 1, y, String("WiFi: Connected")); + + display->drawString(x + SCREEN_WIDTH - display->getStringWidth("RSSI " + String(WiFi.RSSI())), y, + "RSSI " + String(WiFi.RSSI())); + if (config.display.heading_bold) { + display->drawString(x + SCREEN_WIDTH - display->getStringWidth("RSSI " + String(WiFi.RSSI())) - 1, y, + "RSSI " + String(WiFi.RSSI())); + } + } + + display->setColor(WHITE); + + /* + - WL_CONNECTED: assigned when connected to a WiFi network; + - WL_NO_SSID_AVAIL: assigned when no SSID are available; + - WL_CONNECT_FAILED: assigned when the connection fails for all the attempts; + - WL_CONNECTION_LOST: assigned when the connection is lost; + - WL_DISCONNECTED: assigned when disconnected from a network; + - WL_IDLE_STATUS: it is a temporary status assigned when WiFi.begin() is called and remains active until the number of + attempts expires (resulting in WL_CONNECT_FAILED) or a connection is established (resulting in WL_CONNECTED); + - WL_SCAN_COMPLETED: assigned when the scan networks is completed; + - WL_NO_SHIELD: assigned when no WiFi shield is present; + + */ + if (WiFi.status() == WL_CONNECTED) { + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "IP: " + String(WiFi.localIP().toString().c_str())); + } else if (WiFi.status() == WL_NO_SSID_AVAIL) { + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "SSID Not Found"); + } else if (WiFi.status() == WL_CONNECTION_LOST) { + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Connection Lost"); + } else if (WiFi.status() == WL_IDLE_STATUS) { + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Idle ... Reconnecting"); + } else if (WiFi.status() == WL_CONNECT_FAILED) { + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Connection Failed"); + } +#ifdef ARCH_ESP32 + else { + // Codes: + // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, + WiFi.disconnectReasonName(static_cast(getWifiDisconnectReason()))); + } +#else + else { + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Unkown status: " + String(WiFi.status())); + } +#endif + + display->drawString(x, y + FONT_HEIGHT_SMALL * 2, "SSID: " + String(wifiName)); + + display->drawString(x, y + FONT_HEIGHT_SMALL * 3, "http://meshtastic.local"); + + /* Display a heartbeat pixel that blinks every time the frame is redrawn */ +#ifdef SHOW_REDRAWS + if (heartbeat) + display->setPixel(0, 0); + heartbeat = !heartbeat; +#endif +#endif +} + +void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setFont(FONT_SMALL); + + // The coordinates define the left starting point of the text + display->setTextAlignment(TEXT_ALIGN_LEFT); + + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); + } + + char batStr[20]; + if (powerStatus->getHasBattery()) { + int batV = powerStatus->getBatteryVoltageMv() / 1000; + int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; + + snprintf(batStr, sizeof(batStr), "B %01d.%02dV %3d%% %c%c", batV, batCv, powerStatus->getBatteryChargePercent(), + powerStatus->getIsCharging() ? '+' : ' ', powerStatus->getHasUSB() ? 'U' : ' '); + + // Line 1 + display->drawString(x, y, batStr); + if (config.display.heading_bold) + display->drawString(x + 1, y, batStr); + } else { + // Line 1 + display->drawString(x, y, String("USB")); + if (config.display.heading_bold) + display->drawString(x + 1, y, String("USB")); + } + + // auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, true); + + // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode), y, mode); + // if (config.display.heading_bold) + // display->drawString(x + SCREEN_WIDTH - display->getStringWidth(mode) - 1, y, mode); + + uint32_t currentMillis = millis(); + uint32_t seconds = currentMillis / 1000; + uint32_t minutes = seconds / 60; + uint32_t hours = minutes / 60; + uint32_t days = hours / 24; + // currentMillis %= 1000; + // seconds %= 60; + // minutes %= 60; + // hours %= 24; + + // Show uptime as days, hours, minutes OR seconds + std::string uptime = UIRenderer::drawTimeDelta(days, hours, minutes, seconds); + + // Line 1 (Still) + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); + if (config.display.heading_bold) + display->drawString(x - 1 + SCREEN_WIDTH - display->getStringWidth(uptime.c_str()), y, uptime.c_str()); + + display->setColor(WHITE); + + // Setup string to assemble analogClock string + std::string analogClock = ""; + + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone + if (rtc_sec > 0) { + long hms = rtc_sec % SEC_PER_DAY; + // hms += tz.tz_dsttime * SEC_PER_HOUR; + // hms -= tz.tz_minuteswest * SEC_PER_MIN; + // mod `hms` to ensure in positive range of [0...SEC_PER_DAY) + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + // Tear apart hms into h:m:s + int hour = hms / SEC_PER_HOUR; + int min = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + int sec = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN + + char timebuf[12]; + + if (config.display.use_12h_clock) { + std::string meridiem = "am"; + if (hour >= 12) { + if (hour > 12) + hour -= 12; + meridiem = "pm"; + } + if (hour == 00) { + hour = 12; + } + snprintf(timebuf, sizeof(timebuf), "%d:%02d:%02d%s", hour, min, sec, meridiem.c_str()); + } else { + snprintf(timebuf, sizeof(timebuf), "%02d:%02d:%02d", hour, min, sec); + } + analogClock += timebuf; + } + + // Line 2 + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, analogClock.c_str()); + + // Display Channel Utilization + char chUtil[13]; + snprintf(chUtil, sizeof(chUtil), "ChUtil %2.0f%%", airTime->channelUtilizationPercent()); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(chUtil), y + FONT_HEIGHT_SMALL * 1, chUtil); + +#if HAS_GPS + if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + // Line 3 + if (config.display.gps_format != + meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) // if DMS then don't draw altitude + UIRenderer::drawGpsAltitude(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); + + // Line 4 + UIRenderer::drawGpsCoordinates(display, x, y + FONT_HEIGHT_SMALL * 3, gpsStatus); + } else { + UIRenderer::drawGpsPowerStatus(display, x, y + FONT_HEIGHT_SMALL * 2, gpsStatus); + } +#endif +/* Display a heartbeat pixel that blinks every time the frame is redrawn */ +#ifdef SHOW_REDRAWS + if (heartbeat) + display->setPixel(0, 0); + heartbeat = !heartbeat; +#endif +} + +// Trampoline functions for DebugInfo class access +void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + drawFrame(display, state, x, y); +} + +void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + drawFrameSettings(display, state, x, y); +} + +void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + drawFrameWiFi(display, state, x, y); +} + +// **************************** +// * LoRa Focused Screen * +// **************************** +void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // === Header === + graphics::drawCommonHeader(display, x, y); + + // === Draw title (aligned with header baseline) === + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const char *titleStr = (SCREEN_WIDTH > 128) ? "LoRa Info" : "LoRa"; + const int centerX = x + SCREEN_WIDTH / 2; + + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->setColor(BLACK); + } + + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(centerX, textY, titleStr); + if (config.display.heading_bold) { + display->drawString(centerX + 1, textY, titleStr); + } + display->setColor(WHITE); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === First Row: Region / BLE Name === + graphics::UIRenderer::drawNodes(display, x, compactFirstLine + 3, nodeStatus, 0, true); + + uint8_t dmac[6]; + char shortnameble[35]; + getMacAddr(dmac); + snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); + snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", ourId); + int textWidth = display->getStringWidth(shortnameble); + int nameX = (SCREEN_WIDTH - textWidth); + display->drawString(nameX, compactFirstLine, shortnameble); + + // === Second Row: Radio Preset === + auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); + char regionradiopreset[25]; + const char *region = myRegion ? myRegion->name : NULL; + snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); + textWidth = display->getStringWidth(regionradiopreset); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, compactSecondLine, regionradiopreset); + + // === Third Row: Frequency / ChanNum === + char frequencyslot[35]; + char freqStr[16]; + float freq = RadioLibInterface::instance->getFreq(); + snprintf(freqStr, sizeof(freqStr), "%.3f", freq); + if (config.lora.channel_num == 0) { + snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %s", freqStr); + } else { + snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Chan: %s (%d)", freqStr, config.lora.channel_num); + } + size_t len = strlen(frequencyslot); + if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { + frequencyslot[len - 4] = '\0'; // Remove the last three characters + } + textWidth = display->getStringWidth(frequencyslot); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, compactThirdLine, frequencyslot); + + // === Fourth Row: Channel Utilization === + const char *chUtil = "ChUtil:"; + char chUtilPercentage[10]; + snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); + + int chUtil_x = (SCREEN_WIDTH > 128) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; + int chUtil_y = compactFourthLine + 3; + + int chutil_bar_width = (SCREEN_WIDTH > 128) ? 100 : 50; + int chutil_bar_height = (SCREEN_WIDTH > 128) ? 12 : 7; + int extraoffset = (SCREEN_WIDTH > 128) ? 6 : 3; + int chutil_percent = airTime->channelUtilizationPercent(); + + int centerofscreen = SCREEN_WIDTH / 2; + int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; + int starting_position = centerofscreen - total_line_content_width; + + display->drawString(starting_position, compactFourthLine, chUtil); + + // Force 56% or higher to show a full 100% bar, text would still show related percent. + if (chutil_percent >= 61) { + chutil_percent = 100; + } + + // Weighting for nonlinear segments + float milestone1 = 25; + float milestone2 = 40; + float weight1 = 0.45; // Weight for 0–25% + float weight2 = 0.35; // Weight for 25–40% + float weight3 = 0.20; // Weight for 40–100% + float totalWeight = weight1 + weight2 + weight3; + + int seg1 = chutil_bar_width * (weight1 / totalWeight); + int seg2 = chutil_bar_width * (weight2 / totalWeight); + int seg3 = chutil_bar_width * (weight3 / totalWeight); + + int fillRight = 0; + + if (chutil_percent <= milestone1) { + fillRight = (seg1 * (chutil_percent / milestone1)); + } else if (chutil_percent <= milestone2) { + fillRight = seg1 + (seg2 * ((chutil_percent - milestone1) / (milestone2 - milestone1))); + } else { + fillRight = seg1 + seg2 + (seg3 * ((chutil_percent - milestone2) / (100 - milestone2))); + } + + // Draw outline + display->drawRect(starting_position + chUtil_x, chUtil_y, chutil_bar_width, chutil_bar_height); + + // Fill progress + if (fillRight > 0) { + display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); + } + + display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, compactFourthLine, chUtilPercentage); +} + +// **************************** +// * Memory Screen * +// **************************** +void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === Header === + graphics::drawCommonHeader(display, x, y); + + // === Draw title === + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const char *titleStr = (SCREEN_WIDTH > 128) ? "Memory" : "Mem"; + const int centerX = x + SCREEN_WIDTH / 2; + + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->setColor(BLACK); + } + + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(centerX, textY, titleStr); + if (config.display.heading_bold) { + display->drawString(centerX + 1, textY, titleStr); + } + display->setColor(WHITE); + + // === Layout === + int contentY = y + FONT_HEIGHT_SMALL; + const int rowYOffset = FONT_HEIGHT_SMALL - 3; + const int barHeight = 6; + const int labelX = x; + const int barsOffset = (SCREEN_WIDTH > 128) ? 24 : 0; + const int barX = x + 40 + barsOffset; + + int rowY = contentY; + + // === Heap delta tracking (disabled) === + /* + static uint32_t previousHeapFree = 0; + static int32_t totalHeapDelta = 0; + static int deltaChangeCount = 0; + */ + + auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { + if (total == 0) + return; + + int percent = (used * 100) / total; + + char combinedStr[24]; + if (SCREEN_WIDTH > 128) { + snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %lu/%luKB", (percent > 80) ? "! " : "", percent, used / 1024, + total / 1024); + } else { + snprintf(combinedStr, sizeof(combinedStr), "%s%3d%%", (percent > 80) ? "! " : "", percent); + } + + int textWidth = display->getStringWidth(combinedStr); + int adjustedBarWidth = SCREEN_WIDTH - barX - textWidth - 6; + if (adjustedBarWidth < 10) + adjustedBarWidth = 10; + + int fillWidth = (used * adjustedBarWidth) / total; + + // Label + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawString(labelX, rowY, label); + + // Bar + int barY = rowY + (FONT_HEIGHT_SMALL - barHeight) / 2; + display->setColor(WHITE); + display->drawRect(barX, barY, adjustedBarWidth, barHeight); + + display->fillRect(barX, barY, fillWidth, barHeight); + display->setColor(WHITE); + + // Value string + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(SCREEN_WIDTH - 2, rowY, combinedStr); + + rowY += rowYOffset; + + // === Heap delta display (disabled) === + /* + if (isHeap && previousHeapFree > 0) { + int32_t delta = (int32_t)(memGet.getFreeHeap() - previousHeapFree); + if (delta != 0) { + totalHeapDelta += delta; + deltaChangeCount++; + + char deltaStr[16]; + snprintf(deltaStr, sizeof(deltaStr), "%ld", delta); + + int deltaX = centerX - display->getStringWidth(deltaStr) / 2 - 8; + int deltaY = rowY + 1; + + // Triangle + if (delta > 0) { + display->drawLine(deltaX, deltaY + 6, deltaX + 3, deltaY); + display->drawLine(deltaX + 3, deltaY, deltaX + 6, deltaY + 6); + display->drawLine(deltaX, deltaY + 6, deltaX + 6, deltaY + 6); + } else { + display->drawLine(deltaX, deltaY, deltaX + 3, deltaY + 6); + display->drawLine(deltaX + 3, deltaY + 6, deltaX + 6, deltaY); + display->drawLine(deltaX, deltaY, deltaX + 6, deltaY); + } + + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(centerX + 6, deltaY, deltaStr); + rowY += rowYOffset; + } + } + + if (isHeap) { + previousHeapFree = memGet.getFreeHeap(); + } + */ + }; + + // === Memory values === + uint32_t heapUsed = memGet.getHeapSize() - memGet.getFreeHeap(); + uint32_t heapTotal = memGet.getHeapSize(); + + uint32_t psramUsed = memGet.getPsramSize() - memGet.getFreePsram(); + uint32_t psramTotal = memGet.getPsramSize(); + + uint32_t flashUsed = 0, flashTotal = 0; +#ifdef ESP32 + flashUsed = FSCom.usedBytes(); + flashTotal = FSCom.totalBytes(); +#endif + + uint32_t sdUsed = 0, sdTotal = 0; + bool hasSD = false; + /* + #ifdef HAS_SDCARD + hasSD = SD.cardType() != CARD_NONE; + if (hasSD) { + sdUsed = SD.usedBytes(); + sdTotal = SD.totalBytes(); + } + #endif + */ + // === Draw memory rows + drawUsageRow("Heap:", heapUsed, heapTotal, true); + drawUsageRow("PSRAM:", psramUsed, psramTotal); +#ifdef ESP32 + if (flashTotal > 0) + drawUsageRow("Flash:", flashUsed, flashTotal); +#endif + if (hasSD && sdTotal > 0) + drawUsageRow("SD:", sdUsed, sdTotal); +} +} // namespace DebugRenderer +} // namespace graphics diff --git a/src/graphics/draw/DebugRenderer.h b/src/graphics/draw/DebugRenderer.h new file mode 100644 index 000000000..f4d484f58 --- /dev/null +++ b/src/graphics/draw/DebugRenderer.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +namespace graphics +{ + +/// Forward declarations +class Screen; +class DebugInfo; + +/** + * @brief Debug and diagnostic drawing functions + * + * Contains all functions related to drawing debug information, + * WiFi status, settings screens, and diagnostic data. + */ +namespace DebugRenderer +{ +// Debug frame functions +void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// Trampoline functions for framework callback compatibility +void drawDebugInfoTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDebugInfoSettingsTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDebugInfoWiFiTrampoline(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// LoRa information display +void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// Memory screen display +void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +} // namespace DebugRenderer + +} // namespace graphics diff --git a/src/graphics/draw/DrawRenderers.h b/src/graphics/draw/DrawRenderers.h new file mode 100644 index 000000000..6f1929ebd --- /dev/null +++ b/src/graphics/draw/DrawRenderers.h @@ -0,0 +1,38 @@ +#pragma once + +/** + * @brief Master include file for all Screen draw renderers + * + * This file includes all the individual renderer headers to provide + * a convenient single include for accessing all draw functions. + */ + +#include "graphics/draw/ClockRenderer.h" +#include "graphics/draw/CompassRenderer.h" +#include "graphics/draw/DebugRenderer.h" +#include "graphics/draw/NodeListRenderer.h" +#include "graphics/draw/ScreenRenderer.h" +#include "graphics/draw/UIRenderer.h" + +namespace graphics +{ + +/** + * @brief Collection of all draw renderers + * + * This namespace provides access to all the specialized rendering + * functions organized by category. + */ +namespace DrawRenderers +{ +// Re-export all renderer namespaces for convenience +using namespace ClockRenderer; +using namespace CompassRenderer; +using namespace DebugRenderer; +using namespace NodeListRenderer; +using namespace ScreenRenderer; +using namespace UIRenderer; + +} // namespace DrawRenderers + +} // namespace graphics diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp new file mode 100644 index 000000000..a61f40ffc --- /dev/null +++ b/src/graphics/draw/MessageRenderer.cpp @@ -0,0 +1,448 @@ +/* +BaseUI + +Developed and Maintained By: +- Ronald Garcia (HarukiToreda) – Lead development and implementation. +- JasonP (Xaositek) – Screen layout and icon design, UI improvements and testing. +- TonyG (Tropho) – Project management, structural planning, and testing + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#include "MessageRenderer.h" + +// Core includes +#include "NodeDB.h" +#include "configuration.h" +#include "gps/RTC.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/emotes.h" +#include "main.h" +#include "meshUtils.h" + +// Additional includes for UI rendering +#include "UIRenderer.h" + +// Additional includes for dependencies +#include +#include + +// External declarations +extern bool hasUnreadMessage; +extern meshtastic_DeviceState devicestate; + +using graphics::Emote; +using graphics::emotes; +using graphics::numEmotes; + +namespace graphics +{ +namespace MessageRenderer +{ + +// Forward declaration from Screen.cpp - this function needs to be accessible +// For now, we'll implement a local version that matches the Screen.cpp functionality +bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo) +{ + // Cache the result - avoid frequent recalculation + static uint8_t hoursCached = 0, minutesCached = 0; + static uint32_t daysAgoCached = 0; + static uint32_t secondsAgoCached = 0; + static bool validCached = false; + + // Abort: if timezone not set + if (strlen(config.device.tzdef) == 0) { + validCached = false; + return validCached; + } + + // Abort: if invalid pointers passed + if (hours == nullptr || minutes == nullptr || daysAgo == nullptr) { + validCached = false; + return validCached; + } + + // Abort: if time seems invalid.. (> 6 months ago, probably seen before RTC set) + if (secondsAgo > SEC_PER_DAY * 30UL * 6) { + validCached = false; + return validCached; + } + + // If repeated request, don't bother recalculating + if (secondsAgo - secondsAgoCached < 60 && secondsAgoCached != 0) { + if (validCached) { + *hours = hoursCached; + *minutes = minutesCached; + *daysAgo = daysAgoCached; + } + return validCached; + } + + // Get local time + uint32_t secondsRTC = getValidTime(RTCQuality::RTCQualityDevice, true); // Get local time + + // Abort: if RTC not set + if (!secondsRTC) { + validCached = false; + return validCached; + } + + // Get absolute time when last seen + uint32_t secondsSeenAt = secondsRTC - secondsAgo; + + // Calculate daysAgo + *daysAgo = (secondsRTC / SEC_PER_DAY) - (secondsSeenAt / SEC_PER_DAY); // How many "midnights" have passed + + // Get seconds since midnight + uint32_t hms = (secondsRTC - secondsAgo) % SEC_PER_DAY; + hms = (hms + SEC_PER_DAY) % SEC_PER_DAY; + + // Tear apart hms into hours and minutes + *hours = hms / SEC_PER_HOUR; + *minutes = (hms % SEC_PER_HOUR) / SEC_PER_MIN; + + // Cache the result + daysAgoCached = *daysAgo; + hoursCached = *hours; + minutesCached = *minutes; + secondsAgoCached = secondsAgo; + + validCached = true; + return validCached; +} + +void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount) +{ + int cursorX = x; + const int fontHeight = FONT_HEIGHT_SMALL; + + // === Step 1: Find tallest emote in the line === + int maxIconHeight = fontHeight; + for (size_t i = 0; i < line.length();) { + bool matched = false; + for (int e = 0; e < emoteCount; ++e) { + size_t emojiLen = strlen(emotes[e].label); + if (line.compare(i, emojiLen, emotes[e].label) == 0) { + if (emotes[e].height > maxIconHeight) + maxIconHeight = emotes[e].height; + i += emojiLen; + matched = true; + break; + } + } + if (!matched) { + uint8_t c = static_cast(line[i]); + if ((c & 0xE0) == 0xC0) + i += 2; + else if ((c & 0xF0) == 0xE0) + i += 3; + else if ((c & 0xF8) == 0xF0) + i += 4; + else + i += 1; + } + } + + // === Step 2: Baseline alignment === + int lineHeight = std::max(fontHeight, maxIconHeight); + int baselineOffset = (lineHeight - fontHeight) / 2; + int fontY = y + baselineOffset; + int fontMidline = fontY + fontHeight / 2; + + // === Step 3: Render line in segments === + size_t i = 0; + bool inBold = false; + + while (i < line.length()) { + // Check for ** start/end for faux bold + if (line.compare(i, 2, "**") == 0) { + inBold = !inBold; + i += 2; + continue; + } + + // Look ahead for the next emote match + size_t nextEmotePos = std::string::npos; + const Emote *matchedEmote = nullptr; + size_t emojiLen = 0; + + for (int e = 0; e < emoteCount; ++e) { + size_t pos = line.find(emotes[e].label, i); + if (pos != std::string::npos && (nextEmotePos == std::string::npos || pos < nextEmotePos)) { + nextEmotePos = pos; + matchedEmote = &emotes[e]; + emojiLen = strlen(emotes[e].label); + } + } + + // Render normal text segment up to the emote or bold toggle + size_t nextControl = std::min(nextEmotePos, line.find("**", i)); + if (nextControl == std::string::npos) + nextControl = line.length(); + + if (nextControl > i) { + std::string textChunk = line.substr(i, nextControl - i); + if (inBold) { + // Faux bold: draw twice, offset by 1px + display->drawString(cursorX + 1, fontY, textChunk.c_str()); + } + display->drawString(cursorX, fontY, textChunk.c_str()); + cursorX += display->getStringWidth(textChunk.c_str()); + i = nextControl; + continue; + } + + // Render the emote (if found) + if (matchedEmote && i == nextEmotePos) { + int iconY = fontMidline - matchedEmote->height / 2 - 1; + display->drawXbm(cursorX, iconY, matchedEmote->width, matchedEmote->height, matchedEmote->bitmap); + cursorX += matchedEmote->width + 1; + i += emojiLen; + } else { + // No more emotes β€” render the rest of the line + std::string remaining = line.substr(i); + if (inBold) { + display->drawString(cursorX + 1, fontY, remaining.c_str()); + } + display->drawString(cursorX, fontY, remaining.c_str()); + cursorX += display->getStringWidth(remaining.c_str()); + break; + } + } +} + +void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Clear the unread message indicator when viewing the message + hasUnreadMessage = false; + + const meshtastic_MeshPacket &mp = devicestate.rx_text_message; + const char *msg = reinterpret_cast(mp.decoded.payload.bytes); + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + const int navHeight = FONT_HEIGHT_SMALL; + const int scrollBottom = SCREEN_HEIGHT - navHeight; + const int usableHeight = scrollBottom; + const int textWidth = SCREEN_WIDTH; + const int cornerRadius = 2; + + bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED); + bool isBold = config.display.heading_bold; + + // === Header Construction === + meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); + char headerStr[80]; + const char *sender = "???"; + if (node && node->has_user) { + if (SCREEN_WIDTH >= 200 && strlen(node->user.long_name) > 0) { + sender = node->user.long_name; + } else { + sender = node->user.short_name; + } + } + uint32_t seconds = sinceReceived(&mp), minutes = seconds / 60, hours = minutes / 60, days = hours / 24; + uint8_t timestampHours, timestampMinutes; + int32_t daysAgo; + bool useTimestamp = deltaToTimestamp(seconds, ×tampHours, ×tampMinutes, &daysAgo); + + if (useTimestamp && minutes >= 15 && daysAgo == 0) { + std::string prefix = (daysAgo == 1 && SCREEN_WIDTH >= 200) ? "Yesterday" : "At"; + if (config.display.use_12h_clock) { + bool isPM = timestampHours >= 12; + timestampHours = timestampHours % 12; + if (timestampHours == 0) + timestampHours = 12; + snprintf(headerStr, sizeof(headerStr), "%s %d:%02d%s from %s", prefix.c_str(), timestampHours, timestampMinutes, + isPM ? "p" : "a", sender); + } else { + snprintf(headerStr, sizeof(headerStr), "%s %d:%02d from %s", prefix.c_str(), timestampHours, timestampMinutes, + sender); + } + } else { + snprintf(headerStr, sizeof(headerStr), "%s ago from %s", UIRenderer::drawTimeDelta(days, hours, minutes, seconds).c_str(), + sender); + } + +#ifndef EXCLUDE_EMOJI + // === Bounce animation setup === + static uint32_t lastBounceTime = 0; + static int bounceY = 0; + const int bounceRange = 2; // Max pixels to bounce up/down + const int bounceInterval = 60; // How quickly to change bounce direction (ms) + + uint32_t now = millis(); + if (now - lastBounceTime >= bounceInterval) { + lastBounceTime = now; + bounceY = (bounceY + 1) % (bounceRange * 2); + } + for (int i = 0; i < numEmotes; ++i) { + const Emote &e = emotes[i]; + if (strcmp(msg, e.label) == 0) { + // Draw the header + if (isInverted) { + drawRoundedHighlight(display, x, 0, SCREEN_WIDTH, FONT_HEIGHT_SMALL - 1, cornerRadius); + display->setColor(BLACK); + display->drawString(x + 3, 0, headerStr); + if (isBold) + display->drawString(x + 4, 0, headerStr); + display->setColor(WHITE); + } else { + display->drawString(x, 0, headerStr); + if (SCREEN_WIDTH > 128) { + display->drawLine(0, 20, SCREEN_WIDTH, 20); + } else { + display->drawLine(0, 14, SCREEN_WIDTH, 14); + } + } + + // Center the emote below header + apply bounce + int remainingHeight = SCREEN_HEIGHT - FONT_HEIGHT_SMALL - navHeight; + int emoteY = FONT_HEIGHT_SMALL + (remainingHeight - e.height) / 2 + bounceY - bounceRange; + display->drawXbm((SCREEN_WIDTH - e.width) / 2, emoteY, e.width, e.height, e.bitmap); + return; + } + } +#endif + + // === Word-wrap and build line list === + char messageBuf[237]; + snprintf(messageBuf, sizeof(messageBuf), "%s", msg); + + std::vector lines; + lines.push_back(std::string(headerStr)); // Header line is always first + + std::string line, word; + for (int i = 0; messageBuf[i]; ++i) { + char ch = messageBuf[i]; + if (ch == '\n') { + if (!word.empty()) + line += word; + if (!line.empty()) + lines.push_back(line); + line.clear(); + word.clear(); + } else if (ch == ' ') { + line += word + ' '; + word.clear(); + } else { + word += ch; + std::string test = line + word; + if (display->getStringWidth(test.c_str()) > textWidth + 4) { + if (!line.empty()) + lines.push_back(line); + line = word; + word.clear(); + } + } + } + if (!word.empty()) + line += word; + if (!line.empty()) + lines.push_back(line); + + // === Scrolling logic === + std::vector rowHeights; + + for (const auto &line : lines) { + int maxHeight = FONT_HEIGHT_SMALL; + for (int i = 0; i < numEmotes; ++i) { + const Emote &e = emotes[i]; + if (line.find(e.label) != std::string::npos) { + if (e.height > maxHeight) + maxHeight = e.height; + } + } + rowHeights.push_back(maxHeight); + } + int totalHeight = 0; + for (size_t i = 1; i < rowHeights.size(); ++i) { + totalHeight += rowHeights[i]; + } + int usableScrollHeight = usableHeight - rowHeights[0]; // remove header height + int scrollStop = std::max(0, totalHeight - usableScrollHeight); + + static float scrollY = 0.0f; + static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0; + static bool waitingToReset = false, scrollStarted = false; + + // === Smooth scrolling adjustment === + // You can tweak this divisor to change how smooth it scrolls. + // Lower = smoother, but can feel slow. + float delta = (now - lastTime) / 400.0f; + lastTime = now; + + const float scrollSpeed = 2.0f; // pixels per second + + // Delay scrolling start by 2 seconds + if (scrollStartDelay == 0) + scrollStartDelay = now; + if (!scrollStarted && now - scrollStartDelay > 2000) + scrollStarted = true; + + if (totalHeight > usableHeight) { + if (scrollStarted) { + if (!waitingToReset) { + scrollY += delta * scrollSpeed; + if (scrollY >= scrollStop) { + scrollY = scrollStop; + waitingToReset = true; + pauseStart = lastTime; + } + } else if (lastTime - pauseStart > 3000) { + scrollY = 0; + waitingToReset = false; + scrollStarted = false; + scrollStartDelay = lastTime; + } + } + } else { + scrollY = 0; + } + + int scrollOffset = static_cast(scrollY); + int yOffset = -scrollOffset; + if (!isInverted) { + if (SCREEN_WIDTH > 128) { + display->drawLine(0, yOffset + 20, SCREEN_WIDTH, yOffset + 20); + } else { + display->drawLine(0, yOffset + 14, SCREEN_WIDTH, yOffset + 14); + } + } + + // === Render visible lines === + for (size_t i = 0; i < lines.size(); ++i) { + int lineY = yOffset; + for (size_t j = 0; j < i; ++j) + lineY += rowHeights[j]; + if (lineY > -rowHeights[i] && lineY < scrollBottom) { + if (i == 0 && isInverted) { + drawRoundedHighlight(display, x, lineY, SCREEN_WIDTH, FONT_HEIGHT_SMALL - 1, cornerRadius); + display->setColor(BLACK); + display->drawString(x + 3, lineY, lines[i].c_str()); + if (isBold) + display->drawString(x + 4, lineY, lines[i].c_str()); + display->setColor(WHITE); + } else { + drawStringWithEmotes(display, x, lineY, lines[i], emotes, numEmotes); + } + } + } +} + +} // namespace MessageRenderer +} // namespace graphics diff --git a/src/graphics/draw/MessageRenderer.h b/src/graphics/draw/MessageRenderer.h new file mode 100644 index 000000000..d92b96014 --- /dev/null +++ b/src/graphics/draw/MessageRenderer.h @@ -0,0 +1,18 @@ +#pragma once +#include "OLEDDisplay.h" +#include "OLEDDisplayUi.h" +#include "graphics/emotes.h" + +namespace graphics +{ +namespace MessageRenderer +{ + +// Text and emote rendering +void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount); + +/// Draws the text message frame for displaying received messages +void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +} // namespace MessageRenderer +} // namespace graphics diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp new file mode 100644 index 000000000..7bf742f4f --- /dev/null +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -0,0 +1,877 @@ +#include "NodeListRenderer.h" +#include "CompassRenderer.h" +#include "NodeDB.h" +#include "UIRenderer.h" +#include "configuration.h" +#include "gps/GeoCoord.h" +#include "gps/RTC.h" // for getTime() function +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" +#include + +// Forward declarations for functions defined in Screen.cpp +namespace graphics +{ +extern bool haveGlyphs(const char *str); +} // namespace graphics + +// Global screen instance +extern graphics::Screen *screen; + +namespace graphics +{ +namespace NodeListRenderer +{ + +// Function moved from Screen.cpp to NodeListRenderer.cpp since it's primarily used here +void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display) +{ + for (int row = 0; row < height; row++) { + uint8_t rowMask = (1 << row); + for (int col = 0; col < width; col++) { + uint8_t colData = pgm_read_byte(&bitmapXBM[col]); + if (colData & rowMask) { + // Note: rows become X, columns become Y after transpose + display->fillRect(x + row * 2, y + col * 2, 2, 2); + } + } + } +} + +// Static variables for dynamic cycling +static NodeListMode currentMode = MODE_LAST_HEARD; +static int scrollIndex = 0; + +// ============================= +// Utility Functions +// ============================= + +String getSafeNodeName(meshtastic_NodeInfoLite *node) +{ + String nodeName = "?"; + if (node->has_user && strlen(node->user.short_name) > 0) { + bool valid = true; + const char *name = node->user.short_name; + for (size_t i = 0; i < strlen(name); i++) { + uint8_t c = (uint8_t)name[i]; + if (c < 32 || c > 126) { + valid = false; + break; + } + } + if (valid) { + nodeName = name; + } else { + char idStr[6]; + snprintf(idStr, sizeof(idStr), "%04X", (uint16_t)(node->num & 0xFFFF)); + nodeName = String(idStr); + } + } + return nodeName; +} + +uint32_t sinceLastSeen(meshtastic_NodeInfoLite *node) +{ + uint32_t now = getTime(); + uint32_t last_seen = node->last_heard; + if (last_seen == 0 || now < last_seen) { + return UINT32_MAX; + } + return now - last_seen; +} + +const char *getCurrentModeTitle(int screenWidth) +{ + switch (currentMode) { + case MODE_LAST_HEARD: + return "Node List"; + case MODE_HOP_SIGNAL: + return (screenWidth > 128) ? "Hops/Signal" : "Hops/Sig"; + case MODE_DISTANCE: + return "Distance"; + default: + return "Nodes"; + } +} + +// Use dynamic timing based on mode +unsigned long getModeCycleIntervalMs() +{ + return 3000; +} + +// Calculate bearing between two lat/lon points +float calculateBearing(double lat1, double lon1, double lat2, double lon2) +{ + double dLon = (lon2 - lon1) * DEG_TO_RAD; + double y = sin(dLon) * cos(lat2 * DEG_TO_RAD); + double x = cos(lat1 * DEG_TO_RAD) * sin(lat2 * DEG_TO_RAD) - sin(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * cos(dLon); + double bearing = atan2(y, x) * RAD_TO_DEG; + return fmod(bearing + 360.0, 360.0); +} + +int calculateMaxScroll(int totalEntries, int visibleRows) +{ + return std::max(0, (totalEntries - 1) / (visibleRows * 2)); +} + +void retrieveAndSortNodes(std::vector &nodeList) +{ + size_t numNodes = nodeDB->getNumMeshNodes(); + for (size_t i = 0; i < numNodes; i++) { + meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i); + if (!node || node->num == nodeDB->getNodeNum()) + continue; + + NodeEntry entry; + entry.node = node; + entry.sortValue = sinceLastSeen(node); + + nodeList.push_back(entry); + } + + // Sort nodes: favorites first, then by last heard (most recent first) + std::sort(nodeList.begin(), nodeList.end(), [](const NodeEntry &a, const NodeEntry &b) { + bool aFav = a.node->is_favorite; + bool bFav = b.node->is_favorite; + if (aFav != bFav) + return aFav > bFav; + if (a.sortValue == 0 || a.sortValue == UINT32_MAX) + return false; + if (b.sortValue == 0 || b.sortValue == UINT32_MAX) + return true; + return a.sortValue < b.sortValue; + }); +} + +void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) +{ + int columnWidth = display->getWidth() / 2; + int separatorX = x + columnWidth - 1; + for (int y = yStart; y <= yEnd; y += 2) { + display->setPixel(separatorX, y); + } +} + +void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries, int scrollIndex, int columns, int scrollStartY) +{ + if (totalEntries <= visibleNodeRows * columns) + return; + + 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 thumbY = scrollStartY + (scrollIndex * (scrollbarHeight - thumbHeight)) / std::max(1, maxScroll); + + for (int i = 0; i < thumbHeight; i++) { + display->setPixel(scrollbarX, thumbY + i); + } +} + +// ============================= +// Entry Renderers +// ============================= + +void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int timeOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); + + String nodeName = getSafeNodeName(node); + + char timeStr[10]; + uint32_t seconds = sinceLastSeen(node); + if (seconds == 0 || seconds == UINT32_MAX) { + snprintf(timeStr, sizeof(timeStr), "?"); + } else { + uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; + snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"), + (days ? days + : hours ? hours + : minutes), + (days ? 'd' + : hours ? 'h' + : 'm')); + } + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawString(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nodeName); + if (node->is_favorite) { + if (SCREEN_WIDTH > 128) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } + } + + int rightEdge = x + columnWidth - timeOffset; + int textWidth = display->getStringWidth(timeStr); + display->drawString(rightEdge - textWidth, y, timeStr); +} + +void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + bool isLeftCol = (x < SCREEN_WIDTH / 2); + + int nameMaxWidth = columnWidth - 25; + int barsOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 16 : 20) : (isLeftCol ? 15 : 19); + int hopOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 17 : 25) : (isLeftCol ? 13 : 17); + + int barsXOffset = columnWidth - barsOffset; + + String nodeName = getSafeNodeName(node); + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName); + if (node->is_favorite) { + if (SCREEN_WIDTH > 128) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } + } + + // Draw signal strength bars + int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0; + int barWidth = 2; + int barStartX = x + barsXOffset; + int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2; + + for (int b = 0; b < 4; b++) { + if (b < bars) { + int height = (b * 2); + display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height); + } + } + + // Draw hop count + char hopStr[6] = ""; + if (node->has_hops_away && node->hops_away > 0) + snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away); + + if (hopStr[0] != '\0') { + int rightEdge = x + columnWidth - hopOffset; + int textWidth = display->getStringWidth(hopStr); + display->drawString(rightEdge - textWidth, y, hopStr); + } +} + +void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + + String nodeName = getSafeNodeName(node); + char distStr[10] = ""; + + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { + double lat1 = ourNode->position.latitude_i * 1e-7; + double lon1 = ourNode->position.longitude_i * 1e-7; + double lat2 = node->position.latitude_i * 1e-7; + double lon2 = node->position.longitude_i * 1e-7; + + double earthRadiusKm = 6371.0; + double dLat = (lat2 - lat1) * DEG_TO_RAD; + double dLon = (lon2 - lon1) * DEG_TO_RAD; + + double a = + sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); + double c = 2 * atan2(sqrt(a), sqrt(1 - a)); + double distanceKm = earthRadiusKm * c; + + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + double miles = distanceKm * 0.621371; + if (miles < 0.1) { + int feet = (int)(miles * 5280); + if (feet < 1000) + snprintf(distStr, sizeof(distStr), "%dft", feet); + else + snprintf(distStr, sizeof(distStr), "ΒΌmi"); // 4-char max + } else { + int roundedMiles = (int)(miles + 0.5); + if (roundedMiles < 1000) + snprintf(distStr, sizeof(distStr), "%dmi", roundedMiles); + else + snprintf(distStr, sizeof(distStr), "999"); // Max display cap + } + } else { + if (distanceKm < 1.0) { + int meters = (int)(distanceKm * 1000); + if (meters < 1000) + snprintf(distStr, sizeof(distStr), "%dm", meters); + else + snprintf(distStr, sizeof(distStr), "1k"); + } else { + int km = (int)(distanceKm + 0.5); + if (km < 1000) + snprintf(distStr, sizeof(distStr), "%dk", km); + else + snprintf(distStr, sizeof(distStr), "999"); + } + } + } + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName); + if (node->is_favorite) { + if (SCREEN_WIDTH > 128) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } + } + + if (strlen(distStr) > 0) { + int offset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) + : (isLeftCol ? 5 : 8); // 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) +{ + switch (currentMode) { + 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; + } +} + +void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth) +{ + bool isLeftCol = (x < SCREEN_WIDTH / 2); + + // Adjust max text width depending on column and screen width + int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); + + String nodeName = getSafeNodeName(node); + + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 3), y, nameMaxWidth, nodeName); + if (node->is_favorite) { + if (SCREEN_WIDTH > 128) { + drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display); + } else { + display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint); + } + } +} + +void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, + double userLat, double userLon) +{ + if (!nodeDB->hasValidPosition(node)) + return; + + bool isLeftCol = (x < SCREEN_WIDTH / 2); + int arrowXOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18); + + int centerX = x + columnWidth - arrowXOffset; + int centerY = y + FONT_HEIGHT_SMALL / 2; + + double nodeLat = node->position.latitude_i * 1e-7; + double nodeLon = node->position.longitude_i * 1e-7; + float bearingToNode = calculateBearing(userLat, userLon, nodeLat, nodeLon); + float relativeBearing = fmod((bearingToNode - myHeading + 360), 360); + float angle = relativeBearing * DEG_TO_RAD; + + // Shrink size by 2px + int size = FONT_HEIGHT_SMALL - 5; + float halfSize = size / 2.0; + + // Point of the arrow + int tipX = centerX + halfSize * cos(angle); + int tipY = centerY - halfSize * sin(angle); + + float baseAngle = radians(35); + float sideLen = halfSize * 0.95; + float notchInset = halfSize * 0.35; + + // Left and right corners + int leftX = centerX + sideLen * cos(angle + PI - baseAngle); + int leftY = centerY - sideLen * sin(angle + PI - baseAngle); + + int rightX = centerX + sideLen * cos(angle + PI + baseAngle); + int rightY = centerY - sideLen * sin(angle + PI + baseAngle); + + // Center notch (cut-in) + int notchX = centerX - notchInset * cos(angle); + int notchY = centerY + notchInset * sin(angle); + + // Draw the chevron-style arrowhead + display->fillTriangle(tipX, tipY, leftX, leftY, notchX, notchY); + display->fillTriangle(tipX, tipY, notchX, notchY, rightX, rightY); +} + +// ============================= +// Main Screen Functions +// ============================= + +void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, + EntryRenderer renderer, NodeExtrasRenderer extras, float heading, double lat, double lon) +{ + const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1; + const int rowYOffset = FONT_HEIGHT_SMALL - 3; + + int columnWidth = display->getWidth() / 2; + + display->clear(); + + // Draw the battery/time header + graphics::drawCommonHeader(display, x, y); + + // Draw the centered title within the header + const int highlightHeight = COMMON_HEADER_HEIGHT; + const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int centerX = x + SCREEN_WIDTH / 2; + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) + display->setColor(BLACK); + + display->drawString(centerX, textY, title); + if (config.display.heading_bold) + display->drawString(centerX + 1, textY, title); + + display->setColor(WHITE); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // Space below header + y += COMMON_HEADER_HEIGHT; + + // Fetch and display sorted node list + std::vector nodeList; + retrieveAndSortNodes(nodeList); + + int totalEntries = nodeList.size(); + int totalRowsAvailable = (display->getHeight() - y) / rowYOffset; +#ifdef USE_EINK + totalRowsAvailable -= 1; +#endif + int visibleNodeRows = totalRowsAvailable; + int totalColumns = 2; + + 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) { + int xPos = x + (col * columnWidth); + int yPos = y + yOffset; + renderer(display, nodeList[i].node, xPos, yPos, columnWidth); + + if (extras) { + extras(display, nodeList[i].node, xPos, yPos, columnWidth, heading, lat, lon); + } + + lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL); + yOffset += rowYOffset; + shownCount++; + rowCount++; + + if (rowCount >= totalRowsAvailable) { + yOffset = 0; + rowCount = 0; + col++; + if (col > (totalColumns - 1)) + break; + } + } + + // Draw column separator + if (shownCount > 0) { + const int firstNodeY = y + 3; + drawColumnSeparator(display, x, firstNodeY, lastNodeY); + } + + const int scrollStartY = y + 3; + drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY); +} + +// ============================= +// Screen Frame Functions +// ============================= + +#ifndef USE_EINK +void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Static variables to track mode and duration + static NodeListMode lastRenderedMode = MODE_COUNT; + static unsigned long modeStartTime = 0; + + unsigned long now = millis(); + + // On very first call (on boot or state enter) + if (lastRenderedMode == MODE_COUNT) { + currentMode = MODE_LAST_HEARD; + modeStartTime = now; + } + + // Time to switch to next mode? + if (now - modeStartTime >= getModeCycleIntervalMs()) { + currentMode = static_cast((currentMode + 1) % MODE_COUNT); + modeStartTime = now; + } + + // Render screen based on currentMode + const char *title = getCurrentModeTitle(display->getWidth()); + drawNodeListScreen(display, state, x, y, title, drawEntryDynamic); + + // Track the last mode to avoid reinitializing modeStartTime + lastRenderedMode = currentMode; +} +#endif + +#ifdef USE_EINK +void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const char *title = "Node List"; + drawNodeListScreen(display, state, x, y, title, drawEntryLastHeard); +} + +void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const char *title = "Hops/Signal"; + 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; + bool validHeading = false; + double lat = 0; + double lon = 0; + +#if HAS_GPS + if (screen->hasHeading()) { + heading = screen->getHeading(); // degrees + validHeading = true; + } else { + heading = screen->estimatedHeading(lat, lon); + validHeading = !isnan(heading); + } +#endif + + if (!validHeading) + return; + + drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon); +} + +void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Cache favorite nodes for the current frame only, to save computation + static std::vector favoritedNodes; + static int prevFrame = -1; + + // Only rebuild favorites list if we're on a new frame + if (state->currentFrame != prevFrame) { + prevFrame = state->currentFrame; + favoritedNodes.clear(); + size_t total = nodeDB->getNumMeshNodes(); + for (size_t i = 0; i < total; i++) { + meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + // Skip nulls and ourself + if (!n || n->num == nodeDB->getNodeNum()) + continue; + if (n->is_favorite) + favoritedNodes.push_back(n); + } + // Keep a stable, consistent display order + std::sort(favoritedNodes.begin(), favoritedNodes.end(), + [](meshtastic_NodeInfoLite *a, meshtastic_NodeInfoLite *b) { return a->num < b->num; }); + } + if (favoritedNodes.empty()) + return; + + // Only display if index is valid + int nodeIndex = state->currentFrame - (screen->frameCount - favoritedNodes.size()); + if (nodeIndex < 0 || nodeIndex >= (int)favoritedNodes.size()) + return; + + meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; + if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) + return; + + display->clear(); + + // Draw battery/time/mail header (common across screens) + graphics::drawCommonHeader(display, x, y); + + // Draw the short node name centered at the top, with bold shadow if set + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int centerX = x + SCREEN_WIDTH / 2; + const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; + + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) + display->setColor(BLACK); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_SMALL); + display->drawString(centerX, textY, shortName); + if (config.display.heading_bold) + display->drawString(centerX + 1, textY, shortName); + + display->setColor(WHITE); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // Dynamic row stacking with predefined Y positions + const int yPositions[5] = {moreCompactFirstLine, moreCompactSecondLine, moreCompactThirdLine, moreCompactFourthLine, + moreCompactFifthLine}; + int line = 0; + + // 1. Long Name (always try to show first) + const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; + if (username && line < 5) { + display->drawString(x, yPositions[line++], username); + } + + // 2. Signal and Hops (combined on one line, if available) + char signalHopsStr[32] = ""; + bool haveSignal = false; + int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100); + const char *signalLabel = " Sig"; + + // If SNR looks reasonable, show signal + if ((int)((node->snr + 10) * 5) >= 0 && node->snr > -100) { + snprintf(signalHopsStr, sizeof(signalHopsStr), "%s: %d%%", signalLabel, percentSignal); + haveSignal = true; + } + // If hops is valid (>0), show right after signal + if (node->hops_away > 0) { + size_t len = strlen(signalHopsStr); + if (haveSignal) { + snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away, + (node->hops_away == 1 ? "Hop" : "Hops")); + } else { + snprintf(signalHopsStr, sizeof(signalHopsStr), "[%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops")); + } + } + if (signalHopsStr[0] && line < 5) { + display->drawString(x, yPositions[line++], signalHopsStr); + } + + // 3. Heard (last seen, skip if node never seen) + char seenStr[20] = ""; + uint32_t seconds = sinceLastSeen(node); + if (seconds != 0 && seconds != UINT32_MAX) { + uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; + snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard: ?" : " Heard: %d%c ago"), + (days ? days + : hours ? hours + : minutes), + (days ? 'd' + : hours ? 'h' + : 'm')); + } + if (seenStr[0] && line < 5) { + display->drawString(x, yPositions[line++], seenStr); + } + + // 4. Uptime (only show if metric is present) + char uptimeStr[32] = ""; + if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { + uint32_t uptime = node->device_metrics.uptime_seconds; + uint32_t days = uptime / 86400; + uint32_t hours = (uptime % 86400) / 3600; + uint32_t mins = (uptime % 3600) / 60; + + if (days > 0) { + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %dd %dh", days, hours); + } else if (hours > 0) { + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %dh %dm", hours, mins); + } else { + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %dm", mins); + } + } + if (uptimeStr[0] && line < 5) { + display->drawString(x, yPositions[line++], uptimeStr); + } + + // 5. Distance (only if both nodes have GPS position) + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + char distStr[24] = ""; + bool haveDistance = false; + + if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { + double lat1 = ourNode->position.latitude_i * 1e-7; + double lon1 = ourNode->position.longitude_i * 1e-7; + double lat2 = node->position.latitude_i * 1e-7; + double lon2 = node->position.longitude_i * 1e-7; + + double earthRadiusKm = 6371.0; + double dLat = (lat2 - lat1) * DEG_TO_RAD; + double dLon = (lon2 - lon1) * DEG_TO_RAD; + + double a = + sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); + double c = 2 * atan2(sqrt(a), sqrt(1 - a)); + double distanceKm = earthRadiusKm * c; + + // Format distance appropriately + if (distanceKm < 1.0) { + double miles = distanceKm * 0.621371; + if (miles < 0.1) { + int feet = (int)(miles * 5280); + if (feet > 0 && feet < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dft", feet); + haveDistance = true; + } else if (feet >= 1000) { + snprintf(distStr, sizeof(distStr), " Distance: ΒΌmi"); + haveDistance = true; + } + } else { + int roundedMiles = (int)(miles + 0.5); + if (roundedMiles > 0 && roundedMiles < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dmi", roundedMiles); + haveDistance = true; + } + } + } else { + int km = (int)(distanceKm + 0.5); + if (km > 0 && km < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dkm", km); + haveDistance = true; + } + } + } + // Only display if we actually have a value! + if (haveDistance && distStr[0] && line < 5) { + display->drawString(x, yPositions[line++], distStr); + } + + // Compass rendering for different screen orientations + if (SCREEN_WIDTH > SCREEN_HEIGHT) { + // Landscape: side-aligned compass + bool showCompass = false; + if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { + showCompass = true; + } + if (showCompass) { + const int16_t topY = compactFirstLine; + const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); + const int16_t usableHeight = bottomY - topY - 5; + int16_t compassRadius = usableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + const int16_t compassDiam = compassRadius * 2; + const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; + const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + + const auto &op = ourNode->position; + float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 + : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading); + + const auto &p = node->position; + float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); + if (!config.display.compass_north_top) + bearing -= myHeading; + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing); + + display->drawCircle(compassX, compassY, compassRadius); + } + } else { + // Portrait: bottom-centered compass + bool showCompass = false; + if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { + showCompass = true; + } + if (showCompass) { + int yBelowContent = (line > 0 && line <= 5) ? (yPositions[line - 1] + FONT_HEIGHT_SMALL + 2) : moreCompactFirstLine; + + const int margin = 4; +#if defined(USE_EINK) + const int iconSize = (SCREEN_WIDTH > 128) ? 16 : 8; + const int navBarHeight = iconSize + 6; +#else + const int navBarHeight = 0; +#endif + int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; + + if (availableHeight < FONT_HEIGHT_SMALL * 2) + return; + + int compassRadius = availableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + if (compassRadius * 2 > SCREEN_WIDTH - 16) + compassRadius = (SCREEN_WIDTH - 16) / 2; + + int compassX = x + SCREEN_WIDTH / 2; + int compassY = yBelowContent + availableHeight / 2; + + const auto &op = ourNode->position; + float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 + : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading); + + const auto &p = node->position; + float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); + if (!config.display.compass_north_top) + bearing -= myHeading; + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); + + display->drawCircle(compassX, compassY, compassRadius); + } + } +} + +/// Draw a series of fields in a column, wrapping to multiple columns if needed +void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields) +{ + // The coordinates define the left starting point of the text + display->setTextAlignment(TEXT_ALIGN_LEFT); + + const char **f = fields; + int xo = x, yo = y; + while (*f) { + display->drawString(xo, yo, *f); + if ((display->getColor() == BLACK) && config.display.heading_bold) + display->drawString(xo + 1, yo, *f); + + display->setColor(WHITE); + yo += FONT_HEIGHT_SMALL; + if (yo > SCREEN_HEIGHT - FONT_HEIGHT_SMALL) { + xo += SCREEN_WIDTH / 2; + yo = 0; + } + f++; + } +} + +} // namespace NodeListRenderer +} // namespace graphics diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h new file mode 100644 index 000000000..d35350cb8 --- /dev/null +++ b/src/graphics/draw/NodeListRenderer.h @@ -0,0 +1,71 @@ +#pragma once + +#include "graphics/Screen.h" +#include "mesh/generated/meshtastic/mesh.pb.h" +#include +#include + +namespace graphics +{ + +/// Forward declarations +class Screen; + +/** + * @brief Node list and entry rendering functions + * + * Contains all functions related to drawing node lists and individual node entries + * including last heard, hop signal, distance, and compass views. + */ +namespace NodeListRenderer +{ +// Entry renderer function types +typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); +typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double); + +// Node entry structure +struct NodeEntry { + meshtastic_NodeInfoLite *node; + uint32_t sortValue; +}; + +// Node list mode enumeration +enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 }; + +// Main node list screen function +void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, + EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0, + double lon = 0); + +// Entry renderers +void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); +void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); +void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); +void drawEntryDynamic(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); +void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth); + +// Extras renderers +void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, + double userLat, double userLon); + +// Screen frame functions +void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// Utility functions +const char *getCurrentModeTitle(int screenWidth); +void retrieveAndSortNodes(std::vector &nodeList); +String getSafeNodeName(meshtastic_NodeInfoLite *node); +uint32_t sinceLastSeen(meshtastic_NodeInfoLite *node); +void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); + +// Bitmap drawing function +void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display); + +} // namespace NodeListRenderer + +} // namespace graphics diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp new file mode 100644 index 000000000..6d8423adc --- /dev/null +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -0,0 +1,177 @@ +#include "NotificationRenderer.h" +#include "DisplayFormatters.h" +#include "NodeDB.h" +#include "configuration.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" +#include "main.h" +#include +#include +#include + +#ifdef ARCH_ESP32 +#include "esp_task_wdt.h" +#endif + +using namespace meshtastic; + +// External references to global variables from Screen.cpp +extern String alertBannerMessage; +extern uint32_t alertBannerUntil; +extern std::vector functionSymbol; +extern std::string functionSymbolString; +extern bool hasUnreadMessage; + +namespace graphics +{ + +namespace NotificationRenderer +{ + +// Used on boot when a certificate is being created +void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_SMALL); + display->drawString(64 + x, y, "Creating SSL certificate"); + +#ifdef ARCH_ESP32 + yield(); + esp_task_wdt_reset(); +#endif + + display->setFont(FONT_SMALL); + if ((millis() / 1000) % 2) { + display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . ."); + } else { + display->drawString(64 + x, FONT_HEIGHT_SMALL + y + 2, "Please wait . . "); + } +} + +// Used when booting without a region set +void NotificationRenderer::drawWelcomeScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(64 + x, y, "//\\ E S H T /\\ S T / C"); + display->drawString(64 + x, y + FONT_HEIGHT_SMALL, getDeviceName()); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + if ((millis() / 10000) % 2) { + display->drawString(x, y + FONT_HEIGHT_SMALL * 2 - 3, "Set the region using the"); + display->drawString(x, y + FONT_HEIGHT_SMALL * 3 - 3, "Meshtastic Android, iOS,"); + display->drawString(x, y + FONT_HEIGHT_SMALL * 4 - 3, "Web or CLI clients."); + } else { + display->drawString(x, y + FONT_HEIGHT_SMALL * 2 - 3, "Visit meshtastic.org"); + display->drawString(x, y + FONT_HEIGHT_SMALL * 3 - 3, "for more information."); + display->drawString(x, y + FONT_HEIGHT_SMALL * 4 - 3, ""); + } + +#ifdef ARCH_ESP32 + yield(); + esp_task_wdt_reset(); +#endif +} + +void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + // Exit if no message is active or duration has passed + if (alertBannerMessage.length() == 0 || (alertBannerUntil != 0 && millis() > alertBannerUntil)) + return; + + // === Layout Configuration === + constexpr uint16_t padding = 5; // Padding around text inside the box + constexpr uint8_t lineSpacing = 1; // Extra space between lines + + // Search the message to determine if we need the bell added + bool needs_bell = (alertBannerMessage.indexOf("Alert Received") != -1); + + // Setup font and alignment + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); // We will manually center per line + + // === Split the message into lines (supports multi-line banners) === + std::vector lines; + int start = 0, newlineIdx; + while ((newlineIdx = alertBannerMessage.indexOf('\n', start)) != -1) { + lines.push_back(alertBannerMessage.substring(start, newlineIdx)); + start = newlineIdx + 1; + } + lines.push_back(alertBannerMessage.substring(start)); + + // === Measure text dimensions === + uint16_t minWidth = (SCREEN_WIDTH > 128) ? 106 : 78; + uint16_t maxWidth = 0; + std::vector lineWidths; + for (const auto &line : lines) { + uint16_t w = display->getStringWidth(line.c_str(), line.length(), true); + lineWidths.push_back(w); + if (w > maxWidth) + maxWidth = w; + } + + uint16_t boxWidth = padding * 2 + maxWidth; + if (needs_bell && boxWidth < minWidth) + boxWidth += (SCREEN_WIDTH > 128) ? 26 : 20; + + uint16_t boxHeight = padding * 2 + lines.size() * FONT_HEIGHT_SMALL + (lines.size() - 1) * lineSpacing; + + int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); + int16_t boxTop = (display->height() / 2) - (boxHeight / 2); + + // === Draw background box === + display->setColor(BLACK); + display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Slightly oversized box + display->setColor(WHITE); + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border + + // === Draw each line centered in the box === + int16_t lineY = boxTop + padding; + for (size_t i = 0; i < lines.size(); ++i) { + int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; + uint16_t line_width = display->getStringWidth(lines[i].c_str(), lines[i].length(), true); + + if (needs_bell && i == 0) { + int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2; + display->drawXbm(textX - 10, bellY, 8, 8, bell_alert); + display->drawXbm(textX + line_width + 2, bellY, 8, 8, bell_alert); + } + + display->drawString(textX, lineY, lines[i]); + if (SCREEN_WIDTH > 128) + display->drawString(textX + 1, lineY, lines[i]); // Faux bold + + lineY += FONT_HEIGHT_SMALL + lineSpacing; + } +} + +/// Draw the last text message we received +void NotificationRenderer::drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_MEDIUM); + + char tempBuf[24]; + snprintf(tempBuf, sizeof(tempBuf), "Critical fault #%d", error_code); + display->drawString(0 + x, 0 + y, tempBuf); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawString(0 + x, FONT_HEIGHT_MEDIUM + y, "For help, please visit \nmeshtastic.org"); +} + +void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(64 + x, y, "Updating"); + + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->drawStringMaxWidth(0 + x, 2 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), + "Please be patient and do not power off."); +} + +} // namespace NotificationRenderer + +} // namespace graphics diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h new file mode 100644 index 000000000..6f07d75c4 --- /dev/null +++ b/src/graphics/draw/NotificationRenderer.h @@ -0,0 +1,24 @@ +#pragma once + +#include "OLEDDisplay.h" +#include "OLEDDisplayUi.h" + +namespace graphics +{ + +namespace NotificationRenderer +{ + +class NotificationRenderer +{ + public: + static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); + static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawWelcomeScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +}; + +} // namespace NotificationRenderer + +} // namespace graphics diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp new file mode 100644 index 000000000..3564db51a --- /dev/null +++ b/src/graphics/draw/UIRenderer.cpp @@ -0,0 +1,1254 @@ +#include "UIRenderer.h" +#include "CompassRenderer.h" +#include "GPSStatus.h" +#include "NodeDB.h" +#include "NodeListRenderer.h" +#include "configuration.h" +#include "gps/GeoCoord.h" +#include "graphics/Screen.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" +#include "main.h" +#include "target_specific.h" +#include +#include + +#if !MESHTASTIC_EXCLUDE_GPS + +// External variables +extern graphics::Screen *screen; +extern "C" { +extern char ourId[5]; +} + +namespace graphics +{ + +// GeoCoord object for coordinate conversions +extern GeoCoord geoCoord; + +// Threshold values for the GPS lock accuracy bar display +extern uint32_t dopThresholds[5]; + +namespace UIRenderer +{ + +// Draw GPS status summary +void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) +{ + if (config.position.fixed_position) { + // GPS coordinates are currently fixed + display->drawString(x - 1, y - 2, "Fixed GPS"); + if (config.display.heading_bold) + display->drawString(x, y - 2, "Fixed GPS"); + return; + } + if (!gps->getIsConnected()) { + display->drawString(x, y - 2, "No GPS"); + if (config.display.heading_bold) + display->drawString(x + 1, y - 2, "No GPS"); + return; + } + // Adjust position if we're going to draw too wide + int maxDrawWidth = 6; // Position icon + + if (!gps->getHasLock()) { + maxDrawWidth += display->getStringWidth("No sats") + 2; // icon + text + buffer + } else { + maxDrawWidth += (5 * 2) + 8 + display->getStringWidth("99") + 2; // bars + sat icon + text + buffer + } + + if (x + maxDrawWidth > display->getWidth()) { + x = display->getWidth() - maxDrawWidth; + if (x < 0) + x = 0; // Clamp to screen + } + + display->drawFastImage(x, y, 6, 8, gps->getHasLock() ? imgPositionSolid : imgPositionEmpty); + if (!gps->getHasLock()) { + // Draw "No sats" to the right of the icon with slightly more gap + int textX = x + 9; // 6 (icon) + 3px spacing + display->drawString(textX, y - 3, "No sats"); + if (config.display.heading_bold) + display->drawString(textX + 1, y - 3, "No sats"); + return; + } else { + char satsString[3]; + uint8_t bar[2] = {0}; + + // Draw DOP signal bars + for (int i = 0; i < 5; i++) { + if (gps->getDOP() <= dopThresholds[i]) + bar[0] = ~((1 << (5 - i)) - 1); + else + bar[0] = 0b10000000; + + display->drawFastImage(x + 9 + (i * 2), y, 2, 8, bar); + } + + // Draw satellite image + display->drawFastImage(x + 24, y, 8, 8, imgSatellite); + + // Draw the number of satellites + snprintf(satsString, sizeof(satsString), "%u", gps->getNumSatellites()); + int textX = x + 34; + display->drawString(textX, y - 2, satsString); + if (config.display.heading_bold) + display->drawString(textX + 1, y - 2, satsString); + } +} + +// Draw status when GPS is disabled or not present +void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) +{ + String displayLine; + int pos; + if (y < FONT_HEIGHT_SMALL) { // Line 1: use short string + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + pos = display->getWidth() - display->getStringWidth(displayLine); + } else { + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "GPS not present" + : "GPS is disabled"; + pos = (display->getWidth() - display->getStringWidth(displayLine)) / 2; + } + display->drawString(x + pos, y, displayLine); +} + +void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) +{ + String displayLine = ""; + if (!gps->getIsConnected() && !config.position.fixed_position) { + // displayLine = "No GPS Module"; + // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } else if (!gps->getHasLock() && !config.position.fixed_position) { + // displayLine = "No GPS Lock"; + // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } else { + geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); + displayLine = "Altitude: " + String(geoCoord.getAltitude()) + "m"; + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) + displayLine = "Altitude: " + String(geoCoord.getAltitude() * METERS_TO_FEET) + "ft"; + display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } +} + +// Draw GPS status coordinates +void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) +{ + auto gpsFormat = config.display.gps_format; + String displayLine = ""; + + if (!gps->getIsConnected() && !config.position.fixed_position) { + displayLine = "No GPS present"; + display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } else if (!gps->getHasLock() && !config.position.fixed_position) { + displayLine = "No GPS Lock"; + display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); + } else { + + geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); + + if (gpsFormat != meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DMS) { + char coordinateLine[22]; + if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_DEC) { // Decimal Degrees + snprintf(coordinateLine, sizeof(coordinateLine), "%f %f", geoCoord.getLatitude() * 1e-7, + geoCoord.getLongitude() * 1e-7); + } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_UTM) { // Universal Transverse Mercator + snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %06u %07u", geoCoord.getUTMZone(), geoCoord.getUTMBand(), + geoCoord.getUTMEasting(), geoCoord.getUTMNorthing()); + } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_MGRS) { // Military Grid Reference System + snprintf(coordinateLine, sizeof(coordinateLine), "%2i%1c %1c%1c %05u %05u", geoCoord.getMGRSZone(), + geoCoord.getMGRSBand(), geoCoord.getMGRSEast100k(), geoCoord.getMGRSNorth100k(), + geoCoord.getMGRSEasting(), geoCoord.getMGRSNorthing()); + } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OLC) { // Open Location Code + geoCoord.getOLCCode(coordinateLine); + } else if (gpsFormat == meshtastic_Config_DisplayConfig_GpsCoordinateFormat_OSGR) { // Ordnance Survey Grid Reference + if (geoCoord.getOSGRE100k() == 'I' || geoCoord.getOSGRN100k() == 'I') // OSGR is only valid around the UK region + snprintf(coordinateLine, sizeof(coordinateLine), "%s", "Out of Boundary"); + else + snprintf(coordinateLine, sizeof(coordinateLine), "%1c%1c %05u %05u", geoCoord.getOSGRE100k(), + geoCoord.getOSGRN100k(), geoCoord.getOSGREasting(), geoCoord.getOSGRNorthing()); + } + + // If fixed position, display text "Fixed GPS" alternating with the coordinates. + if (config.position.fixed_position) { + if ((millis() / 10000) % 2) { + display->drawString(x + (display->getWidth() - (display->getStringWidth(coordinateLine))) / 2, y, + coordinateLine); + } else { + display->drawString(x + (display->getWidth() - (display->getStringWidth("Fixed GPS"))) / 2, y, "Fixed GPS"); + } + } else { + display->drawString(x + (display->getWidth() - (display->getStringWidth(coordinateLine))) / 2, y, coordinateLine); + } + } else { + char latLine[22]; + char lonLine[22]; + snprintf(latLine, sizeof(latLine), "%2iΒ° %2i' %2u\" %1c", geoCoord.getDMSLatDeg(), geoCoord.getDMSLatMin(), + geoCoord.getDMSLatSec(), geoCoord.getDMSLatCP()); + snprintf(lonLine, sizeof(lonLine), "%3iΒ° %2i' %2u\" %1c", geoCoord.getDMSLonDeg(), geoCoord.getDMSLonMin(), + geoCoord.getDMSLonSec(), geoCoord.getDMSLonCP()); + display->drawString(x + (display->getWidth() - (display->getStringWidth(latLine))) / 2, y - FONT_HEIGHT_SMALL * 1, + latLine); + display->drawString(x + (display->getWidth() - (display->getStringWidth(lonLine))) / 2, y, lonLine); + } + } +} + +// Draw nodes status +void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset, + bool show_total, String additional_words) +{ + char usersString[20]; + int nodes_online = (nodeStatus->getNumOnline() > 0) ? nodeStatus->getNumOnline() + node_offset : 0; + + snprintf(usersString, sizeof(usersString), "%d", nodes_online); + + if (show_total) { + int nodes_total = (nodeStatus->getNumTotal() > 0) ? nodeStatus->getNumTotal() + node_offset : 0; + snprintf(usersString, sizeof(usersString), "%d/%d", nodes_online, nodes_total); + } + +#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ + defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \ + !defined(DISPLAY_FORCE_SMALL_FONTS) + display->drawFastImage(x, y + 3, 8, 8, imgUser); +#else + display->drawFastImage(x, y + 1, 8, 8, imgUser); +#endif + display->drawString(x + 10, y - 2, usersString); + int string_offset = (SCREEN_WIDTH > 128) ? 2 : 1; + if (additional_words != "") { + display->drawString(x + 10 + display->getStringWidth(usersString) + string_offset, y - 2, additional_words); + if (config.display.heading_bold) + display->drawString(x + 11 + display->getStringWidth(usersString) + string_offset, y - 2, additional_words); + } +} + +// ********************** +// * Favorite Node Info * +// ********************** +void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // --- Cache favorite nodes for the current frame only, to save computation --- + static std::vector favoritedNodes; + static int prevFrame = -1; + + // --- Only rebuild favorites list if we're on a new frame --- + if (state->currentFrame != prevFrame) { + prevFrame = state->currentFrame; + favoritedNodes.clear(); + size_t total = nodeDB->getNumMeshNodes(); + for (size_t i = 0; i < total; i++) { + meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + // Skip nulls and ourself + if (!n || n->num == nodeDB->getNodeNum()) + continue; + if (n->is_favorite) + favoritedNodes.push_back(n); + } + // Keep a stable, consistent display order + std::sort(favoritedNodes.begin(), favoritedNodes.end(), + [](meshtastic_NodeInfoLite *a, meshtastic_NodeInfoLite *b) { return a->num < b->num; }); + } + if (favoritedNodes.empty()) + return; + + // --- Only display if index is valid --- + int nodeIndex = state->currentFrame - (screen->frameCount - favoritedNodes.size()); + if (nodeIndex < 0 || nodeIndex >= (int)favoritedNodes.size()) + return; + + meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex]; + if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) + return; + + display->clear(); + + // === Draw battery/time/mail header (common across screens) === + graphics::drawCommonHeader(display, x, y); + + // === Draw the short node name centered at the top, with bold shadow if set === + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const int centerX = x + SCREEN_WIDTH / 2; + const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) + display->setColor(BLACK); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_SMALL); + display->drawString(centerX, textY, shortName); + if (config.display.heading_bold) + display->drawString(centerX + 1, textY, shortName); + + display->setColor(WHITE); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // ===== DYNAMIC ROW STACKING WITH YOUR MACROS ===== + // 1. Each potential info row has a macro-defined Y position (not regular increments!). + // 2. Each row is only shown if it has valid data. + // 3. Each row "moves up" if previous are empty, so there are never any blank rows. + // 4. The first line is ALWAYS at your macro position; subsequent lines use the next available macro slot. + + // List of available macro Y positions in order, from top to bottom. + const int yPositions[5] = {moreCompactFirstLine, moreCompactSecondLine, moreCompactThirdLine, moreCompactFourthLine, + moreCompactFifthLine}; + int line = 0; // which slot to use next + + // === 1. Long Name (always try to show first) === + const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; + if (username && line < 5) { + // Print node's long name (e.g. "Backpack Node") + display->drawString(x, yPositions[line++], username); + } + + // === 2. Signal and Hops (combined on one line, if available) === + // If both are present: "Sig: 97% [2hops]" + // If only one: show only that one + char signalHopsStr[32] = ""; + bool haveSignal = false; + int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100); + + // Always use "Sig" for the label + const char *signalLabel = " Sig"; + + // --- Build the Signal/Hops line --- + // If SNR looks reasonable, show signal + if ((int)((node->snr + 10) * 5) >= 0 && node->snr > -100) { + snprintf(signalHopsStr, sizeof(signalHopsStr), "%s: %d%%", signalLabel, percentSignal); + haveSignal = true; + } + // If hops is valid (>0), show right after signal + if (node->hops_away > 0) { + size_t len = strlen(signalHopsStr); + // Decide between "1 Hop" and "N Hops" + if (haveSignal) { + snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away, + (node->hops_away == 1 ? "Hop" : "Hops")); + } else { + snprintf(signalHopsStr, sizeof(signalHopsStr), "[%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops")); + } + } + if (signalHopsStr[0] && line < 5) { + display->drawString(x, yPositions[line++], signalHopsStr); + } + + // === 3. Heard (last seen, skip if node never seen) === + char seenStr[20] = ""; + uint32_t seconds = sinceLastSeen(node); + if (seconds != 0 && seconds != UINT32_MAX) { + uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; + // Format as "Heard: Xm ago", "Heard: Xh ago", or "Heard: Xd ago" + snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard: ?" : " Heard: %d%c ago"), + (days ? days + : hours ? hours + : minutes), + (days ? 'd' + : hours ? 'h' + : 'm')); + } + if (seenStr[0] && line < 5) { + display->drawString(x, yPositions[line++], seenStr); + } + + // === 4. Uptime (only show if metric is present) === + char uptimeStr[32] = ""; + if (node->has_device_metrics && node->device_metrics.has_uptime_seconds) { + uint32_t uptime = node->device_metrics.uptime_seconds; + uint32_t days = uptime / 86400; + uint32_t hours = (uptime % 86400) / 3600; + uint32_t mins = (uptime % 3600) / 60; + // Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m" + if (days) + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %ud %uh", days, hours); + else if (hours) + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %uh %um", hours, mins); + else + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %um", mins); + } + if (uptimeStr[0] && line < 5) { + display->drawString(x, yPositions[line++], uptimeStr); + } + + // === 5. Distance (only if both nodes have GPS position) === + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + char distStr[24] = ""; // Make buffer big enough for any string + bool haveDistance = false; + + if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { + double lat1 = ourNode->position.latitude_i * 1e-7; + double lon1 = ourNode->position.longitude_i * 1e-7; + double lat2 = node->position.latitude_i * 1e-7; + double lon2 = node->position.longitude_i * 1e-7; + double earthRadiusKm = 6371.0; + double dLat = (lat2 - lat1) * DEG_TO_RAD; + double dLon = (lon2 - lon1) * DEG_TO_RAD; + double a = + sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2); + double c = 2 * atan2(sqrt(a), sqrt(1 - a)); + double distanceKm = earthRadiusKm * c; + + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + double miles = distanceKm * 0.621371; + if (miles < 0.1) { + int feet = (int)(miles * 5280); + if (feet > 0 && feet < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dft", feet); + haveDistance = true; + } else if (feet >= 1000) { + snprintf(distStr, sizeof(distStr), " Distance: ΒΌmi"); + haveDistance = true; + } + } else { + int roundedMiles = (int)(miles + 0.5); + if (roundedMiles > 0 && roundedMiles < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dmi", roundedMiles); + haveDistance = true; + } + } + } else { + if (distanceKm < 1.0) { + int meters = (int)(distanceKm * 1000); + if (meters > 0 && meters < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dm", meters); + haveDistance = true; + } else if (meters >= 1000) { + snprintf(distStr, sizeof(distStr), " Distance: 1km"); + haveDistance = true; + } + } else { + int km = (int)(distanceKm + 0.5); + if (km > 0 && km < 1000) { + snprintf(distStr, sizeof(distStr), " Distance: %dkm", km); + haveDistance = true; + } + } + } + } + // Only display if we actually have a value! + if (haveDistance && distStr[0] && line < 5) { + display->drawString(x, yPositions[line++], distStr); + } + + // --- Compass Rendering: landscape (wide) screens use the original side-aligned logic --- + if (SCREEN_WIDTH > SCREEN_HEIGHT) { + bool showCompass = false; + if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { + showCompass = true; + } + if (showCompass) { + const int16_t topY = compactFirstLine; + const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); + const int16_t usableHeight = bottomY - topY - 5; + int16_t compassRadius = usableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + const int16_t compassDiam = compassRadius * 2; + const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; + const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + + const auto &op = ourNode->position; + float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 + : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading); + + const auto &p = node->position; + float d = + GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); + if (!config.display.compass_north_top) + bearing -= myHeading; + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing); + + display->drawCircle(compassX, compassY, compassRadius); + } + // else show nothing + } else { + // Portrait or square: put compass at the bottom and centered, scaled to fit available space + bool showCompass = false; + if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) { + showCompass = true; + } + if (showCompass) { + int yBelowContent = (line > 0 && line <= 5) ? (yPositions[line - 1] + FONT_HEIGHT_SMALL + 2) : moreCompactFirstLine; + const int margin = 4; +// --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) ----------- +#if defined(USE_EINK) + const int iconSize = (SCREEN_WIDTH > 128) ? 16 : 8; + const int navBarHeight = iconSize + 6; +#else + const int navBarHeight = 0; +#endif + int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin; + // --------- END PATCH FOR EINK NAV BAR ----------- + + if (availableHeight < FONT_HEIGHT_SMALL * 2) + return; + + int compassRadius = availableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + if (compassRadius * 2 > SCREEN_WIDTH - 16) + compassRadius = (SCREEN_WIDTH - 16) / 2; + + int compassX = x + SCREEN_WIDTH / 2; + int compassY = yBelowContent + availableHeight / 2; + + const auto &op = ourNode->position; + float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 + : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading); + + const auto &p = node->position; + float d = + GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); + if (!config.display.compass_north_top) + bearing -= myHeading; + graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing); + + display->drawCircle(compassX, compassY, compassRadius); + } + // else show nothing + } +} + +// **************************** +// * Device Focused Screen * +// **************************** +void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // === Header === + graphics::drawCommonHeader(display, x, y); + + // === Content below header === + + // Determine if we need to show 4 or 5 rows on the screen + int rows = 4; + if (!config.bluetooth.enabled) { + rows = 5; + } + + // === First Row: Region / Channel Utilization and Uptime === + bool origBold = config.display.heading_bold; + config.display.heading_bold = false; + + // Display Region and Channel Utilization + drawNodes(display, x + 1, + ((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)) + 2, nodeStatus, + -1, false, "online"); + + uint32_t uptime = millis() / 1000; + char uptimeStr[6]; + uint32_t minutes = uptime / 60, hours = minutes / 60, days = hours / 24; + + if (days > 365) { + snprintf(uptimeStr, sizeof(uptimeStr), "?"); + } else { + snprintf(uptimeStr, sizeof(uptimeStr), "%d%c", + days ? days + : hours ? hours + : minutes ? minutes + : (int)uptime, + days ? 'd' + : hours ? 'h' + : minutes ? 'm' + : 's'); + } + + char uptimeFullStr[16]; + snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); + display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeFullStr), + ((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)), + uptimeFullStr); + + config.display.heading_bold = origBold; + + // === Second Row: Satellites and Voltage === + config.display.heading_bold = false; + +#if HAS_GPS + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + String displayLine = ""; + if (config.position.fixed_position) { + displayLine = "Fixed GPS"; + } else { + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + } + display->drawString( + 0, ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), + displayLine); + } else { + UIRenderer::drawGps( + display, 0, + ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)) + 3, + gpsStatus); + } +#endif + + char batStr[20]; + if (powerStatus->getHasBattery()) { + int batV = powerStatus->getBatteryVoltageMv() / 1000; + int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; + snprintf(batStr, sizeof(batStr), "%01d.%02dV", batV, batCv); + display->drawString( + x + SCREEN_WIDTH - display->getStringWidth(batStr), + ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), batStr); + } else { + display->drawString( + x + SCREEN_WIDTH - display->getStringWidth("USB"), + ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), + String("USB")); + } + + config.display.heading_bold = origBold; + + // === Third Row: Bluetooth Off (Only If Actually Off) === + if (!config.bluetooth.enabled) { + display->drawString( + 0, ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactThirdLine : moreCompactThirdLine)), "BT off"); + } + + // === Third & Fourth Rows: Node Identity === + int textWidth = 0; + int nameX = 0; + int yOffset = (SCREEN_WIDTH > 128) ? 0 : 7; + const char *longName = nullptr; + meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); + if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { + longName = ourNode->user.long_name; + } + uint8_t dmac[6]; + char shortnameble[35]; + getMacAddr(dmac); + snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); + snprintf(shortnameble, sizeof(shortnameble), "%s", + graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); + + char combinedName[50]; + snprintf(combinedName, sizeof(combinedName), "%s (%s)", longName, shortnameble); + if (SCREEN_WIDTH - (display->getStringWidth(longName) + display->getStringWidth(shortnameble)) > 10) { + size_t len = strlen(combinedName); + if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) { + combinedName[len - 3] = '\0'; // Remove the last three characters + } + textWidth = display->getStringWidth(combinedName); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString( + nameX, + ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine)) + yOffset, + combinedName); + } else { + textWidth = display->getStringWidth(longName); + nameX = (SCREEN_WIDTH - textWidth) / 2; + yOffset = (strcmp(shortnameble, "") == 0) ? 1 : 0; + if (yOffset == 1) { + yOffset = (SCREEN_WIDTH > 128) ? 0 : 7; + } + display->drawString( + nameX, + ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine)) + yOffset, + longName); + + // === Fourth Row: ShortName Centered === + textWidth = display->getStringWidth(shortnameble); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, + ((rows == 4) ? compactFourthLine : ((SCREEN_HEIGHT > 64) ? compactFifthLine : moreCompactFifthLine)), + shortnameble); + } +} + +// Start Functions to write date/time to the screen +// Helper function to check if a year is a leap year +bool isLeapYear(int year) +{ + return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); +} + +// Array of days in each month (non-leap year) +const int daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + +// Fills the buffer with a formatted date/time string and returns pixel width +int formatDateTime(char *buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay *display, bool includeTime) +{ + int sec = rtc_sec % 60; + rtc_sec /= 60; + int min = rtc_sec % 60; + rtc_sec /= 60; + int hour = rtc_sec % 24; + rtc_sec /= 24; + + int year = 1970; + while (true) { + int daysInYear = isLeapYear(year) ? 366 : 365; + if (rtc_sec >= (uint32_t)daysInYear) { + rtc_sec -= daysInYear; + year++; + } else { + break; + } + } + + int month = 0; + while (month < 12) { + int dim = daysInMonth[month]; + if (month == 1 && isLeapYear(year)) + dim++; + if (rtc_sec >= (uint32_t)dim) { + rtc_sec -= dim; + month++; + } else { + break; + } + } + + int day = rtc_sec + 1; + + if (includeTime) { + snprintf(buf, bufSize, "%04d-%02d-%02d %02d:%02d:%02d", year, month + 1, day, hour, min, sec); + } else { + snprintf(buf, bufSize, "%04d-%02d-%02d", year, month + 1, day); + } + + return display->getStringWidth(buf); +} + +// Check if the display can render a string (detect special chars; emoji) +bool haveGlyphs(const char *str) +{ +#if defined(OLED_PL) || defined(OLED_UA) || defined(OLED_RU) || defined(OLED_CS) + // Don't want to make any assumptions about custom language support + return true; +#endif + + // Check each character with the lookup function for the OLED library + // We're not really meant to use this directly.. + bool have = true; + for (uint16_t i = 0; i < strlen(str); i++) { + uint8_t result = Screen::customFontTableLookup((uint8_t)str[i]); + // If font doesn't support a character, it is substituted for ΒΏ + if (result == 191 && (uint8_t)str[i] != 191) { + have = false; + break; + } + } + + // LOG_DEBUG("haveGlyphs=%d", have); + return have; +} + +#ifdef USE_EINK +/// Used on eink displays while in deep sleep +void drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + + // Next frame should use full-refresh, and block while running, else device will sleep before async callback + EINK_ADD_FRAMEFLAG(display, COSMETIC); + EINK_ADD_FRAMEFLAG(display, BLOCKING); + + LOG_DEBUG("Draw deep sleep screen"); + + // Display displayStr on the screen + graphics::UIRenderer::drawIconScreen("Sleeping", display, state, x, y); +} + +/// Used on eink displays when screen updates are paused +void drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + LOG_DEBUG("Draw screensaver overlay"); + + EINK_ADD_FRAMEFLAG(display, COSMETIC); // Take the opportunity for a full-refresh + + // Config + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_LEFT); + const char *pauseText = "Screen Paused"; + const char *idText = owner.short_name; + const bool useId = haveGlyphs(idText); // This bool is used to hide the idText box if we can't render the short name + constexpr uint16_t padding = 5; + constexpr uint8_t dividerGap = 1; + constexpr uint8_t imprecision = 5; // How far the box origins can drift from center. Combat burn-in. + + // Dimensions + const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); // "true": handle utf8 chars + const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText)); + const uint16_t boxWidth = padding + (useId ? idTextWidth + padding + padding : 0) + pauseTextWidth + padding; + const uint16_t boxHeight = padding + FONT_HEIGHT_SMALL + padding; + + // Position + const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2) + random(-imprecision, imprecision + 1); + // const int16_t boxRight = boxLeft + boxWidth - 1; + const int16_t boxTop = (display->height() / 2) - (boxHeight / 2 + random(-imprecision, imprecision + 1)); + const int16_t boxBottom = boxTop + boxHeight - 1; + const int16_t idTextLeft = boxLeft + padding; + const int16_t idTextTop = boxTop + padding; + const int16_t pauseTextLeft = boxLeft + (useId ? padding + idTextWidth + padding : 0) + padding; + const int16_t pauseTextTop = boxTop + padding; + const int16_t dividerX = boxLeft + padding + idTextWidth + padding; + const int16_t dividerTop = boxTop + 1 + dividerGap; + const int16_t dividerBottom = boxBottom - 1 - dividerGap; + + // Draw: box + display->setColor(EINK_WHITE); + display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Clear a slightly oversized area for the box + display->setColor(EINK_BLACK); + display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); + + // Draw: Text + if (useId) + display->drawString(idTextLeft, idTextTop, idText); + display->drawString(pauseTextLeft, pauseTextTop, pauseText); + display->drawString(pauseTextLeft + 1, pauseTextTop, pauseText); // Faux bold + + // Draw: divider + if (useId) + display->drawLine(dividerX, dividerTop, dividerX, dividerBottom); +} +#endif + +/** + * Draw the icon with extra info printed around the corners + */ +void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const char *label = "BaseUI"; + display->setFont(FONT_SMALL); + int textWidth = display->getStringWidth(label); + int r = 3; // corner radius + + if (SCREEN_WIDTH > 128) { + // === ORIGINAL WIDE SCREEN LAYOUT (unchanged) === + int padding = 4; + int boxWidth = max(icon_width, textWidth) + (padding * 2) + 16; + int boxHeight = icon_height + FONT_HEIGHT_SMALL + (padding * 3) - 8; + int boxX = x - 1 + (SCREEN_WIDTH - boxWidth) / 2; + int boxY = y - 6 + (SCREEN_HEIGHT - boxHeight) / 2; + + display->setColor(WHITE); + display->fillRect(boxX + r, boxY, boxWidth - 2 * r, boxHeight); + display->fillRect(boxX, boxY + r, boxWidth - 1, boxHeight - 2 * r); + display->fillCircle(boxX + r, boxY + r, r); // Upper Left + display->fillCircle(boxX + boxWidth - r - 1, boxY + r, r); // Upper Right + display->fillCircle(boxX + r, boxY + boxHeight - r - 1, r); // Lower Left + display->fillCircle(boxX + boxWidth - r - 1, boxY + boxHeight - r - 1, r); // Lower Right + + display->setColor(BLACK); + int iconX = boxX + (boxWidth - icon_width) / 2; + int iconY = boxY + padding - 2; + display->drawXbm(iconX, iconY, icon_width, icon_height, icon_bits); + + int labelY = iconY + icon_height + padding; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(x + SCREEN_WIDTH / 2 - 3, labelY, label); + display->drawString(x + SCREEN_WIDTH / 2 - 2, labelY, label); // faux bold + + } else { + // === TIGHT SMALL SCREEN LAYOUT === + int iconY = y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - icon_height) / 2 + 2; + iconY -= 4; + + int labelY = iconY + icon_height - 2; + + int boxWidth = max(icon_width, textWidth) + 4; + int boxX = x + (SCREEN_WIDTH - boxWidth) / 2; + int boxY = iconY - 1; + int boxBottom = labelY + FONT_HEIGHT_SMALL - 2; + int boxHeight = boxBottom - boxY; + + display->setColor(WHITE); + display->fillRect(boxX + r, boxY, boxWidth - 2 * r, boxHeight); + display->fillRect(boxX, boxY + r, boxWidth - 1, boxHeight - 2 * r); + display->fillCircle(boxX + r, boxY + r, r); + display->fillCircle(boxX + boxWidth - r - 1, boxY + r, r); + display->fillCircle(boxX + r, boxY + boxHeight - r - 1, r); + display->fillCircle(boxX + boxWidth - r - 1, boxY + boxHeight - r - 1, r); + + display->setColor(BLACK); + int iconX = boxX + (boxWidth - icon_width) / 2; + display->drawXbm(iconX, iconY, icon_width, icon_height, icon_bits); + + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(x + SCREEN_WIDTH / 2, labelY, label); + } + + // === Footer and headers (shared) === + display->setFont(FONT_MEDIUM); + display->setColor(WHITE); + display->setTextAlignment(TEXT_ALIGN_LEFT); + const char *title = "meshtastic.org"; + display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); + + display->setFont(FONT_SMALL); + if (upperMsg) + display->drawString(x + 0, y + 0, upperMsg); + + char buf[25]; + snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), + graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(x + SCREEN_WIDTH, y + 0, buf); + + screen->forceDisplay(); + display->setTextAlignment(TEXT_ALIGN_LEFT); +} + +// **************************** +// * My Position Screen * +// **************************** +void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // === Header === + graphics::drawCommonHeader(display, x, y); + + // === Draw title === + const int highlightHeight = FONT_HEIGHT_SMALL - 1; + const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + const char *titleStr = "GPS"; + const int centerX = x + SCREEN_WIDTH / 2; + + if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { + display->setColor(BLACK); + } + + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(centerX, textY, titleStr); + if (config.display.heading_bold) { + display->drawString(centerX + 1, textY, titleStr); + } + display->setColor(WHITE); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === First Row: My Location === +#if HAS_GPS + bool origBold = config.display.heading_bold; + config.display.heading_bold = false; + + String Satelite_String = "Sat:"; + display->drawString(0, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), Satelite_String); + String displayLine = ""; + if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { + if (config.position.fixed_position) { + displayLine = "Fixed GPS"; + } else { + displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; + } + display->drawString(display->getStringWidth(Satelite_String) + 3, + ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), displayLine); + } else { + UIRenderer::drawGps(display, display->getStringWidth(Satelite_String) + 3, + ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine) + 3, gpsStatus); + } + + config.display.heading_bold = origBold; + + // === Update GeoCoord === + geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()), + int32_t(gpsStatus->getAltitude())); + + // === Determine Compass Heading === + float heading; + bool validHeading = false; + + if (screen->hasHeading()) { + heading = radians(screen->getHeading()); + validHeading = true; + } else { + heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7); + validHeading = !isnan(heading); + } + + // If GPS is off, no need to display these parts + if (displayLine != "GPS off" && displayLine != "No GPS") { + + // === Second Row: Altitude === + String displayLine; + displayLine = " Alt: " + String(geoCoord.getAltitude()) + "m"; + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) + displayLine = " Alt: " + String(geoCoord.getAltitude() * METERS_TO_FEET) + "ft"; + display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine), displayLine); + + // === Third Row: Latitude === + char latStr[32]; + snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7); + display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactThirdLine : moreCompactThirdLine), latStr); + + // === Fourth Row: Longitude === + char lonStr[32]; + snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7); + display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine), lonStr); + + // === Fifth Row: Date === + uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); + char datetimeStr[25]; + bool showTime = false; // set to true for full datetime + UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime); + char fullLine[40]; + snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr); + display->drawString(0, ((SCREEN_HEIGHT > 64) ? compactFifthLine : moreCompactFifthLine), fullLine); + } + + // === Draw Compass if heading is valid === + if (validHeading) { + // --- Compass Rendering: landscape (wide) screens use original side-aligned logic --- + if (SCREEN_WIDTH > SCREEN_HEIGHT) { + const int16_t topY = compactFirstLine; + const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height + const int16_t usableHeight = bottomY - topY - 5; + + int16_t compassRadius = usableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + const int16_t compassDiam = compassRadius * 2; + const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8; + + // Center vertically and nudge down slightly to keep "N" clear of header + const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2; + + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, -heading); + display->drawCircle(compassX, compassY, compassRadius); + + // "N" label + float northAngle = -heading; + float radius = compassRadius; + int16_t nX = compassX + (radius - 1) * sin(northAngle); + int16_t nY = compassY - (radius - 1) * cos(northAngle); + int16_t nLabelWidth = display->getStringWidth("N") + 2; + int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; + + display->setColor(BLACK); + display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); + display->setColor(WHITE); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + } else { + // Portrait or square: put compass at the bottom and centered, scaled to fit available space + // For E-Ink screens, account for navigation bar at the bottom! + int yBelowContent = ((SCREEN_HEIGHT > 64) ? compactFifthLine : moreCompactFifthLine) + FONT_HEIGHT_SMALL + 2; + const int margin = 4; + int availableHeight = +#if defined(USE_EINK) + SCREEN_HEIGHT - yBelowContent - 24; // Leave extra space for nav bar on E-Ink +#else + SCREEN_HEIGHT - yBelowContent - margin; +#endif + + if (availableHeight < FONT_HEIGHT_SMALL * 2) + return; + + int compassRadius = availableHeight / 2; + if (compassRadius < 8) + compassRadius = 8; + if (compassRadius * 2 > SCREEN_WIDTH - 16) + compassRadius = (SCREEN_WIDTH - 16) / 2; + + int compassX = x + SCREEN_WIDTH / 2; + int compassY = yBelowContent + availableHeight / 2; + + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading); + display->drawCircle(compassX, compassY, compassRadius); + + // "N" label + float northAngle = -heading; + float radius = compassRadius; + int16_t nX = compassX + (radius - 1) * sin(northAngle); + int16_t nY = compassY - (radius - 1) * cos(northAngle); + int16_t nLabelWidth = display->getStringWidth("N") + 2; + int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1; + + display->setColor(BLACK); + display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox); + display->setColor(WHITE); + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N"); + } + } +#endif +} + +#ifdef USERPREFS_OEM_TEXT + +void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + static const uint8_t xbm[] = USERPREFS_OEM_IMAGE_DATA; + display->drawXbm(x + (SCREEN_WIDTH - USERPREFS_OEM_IMAGE_WIDTH) / 2, + y + (SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM - USERPREFS_OEM_IMAGE_HEIGHT) / 2 + 2, USERPREFS_OEM_IMAGE_WIDTH, + USERPREFS_OEM_IMAGE_HEIGHT, xbm); + + switch (USERPREFS_OEM_FONT_SIZE) { + case 0: + display->setFont(FONT_SMALL); + break; + case 2: + display->setFont(FONT_LARGE); + break; + default: + display->setFont(FONT_MEDIUM); + break; + } + + display->setTextAlignment(TEXT_ALIGN_LEFT); + const char *title = USERPREFS_OEM_TEXT; + display->drawString(x + getStringCenteredX(title), y + SCREEN_HEIGHT - FONT_HEIGHT_MEDIUM, title); + display->setFont(FONT_SMALL); + + // Draw region in upper left + if (upperMsg) + display->drawString(x + 0, y + 0, upperMsg); + + // Draw version and shortname in upper right + char buf[25]; + snprintf(buf, sizeof(buf), "%s\n%s", xstr(APP_VERSION_SHORT), haveGlyphs(owner.short_name) ? owner.short_name : ""); + + display->setTextAlignment(TEXT_ALIGN_RIGHT); + display->drawString(x + SCREEN_WIDTH, y + 0, buf); + screen->forceDisplay(); + + display->setTextAlignment(TEXT_ALIGN_LEFT); // Restore left align, just to be kind to any other unsuspecting code +} + +void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + // Draw region in upper left + const char *region = myRegion ? myRegion->name : NULL; + drawOEMIconScreen(region, display, state, x, y); +} + +#endif + +// Function overlay for showing mute/buzzer modifiers etc. +void drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + // LOG_DEBUG("Draw function overlay"); + if (functionSymbol.begin() != functionSymbol.end()) { + char buf[64]; + display->setFont(FONT_SMALL); + snprintf(buf, sizeof(buf), "%s", functionSymbolString.c_str()); + display->drawString(SCREEN_WIDTH - display->getStringWidth(buf), SCREEN_HEIGHT - FONT_HEIGHT_SMALL, buf); + } +} + +// Navigation bar overlay implementation +static int8_t lastFrameIndex = -1; +static uint32_t lastFrameChangeTime = 0; +constexpr uint32_t ICON_DISPLAY_DURATION_MS = 2000; + +void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state) +{ + int currentFrame = state->currentFrame; + + // Detect frame change and record time + if (currentFrame != lastFrameIndex) { + lastFrameIndex = currentFrame; + lastFrameChangeTime = millis(); + } + + const bool useBigIcons = (SCREEN_WIDTH > 128); + const int iconSize = useBigIcons ? 16 : 8; + const int spacing = useBigIcons ? 8 : 4; + const int bigOffset = useBigIcons ? 1 : 0; + + const size_t totalIcons = screen->indicatorIcons.size(); + if (totalIcons == 0) + return; + + const size_t iconsPerPage = (SCREEN_WIDTH + spacing) / (iconSize + spacing); + const size_t currentPage = currentFrame / iconsPerPage; + const size_t pageStart = currentPage * iconsPerPage; + const size_t pageEnd = min(pageStart + iconsPerPage, totalIcons); + + const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing; + const int xStart = (SCREEN_WIDTH - totalWidth) / 2; + + // Only show bar briefly after switching frames (unless on E-Ink) +#if defined(USE_EINK) + int y = SCREEN_HEIGHT - iconSize - 1; +#else + int y = SCREEN_HEIGHT - iconSize - 1; + if (millis() - lastFrameChangeTime > ICON_DISPLAY_DURATION_MS) { + y = SCREEN_HEIGHT; + } +#endif + + // Pre-calculate bounding rect + const int rectX = xStart - 2 - bigOffset; + const int rectWidth = totalWidth + 4 + (bigOffset * 2); + const int rectHeight = iconSize + 6; + + // Clear background and draw border + display->setColor(BLACK); + display->fillRect(rectX + 1, y - 2, rectWidth - 2, rectHeight - 2); + display->setColor(WHITE); + display->drawRect(rectX, y - 2, rectWidth, rectHeight); + + // Icon drawing loop for the current page + for (size_t i = pageStart; i < pageEnd; ++i) { + const uint8_t *icon = screen->indicatorIcons[i]; + const int x = xStart + (i - pageStart) * (iconSize + spacing); + const bool isActive = (i == static_cast(currentFrame)); + + if (isActive) { + display->setColor(WHITE); + display->fillRect(x - 2, y - 2, iconSize + 4, iconSize + 4); + display->setColor(BLACK); + } + + if (useBigIcons) { + NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display); + } else { + display->drawXbm(x, y, iconSize, iconSize, icon); + } + + if (isActive) { + display->setColor(WHITE); + } + } + + // Knock the corners off the square + display->setColor(BLACK); + display->drawRect(rectX, y - 2, 1, 1); + display->drawRect(rectX + rectWidth - 1, y - 2, 1, 1); + display->setColor(WHITE); +} + +void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *message) +{ + uint16_t x_offset = display->width() / 2; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(x_offset + x, 26 + y, message); +} + +std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds) +{ + std::string uptime; + + if (days > (HOURS_IN_MONTH * 6)) + uptime = "?"; + else if (days >= 2) + uptime = std::to_string(days) + "d"; + else if (hours >= 2) + uptime = std::to_string(hours) + "h"; + else if (minutes >= 1) + uptime = std::to_string(minutes) + "m"; + else + uptime = std::to_string(seconds) + "s"; + return uptime; +} + +} // namespace UIRenderer +} // namespace graphics + +#endif // !MESHTASTIC_EXCLUDE_GPS diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h new file mode 100644 index 000000000..727d5e134 --- /dev/null +++ b/src/graphics/draw/UIRenderer.h @@ -0,0 +1,92 @@ +#pragma once + +#include "graphics/Screen.h" +#include "graphics/emotes.h" +#include +#include +#include + +#define HOURS_IN_MONTH 730 + +// Forward declarations for status types +namespace meshtastic +{ +class PowerStatus; +class NodeStatus; +class GPSStatus; +} // namespace meshtastic + +namespace graphics +{ + +/// Forward declarations +class Screen; + +/** + * @brief UI utility drawing functions + * + * Contains utility functions for drawing common UI elements, overlays, + * battery indicators, and other shared graphical components. + */ +namespace UIRenderer +{ +// Common UI elements +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y); +void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer, const meshtastic::PowerStatus *powerStatus); +void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset = 0, + bool show_total = true, String additional_words = ""); + +// GPS status functions +void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); +void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); +void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); +void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus); + +// Layout and utility functions +void drawScrollbar(OLEDDisplay *display, int visibleItems, int totalItems, int scrollIndex, int x, int startY); + +// Overlay and special screens +void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text); + +// Function overlay for showing mute/buzzer modifiers etc. +void drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); + +// Navigation bar overlay +void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state); + +void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// Icon and screen drawing functions +void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// Compass and location screen +void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// OEM screens +#ifdef USERPREFS_OEM_TEXT +void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +#endif + +#ifdef USE_EINK +/// Used on eink displays while in deep sleep +void drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +/// Used on eink displays when screen updates are paused +void drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); +#endif + +// Time and date utilities +void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength); +std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds); +int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime); + +// Message filtering +bool shouldDrawMessage(const meshtastic_MeshPacket *packet); +// Check if the display can render a string (detect special chars; emoji) +bool haveGlyphs(const char *str); +} // namespace UIRenderer + +} // namespace graphics diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp index 9313c3425..8641990f7 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -2,9 +2,12 @@ #include "NodeDB.h" #include "PowerFSM.h" #include "configuration.h" +#include "graphics/draw/CompassRenderer.h" + #if HAS_SCREEN #include "gps/RTC.h" #include "graphics/Screen.h" +#include "graphics/draw/NodeListRenderer.h" #include "main.h" #endif @@ -119,7 +122,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, // Dimensions / co-ordinates for the compass/circle int16_t compassX = 0, compassY = 0; - uint16_t compassDiam = graphics::Screen::getCompassDiam(display->getWidth(), display->getHeight()); + 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; @@ -137,7 +140,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians else myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - screen->drawCompassNorth(display, compassX, compassY, myHeading); + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading); // Compass bearing to waypoint float bearingToOther = @@ -146,7 +149,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, // If the top of the compass is not a static north we need adjust bearingToOther based on heading if (!config.display.compass_north_top) bearingToOther -= myHeading; - screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); + graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther); float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther; bearingToOtherDegrees = bearingToOtherDegrees * 180 / PI; @@ -189,6 +192,6 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, } // Must be after distStr is populated - screen->drawColumns(display, x, y, fields); + graphics::NodeListRenderer::drawColumns(display, x, y, fields); } #endif diff --git a/src/motion/MotionSensor.cpp b/src/motion/MotionSensor.cpp index 840d64277..438fd4f7a 100755 --- a/src/motion/MotionSensor.cpp +++ b/src/motion/MotionSensor.cpp @@ -1,4 +1,5 @@ #include "MotionSensor.h" +#include "graphics/draw/CompassRenderer.h" #if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C @@ -48,7 +49,7 @@ void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState display->drawString(x, y + 40, timeRemainingBuffer); int16_t compassX = 0, compassY = 0; - uint16_t compassDiam = graphics::Screen::getCompassDiam(display->getWidth(), display->getHeight()); + uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight()); // coordinates for the center of the compass/circle if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) { @@ -59,7 +60,7 @@ void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2; } display->drawCircle(compassX, compassY, compassDiam / 2); - screen->drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180); + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180); } #endif From fd1f028ecf26941e34bd53c406442b2bcda6c754 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 1 Jun 2025 14:23:06 -0500 Subject: [PATCH 187/265] Namespace fixes --- src/graphics/draw/NodeListRenderer.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index 7bf742f4f..d2ba64d05 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -634,7 +634,8 @@ void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, in const int highlightHeight = FONT_HEIGHT_SMALL - 1; const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; const int centerX = x + SCREEN_WIDTH / 2; - const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; + const char *shortName = + (node->has_user && graphics::UIRenderer::haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) display->setColor(BLACK); @@ -662,7 +663,7 @@ void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, in // 2. Signal and Hops (combined on one line, if available) char signalHopsStr[32] = ""; bool haveSignal = false; - int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100); + int percentSignal = std::clamp((int)((node->snr + 10) * 5), 0, 100); const char *signalLabel = " Sig"; // If SNR looks reasonable, show signal From 4dfe1ddab93d23a0ca84a72e3a4811e0ed3bbafc Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 1 Jun 2025 14:47:05 -0500 Subject: [PATCH 188/265] better fix for clamp() on native --- src/graphics/draw/NodeListRenderer.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index d2ba64d05..74fd8b98e 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -8,6 +8,7 @@ #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" +#include "meshUtils.h" #include // Forward declarations for functions defined in Screen.cpp @@ -663,7 +664,7 @@ void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, in // 2. Signal and Hops (combined on one line, if available) char signalHopsStr[32] = ""; bool haveSignal = false; - int percentSignal = std::clamp((int)((node->snr + 10) * 5), 0, 100); + int percentSignal = clamp((int)((node->snr + 10) * 5), 0, 100); const char *signalLabel = " Sig"; // If SNR looks reasonable, show signal From ed2953eb4b2c8dc483cc3fe63c15b510425f7492 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 1 Jun 2025 15:10:34 -0500 Subject: [PATCH 189/265] Pick up missed function --- src/graphics/draw/UIRenderer.cpp | 39 ++++++++++++++++++++++++++++++++ src/graphics/draw/UIRenderer.h | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 3564db51a..421558cb7 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -196,6 +196,45 @@ void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshta } } +void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer, const meshtastic::PowerStatus *powerStatus) +{ + static const uint8_t powerBar[3] = {0x81, 0xBD, 0xBD}; + static const uint8_t lightning[8] = {0xA1, 0xA1, 0xA5, 0xAD, 0xB5, 0xA5, 0x85, 0x85}; + + // Clear the bar area inside the battery image + for (int i = 1; i < 14; i++) { + imgBuffer[i] = 0x81; + } + + // Fill with lightning or power bars + if (powerStatus->getIsCharging()) { + memcpy(imgBuffer + 3, lightning, 8); + } else { + for (int i = 0; i < 4; i++) { + if (powerStatus->getBatteryChargePercent() >= 25 * i) + memcpy(imgBuffer + 1 + (i * 3), powerBar, 3); + } + } + + // Slightly more conservative scaling based on screen width + int scale = 1; + + if (SCREEN_WIDTH >= 200) + scale = 2; + if (SCREEN_WIDTH >= 300) + scale = 2; // Do NOT go higher than 2 + + // Draw scaled battery image (16 columns Γ— 8 rows) + for (int col = 0; col < 16; col++) { + uint8_t colBits = imgBuffer[col]; + for (int row = 0; row < 8; row++) { + if (colBits & (1 << row)) { + display->fillRect(x + col * scale, y + row * scale, scale, scale); + } + } + } +} + // Draw nodes status void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset, bool show_total, String additional_words) diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h index 727d5e134..99f996110 100644 --- a/src/graphics/draw/UIRenderer.h +++ b/src/graphics/draw/UIRenderer.h @@ -31,7 +31,7 @@ class Screen; namespace UIRenderer { // Common UI elements -void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y); +// void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y); void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer, const meshtastic::PowerStatus *powerStatus); void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset = 0, bool show_total = true, String additional_words = ""); From 710a44405706088cc2ed5db4c73f92f0cc80e0d4 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 1 Jun 2025 15:24:40 -0500 Subject: [PATCH 190/265] Actually remove the commented line --- src/graphics/draw/UIRenderer.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h index 99f996110..86231da11 100644 --- a/src/graphics/draw/UIRenderer.h +++ b/src/graphics/draw/UIRenderer.h @@ -31,7 +31,6 @@ class Screen; namespace UIRenderer { // Common UI elements -// void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y); void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer, const meshtastic::PowerStatus *powerStatus); void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset = 0, bool show_total = true, String additional_words = ""); From 14aeeb7286fd43ba1d3703b0bba0621782310567 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 1 Jun 2025 15:31:24 -0500 Subject: [PATCH 191/265] Fix compilation on targets without HAS_SCREEN --- src/ButtonThread.cpp | 2 ++ src/graphics/Screen.h | 10 ++++++++++ src/graphics/draw/NodeListRenderer.cpp | 2 ++ src/graphics/draw/UIRenderer.cpp | 2 ++ src/modules/CannedMessageModule.cpp | 6 +++--- 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp index 3680bb6b8..d898d4839 100644 --- a/src/ButtonThread.cpp +++ b/src/ButtonThread.cpp @@ -27,7 +27,9 @@ using namespace concurrency; ButtonThread *buttonThread; // Declared extern in header +#if HAS_SCREEN extern CannedMessageModule *cannedMessageModule; +#endif volatile ButtonThread::ButtonEventType ButtonThread::btnEvent = ButtonThread::BUTTON_EVENT_NONE; #if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN) diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 29e5df226..0d791e463 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -18,6 +18,14 @@ namespace graphics class Screen { public: + enum FrameFocus : uint8_t { + FOCUS_DEFAULT, // No specific frame + FOCUS_PRESERVE, // Return to the previous frame + FOCUS_FAULT, + FOCUS_TEXTMESSAGE, + FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus + }; + explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); void onPress() {} void setup() {} @@ -31,6 +39,8 @@ class Screen void setFunctionSymbol(std::string) {} void removeFunctionSymbol(std::string) {} void startAlert(const char *) {} + void showOverlayBanner(String, uint32_t durationMs = 3000) {} + void setFrames(FrameFocus focus) {} void endAlert() {} }; } // namespace graphics diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index 74fd8b98e..10eb17639 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -1,3 +1,4 @@ +#if HAS_SCREEN #include "NodeListRenderer.h" #include "CompassRenderer.h" #include "NodeDB.h" @@ -877,3 +878,4 @@ void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields } // namespace NodeListRenderer } // namespace graphics +#endif \ No newline at end of file diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 421558cb7..e0bbdf9ec 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -1,3 +1,4 @@ +#if HAS_SCREEN #include "UIRenderer.h" #include "CompassRenderer.h" #include "GPSStatus.h" @@ -1291,3 +1292,4 @@ std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint3 } // namespace graphics #endif // !MESHTASTIC_EXCLUDE_GPS +#endif // HAS_SCREEN \ No newline at end of file diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index eb3758c30..f245d9707 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -1828,9 +1828,9 @@ String CannedMessageModule::drawWithCursor(String text, int cursor) return result; } -#endif - bool CannedMessageModule::isInterceptingAndFocused() { return this->interceptingKeyboardInput(); -} \ No newline at end of file +} + +#endif \ No newline at end of file From f6a75a5f80c160f9f589e2a1f0cba862384b41b6 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 1 Jun 2025 15:39:11 -0500 Subject: [PATCH 192/265] HAS_SCREEN is only defined after including configuration.h --- src/graphics/draw/NodeListRenderer.cpp | 4 ++-- src/graphics/draw/UIRenderer.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index 10eb17639..ab497fec2 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -1,9 +1,9 @@ +#include "configuration.h" #if HAS_SCREEN -#include "NodeListRenderer.h" #include "CompassRenderer.h" #include "NodeDB.h" +#include "NodeListRenderer.h" #include "UIRenderer.h" -#include "configuration.h" #include "gps/GeoCoord.h" #include "gps/RTC.h" // for getTime() function #include "graphics/ScreenFonts.h" diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index e0bbdf9ec..b2668e5e0 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -1,9 +1,9 @@ +#include "configuration.h" #if HAS_SCREEN -#include "UIRenderer.h" #include "CompassRenderer.h" #include "GPSStatus.h" #include "NodeDB.h" -#include "NodeListRenderer.h" +#include "UIRenderer.h" #include "configuration.h" #include "gps/GeoCoord.h" #include "graphics/Screen.h" From 7208ebe583ce65de6db9502723343920ba2ffbad Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 1 Jun 2025 15:46:35 -0500 Subject: [PATCH 193/265] Fix warnings --- src/graphics/draw/DebugRenderer.cpp | 6 ++++-- src/graphics/draw/NodeListRenderer.cpp | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 167fd602d..00aabedfb 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -431,7 +431,9 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); char regionradiopreset[25]; const char *region = myRegion ? myRegion->name : NULL; - snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); + if (region != nullptr) { + snprintf(regionradiopreset, sizeof(regionradiopreset), "%s/%s", region, mode); + } textWidth = display->getStringWidth(regionradiopreset); nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, compactSecondLine, regionradiopreset); @@ -565,7 +567,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, char combinedStr[24]; if (SCREEN_WIDTH > 128) { - snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %lu/%luKB", (percent > 80) ? "! " : "", percent, used / 1024, + snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024, total / 1024); } else { snprintf(combinedStr, sizeof(combinedStr), "%s%3d%%", (percent > 80) ? "! " : "", percent); diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index ab497fec2..4385e0f1b 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -713,11 +713,11 @@ void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, in uint32_t mins = (uptime % 3600) / 60; if (days > 0) { - snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %dd %dh", days, hours); + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %ud %uh", days, hours); } else if (hours > 0) { - snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %dh %dm", hours, mins); + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %uh %um", hours, mins); } else { - snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %dm", mins); + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %um", mins); } } if (uptimeStr[0] && line < 5) { From 59c51ad24c6d628f785e3f7c4d33762bb0593198 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 1 Jun 2025 15:47:06 -0500 Subject: [PATCH 194/265] Add missed include --- src/graphics/draw/UIRenderer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index b2668e5e0..1965d6457 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -3,6 +3,7 @@ #include "CompassRenderer.h" #include "GPSStatus.h" #include "NodeDB.h" +#include "NodeListRenderer.h" #include "UIRenderer.h" #include "configuration.h" #include "gps/GeoCoord.h" From 56c2d26d90daeb0a79e0f692df36b354b9c767fc Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 1 Jun 2025 15:54:07 -0500 Subject: [PATCH 195/265] Another Warning fix --- src/graphics/draw/UIRenderer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 1965d6457..f8524c1d1 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -592,7 +592,7 @@ void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t if (days > 365) { snprintf(uptimeStr, sizeof(uptimeStr), "?"); } else { - snprintf(uptimeStr, sizeof(uptimeStr), "%d%c", + snprintf(uptimeStr, sizeof(uptimeStr), "%u%c", days ? days : hours ? hours : minutes ? minutes From 1e6e9e0be44503c6dd55a5b381d41c3edef8dc50 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 1 Jun 2025 15:56:54 -0500 Subject: [PATCH 196/265] Add another HAS_SCREEN --- src/graphics/draw/MessageRenderer.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index a61f40ffc..2645a68ad 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -21,6 +21,8 @@ along with this program. If not, see . */ +#include "configuration.h" +#if HAS_SCREEN #include "MessageRenderer.h" // Core includes @@ -446,3 +448,4 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 } // namespace MessageRenderer } // namespace graphics +#endif \ No newline at end of file From dd723cb54c52e1c7537371051cb4af189030f485 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 1 Jun 2025 16:10:13 -0500 Subject: [PATCH 197/265] Namespace fixes --- src/graphics/Screen.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 22842adaf..474a000d5 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -222,7 +222,7 @@ void Screen::drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *sta { display->setTextAlignment(TEXT_ALIGN_LEFT); - drawBattery(display, x, y + 7, imgBattery, powerStatus); + UIRenderer::drawBattery(display, x, y + 7, imgBattery, powerStatus); if (powerStatus->getHasBattery()) { String batteryPercent = String(powerStatus->getBatteryChargePercent()) + "%"; @@ -454,7 +454,7 @@ void Screen::drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *stat { display->setTextAlignment(TEXT_ALIGN_LEFT); - drawBattery(display, x, y + 7, imgBattery, powerStatus); + UIRenderer::drawBattery(display, x, y + 7, imgBattery, powerStatus); if (powerStatus->getHasBattery()) { String batteryPercent = String(powerStatus->getBatteryChargePercent()) + "%"; From 6344be0a1e41c221e5be841750552c46730fe9c6 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Sun, 1 Jun 2025 22:16:11 -0400 Subject: [PATCH 198/265] Removed depricated destination types and re-factored destination screen --- src/modules/CannedMessageModule.cpp | 381 ++++++++++++---------------- src/modules/CannedMessageModule.h | 20 +- 2 files changed, 172 insertions(+), 229 deletions(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index f245d9707..99813cba6 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -56,12 +56,6 @@ CannedMessageModule::CannedMessageModule() disable(); } else { LOG_INFO("CannedMessageModule is enabled"); - - // T-Watch interface currently has no way to select destination type, so default to 'node' -#if defined(USE_VIRTUAL_KEYBOARD) - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; -#endif - this->inputObserver.observe(inputBroker); } } else { @@ -165,7 +159,7 @@ void CannedMessageModule::resetSearch() int previousDestIndex = destIndex; searchQuery = ""; - updateFilteredNodes(); + updateDestinationSelectionList(); // Adjust scrollIndex so previousDestIndex is still visible int totalEntries = activeChannelIndices.size() + filteredNodes.size(); @@ -178,7 +172,7 @@ void CannedMessageModule::resetSearch() lastUpdateMillis = millis(); requestFocus(); } -void CannedMessageModule::updateFilteredNodes() +void CannedMessageModule::updateDestinationSelectionList() { static size_t lastNumMeshNodes = 0; static String lastSearchQuery = ""; @@ -242,7 +236,7 @@ void CannedMessageModule::updateFilteredNodes() }); scrollIndex = 0; // Show first result at the top destIndex = 0; // Highlight the first entry - if (nodesChanged) { + if (nodesChanged && runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { LOG_INFO("Nodes changed, forcing UI refresh."); screen->forceDisplay(); } @@ -385,16 +379,15 @@ bool CannedMessageModule::handleTabSwitch(const InputEvent *event) if (event->kbchar != 0x09) return false; - destSelect = (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) ? CANNED_MESSAGE_DESTINATION_TYPE_NONE - : CANNED_MESSAGE_DESTINATION_TYPE_NODE; - runState = (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) ? CANNED_MESSAGE_RUN_STATE_FREETEXT - : CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + runState = (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) + ? CANNED_MESSAGE_RUN_STATE_FREETEXT + : CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; destIndex = 0; scrollIndex = 0; // RESTORE THIS! if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) - updateFilteredNodes(); + updateDestinationSelectionList(); UIFrameEvent e; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; @@ -405,15 +398,16 @@ bool CannedMessageModule::handleTabSwitch(const InputEvent *event) int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) { - static bool shouldRedraw = false; - if (event->kbchar >= 32 && event->kbchar <= 126 && !isUp && !isDown && event->inputEvent != static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT) && event->inputEvent != static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT) && event->inputEvent != static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) { this->searchQuery += event->kbchar; needsUpdate = true; - runOnce(); // update filter immediately + if ((millis() - lastFilterUpdate) > filterDebounceMs) { + runOnce(); // update filter immediately + lastFilterUpdate = millis(); + } return 0; } @@ -446,7 +440,8 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event else if ((destIndex / columns) >= (scrollIndex + visibleRows)) scrollIndex = (destIndex / columns) - visibleRows + 1; - shouldRedraw = true; + screen->forceDisplay(); + return 0; } // DOWN @@ -455,12 +450,8 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event if ((destIndex / columns) >= (scrollIndex + visibleRows)) scrollIndex = (destIndex / columns) - visibleRows + 1; - shouldRedraw = true; - } - - if (shouldRedraw) { screen->forceDisplay(); - shouldRedraw = false; + return 0; } // SELECT @@ -481,14 +472,12 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event runState = returnToCannedList ? CANNED_MESSAGE_RUN_STATE_ACTIVE : CANNED_MESSAGE_RUN_STATE_FREETEXT; returnToCannedList = false; - destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; screen->forceDisplay(); return 0; } // CANCEL if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) { - destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; searchQuery = ""; @@ -504,7 +493,7 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) { - if (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) + if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) return false; // === Handle Cancel key: go inactive, clear UI state === @@ -539,10 +528,9 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo if (strcmp(current, "[Select Destination]") == 0) { returnToCannedList = true; runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; - destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; destIndex = 0; scrollIndex = 0; - updateFilteredNodes(); // Make sure list is fresh + updateDestinationSelectionList(); // Make sure list is fresh screen->forceDisplay(); return true; } @@ -889,28 +877,26 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha screen->handleTextMessage(&simulatedPacket); } } -bool validEvent = false; -unsigned long lastUpdateMillis = 0; int32_t CannedMessageModule::runOnce() { -#define NODE_UPDATE_IDLE_MS 100 -#define NODE_UPDATE_ACTIVE_MS 80 - - unsigned long updateThreshold = (searchQuery.length() > 0) ? NODE_UPDATE_ACTIVE_MS : NODE_UPDATE_IDLE_MS; - if (needsUpdate && millis() - lastUpdateMillis > updateThreshold) { - updateFilteredNodes(); - lastUpdateMillis = millis(); + if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION && needsUpdate) { + updateDestinationSelectionList(); + needsUpdate = false; } - // Prevent message list activity when selecting destination + + // If we're in node selection, do nothing except keep alive if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { return INACTIVATE_AFTER_MS; } + // Normal module disable/idle handling if (((!moduleConfig.canned_message.enabled) && !CANNED_MESSAGE_MODULE_ENABLE) || - (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { + (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || + (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { temporaryMessage = ""; return INT32_MAX; } + UIFrameEvent e; if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || @@ -919,29 +905,27 @@ int32_t CannedMessageModule::runOnce() temporaryMessage = ""; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; - this->freetext = ""; // clear freetext + this->freetext = ""; this->cursor = 0; - -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(SENSECAP_INDICATOR) - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; -#endif - + #if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(SENSECAP_INDICATOR) + int destSelect = 0; + #endif this->notifyObservers(&e); - } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && - !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { - // Reset module + } + else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && + !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { + // Reset module on inactivity e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; - this->freetext = ""; // clear freetext + this->freetext = ""; this->cursor = 0; - -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD) - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; -#endif - + #if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD) + int destSelect = 0; + #endif this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; this->notifyObservers(&e); - } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { + } + else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { if (this->freetext.length() > 0) { sendText(this->dest, this->channel, this->freetext.c_str(), true); @@ -963,25 +947,18 @@ int32_t CannedMessageModule::runOnce() } this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE; } else { - // LOG_DEBUG("Reset message is empty"); this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; } } - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; - this->freetext = ""; // clear freetext + this->freetext = ""; this->cursor = 0; - -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD) - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; -#endif - this->notifyObservers(&e); return 2000; } - // === Always highlight the first real canned message when entering the message list === + // Always highlight the first real canned message when entering the message list else if ((this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) && (this->currentMessageIndex == -1)) { - // Find first actual canned message (not a special action entry) int firstRealMsgIdx = 0; for (int i = 0; i < this->messagesCount; ++i) { if (strcmp(this->messages[i], "[Select Destination]") != 0 && strcmp(this->messages[i], "[Exit]") != 0 && @@ -991,44 +968,35 @@ int32_t CannedMessageModule::runOnce() } } this->currentMessageIndex = firstRealMsgIdx; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; - } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) { + } + else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) { if (this->messagesCount > 0) { this->currentMessageIndex = getPrevIndex(); - this->freetext = ""; // clear freetext + this->freetext = ""; this->cursor = 0; - -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD) - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; -#endif - this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; LOG_DEBUG("MOVE UP (%d):%s", this->currentMessageIndex, this->getCurrentMessage()); } - } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_DOWN) { + } + else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_DOWN) { if (this->messagesCount > 0) { this->currentMessageIndex = this->getNextIndex(); - this->freetext = ""; // clear freetext + this->freetext = ""; this->cursor = 0; - -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD) - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE; -#endif - this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; LOG_DEBUG("MOVE DOWN (%d):%s", this->currentMessageIndex, this->getCurrentMessage()); } - } else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { + } + else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { switch (this->payload) { case INPUT_BROKER_MSG_LEFT: - // Only handle cursor movement in freetext if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor > 0) { this->cursor--; } break; case INPUT_BROKER_MSG_RIGHT: - // Only handle cursor movement in freetext if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor < this->freetext.length()) { this->cursor++; } @@ -1037,56 +1005,32 @@ int32_t CannedMessageModule::runOnce() break; } if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen - switch (this->payload) { // code below all trigger the freetext window (where you type to send a message) or reset the - // display back to the default window - case 0x08: // backspace + e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; + switch (this->payload) { + case 0x08: // backspace if (this->freetext.length() > 0 && this->highlight == 0x00) { if (this->cursor == this->freetext.length()) { this->freetext = this->freetext.substring(0, this->freetext.length() - 1); } else { this->freetext = this->freetext.substring(0, this->cursor - 1) + - this->freetext.substring(this->cursor, this->freetext.length()); + this->freetext.substring(this->cursor, this->freetext.length()); } this->cursor--; } break; - case INPUT_BROKER_MSG_TAB: // Tab key (Switch to Destination Selection Mode) - { - if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) { - // Enter selection screen - this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE; - this->destIndex = 0; // Reset to first node/channel - this->scrollIndex = 0; // Reset scrolling - this->runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; - - // Ensure UI updates correctly - UIFrameEvent e; - e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; - this->notifyObservers(&e); - } - - // If already inside the selection screen, do nothing (prevent exiting) + case INPUT_BROKER_MSG_TAB: // Tab key: handled by input handler return 0; - } break; case INPUT_BROKER_MSG_LEFT: case INPUT_BROKER_MSG_RIGHT: - // already handled above break; default: - if (this->highlight != 0x00) { - break; - } - + if (this->highlight != 0x00) break; if (this->cursor == this->freetext.length()) { this->freetext += this->payload; } else { - this->freetext = - this->freetext.substring(0, this->cursor) + this->payload + this->freetext.substring(this->cursor); + this->freetext = this->freetext.substring(0, this->cursor) + this->payload + this->freetext.substring(this->cursor); } - this->cursor += 1; - uint16_t maxChars = meshtastic_Constants_DATA_PAYLOAD_LEN - (moduleConfig.canned_message.send_bell ? 1 : 0); if (this->freetext.length() > maxChars) { this->cursor = maxChars; @@ -1095,7 +1039,6 @@ int32_t CannedMessageModule::runOnce() break; } } - this->lastTouchMillis = millis(); this->notifyObservers(&e); return INACTIVATE_AFTER_MS; @@ -1106,10 +1049,6 @@ int32_t CannedMessageModule::runOnce() this->notifyObservers(&e); return INACTIVATE_AFTER_MS; } - if (shouldRedraw) { - screen->forceDisplay(); - shouldRedraw = false; - } return INT32_MAX; } @@ -1407,6 +1346,107 @@ bool CannedMessageModule::interceptingKeyboardInput() } } +// Draw the node/channel selection screen +void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + requestFocus(); + display->setColor(WHITE); // Always draw cleanly + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // === Header === + int titleY = 2; + String titleText = "Select Destination"; + titleText += searchQuery.length() > 0 ? " [" + searchQuery + "]" : " [ ]"; + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(display->getWidth() / 2, titleY, titleText); + display->setTextAlignment(TEXT_ALIGN_LEFT); + + // === List Items === + int rowYOffset = titleY + (FONT_HEIGHT_SMALL - 4); + int numActiveChannels = this->activeChannelIndices.size(); + int totalEntries = numActiveChannels + this->filteredNodes.size(); + int columns = 1; + this->visibleRows = (display->getHeight() - (titleY + FONT_HEIGHT_SMALL)) / (FONT_HEIGHT_SMALL - 4); + if (this->visibleRows < 1) + this->visibleRows = 1; + + // === Clamp scrolling === + if (scrollIndex > totalEntries / columns) + scrollIndex = totalEntries / columns; + if (scrollIndex < 0) + scrollIndex = 0; + + for (int row = 0; row < visibleRows; row++) { + int itemIndex = scrollIndex + row; + if (itemIndex >= totalEntries) + break; + + int xOffset = 0; + int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset; + String entryText; + + // Draw Channels First + if (itemIndex < numActiveChannels) { + uint8_t channelIndex = this->activeChannelIndices[itemIndex]; + entryText = String("@") + String(channels.getName(channelIndex)); + } + // Then Draw Nodes + else { + int nodeIndex = itemIndex - numActiveChannels; + if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { + meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; + if (node) { + entryText = node->is_favorite ? "* " + String(node->user.long_name) : String(node->user.long_name); + } + } + } + + if (entryText.length() == 0 || entryText == "Unknown") + entryText = "?"; + + // === Highlight background (if selected) === + if (itemIndex == destIndex) { + int scrollPadding = 8; // Reserve space for scrollbar + display->fillRect(0, yOffset + 2, display->getWidth() - scrollPadding, FONT_HEIGHT_SMALL - 5); + display->setColor(BLACK); + } + + // === Draw entry text === + display->drawString(xOffset + 2, yOffset, entryText); + display->setColor(WHITE); + + // === Draw key icon (after highlight) === + if (itemIndex >= numActiveChannels) { + int nodeIndex = itemIndex - numActiveChannels; + if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { + meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; + if (node && hasKeyForNode(node)) { + int iconX = display->getWidth() - key_symbol_width - 15; + int iconY = yOffset + (FONT_HEIGHT_SMALL - key_symbol_height) / 2; + + if (itemIndex == destIndex) { + display->setColor(INVERSE); + } else { + display->setColor(WHITE); + } + display->drawXbm(iconX, iconY, key_symbol_width, key_symbol_height, key_symbol); + } + } + } + } + + // Scrollbar + if (totalEntries > visibleRows) { + int scrollbarHeight = visibleRows * (FONT_HEIGHT_SMALL - 4); + int totalScrollable = totalEntries; + int scrollTrackX = display->getWidth() - 6; + display->drawRect(scrollTrackX, rowYOffset, 4, scrollbarHeight); + int scrollHeight = (scrollbarHeight * visibleRows) / totalScrollable; + int scrollPos = rowYOffset + (scrollbarHeight * scrollIndex) / totalScrollable; + display->fillRect(scrollTrackX, scrollPos, 4, scrollHeight); + } +} + void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { this->displayHeight = display->getHeight(); // Store display height for later use @@ -1425,105 +1465,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st } // === Destination Selection === - if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || - this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) { - requestFocus(); - display->setColor(WHITE); // Always draw cleanly - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); - - // === Header === - int titleY = 2; - String titleText = "Select Destination"; - titleText += searchQuery.length() > 0 ? " [" + searchQuery + "]" : " [ ]"; - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(display->getWidth() / 2, titleY, titleText); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - // === List Items === - int rowYOffset = titleY + (FONT_HEIGHT_SMALL - 4); - int numActiveChannels = this->activeChannelIndices.size(); - int totalEntries = numActiveChannels + this->filteredNodes.size(); - int columns = 1; - this->visibleRows = (display->getHeight() - (titleY + FONT_HEIGHT_SMALL)) / (FONT_HEIGHT_SMALL - 4); - if (this->visibleRows < 1) - this->visibleRows = 1; - - // === Clamp scrolling === - if (scrollIndex > totalEntries / columns) - scrollIndex = totalEntries / columns; - if (scrollIndex < 0) - scrollIndex = 0; - - for (int row = 0; row < visibleRows; row++) { - int itemIndex = scrollIndex + row; - if (itemIndex >= totalEntries) - break; - - int xOffset = 0; - int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset; - String entryText; - - // Draw Channels First - if (itemIndex < numActiveChannels) { - uint8_t channelIndex = this->activeChannelIndices[itemIndex]; - entryText = String("@") + String(channels.getName(channelIndex)); - } - // Then Draw Nodes - else { - int nodeIndex = itemIndex - numActiveChannels; - if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { - meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; - if (node) { - entryText = node->is_favorite ? "* " + String(node->user.long_name) : String(node->user.long_name); - } - } - } - - if (entryText.length() == 0 || entryText == "Unknown") - entryText = "?"; - - // === Highlight background (if selected) === - if (itemIndex == destIndex) { - int scrollPadding = 8; // Reserve space for scrollbar - display->fillRect(0, yOffset + 2, display->getWidth() - scrollPadding, FONT_HEIGHT_SMALL - 5); - display->setColor(BLACK); - } - - // === Draw entry text === - display->drawString(xOffset + 2, yOffset, entryText); - display->setColor(WHITE); - - // === Draw key icon (after highlight) === - if (itemIndex >= numActiveChannels) { - int nodeIndex = itemIndex - numActiveChannels; - if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { - meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; - if (node && hasKeyForNode(node)) { - int iconX = display->getWidth() - key_symbol_width - 15; - int iconY = yOffset + (FONT_HEIGHT_SMALL - key_symbol_height) / 2; - - if (itemIndex == destIndex) { - display->setColor(INVERSE); - } else { - display->setColor(WHITE); - } - display->drawXbm(iconX, iconY, key_symbol_width, key_symbol_height, key_symbol); - } - } - } - } - - // Scrollbar - if (totalEntries > visibleRows) { - int scrollbarHeight = visibleRows * (FONT_HEIGHT_SMALL - 4); - int totalScrollable = totalEntries; - int scrollTrackX = display->getWidth() - 6; - display->drawRect(scrollTrackX, rowYOffset, 4, scrollbarHeight); - int scrollHeight = (scrollbarHeight * visibleRows) / totalScrollable; - int scrollPos = rowYOffset + (scrollbarHeight * scrollIndex) / totalScrollable; - display->fillRect(scrollTrackX, scrollPos, 4, scrollHeight); - } + if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + drawDestinationSelectionScreen(display, state, x, y); return; } @@ -1616,7 +1559,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st drawHeader(display, x, y, buffer); // --- Char count right-aligned --- - if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) { + if (runState != CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { uint16_t charsLeft = meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0); snprintf(buffer, sizeof(buffer), "%d left", charsLeft); diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index 563e8b17c..49688a6a4 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -21,11 +21,6 @@ enum cannedMessageModuleRunState { CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION }; -enum cannedMessageDestinationType { - CANNED_MESSAGE_DESTINATION_TYPE_NONE, - CANNED_MESSAGE_DESTINATION_TYPE_NODE, -}; - enum CannedMessageModuleIconType { shift, backspace, space, enter }; #define CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT 50 @@ -74,7 +69,8 @@ class CannedMessageModule : public SinglePortModule, public Observable activeChannelIndices; std::vector filteredNodes; +#if defined(USE_VIRTUAL_KEYBOARD) + bool shift = false; + int charSet = 0; // 0=ABC, 1=123 +#endif + bool isInputSourceAllowed(const InputEvent *event); bool isUpEvent(const InputEvent *event); bool isDownEvent(const InputEvent *event); @@ -263,4 +263,4 @@ class CannedMessageModule : public SinglePortModule, public Observable Date: Sun, 1 Jun 2025 15:54:54 -0500 Subject: [PATCH 199/265] Get rid of Arduino Strings --- src/graphics/Screen.cpp | 86 +++++++++++-------- src/graphics/Screen.h | 6 +- src/graphics/draw/DebugRenderer.cpp | 34 +++++--- src/graphics/draw/NodeListRenderer.cpp | 21 ++--- src/graphics/draw/NodeListRenderer.h | 2 +- src/graphics/draw/NotificationRenderer.cpp | 42 +++++---- src/graphics/draw/UIRenderer.cpp | 44 +++++----- src/modules/CannedMessageModule.cpp | 45 +++++----- src/modules/SerialModule.cpp | 70 +++++++++------ src/modules/Telemetry/HealthTelemetry.cpp | 25 ++++-- src/modules/Telemetry/PowerTelemetry.cpp | 8 +- src/modules/Telemetry/Sensor/BME680Sensor.cpp | 10 +-- src/modules/Telemetry/Sensor/BME680Sensor.h | 2 +- src/nimble/NimbleBluetooth.cpp | 8 +- src/platform/esp32/WiFiOTA.cpp | 8 +- src/platform/esp32/WiFiOTA.h | 2 +- 16 files changed, 237 insertions(+), 176 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 474a000d5..be6d22a27 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -80,9 +80,6 @@ using graphics::numEmotes; using namespace meshtastic; /** @todo remove */ -String alertBannerMessage = ""; -uint32_t alertBannerUntil = 0; - namespace graphics { @@ -97,8 +94,12 @@ namespace graphics // A text message frame + debug frame + all the node infos FrameCallback *normalFrames; static uint32_t targetFramerate = IDLE_FRAMERATE; -static String alertBannerMessage; -static uint32_t alertBannerUntil = 0; +// Global variables for alert banner - explicitly define with extern "C" linkage to prevent optimization +extern "C" { +static char alertBannerBuffer[256] = ""; +char *alertBannerMessage = alertBannerBuffer; +uint32_t alertBannerUntil = 0; +} uint32_t logo_timeout = 5000; // 4 seconds for EACH logo @@ -146,10 +147,11 @@ extern bool hasUnreadMessage; // The banner appears in the center of the screen and disappears after the specified duration // Called to trigger a banner with custom message and duration -void Screen::showOverlayBanner(const String &message, uint32_t durationMs) +void Screen::showOverlayBanner(const char *message, uint32_t durationMs) { // Store the message and set the expiration timestamp - alertBannerMessage = message; + strncpy(alertBannerMessage, message, 255); + alertBannerMessage[255] = '\0'; // Ensure null termination alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; } @@ -225,7 +227,8 @@ void Screen::drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *sta UIRenderer::drawBattery(display, x, y + 7, imgBattery, powerStatus); if (powerStatus->getHasBattery()) { - String batteryPercent = String(powerStatus->getBatteryChargePercent()) + "%"; + char batteryPercent[8]; + snprintf(batteryPercent, sizeof(batteryPercent), "%d%%", powerStatus->getBatteryChargePercent()); display->setFont(FONT_SMALL); @@ -255,16 +258,13 @@ void Screen::drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *sta hour = 12; } - // hours string - String hourString = String(hour); + // Format time string + char timeString[16]; + snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute); - // minutes string - String minuteString = minute < 10 ? "0" + String(minute) : String(minute); - - String timeString = hourString + ":" + minuteString; - - // seconds string - String secondString = second < 10 ? "0" + String(second) : String(second); + // Format seconds string + char secondString[8]; + snprintf(secondString, sizeof(secondString), "%02d", second); float scale = 1.5; @@ -272,12 +272,12 @@ void Screen::drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *sta uint16_t segmentHeight = SEGMENT_HEIGHT * scale; // calculate hours:minutes string width - uint16_t timeStringWidth = timeString.length() * 5; + uint16_t timeStringWidth = strlen(timeString) * 5; - for (uint8_t i = 0; i < timeString.length(); i++) { - String character = String(timeString[i]); + for (uint8_t i = 0; i < strlen(timeString); i++) { + char character = timeString[i]; - if (character == ":") { + if (character == ':') { timeStringWidth += segmentHeight; } else { timeStringWidth += segmentWidth + (segmentHeight * 2) + 4; @@ -285,7 +285,7 @@ void Screen::drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *sta } // calculate seconds string width - uint16_t secondStringWidth = (secondString.length() * 12) + 4; + uint16_t secondStringWidth = (strlen(secondString) * 12) + 4; // sum these to get total string width uint16_t totalWidth = timeStringWidth + secondStringWidth; @@ -297,15 +297,15 @@ void Screen::drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *sta uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2); // iterate over characters in hours:minutes string and draw segmented characters - for (uint8_t i = 0; i < timeString.length(); i++) { - String character = String(timeString[i]); + for (uint8_t i = 0; i < strlen(timeString); i++) { + char character = timeString[i]; - if (character == ":") { + if (character == ':') { drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale); hourMinuteTextX += segmentHeight + 6; } else { - drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character.toInt(), scale); + drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character - '0', scale); hourMinuteTextX += segmentWidth + (segmentHeight * 2) + 4; } @@ -457,7 +457,8 @@ void Screen::drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *stat UIRenderer::drawBattery(display, x, y + 7, imgBattery, powerStatus); if (powerStatus->getHasBattery()) { - String batteryPercent = String(powerStatus->getBatteryChargePercent()) + "%"; + char batteryPercent[8]; + snprintf(batteryPercent, sizeof(batteryPercent), "%d%%", powerStatus->getBatteryChargePercent()); display->setFont(FONT_SMALL); @@ -1711,21 +1712,30 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet) const char *longName = (node && node->has_user) ? node->user.long_name : nullptr; const char *msgRaw = reinterpret_cast(packet->decoded.payload.bytes); - String msg = String(msgRaw); - msg.trim(); // Remove leading/trailing whitespace/newlines - String banner; + char banner[256]; - // Match bell character or exact alert text - if (msg.indexOf("\x07") != -1) { - banner = "Alert Received"; - } else { - banner = "New Message"; + // Check for bell character in message to determine alert type + bool isAlert = false; + for (size_t i = 0; i < packet->decoded.payload.size && i < 100; i++) { + if (msgRaw[i] == '\x07') { + isAlert = true; + break; + } } - if (longName && longName[0]) { - banner += "\nfrom "; - banner += longName; + if (isAlert) { + if (longName && longName[0]) { + snprintf(banner, sizeof(banner), "Alert Received\nfrom %s", longName); + } else { + strcpy(banner, "Alert Received"); + } + } else { + if (longName && longName[0]) { + snprintf(banner, sizeof(banner), "New Message\nfrom %s", longName); + } else { + strcpy(banner, "New Message"); + } } screen->showOverlayBanner(banner, 3000); diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 0d791e463..5f74f12aa 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -279,7 +279,7 @@ class Screen : public concurrency::OSThread enqueueCmd(cmd); } - void showOverlayBanner(const String &message, uint32_t durationMs = 3000); + void showOverlayBanner(const char *message, uint32_t durationMs = 3000); void startFirmwareUpdateScreen() { @@ -698,8 +698,10 @@ class Screen : public concurrency::OSThread } // namespace graphics -extern String alertBannerMessage; +extern "C" { +extern char *alertBannerMessage; extern uint32_t alertBannerUntil; +} // Extern declarations for function symbols used in UIRenderer extern std::vector functionSymbol; diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 00aabedfb..54fba7618 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -181,19 +181,19 @@ void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, i } if (WiFi.status() != WL_CONNECTED) { - display->drawString(x, y, String("WiFi: Not Connected")); + display->drawString(x, y, "WiFi: Not Connected"); if (config.display.heading_bold) - display->drawString(x + 1, y, String("WiFi: Not Connected")); + display->drawString(x + 1, y, "WiFi: Not Connected"); } else { - display->drawString(x, y, String("WiFi: Connected")); + display->drawString(x, y, "WiFi: Connected"); if (config.display.heading_bold) - display->drawString(x + 1, y, String("WiFi: Connected")); + display->drawString(x + 1, y, "WiFi: Connected"); - display->drawString(x + SCREEN_WIDTH - display->getStringWidth("RSSI " + String(WiFi.RSSI())), y, - "RSSI " + String(WiFi.RSSI())); + char rssiStr[32]; + snprintf(rssiStr, sizeof(rssiStr), "RSSI %d", WiFi.RSSI()); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(rssiStr), y, rssiStr); if (config.display.heading_bold) { - display->drawString(x + SCREEN_WIDTH - display->getStringWidth("RSSI " + String(WiFi.RSSI())) - 1, y, - "RSSI " + String(WiFi.RSSI())); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(rssiStr) - 1, y, rssiStr); } } @@ -212,7 +212,9 @@ void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, i */ if (WiFi.status() == WL_CONNECTED) { - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "IP: " + String(WiFi.localIP().toString().c_str())); + char ipStr[64]; + snprintf(ipStr, sizeof(ipStr), "IP: %s", WiFi.localIP().toString().c_str()); + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, ipStr); } else if (WiFi.status() == WL_NO_SSID_AVAIL) { display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "SSID Not Found"); } else if (WiFi.status() == WL_CONNECTION_LOST) { @@ -231,11 +233,15 @@ void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, i } #else else { - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Unkown status: " + String(WiFi.status())); + char statusStr[32]; + snprintf(statusStr, sizeof(statusStr), "Unknown status: %d", WiFi.status()); + display->drawString(x, y + FONT_HEIGHT_SMALL * 1, statusStr); } #endif - display->drawString(x, y + FONT_HEIGHT_SMALL * 2, "SSID: " + String(wifiName)); + char ssidStr[64]; + snprintf(ssidStr, sizeof(ssidStr), "SSID: %s", wifiName); + display->drawString(x, y + FONT_HEIGHT_SMALL * 2, ssidStr); display->drawString(x, y + FONT_HEIGHT_SMALL * 3, "http://meshtastic.local"); @@ -274,9 +280,9 @@ void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t display->drawString(x + 1, y, batStr); } else { // Line 1 - display->drawString(x, y, String("USB")); + display->drawString(x, y, "USB"); if (config.display.heading_bold) - display->drawString(x + 1, y, String("USB")); + display->drawString(x + 1, y, "USB"); } // auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, true); @@ -416,7 +422,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->setTextAlignment(TEXT_ALIGN_LEFT); // === First Row: Region / BLE Name === - graphics::UIRenderer::drawNodes(display, x, compactFirstLine + 3, nodeStatus, 0, true); + graphics::UIRenderer::drawNodes(display, x, compactFirstLine + 3, nodeStatus, 0, true, ""); uint8_t dmac[6]; char shortnameble[35]; diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index 4385e0f1b..bb7556f02 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -49,9 +49,9 @@ static int scrollIndex = 0; // Utility Functions // ============================= -String getSafeNodeName(meshtastic_NodeInfoLite *node) +const char *getSafeNodeName(meshtastic_NodeInfoLite *node) { - String nodeName = "?"; + static char nodeName[16] = "?"; if (node->has_user && strlen(node->user.short_name) > 0) { bool valid = true; const char *name = node->user.short_name; @@ -63,12 +63,13 @@ String getSafeNodeName(meshtastic_NodeInfoLite *node) } } if (valid) { - nodeName = name; + strncpy(nodeName, name, sizeof(nodeName) - 1); + nodeName[sizeof(nodeName) - 1] = '\0'; } else { - char idStr[6]; - snprintf(idStr, sizeof(idStr), "%04X", (uint16_t)(node->num & 0xFFFF)); - nodeName = String(idStr); + snprintf(nodeName, sizeof(nodeName), "%04X", (uint16_t)(node->num & 0xFFFF)); } + } else { + strcpy(nodeName, "?"); } return nodeName; } @@ -181,7 +182,7 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int bool isLeftCol = (x < SCREEN_WIDTH / 2); int timeOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7); - String nodeName = getSafeNodeName(node); + const char *nodeName = getSafeNodeName(node); char timeStr[10]; uint32_t seconds = sinceLastSeen(node); @@ -224,7 +225,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int int barsXOffset = columnWidth - barsOffset; - String nodeName = getSafeNodeName(node); + const char *nodeName = getSafeNodeName(node); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); @@ -268,7 +269,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 bool isLeftCol = (x < SCREEN_WIDTH / 2); int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); - String nodeName = getSafeNodeName(node); + const char *nodeName = getSafeNodeName(node); char distStr[10] = ""; meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); @@ -363,7 +364,7 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 // Adjust max text width depending on column and screen width int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); - String nodeName = getSafeNodeName(node); + const char *nodeName = getSafeNodeName(node); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h index d35350cb8..aa92e34ea 100644 --- a/src/graphics/draw/NodeListRenderer.h +++ b/src/graphics/draw/NodeListRenderer.h @@ -59,7 +59,7 @@ void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, in // Utility functions const char *getCurrentModeTitle(int screenWidth); void retrieveAndSortNodes(std::vector &nodeList); -String getSafeNodeName(meshtastic_NodeInfoLite *node); +const char *getSafeNodeName(meshtastic_NodeInfoLite *node); uint32_t sinceLastSeen(meshtastic_NodeInfoLite *node); void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 6d8423adc..18c1c0d9f 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -2,6 +2,7 @@ #include "DisplayFormatters.h" #include "NodeDB.h" #include "configuration.h" +#include "graphics/Screen.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" @@ -17,8 +18,6 @@ using namespace meshtastic; // External references to global variables from Screen.cpp -extern String alertBannerMessage; -extern uint32_t alertBannerUntil; extern std::vector functionSymbol; extern std::string functionSymbolString; extern bool hasUnreadMessage; @@ -77,7 +76,7 @@ void NotificationRenderer::drawWelcomeScreen(OLEDDisplay *display, OLEDDisplayUi void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) { // Exit if no message is active or duration has passed - if (alertBannerMessage.length() == 0 || (alertBannerUntil != 0 && millis() > alertBannerUntil)) + if (strlen(alertBannerMessage) == 0 || (alertBannerUntil != 0 && millis() > alertBannerUntil)) return; // === Layout Configuration === @@ -85,28 +84,37 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp constexpr uint8_t lineSpacing = 1; // Extra space between lines // Search the message to determine if we need the bell added - bool needs_bell = (alertBannerMessage.indexOf("Alert Received") != -1); + bool needs_bell = (strstr(alertBannerMessage, "Alert Received") != nullptr); // Setup font and alignment display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); // We will manually center per line // === Split the message into lines (supports multi-line banners) === - std::vector lines; - int start = 0, newlineIdx; - while ((newlineIdx = alertBannerMessage.indexOf('\n', start)) != -1) { - lines.push_back(alertBannerMessage.substring(start, newlineIdx)); - start = newlineIdx + 1; + const int MAX_LINES = 10; + char lines[MAX_LINES][256]; + int lineCount = 0; + + // Create a working copy of the message to tokenize + char messageCopy[256]; + strncpy(messageCopy, alertBannerMessage, sizeof(messageCopy) - 1); + messageCopy[sizeof(messageCopy) - 1] = '\0'; + + char *line = strtok(messageCopy, "\n"); + while (line != nullptr && lineCount < MAX_LINES) { + strncpy(lines[lineCount], line, sizeof(lines[lineCount]) - 1); + lines[lineCount][sizeof(lines[lineCount]) - 1] = '\0'; + lineCount++; + line = strtok(nullptr, "\n"); } - lines.push_back(alertBannerMessage.substring(start)); // === Measure text dimensions === uint16_t minWidth = (SCREEN_WIDTH > 128) ? 106 : 78; uint16_t maxWidth = 0; - std::vector lineWidths; - for (const auto &line : lines) { - uint16_t w = display->getStringWidth(line.c_str(), line.length(), true); - lineWidths.push_back(w); + uint16_t lineWidths[MAX_LINES]; + for (int i = 0; i < lineCount; i++) { + uint16_t w = display->getStringWidth(lines[i], strlen(lines[i]), true); + lineWidths[i] = w; if (w > maxWidth) maxWidth = w; } @@ -115,7 +123,7 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp if (needs_bell && boxWidth < minWidth) boxWidth += (SCREEN_WIDTH > 128) ? 26 : 20; - uint16_t boxHeight = padding * 2 + lines.size() * FONT_HEIGHT_SMALL + (lines.size() - 1) * lineSpacing; + uint16_t boxHeight = padding * 2 + lineCount * FONT_HEIGHT_SMALL + (lineCount - 1) * lineSpacing; int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); int16_t boxTop = (display->height() / 2) - (boxHeight / 2); @@ -128,9 +136,9 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp // === Draw each line centered in the box === int16_t lineY = boxTop + padding; - for (size_t i = 0; i < lines.size(); ++i) { + for (int i = 0; i < lineCount; i++) { int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; - uint16_t line_width = display->getStringWidth(lines[i].c_str(), lines[i].length(), true); + uint16_t line_width = display->getStringWidth(lines[i], strlen(lines[i]), true); if (needs_bell && i == 0) { int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2; diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index f8524c1d1..becc5347d 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -15,6 +15,7 @@ #include "target_specific.h" #include #include +#include #if !MESHTASTIC_EXCLUDE_GPS @@ -104,7 +105,7 @@ void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSSt // Draw status when GPS is disabled or not present void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) { - String displayLine; + const char *displayLine; int pos; if (y < FONT_HEIGHT_SMALL) { // Line 1: use short string displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; @@ -119,7 +120,7 @@ void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshta void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) { - String displayLine = ""; + char displayLine[32]; if (!gps->getIsConnected() && !config.position.fixed_position) { // displayLine = "No GPS Module"; // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); @@ -128,9 +129,10 @@ void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtasti // display->drawString(x + (SCREEN_WIDTH - (display->getStringWidth(displayLine))) / 2, y, displayLine); } else { geoCoord.updateCoords(int32_t(gps->getLatitude()), int32_t(gps->getLongitude()), int32_t(gps->getAltitude())); - displayLine = "Altitude: " + String(geoCoord.getAltitude()) + "m"; if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) - displayLine = "Altitude: " + String(geoCoord.getAltitude() * METERS_TO_FEET) + "ft"; + snprintf(displayLine, sizeof(displayLine), "Altitude: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); + else + snprintf(displayLine, sizeof(displayLine), "Altitude: %.0fm", geoCoord.getAltitude()); display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); } } @@ -139,13 +141,13 @@ void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtasti void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) { auto gpsFormat = config.display.gps_format; - String displayLine = ""; + char displayLine[32]; if (!gps->getIsConnected() && !config.position.fixed_position) { - displayLine = "No GPS present"; + strcpy(displayLine, "No GPS present"); display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); } else if (!gps->getHasLock() && !config.position.fixed_position) { - displayLine = "No GPS Lock"; + strcpy(displayLine, "No GPS Lock"); display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); } else { @@ -260,10 +262,10 @@ void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::Nod #endif display->drawString(x + 10, y - 2, usersString); int string_offset = (SCREEN_WIDTH > 128) ? 2 : 1; - if (additional_words != "") { - display->drawString(x + 10 + display->getStringWidth(usersString) + string_offset, y - 2, additional_words); + if (additional_words.length() > 0) { + display->drawString(x + 10 + display->getStringWidth(usersString) + string_offset, y - 2, additional_words.c_str()); if (config.display.heading_bold) - display->drawString(x + 11 + display->getStringWidth(usersString) + string_offset, y - 2, additional_words); + display->drawString(x + 11 + display->getStringWidth(usersString) + string_offset, y - 2, additional_words.c_str()); } } @@ -616,7 +618,7 @@ void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t #if HAS_GPS if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { - String displayLine = ""; + const char *displayLine; if (config.position.fixed_position) { displayLine = "Fixed GPS"; } else { @@ -644,8 +646,7 @@ void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t } else { display->drawString( x + SCREEN_WIDTH - display->getStringWidth("USB"), - ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), - String("USB")); + ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), "USB"); } config.display.heading_bold = origBold; @@ -975,9 +976,9 @@ void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *stat bool origBold = config.display.heading_bold; config.display.heading_bold = false; - String Satelite_String = "Sat:"; + const char *Satelite_String = "Sat:"; display->drawString(0, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), Satelite_String); - String displayLine = ""; + const char *displayLine = ""; // Initialize to empty string by default if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { if (config.position.fixed_position) { displayLine = "Fixed GPS"; @@ -987,6 +988,7 @@ void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *stat display->drawString(display->getStringWidth(Satelite_String) + 3, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), displayLine); } else { + displayLine = "GPS enabled"; // Set a value when GPS is enabled UIRenderer::drawGps(display, display->getStringWidth(Satelite_String) + 3, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine) + 3, gpsStatus); } @@ -1010,13 +1012,15 @@ void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *stat } // If GPS is off, no need to display these parts - if (displayLine != "GPS off" && displayLine != "No GPS") { + if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) { // === Second Row: Altitude === - String displayLine; - displayLine = " Alt: " + String(geoCoord.getAltitude()) + "m"; - if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) - displayLine = " Alt: " + String(geoCoord.getAltitude() * METERS_TO_FEET) + "ft"; + char displayLine[32]; + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + snprintf(displayLine, sizeof(displayLine), " Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); + } else { + snprintf(displayLine, sizeof(displayLine), " Alt: %.0fm", geoCoord.getAltitude()); + } display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine), displayLine); // === Third Row: Latitude === diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 99813cba6..254e417f9 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -11,6 +11,7 @@ #include "PowerFSM.h" // needed for button bypass #include "SPILock.h" #include "detect/ScanI2C.h" +#include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" #include "input/ScanAndSelect.h" @@ -720,9 +721,7 @@ bool CannedMessageModule::handleSystemCommandInput(const InputEvent *event) return false; // Block ALL input if an alert banner is active - extern String alertBannerMessage; - extern uint32_t alertBannerUntil; - if (alertBannerMessage.length() > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil)) { + if (strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil)) { return true; } @@ -1381,28 +1380,32 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O if (itemIndex >= totalEntries) break; - int xOffset = 0; - int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset; - String entryText; + int xOffset = 0; + int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset; + char entryText[64]; - // Draw Channels First - if (itemIndex < numActiveChannels) { - uint8_t channelIndex = this->activeChannelIndices[itemIndex]; - entryText = String("@") + String(channels.getName(channelIndex)); - } - // Then Draw Nodes - else { - int nodeIndex = itemIndex - numActiveChannels; - if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { - meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; - if (node) { - entryText = node->is_favorite ? "* " + String(node->user.long_name) : String(node->user.long_name); + // Draw Channels First + if (itemIndex < numActiveChannels) { + uint8_t channelIndex = this->activeChannelIndices[itemIndex]; + snprintf(entryText, sizeof(entryText), "@%s", channels.getName(channelIndex)); + } + // Then Draw Nodes + else { + int nodeIndex = itemIndex - numActiveChannels; + if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { + meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; + if (node) { + if (node->is_favorite) { + snprintf(entryText, sizeof(entryText), "* %s", node->user.long_name); + } else { + snprintf(entryText, sizeof(entryText), "%s", node->user.long_name); + } + } } } - } - if (entryText.length() == 0 || entryText == "Unknown") - entryText = "?"; + if (strlen(entryText) == 0 || strcmp(entryText, "Unknown") == 0) + strcpy(entryText, "?"); // === Highlight background (if selected) === if (itemIndex == destIndex) { diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index 8d280581c..f3921ef19 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -341,7 +341,7 @@ ProcessMessage SerialModuleRadio::handleReceived(const meshtastic_MeshPacket &mp serialPrint->write(p.payload.bytes, p.payload.size); } else if (moduleConfig.serial.mode == meshtastic_ModuleConfig_SerialConfig_Serial_Mode_TEXTMSG) { meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(getFrom(&mp)); - String sender = (node && node->has_user) ? node->user.short_name : "???"; + const char *sender = (node && node->has_user) ? node->user.short_name : "???"; serialPrint->println(); serialPrint->printf("%s: %s", sender, p.payload.bytes); serialPrint->println(); @@ -410,8 +410,8 @@ uint32_t SerialModule::getBaudRate() // Add this structure to help with parsing WindGust = 24.4 serial lines. struct ParsedLine { - String name; - String value; + char name[64]; + char value[128]; }; /** @@ -438,16 +438,30 @@ ParsedLine parseLine(const char *line) strncpy(nameBuf, line, nameLen); nameBuf[nameLen] = '\0'; - // Create trimmed name string - String name = String(nameBuf); - name.trim(); + // Trim whitespace from name + char *nameStart = nameBuf; + while (*nameStart && isspace(*nameStart)) + nameStart++; + char *nameEnd = nameStart + strlen(nameStart) - 1; + while (nameEnd > nameStart && isspace(*nameEnd)) + *nameEnd-- = '\0'; - // Extract value after equals sign - String value = String(equals + 1); - value.trim(); + // Copy trimmed name + strncpy(result.name, nameStart, sizeof(result.name) - 1); + result.name[sizeof(result.name) - 1] = '\0'; + + // Extract value part (after equals) + const char *valueStart = equals + 1; + while (*valueStart && isspace(*valueStart)) + valueStart++; + strncpy(result.value, valueStart, sizeof(result.value) - 1); + result.value[sizeof(result.value) - 1] = '\0'; + + // Trim trailing whitespace from value + char *valueEnd = result.value + strlen(result.value) - 1; + while (valueEnd > result.value && isspace(*valueEnd)) + *valueEnd-- = '\0'; - result.name = name; - result.value = value; return result; } @@ -517,16 +531,16 @@ void SerialModule::processWXSerial() memcpy(line, &serialBytes[lineStart], lineEnd - lineStart); ParsedLine parsed = parseLine(line); - if (parsed.name.length() > 0) { - if (parsed.name == "WindDir") { - strlcpy(windDir, parsed.value.c_str(), sizeof(windDir)); + if (strlen(parsed.name) > 0) { + if (strcmp(parsed.name, "WindDir") == 0) { + strlcpy(windDir, parsed.value, sizeof(windDir)); double radians = GeoCoord::toRadians(strtof(windDir, nullptr)); dir_sum_sin += sin(radians); dir_sum_cos += cos(radians); dirCount++; gotwind = true; - } else if (parsed.name == "WindSpeed") { - strlcpy(windVel, parsed.value.c_str(), sizeof(windVel)); + } else if (strcmp(parsed.name, "WindSpeed") == 0) { + strlcpy(windVel, parsed.value, sizeof(windVel)); float newv = strtof(windVel, nullptr); velSum += newv; velCount++; @@ -534,28 +548,28 @@ void SerialModule::processWXSerial() lull = newv; } gotwind = true; - } else if (parsed.name == "WindGust") { - strlcpy(windGust, parsed.value.c_str(), sizeof(windGust)); + } else if (strcmp(parsed.name, "WindGust") == 0) { + strlcpy(windGust, parsed.value, sizeof(windGust)); float newg = strtof(windGust, nullptr); if (newg > gust) { gust = newg; } gotwind = true; - } else if (parsed.name == "BatVoltage") { - strlcpy(batVoltage, parsed.value.c_str(), sizeof(batVoltage)); + } else if (strcmp(parsed.name, "BatVoltage") == 0) { + strlcpy(batVoltage, parsed.value, sizeof(batVoltage)); batVoltageF = strtof(batVoltage, nullptr); break; // last possible data we want so break - } else if (parsed.name == "CapVoltage") { - strlcpy(capVoltage, parsed.value.c_str(), sizeof(capVoltage)); + } else if (strcmp(parsed.name, "CapVoltage") == 0) { + strlcpy(capVoltage, parsed.value, sizeof(capVoltage)); capVoltageF = strtof(capVoltage, nullptr); - } else if (parsed.name == "GXTS04Temp" || parsed.name == "Temperature") { - strlcpy(temperature, parsed.value.c_str(), sizeof(temperature)); + } else if (strcmp(parsed.name, "GXTS04Temp") == 0 || strcmp(parsed.name, "Temperature") == 0) { + strlcpy(temperature, parsed.value, sizeof(temperature)); temperatureF = strtof(temperature, nullptr); - } else if (parsed.name == "RainIntSum") { - strlcpy(rainStr, parsed.value.c_str(), sizeof(rainStr)); + } else if (strcmp(parsed.name, "RainIntSum") == 0) { + strlcpy(rainStr, parsed.value, sizeof(rainStr)); rainSum = int(strtof(rainStr, nullptr)); - } else if (parsed.name == "Rain") { - strlcpy(rainStr, parsed.value.c_str(), sizeof(rainStr)); + } else if (strcmp(parsed.name, "Rain") == 0) { + strlcpy(rainStr, parsed.value, sizeof(rainStr)); rain = strtof(rainStr, nullptr); } } diff --git a/src/modules/Telemetry/HealthTelemetry.cpp b/src/modules/Telemetry/HealthTelemetry.cpp index a2a18ba03..3a735b1fa 100644 --- a/src/modules/Telemetry/HealthTelemetry.cpp +++ b/src/modules/Telemetry/HealthTelemetry.cpp @@ -118,22 +118,31 @@ void HealthTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState * } // Display "Health From: ..." on its own - display->drawString(x, y, "Health From: " + String(lastSender) + "(" + String(agoSecs) + "s)"); + char headerStr[64]; + snprintf(headerStr, sizeof(headerStr), "Health From: %s(%ds)", lastSender, (int)agoSecs); + display->drawString(x, y, headerStr); - String last_temp = String(lastMeasurement.variant.health_metrics.temperature, 0) + "Β°C"; + char last_temp[16]; if (moduleConfig.telemetry.environment_display_fahrenheit) { - last_temp = String(UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.health_metrics.temperature), 0) + "Β°F"; + snprintf(last_temp, sizeof(last_temp), "%.0fΒ°F", + UnitConversions::CelsiusToFahrenheit(lastMeasurement.variant.health_metrics.temperature)); + } else { + snprintf(last_temp, sizeof(last_temp), "%.0fΒ°C", lastMeasurement.variant.health_metrics.temperature); } // Continue with the remaining details - display->drawString(x, y += _fontHeight(FONT_SMALL), "Temp: " + last_temp); + char tempStr[32]; + snprintf(tempStr, sizeof(tempStr), "Temp: %s", last_temp); + display->drawString(x, y += _fontHeight(FONT_SMALL), tempStr); if (lastMeasurement.variant.health_metrics.has_heart_bpm) { - display->drawString(x, y += _fontHeight(FONT_SMALL), - "Heart Rate: " + String(lastMeasurement.variant.health_metrics.heart_bpm, 0) + " bpm"); + char heartStr[32]; + snprintf(heartStr, sizeof(heartStr), "Heart Rate: %.0f bpm", lastMeasurement.variant.health_metrics.heart_bpm); + display->drawString(x, y += _fontHeight(FONT_SMALL), heartStr); } if (lastMeasurement.variant.health_metrics.has_spO2) { - display->drawString(x, y += _fontHeight(FONT_SMALL), - "spO2: " + String(lastMeasurement.variant.health_metrics.spO2, 0) + " %"); + char spo2Str[32]; + snprintf(spo2Str, sizeof(spo2Str), "spO2: %.0f %%", lastMeasurement.variant.health_metrics.spO2); + display->drawString(x, y += _fontHeight(FONT_SMALL), spo2Str); } } diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index e42d718a5..e635b022d 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -152,14 +152,18 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s } // Display "Pow. From: ..." - display->drawString(x, compactFirstLine, "Pow. From: " + String(lastSender) + " (" + String(agoSecs) + "s)"); + char fromStr[64]; + snprintf(fromStr, sizeof(fromStr), "Pow. From: %s (%ds)", lastSender, agoSecs); + display->drawString(x, compactFirstLine, fromStr); // Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags const auto &m = lastMeasurement.variant.power_metrics; int lineY = compactSecondLine; auto drawLine = [&](const char *label, float voltage, float current) { - display->drawString(x, lineY, String(label) + ": " + String(voltage, 2) + "V " + String(current, 0) + "mA"); + char lineStr[64]; + snprintf(lineStr, sizeof(lineStr), "%s: %.2fV %.0fmA", label, voltage, current); + display->drawString(x, lineY, lineStr); lineY += _fontHeight(FONT_SMALL); }; diff --git a/src/modules/Telemetry/Sensor/BME680Sensor.cpp b/src/modules/Telemetry/Sensor/BME680Sensor.cpp index 0e0212bc5..fce029deb 100644 --- a/src/modules/Telemetry/Sensor/BME680Sensor.cpp +++ b/src/modules/Telemetry/Sensor/BME680Sensor.cpp @@ -137,17 +137,17 @@ void BME680Sensor::updateState() #endif } -void BME680Sensor::checkStatus(String functionName) +void BME680Sensor::checkStatus(const char *functionName) { if (bme680.status < BSEC_OK) - LOG_ERROR("%s BSEC2 code: %s", functionName.c_str(), String(bme680.status).c_str()); + LOG_ERROR("%s BSEC2 code: %d", functionName, bme680.status); else if (bme680.status > BSEC_OK) - LOG_WARN("%s BSEC2 code: %s", functionName.c_str(), String(bme680.status).c_str()); + LOG_WARN("%s BSEC2 code: %d", functionName, bme680.status); if (bme680.sensor.status < BME68X_OK) - LOG_ERROR("%s BME68X code: %s", functionName.c_str(), String(bme680.sensor.status).c_str()); + LOG_ERROR("%s BME68X code: %d", functionName, bme680.sensor.status); else if (bme680.sensor.status > BME68X_OK) - LOG_WARN("%s BME68X code: %s", functionName.c_str(), String(bme680.sensor.status).c_str()); + LOG_WARN("%s BME68X code: %d", functionName, bme680.sensor.status); } #endif diff --git a/src/modules/Telemetry/Sensor/BME680Sensor.h b/src/modules/Telemetry/Sensor/BME680Sensor.h index 249c4b3e7..ce1fa4f3b 100644 --- a/src/modules/Telemetry/Sensor/BME680Sensor.h +++ b/src/modules/Telemetry/Sensor/BME680Sensor.h @@ -34,7 +34,7 @@ class BME680Sensor : public TelemetrySensor BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY}; void loadState(); void updateState(); - void checkStatus(String functionName); + void checkStatus(const char *functionName); public: BME680Sensor(); diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 80787092d..ef834db37 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -109,14 +109,14 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks display->drawString(x_offset + x, y_offset + y, "Enter this code"); display->setFont(FONT_LARGE); - String displayPin(btPIN); - String pin = displayPin.substring(0, 3) + " " + displayPin.substring(3, 6); + char pin[8]; + snprintf(pin, sizeof(pin), "%.3s %.3s", btPIN, btPIN + 3); y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5; display->drawString(x_offset + x, y_offset + y, pin); display->setFont(FONT_SMALL); - String deviceName = "Name: "; - deviceName.concat(getDeviceName()); + char deviceName[64]; + snprintf(deviceName, sizeof(deviceName), "Name: %s", getDeviceName()); y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5; display->drawString(x_offset + x, y_offset + y, deviceName); }); diff --git a/src/platform/esp32/WiFiOTA.cpp b/src/platform/esp32/WiFiOTA.cpp index eac124dda..4cf157b4c 100644 --- a/src/platform/esp32/WiFiOTA.cpp +++ b/src/platform/esp32/WiFiOTA.cpp @@ -80,13 +80,13 @@ bool trySwitchToOTA() return true; } -String getVersion() +const char *getVersion() { const esp_partition_t *part = getAppPartition(); - esp_app_desc_t app_desc; + static esp_app_desc_t app_desc; if (!getAppDesc(part, &app_desc)) - return String(); - return String(app_desc.version); + return ""; + return app_desc.version; } } // namespace WiFiOTA diff --git a/src/platform/esp32/WiFiOTA.h b/src/platform/esp32/WiFiOTA.h index 61860ed5e..5a7ee348a 100644 --- a/src/platform/esp32/WiFiOTA.h +++ b/src/platform/esp32/WiFiOTA.h @@ -12,7 +12,7 @@ bool isUpdated(); void recoverConfig(meshtastic_Config_NetworkConfig *network); void saveConfig(meshtastic_Config_NetworkConfig *network); bool trySwitchToOTA(); -String getVersion(); +const char *getVersion(); } // namespace WiFiOTA #endif // WIFIOTA_H From 6f6ee8c06ae639002be36d42c58cab20ed893f05 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sun, 1 Jun 2025 22:47:44 -0500 Subject: [PATCH 200/265] Clean up after Copilot --- src/graphics/Screen.cpp | 13 +--- src/graphics/Screen.h | 12 +-- src/graphics/draw/DebugRenderer.cpp | 40 +++++----- src/graphics/draw/NotificationRenderer.cpp | 57 +++++++------- src/graphics/draw/UIRenderer.cpp | 5 +- src/modules/CannedMessageModule.cpp | 90 +++++++++++----------- 6 files changed, 97 insertions(+), 120 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index be6d22a27..33734116f 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -95,11 +95,6 @@ namespace graphics FrameCallback *normalFrames; static uint32_t targetFramerate = IDLE_FRAMERATE; // Global variables for alert banner - explicitly define with extern "C" linkage to prevent optimization -extern "C" { -static char alertBannerBuffer[256] = ""; -char *alertBannerMessage = alertBannerBuffer; -uint32_t alertBannerUntil = 0; -} uint32_t logo_timeout = 5000; // 4 seconds for EACH logo @@ -117,12 +112,6 @@ std::vector moduleFrames; std::vector functionSymbol; std::string functionSymbolString; -// Stores the last 4 of our hardware ID, to make finding the device for pairing easier -// FIXME: Needs refactoring and getMacAddr needs to be moved to a utility class -extern "C" { -char ourId[5]; -} - #if HAS_GPS // GeoCoord object for the screen GeoCoord geoCoord; @@ -1006,7 +995,7 @@ void Screen::setup() // === Generate device ID from MAC address === uint8_t dmac[6]; getMacAddr(dmac); - snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); + snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); #if ARCH_PORTDUINO handleSetOn(false); // Ensure proper init for Arduino targets diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 5f74f12aa..0175b7721 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -220,6 +220,13 @@ class Screen : public concurrency::OSThread meshtastic_Config_DisplayConfig_OledType model; OLEDDISPLAY_GEOMETRY geometry; + char alertBannerMessage[256] = {0}; + uint32_t alertBannerUntil = 0; + + // Stores the last 4 of our hardware ID, to make finding the device for pairing easier + // FIXME: Needs refactoring and getMacAddr needs to be moved to a utility class + char ourId[5]; + /// Initializes the UI, turns on the display, starts showing boot screen. // // Not thread safe - must be called before any other methods are called. @@ -698,11 +705,6 @@ class Screen : public concurrency::OSThread } // namespace graphics -extern "C" { -extern char *alertBannerMessage; -extern uint32_t alertBannerUntil; -} - // Extern declarations for function symbols used in UIRenderer extern std::vector functionSymbol; extern std::string functionSymbolString; diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 54fba7618..b4d90ebb3 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -41,9 +41,6 @@ extern PowerStatus *powerStatus; extern NodeStatus *nodeStatus; extern GPSStatus *gpsStatus; extern Channels channels; -extern "C" { -extern char ourId[5]; -} extern AirTime *airTime; // External functions from Screen.cpp @@ -116,25 +113,25 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16 #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, - imgQuestionL1); - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, - imgQuestionL2); + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, + 8, imgQuestionL1); + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12, + 8, imgQuestionL2); #else - display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, - imgQuestion); + display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8, + 8, imgQuestion); #endif } else { #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(ourId), y + 3 + FONT_HEIGHT_SMALL, 16, 8, - imgSFL1); - display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(ourId), y + 11 + FONT_HEIGHT_SMALL, 16, 8, - imgSFL2); + display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 16, + 8, imgSFL1); + display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 16, + 8, imgSFL2); #else - display->drawFastImage(x + SCREEN_WIDTH - 13 - display->getStringWidth(ourId), y + 2 + FONT_HEIGHT_SMALL, 11, 8, - imgSF); + display->drawFastImage(x + SCREEN_WIDTH - 13 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 11, + 8, imgSF); #endif } #endif @@ -143,16 +140,17 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16 #if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \ defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS) || ARCH_PORTDUINO) && \ !defined(DISPLAY_FORCE_SMALL_FONTS) - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgInfoL1); - display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, + display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(screen->ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, imgInfoL2); #else - display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, imgInfo); + display->drawFastImage(x + SCREEN_WIDTH - 10 - display->getStringWidth(screen->ourId), y + 2 + FONT_HEIGHT_SMALL, 8, 8, + imgInfo); #endif } - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(ourId), y + FONT_HEIGHT_SMALL, ourId); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(screen->ourId), y + FONT_HEIGHT_SMALL, screen->ourId); // Draw any log messages display->drawLogBuffer(x, y + (FONT_HEIGHT_SMALL * 2)); @@ -427,8 +425,8 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, uint8_t dmac[6]; char shortnameble[35]; getMacAddr(dmac); - snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); - snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", ourId); + snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); + snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId); int textWidth = display->getStringWidth(shortnameble); int nameX = (SCREEN_WIDTH - textWidth); display->drawString(nameX, compactFirstLine, shortnameble); diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 18c1c0d9f..e9d2eac55 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -76,7 +76,7 @@ void NotificationRenderer::drawWelcomeScreen(OLEDDisplay *display, OLEDDisplayUi void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) { // Exit if no message is active or duration has passed - if (strlen(alertBannerMessage) == 0 || (alertBannerUntil != 0 && millis() > alertBannerUntil)) + if (strlen(screen->alertBannerMessage) == 0 || (screen->alertBannerUntil != 0 && millis() > screen->alertBannerUntil)) return; // === Layout Configuration === @@ -84,45 +84,38 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp constexpr uint8_t lineSpacing = 1; // Extra space between lines // Search the message to determine if we need the bell added - bool needs_bell = (strstr(alertBannerMessage, "Alert Received") != nullptr); + bool needs_bell = (strstr(screen->alertBannerMessage, "Alert Received") != nullptr); // Setup font and alignment display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); // We will manually center per line - - // === Split the message into lines (supports multi-line banners) === const int MAX_LINES = 10; - char lines[MAX_LINES][256]; - int lineCount = 0; - // Create a working copy of the message to tokenize - char messageCopy[256]; - strncpy(messageCopy, alertBannerMessage, sizeof(messageCopy) - 1); - messageCopy[sizeof(messageCopy) - 1] = '\0'; - - char *line = strtok(messageCopy, "\n"); - while (line != nullptr && lineCount < MAX_LINES) { - strncpy(lines[lineCount], line, sizeof(lines[lineCount]) - 1); - lines[lineCount][sizeof(lines[lineCount]) - 1] = '\0'; - lineCount++; - line = strtok(nullptr, "\n"); - } - - // === Measure text dimensions === - uint16_t minWidth = (SCREEN_WIDTH > 128) ? 106 : 78; uint16_t maxWidth = 0; uint16_t lineWidths[MAX_LINES]; - for (int i = 0; i < lineCount; i++) { - uint16_t w = display->getStringWidth(lines[i], strlen(lines[i]), true); - lineWidths[i] = w; - if (w > maxWidth) - maxWidth = w; + char *lineStarts[MAX_LINES]; + uint16_t lineCount = 0; + char lineBuffer[40] = {0}; + uint16_t alertLength = strnlen(screen->alertBannerMessage, sizeof(screen->alertBannerMessage)); + lineStarts[lineCount] = screen->alertBannerMessage; + + // loop through lines finding \n characters + while (lineCount < 10 && lineStarts[lineCount] != screen->alertBannerMessage + alertLength) { + lineStarts[lineCount + 1] = std::find(lineStarts[lineCount], screen->alertBannerMessage + alertLength, '\n'); + lineWidths[lineCount] = + display->getStringWidth(lineStarts[lineCount], lineStarts[lineCount + 1] - lineStarts[lineCount], true); + if (lineWidths[lineCount] > maxWidth) { + maxWidth = lineWidths[lineCount]; + } + lineCount++; } + // set width from longest line uint16_t boxWidth = padding * 2 + maxWidth; - if (needs_bell && boxWidth < minWidth) + if (needs_bell && boxWidth < (SCREEN_WIDTH > 128) ? 106 : 78) boxWidth += (SCREEN_WIDTH > 128) ? 26 : 20; + // set height from line count uint16_t boxHeight = padding * 2 + lineCount * FONT_HEIGHT_SMALL + (lineCount - 1) * lineSpacing; int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); @@ -137,18 +130,20 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp // === Draw each line centered in the box === int16_t lineY = boxTop + padding; for (int i = 0; i < lineCount; i++) { + strncpy(lineBuffer, lineStarts[i], 40); + lineStarts[i][40] = '\0'; + int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; - uint16_t line_width = display->getStringWidth(lines[i], strlen(lines[i]), true); if (needs_bell && i == 0) { int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2; display->drawXbm(textX - 10, bellY, 8, 8, bell_alert); - display->drawXbm(textX + line_width + 2, bellY, 8, 8, bell_alert); + display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert); } - display->drawString(textX, lineY, lines[i]); + display->drawString(textX, lineY, lineBuffer); if (SCREEN_WIDTH > 128) - display->drawString(textX + 1, lineY, lines[i]); // Faux bold + display->drawString(textX + 1, lineY, lineBuffer); // Faux bold lineY += FONT_HEIGHT_SMALL + lineSpacing; } diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index becc5347d..6e2b45810 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -21,9 +21,6 @@ // External variables extern graphics::Screen *screen; -extern "C" { -extern char ourId[5]; -} namespace graphics { @@ -669,7 +666,7 @@ void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t uint8_t dmac[6]; char shortnameble[35]; getMacAddr(dmac); - snprintf(ourId, sizeof(ourId), "%02x%02x", dmac[4], dmac[5]); + snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]); snprintf(shortnameble, sizeof(shortnameble), "%s", graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : ""); diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 254e417f9..753967aff 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -380,9 +380,8 @@ bool CannedMessageModule::handleTabSwitch(const InputEvent *event) if (event->kbchar != 0x09) return false; - runState = (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) - ? CANNED_MESSAGE_RUN_STATE_FREETEXT - : CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; + runState = (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) ? CANNED_MESSAGE_RUN_STATE_FREETEXT + : CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION; destIndex = 0; scrollIndex = 0; @@ -720,8 +719,8 @@ bool CannedMessageModule::handleSystemCommandInput(const InputEvent *event) if (event->inputEvent != static_cast(ANYKEY)) return false; - // Block ALL input if an alert banner is active - if (strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil)) { + // Block ALL input if an alert banner is active // TODO: Make an accessor function + if (strlen(screen->alertBannerMessage) > 0 && (screen->alertBannerUntil == 0 || millis() <= screen->alertBannerUntil)) { return true; } @@ -890,8 +889,7 @@ int32_t CannedMessageModule::runOnce() // Normal module disable/idle handling if (((!moduleConfig.canned_message.enabled) && !CANNED_MESSAGE_MODULE_ENABLE) || - (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || - (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { + (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { temporaryMessage = ""; return INT32_MAX; } @@ -906,25 +904,23 @@ int32_t CannedMessageModule::runOnce() this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; - #if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(SENSECAP_INDICATOR) +#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(SENSECAP_INDICATOR) int destSelect = 0; - #endif +#endif this->notifyObservers(&e); - } - else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && - !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { + } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && + !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { // Reset module on inactivity e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; - #if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD) +#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD) int destSelect = 0; - #endif +#endif this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; this->notifyObservers(&e); - } - else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { + } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) { if (this->freetext.length() > 0) { sendText(this->dest, this->channel, this->freetext.c_str(), true); @@ -969,8 +965,7 @@ int32_t CannedMessageModule::runOnce() this->currentMessageIndex = firstRealMsgIdx; e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; - } - else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) { + } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) { if (this->messagesCount > 0) { this->currentMessageIndex = getPrevIndex(); this->freetext = ""; @@ -978,8 +973,7 @@ int32_t CannedMessageModule::runOnce() this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; LOG_DEBUG("MOVE UP (%d):%s", this->currentMessageIndex, this->getCurrentMessage()); } - } - else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_DOWN) { + } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_DOWN) { if (this->messagesCount > 0) { this->currentMessageIndex = this->getNextIndex(); this->freetext = ""; @@ -987,8 +981,7 @@ int32_t CannedMessageModule::runOnce() this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; LOG_DEBUG("MOVE DOWN (%d):%s", this->currentMessageIndex, this->getCurrentMessage()); } - } - else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { + } else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { switch (this->payload) { case INPUT_BROKER_MSG_LEFT: if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT && this->cursor > 0) { @@ -1012,7 +1005,7 @@ int32_t CannedMessageModule::runOnce() this->freetext = this->freetext.substring(0, this->freetext.length() - 1); } else { this->freetext = this->freetext.substring(0, this->cursor - 1) + - this->freetext.substring(this->cursor, this->freetext.length()); + this->freetext.substring(this->cursor, this->freetext.length()); } this->cursor--; } @@ -1023,11 +1016,13 @@ int32_t CannedMessageModule::runOnce() case INPUT_BROKER_MSG_RIGHT: break; default: - if (this->highlight != 0x00) break; + if (this->highlight != 0x00) + break; if (this->cursor == this->freetext.length()) { this->freetext += this->payload; } else { - this->freetext = this->freetext.substring(0, this->cursor) + this->payload + this->freetext.substring(this->cursor); + this->freetext = + this->freetext.substring(0, this->cursor) + this->payload + this->freetext.substring(this->cursor); } this->cursor += 1; uint16_t maxChars = meshtastic_Constants_DATA_PAYLOAD_LEN - (moduleConfig.canned_message.send_bell ? 1 : 0); @@ -1346,7 +1341,8 @@ bool CannedMessageModule::interceptingKeyboardInput() } // Draw the node/channel selection screen -void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { +void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ requestFocus(); display->setColor(WHITE); // Always draw cleanly display->setTextAlignment(TEXT_ALIGN_LEFT); @@ -1380,32 +1376,32 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O if (itemIndex >= totalEntries) break; - int xOffset = 0; - int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset; - char entryText[64]; + int xOffset = 0; + int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset; + char entryText[64]; - // Draw Channels First - if (itemIndex < numActiveChannels) { - uint8_t channelIndex = this->activeChannelIndices[itemIndex]; - snprintf(entryText, sizeof(entryText), "@%s", channels.getName(channelIndex)); - } - // Then Draw Nodes - else { - int nodeIndex = itemIndex - numActiveChannels; - if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { - meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; - if (node) { - if (node->is_favorite) { - snprintf(entryText, sizeof(entryText), "* %s", node->user.long_name); - } else { - snprintf(entryText, sizeof(entryText), "%s", node->user.long_name); - } + // Draw Channels First + if (itemIndex < numActiveChannels) { + uint8_t channelIndex = this->activeChannelIndices[itemIndex]; + snprintf(entryText, sizeof(entryText), "@%s", channels.getName(channelIndex)); + } + // Then Draw Nodes + else { + int nodeIndex = itemIndex - numActiveChannels; + if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { + meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; + if (node) { + if (node->is_favorite) { + snprintf(entryText, sizeof(entryText), "* %s", node->user.long_name); + } else { + snprintf(entryText, sizeof(entryText), "%s", node->user.long_name); } } } + } - if (strlen(entryText) == 0 || strcmp(entryText, "Unknown") == 0) - strcpy(entryText, "?"); + if (strlen(entryText) == 0 || strcmp(entryText, "Unknown") == 0) + strcpy(entryText, "?"); // === Highlight background (if selected) === if (itemIndex == destIndex) { From 9a814ae84871399275a1223328955fdf52c85995 Mon Sep 17 00:00:00 2001 From: Jason P Date: Sun, 1 Jun 2025 22:59:44 -0500 Subject: [PATCH 201/265] SixthLine Def, Screen Rename Added Sixth Line Definition Screen Rename, and Automatic Line Adjustment --- src/graphics/SharedUIDisplay.h | 2 + src/graphics/draw/DebugRenderer.cpp | 108 ++++++++++++++-------------- 2 files changed, 54 insertions(+), 56 deletions(-) diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index 1a8f5fb23..c5f10451b 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -15,6 +15,7 @@ namespace graphics #define compactThirdLine ((FONT_HEIGHT_SMALL - 1) * 3) - 4 #define compactFourthLine ((FONT_HEIGHT_SMALL - 1) * 4) - 6 #define compactFifthLine ((FONT_HEIGHT_SMALL - 1) * 5) - 8 +#define compactSixLine ((FONT_HEIGHT_SMALL - 1) * 6) - 10 // Standard line layout #define standardFirstLine (FONT_HEIGHT_SMALL + 1) * 1 @@ -28,6 +29,7 @@ namespace graphics #define moreCompactThirdLine (moreCompactSecondLine + (FONT_HEIGHT_SMALL - 5)) #define moreCompactFourthLine (moreCompactThirdLine + (FONT_HEIGHT_SMALL - 5)) #define moreCompactFifthLine (moreCompactFourthLine + (FONT_HEIGHT_SMALL - 5)) +#define moreCompactSixthLine (moreCompactFifthLine + (FONT_HEIGHT_SMALL - 5)) // Quick screen access #define SCREEN_WIDTH display->getWidth() diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index b4d90ebb3..ef1af7487 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -532,7 +532,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, // === Draw title === const int highlightHeight = FONT_HEIGHT_SMALL - 1; const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - const char *titleStr = (SCREEN_WIDTH > 128) ? "Memory" : "Mem"; + const char *titleStr = (SCREEN_WIDTH > 128) ? "System" : "Sys"; const int centerX = x + SCREEN_WIDTH / 2; if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { @@ -547,22 +547,14 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->setColor(WHITE); // === Layout === - int contentY = y + FONT_HEIGHT_SMALL; - const int rowYOffset = FONT_HEIGHT_SMALL - 3; + const int yPositions[6] = {moreCompactFirstLine, moreCompactSecondLine, moreCompactThirdLine, + moreCompactFourthLine, moreCompactFifthLine, moreCompactSixthLine}; + int line = 0; const int barHeight = 6; const int labelX = x; const int barsOffset = (SCREEN_WIDTH > 128) ? 24 : 0; const int barX = x + 40 + barsOffset; - int rowY = contentY; - - // === Heap delta tracking (disabled) === - /* - static uint32_t previousHeapFree = 0; - static int32_t totalHeapDelta = 0; - static int deltaChangeCount = 0; - */ - auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) { if (total == 0) return; @@ -586,10 +578,10 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, // Label display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawString(labelX, rowY, label); + display->drawString(labelX, yPositions[line], label); // Bar - int barY = rowY + (FONT_HEIGHT_SMALL - barHeight) / 2; + int barY = yPositions[line] + (FONT_HEIGHT_SMALL - barHeight) / 2; display->setColor(WHITE); display->drawRect(barX, barY, adjustedBarWidth, barHeight); @@ -598,45 +590,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, // Value string display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(SCREEN_WIDTH - 2, rowY, combinedStr); - - rowY += rowYOffset; - - // === Heap delta display (disabled) === - /* - if (isHeap && previousHeapFree > 0) { - int32_t delta = (int32_t)(memGet.getFreeHeap() - previousHeapFree); - if (delta != 0) { - totalHeapDelta += delta; - deltaChangeCount++; - - char deltaStr[16]; - snprintf(deltaStr, sizeof(deltaStr), "%ld", delta); - - int deltaX = centerX - display->getStringWidth(deltaStr) / 2 - 8; - int deltaY = rowY + 1; - - // Triangle - if (delta > 0) { - display->drawLine(deltaX, deltaY + 6, deltaX + 3, deltaY); - display->drawLine(deltaX + 3, deltaY, deltaX + 6, deltaY + 6); - display->drawLine(deltaX, deltaY + 6, deltaX + 6, deltaY + 6); - } else { - display->drawLine(deltaX, deltaY, deltaX + 3, deltaY + 6); - display->drawLine(deltaX + 3, deltaY + 6, deltaX + 6, deltaY); - display->drawLine(deltaX, deltaY, deltaX + 6, deltaY); - } - - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(centerX + 6, deltaY, deltaStr); - rowY += rowYOffset; - } - } - - if (isHeap) { - previousHeapFree = memGet.getFreeHeap(); - } - */ + display->drawString(SCREEN_WIDTH - 2, yPositions[line], combinedStr); }; // === Memory values === @@ -665,13 +619,55 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, */ // === Draw memory rows drawUsageRow("Heap:", heapUsed, heapTotal, true); - drawUsageRow("PSRAM:", psramUsed, psramTotal); #ifdef ESP32 - if (flashTotal > 0) + line += 1; + drawUsageRow("PSRAM:", psramUsed, psramTotal); + if (flashTotal > 0) { + line += 1; drawUsageRow("Flash:", flashUsed, flashTotal); + } #endif - if (hasSD && sdTotal > 0) + if (hasSD && sdTotal > 0) { + line += 1; drawUsageRow("SD:", sdUsed, sdTotal); + } + + display->setTextAlignment(TEXT_ALIGN_LEFT); + // System Uptime + if (line < 3) { + line += 1; + } + line += 1; + char appversionstr[35]; + snprintf(appversionstr, sizeof(appversionstr), "Ver.: %s", optstr(APP_VERSION)); + int textWidth = display->getStringWidth(appversionstr); + int nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, yPositions[line], appversionstr); + + line += 1; + uint32_t uptime = millis() / 1000; + char uptimeStr[6]; + uint32_t minutes = uptime / 60, hours = minutes / 60, days = hours / 24; + + if (days > 365) { + snprintf(uptimeStr, sizeof(uptimeStr), "?"); + } else { + snprintf(uptimeStr, sizeof(uptimeStr), "%u%c", + days ? days + : hours ? hours + : minutes ? minutes + : (int)uptime, + days ? 'd' + : hours ? 'h' + : minutes ? 'm' + : 's'); + } + + char uptimeFullStr[16]; + snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); + textWidth = display->getStringWidth(uptimeFullStr); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, yPositions[line], uptimeFullStr); } } // namespace DebugRenderer } // namespace graphics From 693aef8256bda0da54161a5432a499947127d87e Mon Sep 17 00:00:00 2001 From: Jason P Date: Sun, 1 Jun 2025 23:03:35 -0500 Subject: [PATCH 202/265] Consistency is hard - fixed "Sixth" --- src/graphics/SharedUIDisplay.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index c5f10451b..5a1fa74d5 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -15,7 +15,7 @@ namespace graphics #define compactThirdLine ((FONT_HEIGHT_SMALL - 1) * 3) - 4 #define compactFourthLine ((FONT_HEIGHT_SMALL - 1) * 4) - 6 #define compactFifthLine ((FONT_HEIGHT_SMALL - 1) * 5) - 8 -#define compactSixLine ((FONT_HEIGHT_SMALL - 1) * 6) - 10 +#define compactSixthLine ((FONT_HEIGHT_SMALL - 1) * 6) - 10 // Standard line layout #define standardFirstLine (FONT_HEIGHT_SMALL + 1) * 1 From 65869265a9236d52d780b74afe80eb94e7d2e9a5 Mon Sep 17 00:00:00 2001 From: Jason P Date: Mon, 2 Jun 2025 08:23:41 -0500 Subject: [PATCH 203/265] System Frame Updates Adjusted line construction to ensure we fit maximum content per screen. --- src/graphics/draw/DebugRenderer.cpp | 54 ++++++++++++++++------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index ef1af7487..8ee31d6e4 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -620,8 +620,10 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, // === Draw memory rows drawUsageRow("Heap:", heapUsed, heapTotal, true); #ifdef ESP32 - line += 1; - drawUsageRow("PSRAM:", psramUsed, psramTotal); + if (psramUsed > 0) { + line += 1; + drawUsageRow("PSRAM:", psramUsed, psramTotal); + } if (flashTotal > 0) { line += 1; drawUsageRow("Flash:", flashUsed, flashTotal); @@ -634,7 +636,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->setTextAlignment(TEXT_ALIGN_LEFT); // System Uptime - if (line < 3) { + if (line < 2) { line += 1; } line += 1; @@ -644,30 +646,32 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, yPositions[line], appversionstr); - line += 1; - uint32_t uptime = millis() / 1000; - char uptimeStr[6]; - uint32_t minutes = uptime / 60, hours = minutes / 60, days = hours / 24; + if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line < 4)) { // Only show uptime if the screen can show it + line += 1; + uint32_t uptime = millis() / 1000; + char uptimeStr[6]; + uint32_t minutes = uptime / 60, hours = minutes / 60, days = hours / 24; - if (days > 365) { - snprintf(uptimeStr, sizeof(uptimeStr), "?"); - } else { - snprintf(uptimeStr, sizeof(uptimeStr), "%u%c", - days ? days - : hours ? hours - : minutes ? minutes - : (int)uptime, - days ? 'd' - : hours ? 'h' - : minutes ? 'm' - : 's'); + if (days > 365) { + snprintf(uptimeStr, sizeof(uptimeStr), "?"); + } else { + snprintf(uptimeStr, sizeof(uptimeStr), "%u%c", + days ? days + : hours ? hours + : minutes ? minutes + : (int)uptime, + days ? 'd' + : hours ? 'h' + : minutes ? 'm' + : 's'); + } + + char uptimeFullStr[16]; + snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); + textWidth = display->getStringWidth(uptimeFullStr); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, yPositions[line], uptimeFullStr); } - - char uptimeFullStr[16]; - snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); - textWidth = display->getStringWidth(uptimeFullStr); - nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, yPositions[line], uptimeFullStr); } } // namespace DebugRenderer } // namespace graphics From 78c990d48b4386ed84b32eb86d00b046a8d855e3 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Mon, 2 Jun 2025 12:10:54 -0500 Subject: [PATCH 204/265] Fix up notifications --- src/graphics/draw/NotificationRenderer.cpp | 33 ++++++++++++++-------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index e9d2eac55..33d4efc3e 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -92,18 +92,21 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp const int MAX_LINES = 10; uint16_t maxWidth = 0; - uint16_t lineWidths[MAX_LINES]; - char *lineStarts[MAX_LINES]; + uint16_t lineWidths[MAX_LINES] = {0}; + uint16_t lineLengths[MAX_LINES] = {0}; + char *lineStarts[MAX_LINES + 1]; uint16_t lineCount = 0; char lineBuffer[40] = {0}; - uint16_t alertLength = strnlen(screen->alertBannerMessage, sizeof(screen->alertBannerMessage)); + // pointer to the terminating null + char *alertEnd = screen->alertBannerMessage + strnlen(screen->alertBannerMessage, sizeof(screen->alertBannerMessage)); lineStarts[lineCount] = screen->alertBannerMessage; + LOG_WARN(lineStarts[lineCount]); // loop through lines finding \n characters - while (lineCount < 10 && lineStarts[lineCount] != screen->alertBannerMessage + alertLength) { - lineStarts[lineCount + 1] = std::find(lineStarts[lineCount], screen->alertBannerMessage + alertLength, '\n'); - lineWidths[lineCount] = - display->getStringWidth(lineStarts[lineCount], lineStarts[lineCount + 1] - lineStarts[lineCount], true); + while ((lineCount < 10) && (lineStarts[lineCount] < alertEnd)) { + lineStarts[lineCount + 1] = std::find(lineStarts[lineCount], alertEnd, '\n') + 1; + lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount]; + lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true); if (lineWidths[lineCount] > maxWidth) { maxWidth = lineWidths[lineCount]; } @@ -112,15 +115,20 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp // set width from longest line uint16_t boxWidth = padding * 2 + maxWidth; - if (needs_bell && boxWidth < (SCREEN_WIDTH > 128) ? 106 : 78) - boxWidth += (SCREEN_WIDTH > 128) ? 26 : 20; + if (needs_bell) { + if (SCREEN_WIDTH > 128 && boxWidth < 106) { + boxWidth += 26; + } + if (SCREEN_WIDTH <= 128 && boxWidth < 78) { + boxWidth += 20; + } + } // set height from line count uint16_t boxHeight = padding * 2 + lineCount * FONT_HEIGHT_SMALL + (lineCount - 1) * lineSpacing; int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); int16_t boxTop = (display->height() / 2) - (boxHeight / 2); - // === Draw background box === display->setColor(BLACK); display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Slightly oversized box @@ -131,7 +139,10 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp int16_t lineY = boxTop + padding; for (int i = 0; i < lineCount; i++) { strncpy(lineBuffer, lineStarts[i], 40); - lineStarts[i][40] = '\0'; + if (lineLengths[i] > 39) + lineBuffer[39] = '\0'; + else + lineBuffer[lineLengths[i]] = '\0'; int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; From 0f5413d113b12909cfd583260dd8a94829e8e9ef Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Mon, 2 Jun 2025 12:17:39 -0500 Subject: [PATCH 205/265] Add a couple more ifdef HAS_SCREEN lines --- src/graphics/draw/DebugRenderer.cpp | 6 ++++-- src/graphics/draw/NotificationRenderer.cpp | 7 +++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 8ee31d6e4..36f8a9186 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -1,11 +1,12 @@ -#include "DebugRenderer.h" +#include "configuration.h" +#if HAS_SCREEN #include "../Screen.h" +#include "DebugRenderer.h" #include "FSCommon.h" #include "NodeDB.h" #include "Throttle.h" #include "UIRenderer.h" #include "airtime.h" -#include "configuration.h" #include "gps/RTC.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" @@ -675,3 +676,4 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, } } // namespace DebugRenderer } // namespace graphics +#endif \ No newline at end of file diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 33d4efc3e..1c5157abe 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -1,7 +1,9 @@ -#include "NotificationRenderer.h" +#include "configuration.h" +#if HAS_SCREEN + #include "DisplayFormatters.h" #include "NodeDB.h" -#include "configuration.h" +#include "NotificationRenderer.h" #include "graphics/Screen.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" @@ -189,3 +191,4 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi } // namespace NotificationRenderer } // namespace graphics +#endif \ No newline at end of file From cb3a20feb80bc8edbd4d0e115d5424f7ff510cf3 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Mon, 2 Jun 2025 15:03:22 -0500 Subject: [PATCH 206/265] Add screen->isOverlayBannerShowing() --- src/graphics/Screen.h | 7 ++++++- src/graphics/draw/NotificationRenderer.cpp | 2 +- src/modules/CannedMessageModule.cpp | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 0175b7721..fee1dad9f 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -221,7 +221,7 @@ class Screen : public concurrency::OSThread OLEDDISPLAY_GEOMETRY geometry; char alertBannerMessage[256] = {0}; - uint32_t alertBannerUntil = 0; + uint32_t alertBannerUntil = 0; // 0 is a special case meaning forever // Stores the last 4 of our hardware ID, to make finding the device for pairing easier // FIXME: Needs refactoring and getMacAddr needs to be moved to a utility class @@ -288,6 +288,11 @@ class Screen : public concurrency::OSThread void showOverlayBanner(const char *message, uint32_t durationMs = 3000); + bool isOverlayBannerShowing() + { + return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); + } + void startFirmwareUpdateScreen() { ScreenCmd cmd; diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 1c5157abe..1b55008eb 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -78,7 +78,7 @@ void NotificationRenderer::drawWelcomeScreen(OLEDDisplay *display, OLEDDisplayUi void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) { // Exit if no message is active or duration has passed - if (strlen(screen->alertBannerMessage) == 0 || (screen->alertBannerUntil != 0 && millis() > screen->alertBannerUntil)) + if (screen->isOverlayBannerShowing()) return; // === Layout Configuration === diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 753967aff..b325fe617 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -720,7 +720,7 @@ bool CannedMessageModule::handleSystemCommandInput(const InputEvent *event) return false; // Block ALL input if an alert banner is active // TODO: Make an accessor function - if (strlen(screen->alertBannerMessage) > 0 && (screen->alertBannerUntil == 0 || millis() <= screen->alertBannerUntil)) { + if (screen && screen->isOverlayBannerShowing()) { return true; } From d83ac30d3cb607745e7fc79815ca94f5a599eb4b Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Mon, 2 Jun 2025 15:25:10 -0500 Subject: [PATCH 207/265] Don't forget the invert! --- src/graphics/draw/NotificationRenderer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 1b55008eb..9ec45a70a 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -78,7 +78,7 @@ void NotificationRenderer::drawWelcomeScreen(OLEDDisplay *display, OLEDDisplayUi void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) { // Exit if no message is active or duration has passed - if (screen->isOverlayBannerShowing()) + if (!screen->isOverlayBannerShowing()) return; // === Layout Configuration === From f7d075a951a2827337599051b7fb3a9f04c58eb0 Mon Sep 17 00:00:00 2001 From: Jason P Date: Mon, 2 Jun 2025 15:45:34 -0500 Subject: [PATCH 208/265] Adjust Nodelist Center Divider Adjust Nodelist Center Divider --- src/graphics/draw/NodeListRenderer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index bb7556f02..58a52cbcd 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -151,7 +151,7 @@ void retrieveAndSortNodes(std::vector &nodeList) void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd) { int columnWidth = display->getWidth() / 2; - int separatorX = x + columnWidth - 1; + int separatorX = x + columnWidth - 2; for (int y = yStart; y <= yEnd; y += 2) { display->setPixel(separatorX, y); } From d22c7c7eeef258227d74811fb7653273850154ac Mon Sep 17 00:00:00 2001 From: Jason P Date: Mon, 2 Jun 2025 16:13:35 -0500 Subject: [PATCH 209/265] Fix variable casting --- src/graphics/draw/UIRenderer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 6e2b45810..9a8ef7baa 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -129,7 +129,7 @@ void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtasti if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) snprintf(displayLine, sizeof(displayLine), "Altitude: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); else - snprintf(displayLine, sizeof(displayLine), "Altitude: %.0fm", geoCoord.getAltitude()); + snprintf(displayLine, sizeof(displayLine), "Altitude: %.0fm", (double)geoCoord.getAltitude()); display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); } } @@ -1016,7 +1016,7 @@ void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *stat if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { snprintf(displayLine, sizeof(displayLine), " Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); } else { - snprintf(displayLine, sizeof(displayLine), " Alt: %.0fm", geoCoord.getAltitude()); + snprintf(displayLine, sizeof(displayLine), " Alt: %.0fm", (double)geoCoord.getAltitude()); } display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine), displayLine); From 17b531ec99e4679c20542bcda8344f028bfe56b7 Mon Sep 17 00:00:00 2001 From: Jason P Date: Mon, 2 Jun 2025 16:34:08 -0500 Subject: [PATCH 210/265] Fix entryText variable as empty before update to fix validation --- src/modules/CannedMessageModule.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index b325fe617..6ba1937a5 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -1378,7 +1378,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O int xOffset = 0; int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset; - char entryText[64]; + char entryText[64] = ""; // Draw Channels First if (itemIndex < numActiveChannels) { From 38d7780df322dfd76cc7cd3733233f2158619a05 Mon Sep 17 00:00:00 2001 From: Jason P Date: Mon, 2 Jun 2025 16:58:49 -0500 Subject: [PATCH 211/265] Altitude is int32_t --- src/graphics/draw/UIRenderer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 9a8ef7baa..d43e9f2cd 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -129,7 +129,7 @@ void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtasti if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) snprintf(displayLine, sizeof(displayLine), "Altitude: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); else - snprintf(displayLine, sizeof(displayLine), "Altitude: %.0fm", (double)geoCoord.getAltitude()); + snprintf(displayLine, sizeof(displayLine), "Altitude: %.0im", geoCoord.getAltitude()); display->drawString(x + (display->getWidth() - (display->getStringWidth(displayLine))) / 2, y, displayLine); } } @@ -1016,7 +1016,7 @@ void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *stat if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { snprintf(displayLine, sizeof(displayLine), " Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); } else { - snprintf(displayLine, sizeof(displayLine), " Alt: %.0fm", (double)geoCoord.getAltitude()); + snprintf(displayLine, sizeof(displayLine), " Alt: %.0im", geoCoord.getAltitude()); } display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine), displayLine); From 1226b10c7a28a1b4375850cd3f2c4b36b74b039c Mon Sep 17 00:00:00 2001 From: Jason P Date: Mon, 2 Jun 2025 18:45:55 -0500 Subject: [PATCH 212/265] Update PowerTelemetry to have correct data type --- src/modules/Telemetry/PowerTelemetry.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index e635b022d..55ad3a3fd 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -153,7 +153,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s // Display "Pow. From: ..." char fromStr[64]; - snprintf(fromStr, sizeof(fromStr), "Pow. From: %s (%ds)", lastSender, agoSecs); + snprintf(fromStr, sizeof(fromStr), "Pow. From: %s (%us)", lastSender, agoSecs); display->drawString(x, compactFirstLine, fromStr); // Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags From 02ccc5643d78694382694a9e1160166b92a4f1d1 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Mon, 2 Jun 2025 21:21:13 -0500 Subject: [PATCH 213/265] Fix cppcheck warnings (#6945) * Fix cppcheck warnings * Adjust logic in Power.cpp for power sensor --------- Co-authored-by: Jason P --- bin/check-all.sh | 2 +- src/Power.cpp | 14 ++++++++------ src/graphics/Screen.cpp | 2 +- src/graphics/SharedUIDisplay.cpp | 6 ------ src/graphics/draw/MessageRenderer.cpp | 4 ++-- src/graphics/draw/NodeListRenderer.cpp | 16 +++------------- src/graphics/draw/NodeListRenderer.h | 3 +-- src/graphics/draw/UIRenderer.cpp | 20 +++++++++++--------- src/graphics/draw/UIRenderer.h | 2 +- src/input/TCA8418Keyboard.cpp | 1 - src/input/TCA8418Keyboard.h | 1 - src/main.h | 1 - src/mesh/MeshPacketQueue.cpp | 4 ++-- src/mesh/MeshPacketQueue.h | 2 +- src/modules/AdminModule.cpp | 2 +- src/modules/CannedMessageModule.cpp | 18 ++++++------------ src/mqtt/MQTT.cpp | 6 +++--- suppressions.txt | 6 +++++- 18 files changed, 46 insertions(+), 64 deletions(-) diff --git a/bin/check-all.sh b/bin/check-all.sh index d1b50a8aa..29d6b5532 100755 --- a/bin/check-all.sh +++ b/bin/check-all.sh @@ -23,4 +23,4 @@ for BOARD in $BOARDS; do CHECK="${CHECK} -e ${BOARD}" done -pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt" $CHECK --skip-packages --pattern="src/" --fail-on-defect=medium --fail-on-defect=high +pio check --flags "-DAPP_VERSION=${APP_VERSION} --suppressions-list=suppressions.txt --inline-suppr" $CHECK --skip-packages --pattern="src/" --fail-on-defect=medium --fail-on-defect=high diff --git a/src/Power.cpp b/src/Power.cpp index 12b1a0ff2..400b6c6eb 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -661,12 +661,14 @@ bool Power::analogInit() */ bool Power::setup() { - // initialise one power sensor (only) - bool found = axpChipInit(); - if (!found) - found = lipoInit(); - if (!found) - found = analogInit(); + bool found = false; + if (axpChipInit()) { + found = true; + } else if (lipoInit()) { + found = true; + } else if (analogInit()) { + found = true; + } #ifdef NRF_APM found = true; diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 33734116f..109ccb518 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1411,7 +1411,7 @@ void Screen::setFrames(FrameFocus focus) } for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) { - meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); + const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i); if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) { normalFrames[numframes++] = graphics::UIRenderer::drawNodeInfo; indicatorIcons.push_back(icon_node); diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 594b2dcf5..dc5850c87 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -73,8 +73,6 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) // === Battery State === int chargePercent = powerStatus->getBatteryChargePercent(); bool isCharging = powerStatus->getIsCharging() == meshtastic::OptionalBool::OptTrue; - static uint32_t lastBlinkShared = 0; - static bool isBoltVisibleShared = true; uint32_t now = millis(); #ifndef USE_EINK @@ -160,8 +158,6 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) // === Show Mail or Mute Icon to the Left of Time === int iconRightEdge = timeX - 2; - static bool isMailIconVisible = true; - static uint32_t lastMailBlink = 0; bool showMail = false; #ifndef USE_EINK @@ -212,8 +208,6 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) // === No Time Available: Mail/Mute Icon Moves to Far Right === int iconRightEdge = screenW - xOffset; - static bool isMailIconVisible = true; - static uint32_t lastMailBlink = 0; bool showMail = false; if (hasUnreadMessage) { diff --git a/src/graphics/draw/MessageRenderer.cpp b/src/graphics/draw/MessageRenderer.cpp index 2645a68ad..a713cd45b 100644 --- a/src/graphics/draw/MessageRenderer.cpp +++ b/src/graphics/draw/MessageRenderer.cpp @@ -360,11 +360,11 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // === Scrolling logic === std::vector rowHeights; - for (const auto &line : lines) { + for (const auto &_line : lines) { int maxHeight = FONT_HEIGHT_SMALL; for (int i = 0; i < numEmotes; ++i) { const Emote &e = emotes[i]; - if (line.find(e.label) != std::string::npos) { + if (_line.find(e.label) != std::string::npos) { if (e.height > maxHeight) maxHeight = e.height; } diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index 58a52cbcd..c356484cb 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -74,16 +74,6 @@ const char *getSafeNodeName(meshtastic_NodeInfoLite *node) return nodeName; } -uint32_t sinceLastSeen(meshtastic_NodeInfoLite *node) -{ - uint32_t now = getTime(); - uint32_t last_seen = node->last_heard; - if (last_seen == 0 || now < last_seen) { - return UINT32_MAX; - } - return now - last_seen; -} - const char *getCurrentModeTitle(int screenWidth) { switch (currentMode) { @@ -139,7 +129,7 @@ void retrieveAndSortNodes(std::vector &nodeList) bool aFav = a.node->is_favorite; bool bFav = b.node->is_favorite; if (aFav != bFav) - return aFav > bFav; + return aFav; if (a.sortValue == 0 || a.sortValue == UINT32_MAX) return false; if (b.sortValue == 0 || b.sortValue == UINT32_MAX) @@ -593,7 +583,7 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon); } -void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y) { // Cache favorite nodes for the current frame only, to save computation static std::vector favoritedNodes; @@ -614,7 +604,7 @@ void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, in } // Keep a stable, consistent display order std::sort(favoritedNodes.begin(), favoritedNodes.end(), - [](meshtastic_NodeInfoLite *a, meshtastic_NodeInfoLite *b) { return a->num < b->num; }); + [](const meshtastic_NodeInfoLite *a, const meshtastic_NodeInfoLite *b) { return a->num < b->num; }); } if (favoritedNodes.empty()) return; diff --git a/src/graphics/draw/NodeListRenderer.h b/src/graphics/draw/NodeListRenderer.h index aa92e34ea..3fa37e91a 100644 --- a/src/graphics/draw/NodeListRenderer.h +++ b/src/graphics/draw/NodeListRenderer.h @@ -54,13 +54,12 @@ void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); -void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y); // Utility functions const char *getCurrentModeTitle(int screenWidth); void retrieveAndSortNodes(std::vector &nodeList); const char *getSafeNodeName(meshtastic_NodeInfoLite *node); -uint32_t sinceLastSeen(meshtastic_NodeInfoLite *node); void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields); // Bitmap drawing function diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index d43e9f2cd..021191f4e 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -269,7 +269,7 @@ void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::Nod // ********************** // * Favorite Node Info * // ********************** -void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y) { // --- Cache favorite nodes for the current frame only, to save computation --- static std::vector favoritedNodes; @@ -290,7 +290,7 @@ void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, in } // Keep a stable, consistent display order std::sort(favoritedNodes.begin(), favoritedNodes.end(), - [](meshtastic_NodeInfoLite *a, meshtastic_NodeInfoLite *b) { return a->num < b->num; }); + [](const meshtastic_NodeInfoLite *a, const meshtastic_NodeInfoLite *b) { return a->num < b->num; }); } if (favoritedNodes.empty()) return; @@ -495,8 +495,10 @@ void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, in CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading); const auto &p = node->position; + /* unused float d = GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + */ float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); if (!config.display.compass_north_top) bearing -= myHeading; @@ -542,8 +544,10 @@ void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, in graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading); const auto &p = node->position; + /* unused float d = GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + */ float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); if (!config.display.compass_north_top) bearing -= myHeading; @@ -608,8 +612,6 @@ void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t ((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)), uptimeFullStr); - config.display.heading_bold = origBold; - // === Second Row: Satellites and Voltage === config.display.heading_bold = false; @@ -632,8 +634,8 @@ void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t } #endif - char batStr[20]; if (powerStatus->getHasBattery()) { + char batStr[20]; int batV = powerStatus->getBatteryVoltageMv() / 1000; int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; snprintf(batStr, sizeof(batStr), "%01d.%02dV", batV, batCv); @@ -1012,13 +1014,13 @@ void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *stat if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) { // === Second Row: Altitude === - char displayLine[32]; + char DisplayLineTwo[32] = {0}; if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - snprintf(displayLine, sizeof(displayLine), " Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); + snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); } else { - snprintf(displayLine, sizeof(displayLine), " Alt: %.0im", geoCoord.getAltitude()); + snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0im", geoCoord.getAltitude()); } - display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine), displayLine); + display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine), DisplayLineTwo); // === Third Row: Latitude === char latStr[32]; diff --git a/src/graphics/draw/UIRenderer.h b/src/graphics/draw/UIRenderer.h index 86231da11..d45991c33 100644 --- a/src/graphics/draw/UIRenderer.h +++ b/src/graphics/draw/UIRenderer.h @@ -53,7 +53,7 @@ void drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); // Navigation bar overlay void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state); -void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); +void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y); void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); diff --git a/src/input/TCA8418Keyboard.cpp b/src/input/TCA8418Keyboard.cpp index 21cd7b2d5..d99379b23 100644 --- a/src/input/TCA8418Keyboard.cpp +++ b/src/input/TCA8418Keyboard.cpp @@ -147,7 +147,6 @@ TCA8418Keyboard::TCA8418Keyboard() : m_wire(nullptr), m_addr(0), readCallback(nu { state = Init; last_key = -1; - next_key = -1; should_backspace = false; last_tap = 0L; char_idx = 0; diff --git a/src/input/TCA8418Keyboard.h b/src/input/TCA8418Keyboard.h index c7f3c1f28..5c53452a4 100644 --- a/src/input/TCA8418Keyboard.h +++ b/src/input/TCA8418Keyboard.h @@ -21,7 +21,6 @@ class TCA8418Keyboard KeyState state; int8_t last_key; - int8_t next_key; bool should_backspace; uint32_t last_tap; uint8_t char_idx; diff --git a/src/main.h b/src/main.h index beeb1f940..aa3223af0 100644 --- a/src/main.h +++ b/src/main.h @@ -37,7 +37,6 @@ extern ScanI2C::FoundDevice rgb_found; extern bool eink_found; extern bool pmu_found; -extern bool isCharging; extern bool isUSBPowered; #ifdef T_WATCH_S3 diff --git a/src/mesh/MeshPacketQueue.cpp b/src/mesh/MeshPacketQueue.cpp index 0c312fd1e..f8af81321 100644 --- a/src/mesh/MeshPacketQueue.cpp +++ b/src/mesh/MeshPacketQueue.cpp @@ -118,10 +118,10 @@ meshtastic_MeshPacket *MeshPacketQueue::remove(NodeNum from, PacketId id, bool t } /* Attempt to find a packet from this queue. Return true if it was found. */ -bool MeshPacketQueue::find(NodeNum from, PacketId id) +bool MeshPacketQueue::find(const NodeNum from, const PacketId id) { for (auto it = queue.begin(); it != queue.end(); it++) { - auto p = (*it); + const auto p = (*it); if (getFrom(p) == from && p->id == id) { return true; } diff --git a/src/mesh/MeshPacketQueue.h b/src/mesh/MeshPacketQueue.h index 6b2c3998a..1b338f9ed 100644 --- a/src/mesh/MeshPacketQueue.h +++ b/src/mesh/MeshPacketQueue.h @@ -39,5 +39,5 @@ class MeshPacketQueue meshtastic_MeshPacket *remove(NodeNum from, PacketId id, bool tx_normal = true, bool tx_late = true); /* Attempt to find a packet from this queue. Return true if it was found. */ - bool find(NodeNum from, PacketId id); + bool find(const NodeNum from, const PacketId id); }; \ No newline at end of file diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index d22c648ca..14fc9b3cd 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -513,7 +513,7 @@ void AdminModule::handleSetOwner(const meshtastic_User &o) if (owner.has_is_unmessagable != o.has_is_unmessagable || (o.has_is_unmessagable && owner.is_unmessagable != o.is_unmessagable)) { changed = 1; - owner.has_is_unmessagable = o.has_is_unmessagable || o.has_is_unmessagable; + owner.has_is_unmessagable = owner.has_is_unmessagable || o.has_is_unmessagable; owner.is_unmessagable = o.is_unmessagable; } diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 6ba1937a5..af7acc736 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -462,7 +462,7 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event } else { int nodeIndex = destIndex - static_cast(activeChannelIndices.size()); if (nodeIndex >= 0 && nodeIndex < static_cast(filteredNodes.size())) { - meshtastic_NodeInfoLite *selectedNode = filteredNodes[nodeIndex].node; + const meshtastic_NodeInfoLite *selectedNode = filteredNodes[nodeIndex].node; if (selectedNode) { dest = selectedNode->num; channel = selectedNode->channel; @@ -904,9 +904,6 @@ int32_t CannedMessageModule::runOnce() this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(SENSECAP_INDICATOR) - int destSelect = 0; -#endif this->notifyObservers(&e); } else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) && !Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) { @@ -915,9 +912,6 @@ int32_t CannedMessageModule::runOnce() this->currentMessageIndex = -1; this->freetext = ""; this->cursor = 0; -#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD) - int destSelect = 0; -#endif this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; this->notifyObservers(&e); } else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) { @@ -1418,7 +1412,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O if (itemIndex >= numActiveChannels) { int nodeIndex = itemIndex - numActiveChannels; if (nodeIndex >= 0 && nodeIndex < static_cast(this->filteredNodes.size())) { - meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; + const meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node; if (node && hasKeyForNode(node)) { int iconX = display->getWidth() - key_symbol_width - 15; int iconY = yOffset + (FONT_HEIGHT_SMALL - key_symbol_height) / 2; @@ -1585,7 +1579,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st // Shift message list upward by 3 pixels to reduce spacing between header and first message const int listYOffset = y + FONT_HEIGHT_SMALL - 3; - const int visibleRows = (display->getHeight() - listYOffset) / rowSpacing; + visibleRows = (display->getHeight() - listYOffset) / rowSpacing; int topMsg = (messagesCount > visibleRows && currentMessageIndex >= visibleRows - 1) ? currentMessageIndex - visibleRows + 2 : 0; @@ -1644,17 +1638,17 @@ ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket & // Determine ACK status bool isAck = (decoded.error_reason == meshtastic_Routing_Error_NONE); bool isFromDest = (mp.from == this->lastSentNode); - bool isBroadcast = (this->lastSentNode == NODENUM_BROADCAST); + bool wasBroadcast = (this->lastSentNode == NODENUM_BROADCAST); // Identify the responding node - if (isBroadcast && mp.from != nodeDB->getNodeNum()) { + if (wasBroadcast && mp.from != nodeDB->getNodeNum()) { this->incoming = mp.from; // Relayed by another node } else { this->incoming = this->lastSentNode; // Direct reply } // Final ACK confirmation logic - this->ack = isAck && (isBroadcast || isFromDest); + this->ack = isAck && (wasBroadcast || isFromDest); waitingForAck = false; this->notifyObservers(&e); diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index dca8a3b44..84c076885 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -355,7 +355,7 @@ void MQTT::onReceive(char *topic, byte *payload, size_t length) // if another "/" was added, parse string up to that character channelName = strtok(channelName, "/") ? strtok(channelName, "/") : channelName; // We allow downlink JSON packets only on a channel named "mqtt" - meshtastic_Channel &sendChannel = channels.getByName(channelName); + const meshtastic_Channel &sendChannel = channels.getByName(channelName); if (!(strncasecmp(channels.getGlobalId(sendChannel.index), Channels::mqttChannel, strlen(Channels::mqttChannel)) == 0 && sendChannel.settings.downlink_enabled)) { LOG_WARN("JSON downlink received on channel not called 'mqtt' or without downlink enabled"); @@ -491,7 +491,7 @@ void MQTT::reconnect() return; // Don't try to connect directly to the server } #if HAS_NETWORKING - const PubSubConfig config(moduleConfig.mqtt); + const PubSubConfig ps_config(moduleConfig.mqtt); MQTTClient *clientConnection = mqttClient.get(); #if MQTT_SUPPORTS_TLS if (moduleConfig.mqtt.tls_enabled) { @@ -502,7 +502,7 @@ void MQTT::reconnect() LOG_INFO("Use non-TLS-encrypted session"); } #endif - if (connectPubSub(config, pubSub, *clientConnection)) { + if (connectPubSub(ps_config, pubSub, *clientConnection)) { enabled = true; // Start running background process again runASAP = true; reconnectCount = 0; diff --git a/suppressions.txt b/suppressions.txt index 04937523d..ab57c9298 100644 --- a/suppressions.txt +++ b/suppressions.txt @@ -53,4 +53,8 @@ internalAstError:*/CrossPlatformCryptoEngine.cpp uninitMemberVar:*/AudioThread.h // False positive constVariableReference:*/Channels.cpp -constParameterPointer:*/unishox2.c \ No newline at end of file +constParameterPointer:*/unishox2.c + +useStlAlgorithm + +variableScope \ No newline at end of file From 9c7d16fc64bb45816201c1b04d89fe0799c040ca Mon Sep 17 00:00:00 2001 From: Jason P Date: Mon, 2 Jun 2025 21:59:11 -0500 Subject: [PATCH 214/265] More pixel wrangling so things line up NodeList edition --- src/graphics/draw/NodeListRenderer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index c356484cb..8781ca400 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -323,7 +323,7 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16 if (strlen(distStr) > 0) { int offset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column) - : (isLeftCol ? 5 : 8); // Offset for Narrow 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); From 572b2de504ea9cc85bb4e1e50f1396a18d637382 Mon Sep 17 00:00:00 2001 From: Jason P Date: Mon, 2 Jun 2025 22:31:03 -0500 Subject: [PATCH 215/265] Adjust NodeList alignments and plumb some background padding for a possible title fix --- src/graphics/SharedUIDisplay.cpp | 9 +++++++++ src/graphics/draw/NodeListRenderer.cpp | 6 ++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index dc5850c87..191a86386 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -179,6 +179,15 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) int iconW = 16, iconH = 12; int iconX = iconRightEdge - iconW; int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; + if (isInverted) { + display->setColor(BLACK); + display->drawRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); + display->setColor(WHITE); + } else { + display->setColor(WHITE); + display->drawRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); + display->setColor(BLACK); + } display->drawRect(iconX, iconY, iconW + 1, iconH); display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index 8781ca400..e2765e53f 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -201,6 +201,8 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int } int rightEdge = x + columnWidth - timeOffset; + if (timeStr[strlen(timeStr) - 1] == 'm') // Fix the fact that our fonts don't line up well all the time + rightEdge -= 1; int textWidth = display->getStringWidth(timeStr); display->drawString(rightEdge - textWidth, y, timeStr); } @@ -210,8 +212,8 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int bool isLeftCol = (x < SCREEN_WIDTH / 2); int nameMaxWidth = columnWidth - 25; - int barsOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 16 : 20) : (isLeftCol ? 15 : 19); - int hopOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 17 : 25) : (isLeftCol ? 13 : 17); + int barsOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19); + int hopOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17); int barsXOffset = columnWidth - barsOffset; From 6746fe23877b7b8602384441dc3b61305b7cbecc Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Mon, 2 Jun 2025 22:38:29 -0500 Subject: [PATCH 216/265] Better alignment for banner notifications --- src/graphics/draw/NotificationRenderer.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 9ec45a70a..27e0b5288 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -106,8 +106,11 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp // loop through lines finding \n characters while ((lineCount < 10) && (lineStarts[lineCount] < alertEnd)) { - lineStarts[lineCount + 1] = std::find(lineStarts[lineCount], alertEnd, '\n') + 1; + lineStarts[lineCount + 1] = std::find(lineStarts[lineCount], alertEnd, '\n'); lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount]; + if (lineStarts[lineCount + 1][0] == '\n') { + lineStarts[lineCount + 1] += 1; // Move the start pointer beyond the \n + } lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true); if (lineWidths[lineCount] > maxWidth) { maxWidth = lineWidths[lineCount]; From e7f153ae48f1690dce589406c4b81cab5614b39f Mon Sep 17 00:00:00 2001 From: Jason P Date: Mon, 2 Jun 2025 23:24:50 -0500 Subject: [PATCH 217/265] Move title into drawCommonHeader; initial screen tested --- src/graphics/SharedUIDisplay.cpp | 51 ++++++++++++++++--- src/graphics/SharedUIDisplay.h | 2 +- src/graphics/draw/NodeListRenderer.cpp | 20 +------- .../Telemetry/EnvironmentTelemetry.cpp | 2 +- src/modules/Telemetry/PowerTelemetry.cpp | 2 +- 5 files changed, 48 insertions(+), 29 deletions(-) diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 191a86386..763c28b71 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -40,7 +40,7 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, // ************************* // * Common Header Drawing * // ************************* -void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr) { constexpr int HEADER_OFFSET_Y = 1; y += HEADER_OFFSET_Y; @@ -70,6 +70,14 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) } } + // === Screen Title === + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(SCREEN_WIDTH / 2, y, titleStr); + if (config.display.heading_bold) { + display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr); + } + display->setTextAlignment(TEXT_ALIGN_LEFT); + // === Battery State === int chargePercent = powerStatus->getBatteryChargePercent(); bool isCharging = powerStatus->getIsCharging() == meshtastic::OptionalBool::OptTrue; @@ -156,7 +164,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) timeX = screenW - xOffset - timeStrWidth + 4; // === Show Mail or Mute Icon to the Left of Time === - int iconRightEdge = timeX - 2; + int iconRightEdge = timeX; bool showMail = false; @@ -180,30 +188,59 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) int iconX = iconRightEdge - iconW; int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1; if (isInverted) { - display->setColor(BLACK); - display->drawRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); + display->setColor(BLACK); } else { - display->setColor(WHITE); - display->drawRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2); + display->setColor(WHITE); } display->drawRect(iconX, iconY, iconW + 1, iconH); display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4); display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4); } else { - int iconX = iconRightEdge - mail_width; + int iconX = iconRightEdge - (mail_width - 2); int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + if (isInverted) { + display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2); + display->setColor(WHITE); + } display->drawXbm(iconX, iconY, mail_width, mail_height, mail); } } else if (isMuted) { if (useBigIcons) { int iconX = iconRightEdge - mute_symbol_big_width; int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2; + + if (isInverted) { + display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2); + display->setColor(WHITE); + } display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big); } else { int iconX = iconRightEdge - mute_symbol_width; int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2; + + if (isInverted) { + display->setColor(WHITE); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); + display->setColor(BLACK); + } else { + display->setColor(BLACK); + display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2); + display->setColor(WHITE); + } display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol); } } diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index 5a1fa74d5..bf515b035 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -43,6 +43,6 @@ extern bool isMuted; void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r); // Shared battery/time/mail header -void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y); +void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = ""); } // namespace graphics diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index e2765e53f..522e8171b 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -431,25 +431,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t display->clear(); // Draw the battery/time header - graphics::drawCommonHeader(display, x, y); - - // Draw the centered title within the header - const int highlightHeight = COMMON_HEADER_HEIGHT; - const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - const int centerX = x + SCREEN_WIDTH / 2; - - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) - display->setColor(BLACK); - - display->drawString(centerX, textY, title); - if (config.display.heading_bold) - display->drawString(centerX + 1, textY, title); - - display->setColor(WHITE); - display->setTextAlignment(TEXT_ALIGN_LEFT); + graphics::drawCommonHeader(display, x, y, title); // Space below header y += COMMON_HEADER_HEIGHT; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index ea796fca6..d88fc0bed 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -32,7 +32,7 @@ namespace graphics { -extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y); +extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr); } #if __has_include() #include "Sensor/AHT10.h" diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index 55ad3a3fd..66cf1e901 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -24,7 +24,7 @@ namespace graphics { -extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y); +extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr); } int32_t PowerTelemetryModule::runOnce() From 2cc2002675ca78b38bf8f752c532952e62254253 Mon Sep 17 00:00:00 2001 From: Jason P Date: Tue, 3 Jun 2025 06:57:19 -0500 Subject: [PATCH 218/265] Fonts make spacing items difficult --- src/graphics/SharedUIDisplay.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 763c28b71..af79aadd5 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -164,7 +164,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti timeX = screenW - xOffset - timeStrWidth + 4; // === Show Mail or Mute Icon to the Left of Time === - int iconRightEdge = timeX; + int iconRightEdge = timeX - 1; bool showMail = false; From 25fbf5844493260d210acd515b329b46b2c0b093 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 3 Jun 2025 07:08:31 -0500 Subject: [PATCH 219/265] Improved beeping booping and other buzzer based feedback (#6947) * Improved beeping booping and other buzzer based feedback * audible button feedback (#6949) * Refactor --------- Co-authored-by: todd-herbert --- src/ButtonThread.cpp | 117 ++++++++++++++++++ src/ButtonThread.h | 24 ++++ src/buzz/buzz.cpp | 33 +++++ src/buzz/buzz.h | 5 +- src/graphics/niche/InkHUD/Events.cpp | 13 ++ src/modules/CannedMessageModule.cpp | 2 + variants/ELECROW-ThinkNode-M1/nicheGraphics.h | 13 +- .../heltec_vision_master_e213/nicheGraphics.h | 8 +- .../heltec_vision_master_e290/nicheGraphics.h | 8 +- 9 files changed, 218 insertions(+), 5 deletions(-) diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp index d898d4839..a423b4f36 100644 --- a/src/ButtonThread.cpp +++ b/src/ButtonThread.cpp @@ -172,6 +172,7 @@ void ButtonThread::sendAdHocPosition() screen->print("Sent ad-hoc nodeinfo\n"); screen->forceDisplay(true); // Force a new UI frame, then force an EInk update } + playComboTune(); } int32_t ButtonThread::runOnce() @@ -197,10 +198,62 @@ int32_t ButtonThread::runOnce() canSleep &= userButtonTouch.isIdle(); #endif + // Check for combination timeout + if (waitingForLongPress && (millis() - shortPressTime) > BUTTON_COMBO_TIMEOUT_MS) { + waitingForLongPress = false; + } + + // Check if we should play lead-up sound during long press + // Play lead-up when button has been held for BUTTON_LEADUP_MS but before long press triggers +#if defined(BUTTON_PIN) || defined(USERPREFS_BUTTON_PIN) || defined(ARCH_PORTDUINO) + bool buttonCurrentlyPressed = false; +#if defined(BUTTON_PIN) || defined(USERPREFS_BUTTON_PIN) + // Read the actual physical state of the button pin +#if !defined(USERPREFS_BUTTON_PIN) + int buttonPin = config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN; +#else + int buttonPin = config.device.button_gpio ? config.device.button_gpio : USERPREFS_BUTTON_PIN; +#endif + buttonCurrentlyPressed = isButtonPressed(buttonPin); +#elif defined(ARCH_PORTDUINO) + if (settingsMap.count(user) != 0 && settingsMap[user] != RADIOLIB_NC) { + // For portduino, assume active low + buttonCurrentlyPressed = isButtonPressed(settingsMap[user]); + } +#endif + + static uint32_t buttonPressStartTime = 0; + static bool buttonWasPressed = false; + + // Detect start of button press + if (buttonCurrentlyPressed && !buttonWasPressed) { + buttonPressStartTime = millis(); + leadUpPlayed = false; + } + + // Check if we should play lead-up sound + if (buttonCurrentlyPressed && !leadUpPlayed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS && + (millis() - buttonPressStartTime) < BUTTON_LONGPRESS_MS) { + playLongPressLeadUp(); + leadUpPlayed = true; + } + + // Reset when button is released + if (!buttonCurrentlyPressed && buttonWasPressed) { + leadUpPlayed = false; + } + + buttonWasPressed = buttonCurrentlyPressed; +#endif + if (btnEvent != BUTTON_EVENT_NONE) { switch (btnEvent) { case BUTTON_EVENT_PRESSED: { LOG_BUTTON("press!"); + + // Play boop sound for every button press + playBoop(); + // If a nag notification is running, stop it and prevent other actions if (moduleConfig.external_notification.enabled && (externalNotificationModule->nagCycleCutoff != UINT32_MAX)) { externalNotificationModule->stopNow(); @@ -210,12 +263,24 @@ int32_t ButtonThread::runOnce() sendAdHocPosition(); break; #endif + + // Start tracking for potential combination + waitingForLongPress = true; + shortPressTime = millis(); + switchPage(); break; } case BUTTON_EVENT_PRESSED_SCREEN: { LOG_BUTTON("AltPress!"); + + // Play boop sound for every button press + playBoop(); + + // Reset combination tracking + waitingForLongPress = false; + #ifdef ELECROW_ThinkNode_M1 // If a nag notification is running, stop it and prevent other actions if (moduleConfig.external_notification.enabled && (externalNotificationModule->nagCycleCutoff != UINT32_MAX)) { @@ -235,6 +300,12 @@ int32_t ButtonThread::runOnce() case BUTTON_EVENT_DOUBLE_PRESSED: { LOG_BUTTON("Double press!"); + // Play boop sound for every button press + playBoop(); + + // Reset combination tracking + waitingForLongPress = false; + #ifdef ELECROW_ThinkNode_M1 digitalWrite(PIN_EINK_EN, digitalRead(PIN_EINK_EN) == LOW); break; @@ -250,6 +321,13 @@ int32_t ButtonThread::runOnce() case BUTTON_EVENT_MULTI_PRESSED: { LOG_BUTTON("Mulitipress! %hux", multipressClickCount); + + // Play boop sound for every button press + playBoop(); + + // Reset combination tracking + waitingForLongPress = false; + switch (multipressClickCount) { #if HAS_GPS && !defined(ELECROW_ThinkNode_M1) // 3 clicks: toggle GPS @@ -307,6 +385,18 @@ int32_t ButtonThread::runOnce() case BUTTON_EVENT_LONG_PRESSED: { LOG_BUTTON("Long press!"); + + // Check if this is part of a short-press + long-press combination + if (waitingForLongPress && (millis() - shortPressTime) <= BUTTON_COMBO_TIMEOUT_MS) { + LOG_BUTTON("Combo detected: short-press + long-press!"); + btnEvent = BUTTON_EVENT_COMBO_SHORT_LONG; + waitingForLongPress = false; + break; + } + + // Reset combination tracking + waitingForLongPress = false; + powerFSM.trigger(EVENT_PRESS); if (screen) { @@ -314,6 +404,8 @@ int32_t ButtonThread::runOnce() screen->showOverlayBanner("Shutting Down..."); // Display for 3 seconds } + // Lead-up sound already played during button hold + // Just a simple beep to confirm long press threshold reached playBeep(); break; } @@ -322,6 +414,10 @@ int32_t ButtonThread::runOnce() // may wake the board immediatedly. case BUTTON_EVENT_LONG_RELEASED: { LOG_INFO("Shutdown from long press"); + + // Reset combination tracking + waitingForLongPress = false; + playShutdownMelody(); delay(3000); power->shutdown(); @@ -332,6 +428,13 @@ int32_t ButtonThread::runOnce() #ifdef BUTTON_PIN_TOUCH case BUTTON_EVENT_TOUCH_LONG_PRESSED: { LOG_BUTTON("Touch press!"); + + // Play boop sound for every button press + playBoop(); + + // Reset combination tracking + waitingForLongPress = false; + // Ignore if: no screen if (!screen) break; @@ -353,6 +456,20 @@ int32_t ButtonThread::runOnce() } #endif // BUTTON_PIN_TOUCH + case BUTTON_EVENT_COMBO_SHORT_LONG: { + // Placeholder for short-press + long-press combination + LOG_BUTTON("Short-press + Long-press combination detected!"); + + // Play the combination tune + playComboTune(); + + // Optionally show a message on screen + if (screen) { + screen->showOverlayBanner("Combo Tune Played", 2000); + } + break; + } + default: break; } diff --git a/src/ButtonThread.h b/src/ButtonThread.h index 22ead4156..05fa46892 100644 --- a/src/ButtonThread.h +++ b/src/ButtonThread.h @@ -16,6 +16,14 @@ #define BUTTON_TOUCH_MS 400 #endif +#ifndef BUTTON_COMBO_TIMEOUT_MS +#define BUTTON_COMBO_TIMEOUT_MS 2000 // 2 seconds to complete the combination +#endif + +#ifndef BUTTON_LEADUP_MS +#define BUTTON_LEADUP_MS 2500 // Play lead-up sound after 2.5 seconds of holding +#endif + class ButtonThread : public concurrency::OSThread { public: @@ -30,6 +38,7 @@ class ButtonThread : public concurrency::OSThread BUTTON_EVENT_LONG_PRESSED, BUTTON_EVENT_LONG_RELEASED, BUTTON_EVENT_TOUCH_LONG_PRESSED, + BUTTON_EVENT_COMBO_SHORT_LONG, }; ButtonThread(); @@ -41,6 +50,14 @@ class ButtonThread : public concurrency::OSThread void setScreenFlag(bool flag) { screen_flag = flag; } bool getScreenFlag() { return screen_flag; } bool isInterceptingAndFocused(); + bool isButtonPressed(int buttonPin) + { +#ifdef BUTTON_ACTIVE_LOW + return !digitalRead(buttonPin); // Active low: pressed = LOW +#else + return digitalRead(buttonPin); // Most buttons are active low by default +#endif + } // Disconnect and reconnect interrupts for light sleep #ifdef ARCH_ESP32 @@ -74,6 +91,13 @@ class ButtonThread : public concurrency::OSThread // Store click count during callback, for later use volatile int multipressClickCount = 0; + // Combination tracking state + bool waitingForLongPress = false; + uint32_t shortPressTime = 0; + + // Long press lead-up tracking + bool leadUpPlayed = false; + static void wakeOnIrq(int irq, int mode); static void sendAdHocPosition(); diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index 6ba2f4140..8c7d4ec2b 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -87,3 +87,36 @@ void playShutdownMelody() ToneDuration melody[] = {{NOTE_CS4, DURATION_1_8}, {NOTE_AS3, DURATION_1_8}, {NOTE_FS3, DURATION_1_4}}; playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } + +void playBoop() +{ + // A short, friendly "boop" sound for button presses + ToneDuration melody[] = {{NOTE_A3, 50}}; // Very short A3 note + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} + +void playLongPressLeadUp() +{ + // An ascending lead-up sequence for long press - builds anticipation + ToneDuration melody[] = { + {NOTE_C3, 100}, // Start low + {NOTE_E3, 100}, // Step up + {NOTE_G3, 100}, // Keep climbing + {NOTE_B3, 150} // Peak with longer note for emphasis + }; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} + +void playComboTune() +{ + // Quick high-pitched notes with trills + ToneDuration melody[] = { + {NOTE_G3, 80}, // Quick chirp + {NOTE_B3, 60}, // Higher chirp + {NOTE_CS4, 80}, // Even higher + {NOTE_G3, 60}, // Quick trill down + {NOTE_CS4, 60}, // Quick trill up + {NOTE_B3, 120} // Ending chirp + }; + playTones(melody, sizeof(melody) / sizeof(ToneDuration)); +} diff --git a/src/buzz/buzz.h b/src/buzz/buzz.h index adeaca73d..75afe6d90 100644 --- a/src/buzz/buzz.h +++ b/src/buzz/buzz.h @@ -5,4 +5,7 @@ void playLongBeep(); void playStartMelody(); void playShutdownMelody(); void playGPSEnableBeep(); -void playGPSDisableBeep(); \ No newline at end of file +void playGPSDisableBeep(); +void playComboTune(); +void playBoop(); +void playLongPressLeadUp(); \ No newline at end of file diff --git a/src/graphics/niche/InkHUD/Events.cpp b/src/graphics/niche/InkHUD/Events.cpp index ee6c04938..73d27fe56 100644 --- a/src/graphics/niche/InkHUD/Events.cpp +++ b/src/graphics/niche/InkHUD/Events.cpp @@ -3,6 +3,7 @@ #include "./Events.h" #include "RTC.h" +#include "buzz.h" #include "modules/AdminModule.h" #include "modules/TextMessageModule.h" #include "sleep.h" @@ -37,6 +38,10 @@ void InkHUD::Events::begin() void InkHUD::Events::onButtonShort() { + // Audio feedback (via buzzer) + // Short low tone + playBoop(); + // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { @@ -55,6 +60,10 @@ void InkHUD::Events::onButtonShort() void InkHUD::Events::onButtonLong() { + // Audio feedback (via buzzer) + // Low tone, longer than playBoop + playBeep(); + // Check which system applet wants to handle the button press (if any) SystemApplet *consumer = nullptr; for (SystemApplet *sa : inkhud->systemApplets) { @@ -102,6 +111,10 @@ int InkHUD::Events::beforeDeepSleep(void *unused) inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false); delay(1000); // Cooldown, before potentially yanking display power + // InkHUD shutdown complete + // Firmware shutdown continues for several seconds more; flash write still pending + playShutdownMelody(); + return 0; // We agree: deep sleep now } diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index af7acc736..0d48a7376 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -10,6 +10,7 @@ #include "NodeDB.h" #include "PowerFSM.h" // needed for button bypass #include "SPILock.h" +#include "buzz.h" #include "detect/ScanI2C.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" @@ -874,6 +875,7 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha simulatedPacket.from = 0; // Local device screen->handleTextMessage(&simulatedPacket); } + playComboTune(); } int32_t CannedMessageModule::runOnce() { diff --git a/variants/ELECROW-ThinkNode-M1/nicheGraphics.h b/variants/ELECROW-ThinkNode-M1/nicheGraphics.h index c2c351925..f3b709261 100644 --- a/variants/ELECROW-ThinkNode-M1/nicheGraphics.h +++ b/variants/ELECROW-ThinkNode-M1/nicheGraphics.h @@ -22,6 +22,9 @@ #include "graphics/niche/Drivers/EInk/GDEY0154D67.h" #include "graphics/niche/Inputs/TwoButton.h" +// Button feedback +#include "buzz.h" + void setupNicheGraphics() { using namespace NicheGraphics; @@ -98,8 +101,14 @@ void setupNicheGraphics() buttons->setWiring(1, PIN_BUTTON1); buttons->setTiming(1, 50, 500); // 500ms before latch buttons->setHandlerDown(1, [backlight]() { backlight->peek(); }); - buttons->setHandlerLongPress(1, [backlight]() { backlight->latch(); }); - buttons->setHandlerShortPress(1, [backlight]() { backlight->off(); }); + buttons->setHandlerLongPress(1, [backlight]() { + backlight->latch(); + playBeep(); + }); + buttons->setHandlerShortPress(1, [backlight]() { + backlight->off(); + playBoop(); + }); // Begin handling button events buttons->start(); diff --git a/variants/heltec_vision_master_e213/nicheGraphics.h b/variants/heltec_vision_master_e213/nicheGraphics.h index 7eccb2955..c0063ba3f 100644 --- a/variants/heltec_vision_master_e213/nicheGraphics.h +++ b/variants/heltec_vision_master_e213/nicheGraphics.h @@ -21,6 +21,9 @@ #include "graphics/niche/Drivers/EInk/LCMEN2R13EFC1.h" #include "graphics/niche/Inputs/TwoButton.h" +// Button feedback +#include "buzz.h" + void setupNicheGraphics() { using namespace NicheGraphics; @@ -85,7 +88,10 @@ void setupNicheGraphics() // #1: Aux Button buttons->setWiring(1, BUTTON_PIN_SECONDARY); - buttons->setHandlerShortPress(1, [inkhud]() { inkhud->nextTile(); }); + buttons->setHandlerShortPress(1, [inkhud]() { + inkhud->nextTile(); + playBoop(); + }); // Begin handling button events buttons->start(); diff --git a/variants/heltec_vision_master_e290/nicheGraphics.h b/variants/heltec_vision_master_e290/nicheGraphics.h index af78df746..e5b487158 100644 --- a/variants/heltec_vision_master_e290/nicheGraphics.h +++ b/variants/heltec_vision_master_e290/nicheGraphics.h @@ -34,6 +34,9 @@ Different NicheGraphics UIs and different hardware variants will each have their #include "graphics/niche/Drivers/EInk/DEPG0290BNS800.h" #include "graphics/niche/Inputs/TwoButton.h" +// Button feedback +#include "buzz.h" + void setupNicheGraphics() { using namespace NicheGraphics; @@ -98,7 +101,10 @@ void setupNicheGraphics() // #1: Aux Button buttons->setWiring(1, BUTTON_PIN_SECONDARY); - buttons->setHandlerShortPress(1, [inkhud]() { inkhud->nextTile(); }); + buttons->setHandlerShortPress(1, [inkhud]() { + inkhud->nextTile(); + playBoop(); + }); // Begin handling button events buttons->start(); From 4b60f8de05a0ecc7f04f106650fa0878530a4f84 Mon Sep 17 00:00:00 2001 From: Jason P Date: Tue, 3 Jun 2025 07:31:54 -0500 Subject: [PATCH 220/265] Sandpapered the corners of the notification popup --- src/graphics/draw/NotificationRenderer.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 27e0b5288..612e8bab2 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -139,6 +139,12 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Slightly oversized box display->setColor(WHITE); display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border + display->setColor(BLACK); + display->fillRect(boxLeft, boxTop, 1, 1); // Top Left + display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); // Top Right + display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); // Bottom Left + display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); // Bottom Right + display->setColor(WHITE); // === Draw each line centered in the box === int16_t lineY = boxTop + padding; From 51fc8b356873d5da880d0515702490d4f4a4be5a Mon Sep 17 00:00:00 2001 From: Jason P Date: Tue, 3 Jun 2025 08:20:35 -0500 Subject: [PATCH 221/265] Finalize drawCommonHeader migration --- src/graphics/draw/DebugRenderer.cpp | 41 +++---------------- src/graphics/draw/NodeListRenderer.cpp | 21 ++-------- src/graphics/draw/UIRenderer.cpp | 41 +++---------------- .../Telemetry/EnvironmentTelemetry.cpp | 23 ++--------- src/modules/Telemetry/PowerTelemetry.cpp | 20 ++------- 5 files changed, 21 insertions(+), 125 deletions(-) diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 36f8a9186..02a1a0bdb 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -399,26 +399,11 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - // === Header === - graphics::drawCommonHeader(display, x, y); - - // === Draw title (aligned with header baseline) === - const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + // === Set Title const char *titleStr = (SCREEN_WIDTH > 128) ? "LoRa Info" : "LoRa"; - const int centerX = x + SCREEN_WIDTH / 2; - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->setColor(BLACK); - } - - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(centerX, textY, titleStr); - if (config.display.heading_bold) { - display->drawString(centerX + 1, textY, titleStr); - } - display->setColor(WHITE); - display->setTextAlignment(TEXT_ALIGN_LEFT); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); // === First Row: Region / BLE Name === graphics::UIRenderer::drawNodes(display, x, compactFirstLine + 3, nodeStatus, 0, true, ""); @@ -527,25 +512,11 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); - // === Header === - graphics::drawCommonHeader(display, x, y); - - // === Draw title === - const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + // === Set Title const char *titleStr = (SCREEN_WIDTH > 128) ? "System" : "Sys"; - const int centerX = x + SCREEN_WIDTH / 2; - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->setColor(BLACK); - } - - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(centerX, textY, titleStr); - if (config.display.heading_bold) { - display->drawString(centerX + 1, textY, titleStr); - } - display->setColor(WHITE); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); // === Layout === const int yPositions[6] = {moreCompactFirstLine, moreCompactSecondLine, moreCompactThirdLine, diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index 522e8171b..2d465fffa 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -604,27 +604,12 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t display->clear(); - // Draw battery/time/mail header (common across screens) - graphics::drawCommonHeader(display, x, y); - - // Draw the short node name centered at the top, with bold shadow if set - const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - const int centerX = x + SCREEN_WIDTH / 2; + // === Set Title const char *shortName = (node->has_user && graphics::UIRenderer::haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) - display->setColor(BLACK); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_SMALL); - display->drawString(centerX, textY, shortName); - if (config.display.heading_bold) - display->drawString(centerX + 1, textY, shortName); - - display->setColor(WHITE); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); + // === Header === + graphics::drawCommonHeader(display, x, y, shortName); // Dynamic row stacking with predefined Y positions const int yPositions[5] = {moreCompactFirstLine, moreCompactSecondLine, moreCompactThirdLine, moreCompactFourthLine, diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 021191f4e..c16f7c41e 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -306,25 +306,11 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t display->clear(); - // === Draw battery/time/mail header (common across screens) === - graphics::drawCommonHeader(display, x, y); - // === Draw the short node name centered at the top, with bold shadow if set === - const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; - const int centerX = x + SCREEN_WIDTH / 2; const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) - display->setColor(BLACK); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_SMALL); - display->drawString(centerX, textY, shortName); - if (config.display.heading_bold) - display->drawString(centerX + 1, textY, shortName); - display->setColor(WHITE); - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(FONT_SMALL); + // === Draw battery/time/mail header (common across screens) === + graphics::drawCommonHeader(display, x, y, shortName); // ===== DYNAMIC ROW STACKING WITH YOUR MACROS ===== // 1. Each potential info row has a macro-defined Y position (not regular increments!). @@ -569,7 +555,7 @@ void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t display->setFont(FONT_SMALL); // === Header === - graphics::drawCommonHeader(display, x, y); + graphics::drawCommonHeader(display, x, y, ""); // === Content below header === @@ -949,26 +935,11 @@ void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *stat display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - // === Header === - graphics::drawCommonHeader(display, x, y); - - // === Draw title === - const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + // === Set Title const char *titleStr = "GPS"; - const int centerX = x + SCREEN_WIDTH / 2; - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->setColor(BLACK); - } - - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(centerX, textY, titleStr); - if (config.display.heading_bold) { - display->drawString(centerX + 1, textY, titleStr); - } - display->setColor(WHITE); - display->setTextAlignment(TEXT_ALIGN_LEFT); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); // === First Row: My Location === #if HAS_GPS diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index d88fc0bed..37eeb049a 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -344,28 +344,11 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); - // Draw shared header bar (battery, time, etc.) - graphics::drawCommonHeader(display, x, y); - - // === Draw Title (Centered under header) === - const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int titleY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + // === Set Title const char *titleStr = (SCREEN_WIDTH > 128) ? "Environment" : "Env."; - const int centerX = x + SCREEN_WIDTH / 2; - - // Use black text on white background if in inverted mode - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) - display->setColor(BLACK); - - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(centerX, titleY, titleStr); // Centered title - if (config.display.heading_bold) - display->drawString(centerX + 1, titleY, titleStr); // Bold effect via 1px offset - - // Restore text color & alignment - display->setColor(WHITE); - display->setTextAlignment(TEXT_ALIGN_LEFT); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); // === Row spacing setup === const int rowHeight = FONT_HEIGHT_SMALL - 4; diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index 66cf1e901..4de886de6 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -113,25 +113,11 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - graphics::drawCommonHeader(display, x, y); // Shared UI header - - // === Draw title (aligned with header baseline) === - const int highlightHeight = FONT_HEIGHT_SMALL - 1; - const int textY = y + 1 + (highlightHeight - FONT_HEIGHT_SMALL) / 2; + // === Set Title const char *titleStr = (SCREEN_WIDTH > 128) ? "Power Telem." : "Power"; - const int centerX = x + SCREEN_WIDTH / 2; - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->setColor(BLACK); - } - - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(centerX, textY, titleStr); - if (config.display.heading_bold) { - display->drawString(centerX + 1, textY, titleStr); - } - display->setColor(WHITE); - display->setTextAlignment(TEXT_ALIGN_LEFT); + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); if (lastMeasurementPacket == nullptr) { // In case of no valid packet, display "Power Telemetry", "No measurement" From f995295ad8ba4728400a62a40c13c23debd254f3 Mon Sep 17 00:00:00 2001 From: Jason P Date: Tue, 3 Jun 2025 08:44:59 -0500 Subject: [PATCH 222/265] Update Title of Favorite Node Screens --- src/graphics/draw/UIRenderer.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index c16f7c41e..095696b84 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -306,11 +306,13 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t display->clear(); - // === Draw the short node name centered at the top, with bold shadow if set === + // === Create the shortName and title string === const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; + char titlestr[32] = {0}; + snprintf(titlestr, sizeof(titlestr), (SCREEN_WIDTH > 128) ? "Fave: %s" : "Fav: %s", shortName); // === Draw battery/time/mail header (common across screens) === - graphics::drawCommonHeader(display, x, y, shortName); + graphics::drawCommonHeader(display, x, y, titlestr); // ===== DYNAMIC ROW STACKING WITH YOUR MACROS ===== // 1. Each potential info row has a macro-defined Y position (not regular increments!). From 34eecc0820810c49ce856a81dbc3fca1bea2128a Mon Sep 17 00:00:00 2001 From: Jason P Date: Tue, 3 Jun 2025 14:22:02 -0500 Subject: [PATCH 223/265] Update node metric alignment on LoRa screen --- src/graphics/draw/DebugRenderer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 02a1a0bdb..ea4294d8b 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -406,7 +406,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, graphics::drawCommonHeader(display, x, y, titleStr); // === First Row: Region / BLE Name === - graphics::UIRenderer::drawNodes(display, x, compactFirstLine + 3, nodeStatus, 0, true, ""); + graphics::UIRenderer::drawNodes(display, x, compactFirstLine + 2, nodeStatus, 0, true, ""); uint8_t dmac[6]; char shortnameble[35]; From 50b3da262dcc9cad7ce746c7aaf316264c6792cb Mon Sep 17 00:00:00 2001 From: Jason P Date: Tue, 3 Jun 2025 15:35:34 -0500 Subject: [PATCH 224/265] Update the border for popups to separate it from background --- src/graphics/draw/NotificationRenderer.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 612e8bab2..fb55f1ceb 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -137,6 +137,10 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp // === Draw background box === display->setColor(BLACK); display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Slightly oversized box + display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); // Top Line + display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); // Bottom Line + display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); // Left Line + display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); // Right Line display->setColor(WHITE); display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border display->setColor(BLACK); From 993f6449546f74c6ad3cc5c88a4e14cb2cffc638 Mon Sep 17 00:00:00 2001 From: Jason P Date: Tue, 3 Jun 2025 16:18:12 -0500 Subject: [PATCH 225/265] Update PaxcounterModule.cpp with CommonHeader --- src/modules/esp32/PaxcounterModule.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/modules/esp32/PaxcounterModule.cpp b/src/modules/esp32/PaxcounterModule.cpp index b27586771..e83cb5b3c 100644 --- a/src/modules/esp32/PaxcounterModule.cpp +++ b/src/modules/esp32/PaxcounterModule.cpp @@ -3,6 +3,9 @@ #include "Default.h" #include "MeshService.h" #include "PaxcounterModule.h" +#include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" +#include "graphics/images.h" #include PaxcounterModule *paxcounterModule; @@ -115,16 +118,25 @@ int32_t PaxcounterModule::runOnce() void PaxcounterModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + + // === Set Title + const char *titleStr = "Pax"; + + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + char buffer[50]; display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - display->drawString(x + 0, y + 0, "PAX"); libpax_counter_count(&count_from_libpax); display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_SMALL); - display->drawStringf(display->getWidth() / 2 + x, 0 + y + 12, buffer, "WiFi: %d\nBLE: %d\nuptime: %ds", + display->drawStringf(display->getWidth() / 2 + x, compactFirstLine, buffer, "WiFi: %d\nBLE: %d\nUptime: %ds", count_from_libpax.wifi_count, count_from_libpax.ble_count, millis() / 1000); } #endif // HAS_SCREEN From c41757c2f429793e09e10d11b529fe5ae274fb7b Mon Sep 17 00:00:00 2001 From: Jason P Date: Tue, 3 Jun 2025 16:39:28 -0500 Subject: [PATCH 226/265] Update WiFi screen with CommonHeader and related data reflow --- src/graphics/draw/DebugRenderer.cpp | 54 +++++++++++++---------------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index ea4294d8b..402c32baf 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -164,40 +164,34 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16 #endif } +// **************************** +// * WiFi Screen * +// **************************** void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { #if HAS_WIFI && !defined(ARCH_PORTDUINO) - const char *wifiName = config.network.wifi_ssid; - + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); + // === Set Title + const char *titleStr = "WiFi"; - if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) { - display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - } + // === Header === + graphics::drawCommonHeader(display, x, y, titleStr); + + const char *wifiName = config.network.wifi_ssid; if (WiFi.status() != WL_CONNECTED) { - display->drawString(x, y, "WiFi: Not Connected"); - if (config.display.heading_bold) - display->drawString(x + 1, y, "WiFi: Not Connected"); + display->drawString(x, moreCompactFirstLine, "WiFi: Not Connected"); } else { - display->drawString(x, y, "WiFi: Connected"); - if (config.display.heading_bold) - display->drawString(x + 1, y, "WiFi: Connected"); + display->drawString(x, moreCompactFirstLine, "WiFi: Connected"); char rssiStr[32]; - snprintf(rssiStr, sizeof(rssiStr), "RSSI %d", WiFi.RSSI()); - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(rssiStr), y, rssiStr); - if (config.display.heading_bold) { - display->drawString(x + SCREEN_WIDTH - display->getStringWidth(rssiStr) - 1, y, rssiStr); - } + snprintf(rssiStr, sizeof(rssiStr), "RSSI: %d", WiFi.RSSI()); + display->drawString(x, moreCompactSecondLine, rssiStr); } - display->setColor(WHITE); - /* - WL_CONNECTED: assigned when connected to a WiFi network; - WL_NO_SSID_AVAIL: assigned when no SSID are available; @@ -213,36 +207,36 @@ void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, i if (WiFi.status() == WL_CONNECTED) { char ipStr[64]; snprintf(ipStr, sizeof(ipStr), "IP: %s", WiFi.localIP().toString().c_str()); - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, ipStr); + display->drawString(x, moreCompactThirdLine, ipStr); } else if (WiFi.status() == WL_NO_SSID_AVAIL) { - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "SSID Not Found"); + display->drawString(x, moreCompactThirdLine, "SSID Not Found"); } else if (WiFi.status() == WL_CONNECTION_LOST) { - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Connection Lost"); + display->drawString(x, moreCompactThirdLine, "Connection Lost"); } else if (WiFi.status() == WL_IDLE_STATUS) { - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Idle ... Reconnecting"); + display->drawString(x, moreCompactThirdLine, "Idle ... Reconnecting"); } else if (WiFi.status() == WL_CONNECT_FAILED) { - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, "Connection Failed"); + display->drawString(x, moreCompactThirdLine, "Connection Failed"); } #ifdef ARCH_ESP32 else { // Codes: // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, + display->drawString(x, moreCompactThirdLine, WiFi.disconnectReasonName(static_cast(getWifiDisconnectReason()))); } #else else { char statusStr[32]; snprintf(statusStr, sizeof(statusStr), "Unknown status: %d", WiFi.status()); - display->drawString(x, y + FONT_HEIGHT_SMALL * 1, statusStr); + display->drawString(x, moreCompactThirdLine, statusStr); } #endif char ssidStr[64]; snprintf(ssidStr, sizeof(ssidStr), "SSID: %s", wifiName); - display->drawString(x, y + FONT_HEIGHT_SMALL * 2, ssidStr); + display->drawString(x, moreCompactFourthLine, ssidStr); - display->drawString(x, y + FONT_HEIGHT_SMALL * 3, "http://meshtastic.local"); + display->drawString(x, moreCompactFifthLine, "URL: http://meshtastic.local"); /* Display a heartbeat pixel that blinks every time the frame is redrawn */ #ifdef SHOW_REDRAWS From 4089cee59d5af047108919eaf097477f05b46ed3 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Tue, 3 Jun 2025 17:13:16 -0500 Subject: [PATCH 227/265] It was not, in fact, pointing up --- src/graphics/draw/CompassRenderer.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/graphics/draw/CompassRenderer.cpp b/src/graphics/draw/CompassRenderer.cpp index b55dd5141..7c577a739 100644 --- a/src/graphics/draw/CompassRenderer.cpp +++ b/src/graphics/draw/CompassRenderer.cpp @@ -58,9 +58,9 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) { - Point tip(0.0f, 0.5f), tail(0.0f, -0.35f); // pointing up initially - float arrowOffsetX = 0.14f, arrowOffsetY = 1.0f; - Point leftArrow(tip.x - arrowOffsetX, tip.y - arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y - arrowOffsetY); + Point tip(0.0f, -0.5f), tail(0.0f, 0.35f); // pointing up initially + float arrowOffsetX = 0.14f, arrowOffsetY = 0.9f; + Point leftArrow(tip.x - arrowOffsetX, tip.y + arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y + arrowOffsetY); Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow}; From 34f22c40b39f4863776d0bde0e570b196cecb4ea Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Tue, 3 Jun 2025 17:23:12 -0500 Subject: [PATCH 228/265] Fix build on wismeshtap --- src/modules/CannedMessageModule.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 0d48a7376..1f2240ba2 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -628,7 +628,7 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) currentMessageIndex = -1; shift = false; valid = true; - } else if (!keyTapped.isEmpty()) { + } else if (!(keyTapped == "")) { #ifndef RAK14014 highlight = keyTapped[0]; #endif From f1cebe9d1df79b02ced6dc784b3dc7f55b9a647d Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Tue, 3 Jun 2025 18:23:36 -0500 Subject: [PATCH 229/265] T-deck trackball debounce --- src/input/TrackballInterruptBase.cpp | 22 ++++++++++++++++++++-- src/input/TrackballInterruptBase.h | 1 + 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp index e35da3622..ca6de610d 100644 --- a/src/input/TrackballInterruptBase.cpp +++ b/src/input/TrackballInterruptBase.cpp @@ -40,7 +40,24 @@ int32_t TrackballInterruptBase::runOnce() { InputEvent e; e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; - +#if defined(T_DECK) // T-deck gets a super-simple debounce on trackball + if (this->action == TB_ACTION_PRESSED) { + // LOG_DEBUG("Trackball event Press"); + e.inputEvent = this->_eventPressed; + } else if (this->action == TB_ACTION_UP && lastEvent == TB_ACTION_UP) { + // LOG_DEBUG("Trackball event UP"); + e.inputEvent = this->_eventUp; + } else if (this->action == TB_ACTION_DOWN && lastEvent == TB_ACTION_DOWN) { + // LOG_DEBUG("Trackball event DOWN"); + e.inputEvent = this->_eventDown; + } else if (this->action == TB_ACTION_LEFT && lastEvent == TB_ACTION_LEFT) { + // LOG_DEBUG("Trackball event LEFT"); + e.inputEvent = this->_eventLeft; + } else if (this->action == TB_ACTION_RIGHT && lastEvent == TB_ACTION_RIGHT) { + // LOG_DEBUG("Trackball event RIGHT"); + e.inputEvent = this->_eventRight; + } +#else if (this->action == TB_ACTION_PRESSED) { // LOG_DEBUG("Trackball event Press"); e.inputEvent = this->_eventPressed; @@ -57,13 +74,14 @@ int32_t TrackballInterruptBase::runOnce() // LOG_DEBUG("Trackball event RIGHT"); e.inputEvent = this->_eventRight; } +#endif if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) { e.source = this->_originName; e.kbchar = 0x00; this->notifyObservers(&e); } - + lastEvent = action; this->action = TB_ACTION_NONE; return 100; diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h index e7fc99f54..77a468165 100644 --- a/src/input/TrackballInterruptBase.h +++ b/src/input/TrackballInterruptBase.h @@ -41,4 +41,5 @@ class TrackballInterruptBase : public Observable, public con char _eventRight = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; char _eventPressed = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; const char *_originName; + TrackballInterruptBaseActionType lastEvent = TB_ACTION_NONE; }; From c847ae05096681d1d53e1a974a61bd5e728d66bb Mon Sep 17 00:00:00 2001 From: Jason P Date: Tue, 3 Jun 2025 19:22:44 -0500 Subject: [PATCH 230/265] Fix uptime on Device Focused page to actually detail --- src/graphics/draw/UIRenderer.cpp | 34 ++++++++++++-------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 095696b84..ccfc4a150 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -576,29 +576,21 @@ void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t ((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)) + 2, nodeStatus, -1, false, "online"); + char uptimeStr[32] = ""; uint32_t uptime = millis() / 1000; - char uptimeStr[6]; - uint32_t minutes = uptime / 60, hours = minutes / 60, days = hours / 24; - - if (days > 365) { - snprintf(uptimeStr, sizeof(uptimeStr), "?"); - } else { - snprintf(uptimeStr, sizeof(uptimeStr), "%u%c", - days ? days - : hours ? hours - : minutes ? minutes - : (int)uptime, - days ? 'd' - : hours ? 'h' - : minutes ? 'm' - : 's'); - } - - char uptimeFullStr[16]; - snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); - display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeFullStr), + uint32_t days = uptime / 86400; + uint32_t hours = (uptime % 86400) / 3600; + uint32_t mins = (uptime % 3600) / 60; + // Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m" + if (days) + snprintf(uptimeStr, sizeof(uptimeStr), " Up: %ud %uh", days, hours); + else if (hours) + snprintf(uptimeStr, sizeof(uptimeStr), " Up: %uh %um", hours, mins); + else + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %um", mins); + display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), ((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)), - uptimeFullStr); + uptimeStr); // === Second Row: Satellites and Voltage === config.display.heading_bold = false; From fc00af4b55ca5c16acb91f82b191f2d6d28d9b36 Mon Sep 17 00:00:00 2001 From: Jason P Date: Tue, 3 Jun 2025 21:16:11 -0500 Subject: [PATCH 231/265] Update Sys screen for new uptime, add label to Freq/Chan on LoRa --- src/graphics/draw/DebugRenderer.cpp | 42 +++++++++++++---------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 402c32baf..b8aa3e353 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -428,9 +428,13 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, float freq = RadioLibInterface::instance->getFreq(); snprintf(freqStr, sizeof(freqStr), "%.3f", freq); if (config.lora.channel_num == 0) { - snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %s", freqStr); + snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %smhz", freqStr); } else { - snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Chan: %s (%d)", freqStr, config.lora.channel_num); + if (SCREEN_WIDTH > 128) { + snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Chan: %smhz (%d)", freqStr, config.lora.channel_num); + } else { + snprintf(frequencyslot, sizeof(frequencyslot), "Frq/Ch: %smhz (%d)", freqStr, config.lora.channel_num); + } } size_t len = strlen(frequencyslot); if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { @@ -614,29 +618,21 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line < 4)) { // Only show uptime if the screen can show it line += 1; + char uptimeStr[32] = ""; uint32_t uptime = millis() / 1000; - char uptimeStr[6]; - uint32_t minutes = uptime / 60, hours = minutes / 60, days = hours / 24; - - if (days > 365) { - snprintf(uptimeStr, sizeof(uptimeStr), "?"); - } else { - snprintf(uptimeStr, sizeof(uptimeStr), "%u%c", - days ? days - : hours ? hours - : minutes ? minutes - : (int)uptime, - days ? 'd' - : hours ? 'h' - : minutes ? 'm' - : 's'); - } - - char uptimeFullStr[16]; - snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr); - textWidth = display->getStringWidth(uptimeFullStr); + uint32_t days = uptime / 86400; + uint32_t hours = (uptime % 86400) / 3600; + uint32_t mins = (uptime % 3600) / 60; + // Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m" + if (days) + snprintf(uptimeStr, sizeof(uptimeStr), " Up: %ud %uh", days, hours); + else if (hours) + snprintf(uptimeStr, sizeof(uptimeStr), " Up: %uh %um", hours, mins); + else + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %um", mins); + textWidth = display->getStringWidth(uptimeStr); nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, yPositions[line], uptimeFullStr); + display->drawString(nameX, yPositions[line], uptimeStr); } } } // namespace DebugRenderer From 1ad5766dbb5be0726e444dd82898f13e36fa9300 Mon Sep 17 00:00:00 2001 From: Jason P Date: Wed, 4 Jun 2025 16:29:23 -0500 Subject: [PATCH 232/265] Don't display DOP any longer, make Uptime consistent --- src/graphics/draw/UIRenderer.cpp | 56 ++++++-------------------------- 1 file changed, 10 insertions(+), 46 deletions(-) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index ccfc4a150..c88c9c777 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -37,62 +37,26 @@ namespace UIRenderer // Draw GPS status summary void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) { + // Draw satellite image + display->drawFastImage(x + 2, y, 8, 8, imgSatellite); + if (config.position.fixed_position) { // GPS coordinates are currently fixed - display->drawString(x - 1, y - 2, "Fixed GPS"); - if (config.display.heading_bold) - display->drawString(x, y - 2, "Fixed GPS"); + display->drawString(x + 12, y - 2, (SCREEN_WIDTH > 128) ? "GPS: Fixed" : "Fixed"); return; } if (!gps->getIsConnected()) { - display->drawString(x, y - 2, "No GPS"); - if (config.display.heading_bold) - display->drawString(x + 1, y - 2, "No GPS"); + display->drawString(x + 12, y - 2, (SCREEN_WIDTH > 128) ? "GPS: No Lock" : "No Lock"); return; } - // Adjust position if we're going to draw too wide - int maxDrawWidth = 6; // Position icon - - if (!gps->getHasLock()) { - maxDrawWidth += display->getStringWidth("No sats") + 2; // icon + text + buffer - } else { - maxDrawWidth += (5 * 2) + 8 + display->getStringWidth("99") + 2; // bars + sat icon + text + buffer - } - - if (x + maxDrawWidth > display->getWidth()) { - x = display->getWidth() - maxDrawWidth; - if (x < 0) - x = 0; // Clamp to screen - } - - display->drawFastImage(x, y, 6, 8, gps->getHasLock() ? imgPositionSolid : imgPositionEmpty); if (!gps->getHasLock()) { // Draw "No sats" to the right of the icon with slightly more gap - int textX = x + 9; // 6 (icon) + 3px spacing - display->drawString(textX, y - 3, "No sats"); - if (config.display.heading_bold) - display->drawString(textX + 1, y - 3, "No sats"); + display->drawString(x + 12, y - 3, (SCREEN_WIDTH > 128) ? "GPS: No Sats" : "No Sats"); return; } else { char satsString[3]; - uint8_t bar[2] = {0}; - - // Draw DOP signal bars - for (int i = 0; i < 5; i++) { - if (gps->getDOP() <= dopThresholds[i]) - bar[0] = ~((1 << (5 - i)) - 1); - else - bar[0] = 0b10000000; - - display->drawFastImage(x + 9 + (i * 2), y, 2, 8, bar); - } - - // Draw satellite image - display->drawFastImage(x + 24, y, 8, 8, imgSatellite); - - // Draw the number of satellites snprintf(satsString, sizeof(satsString), "%u", gps->getNumSatellites()); - int textX = x + 34; + int textX = x + 12; display->drawString(textX, y - 2, satsString); if (config.display.heading_bold) display->drawString(textX + 1, y - 2, satsString); @@ -390,11 +354,11 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t uint32_t mins = (uptime % 3600) / 60; // Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m" if (days) - snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %ud %uh", days, hours); + snprintf(uptimeStr, sizeof(uptimeStr), "Up: %ud %uh", days, hours); else if (hours) - snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %uh %um", hours, mins); + snprintf(uptimeStr, sizeof(uptimeStr), "Up: %uh %um", hours, mins); else - snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %um", mins); + snprintf(uptimeStr, sizeof(uptimeStr), "Up: %um", mins); } if (uptimeStr[0] && line < 5) { display->drawString(x, yPositions[line++], uptimeStr); From e7c30092476d77f3994b21ba2c716fa17649daad Mon Sep 17 00:00:00 2001 From: Jason P Date: Wed, 4 Jun 2025 16:34:13 -0500 Subject: [PATCH 233/265] Revert Uptime change on Favorites, Apply to Device Focused --- src/graphics/draw/UIRenderer.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index c88c9c777..7d301ead1 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -354,11 +354,11 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t uint32_t mins = (uptime % 3600) / 60; // Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m" if (days) - snprintf(uptimeStr, sizeof(uptimeStr), "Up: %ud %uh", days, hours); + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %ud %uh", days, hours); else if (hours) - snprintf(uptimeStr, sizeof(uptimeStr), "Up: %uh %um", hours, mins); + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %uh %um", hours, mins); else - snprintf(uptimeStr, sizeof(uptimeStr), "Up: %um", mins); + snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %um", mins); } if (uptimeStr[0] && line < 5) { display->drawString(x, yPositions[line++], uptimeStr); @@ -547,11 +547,11 @@ void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t uint32_t mins = (uptime % 3600) / 60; // Show as "Up: 2d 3h", "Up: 5h 14m", or "Up: 37m" if (days) - snprintf(uptimeStr, sizeof(uptimeStr), " Up: %ud %uh", days, hours); + snprintf(uptimeStr, sizeof(uptimeStr), "Up: %ud %uh", days, hours); else if (hours) - snprintf(uptimeStr, sizeof(uptimeStr), " Up: %uh %um", hours, mins); + snprintf(uptimeStr, sizeof(uptimeStr), "Up: %uh %um", hours, mins); else - snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %um", mins); + snprintf(uptimeStr, sizeof(uptimeStr), "Up: %um", mins); display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), ((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)), uptimeStr); From 5894a99338a461291b514ba1d7f3240280c54c1f Mon Sep 17 00:00:00 2001 From: Jason P Date: Wed, 4 Jun 2025 16:51:37 -0500 Subject: [PATCH 234/265] Label the satelite number to avoid confusion --- src/graphics/draw/UIRenderer.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 7d301ead1..2325c7a6c 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -54,12 +54,9 @@ void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSSt display->drawString(x + 12, y - 3, (SCREEN_WIDTH > 128) ? "GPS: No Sats" : "No Sats"); return; } else { - char satsString[3]; - snprintf(satsString, sizeof(satsString), "%u", gps->getNumSatellites()); - int textX = x + 12; - display->drawString(textX, y - 2, satsString); - if (config.display.heading_bold) - display->drawString(textX + 1, y - 2, satsString); + char satsString[8]; + snprintf(satsString, sizeof(satsString), "%u sats", gps->getNumSatellites()); + display->drawString(x + 12, y - 2, satsString); } } From fe0a64da8089076df5357442fddb771af17209a0 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 4 Jun 2025 19:04:48 -0500 Subject: [PATCH 235/265] Boop boop boop boop --- src/ButtonThread.cpp | 23 +++++++++++++++++++---- src/ButtonThread.h | 4 +++- src/buzz/buzz.cpp | 29 +++++++++++++++++++++++++++++ src/buzz/buzz.h | 4 +++- 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp index a423b4f36..4c6a7d4c4 100644 --- a/src/ButtonThread.cpp +++ b/src/ButtonThread.cpp @@ -229,18 +229,33 @@ int32_t ButtonThread::runOnce() if (buttonCurrentlyPressed && !buttonWasPressed) { buttonPressStartTime = millis(); leadUpPlayed = false; + leadUpSequenceActive = false; + resetLeadUpSequence(); } - // Check if we should play lead-up sound - if (buttonCurrentlyPressed && !leadUpPlayed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS && + // Progressive lead-up sound system + if (buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS && (millis() - buttonPressStartTime) < BUTTON_LONGPRESS_MS) { - playLongPressLeadUp(); - leadUpPlayed = true; + + // Start the progressive sequence if not already active + if (!leadUpSequenceActive) { + leadUpSequenceActive = true; + lastLeadUpNoteTime = millis(); + playNextLeadUpNote(); // Play the first note immediately + } + // Continue playing notes at intervals + else if ((millis() - lastLeadUpNoteTime) >= 400) { // 400ms interval between notes + if (playNextLeadUpNote()) { + lastLeadUpNoteTime = millis(); + } + } } // Reset when button is released if (!buttonCurrentlyPressed && buttonWasPressed) { leadUpPlayed = false; + leadUpSequenceActive = false; + resetLeadUpSequence(); } buttonWasPressed = buttonCurrentlyPressed; diff --git a/src/ButtonThread.h b/src/ButtonThread.h index 05fa46892..1fbc38672 100644 --- a/src/ButtonThread.h +++ b/src/ButtonThread.h @@ -21,7 +21,7 @@ #endif #ifndef BUTTON_LEADUP_MS -#define BUTTON_LEADUP_MS 2500 // Play lead-up sound after 2.5 seconds of holding +#define BUTTON_LEADUP_MS 2200 // Play lead-up sound after 2.5 seconds of holding #endif class ButtonThread : public concurrency::OSThread @@ -97,6 +97,8 @@ class ButtonThread : public concurrency::OSThread // Long press lead-up tracking bool leadUpPlayed = false; + uint32_t lastLeadUpNoteTime = 0; + bool leadUpSequenceActive = false; static void wakeOnIrq(int irq, int mode); diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index 8c7d4ec2b..fc113dcae 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -107,6 +107,35 @@ void playLongPressLeadUp() playTones(melody, sizeof(melody) / sizeof(ToneDuration)); } +// Static state for progressive lead-up notes +static int leadUpNoteIndex = 0; +static const ToneDuration leadUpNotes[] = { + {NOTE_C3, 100}, // Start low + {NOTE_E3, 100}, // Step up + {NOTE_G3, 100}, // Keep climbing + {NOTE_B3, 150} // Peak with longer note for emphasis +}; +static const int leadUpNotesCount = sizeof(leadUpNotes) / sizeof(ToneDuration); + +bool playNextLeadUpNote() +{ + if (leadUpNoteIndex >= leadUpNotesCount) { + return false; // All notes have been played + } + + // Use playTones to handle buzzer logic consistently + const auto ¬e = leadUpNotes[leadUpNoteIndex]; + playTones(¬e, 1); // Play single note using existing playTones function + + leadUpNoteIndex++; + return true; // Note was played (playTones handles buzzer availability internally) +} + +void resetLeadUpSequence() +{ + leadUpNoteIndex = 0; +} + void playComboTune() { // Quick high-pitched notes with trills diff --git a/src/buzz/buzz.h b/src/buzz/buzz.h index 75afe6d90..4b7302383 100644 --- a/src/buzz/buzz.h +++ b/src/buzz/buzz.h @@ -8,4 +8,6 @@ void playGPSEnableBeep(); void playGPSDisableBeep(); void playComboTune(); void playBoop(); -void playLongPressLeadUp(); \ No newline at end of file +void playLongPressLeadUp(); +bool playNextLeadUpNote(); // Play the next note in the lead-up sequence +void resetLeadUpSequence(); // Reset the lead-up sequence to start from beginning \ No newline at end of file From fa3161f4c3660d17ff4e0ee05711db05c1c24da2 Mon Sep 17 00:00:00 2001 From: Jason P Date: Wed, 4 Jun 2025 22:44:07 -0500 Subject: [PATCH 236/265] Correct GPS positioning and string consistency across strings for GPS --- src/graphics/draw/UIRenderer.cpp | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 2325c7a6c..b9873d180 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -38,26 +38,24 @@ namespace UIRenderer void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) { // Draw satellite image - display->drawFastImage(x + 2, y, 8, 8, imgSatellite); + int yOffset = (SCREEN_WIDTH > 128) ? 4 : 2; + display->drawFastImage(x + 1, y + yOffset, 8, 8, imgSatellite); + char textString[10]; if (config.position.fixed_position) { // GPS coordinates are currently fixed - display->drawString(x + 12, y - 2, (SCREEN_WIDTH > 128) ? "GPS: Fixed" : "Fixed"); - return; + snprintf(textString, sizeof(textString), "Fixed"); } if (!gps->getIsConnected()) { - display->drawString(x + 12, y - 2, (SCREEN_WIDTH > 128) ? "GPS: No Lock" : "No Lock"); - return; + snprintf(textString, sizeof(textString), "No Lock"); } if (!gps->getHasLock()) { // Draw "No sats" to the right of the icon with slightly more gap - display->drawString(x + 12, y - 3, (SCREEN_WIDTH > 128) ? "GPS: No Sats" : "No Sats"); - return; + snprintf(textString, sizeof(textString), "No Sats"); } else { - char satsString[8]; - snprintf(satsString, sizeof(satsString), "%u sats", gps->getNumSatellites()); - display->drawString(x + 12, y - 2, satsString); + snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites()); } + display->drawString(x + 12, y, textString); } // Draw status when GPS is disabled or not present @@ -569,8 +567,7 @@ void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t displayLine); } else { UIRenderer::drawGps( - display, 0, - ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)) + 3, + display, 0, ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), gpsStatus); } #endif @@ -901,8 +898,6 @@ void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *stat bool origBold = config.display.heading_bold; config.display.heading_bold = false; - const char *Satelite_String = "Sat:"; - display->drawString(0, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), Satelite_String); const char *displayLine = ""; // Initialize to empty string by default if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) { if (config.position.fixed_position) { @@ -910,12 +905,10 @@ void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *stat } else { displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } - display->drawString(display->getStringWidth(Satelite_String) + 3, - ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), displayLine); + display->drawFastImage(x + 1, y, 8, 8, imgSatellite); + display->drawString(x + 12, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), displayLine); } else { - displayLine = "GPS enabled"; // Set a value when GPS is enabled - UIRenderer::drawGps(display, display->getStringWidth(Satelite_String) + 3, - ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine) + 3, gpsStatus); + UIRenderer::drawGps(display, 0, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), gpsStatus); } config.display.heading_bold = origBold; From aec6a9254877fb1a6dcb8c23dc2ebcd9bfc536a6 Mon Sep 17 00:00:00 2001 From: Jason P Date: Wed, 4 Jun 2025 22:48:35 -0500 Subject: [PATCH 237/265] Fix GPS text alignment --- src/graphics/draw/UIRenderer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index b9873d180..c10647a71 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -55,7 +55,7 @@ void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSSt } else { snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites()); } - display->drawString(x + 12, y, textString); + display->drawString(x + 11, y, textString); } // Draw status when GPS is disabled or not present @@ -906,7 +906,7 @@ void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *stat displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } display->drawFastImage(x + 1, y, 8, 8, imgSatellite); - display->drawString(x + 12, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), displayLine); + display->drawString(x + 11, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), displayLine); } else { UIRenderer::drawGps(display, 0, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), gpsStatus); } From c6343939a4f64545363a6a94f00ca7fd8875636c Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 5 Jun 2025 06:31:15 -0500 Subject: [PATCH 238/265] Enable canned messages by default --- src/mesh/NodeDB.cpp | 2 +- src/modules/CannedMessageModule.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 27c02f18b..bfd6b6e8f 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -768,7 +768,7 @@ void NodeDB::installDefaultModuleConfig() #endif moduleConfig.has_canned_message = true; - + moduleConfig.canned_message.enabled = true; #if USERPREFS_MQTT_ENABLED && !MESHTASTIC_EXCLUDE_MQTT moduleConfig.mqtt.enabled = true; #endif diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 1f2240ba2..a169c02d2 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -1696,7 +1696,7 @@ bool CannedMessageModule::saveProtoForModule() */ void CannedMessageModule::installDefaultCannedMessageModuleConfig() { - memset(cannedMessageModuleConfig.messages, 0, sizeof(cannedMessageModuleConfig.messages)); + strncpy(cannedMessageModuleConfig.messages, "Hi|Bye|Yes|No|Ok", sizeof(cannedMessageModuleConfig.messages)); } /** From 0a8c7662b911b959f7852a66e5e39fef2ab38636 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 5 Jun 2025 20:05:29 -0500 Subject: [PATCH 239/265] Don't wake screen on new nodes --- src/PowerFSM.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index 333f73610..398e56097 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -338,10 +338,10 @@ void PowerFSM_setup() // if any packet destined for phone arrives, turn on bluetooth at least powerFSM.add_transition(&stateNB, &stateDARK, EVENT_PACKET_FOR_PHONE, NULL, "Packet for phone"); - // show the latest node when we get a new node db update - powerFSM.add_transition(&stateNB, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); - powerFSM.add_transition(&stateDARK, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); - powerFSM.add_transition(&stateON, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); + // Removed 2.7: we don't show the nodes individually for every node on the screen anymore + // powerFSM.add_transition(&stateNB, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); + // powerFSM.add_transition(&stateDARK, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); + // powerFSM.add_transition(&stateON, &stateON, EVENT_NODEDB_UPDATED, NULL, "NodeDB update"); // Show the received text message powerFSM.add_transition(&stateLS, &stateON, EVENT_RECEIVED_MSG, NULL, "Received text"); From 0c1d49e254ea41c058a37af9525c4303ca2ca904 Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 5 Jun 2025 21:22:32 -0400 Subject: [PATCH 240/265] Cannedmessage list emote support added --- src/modules/CannedMessageModule.cpp | 143 ++++++++++++++++++++++++---- 1 file changed, 124 insertions(+), 19 deletions(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index a169c02d2..c2556cdcb 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -15,6 +15,7 @@ #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" +#include "graphics/emotes.h" #include "input/ScanAndSelect.h" #include "main.h" // for cardkb_found #include "mesh/generated/meshtastic/cannedmessages.pb.h" @@ -1574,36 +1575,140 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); - const int rowSpacing = FONT_HEIGHT_SMALL - 4; + // ====== Precompute per-row heights based on emotes (centered if present) ====== + const int baseRowSpacing = FONT_HEIGHT_SMALL - 4; + + int topMsg; + std::vector rowHeights; + int visibleRows; // Draw header (To: ...) drawHeader(display, x, y, buffer); // Shift message list upward by 3 pixels to reduce spacing between header and first message const int listYOffset = y + FONT_HEIGHT_SMALL - 3; - visibleRows = (display->getHeight() - listYOffset) / rowSpacing; + visibleRows = (display->getHeight() - listYOffset) / baseRowSpacing; - int topMsg = + // Figure out which messages are visible and their needed heights + topMsg = (messagesCount > visibleRows && currentMessageIndex >= visibleRows - 1) ? currentMessageIndex - visibleRows + 2 : 0; + int countRows = std::min(messagesCount, visibleRows); - for (int i = 0; i < std::min(messagesCount, visibleRows); i++) { - int lineY = listYOffset + rowSpacing * i; + // --- Build per-row max height based on all emotes in line --- + for (int i = 0; i < countRows; i++) { const char *msg = getMessageByIndex(topMsg + i); - - if ((topMsg + i) == currentMessageIndex) { -#ifdef USE_EINK - display->drawString(x + 0, lineY, ">"); - display->drawString(x + 12, lineY, msg); -#else - int scrollPadding = 8; - display->fillRect(x + 0, lineY + 2, display->getWidth() - scrollPadding, FONT_HEIGHT_SMALL - 5); - display->setColor(BLACK); - display->drawString(x + 2, lineY, msg); - display->setColor(WHITE); -#endif - } else { - display->drawString(x + 0, lineY, msg); + int maxEmoteHeight = 0; + for (int j = 0; j < graphics::numEmotes; j++) { + const char* label = graphics::emotes[j].label; + if (!label || !*label) continue; + const char* search = msg; + while ((search = strstr(search, label))) { + if (graphics::emotes[j].height > maxEmoteHeight) maxEmoteHeight = graphics::emotes[j].height; + search += strlen(label); // Advance past this emote + } } + rowHeights.push_back(std::max(baseRowSpacing, maxEmoteHeight + 2)); + } + + // --- Draw all message rows with multi-emote support --- + int yCursor = listYOffset; + for (int vis = 0; vis < countRows; vis++) { + int msgIdx = topMsg + vis; + int lineY = yCursor; + const char *msg = getMessageByIndex(msgIdx); + int rowHeight = rowHeights[vis]; + bool highlight = (msgIdx == currentMessageIndex); + + // --- Multi-emote tokenization --- + std::vector> tokens; // (isEmote, token) + int pos = 0; + int msgLen = strlen(msg); + while (pos < msgLen) { + const graphics::Emote* foundEmote = nullptr; + int foundAt = -1, foundLen = 0; + + // Look for any emote at this pos (prefer longest match) + for (int j = 0; j < graphics::numEmotes; j++) { + const char* label = graphics::emotes[j].label; + int labelLen = strlen(label); + if (labelLen == 0) continue; + if (strncmp(msg + pos, label, labelLen) == 0) { + if (!foundEmote || labelLen > foundLen) { + foundEmote = &graphics::emotes[j]; + foundAt = pos; + foundLen = labelLen; + } + } + } + + if (foundEmote && foundAt == pos) { + // Emote at current pos + tokens.emplace_back(true, String(foundEmote->label)); + pos += foundLen; + } else { + // Find next emote + int nextEmote = msgLen; + for (int j = 0; j < graphics::numEmotes; j++) { + const char* label = graphics::emotes[j].label; + if (label[0] == 0) continue; + char* found = strstr(msg + pos, label); + if (found && (found - msg) < nextEmote) { + nextEmote = found - msg; + } + } + int textLen = (nextEmote > pos) ? (nextEmote - pos) : (msgLen - pos); + if (textLen > 0) { + tokens.emplace_back(false, String(msg + pos).substring(0, textLen)); + pos += textLen; + } else { + break; + } + } + } + // --- End multi-emote tokenization --- + + // Vertically center based on rowHeight + int textYOffset = (rowHeight - FONT_HEIGHT_SMALL) / 2; + +#ifdef USE_EINK + int nextX = x + (highlight ? 12 : 0); + if (highlight) display->drawString(x + 0, lineY + textYOffset, ">"); +#else + int scrollPadding = 8; + if (highlight) { + display->fillRect(x + 0, lineY, display->getWidth() - scrollPadding, rowHeight); + display->setColor(BLACK); + } + int nextX = x + (highlight ? 2 : 0); +#endif + + // Draw all tokens left to right + for (auto& token : tokens) { + if (token.first) { + // Emote + const graphics::Emote* emote = nullptr; + for (int j = 0; j < graphics::numEmotes; j++) { + if (token.second == graphics::emotes[j].label) { + emote = &graphics::emotes[j]; + break; + } + } + if (emote) { + int emoteYOffset = (rowHeight - emote->height) / 2; + display->drawXbm(nextX, lineY + emoteYOffset, emote->width, emote->height, emote->bitmap); + nextX += emote->width + 2; + } + } else { + // Text + display->drawString(nextX, lineY + textYOffset, token.second); + nextX += display->getStringWidth(token.second); + } + } +#ifndef USE_EINK + if (highlight) display->setColor(WHITE); +#endif + + yCursor += rowHeight; } // Scrollbar From 97eb03cb35d35fadffb92ecc4e3914fe800bebee Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 6 Jun 2025 00:12:04 -0400 Subject: [PATCH 241/265] Fn+e emote picker for freetext screen --- src/input/InputBroker.h | 1 + src/input/kbI2cBase.cpp | 1 + src/modules/CannedMessageModule.cpp | 325 +++++++++++++++++++++++++--- src/modules/CannedMessageModule.h | 10 +- 4 files changed, 307 insertions(+), 30 deletions(-) diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index 72084dad3..3278a5a73 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -20,6 +20,7 @@ #define INPUT_BROKER_MSG_FN_SYMBOL_OFF 0xf2 #define INPUT_BROKER_MSG_BLUETOOTH_TOGGLE 0xAA #define INPUT_BROKER_MSG_TAB 0x09 +#define INPUT_BROKER_MSG_EMOTE_LIST 0x8F typedef struct _InputEvent { const char *source; diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp index 70e9e4365..83bac7c74 100644 --- a/src/input/kbI2cBase.cpp +++ b/src/input/kbI2cBase.cpp @@ -435,6 +435,7 @@ int32_t KbI2cBase::runOnce() case 0xaf: // fn+space INPUT_BROKER_MSG_SEND_PING case 0x8b: // fn+del INPUT_BROKEN_MSG_DISMISS_FRAME case 0xAA: // fn+b INPUT_BROKER_MSG_BLUETOOTH_TOGGLE + case 0x8F: // fn+e INPUT_BROKER_MSG_EMOTE_LIST // just pass those unmodified e.inputEvent = ANYKEY; e.kbchar = c; diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index c2556cdcb..b15ba26c2 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -298,6 +298,10 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE: return 1; + // If sending, block all input except global/system (handled above) + case CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER: + return handleEmotePickerInput(event); + case CANNED_MESSAGE_RUN_STATE_INACTIVE: if (isSelect) { // When inactive, call the onebutton shortpress instead. Activate module only on up/down @@ -647,6 +651,12 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) // ---- All hardware keys fall through to here (CardKB, physical, etc.) ---- + if (event->kbchar == INPUT_BROKER_MSG_EMOTE_LIST) { + runState = CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER; + requestFocus(); + screen->forceDisplay(); + return true; + } // Confirm select (Enter) bool isSelect = isSelectEvent(event); if (isSelect) { @@ -715,6 +725,47 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) return false; } +int CannedMessageModule::handleEmotePickerInput(const InputEvent *event) +{ + int numEmotes = graphics::numEmotes; + bool isUp = isUpEvent(event); + bool isDown = isDownEvent(event); + bool isSelect = isSelectEvent(event); + + // Scroll emote list + if (isUp && emotePickerIndex > 0) { + emotePickerIndex--; + screen->forceDisplay(); + return 1; + } + if (isDown && emotePickerIndex < numEmotes - 1) { + emotePickerIndex++; + screen->forceDisplay(); + return 1; + } + // Select emote: insert into freetext at cursor and return to freetext + if (isSelect) { + String label = graphics::emotes[emotePickerIndex].label; + String emoteInsert = label; // Just the text label, e.g., ":thumbsup:" + if (cursor == freetext.length()) { + freetext += emoteInsert; + } else { + freetext = freetext.substring(0, cursor) + emoteInsert + freetext.substring(cursor); + } + cursor += emoteInsert.length(); + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + screen->forceDisplay(); + return 1; + } + // Cancel returns to freetext + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) { + runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; + screen->forceDisplay(); + return 1; + } + return 0; +} + bool CannedMessageModule::handleSystemCommandInput(const InputEvent *event) { // Only respond to "ANYKEY" events for system keys @@ -997,14 +1048,16 @@ int32_t CannedMessageModule::runOnce() e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; switch (this->payload) { case 0x08: // backspace - if (this->freetext.length() > 0 && this->highlight == 0x00) { - if (this->cursor == this->freetext.length()) { - this->freetext = this->freetext.substring(0, this->freetext.length() - 1); - } else { - this->freetext = this->freetext.substring(0, this->cursor - 1) + - this->freetext.substring(this->cursor, this->freetext.length()); + if (this->freetext.length() > 0) { + if (this->cursor > 0) { + if (this->cursor == this->freetext.length()) { + this->freetext = this->freetext.substring(0, this->freetext.length() - 1); + } else { + this->freetext = this->freetext.substring(0, this->cursor - 1) + + this->freetext.substring(this->cursor, this->freetext.length()); + } + this->cursor--; } - this->cursor--; } break; case INPUT_BROKER_MSG_TAB: // Tab key: handled by input handler @@ -1013,19 +1066,20 @@ int32_t CannedMessageModule::runOnce() case INPUT_BROKER_MSG_RIGHT: break; default: - if (this->highlight != 0x00) - break; - if (this->cursor == this->freetext.length()) { - this->freetext += this->payload; - } else { - this->freetext = - this->freetext.substring(0, this->cursor) + this->payload + this->freetext.substring(this->cursor); - } - this->cursor += 1; - uint16_t maxChars = meshtastic_Constants_DATA_PAYLOAD_LEN - (moduleConfig.canned_message.send_bell ? 1 : 0); - if (this->freetext.length() > maxChars) { - this->cursor = maxChars; - this->freetext = this->freetext.substring(0, maxChars); + // Only insert ASCII printable characters (32–126) + if (this->payload >= 32 && this->payload <= 126) { + if (this->cursor == this->freetext.length()) { + this->freetext += (char)this->payload; + } else { + this->freetext = + this->freetext.substring(0, this->cursor) + (char)this->payload + this->freetext.substring(this->cursor); + } + this->cursor++; + uint16_t maxChars = meshtastic_Constants_DATA_PAYLOAD_LEN - (moduleConfig.canned_message.send_bell ? 1 : 0); + if (this->freetext.length() > maxChars) { + this->cursor = maxChars; + this->freetext = this->freetext.substring(0, maxChars); + } } break; } @@ -1443,6 +1497,81 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O } } +void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + const int headerFontHeight = FONT_HEIGHT_SMALL; // Make sure this matches your actual small font height + const int headerMargin = 2; // Extra pixels below header + const int labelGap = 6; + const int bitmapGapX = 4; + + // Find max emote height (assume all same, or precalculated) + int maxEmoteHeight = 0; + for (int i = 0; i < graphics::numEmotes; ++i) + if (graphics::emotes[i].height > maxEmoteHeight) + maxEmoteHeight = graphics::emotes[i].height; + + const int rowHeight = maxEmoteHeight + 2; + + // Place header at top, then compute start of emote list + int headerY = y; + int listTop = headerY + headerFontHeight + headerMargin; + + int visibleRows = (display->getHeight() - listTop - 2) / rowHeight; + int numEmotes = graphics::numEmotes; + + // Clamp highlight index + if (emotePickerIndex < 0) emotePickerIndex = 0; + if (emotePickerIndex >= numEmotes) emotePickerIndex = numEmotes - 1; + + // Determine which emote is at the top + int topIndex = emotePickerIndex - visibleRows / 2; + if (topIndex < 0) topIndex = 0; + if (topIndex > numEmotes - visibleRows) topIndex = std::max(0, numEmotes - visibleRows); + + // Draw header/title + display->setFont(FONT_SMALL); + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->drawString(display->getWidth() / 2, headerY, "Select Emote"); + + // Draw emote rows + display->setTextAlignment(TEXT_ALIGN_LEFT); + + for (int vis = 0; vis < visibleRows; ++vis) { + int emoteIdx = topIndex + vis; + if (emoteIdx >= numEmotes) break; + const graphics::Emote& emote = graphics::emotes[emoteIdx]; + int rowY = listTop + vis * rowHeight; + + // Draw highlight box 2px taller than emote (1px margin above and below) + if (emoteIdx == emotePickerIndex) { + display->fillRect(x, rowY, display->getWidth() - 8, emote.height + 2); + display->setColor(BLACK); + } + + // Emote bitmap (left), 1px margin from highlight bar top + int emoteY = rowY + 1; + display->drawXbm(x + bitmapGapX, emoteY, emote.width, emote.height, emote.bitmap); + + // Emote label (right of bitmap) + display->setFont(FONT_MEDIUM); + int labelY = rowY + ((rowHeight - FONT_HEIGHT_MEDIUM) / 2); + display->drawString(x + bitmapGapX + emote.width + labelGap, labelY, emote.label); + + if (emoteIdx == emotePickerIndex) + display->setColor(WHITE); + } + + // Draw scrollbar if needed + if (numEmotes > visibleRows) { + int scrollbarHeight = visibleRows * rowHeight; + int scrollTrackX = display->getWidth() - 6; + display->drawRect(scrollTrackX, listTop, 4, scrollbarHeight); + int scrollBarLen = std::max(6, (scrollbarHeight * visibleRows) / numEmotes); + int scrollBarPos = listTop + (scrollbarHeight * topIndex) / numEmotes; + display->fillRect(scrollTrackX, scrollBarPos, 4, scrollBarLen); + } +} + void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { this->displayHeight = display->getHeight(); // Store display height for later use @@ -1460,6 +1589,12 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st return; } + // === Emote Picker Screen === + if (this->runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) { + drawEmotePickerScreen(display, state, x, y); // <-- Call your emote picker drawer here + return; + } + // === Destination Selection === if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { drawDestinationSelectionScreen(display, state, x, y); @@ -1562,10 +1697,145 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st display->drawString(x + display->getWidth() - display->getStringWidth(buffer), y + 0, buffer); } - // --- Draw Free Text input, shifted down --- + // --- Draw Free Text input with multi-emote support and proper line wrapping --- display->setColor(WHITE); - display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(), - drawWithCursor(this->freetext, this->cursor)); + { + int inputY = 0 + y + FONT_HEIGHT_SMALL; + String msgWithCursor = this->drawWithCursor(this->freetext, this->cursor); + + // Tokenize input into (isEmote, token) pairs + std::vector> tokens; + const char* msg = msgWithCursor.c_str(); + int msgLen = strlen(msg); + int pos = 0; + while (pos < msgLen) { + const graphics::Emote* foundEmote = nullptr; + int foundLen = 0; + for (int j = 0; j < graphics::numEmotes; j++) { + const char* label = graphics::emotes[j].label; + int labelLen = strlen(label); + if (labelLen == 0) continue; + if (strncmp(msg + pos, label, labelLen) == 0) { + if (!foundEmote || labelLen > foundLen) { + foundEmote = &graphics::emotes[j]; + foundLen = labelLen; + } + } + } + if (foundEmote) { + tokens.emplace_back(true, String(foundEmote->label)); + pos += foundLen; + } else { + // Find next emote + int nextEmote = msgLen; + for (int j = 0; j < graphics::numEmotes; j++) { + const char* label = graphics::emotes[j].label; + if (!label || !*label) continue; + char* found = strstr(msg + pos, label); + if (found && (found - msg) < nextEmote) { + nextEmote = found - msg; + } + } + int textLen = (nextEmote > pos) ? (nextEmote - pos) : (msgLen - pos); + if (textLen > 0) { + tokens.emplace_back(false, String(msg + pos).substring(0, textLen)); + pos += textLen; + } else { + break; + } + } + } + + // ===== Advanced word-wrapping (emotes + text, split by word, wrap by char if needed) ===== + std::vector>> lines; + std::vector> currentLine; + int lineWidth = 0; + int maxWidth = display->getWidth(); + for (auto& token : tokens) { + if (token.first) { + // Emote + int tokenWidth = 0; + for (int j = 0; j < graphics::numEmotes; j++) { + if (token.second == graphics::emotes[j].label) { + tokenWidth = graphics::emotes[j].width + 2; + break; + } + } + if (lineWidth + tokenWidth > maxWidth && !currentLine.empty()) { + lines.push_back(currentLine); + currentLine.clear(); + lineWidth = 0; + } + currentLine.push_back(token); + lineWidth += tokenWidth; + } else { + // Text: split by words and wrap inside word if needed + String text = token.second; + int pos = 0; + while (pos < text.length()) { + // Find next space (or end) + int spacePos = text.indexOf(' ', pos); + int endPos = (spacePos == -1) ? text.length() : spacePos + 1; // Include space + String word = text.substring(pos, endPos); + int wordWidth = display->getStringWidth(word); + + if (lineWidth + wordWidth > maxWidth && lineWidth > 0) { + lines.push_back(currentLine); + currentLine.clear(); + lineWidth = 0; + } + // If word itself too big, split by character + if (wordWidth > maxWidth) { + int charPos = 0; + while (charPos < word.length()) { + String oneChar = word.substring(charPos, charPos + 1); + int charWidth = display->getStringWidth(oneChar); + if (lineWidth + charWidth > maxWidth && lineWidth > 0) { + lines.push_back(currentLine); + currentLine.clear(); + lineWidth = 0; + } + currentLine.push_back({false, oneChar}); + lineWidth += charWidth; + charPos++; + } + } else { + currentLine.push_back({false, word}); + lineWidth += wordWidth; + } + pos = endPos; + } + } + } + if (!currentLine.empty()) lines.push_back(currentLine); + + // Draw lines with emotes + int rowHeight = FONT_HEIGHT_SMALL; + int yLine = inputY; + for (auto& line : lines) { + int nextX = x; + for (auto& token : line) { + if (token.first) { + const graphics::Emote* emote = nullptr; + for (int j = 0; j < graphics::numEmotes; j++) { + if (token.second == graphics::emotes[j].label) { + emote = &graphics::emotes[j]; + break; + } + } + if (emote) { + int emoteYOffset = (rowHeight - emote->height) / 2; + display->drawXbm(nextX, yLine + emoteYOffset, emote->width, emote->height, emote->bitmap); + nextX += emote->width + 2; + } + } else { + display->drawString(nextX, yLine, token.second); + nextX += display->getStringWidth(token.second); + } + } + yLine += rowHeight; + } + } #endif return; } @@ -1625,9 +1895,9 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st int msgLen = strlen(msg); while (pos < msgLen) { const graphics::Emote* foundEmote = nullptr; - int foundAt = -1, foundLen = 0; + int foundLen = 0; - // Look for any emote at this pos (prefer longest match) + // Look for any emote label at this pos (prefer longest match) for (int j = 0; j < graphics::numEmotes; j++) { const char* label = graphics::emotes[j].label; int labelLen = strlen(label); @@ -1635,14 +1905,11 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st if (strncmp(msg + pos, label, labelLen) == 0) { if (!foundEmote || labelLen > foundLen) { foundEmote = &graphics::emotes[j]; - foundAt = pos; foundLen = labelLen; } } } - - if (foundEmote && foundAt == pos) { - // Emote at current pos + if (foundEmote) { tokens.emplace_back(true, String(foundEmote->label)); pos += foundLen; } else { diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index 49688a6a4..c08e9bedc 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -18,7 +18,8 @@ enum cannedMessageModuleRunState { CANNED_MESSAGE_RUN_STATE_ACTION_DOWN, CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION, CANNED_MESSAGE_RUN_STATE_FREETEXT, - CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION + CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION, + CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER }; enum CannedMessageModuleIconType { shift, backspace, space, enter }; @@ -57,6 +58,9 @@ class CannedMessageModule : public SinglePortModule, public Observable Date: Thu, 5 Jun 2025 23:19:29 -0500 Subject: [PATCH 242/265] Actually block CannedInput actions while display is shown --- src/modules/CannedMessageModule.cpp | 101 ++++++++++++++++------------ 1 file changed, 57 insertions(+), 44 deletions(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index b15ba26c2..838ce993a 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -14,8 +14,8 @@ #include "detect/ScanI2C.h" #include "graphics/Screen.h" #include "graphics/SharedUIDisplay.h" -#include "graphics/images.h" #include "graphics/emotes.h" +#include "graphics/images.h" #include "input/ScanAndSelect.h" #include "main.h" // for cardkb_found #include "mesh/generated/meshtastic/cannedmessages.pb.h" @@ -257,6 +257,10 @@ bool CannedMessageModule::isCharInputAllowed() const */ int CannedMessageModule::handleInputEvent(const InputEvent *event) { + // Block ALL input if an alert banner is active + if (screen && screen->isOverlayBannerShowing()) { + return 0; + } // Allow input only from configured source (hardware/software filter) if (!isInputSourceAllowed(event)) return 0; @@ -772,11 +776,6 @@ bool CannedMessageModule::handleSystemCommandInput(const InputEvent *event) if (event->inputEvent != static_cast(ANYKEY)) return false; - // Block ALL input if an alert banner is active // TODO: Make an accessor function - if (screen && screen->isOverlayBannerShowing()) { - return true; - } - // System commands (all others fall through to return false) switch (event->kbchar) { // Fn key symbols @@ -1054,7 +1053,7 @@ int32_t CannedMessageModule::runOnce() this->freetext = this->freetext.substring(0, this->freetext.length() - 1); } else { this->freetext = this->freetext.substring(0, this->cursor - 1) + - this->freetext.substring(this->cursor, this->freetext.length()); + this->freetext.substring(this->cursor, this->freetext.length()); } this->cursor--; } @@ -1071,8 +1070,8 @@ int32_t CannedMessageModule::runOnce() if (this->cursor == this->freetext.length()) { this->freetext += (char)this->payload; } else { - this->freetext = - this->freetext.substring(0, this->cursor) + (char)this->payload + this->freetext.substring(this->cursor); + this->freetext = this->freetext.substring(0, this->cursor) + (char)this->payload + + this->freetext.substring(this->cursor); } this->cursor++; uint16_t maxChars = meshtastic_Constants_DATA_PAYLOAD_LEN - (moduleConfig.canned_message.send_bell ? 1 : 0); @@ -1500,7 +1499,7 @@ void CannedMessageModule::drawDestinationSelectionScreen(OLEDDisplay *display, O void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { const int headerFontHeight = FONT_HEIGHT_SMALL; // Make sure this matches your actual small font height - const int headerMargin = 2; // Extra pixels below header + const int headerMargin = 2; // Extra pixels below header const int labelGap = 6; const int bitmapGapX = 4; @@ -1520,13 +1519,17 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla int numEmotes = graphics::numEmotes; // Clamp highlight index - if (emotePickerIndex < 0) emotePickerIndex = 0; - if (emotePickerIndex >= numEmotes) emotePickerIndex = numEmotes - 1; + if (emotePickerIndex < 0) + emotePickerIndex = 0; + if (emotePickerIndex >= numEmotes) + emotePickerIndex = numEmotes - 1; // Determine which emote is at the top int topIndex = emotePickerIndex - visibleRows / 2; - if (topIndex < 0) topIndex = 0; - if (topIndex > numEmotes - visibleRows) topIndex = std::max(0, numEmotes - visibleRows); + if (topIndex < 0) + topIndex = 0; + if (topIndex > numEmotes - visibleRows) + topIndex = std::max(0, numEmotes - visibleRows); // Draw header/title display->setFont(FONT_SMALL); @@ -1538,8 +1541,9 @@ void CannedMessageModule::drawEmotePickerScreen(OLEDDisplay *display, OLEDDispla for (int vis = 0; vis < visibleRows; ++vis) { int emoteIdx = topIndex + vis; - if (emoteIdx >= numEmotes) break; - const graphics::Emote& emote = graphics::emotes[emoteIdx]; + if (emoteIdx >= numEmotes) + break; + const graphics::Emote &emote = graphics::emotes[emoteIdx]; int rowY = listTop + vis * rowHeight; // Draw highlight box 2px taller than emote (1px margin above and below) @@ -1591,10 +1595,10 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st // === Emote Picker Screen === if (this->runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) { - drawEmotePickerScreen(display, state, x, y); // <-- Call your emote picker drawer here + drawEmotePickerScreen(display, state, x, y); // <-- Call your emote picker drawer here return; } - + // === Destination Selection === if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { drawDestinationSelectionScreen(display, state, x, y); @@ -1705,16 +1709,17 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st // Tokenize input into (isEmote, token) pairs std::vector> tokens; - const char* msg = msgWithCursor.c_str(); + const char *msg = msgWithCursor.c_str(); int msgLen = strlen(msg); int pos = 0; while (pos < msgLen) { - const graphics::Emote* foundEmote = nullptr; + const graphics::Emote *foundEmote = nullptr; int foundLen = 0; for (int j = 0; j < graphics::numEmotes; j++) { - const char* label = graphics::emotes[j].label; + const char *label = graphics::emotes[j].label; int labelLen = strlen(label); - if (labelLen == 0) continue; + if (labelLen == 0) + continue; if (strncmp(msg + pos, label, labelLen) == 0) { if (!foundEmote || labelLen > foundLen) { foundEmote = &graphics::emotes[j]; @@ -1729,9 +1734,10 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st // Find next emote int nextEmote = msgLen; for (int j = 0; j < graphics::numEmotes; j++) { - const char* label = graphics::emotes[j].label; - if (!label || !*label) continue; - char* found = strstr(msg + pos, label); + const char *label = graphics::emotes[j].label; + if (!label || !*label) + continue; + char *found = strstr(msg + pos, label); if (found && (found - msg) < nextEmote) { nextEmote = found - msg; } @@ -1751,7 +1757,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st std::vector> currentLine; int lineWidth = 0; int maxWidth = display->getWidth(); - for (auto& token : tokens) { + for (auto &token : tokens) { if (token.first) { // Emote int tokenWidth = 0; @@ -1807,16 +1813,17 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st } } } - if (!currentLine.empty()) lines.push_back(currentLine); + if (!currentLine.empty()) + lines.push_back(currentLine); // Draw lines with emotes int rowHeight = FONT_HEIGHT_SMALL; int yLine = inputY; - for (auto& line : lines) { + for (auto &line : lines) { int nextX = x; - for (auto& token : line) { + for (auto &token : line) { if (token.first) { - const graphics::Emote* emote = nullptr; + const graphics::Emote *emote = nullptr; for (int j = 0; j < graphics::numEmotes; j++) { if (token.second == graphics::emotes[j].label) { emote = &graphics::emotes[j]; @@ -1869,11 +1876,13 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st const char *msg = getMessageByIndex(topMsg + i); int maxEmoteHeight = 0; for (int j = 0; j < graphics::numEmotes; j++) { - const char* label = graphics::emotes[j].label; - if (!label || !*label) continue; - const char* search = msg; + const char *label = graphics::emotes[j].label; + if (!label || !*label) + continue; + const char *search = msg; while ((search = strstr(search, label))) { - if (graphics::emotes[j].height > maxEmoteHeight) maxEmoteHeight = graphics::emotes[j].height; + if (graphics::emotes[j].height > maxEmoteHeight) + maxEmoteHeight = graphics::emotes[j].height; search += strlen(label); // Advance past this emote } } @@ -1894,14 +1903,15 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st int pos = 0; int msgLen = strlen(msg); while (pos < msgLen) { - const graphics::Emote* foundEmote = nullptr; + const graphics::Emote *foundEmote = nullptr; int foundLen = 0; // Look for any emote label at this pos (prefer longest match) for (int j = 0; j < graphics::numEmotes; j++) { - const char* label = graphics::emotes[j].label; + const char *label = graphics::emotes[j].label; int labelLen = strlen(label); - if (labelLen == 0) continue; + if (labelLen == 0) + continue; if (strncmp(msg + pos, label, labelLen) == 0) { if (!foundEmote || labelLen > foundLen) { foundEmote = &graphics::emotes[j]; @@ -1916,9 +1926,10 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st // Find next emote int nextEmote = msgLen; for (int j = 0; j < graphics::numEmotes; j++) { - const char* label = graphics::emotes[j].label; - if (label[0] == 0) continue; - char* found = strstr(msg + pos, label); + const char *label = graphics::emotes[j].label; + if (label[0] == 0) + continue; + char *found = strstr(msg + pos, label); if (found && (found - msg) < nextEmote) { nextEmote = found - msg; } @@ -1939,7 +1950,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st #ifdef USE_EINK int nextX = x + (highlight ? 12 : 0); - if (highlight) display->drawString(x + 0, lineY + textYOffset, ">"); + if (highlight) + display->drawString(x + 0, lineY + textYOffset, ">"); #else int scrollPadding = 8; if (highlight) { @@ -1950,10 +1962,10 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st #endif // Draw all tokens left to right - for (auto& token : tokens) { + for (auto &token : tokens) { if (token.first) { // Emote - const graphics::Emote* emote = nullptr; + const graphics::Emote *emote = nullptr; for (int j = 0; j < graphics::numEmotes; j++) { if (token.second == graphics::emotes[j].label) { emote = &graphics::emotes[j]; @@ -1972,7 +1984,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st } } #ifndef USE_EINK - if (highlight) display->setColor(WHITE); + if (highlight) + display->setColor(WHITE); #endif yCursor += rowHeight; From 2df032bb06a0a75a39cb0ee3e0f7813f19da668a Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Thu, 5 Jun 2025 23:38:08 -0500 Subject: [PATCH 243/265] Add selection menu to bannerOverlay --- src/graphics/Screen.cpp | 81 ++++++++---- src/graphics/Screen.h | 13 +- src/graphics/draw/NotificationRenderer.cpp | 142 ++++++++++++++------- src/graphics/draw/NotificationRenderer.h | 14 +- 4 files changed, 166 insertions(+), 84 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 109ccb518..21cf32283 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -136,12 +136,14 @@ extern bool hasUnreadMessage; // The banner appears in the center of the screen and disappears after the specified duration // Called to trigger a banner with custom message and duration -void Screen::showOverlayBanner(const char *message, uint32_t durationMs) +void Screen::showOverlayBanner(const char *message, uint32_t durationMs, uint8_t options, std::function bannerCallback) { // Store the message and set the expiration timestamp - strncpy(alertBannerMessage, message, 255); - alertBannerMessage[255] = '\0'; // Ensure null termination - alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; + strncpy(NotificationRenderer::alertBannerMessage, message, 255); + NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination + NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; + NotificationRenderer::alertBannerOptions = options; + NotificationRenderer::alertBannerCallback = bannerCallback; } static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) @@ -1116,10 +1118,45 @@ int32_t Screen::runOnce() #endif #ifndef DISABLE_WELCOME_UNSET - if (showingNormalScreen && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { - setWelcomeFrames(); + if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) { + showOverlayBanner( + "Set the LoRa " + "region\nUS\nEU_433\nEU_868\nCN\nJP\nANZ\nKR\nTW\nRU\nIN\nNZ_865\nTH\nLORA_24\nUA_433\nUA_868\nMY_433\nMY_919\nSG_" + "923\nPH_433\nPH_868\nPH_915", + 0, 21, [](int selected) -> void { + LOG_WARN("Chose %d", selected); + config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected + 1); + if (!owner.is_licensed) { + bool keygenSuccess = false; + if (config.security.private_key.size == 32) { + if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) { + keygenSuccess = true; + } + } else { + LOG_INFO("Generate new PKI keys"); + crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes); + keygenSuccess = true; + } + if (keygenSuccess) { + config.security.public_key.size = 32; + config.security.private_key.size = 32; + owner.public_key.size = 32; + memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32); + } + } + config.lora.tx_enabled = true; + initRegion(); + if (myRegion->dutyCycle < 100) { + config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit + } + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + }); } #endif + if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) { + showOverlayBanner("Rebooting...", 0); + } // Process incoming commands. for (;;) { @@ -1226,23 +1263,12 @@ void Screen::setSSLFrames() { if (address_found.address) { // LOG_DEBUG("Show SSL frames"); - static FrameCallback sslFrames[] = {graphics::NotificationRenderer::NotificationRenderer::drawSSLScreen}; + static FrameCallback sslFrames[] = {NotificationRenderer::drawSSLScreen}; ui->setFrames(sslFrames, 1); ui->update(); } } -/* show a message that the SSL cert is being built - * it is expected that this will be used during the boot phase */ -void Screen::setWelcomeFrames() -{ - if (address_found.address) { - // LOG_DEBUG("Show Welcome frames"); - static FrameCallback frames[] = {graphics::NotificationRenderer::NotificationRenderer::drawWelcomeScreen}; - setFrameImmediateDraw(frames); - } -} - #ifdef USE_EINK /// Determine which screensaver frame to use, then set the FrameCallback void Screen::setScreensaverFrames(FrameCallback einkScreensaver) @@ -1357,7 +1383,7 @@ void Screen::setFrames(FrameFocus focus) // If we have a critical fault, show it first fsi.positions.fault = numframes; if (error_code) { - normalFrames[numframes++] = graphics::NotificationRenderer::NotificationRenderer::drawCriticalFaultFrame; + normalFrames[numframes++] = NotificationRenderer::drawCriticalFaultFrame; indicatorIcons.push_back(icon_error); focus = FOCUS_FAULT; // Change our "focus" parameter, to ensure we show the fault frame } @@ -1445,8 +1471,7 @@ void Screen::setFrames(FrameFocus focus) ui->disableAllIndicators(); // Add overlays: frame icons and alert banner) - static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, - graphics::NotificationRenderer::NotificationRenderer::drawAlertBannerOverlay}; + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawAlertBannerOverlay}; ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list @@ -1532,7 +1557,7 @@ void Screen::handleStartFirmwareUpdateScreen() showingNormalScreen = false; EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // E-Ink: Explicitly use fast-refresh for next frame - static FrameCallback frames[] = {graphics::NotificationRenderer::NotificationRenderer::drawFrameFirmware}; + static FrameCallback frames[] = {graphics::NotificationRenderer::drawFrameFirmware}; setFrameImmediateDraw(frames); } @@ -1770,7 +1795,12 @@ int Screen::handleInputEvent(const InputEvent *event) return 0; } #endif - + if (NotificationRenderer::isOverlayBannerShowing()) { + NotificationRenderer::inEvent = event->inputEvent; + setFrames(); + ui->update(); + return 0; + } // Use left or right input from a keyboard to move between frames, // so long as a mesh module isn't using these events for some other purpose if (showingNormalScreen) { @@ -1809,6 +1839,11 @@ int Screen::handleAdminMessage(const meshtastic_AdminMessage *arg) return 0; } +bool Screen::isOverlayBannerShowing() +{ + return NotificationRenderer::isOverlayBannerShowing(); +} + } // namespace graphics #else graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {} diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index fee1dad9f..fc2592c4d 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -220,8 +220,7 @@ class Screen : public concurrency::OSThread meshtastic_Config_DisplayConfig_OledType model; OLEDDISPLAY_GEOMETRY geometry; - char alertBannerMessage[256] = {0}; - uint32_t alertBannerUntil = 0; // 0 is a special case meaning forever + bool isOverlayBannerShowing(); // Stores the last 4 of our hardware ID, to make finding the device for pairing easier // FIXME: Needs refactoring and getMacAddr needs to be moved to a utility class @@ -286,12 +285,8 @@ class Screen : public concurrency::OSThread enqueueCmd(cmd); } - void showOverlayBanner(const char *message, uint32_t durationMs = 3000); - - bool isOverlayBannerShowing() - { - return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); - } + void showOverlayBanner(const char *message, uint32_t durationMs = 3000, uint8_t options = 0, + std::function bannerCallback = NULL); void startFirmwareUpdateScreen() { @@ -571,8 +566,6 @@ class Screen : public concurrency::OSThread /// Draws our SSL cert screen during boot (called from WebServer) void setSSLFrames(); - void setWelcomeFrames(); - // Dismiss the currently focussed frame, if possible (e.g. text message, waypoint) void dismissCurrentFrame(); diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index fb55f1ceb..88a14fce0 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -4,7 +4,6 @@ #include "DisplayFormatters.h" #include "NodeDB.h" #include "NotificationRenderer.h" -#include "graphics/Screen.h" #include "graphics/ScreenFonts.h" #include "graphics/SharedUIDisplay.h" #include "graphics/images.h" @@ -27,8 +26,12 @@ extern bool hasUnreadMessage; namespace graphics { -namespace NotificationRenderer -{ +char NotificationRenderer::inEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE); +int8_t NotificationRenderer::curSelected = 0; +char NotificationRenderer::alertBannerMessage[256] = {0}; +uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever +uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are seelctable options +std::function NotificationRenderer::alertBannerCallback = NULL; // Used on boot when a certificate is being created void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) @@ -36,6 +39,7 @@ void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiStat display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_SMALL); display->drawString(64 + x, y, "Creating SSL certificate"); + uint32_t alertBannerUntil = 0; // 0 is a special case meaning forever #ifdef ARCH_ESP32 yield(); @@ -50,48 +54,27 @@ void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiStat } } -// Used when booting without a region set -void NotificationRenderer::drawWelcomeScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - display->setFont(FONT_SMALL); - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(64 + x, y, "//\\ E S H T /\\ S T / C"); - display->drawString(64 + x, y + FONT_HEIGHT_SMALL, getDeviceName()); - display->setTextAlignment(TEXT_ALIGN_LEFT); - - if ((millis() / 10000) % 2) { - display->drawString(x, y + FONT_HEIGHT_SMALL * 2 - 3, "Set the region using the"); - display->drawString(x, y + FONT_HEIGHT_SMALL * 3 - 3, "Meshtastic Android, iOS,"); - display->drawString(x, y + FONT_HEIGHT_SMALL * 4 - 3, "Web or CLI clients."); - } else { - display->drawString(x, y + FONT_HEIGHT_SMALL * 2 - 3, "Visit meshtastic.org"); - display->drawString(x, y + FONT_HEIGHT_SMALL * 3 - 3, "for more information."); - display->drawString(x, y + FONT_HEIGHT_SMALL * 4 - 3, ""); - } - -#ifdef ARCH_ESP32 - yield(); - esp_task_wdt_reset(); -#endif -} - void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) { // Exit if no message is active or duration has passed - if (!screen->isOverlayBannerShowing()) + if (!isOverlayBannerShowing()) return; - + LOG_DEBUG("event: %u, curSelected: %d", inEvent, curSelected); // === Layout Configuration === constexpr uint16_t padding = 5; // Padding around text inside the box + constexpr uint16_t vPadding = 2; // Padding around text inside the box constexpr uint8_t lineSpacing = 1; // Extra space between lines // Search the message to determine if we need the bell added - bool needs_bell = (strstr(screen->alertBannerMessage, "Alert Received") != nullptr); + bool needs_bell = (strstr(alertBannerMessage, "Alert Received") != nullptr); + + uint8_t firstOption = 0; + uint8_t firstOptionToShow = 0; // Setup font and alignment display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); // We will manually center per line - const int MAX_LINES = 10; + const int MAX_LINES = 23; uint16_t maxWidth = 0; uint16_t lineWidths[MAX_LINES] = {0}; @@ -100,12 +83,11 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp uint16_t lineCount = 0; char lineBuffer[40] = {0}; // pointer to the terminating null - char *alertEnd = screen->alertBannerMessage + strnlen(screen->alertBannerMessage, sizeof(screen->alertBannerMessage)); - lineStarts[lineCount] = screen->alertBannerMessage; - LOG_WARN(lineStarts[lineCount]); + char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage)); + lineStarts[lineCount] = alertBannerMessage; // loop through lines finding \n characters - while ((lineCount < 10) && (lineStarts[lineCount] < alertEnd)) { + while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) { lineStarts[lineCount + 1] = std::find(lineStarts[lineCount], alertEnd, '\n'); lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount]; if (lineStarts[lineCount + 1][0] == '\n') { @@ -116,6 +98,38 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp maxWidth = lineWidths[lineCount]; } lineCount++; + // if we are doing a selection, add extra width for arrows + } + + if (alertBannerOptions > 0) { + // respond to input + if (inEvent == meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP) { + curSelected--; + } else if (inEvent == meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN) { + curSelected++; + } else if (inEvent == meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT) { + alertBannerCallback(curSelected); + alertBannerMessage[0] = '\0'; + } + if (curSelected == -1) + curSelected = alertBannerOptions - 1; + if (curSelected == alertBannerOptions) + curSelected = 0; + // compare number of options to number of lines + if (lineCount < alertBannerOptions) + return; + firstOption = lineCount - alertBannerOptions; + if (curSelected > 1 && alertBannerOptions > 3) { + firstOptionToShow = curSelected + firstOption - 1; + // put the selected option in the middle + } else { + firstOptionToShow = firstOption; + } + } else { // not in an alert with a callback + // TODO: check that at least a second has passed since the alert started + if (inEvent == meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT) { + alertBannerMessage[0] = '\0'; // end the alert early + } } // set width from longest line @@ -128,9 +142,14 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp boxWidth += 20; } } - + // calculate max lines on screen? for now it's 4 // set height from line count - uint16_t boxHeight = padding * 2 + lineCount * FONT_HEIGHT_SMALL + (lineCount - 1) * lineSpacing; + uint16_t boxHeight; + if (lineCount <= 4) { + boxHeight = vPadding * 2 + lineCount * FONT_HEIGHT_SMALL + (lineCount - 1) * lineSpacing; + } else { + boxHeight = vPadding * 2 + 4 * FONT_HEIGHT_SMALL + 4 * lineSpacing; + } int16_t boxLeft = (display->width() / 2) - (boxWidth / 2); int16_t boxTop = (display->height() / 2) - (boxHeight / 2); @@ -151,13 +170,42 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp display->setColor(WHITE); // === Draw each line centered in the box === - int16_t lineY = boxTop + padding; + int16_t lineY = boxTop + vPadding; + + LOG_DEBUG("firstOptionToShow: %u, firstOption: %u", firstOptionToShow, firstOption); + // for (int i = 0; i < lineCount; i++) { - strncpy(lineBuffer, lineStarts[i], 40); - if (lineLengths[i] > 39) - lineBuffer[39] = '\0'; - else - lineBuffer[lineLengths[i]] = '\0'; + // is this line selected? + // if so, start the buffer with -> and strncpy to the 4th location + if (i == 0 || alertBannerOptions == 0) { + strncpy(lineBuffer, lineStarts[i], 40); + if (lineLengths[i] > 39) + lineBuffer[39] = '\0'; + else + lineBuffer[lineLengths[i]] = '\0'; + } else if (i >= firstOptionToShow && i <= firstOptionToShow + 3) { + if (i == curSelected + firstOption) { + if (lineLengths[i] > 35) + lineLengths[i] = 35; + strncpy(lineBuffer, "->", 3); + strncpy(lineBuffer + 2, lineStarts[i], 36); + strncpy(lineBuffer + lineLengths[i] + 2, "<-", 3); + lineLengths[i] += 4; + lineWidths[i] += display->getStringWidth("-><-", 4, true); + if (lineLengths[i] > 35) + lineBuffer[39] = '\0'; + else + lineBuffer[lineLengths[i]] = '\0'; + } else { + strncpy(lineBuffer, lineStarts[i], 40); + if (lineLengths[i] > 39) + lineBuffer[39] = '\0'; + else + lineBuffer[lineLengths[i]] = '\0'; + } + } else { // add break for the additional lines + continue; + } int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2; @@ -173,6 +221,7 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp lineY += FONT_HEIGHT_SMALL + lineSpacing; } + inEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE); } /// Draw the last text message we received @@ -201,7 +250,10 @@ void NotificationRenderer::drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUi "Please be patient and do not power off."); } -} // namespace NotificationRenderer +bool NotificationRenderer::isOverlayBannerShowing() +{ + return strlen(alertBannerMessage) > 0 && (alertBannerUntil == 0 || millis() <= alertBannerUntil); +} } // namespace graphics #endif \ No newline at end of file diff --git a/src/graphics/draw/NotificationRenderer.h b/src/graphics/draw/NotificationRenderer.h index 6f07d75c4..2fe758d5f 100644 --- a/src/graphics/draw/NotificationRenderer.h +++ b/src/graphics/draw/NotificationRenderer.h @@ -6,19 +6,21 @@ namespace graphics { -namespace NotificationRenderer -{ - class NotificationRenderer { public: + static char inEvent; + static int8_t curSelected; + static char alertBannerMessage[256]; + static uint32_t alertBannerUntil; // 0 is a special case meaning forever + static uint8_t alertBannerOptions; // last x lines are seelctable options + static std::function alertBannerCallback; + static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state); static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - static void drawWelcomeScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + static bool isOverlayBannerShowing(); }; -} // namespace NotificationRenderer - } // namespace graphics From af1a7346741fb0720387051cc80245aad47d9efc Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Thu, 5 Jun 2025 23:52:06 -0500 Subject: [PATCH 244/265] Off by one --- src/graphics/draw/NotificationRenderer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index 88a14fce0..d9baaaee2 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -183,7 +183,7 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp lineBuffer[39] = '\0'; else lineBuffer[lineLengths[i]] = '\0'; - } else if (i >= firstOptionToShow && i <= firstOptionToShow + 3) { + } else if (i >= firstOptionToShow && i < firstOptionToShow + 3) { if (i == curSelected + firstOption) { if (lineLengths[i] > 35) lineLengths[i] = 35; From 37145abbfb789be713bfe291a5a83114521ef534 Mon Sep 17 00:00:00 2001 From: Jason P Date: Fri, 6 Jun 2025 11:17:20 -0500 Subject: [PATCH 245/265] Move to unified text layouts and spacing --- src/graphics/SharedUIDisplay.h | 29 ++---- src/graphics/draw/DebugRenderer.cpp | 57 +++++------ src/graphics/draw/NodeListRenderer.cpp | 21 ++-- src/graphics/draw/UIRenderer.cpp | 95 ++++++++----------- src/graphics/images.h | 7 +- .../Telemetry/EnvironmentTelemetry.cpp | 6 +- src/modules/Telemetry/PowerTelemetry.cpp | 12 ++- src/modules/esp32/PaxcounterModule.cpp | 7 +- 8 files changed, 112 insertions(+), 122 deletions(-) diff --git a/src/graphics/SharedUIDisplay.h b/src/graphics/SharedUIDisplay.h index bf515b035..b18bb1c4f 100644 --- a/src/graphics/SharedUIDisplay.h +++ b/src/graphics/SharedUIDisplay.h @@ -9,27 +9,14 @@ namespace graphics // Shared UI Helpers // ======================= -// Compact line layout -#define compactFirstLine ((FONT_HEIGHT_SMALL - 1) * 1) -#define compactSecondLine ((FONT_HEIGHT_SMALL - 1) * 2) - 2 -#define compactThirdLine ((FONT_HEIGHT_SMALL - 1) * 3) - 4 -#define compactFourthLine ((FONT_HEIGHT_SMALL - 1) * 4) - 6 -#define compactFifthLine ((FONT_HEIGHT_SMALL - 1) * 5) - 8 -#define compactSixthLine ((FONT_HEIGHT_SMALL - 1) * 6) - 10 - -// Standard line layout -#define standardFirstLine (FONT_HEIGHT_SMALL + 1) * 1 -#define standardSecondLine (FONT_HEIGHT_SMALL + 1) * 2 -#define standardThirdLine (FONT_HEIGHT_SMALL + 1) * 3 -#define standardFourthLine (FONT_HEIGHT_SMALL + 1) * 4 - -// More Compact line layout -#define moreCompactFirstLine compactFirstLine -#define moreCompactSecondLine (moreCompactFirstLine + (FONT_HEIGHT_SMALL - 5)) -#define moreCompactThirdLine (moreCompactSecondLine + (FONT_HEIGHT_SMALL - 5)) -#define moreCompactFourthLine (moreCompactThirdLine + (FONT_HEIGHT_SMALL - 5)) -#define moreCompactFifthLine (moreCompactFourthLine + (FONT_HEIGHT_SMALL - 5)) -#define moreCompactSixthLine (moreCompactFifthLine + (FONT_HEIGHT_SMALL - 5)) +// Consistent Line Spacing +#define textZeroLine 0 +#define textFirstLine ((FONT_HEIGHT_SMALL - 1) * 1) +#define textSecondLine (textFirstLine + (FONT_HEIGHT_SMALL - 5)) +#define textThirdLine (textSecondLine + (FONT_HEIGHT_SMALL - 5)) +#define textFourthLine (textThirdLine + (FONT_HEIGHT_SMALL - 5)) +#define textFifthLine (textFourthLine + (FONT_HEIGHT_SMALL - 5)) +#define textSixthLine (textFifthLine + (FONT_HEIGHT_SMALL - 5)) // Quick screen access #define SCREEN_WIDTH display->getWidth() diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index b8aa3e353..a90a34d99 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -16,6 +16,9 @@ #include "mesh/generated/meshtastic/deviceonly.pb.h" #include "sleep.h" +const int textPositions[7] = {textZeroLine, textFirstLine, textSecondLine, textThirdLine, + textFourthLine, textFifthLine, textSixthLine}; + #if HAS_WIFI && !defined(ARCH_PORTDUINO) #include "mesh/wifi/WiFiAPClient.h" #include @@ -173,6 +176,7 @@ void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, i display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); + int line = 1; // === Set Title const char *titleStr = "WiFi"; @@ -183,13 +187,13 @@ void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, i const char *wifiName = config.network.wifi_ssid; if (WiFi.status() != WL_CONNECTED) { - display->drawString(x, moreCompactFirstLine, "WiFi: Not Connected"); + display->drawString(x, textPositions[line++], "WiFi: Not Connected"); } else { - display->drawString(x, moreCompactFirstLine, "WiFi: Connected"); + display->drawString(x, textPositions[line++], "WiFi: Connected"); char rssiStr[32]; snprintf(rssiStr, sizeof(rssiStr), "RSSI: %d", WiFi.RSSI()); - display->drawString(x, moreCompactSecondLine, rssiStr); + display->drawString(x, textPositions[line++], rssiStr); } /* @@ -207,36 +211,36 @@ void drawFrameWiFi(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, i if (WiFi.status() == WL_CONNECTED) { char ipStr[64]; snprintf(ipStr, sizeof(ipStr), "IP: %s", WiFi.localIP().toString().c_str()); - display->drawString(x, moreCompactThirdLine, ipStr); + display->drawString(x, textPositions[line++], ipStr); } else if (WiFi.status() == WL_NO_SSID_AVAIL) { - display->drawString(x, moreCompactThirdLine, "SSID Not Found"); + display->drawString(x, textPositions[line++], "SSID Not Found"); } else if (WiFi.status() == WL_CONNECTION_LOST) { - display->drawString(x, moreCompactThirdLine, "Connection Lost"); + display->drawString(x, textPositions[line++], "Connection Lost"); } else if (WiFi.status() == WL_IDLE_STATUS) { - display->drawString(x, moreCompactThirdLine, "Idle ... Reconnecting"); + display->drawString(x, textPositions[line++], "Idle ... Reconnecting"); } else if (WiFi.status() == WL_CONNECT_FAILED) { - display->drawString(x, moreCompactThirdLine, "Connection Failed"); + display->drawString(x, textPositions[line++], "Connection Failed"); } #ifdef ARCH_ESP32 else { // Codes: // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-guides/wifi.html#wi-fi-reason-code - display->drawString(x, moreCompactThirdLine, + display->drawString(x, textPositions[line++], WiFi.disconnectReasonName(static_cast(getWifiDisconnectReason()))); } #else else { char statusStr[32]; snprintf(statusStr, sizeof(statusStr), "Unknown status: %d", WiFi.status()); - display->drawString(x, moreCompactThirdLine, statusStr); + display->drawString(x, textPositions[line++], statusStr); } #endif char ssidStr[64]; snprintf(ssidStr, sizeof(ssidStr), "SSID: %s", wifiName); - display->drawString(x, moreCompactFourthLine, ssidStr); + display->drawString(x, textPositions[line++], ssidStr); - display->drawString(x, moreCompactFifthLine, "URL: http://meshtastic.local"); + display->drawString(x, textPositions[line++], "URL: http://meshtastic.local"); /* Display a heartbeat pixel that blinks every time the frame is redrawn */ #ifdef SHOW_REDRAWS @@ -392,6 +396,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); + int line = 1; // === Set Title const char *titleStr = (SCREEN_WIDTH > 128) ? "LoRa Info" : "LoRa"; @@ -400,7 +405,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, graphics::drawCommonHeader(display, x, y, titleStr); // === First Row: Region / BLE Name === - graphics::UIRenderer::drawNodes(display, x, compactFirstLine + 2, nodeStatus, 0, true, ""); + graphics::UIRenderer::drawNodes(display, x, textPositions[line] + 2, nodeStatus, 0, true, ""); uint8_t dmac[6]; char shortnameble[35]; @@ -409,7 +414,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, snprintf(shortnameble, sizeof(shortnameble), "BLE: %s", screen->ourId); int textWidth = display->getStringWidth(shortnameble); int nameX = (SCREEN_WIDTH - textWidth); - display->drawString(nameX, compactFirstLine, shortnameble); + display->drawString(nameX, textPositions[line++], shortnameble); // === Second Row: Radio Preset === auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false); @@ -420,7 +425,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, } textWidth = display->getStringWidth(regionradiopreset); nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, compactSecondLine, regionradiopreset); + display->drawString(nameX, textPositions[line++], regionradiopreset); // === Third Row: Frequency / ChanNum === char frequencyslot[35]; @@ -442,7 +447,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, } textWidth = display->getStringWidth(frequencyslot); nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, compactThirdLine, frequencyslot); + display->drawString(nameX, textPositions[line++], frequencyslot); // === Fourth Row: Channel Utilization === const char *chUtil = "ChUtil:"; @@ -450,7 +455,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); int chUtil_x = (SCREEN_WIDTH > 128) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5; - int chUtil_y = compactFourthLine + 3; + int chUtil_y = textPositions[line] + 3; int chutil_bar_width = (SCREEN_WIDTH > 128) ? 100 : 50; int chutil_bar_height = (SCREEN_WIDTH > 128) ? 12 : 7; @@ -461,7 +466,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; int starting_position = centerofscreen - total_line_content_width; - display->drawString(starting_position, compactFourthLine, chUtil); + display->drawString(starting_position, textPositions[line++], chUtil); // Force 56% or higher to show a full 100% bar, text would still show related percent. if (chutil_percent >= 61) { @@ -498,7 +503,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); } - display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, compactFourthLine, chUtilPercentage); + display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, textPositions[4], chUtilPercentage); } // **************************** @@ -517,9 +522,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, graphics::drawCommonHeader(display, x, y, titleStr); // === Layout === - const int yPositions[6] = {moreCompactFirstLine, moreCompactSecondLine, moreCompactThirdLine, - moreCompactFourthLine, moreCompactFifthLine, moreCompactSixthLine}; - int line = 0; + int line = 1; const int barHeight = 6; const int labelX = x; const int barsOffset = (SCREEN_WIDTH > 128) ? 24 : 0; @@ -548,10 +551,10 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, // Label display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawString(labelX, yPositions[line], label); + display->drawString(labelX, textPositions[line], label); // Bar - int barY = yPositions[line] + (FONT_HEIGHT_SMALL - barHeight) / 2; + int barY = textPositions[line] + (FONT_HEIGHT_SMALL - barHeight) / 2; display->setColor(WHITE); display->drawRect(barX, barY, adjustedBarWidth, barHeight); @@ -560,7 +563,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, // Value string display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(SCREEN_WIDTH - 2, yPositions[line], combinedStr); + display->drawString(SCREEN_WIDTH - 2, textPositions[line], combinedStr); }; // === Memory values === @@ -614,7 +617,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, snprintf(appversionstr, sizeof(appversionstr), "Ver.: %s", optstr(APP_VERSION)); int textWidth = display->getStringWidth(appversionstr); int nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, yPositions[line], appversionstr); + display->drawString(nameX, textPositions[line], appversionstr); if (SCREEN_HEIGHT > 64 || (SCREEN_HEIGHT <= 64 && line < 4)) { // Only show uptime if the screen can show it line += 1; @@ -632,7 +635,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %um", mins); textWidth = display->getStringWidth(uptimeStr); nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, yPositions[line], uptimeStr); + display->drawString(nameX, textPositions[line], uptimeStr); } } } // namespace DebugRenderer diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index 2d465fffa..bcc43d5b6 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -12,6 +12,9 @@ #include "meshUtils.h" #include +const int textPositions[7] = {textZeroLine, textFirstLine, textSecondLine, textThirdLine, + textFourthLine, textFifthLine, textSixthLine}; + // Forward declarations for functions defined in Screen.cpp namespace graphics { @@ -612,14 +615,12 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t graphics::drawCommonHeader(display, x, y, shortName); // Dynamic row stacking with predefined Y positions - const int yPositions[5] = {moreCompactFirstLine, moreCompactSecondLine, moreCompactThirdLine, moreCompactFourthLine, - moreCompactFifthLine}; - int line = 0; + int line = 1; // 1. Long Name (always try to show first) const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; if (username && line < 5) { - display->drawString(x, yPositions[line++], username); + display->drawString(x, textPositions[line++], username); } // 2. Signal and Hops (combined on one line, if available) @@ -644,7 +645,7 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t } } if (signalHopsStr[0] && line < 5) { - display->drawString(x, yPositions[line++], signalHopsStr); + display->drawString(x, textPositions[line++], signalHopsStr); } // 3. Heard (last seen, skip if node never seen) @@ -661,7 +662,7 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t : 'm')); } if (seenStr[0] && line < 5) { - display->drawString(x, yPositions[line++], seenStr); + display->drawString(x, textPositions[line++], seenStr); } // 4. Uptime (only show if metric is present) @@ -681,7 +682,7 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t } } if (uptimeStr[0] && line < 5) { - display->drawString(x, yPositions[line++], uptimeStr); + display->drawString(x, textPositions[line++], uptimeStr); } // 5. Distance (only if both nodes have GPS position) @@ -733,7 +734,7 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t } // Only display if we actually have a value! if (haveDistance && distStr[0] && line < 5) { - display->drawString(x, yPositions[line++], distStr); + display->drawString(x, textPositions[line++], distStr); } // Compass rendering for different screen orientations @@ -744,7 +745,7 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t showCompass = true; } if (showCompass) { - const int16_t topY = compactFirstLine; + const int16_t topY = textPositions[1]; const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); const int16_t usableHeight = bottomY - topY - 5; int16_t compassRadius = usableHeight / 2; @@ -774,7 +775,7 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t showCompass = true; } if (showCompass) { - int yBelowContent = (line > 0 && line <= 5) ? (yPositions[line - 1] + FONT_HEIGHT_SMALL + 2) : moreCompactFirstLine; + int yBelowContent = (line > 1 && line <= 6) ? (textPositions[line - 1] + FONT_HEIGHT_SMALL + 2) : textPositions[1]; const int margin = 4; #if defined(USE_EINK) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index c10647a71..14d55eea0 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -17,6 +17,9 @@ #include #include +const int textPositions[7] = {textZeroLine, textFirstLine, textSecondLine, textThirdLine, + textFourthLine, textFifthLine, textSixthLine}; + #if !MESHTASTIC_EXCLUDE_GPS // External variables @@ -38,8 +41,8 @@ namespace UIRenderer void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps) { // Draw satellite image - int yOffset = (SCREEN_WIDTH > 128) ? 4 : 2; - display->drawFastImage(x + 1, y + yOffset, 8, 8, imgSatellite); + int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1; + display->drawXbm(x + 1, y + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite); char textString[10]; if (config.position.fixed_position) { @@ -280,15 +283,13 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t // 4. The first line is ALWAYS at your macro position; subsequent lines use the next available macro slot. // List of available macro Y positions in order, from top to bottom. - const int yPositions[5] = {moreCompactFirstLine, moreCompactSecondLine, moreCompactThirdLine, moreCompactFourthLine, - moreCompactFifthLine}; - int line = 0; // which slot to use next + int line = 1; // which slot to use next // === 1. Long Name (always try to show first) === const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr; if (username && line < 5) { // Print node's long name (e.g. "Backpack Node") - display->drawString(x, yPositions[line++], username); + display->drawString(x, textPositions[line++], username); } // === 2. Signal and Hops (combined on one line, if available) === @@ -319,7 +320,7 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t } } if (signalHopsStr[0] && line < 5) { - display->drawString(x, yPositions[line++], signalHopsStr); + display->drawString(x, textPositions[line++], signalHopsStr); } // === 3. Heard (last seen, skip if node never seen) === @@ -337,7 +338,7 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t : 'm')); } if (seenStr[0] && line < 5) { - display->drawString(x, yPositions[line++], seenStr); + display->drawString(x, textPositions[line++], seenStr); } // === 4. Uptime (only show if metric is present) === @@ -356,7 +357,7 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t snprintf(uptimeStr, sizeof(uptimeStr), " Uptime: %um", mins); } if (uptimeStr[0] && line < 5) { - display->drawString(x, yPositions[line++], uptimeStr); + display->drawString(x, textPositions[line++], uptimeStr); } // === 5. Distance (only if both nodes have GPS position) === @@ -416,7 +417,7 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t } // Only display if we actually have a value! if (haveDistance && distStr[0] && line < 5) { - display->drawString(x, yPositions[line++], distStr); + display->drawString(x, textPositions[line++], distStr); } // --- Compass Rendering: landscape (wide) screens use the original side-aligned logic --- @@ -426,7 +427,7 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t showCompass = true; } if (showCompass) { - const int16_t topY = compactFirstLine; + const int16_t topY = textPositions[1]; const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); const int16_t usableHeight = bottomY - topY - 5; int16_t compassRadius = usableHeight / 2; @@ -461,7 +462,7 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t showCompass = true; } if (showCompass) { - int yBelowContent = (line > 0 && line <= 5) ? (yPositions[line - 1] + FONT_HEIGHT_SMALL + 2) : moreCompactFirstLine; + int yBelowContent = (line > 0 && line <= 5) ? (textPositions[line - 1] + FONT_HEIGHT_SMALL + 2) : textPositions[1]; const int margin = 4; // --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) ----------- #if defined(USE_EINK) @@ -514,6 +515,7 @@ void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); + int line = 1; // === Header === graphics::drawCommonHeader(display, x, y, ""); @@ -531,9 +533,7 @@ void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t config.display.heading_bold = false; // Display Region and Channel Utilization - drawNodes(display, x + 1, - ((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)) + 2, nodeStatus, - -1, false, "online"); + drawNodes(display, x + 1, textPositions[line] + 2, nodeStatus, -1, false, "online"); char uptimeStr[32] = ""; uint32_t uptime = millis() / 1000; @@ -547,9 +547,7 @@ void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t snprintf(uptimeStr, sizeof(uptimeStr), "Up: %uh %um", hours, mins); else snprintf(uptimeStr, sizeof(uptimeStr), "Up: %um", mins); - display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), - ((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)), - uptimeStr); + display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeStr), textPositions[line++], uptimeStr); // === Second Row: Satellites and Voltage === config.display.heading_bold = false; @@ -562,13 +560,9 @@ void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t } else { displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } - display->drawString( - 0, ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), - displayLine); + display->drawString(0, textPositions[line], displayLine); } else { - UIRenderer::drawGps( - display, 0, ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), - gpsStatus); + UIRenderer::drawGps(display, 0, textPositions[line], gpsStatus); } #endif @@ -577,21 +571,16 @@ void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t int batV = powerStatus->getBatteryVoltageMv() / 1000; int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10; snprintf(batStr, sizeof(batStr), "%01d.%02dV", batV, batCv); - display->drawString( - x + SCREEN_WIDTH - display->getStringWidth(batStr), - ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), batStr); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth(batStr), textPositions[line++], batStr); } else { - display->drawString( - x + SCREEN_WIDTH - display->getStringWidth("USB"), - ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), "USB"); + display->drawString(x + SCREEN_WIDTH - display->getStringWidth("USB"), textPositions[line++], "USB"); } config.display.heading_bold = origBold; // === Third Row: Bluetooth Off (Only If Actually Off) === if (!config.bluetooth.enabled) { - display->drawString( - 0, ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactThirdLine : moreCompactThirdLine)), "BT off"); + display->drawString(0, textPositions[line++], "BT off"); } // === Third & Fourth Rows: Node Identity === @@ -619,28 +608,18 @@ void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t } textWidth = display->getStringWidth(combinedName); nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString( - nameX, - ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine)) + yOffset, - combinedName); + display->drawString(nameX, ((rows == 4) ? textPositions[line++] : textPositions[line++]) + yOffset, combinedName); } else { + // === LongName Centered === textWidth = display->getStringWidth(longName); nameX = (SCREEN_WIDTH - textWidth) / 2; - yOffset = (strcmp(shortnameble, "") == 0) ? 1 : 0; - if (yOffset == 1) { - yOffset = (SCREEN_WIDTH > 128) ? 0 : 7; - } - display->drawString( - nameX, - ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine)) + yOffset, - longName); + yOffset = (rows == 4 && SCREEN_WIDTH <= 128) ? 7 : 0; + display->drawString(nameX, textPositions[line++] + yOffset, longName); - // === Fourth Row: ShortName Centered === + // === ShortName Centered === textWidth = display->getStringWidth(shortnameble); nameX = (SCREEN_WIDTH - textWidth) / 2; - display->drawString(nameX, - ((rows == 4) ? compactFourthLine : ((SCREEN_HEIGHT > 64) ? compactFifthLine : moreCompactFifthLine)), - shortnameble); + display->drawString(nameX, textPositions[line++] + yOffset, shortnameble); } } @@ -886,6 +865,7 @@ void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *stat display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); + int line = 1; // === Set Title const char *titleStr = "GPS"; @@ -905,10 +885,11 @@ void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *stat } else { displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off"; } - display->drawFastImage(x + 1, y, 8, 8, imgSatellite); - display->drawString(x + 11, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), displayLine); + int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1; + display->drawXbm(x + 1, textPositions[line] + yOffset, imgSatellite_width, imgSatellite_height, imgSatellite); + display->drawString(x + 11, textPositions[line++], displayLine); } else { - UIRenderer::drawGps(display, 0, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), gpsStatus); + UIRenderer::drawGps(display, 0, textPositions[line++], gpsStatus); } config.display.heading_bold = origBold; @@ -939,17 +920,17 @@ void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *stat } else { snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0im", geoCoord.getAltitude()); } - display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine), DisplayLineTwo); + display->drawString(x, textPositions[line++], DisplayLineTwo); // === Third Row: Latitude === char latStr[32]; snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7); - display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactThirdLine : moreCompactThirdLine), latStr); + display->drawString(x, textPositions[line++], latStr); // === Fourth Row: Longitude === char lonStr[32]; snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7); - display->drawString(x, ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine), lonStr); + display->drawString(x, textPositions[line++], lonStr); // === Fifth Row: Date === uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); @@ -958,14 +939,14 @@ void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *stat UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime); char fullLine[40]; snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr); - display->drawString(0, ((SCREEN_HEIGHT > 64) ? compactFifthLine : moreCompactFifthLine), fullLine); + display->drawString(0, textPositions[line++], fullLine); } // === Draw Compass if heading is valid === if (validHeading) { // --- Compass Rendering: landscape (wide) screens use original side-aligned logic --- if (SCREEN_WIDTH > SCREEN_HEIGHT) { - const int16_t topY = compactFirstLine; + const int16_t topY = textPositions[1]; const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height const int16_t usableHeight = bottomY - topY - 5; @@ -998,7 +979,7 @@ void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *stat } else { // Portrait or square: put compass at the bottom and centered, scaled to fit available space // For E-Ink screens, account for navigation bar at the bottom! - int yBelowContent = ((SCREEN_HEIGHT > 64) ? compactFifthLine : moreCompactFifthLine) + FONT_HEIGHT_SMALL + 2; + int yBelowContent = textPositions[5] + FONT_HEIGHT_SMALL + 2; const int margin = 4; int availableHeight = #if defined(USE_EINK) diff --git a/src/graphics/images.h b/src/graphics/images.h index f11acc084..c1dc28c7f 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -6,7 +6,12 @@ const uint8_t SATELLITE_IMAGE[] PROGMEM = {0x00, 0x08, 0x00, 0x1C, 0x00, 0x0E, 0 0xF8, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC8, 0x01, 0x9C, 0x54, 0x0E, 0x52, 0x07, 0x48, 0x02, 0x26, 0x00, 0x10, 0x00, 0x0E}; -const uint8_t imgSatellite[] PROGMEM = {0x70, 0x71, 0x22, 0xFA, 0xFA, 0x22, 0x71, 0x70}; +#define imgSatellite_width 8 +#define imgSatellite_height 8 +const uint8_t imgSatellite[] PROGMEM = { + 0b00000000, 0b00000000, 0b00000000, 0b00011000, 0b11011011, 0b11111111, 0b11011011, 0b00011000, +}; + const uint8_t imgUSB[] PROGMEM = {0x60, 0x60, 0x30, 0x18, 0x18, 0x18, 0x24, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x24, 0x24, 0x24, 0x3C}; const uint8_t imgPower[] PROGMEM = {0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08, 0x1C, 0x22, 0x22, 0x41, 0x7F, 0x22, 0x22, 0x22}; diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 37eeb049a..33bc98392 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -22,6 +22,9 @@ #include #include +const int textPositions[7] = {textZeroLine, textFirstLine, textSecondLine, textThirdLine, + textFourthLine, textFifthLine, textSixthLine}; + #if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL // Sensors @@ -343,6 +346,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt display->clear(); display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_LEFT); + int line = 1; // === Set Title const char *titleStr = (SCREEN_WIDTH > 128) ? "Environment" : "Env."; @@ -352,7 +356,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt // === Row spacing setup === const int rowHeight = FONT_HEIGHT_SMALL - 4; - int currentY = compactFirstLine; + int currentY = textPositions[line++]; // === Show "No Telemetry" if no data available === if (!lastMeasurementPacket) { diff --git a/src/modules/Telemetry/PowerTelemetry.cpp b/src/modules/Telemetry/PowerTelemetry.cpp index 4de886de6..86869007a 100644 --- a/src/modules/Telemetry/PowerTelemetry.cpp +++ b/src/modules/Telemetry/PowerTelemetry.cpp @@ -16,6 +16,9 @@ #include "sleep.h" #include "target_specific.h" +const int textPositions[7] = {textZeroLine, textFirstLine, textSecondLine, textThirdLine, + textFourthLine, textFifthLine, textSixthLine}; + #define FAILED_STATE_SENSOR_READ_MULTIPLIER 10 #define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true @@ -112,6 +115,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); + int line = 1; // === Set Title const char *titleStr = (SCREEN_WIDTH > 128) ? "Power Telem." : "Power"; @@ -121,7 +125,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s if (lastMeasurementPacket == nullptr) { // In case of no valid packet, display "Power Telemetry", "No measurement" - display->drawString(x, compactFirstLine, "No measurement"); + display->drawString(x, textPositions[line++], "No measurement"); return; } @@ -132,7 +136,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s const meshtastic_Data &p = lastMeasurementPacket->decoded; if (!pb_decode_from_bytes(p.payload.bytes, p.payload.size, &meshtastic_Telemetry_msg, &lastMeasurement)) { - display->drawString(x, compactFirstLine, "Measurement Error"); + display->drawString(x, textPositions[line++], "Measurement Error"); LOG_ERROR("Unable to decode last packet"); return; } @@ -140,11 +144,11 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s // Display "Pow. From: ..." char fromStr[64]; snprintf(fromStr, sizeof(fromStr), "Pow. From: %s (%us)", lastSender, agoSecs); - display->drawString(x, compactFirstLine, fromStr); + display->drawString(x, textPositions[line++], fromStr); // Display current and voltage based on ...power_metrics.has_[channel/voltage/current]... flags const auto &m = lastMeasurement.variant.power_metrics; - int lineY = compactSecondLine; + int lineY = textSecondLine; auto drawLine = [&](const char *label, float voltage, float current) { char lineStr[64]; diff --git a/src/modules/esp32/PaxcounterModule.cpp b/src/modules/esp32/PaxcounterModule.cpp index e83cb5b3c..dd9ab195d 100644 --- a/src/modules/esp32/PaxcounterModule.cpp +++ b/src/modules/esp32/PaxcounterModule.cpp @@ -115,12 +115,17 @@ int32_t PaxcounterModule::runOnce() #if HAS_SCREEN #include "graphics/ScreenFonts.h" +#include "graphics/SharedUIDisplay.h" + +const int textPositions[7] = {textZeroLine, textFirstLine, textSecondLine, textThirdLine, + textFourthLine, textFifthLine, textSixthLine}; void PaxcounterModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->clear(); display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); + int line = 1; // === Set Title const char *titleStr = "Pax"; @@ -136,7 +141,7 @@ void PaxcounterModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_SMALL); - display->drawStringf(display->getWidth() / 2 + x, compactFirstLine, buffer, "WiFi: %d\nBLE: %d\nUptime: %ds", + display->drawStringf(display->getWidth() / 2 + x, textPositions[line++], buffer, "WiFi: %d\nBLE: %d\nUptime: %ds", count_from_libpax.wifi_count, count_from_libpax.ble_count, millis() / 1000); } #endif // HAS_SCREEN From 2b5a7ab06d86c7351457420958a57bdef9b8ddfe Mon Sep 17 00:00:00 2001 From: Jason P Date: Fri, 6 Jun 2025 11:33:10 -0500 Subject: [PATCH 246/265] Still my Fav without an "e" --- src/graphics/draw/UIRenderer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 14d55eea0..2b8f6cd6f 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -271,7 +271,7 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t // === Create the shortName and title string === const char *shortName = (node->has_user && haveGlyphs(node->user.short_name)) ? node->user.short_name : "Node"; char titlestr[32] = {0}; - snprintf(titlestr, sizeof(titlestr), (SCREEN_WIDTH > 128) ? "Fave: %s" : "Fav: %s", shortName); + snprintf(titlestr, sizeof(titlestr), "Fav: %s", shortName); // === Draw battery/time/mail header (common across screens) === graphics::drawCommonHeader(display, x, y, titlestr); From 6b4f6a0cef3c84203ae6a4e78958f22b0434d189 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 6 Jun 2025 11:58:44 -0500 Subject: [PATCH 247/265] Fully remove EVENT_NODEDB_UPDATED --- src/PowerFSM.h | 2 +- src/mesh/NodeDB.cpp | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PowerFSM.h b/src/PowerFSM.h index 13dfdc4cc..beb233f11 100644 --- a/src/PowerFSM.h +++ b/src/PowerFSM.h @@ -11,7 +11,7 @@ #define EVENT_RECEIVED_MSG 5 // #define EVENT_BOOT 6 // now done with a timed transition #define EVENT_BLUETOOTH_PAIR 7 -#define EVENT_NODEDB_UPDATED 8 // NodeDB has a big enough change that we think you should turn on the screen +// #define EVENT_NODEDB_UPDATED 8 // Now defunct: NodeDB has a big enough change that we think you should turn on the screen #define EVENT_CONTACT_FROM_PHONE 9 // the phone just talked to us over bluetooth #define EVENT_LOW_BATTERY 10 // Battery is critically low, go to sleep #define EVENT_SERIAL_CONNECTED 11 diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index bfd6b6e8f..b68a28082 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1524,7 +1524,6 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact) // Mark the node's key as manually verified to indicate trustworthiness. info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK; updateGUIforNode = info; - powerFSM.trigger(EVENT_NODEDB_UPDATED); notifyObservers(true); // Force an update whether or not our node counts have changed saveNodeDatabaseToDisk(); } @@ -1568,7 +1567,6 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde if (changed) { updateGUIforNode = info; - powerFSM.trigger(EVENT_NODEDB_UPDATED); notifyObservers(true); // Force an update whether or not our node counts have changed // We just changed something about a User, From 3f9b116a13652daa14253a0341d1ff5f6ff1ca07 Mon Sep 17 00:00:00 2001 From: Jason P Date: Fri, 6 Jun 2025 12:28:20 -0500 Subject: [PATCH 248/265] Simply LoRa screen --- src/graphics/draw/DebugRenderer.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index a90a34d99..4b0c0b273 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -435,11 +435,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, if (config.lora.channel_num == 0) { snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %smhz", freqStr); } else { - if (SCREEN_WIDTH > 128) { - snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Chan: %smhz (%d)", freqStr, config.lora.channel_num); - } else { - snprintf(frequencyslot, sizeof(frequencyslot), "Frq/Ch: %smhz (%d)", freqStr, config.lora.channel_num); - } + snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Ch: %smhz (%d)", freqStr, config.lora.channel_num); } size_t len = strlen(frequencyslot); if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) { From 58ec9893df9693c2a5502456e2007d4e2dbeb89e Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Fri, 6 Jun 2025 13:47:23 -0500 Subject: [PATCH 249/265] Make some char pointers const to fix compilation on native targets --- src/modules/CannedMessageModule.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 838ce993a..85eb8a927 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -1737,7 +1737,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st const char *label = graphics::emotes[j].label; if (!label || !*label) continue; - char *found = strstr(msg + pos, label); + const char *found = strstr(msg + pos, label); if (found && (found - msg) < nextEmote) { nextEmote = found - msg; } @@ -1929,7 +1929,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st const char *label = graphics::emotes[j].label; if (label[0] == 0) continue; - char *found = strstr(msg + pos, label); + const char *found = strstr(msg + pos, label); if (found && (found - msg) < nextEmote) { nextEmote = found - msg; } From 673f5d3edebac53e293761e9d6a749399abbc3e9 Mon Sep 17 00:00:00 2001 From: Jason P Date: Fri, 6 Jun 2025 15:35:20 -0500 Subject: [PATCH 250/265] Update drawCompassNorth to include radius --- src/graphics/draw/CompassRenderer.cpp | 16 ++++++++++++---- src/graphics/draw/CompassRenderer.h | 2 +- src/graphics/draw/NodeListRenderer.cpp | 4 ++-- src/graphics/draw/UIRenderer.cpp | 6 +++--- src/modules/WaypointModule.cpp | 2 +- src/motion/MotionSensor.cpp | 2 +- 6 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/graphics/draw/CompassRenderer.cpp b/src/graphics/draw/CompassRenderer.cpp index 7c577a739..fef993e2d 100644 --- a/src/graphics/draw/CompassRenderer.cpp +++ b/src/graphics/draw/CompassRenderer.cpp @@ -39,20 +39,28 @@ struct Point { } }; -void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading) +void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius) { // Show the compass heading (not implemented in original) // This could draw a "N" indicator or north arrow // For now, we'll draw a simple north indicator - const float radius = 8.0f; + // const float radius = 17.0f; + if (display->width() > 128) { + radius += 4; + } Point north(0, -radius); north.rotate(-myHeading); north.translate(compassX, compassY); - // Draw a small "N" or north indicator - display->drawCircle(north.x, north.y, 2); display->setFont(FONT_SMALL); display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setColor(BLACK); + if (display->width() > 128) { + display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6); + } else { + display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6); + } + display->setColor(WHITE); display->drawString(north.x, north.y - 3, "N"); } diff --git a/src/graphics/draw/CompassRenderer.h b/src/graphics/draw/CompassRenderer.h index 2f7197084..4b26e6463 100644 --- a/src/graphics/draw/CompassRenderer.h +++ b/src/graphics/draw/CompassRenderer.h @@ -20,7 +20,7 @@ class Screen; namespace CompassRenderer { // Compass drawing functions -void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading); +void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius); void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian); void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing); diff --git a/src/graphics/draw/NodeListRenderer.cpp b/src/graphics/draw/NodeListRenderer.cpp index bcc43d5b6..070497198 100644 --- a/src/graphics/draw/NodeListRenderer.cpp +++ b/src/graphics/draw/NodeListRenderer.cpp @@ -758,7 +758,7 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t const auto &op = ourNode->position; float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading); + CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); const auto &p = node->position; float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); @@ -801,7 +801,7 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t const auto &op = ourNode->position; float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading); + CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); const auto &p = node->position; float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 2b8f6cd6f..7b7191bac 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -440,7 +440,6 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t const auto &op = ourNode->position; float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading); const auto &p = node->position; /* unused @@ -450,9 +449,10 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i)); if (!config.display.compass_north_top) bearing -= myHeading; - CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing); display->drawCircle(compassX, compassY, compassRadius); + CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); + CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing); } // else show nothing } else { @@ -489,7 +489,7 @@ void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t const auto &op = ourNode->position; float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180 : screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading); + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius); const auto &p = node->position; /* unused diff --git a/src/modules/WaypointModule.cpp b/src/modules/WaypointModule.cpp index 8641990f7..f23c46e60 100644 --- a/src/modules/WaypointModule.cpp +++ b/src/modules/WaypointModule.cpp @@ -140,7 +140,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, myHeading = (screen->getHeading()) * PI / 180; // gotta convert compass degrees to Radians else myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); - graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading); + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, (compassDiam / 2)); // Compass bearing to waypoint float bearingToOther = diff --git a/src/motion/MotionSensor.cpp b/src/motion/MotionSensor.cpp index 438fd4f7a..b00460aff 100755 --- a/src/motion/MotionSensor.cpp +++ b/src/motion/MotionSensor.cpp @@ -60,7 +60,7 @@ void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2; } display->drawCircle(compassX, compassY, compassDiam / 2); - graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180); + graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180, (compassDiam / 2)); } #endif From 1858031ad6e1f672c205d183be658cb6c6aa3153 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 6 Jun 2025 19:49:06 -0500 Subject: [PATCH 251/265] Fix warning --- src/modules/CannedMessageModule.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 85eb8a927..2714353b4 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -1777,7 +1777,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st } else { // Text: split by words and wrap inside word if needed String text = token.second; - int pos = 0; + uint16_t pos = 0; while (pos < text.length()) { // Find next space (or end) int spacePos = text.indexOf(' ', pos); From e869e1b146c328a6f7da6a2993d5aa1cb8a63fab Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Fri, 6 Jun 2025 22:06:45 -0400 Subject: [PATCH 252/265] button thread cleanup --- src/ButtonThread.cpp | 44 +++++++++++++++++++++++++++++ src/ButtonThread.h | 9 ++++-- src/graphics/Screen.cpp | 12 +++++++- src/input/InputBroker.h | 3 ++ src/modules/CannedMessageModule.cpp | 35 +++++++++++++++++++++-- 5 files changed, 98 insertions(+), 5 deletions(-) diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp index 4c6a7d4c4..8c62a7141 100644 --- a/src/ButtonThread.cpp +++ b/src/ButtonThread.cpp @@ -13,6 +13,7 @@ #include "modules/ExternalNotificationModule.h" #include "power.h" #include "sleep.h" +#include "input/InputBroker.h" #ifdef ARCH_PORTDUINO #include "platform/portduino/PortduinoGlue.h" #endif @@ -262,6 +263,48 @@ int32_t ButtonThread::runOnce() #endif if (btnEvent != BUTTON_EVENT_NONE) { +#if HAS_SCREEN + switch (btnEvent) { + case BUTTON_EVENT_PRESSED: { + LOG_BUTTON("press!"); + + // Play boop sound for every button press + playBoop(); + + // Forward single press to InputBroker (but NOT as DOWN/SELECT, just forward a "button press" event) + if (inputBroker) { + InputEvent evt = { "button", INPUT_BROKER_MSG_BUTTON_PRESSED, 0, 0, 0 }; + inputBroker->injectInputEvent(&evt); + } + + // Start tracking for potential combination + waitingForLongPress = true; + shortPressTime = millis(); + + break; + } + case BUTTON_EVENT_LONG_PRESSED: { + LOG_BUTTON("Long press!"); + + // Play beep sound + playBeep(); + + // Forward long press to InputBroker (but NOT as DOWN/SELECT, just forward a "button long press" event) + if (inputBroker) { + InputEvent evt = { "button", INPUT_BROKER_MSG_BUTTON_LONG_PRESSED, 0, 0, 0 }; + inputBroker->injectInputEvent(&evt); + } + + waitingForLongPress = false; + break; + } + default: + // Ignore all other events on screen devices + break; + } + btnEvent = BUTTON_EVENT_NONE; +#else + // On devices without screen: full legacy logic switch (btnEvent) { case BUTTON_EVENT_PRESSED: { LOG_BUTTON("press!"); @@ -489,6 +532,7 @@ int32_t ButtonThread::runOnce() break; } btnEvent = BUTTON_EVENT_NONE; +#endif } return 50; diff --git a/src/ButtonThread.h b/src/ButtonThread.h index 1fbc38672..e4bbba8f0 100644 --- a/src/ButtonThread.h +++ b/src/ButtonThread.h @@ -8,8 +8,13 @@ #define BUTTON_CLICK_MS 250 #endif -#ifndef BUTTON_LONGPRESS_MS -#define BUTTON_LONGPRESS_MS 5000 +#ifdef HAS_SCREEN + #undef BUTTON_LONGPRESS_MS + #define BUTTON_LONGPRESS_MS 500 +#else + #ifndef BUTTON_LONGPRESS_MS + #define BUTTON_LONGPRESS_MS 5000 + #endif #endif #ifndef BUTTON_TOUCH_MS diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 21cf32283..9c150c14e 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1781,7 +1781,6 @@ int Screen::handleUIFrameEvent(const UIFrameEvent *event) int Screen::handleInputEvent(const InputEvent *event) { - #if defined(DISPLAY_CLOCK_FRAME) // For the T-Watch, intercept touches to the 'toggle digital/analog watch face' button uint8_t watchFaceFrame = error_code ? 1 : 0; @@ -1812,6 +1811,17 @@ int Screen::handleInputEvent(const InputEvent *event) inputIntercepted = true; } + // Only allow BUTTON_PRESSED and BUTTON_LONG_PRESSED to trigger frame changes if no module is handling input + if (!inputIntercepted) { + if (event->inputEvent == INPUT_BROKER_MSG_BUTTON_PRESSED) { + showNextFrame(); + return 0; + } else if (event->inputEvent == INPUT_BROKER_MSG_BUTTON_LONG_PRESSED) { + // Optional: Define alternate screen action or no-op + return 0; + } + } + // If no modules are using the input, move between frames if (!inputIntercepted) { if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index 3278a5a73..6948390d2 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -21,6 +21,8 @@ #define INPUT_BROKER_MSG_BLUETOOTH_TOGGLE 0xAA #define INPUT_BROKER_MSG_TAB 0x09 #define INPUT_BROKER_MSG_EMOTE_LIST 0x8F +#define INPUT_BROKER_MSG_BUTTON_PRESSED 0xe1 +#define INPUT_BROKER_MSG_BUTTON_LONG_PRESSED 0xe2 typedef struct _InputEvent { const char *source; @@ -37,6 +39,7 @@ class InputBroker : public Observable public: InputBroker(); void registerSource(Observable *source); + void injectInputEvent(const InputEvent *event) { handleInputEvent(event); } protected: int handleInputEvent(const InputEvent *event); diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 85eb8a927..fd8228c1f 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -292,7 +292,9 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) switch (runState) { // Node/Channel destination selection mode: Handles character search, arrows, select, cancel, backspace case CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION: - return handleDestinationSelectionInput(event, isUp, isDown, isSelect); // All allowed input for this state + if (handleDestinationSelectionInput(event, isUp, isDown, isSelect)) + return 1; + return 0; // prevent fall-through to selector input // Free text input mode: Handles character input, cancel, backspace, select, etc. case CANNED_MESSAGE_RUN_STATE_FREETEXT: @@ -408,6 +410,15 @@ bool CannedMessageModule::handleTabSwitch(const InputEvent *event) int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) { + // Override isDown and isSelect ONLY for destination selector behavior + if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) { + if (event->inputEvent == INPUT_BROKER_MSG_BUTTON_PRESSED) { + isDown = true; + } else if (event->inputEvent == INPUT_BROKER_MSG_BUTTON_LONG_PRESSED) { + isSelect = true; + } + } + if (event->kbchar >= 32 && event->kbchar <= 126 && !isUp && !isDown && event->inputEvent != static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT) && event->inputEvent != static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT) && @@ -503,6 +514,15 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect) { + // Override isDown and isSelect ONLY for canned message list behavior + if (runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { + if (event->inputEvent == INPUT_BROKER_MSG_BUTTON_PRESSED) { + isDown = true; + } else if (event->inputEvent == INPUT_BROKER_MSG_BUTTON_LONG_PRESSED) { + isSelect = true; + } + } + if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) return false; @@ -591,7 +611,6 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bo return handled; } - bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) { // Always process only if in FREETEXT mode @@ -732,9 +751,18 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent *event) int CannedMessageModule::handleEmotePickerInput(const InputEvent *event) { int numEmotes = graphics::numEmotes; + + // Override isDown and isSelect ONLY for emote picker behavior bool isUp = isUpEvent(event); bool isDown = isDownEvent(event); bool isSelect = isSelectEvent(event); + if (runState == CANNED_MESSAGE_RUN_STATE_EMOTE_PICKER) { + if (event->inputEvent == INPUT_BROKER_MSG_BUTTON_PRESSED) { + isDown = true; + } else if (event->inputEvent == INPUT_BROKER_MSG_BUTTON_LONG_PRESSED) { + isSelect = true; + } + } // Scroll emote list if (isUp && emotePickerIndex > 0) { @@ -747,6 +775,7 @@ int CannedMessageModule::handleEmotePickerInput(const InputEvent *event) screen->forceDisplay(); return 1; } + // Select emote: insert into freetext at cursor and return to freetext if (isSelect) { String label = graphics::emotes[emotePickerIndex].label; @@ -761,12 +790,14 @@ int CannedMessageModule::handleEmotePickerInput(const InputEvent *event) screen->forceDisplay(); return 1; } + // Cancel returns to freetext if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) { runState = CANNED_MESSAGE_RUN_STATE_FREETEXT; screen->forceDisplay(); return 1; } + return 0; } From b177329813f7bdd3cbe5cf89ca215718e41733b3 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Fri, 6 Jun 2025 21:26:01 -0500 Subject: [PATCH 253/265] Pull OneButton handling from PowerFSM and add MUI switch (#6973) --- src/PowerFSM.cpp | 12 +++--------- src/graphics/Screen.cpp | 19 +++++++++++++++++-- src/modules/CannedMessageModule.cpp | 2 +- variants/seeed-sensecap-indicator/variant.h | 3 +-- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index 398e56097..440195a9f 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -253,12 +253,6 @@ static void onIdle() } } -static void screenPress() -{ - if (screen) - screen->onPress(); -} - static void bootEnter() { LOG_DEBUG("State: BOOT"); @@ -302,9 +296,9 @@ void PowerFSM_setup() powerFSM.add_transition(&stateLS, &stateON, EVENT_PRESS, NULL, "Press"); powerFSM.add_transition(&stateNB, &stateON, EVENT_PRESS, NULL, "Press"); powerFSM.add_transition(&stateDARK, isPowered() ? &statePOWER : &stateON, EVENT_PRESS, NULL, "Press"); - powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_PRESS, screenPress, "Press"); - powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, screenPress, "Press"); // reenter On to restart our timers - powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, screenPress, + powerFSM.add_transition(&statePOWER, &statePOWER, EVENT_PRESS, NULL, "Press"); + powerFSM.add_transition(&stateON, &stateON, EVENT_PRESS, NULL, "Press"); // reenter On to restart our timers + powerFSM.add_transition(&stateSERIAL, &stateSERIAL, EVENT_PRESS, NULL, "Press"); // Allow button to work while in serial API // Handle critically low power battery by forcing deep sleep diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 9c150c14e..25cd25663 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1824,10 +1824,25 @@ int Screen::handleInputEvent(const InputEvent *event) // If no modules are using the input, move between frames if (!inputIntercepted) { - if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) { showPrevFrame(); - else if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) + } else if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) { showNextFrame(); + } else if (event->inputEvent == + static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) { +#if HAS_TFT + if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) { + showOverlayBanner("Switch to MUI?\nYES\nNO", 30000, 2, [](int selected) -> void { + if (selected == 0) { + config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR; + config.bluetooth.enabled = false; + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + } + }); + } +#endif + } } } diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index aaabe41f3..4d6239b57 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -312,7 +312,7 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) if (isSelect) { // When inactive, call the onebutton shortpress instead. Activate module only on up/down powerFSM.trigger(EVENT_PRESS); - return 1; // Let caller know we handled it + return 0; // Main button press no longer runs through powerFSM } // Let LEFT/RIGHT pass through so frame navigation works if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT) || diff --git a/variants/seeed-sensecap-indicator/variant.h b/variants/seeed-sensecap-indicator/variant.h index 1010e04c8..c83a3e72d 100644 --- a/variants/seeed-sensecap-indicator/variant.h +++ b/variants/seeed-sensecap-indicator/variant.h @@ -7,9 +7,8 @@ #define SENSOR_PORT_NUM 2 #define SENSOR_BAUD_RATE 115200 -#if !HAS_TFT #define BUTTON_PIN 38 -#endif + // #define BUTTON_NEED_PULLUP // #define BATTERY_PIN 27 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage From f4c5e31f3d53ca5f06324682b2101af1b42457f6 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Fri, 6 Jun 2025 21:55:30 -0500 Subject: [PATCH 254/265] Trunk --- src/ButtonThread.cpp | 66 ++++++++++++++++++++++---------------------- src/ButtonThread.h | 10 +++---- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/ButtonThread.cpp b/src/ButtonThread.cpp index 8c62a7141..574301249 100644 --- a/src/ButtonThread.cpp +++ b/src/ButtonThread.cpp @@ -8,12 +8,12 @@ #include "PowerFSM.h" #include "RadioLibInterface.h" #include "buzz.h" +#include "input/InputBroker.h" #include "main.h" #include "modules/CannedMessageModule.h" #include "modules/ExternalNotificationModule.h" #include "power.h" #include "sleep.h" -#include "input/InputBroker.h" #ifdef ARCH_PORTDUINO #include "platform/portduino/PortduinoGlue.h" #endif @@ -264,45 +264,45 @@ int32_t ButtonThread::runOnce() if (btnEvent != BUTTON_EVENT_NONE) { #if HAS_SCREEN - switch (btnEvent) { - case BUTTON_EVENT_PRESSED: { - LOG_BUTTON("press!"); + switch (btnEvent) { + case BUTTON_EVENT_PRESSED: { + LOG_BUTTON("press!"); - // Play boop sound for every button press - playBoop(); + // Play boop sound for every button press + playBoop(); - // Forward single press to InputBroker (but NOT as DOWN/SELECT, just forward a "button press" event) - if (inputBroker) { - InputEvent evt = { "button", INPUT_BROKER_MSG_BUTTON_PRESSED, 0, 0, 0 }; - inputBroker->injectInputEvent(&evt); - } - - // Start tracking for potential combination - waitingForLongPress = true; - shortPressTime = millis(); - - break; + // Forward single press to InputBroker (but NOT as DOWN/SELECT, just forward a "button press" event) + if (inputBroker) { + InputEvent evt = {"button", INPUT_BROKER_MSG_BUTTON_PRESSED, 0, 0, 0}; + inputBroker->injectInputEvent(&evt); } - case BUTTON_EVENT_LONG_PRESSED: { - LOG_BUTTON("Long press!"); - // Play beep sound - playBeep(); + // Start tracking for potential combination + waitingForLongPress = true; + shortPressTime = millis(); - // Forward long press to InputBroker (but NOT as DOWN/SELECT, just forward a "button long press" event) - if (inputBroker) { - InputEvent evt = { "button", INPUT_BROKER_MSG_BUTTON_LONG_PRESSED, 0, 0, 0 }; - inputBroker->injectInputEvent(&evt); - } + break; + } + case BUTTON_EVENT_LONG_PRESSED: { + LOG_BUTTON("Long press!"); - waitingForLongPress = false; - break; + // Play beep sound + playBeep(); + + // Forward long press to InputBroker (but NOT as DOWN/SELECT, just forward a "button long press" event) + if (inputBroker) { + InputEvent evt = {"button", INPUT_BROKER_MSG_BUTTON_LONG_PRESSED, 0, 0, 0}; + inputBroker->injectInputEvent(&evt); } - default: - // Ignore all other events on screen devices - break; - } - btnEvent = BUTTON_EVENT_NONE; + + waitingForLongPress = false; + break; + } + default: + // Ignore all other events on screen devices + break; + } + btnEvent = BUTTON_EVENT_NONE; #else // On devices without screen: full legacy logic switch (btnEvent) { diff --git a/src/ButtonThread.h b/src/ButtonThread.h index e4bbba8f0..2d7be7bdd 100644 --- a/src/ButtonThread.h +++ b/src/ButtonThread.h @@ -9,12 +9,12 @@ #endif #ifdef HAS_SCREEN - #undef BUTTON_LONGPRESS_MS - #define BUTTON_LONGPRESS_MS 500 +#undef BUTTON_LONGPRESS_MS +#define BUTTON_LONGPRESS_MS 500 #else - #ifndef BUTTON_LONGPRESS_MS - #define BUTTON_LONGPRESS_MS 5000 - #endif +#ifndef BUTTON_LONGPRESS_MS +#define BUTTON_LONGPRESS_MS 5000 +#endif #endif #ifndef BUTTON_TOUCH_MS From 652033a0b4dd66a1ce49d98ad94b05f68e4ba13b Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Fri, 6 Jun 2025 22:33:04 -0500 Subject: [PATCH 255/265] Onebutton Menu Support --- src/graphics/Screen.cpp | 19 +++++++++++-------- src/graphics/draw/NotificationRenderer.cpp | 10 +++++----- src/{ => input}/ButtonThread.cpp | 20 ++++++++++++++++++-- src/{ => input}/ButtonThread.h | 0 src/input/InputBroker.h | 1 + src/main.cpp | 2 +- 6 files changed, 36 insertions(+), 16 deletions(-) rename src/{ => input}/ButtonThread.cpp (97%) rename src/{ => input}/ButtonThread.h (100%) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 25cd25663..4fcb2b1a1 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -36,7 +36,6 @@ along with this program. If not, see . #if !MESHTASTIC_EXCLUDE_GPS #include "GPS.h" #endif -#include "ButtonThread.h" #include "FSCommon.h" #include "MeshService.h" #include "NodeDB.h" @@ -48,6 +47,7 @@ along with this program. If not, see . #include "graphics/SharedUIDisplay.h" #include "graphics/emotes.h" #include "graphics/images.h" +#include "input/ButtonThread.h" #include "input/ScanAndSelect.h" #include "input/TouchScreenImpl1.h" #include "main.h" @@ -1781,6 +1781,13 @@ int Screen::handleUIFrameEvent(const UIFrameEvent *event) int Screen::handleInputEvent(const InputEvent *event) { + + if (NotificationRenderer::isOverlayBannerShowing()) { + NotificationRenderer::inEvent = event->inputEvent; + setFrames(); + ui->update(); + return 0; + } #if defined(DISPLAY_CLOCK_FRAME) // For the T-Watch, intercept touches to the 'toggle digital/analog watch face' button uint8_t watchFaceFrame = error_code ? 1 : 0; @@ -1794,12 +1801,7 @@ int Screen::handleInputEvent(const InputEvent *event) return 0; } #endif - if (NotificationRenderer::isOverlayBannerShowing()) { - NotificationRenderer::inEvent = event->inputEvent; - setFrames(); - ui->update(); - return 0; - } + // Use left or right input from a keyboard to move between frames, // so long as a mesh module isn't using these events for some other purpose if (showingNormalScreen) { @@ -1829,7 +1831,8 @@ int Screen::handleInputEvent(const InputEvent *event) } else if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) { showNextFrame(); } else if (event->inputEvent == - static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) { + static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT) || + event->inputEvent == INPUT_BROKER_MSG_BUTTON_DOUBLE_PRESSED) { #if HAS_TFT if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) { showOverlayBanner("Switch to MUI?\nYES\nNO", 30000, 2, [](int selected) -> void { diff --git a/src/graphics/draw/NotificationRenderer.cpp b/src/graphics/draw/NotificationRenderer.cpp index d9baaaee2..9bde377b2 100644 --- a/src/graphics/draw/NotificationRenderer.cpp +++ b/src/graphics/draw/NotificationRenderer.cpp @@ -59,7 +59,7 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp // Exit if no message is active or duration has passed if (!isOverlayBannerShowing()) return; - LOG_DEBUG("event: %u, curSelected: %d", inEvent, curSelected); + // === Layout Configuration === constexpr uint16_t padding = 5; // Padding around text inside the box constexpr uint16_t vPadding = 2; // Padding around text inside the box @@ -105,9 +105,11 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp // respond to input if (inEvent == meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP) { curSelected--; - } else if (inEvent == meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN) { + } else if (inEvent == meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN || + inEvent == INPUT_BROKER_MSG_BUTTON_PRESSED) { curSelected++; - } else if (inEvent == meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT) { + } else if (inEvent == meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT || + inEvent == INPUT_BROKER_MSG_BUTTON_DOUBLE_PRESSED) { alertBannerCallback(curSelected); alertBannerMessage[0] = '\0'; } @@ -172,8 +174,6 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp // === Draw each line centered in the box === int16_t lineY = boxTop + vPadding; - LOG_DEBUG("firstOptionToShow: %u, firstOption: %u", firstOptionToShow, firstOption); - // for (int i = 0; i < lineCount; i++) { // is this line selected? // if so, start the buffer with -> and strncpy to the 4th location diff --git a/src/ButtonThread.cpp b/src/input/ButtonThread.cpp similarity index 97% rename from src/ButtonThread.cpp rename to src/input/ButtonThread.cpp index 574301249..074b4b4b7 100644 --- a/src/ButtonThread.cpp +++ b/src/input/ButtonThread.cpp @@ -266,7 +266,7 @@ int32_t ButtonThread::runOnce() #if HAS_SCREEN switch (btnEvent) { case BUTTON_EVENT_PRESSED: { - LOG_BUTTON("press!"); + LOG_WARN("press!"); // Play boop sound for every button press playBoop(); @@ -283,8 +283,24 @@ int32_t ButtonThread::runOnce() break; } + case BUTTON_EVENT_DOUBLE_PRESSED: { + LOG_WARN("press!"); + + // Play boop sound for every button press + playBoop(); + + // Forward single press to InputBroker (but NOT as DOWN/SELECT, just forward a "button press" event) + if (inputBroker) { + InputEvent evt = {"button", INPUT_BROKER_MSG_BUTTON_DOUBLE_PRESSED, 0, 0, 0}; + inputBroker->injectInputEvent(&evt); + } + + waitingForLongPress = false; + + break; + } case BUTTON_EVENT_LONG_PRESSED: { - LOG_BUTTON("Long press!"); + LOG_WARN("Long press!"); // Play beep sound playBeep(); diff --git a/src/ButtonThread.h b/src/input/ButtonThread.h similarity index 100% rename from src/ButtonThread.h rename to src/input/ButtonThread.h diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h index 6948390d2..316ea60a7 100644 --- a/src/input/InputBroker.h +++ b/src/input/InputBroker.h @@ -23,6 +23,7 @@ #define INPUT_BROKER_MSG_EMOTE_LIST 0x8F #define INPUT_BROKER_MSG_BUTTON_PRESSED 0xe1 #define INPUT_BROKER_MSG_BUTTON_LONG_PRESSED 0xe2 +#define INPUT_BROKER_MSG_BUTTON_DOUBLE_PRESSED 0xe3 typedef struct _InputEvent { const char *source; diff --git a/src/main.cpp b/src/main.cpp index 6c6269056..6744addfd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -99,7 +99,7 @@ NRF52Bluetooth *nrf52Bluetooth = nullptr; #endif #if HAS_BUTTON || defined(ARCH_PORTDUINO) -#include "ButtonThread.h" +#include "input/ButtonThread.h" #endif #include "AmbientLightingThread.h" From 4dde0a92024479eabbdbcae3cebc437709ef55f2 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Fri, 6 Jun 2025 23:25:16 -0500 Subject: [PATCH 256/265] Add temporary clock icon --- src/graphics/Screen.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 4fcb2b1a1..4f85dc7bf 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1390,6 +1390,7 @@ void Screen::setFrames(FrameFocus focus) #if defined(DISPLAY_CLOCK_FRAME) normalFrames[numframes++] = screen->digitalWatchFace ? &Screen::drawDigitalClockFrame : &Screen::drawAnalogClockFrame; + indicatorIcons.push_back(icon_compass); #endif // Declare this early so it’s available in FOCUS_PRESERVE block From 6c3f24dfe61aea73a997261caef180f389008cfe Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Fri, 6 Jun 2025 23:25:53 -0500 Subject: [PATCH 257/265] Add gps location to fsi --- src/graphics/Screen.cpp | 1 + src/graphics/Screen.h | 1 + 2 files changed, 2 insertions(+) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 4f85dc7bf..c6d3c5f13 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1425,6 +1425,7 @@ void Screen::setFrames(FrameFocus focus) normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses; indicatorIcons.push_back(icon_list); + fsi.positions.gps = numframes; normalFrames[numframes++] = graphics::UIRenderer::drawCompassAndLocationScreen; indicatorIcons.push_back(icon_compass); diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index fc2592c4d..8ee824f25 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -632,6 +632,7 @@ class Screen : public concurrency::OSThread uint8_t wifi = 255; uint8_t deviceFocused = 255; uint8_t memory = 255; + uint8_t gps = 255; } positions; uint8_t frameCount = 0; From 67d3cafc6f0fbadce6412423c7ea8983320f51e2 Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Fri, 6 Jun 2025 23:42:05 -0500 Subject: [PATCH 258/265] Banner message state reset --- src/graphics/Screen.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index c6d3c5f13..6906dfe14 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -144,6 +144,7 @@ void Screen::showOverlayBanner(const char *message, uint32_t durationMs, uint8_t NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs; NotificationRenderer::alertBannerOptions = options; NotificationRenderer::alertBannerCallback = bannerCallback; + NotificationRenderer::curSelected = 0; } static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) @@ -1846,6 +1847,19 @@ int Screen::handleInputEvent(const InputEvent *event) } }); } +#endif +#if HAS_GPS + if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps) { + showOverlayBanner("Toggle GPS\nENABLED\nDISABLED", 30000, 2, [](int selected) -> void { + if (selected == 0) { + config.position.gps_enabled = true; + gps->enable(); + } else if (selected == 1) { + config.position.gps_enabled = false; + gps->disable(); + } + }); + } #endif } } From f8ea9c0e405bb3accc10cd233758bee155793d0f Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sat, 7 Jun 2025 00:02:49 -0500 Subject: [PATCH 259/265] Cast to char to satisfy compiler --- src/input/ButtonThread.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp index 074b4b4b7..4311055d6 100644 --- a/src/input/ButtonThread.cpp +++ b/src/input/ButtonThread.cpp @@ -273,7 +273,7 @@ int32_t ButtonThread::runOnce() // Forward single press to InputBroker (but NOT as DOWN/SELECT, just forward a "button press" event) if (inputBroker) { - InputEvent evt = {"button", INPUT_BROKER_MSG_BUTTON_PRESSED, 0, 0, 0}; + InputEvent evt = {"button", (char)INPUT_BROKER_MSG_BUTTON_PRESSED, 0, 0, 0}; inputBroker->injectInputEvent(&evt); } @@ -291,7 +291,7 @@ int32_t ButtonThread::runOnce() // Forward single press to InputBroker (but NOT as DOWN/SELECT, just forward a "button press" event) if (inputBroker) { - InputEvent evt = {"button", INPUT_BROKER_MSG_BUTTON_DOUBLE_PRESSED, 0, 0, 0}; + InputEvent evt = {"button", (char)INPUT_BROKER_MSG_BUTTON_DOUBLE_PRESSED, 0, 0, 0}; inputBroker->injectInputEvent(&evt); } @@ -307,7 +307,7 @@ int32_t ButtonThread::runOnce() // Forward long press to InputBroker (but NOT as DOWN/SELECT, just forward a "button long press" event) if (inputBroker) { - InputEvent evt = {"button", INPUT_BROKER_MSG_BUTTON_LONG_PRESSED, 0, 0, 0}; + InputEvent evt = {"button", (char)INPUT_BROKER_MSG_BUTTON_LONG_PRESSED, 0, 0, 0}; inputBroker->injectInputEvent(&evt); } From fd8cc1c78b9e39c8bfcee661348189f162731f2a Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Sat, 7 Jun 2025 00:51:10 -0500 Subject: [PATCH 260/265] Better fast handling of input during banner --- src/graphics/Screen.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 6906dfe14..0429e9e78 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -1787,7 +1787,10 @@ int Screen::handleInputEvent(const InputEvent *event) if (NotificationRenderer::isOverlayBannerShowing()) { NotificationRenderer::inEvent = event->inputEvent; - setFrames(); + static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, + NotificationRenderer::drawAlertBannerOverlay}; + ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0])); + setFastFramerate(); // Draw ASAP ui->update(); return 0; } From a8706ca6358137b409ac69df32165a3eafc2bb2e Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 7 Jun 2025 06:23:23 -0500 Subject: [PATCH 261/265] Fix warning --- src/modules/CannedMessageModule.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 4d6239b57..798d00049 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -1823,7 +1823,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st } // If word itself too big, split by character if (wordWidth > maxWidth) { - int charPos = 0; + uint16_t charPos = 0; while (charPos < word.length()) { String oneChar = word.substring(charPos, charPos + 1); int charWidth = display->getStringWidth(oneChar); From 6b26fd2be2b393eaf508d7dfd9621a7fcb6bea24 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 7 Jun 2025 06:24:29 -0500 Subject: [PATCH 262/265] Derp --- protobufs | 2 +- src/ButtonThread.h | 118 +++++++++++++++++++++++++++++++++++++ src/input/ButtonThread.cpp | 2 +- 3 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 src/ButtonThread.h diff --git a/protobufs b/protobufs index 24c7a3d28..cec9223ae 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 24c7a3d287a4bd269ce191827e5dabd8ce8f57a7 +Subproject commit cec9223ae1151fff326d94337570c0e46bacb6f4 diff --git a/src/ButtonThread.h b/src/ButtonThread.h new file mode 100644 index 000000000..1fbc38672 --- /dev/null +++ b/src/ButtonThread.h @@ -0,0 +1,118 @@ +#pragma once + +#include "OneButton.h" +#include "concurrency/OSThread.h" +#include "configuration.h" + +#ifndef BUTTON_CLICK_MS +#define BUTTON_CLICK_MS 250 +#endif + +#ifndef BUTTON_LONGPRESS_MS +#define BUTTON_LONGPRESS_MS 5000 +#endif + +#ifndef BUTTON_TOUCH_MS +#define BUTTON_TOUCH_MS 400 +#endif + +#ifndef BUTTON_COMBO_TIMEOUT_MS +#define BUTTON_COMBO_TIMEOUT_MS 2000 // 2 seconds to complete the combination +#endif + +#ifndef BUTTON_LEADUP_MS +#define BUTTON_LEADUP_MS 2200 // Play lead-up sound after 2.5 seconds of holding +#endif + +class ButtonThread : public concurrency::OSThread +{ + public: + static const uint32_t c_holdOffTime = 30000; // hold off 30s after boot + + enum ButtonEventType { + BUTTON_EVENT_NONE, + BUTTON_EVENT_PRESSED, + BUTTON_EVENT_PRESSED_SCREEN, + BUTTON_EVENT_DOUBLE_PRESSED, + BUTTON_EVENT_MULTI_PRESSED, + BUTTON_EVENT_LONG_PRESSED, + BUTTON_EVENT_LONG_RELEASED, + BUTTON_EVENT_TOUCH_LONG_PRESSED, + BUTTON_EVENT_COMBO_SHORT_LONG, + }; + + ButtonThread(); + int32_t runOnce() override; + void attachButtonInterrupts(); + void detachButtonInterrupts(); + void storeClickCount(); + bool isBuzzing() { return buzzer_flag; } + void setScreenFlag(bool flag) { screen_flag = flag; } + bool getScreenFlag() { return screen_flag; } + bool isInterceptingAndFocused(); + bool isButtonPressed(int buttonPin) + { +#ifdef BUTTON_ACTIVE_LOW + return !digitalRead(buttonPin); // Active low: pressed = LOW +#else + return digitalRead(buttonPin); // Most buttons are active low by default +#endif + } + + // Disconnect and reconnect interrupts for light sleep +#ifdef ARCH_ESP32 + int beforeLightSleep(void *unused); + int afterLightSleep(esp_sleep_wakeup_cause_t cause); +#endif + private: +#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN) + static OneButton userButton; // Static - accessed from an interrupt +#endif +#ifdef BUTTON_PIN_ALT + OneButton userButtonAlt; +#endif +#ifdef BUTTON_PIN_TOUCH + OneButton userButtonTouch; +#endif + +#ifdef ARCH_ESP32 + // Get notified when lightsleep begins and ends + CallbackObserver lsObserver = + CallbackObserver(this, &ButtonThread::beforeLightSleep); + CallbackObserver lsEndObserver = + CallbackObserver(this, &ButtonThread::afterLightSleep); +#endif + + // set during IRQ + static volatile ButtonEventType btnEvent; + bool buzzer_flag = false; + bool screen_flag = true; + + // Store click count during callback, for later use + volatile int multipressClickCount = 0; + + // Combination tracking state + bool waitingForLongPress = false; + uint32_t shortPressTime = 0; + + // Long press lead-up tracking + bool leadUpPlayed = false; + uint32_t lastLeadUpNoteTime = 0; + bool leadUpSequenceActive = false; + + static void wakeOnIrq(int irq, int mode); + + static void sendAdHocPosition(); + static void switchPage(); + + // IRQ callbacks + static void userButtonPressed() { btnEvent = BUTTON_EVENT_PRESSED; } + static void userButtonPressedScreen() { btnEvent = BUTTON_EVENT_PRESSED_SCREEN; } + static void userButtonDoublePressed() { btnEvent = BUTTON_EVENT_DOUBLE_PRESSED; } + static void userButtonMultiPressed(void *callerThread); // Retrieve click count from non-static Onebutton while still valid + static void userButtonPressedLongStart(); + static void userButtonPressedLongStop(); + static void touchPressedLongStart() { btnEvent = BUTTON_EVENT_TOUCH_LONG_PRESSED; } +}; + +extern ButtonThread *buttonThread; diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp index 4311055d6..21d788b25 100644 --- a/src/input/ButtonThread.cpp +++ b/src/input/ButtonThread.cpp @@ -18,7 +18,7 @@ #include "platform/portduino/PortduinoGlue.h" #endif -#define DEBUG_BUTTONS 0 +#define DEBUG_BUTTONS 1 #if DEBUG_BUTTONS #define LOG_BUTTON(...) LOG_DEBUG(__VA_ARGS__) #else From d8292fe9b4f02c2d68d1572de9199a6715a7cea3 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 7 Jun 2025 06:50:10 -0500 Subject: [PATCH 263/265] oops --- src/ButtonThread.h | 118 --------------------------------------------- 1 file changed, 118 deletions(-) delete mode 100644 src/ButtonThread.h diff --git a/src/ButtonThread.h b/src/ButtonThread.h deleted file mode 100644 index 1fbc38672..000000000 --- a/src/ButtonThread.h +++ /dev/null @@ -1,118 +0,0 @@ -#pragma once - -#include "OneButton.h" -#include "concurrency/OSThread.h" -#include "configuration.h" - -#ifndef BUTTON_CLICK_MS -#define BUTTON_CLICK_MS 250 -#endif - -#ifndef BUTTON_LONGPRESS_MS -#define BUTTON_LONGPRESS_MS 5000 -#endif - -#ifndef BUTTON_TOUCH_MS -#define BUTTON_TOUCH_MS 400 -#endif - -#ifndef BUTTON_COMBO_TIMEOUT_MS -#define BUTTON_COMBO_TIMEOUT_MS 2000 // 2 seconds to complete the combination -#endif - -#ifndef BUTTON_LEADUP_MS -#define BUTTON_LEADUP_MS 2200 // Play lead-up sound after 2.5 seconds of holding -#endif - -class ButtonThread : public concurrency::OSThread -{ - public: - static const uint32_t c_holdOffTime = 30000; // hold off 30s after boot - - enum ButtonEventType { - BUTTON_EVENT_NONE, - BUTTON_EVENT_PRESSED, - BUTTON_EVENT_PRESSED_SCREEN, - BUTTON_EVENT_DOUBLE_PRESSED, - BUTTON_EVENT_MULTI_PRESSED, - BUTTON_EVENT_LONG_PRESSED, - BUTTON_EVENT_LONG_RELEASED, - BUTTON_EVENT_TOUCH_LONG_PRESSED, - BUTTON_EVENT_COMBO_SHORT_LONG, - }; - - ButtonThread(); - int32_t runOnce() override; - void attachButtonInterrupts(); - void detachButtonInterrupts(); - void storeClickCount(); - bool isBuzzing() { return buzzer_flag; } - void setScreenFlag(bool flag) { screen_flag = flag; } - bool getScreenFlag() { return screen_flag; } - bool isInterceptingAndFocused(); - bool isButtonPressed(int buttonPin) - { -#ifdef BUTTON_ACTIVE_LOW - return !digitalRead(buttonPin); // Active low: pressed = LOW -#else - return digitalRead(buttonPin); // Most buttons are active low by default -#endif - } - - // Disconnect and reconnect interrupts for light sleep -#ifdef ARCH_ESP32 - int beforeLightSleep(void *unused); - int afterLightSleep(esp_sleep_wakeup_cause_t cause); -#endif - private: -#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN) - static OneButton userButton; // Static - accessed from an interrupt -#endif -#ifdef BUTTON_PIN_ALT - OneButton userButtonAlt; -#endif -#ifdef BUTTON_PIN_TOUCH - OneButton userButtonTouch; -#endif - -#ifdef ARCH_ESP32 - // Get notified when lightsleep begins and ends - CallbackObserver lsObserver = - CallbackObserver(this, &ButtonThread::beforeLightSleep); - CallbackObserver lsEndObserver = - CallbackObserver(this, &ButtonThread::afterLightSleep); -#endif - - // set during IRQ - static volatile ButtonEventType btnEvent; - bool buzzer_flag = false; - bool screen_flag = true; - - // Store click count during callback, for later use - volatile int multipressClickCount = 0; - - // Combination tracking state - bool waitingForLongPress = false; - uint32_t shortPressTime = 0; - - // Long press lead-up tracking - bool leadUpPlayed = false; - uint32_t lastLeadUpNoteTime = 0; - bool leadUpSequenceActive = false; - - static void wakeOnIrq(int irq, int mode); - - static void sendAdHocPosition(); - static void switchPage(); - - // IRQ callbacks - static void userButtonPressed() { btnEvent = BUTTON_EVENT_PRESSED; } - static void userButtonPressedScreen() { btnEvent = BUTTON_EVENT_PRESSED_SCREEN; } - static void userButtonDoublePressed() { btnEvent = BUTTON_EVENT_DOUBLE_PRESSED; } - static void userButtonMultiPressed(void *callerThread); // Retrieve click count from non-static Onebutton while still valid - static void userButtonPressedLongStart(); - static void userButtonPressedLongStop(); - static void touchPressedLongStart() { btnEvent = BUTTON_EVENT_TOUCH_LONG_PRESSED; } -}; - -extern ButtonThread *buttonThread; From 706fbc86aea17d7d6fc8e8f39de991fa16fb7d93 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 7 Jun 2025 07:57:27 -0500 Subject: [PATCH 264/265] Update ref --- protobufs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protobufs b/protobufs index cec9223ae..db60f07ac 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit cec9223ae1151fff326d94337570c0e46bacb6f4 +Subproject commit db60f07ac298b6161ca553b3868b542cceadcac4 From 94e0b209bcf16e73a6aa13ec1cef184d005e6912 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 7 Jun 2025 08:11:44 -0500 Subject: [PATCH 265/265] Wire buzzer_mode --- src/buzz/buzz.cpp | 5 +++++ src/modules/ExternalNotificationModule.cpp | 20 ++++++++++++++++---- src/modules/ExternalNotificationModule.h | 2 ++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/buzz/buzz.cpp b/src/buzz/buzz.cpp index fc113dcae..572e25d82 100644 --- a/src/buzz/buzz.cpp +++ b/src/buzz/buzz.cpp @@ -38,6 +38,11 @@ const int DURATION_1_1 = 1000; // 1/1 note void playTones(const ToneDuration *tone_durations, int size) { + if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED && + config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_NOTIFICATIONS_ONLY) { + // Buzzer is disabled or not set to system tones + return; + } #ifdef PIN_BUZZER if (!config.device.buzzer_gpio) config.device.buzzer_gpio = PIN_BUZZER; diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index dc17460f6..fe80404f3 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -188,7 +188,7 @@ int32_t ExternalNotificationModule::runOnce() // Play RTTTL over i2s audio interface if enabled as buzzer #ifdef HAS_I2S - if (moduleConfig.external_notification.use_i2s_as_buzzer) { + if (moduleConfig.external_notification.use_i2s_as_buzzer && canBuzz()) { if (audioThread->isPlaying()) { // Continue playing } else if (isNagging && (nagCycleCutoff >= millis())) { @@ -197,7 +197,7 @@ int32_t ExternalNotificationModule::runOnce() } #endif // now let the PWM buzzer play - if (moduleConfig.external_notification.use_pwm && config.device.buzzer_gpio) { + if (moduleConfig.external_notification.use_pwm && config.device.buzzer_gpio && canBuzz()) { if (rtttl::isPlaying()) { rtttl::play(); } else if (isNagging && (nagCycleCutoff >= millis())) { @@ -210,6 +210,18 @@ int32_t ExternalNotificationModule::runOnce() } } +/** + * Based on buzzer mode, return true if we can buzz. + */ +bool ExternalNotificationModule::canBuzz() +{ + if (config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED && + config.device.buzzer_mode != meshtastic_Config_DeviceConfig_BuzzerMode_SYSTEM_ONLY) { + return true; + } + return false; +} + bool ExternalNotificationModule::wantPacket(const meshtastic_MeshPacket *p) { return MeshService::isTextPayload(p); @@ -364,7 +376,7 @@ ExternalNotificationModule::ExternalNotificationModule() setExternalState(1, false); externalTurnedOn[1] = 0; } - if (moduleConfig.external_notification.output_buzzer) { + if (moduleConfig.external_notification.output_buzzer && canBuzz()) { if (!moduleConfig.external_notification.use_pwm) { LOG_INFO("Use Pin %i for buzzer", moduleConfig.external_notification.output_buzzer); pinMode(moduleConfig.external_notification.output_buzzer, OUTPUT); @@ -454,7 +466,7 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP } } - if (moduleConfig.external_notification.alert_bell_buzzer) { + if (moduleConfig.external_notification.alert_bell_buzzer && canBuzz()) { if (containsBell) { LOG_INFO("externalNotificationModule - Notification Bell (Buzzer)"); isNagging = true; diff --git a/src/modules/ExternalNotificationModule.h b/src/modules/ExternalNotificationModule.h index 841ca6de9..089e0b8a3 100644 --- a/src/modules/ExternalNotificationModule.h +++ b/src/modules/ExternalNotificationModule.h @@ -40,6 +40,8 @@ class ExternalNotificationModule : public SinglePortModule, private concurrency: void setMute(bool mute) { isMuted = mute; } bool getMute() { return isMuted; } + bool canBuzz(); + void stopNow(); void handleGetRingtone(const meshtastic_MeshPacket &req, meshtastic_AdminMessage *response);