Added screen 2, BatteryDeviceLoRa

pull/6459/head
Xaositek 2025-03-29 20:49:38 -05:00
rodzic a3a0c14923
commit a77225260b
1 zmienionych plików z 220 dodań i 80 usunięć

Wyświetl plik

@ -444,8 +444,10 @@ static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *img
int screenWidth = display->getWidth(); int screenWidth = display->getWidth();
int scale = 1; int scale = 1;
if (screenWidth >= 200) scale = 2; if (screenWidth >= 200)
if (screenWidth >= 300) scale = 2; // Do NOT go higher than 2 scale = 2;
if (screenWidth >= 300)
scale = 2; // Do NOT go higher than 2
// Draw scaled battery image (16 columns × 8 rows) // Draw scaled battery image (16 columns × 8 rows)
for (int col = 0; col < 16; col++) { for (int col = 0; col < 16; col++) {
@ -1151,20 +1153,21 @@ static void drawGPS(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus
int maxDrawWidth = 6; // Position icon int maxDrawWidth = 6; // Position icon
if (!gps->getHasLock()) { if (!gps->getHasLock()) {
maxDrawWidth += display->getStringWidth("No sats") + 2; // icon + text + buffer maxDrawWidth += display->getStringWidth("No sats") + 2; // icon + text + buffer
} else { } else {
maxDrawWidth += (5 * 2) + 8 + display->getStringWidth("99") + 2; // bars + sat icon + text + buffer maxDrawWidth += (5 * 2) + 8 + display->getStringWidth("99") + 2; // bars + sat icon + text + buffer
} }
if (x + maxDrawWidth > SCREEN_WIDTH) { if (x + maxDrawWidth > SCREEN_WIDTH) {
x = SCREEN_WIDTH - maxDrawWidth; 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); display->drawFastImage(x, y, 6, 8, gps->getHasLock() ? imgPositionSolid : imgPositionEmpty);
if (!gps->getHasLock()) { if (!gps->getHasLock()) {
// Draw "No sats" to the right of the icon with slightly more gap // 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"); display->drawString(textX, y - 2, "No sats");
if (config.display.heading_bold) if (config.display.heading_bold)
display->drawString(textX + 1, y - 2, "No sats"); display->drawString(textX + 1, y - 2, "No sats");
@ -1195,7 +1198,6 @@ static void drawGPS(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus
} }
} }
// Draw status when GPS is disabled or not present // Draw status when GPS is disabled or not present
static void drawGPSpowerstat(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) static void drawGPSpowerstat(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps)
{ {
@ -1333,7 +1335,7 @@ void Screen::drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t com
Serial.print(headingRadian); Serial.print(headingRadian);
Serial.print(" | (deg): "); Serial.print(" | (deg): ");
Serial.println(headingRadian * RAD_TO_DEG); Serial.println(headingRadian * RAD_TO_DEG);
Point tip(0.0f, 0.5f), tail(0.0f, -0.35f); // pointing up initially Point tip(0.0f, 0.5f), tail(0.0f, -0.35f); // pointing up initially
float arrowOffsetX = 0.14f, arrowOffsetY = 1.0f; float arrowOffsetX = 0.14f, arrowOffsetY = 1.0f;
Point leftArrow(tip.x - arrowOffsetX, tip.y - arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y - arrowOffsetY); Point leftArrow(tip.x - arrowOffsetX, tip.y - arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y - arrowOffsetY);
@ -1352,10 +1354,14 @@ void Screen::drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t com
display->drawLine(leftArrow.x, leftArrow.y, tail.x, tail.y); display->drawLine(leftArrow.x, leftArrow.y, tail.x, tail.y);
display->drawLine(rightArrow.x, rightArrow.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("🔥 Arrow Tail X: ");
Serial.print(" | Y: "); Serial.print(tail.y); Serial.print(tail.x);
Serial.print(" | Tip X: "); Serial.print(tip.x); Serial.print(" | Y: ");
Serial.print(" | Tip Y: "); Serial.println(tip.y); Serial.print(tail.y);
Serial.print(" | Tip X: ");
Serial.print(tip.x);
Serial.print(" | Tip Y: ");
Serial.println(tip.y);
#ifdef USE_EINK #ifdef USE_EINK
display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y); display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y);
#else #else
@ -1417,9 +1423,10 @@ void Screen::drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t co
rosePoints[i]->translate(compassX, compassY); rosePoints[i]->translate(compassX, compassY);
} }
display->drawCircle(NC1.x, NC1.y, 4); // North sign circle, 4px radius is sufficient for all displays. 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("🔥 North Marker X: ");
Serial.print(" | Y: "); Serial.println(NC1.y); Serial.print(NC1.x);
Serial.print(" | Y: ");
Serial.println(NC1.y);
} }
uint16_t Screen::getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) uint16_t Screen::getCompassDiam(uint32_t displayWidth, uint32_t displayHeight)
@ -1567,17 +1574,18 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
} }
// h! Makes header invert rounder // 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 // Center rectangles
display->fillRect(x + r, y, w - 2 * r, h); display->fillRect(x + r, y, w - 2 * r, h);
display->fillRect(x, y + r, r, h - 2 * r); display->fillRect(x, y + r, r, h - 2 * r);
display->fillRect(x + w - r, y + r, r, h - 2 * r); display->fillRect(x + w - r, y + r, r, h - 2 * r);
// Rounded corners // Rounded corners
display->fillCircle(x + r, y + r, r); // Top-left display->fillCircle(x + r, y + r, r); // Top-left
display->fillCircle(x + w - r - 1, y + r, r); // Top-right 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, y + h - r - 1, r); // Bottom-left
display->fillCircle(x + w - r - 1, y + h - r - 1, r); // Bottom-right 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 // h! Each node entry holds a reference to its info and how long ago it was heard from
struct NodeEntry { struct NodeEntry {
@ -1586,7 +1594,8 @@ struct NodeEntry {
}; };
// h! 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) { float calculateBearing(double lat1, double lon1, double lat2, double lon2)
{
double dLon = (lon2 - lon1) * DEG_TO_RAD; double dLon = (lon2 - lon1) * DEG_TO_RAD;
lat1 = lat1 * DEG_TO_RAD; lat1 = lat1 * DEG_TO_RAD;
lat2 = lat2 * DEG_TO_RAD; lat2 = lat2 * DEG_TO_RAD;
@ -1595,22 +1604,25 @@ float calculateBearing(double lat1, double lon1, double lat2, double lon2) {
double x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon); double x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon);
double initialBearing = atan2(y, x); 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 // Shared scroll index state for node screens
static int scrollIndex = 0; static int scrollIndex = 0;
// Helper: Calculates max scroll index based on total entries // 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; int totalRows = (totalEntries + 1) / 2;
return std::max(0, totalRows - visibleRows); return std::max(0, totalRows - visibleRows);
} }
// Helper: Draw vertical scrollbar matching CannedMessageModule style // 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; 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 scrollAreaHeight = visibleNodeRows * (FONT_HEIGHT_SMALL - 3); // true pixel height used per row
int scrollbarX = display->getWidth() - 6; int scrollbarX = display->getWidth() - 6;
@ -1624,32 +1636,38 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
} }
// Grabs all nodes from the DB and sorts them (favorites and most recently heard first) // Grabs all nodes from the DB and sorts them (favorites and most recently heard first)
void retrieveAndSortNodes(std::vector<NodeEntry> &nodeList) { void retrieveAndSortNodes(std::vector<NodeEntry> &nodeList)
{
size_t numNodes = nodeDB->getNumMeshNodes(); size_t numNodes = nodeDB->getNumMeshNodes();
for (size_t i = 0; i < numNodes; i++) { for (size_t i = 0; i < numNodes; i++) {
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(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)}); nodeList.push_back({node, sinceLastSeen(node)});
} }
std::sort(nodeList.begin(), nodeList.end(), [](const NodeEntry &a, const NodeEntry &b) { std::sort(nodeList.begin(), nodeList.end(), [](const NodeEntry &a, const NodeEntry &b) {
bool aFav = a.node->is_favorite; bool aFav = a.node->is_favorite;
bool bFav = b.node->is_favorite; bool bFav = b.node->is_favorite;
if (aFav != bFav) return aFav > bFav; if (aFav != bFav)
if (a.lastHeard == 0 || a.lastHeard == UINT32_MAX) return false; return aFav > bFav;
if (b.lastHeard == 0 || b.lastHeard == UINT32_MAX) return true; 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; return a.lastHeard < b.lastHeard;
}); });
} }
// Helper: Fallback-NodeID if emote is on ShortName for display purposes // Helper: Fallback-NodeID if emote is on ShortName for display purposes
String getSafeNodeName(meshtastic_NodeInfoLite *node) { String getSafeNodeName(meshtastic_NodeInfoLite *node)
{
String nodeName = "?"; String nodeName = "?";
if (node->has_user && strlen(node->user.short_name) > 0) { if (node->has_user && strlen(node->user.short_name) > 0) {
bool valid = true; 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++) { for (size_t i = 0; i < strlen(name); i++) {
uint8_t c = (uint8_t)name[i]; uint8_t c = (uint8_t)name[i];
@ -1669,12 +1687,14 @@ String getSafeNodeName(meshtastic_NodeInfoLite *node) {
} }
} }
if (node->is_favorite) nodeName = "*" + nodeName; if (node->is_favorite)
nodeName = "*" + nodeName;
return nodeName; return nodeName;
} }
// Draws the top header bar (optionally inverted or bold) // 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 isInverted = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
bool isBold = config.display.heading_bold; bool isBold = config.display.heading_bold;
@ -1698,25 +1718,31 @@ void drawScreenHeader(OLEDDisplay *display, const char *title, int16_t x, int16_
// Draw text centered vertically and horizontally // Draw text centered vertically and horizontally
display->drawString(titleX, textY, title); display->drawString(titleX, textY, title);
if (isBold) display->drawString(titleX + 1, textY, title); if (isBold)
display->drawString(titleX + 1, textY, title);
display->setColor(WHITE); display->setColor(WHITE);
} }
// Draws separator line // 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 columnWidth = display->getWidth() / 2;
int separatorX = x + columnWidth - 2; int separatorX = x + columnWidth - 2;
display->drawLine(separatorX, yStart, separatorX, yEnd - 3); display->drawLine(separatorX, yStart, separatorX, yEnd - 3);
} }
// Draws node name with how long ago it was last heard from // 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(); int screenWidth = display->getWidth();
bool isLeftCol = (x < screenWidth / 2); bool isLeftCol = (x < screenWidth / 2);
// Adjust offset based on column and screen width // 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); String nodeName = getSafeNodeName(node);
@ -1727,7 +1753,12 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
} else { } else {
uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24; uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24;
snprintf(timeStr, sizeof(timeStr), (days > 365 ? "?" : "%d%c"), 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); display->setTextAlignment(TEXT_ALIGN_LEFT);
@ -1736,13 +1767,20 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->drawString(x + columnWidth - timeOffset, y, timeStr); display->drawString(x + columnWidth - timeOffset, y, timeStr);
} }
// Draws each node's name, hop count, and signal bars // 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(); int screenWidth = display->getWidth();
bool isLeftCol = (x < screenWidth / 2); bool isLeftCol = (x < screenWidth / 2);
int nameMaxWidth = columnWidth - 25; 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 barsOffset =
int hopOffset = (screenWidth > 128) ? (isLeftCol ? 32 : 38) : (isLeftCol ? 18 : 20);//offset large screen (?Left:Right column), offset small screen (?Left:Right column) (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; int barsXOffset = columnWidth - barsOffset;
@ -1776,10 +1814,12 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
} }
// Typedef for passing different render functions into one reusable screen function // 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) // 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 columnWidth = display->getWidth() / 2;
int totalRowsAvailable = (display->getHeight() - y - FONT_HEIGHT_SMALL) / (FONT_HEIGHT_SMALL - 3); int totalRowsAvailable = (display->getHeight() - y - FONT_HEIGHT_SMALL) / (FONT_HEIGHT_SMALL - 3);
int visibleNodeRows = std::min(6, totalRowsAvailable); int visibleNodeRows = std::min(6, totalRowsAvailable);
@ -1814,7 +1854,8 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
if (y + yOffset > display->getHeight() - FONT_HEIGHT_SMALL) { if (y + yOffset > display->getHeight() - FONT_HEIGHT_SMALL) {
yOffset = rowYOffset; yOffset = rowYOffset;
col++; col++;
if (col > 1) break; if (col > 1)
break;
} }
} }
@ -1823,21 +1864,20 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
} }
// Public screen function: shows how recently nodes were heard // 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); drawNodeListScreen(display, state, x, y, "Node List", drawEntryLastHeard);
} }
// Public screen function: shows hop count + signal strength // 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); drawNodeListScreen(display, state, x, y, "Hops/Signal", drawEntryHopSignal);
} }
// Helper function: Draw a single node entry for Node List (Modified for Compass Screen) // 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(); int screenWidth = display->getWidth();
bool isLeftCol = (x < screenWidth / 2); bool isLeftCol = (x < screenWidth / 2);
@ -1852,10 +1892,14 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
} }
// Extra compass element drawer (injects compass arrows) // 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) { void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,
if (!nodeDB->hasValidPosition(node)) return; double userLat, double userLon)
{
if (!nodeDB->hasValidPosition(node))
return;
int screenWidth = display->getWidth(); int screenWidth = display->getWidth();
bool isLeftCol = (x < screenWidth / 2); bool isLeftCol = (x < screenWidth / 2);
@ -1884,7 +1928,8 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
// Generic node+compass renderer (like drawNodeListScreen but with compass support) // 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, 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 columnWidth = display->getWidth() / 2;
int totalRowsAvailable = (display->getHeight() - y - FONT_HEIGHT_SMALL) / (FONT_HEIGHT_SMALL - 3); int totalRowsAvailable = (display->getHeight() - y - FONT_HEIGHT_SMALL) / (FONT_HEIGHT_SMALL - 3);
int visibleNodeRows = std::min(6, totalRowsAvailable); int visibleNodeRows = std::min(6, totalRowsAvailable);
@ -1935,7 +1980,8 @@ void drawNodeListWithExtrasScreen(OLEDDisplay *display, OLEDDisplayUiState *stat
if (y + yOffset > display->getHeight() - FONT_HEIGHT_SMALL) { if (y + yOffset > display->getHeight() - FONT_HEIGHT_SMALL) {
yOffset = rowYOffset; yOffset = rowYOffset;
col++; col++;
if (col > 1) break; if (col > 1)
break;
} }
} }
@ -1943,13 +1989,14 @@ void drawNodeListWithExtrasScreen(OLEDDisplay *display, OLEDDisplayUiState *stat
drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, rowYOffset); drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, rowYOffset);
} }
// Public screen entry for compass // 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); 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(); int screenWidth = display->getWidth();
bool isLeftCol = (x < screenWidth / 2); bool isLeftCol = (x < screenWidth / 2);
int nameMaxWidth = columnWidth - (screenWidth > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22)); int nameMaxWidth = columnWidth - (screenWidth > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
@ -1969,28 +2016,27 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
double dLat = (lat2 - lat1) * DEG_TO_RAD; double dLat = (lat2 - lat1) * DEG_TO_RAD;
double dLon = (lon2 - lon1) * DEG_TO_RAD; double dLon = (lon2 - lon1) * DEG_TO_RAD;
double a = sin(dLat / 2) * sin(dLat / 2) + double a =
cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2);
sin(dLon / 2) * sin(dLon / 2);
double c = 2 * atan2(sqrt(a), sqrt(1 - a)); double c = 2 * atan2(sqrt(a), sqrt(1 - a));
double distanceKm = earthRadiusKm * c; double distanceKm = earthRadiusKm * c;
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
double miles = distanceKm * 0.621371; double miles = distanceKm * 0.621371;
if (miles < 0.1) { 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) { } else if (miles < 10.0) {
snprintf(distStr, sizeof(distStr), "%.1fmi", miles); // 1 decimal snprintf(distStr, sizeof(distStr), "%.1fmi", miles); // 1 decimal
} else { } else {
snprintf(distStr, sizeof(distStr), "%dmi", (int)miles); // no decimal snprintf(distStr, sizeof(distStr), "%dmi", (int)miles); // no decimal
} }
} else { } else {
if (distanceKm < 1.0) { 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) { } else if (distanceKm < 10.0) {
snprintf(distStr, sizeof(distStr), "%.1fkm", distanceKm); // 1 decimal snprintf(distStr, sizeof(distStr), "%.1fkm", distanceKm); // 1 decimal
} else { } else {
snprintf(distStr, sizeof(distStr), "%dkm", (int)distanceKm); // no decimal snprintf(distStr, sizeof(distStr), "%dkm", (int)distanceKm); // no decimal
} }
} }
} }
@ -2005,12 +2051,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); 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) { {
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; bool isBold = config.display.heading_bold;
const int xOffset = 3; const int xOffset = 3;
@ -2037,7 +2084,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) {
int batteryOffset = screenWidth > 128 ? 34 : 18; int batteryOffset = screenWidth > 128 ? 34 : 18;
int percentX = x + xOffset + batteryOffset; int percentX = x + xOffset + batteryOffset;
display->drawString(percentX, textY, percentStr); display->drawString(percentX, textY, percentStr);
if (isBold) display->drawString(percentX + 1, textY, percentStr); if (isBold)
display->drawString(percentX + 1, textY, percentStr);
// Optional: Local time // Optional: Local time
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
@ -2050,19 +2098,26 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y) {
bool isPM = hour >= 12; bool isPM = hour >= 12;
hour = hour % 12; hour = hour % 12;
if (hour == 0) hour = 12; if (hour == 0)
hour = 12;
char timeStr[10]; 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 ? "PM" : "AM");
int timeX = SCREEN_WIDTH - xOffset - display->getStringWidth(timeStr); int timeX = SCREEN_WIDTH - xOffset - display->getStringWidth(timeStr);
display->drawString(timeX, textY, 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); display->setColor(WHITE);
} }
static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) {
// ****************************
// * Default Screen *
// ****************************
static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{
display->clear(); display->clear();
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
@ -2081,7 +2136,8 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i
if (config.position.fixed_position) { if (config.position.fixed_position) {
drawGPS(display, SCREEN_WIDTH - 44, secondRowY, gpsStatus); drawGPS(display, SCREEN_WIDTH - 44, secondRowY, gpsStatus);
} else if (!gpsStatus || !gpsStatus->getIsConnected()) { } 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; int posX = SCREEN_WIDTH - display->getStringWidth(displayLine) - 2;
display->drawString(posX, secondRowY, displayLine); display->drawString(posX, secondRowY, displayLine);
} else { } else {
@ -2094,7 +2150,7 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i
// === Third Row: LongName Centered === // === Third Row: LongName Centered ===
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) { 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 textWidth = display->getStringWidth(longName);
int nameX = (SCREEN_WIDTH - textWidth) / 2; int nameX = (SCREEN_WIDTH - textWidth) / 2;
int nameY = y + (FONT_HEIGHT_SMALL + 1) * 2; int nameY = y + (FONT_HEIGHT_SMALL + 1) * 2;
@ -2110,8 +2166,14 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i
snprintf(uptimeStr, sizeof(uptimeStr), "?"); snprintf(uptimeStr, sizeof(uptimeStr), "?");
} else { } else {
snprintf(uptimeStr, sizeof(uptimeStr), "%d%c", snprintf(uptimeStr, sizeof(uptimeStr), "%d%c",
days ? days : hours ? hours : minutes ? minutes : (int)uptime, days ? days
days ? 'd' : hours ? 'h' : minutes ? 'm' : 's'); : hours ? hours
: minutes ? minutes
: (int)uptime,
days ? 'd'
: hours ? 'h'
: minutes ? 'm'
: 's');
} }
char uptimeFullStr[16]; char uptimeFullStr[16];
@ -2121,6 +2183,83 @@ static void drawDefaultScreen(OLEDDisplay *display, OLEDDisplayUiState *state, i
display->drawString(uptimeX, uptimeY, uptimeFullStr); 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);
}
#if defined(ESP_PLATFORM) && defined(USE_ST7789) #if defined(ESP_PLATFORM) && defined(USE_ST7789)
SPIClass SPI1(HSPI); SPIClass SPI1(HSPI);
@ -2749,9 +2888,10 @@ void Screen::setFrames(FrameFocus focus)
} }
normalFrames[numframes++] = drawDefaultScreen; normalFrames[numframes++] = drawDefaultScreen;
normalFrames[numframes++] = drawBatteryDeviceLoRa;
normalFrames[numframes++] = drawLastHeardScreen; normalFrames[numframes++] = drawLastHeardScreen;
normalFrames[numframes++] = drawDistanceScreen; normalFrames[numframes++] = drawDistanceScreen;
normalFrames[numframes++] = drawNodeListWithCompasses; normalFrames[numframes++] = drawNodeListWithCompasses;
normalFrames[numframes++] = drawHopSignalScreen; normalFrames[numframes++] = drawHopSignalScreen;
// then all the nodes // then all the nodes