Node list cleanup and optimization

This commit is contained in:
HarukiToreda 2025-04-10 02:10:42 -04:00
parent 5fa236c77d
commit 35d4784c5c

View File

@ -1883,16 +1883,41 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
display->setColor(WHITE); display->setColor(WHITE);
} }
typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int); // Combined dynamic node list frame cycling through LastHeard, HopSignal, and Distance modes
typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int columnWidth, float heading, // Uses a single frame and changes data every few seconds (E-Ink variant is separate)
double lat, double lon);
// =============================
// 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 { struct NodeEntry {
meshtastic_NodeInfoLite *node; meshtastic_NodeInfoLite *node;
uint32_t lastHeard; 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) // 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)
{ {
@ -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° 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; int totalRows = (totalEntries + 1) / 2;
return std::max(0, totalRows - visibleRows); 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) // Node Sorting and Scroll Helpers
{ // =============================
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<NodeEntry> &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 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];
if (c < 32 || c > 126) { if (c < 32 || c > 126) {
@ -1979,23 +1954,63 @@ String getSafeNodeName(meshtastic_NodeInfoLite *node)
break; break;
} }
} }
if (valid) { if (valid) {
nodeName = name; nodeName = name;
} else { } else {
// fallback: last 4 hex digits of node ID, no prefix
char idStr[6]; char idStr[6];
snprintf(idStr, sizeof(idStr), "%04X", (uint16_t)(node->num & 0xFFFF)); snprintf(idStr, sizeof(idStr), "%04X", (uint16_t)(node->num & 0xFFFF));
nodeName = String(idStr); nodeName = String(idStr);
} }
} }
if (node->is_favorite) nodeName = "*" + nodeName;
if (node->is_favorite)
nodeName = "*" + nodeName;
return nodeName; return nodeName;
} }
// Draws separator line void retrieveAndSortNodes(std::vector<NodeEntry> &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) void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
{ {
int columnWidth = display->getWidth() / 2; 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); 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, 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, EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0,
double lon = 0) double lon = 0)
@ -2086,18 +2118,13 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
drawScrollbar(display, visibleNodeRows, totalEntries, scrollIndex, 2, scrollStartY); 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) void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
{ {
bool isLeftCol = (x < SCREEN_WIDTH / 2); bool isLeftCol = (x < SCREEN_WIDTH / 2);
int timeOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 41 : 45) : (isLeftCol ? 24 : 30);
// 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)
String nodeName = getSafeNodeName(node); String nodeName = getSafeNodeName(node);
@ -2108,12 +2135,8 @@ 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 (days ? days : hours ? hours : minutes),
: hours ? hours (days ? 'd' : hours ? 'h' : 'm'));
: minutes),
(days ? 'd'
: hours ? 'h'
: 'm'));
} }
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
@ -2130,15 +2153,8 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
bool isLeftCol = (x < SCREEN_WIDTH / 2); bool isLeftCol = (x < SCREEN_WIDTH / 2);
int nameMaxWidth = columnWidth - 25; int nameMaxWidth = columnWidth - 25;
int barsOffset = int barsOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 26 : 30) : (isLeftCol ? 17 : 19);
(SCREEN_WIDTH > 128) int hopOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 32 : 38) : (isLeftCol ? 18 : 20);
? (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 barsXOffset = columnWidth - barsOffset; int barsXOffset = columnWidth - barsOffset;
String nodeName = getSafeNodeName(node); String nodeName = getSafeNodeName(node);
@ -2156,7 +2172,6 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
display->drawString(hopX, y, 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 bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
int barWidth = 2; int barWidth = 2;
int barStartX = x + barsXOffset; int barStartX = x + barsXOffset;
@ -2182,45 +2197,38 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
char distStr[10] = ""; char distStr[10] = "";
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum()); meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) { if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) {
double lat1 = ourNode->position.latitude_i * 1e-7; double lat1 = ourNode->position.latitude_i * 1e-7;
double lon1 = ourNode->position.longitude_i * 1e-7; double lon1 = ourNode->position.longitude_i * 1e-7;
double lat2 = node->position.latitude_i * 1e-7; double lat2 = node->position.latitude_i * 1e-7;
double lon2 = node->position.longitude_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 earthRadiusKm = 6371.0;
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 = 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 c = 2 * atan2(sqrt(a), sqrt(1 - a));
double distanceKm = earthRadiusKm * c; double distanceKm = earthRadiusKm * c;
// Convert to imperial or metric string based on config
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)); snprintf(distStr, sizeof(distStr), "%dft", (int)(miles * 5280));
} else if (miles < 10.0) { else if (miles < 10.0)
snprintf(distStr, sizeof(distStr), "%.1fmi", miles); snprintf(distStr, sizeof(distStr), "%.1fmi", miles);
} else { else
snprintf(distStr, sizeof(distStr), "%dmi", (int)miles); snprintf(distStr, sizeof(distStr), "%dmi", (int)miles);
}
} else { } else {
if (distanceKm < 1.0) { if (distanceKm < 1.0)
snprintf(distStr, sizeof(distStr), "%dm", (int)(distanceKm * 1000)); snprintf(distStr, sizeof(distStr), "%dm", (int)(distanceKm * 1000));
} else if (distanceKm < 10.0) { else if (distanceKm < 10.0)
snprintf(distStr, sizeof(distStr), "%.1fkm", distanceKm); snprintf(distStr, sizeof(distStr), "%.1fkm", distanceKm);
} else { else
snprintf(distStr, sizeof(distStr), "%dkm", (int)distanceKm); snprintf(distStr, sizeof(distStr), "%dkm", (int)distanceKm);
}
} }
} }
// Render node name and distance
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(FONT_SMALL); display->setFont(FONT_SMALL);
display->drawStringMaxWidth(x, y, nameMaxWidth, nodeName); display->drawStringMaxWidth(x, y, nameMaxWidth, nodeName);
@ -2231,77 +2239,74 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
} }
} }
#ifdef USE_EINK // =============================
// Dynamic Unified Entry Renderer
// Public screen function: shows how recently nodes were heard // =============================
static void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) 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 const char* getCurrentModeTitle()
static void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
{ {
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) // OLED/TFT Version (cycles every few seconds)
{ // =============================
drawNodeListScreen(display, state, x, y, "Distances", drawNodeDistance); #ifndef USE_EINK
} static void drawDynamicNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
#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(); 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) { if (state->ticksSinceLastStateSwitch == 0) {
currentRendererIndex = 0; currentMode = MODE_LAST_HEARD;
lastSwitchTime = now; lastModeSwitchTime = now;
} }
// ⏱️ Cycle content every interval if (now - lastModeSwitchTime >= getModeCycleIntervalMs()) {
if (now - lastSwitchTime >= RENDER_INTERVAL_MS) { lastModeSwitchTime = now;
lastSwitchTime = now; currentMode = static_cast<NodeListMode>((currentMode + 1) % MODE_COUNT);
currentRendererIndex = (currentRendererIndex + 1) % NUM_RENDERERS;
} }
// Get the correct renderer and title for the current screen const char* title = getCurrentModeTitle();
EntryRenderer currentRenderer = entryRenderers[currentRendererIndex]; drawNodeListScreen(display, state, x, y, title, drawEntryDynamic);
// Show the screen
drawNodeListScreen(display, state, x, y, titles[currentRendererIndex], currentRenderer);
} }
#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) // 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)
@ -3501,7 +3506,7 @@ void Screen::setFrames(FrameFocus focus)
normalFrames[numframes++] = drawDeviceFocused; normalFrames[numframes++] = drawDeviceFocused;
normalFrames[numframes++] = drawCyclingNodeScreen; normalFrames[numframes++] = drawDynamicNodeListScreen;
// Show detailed node views only on E-Ink builds // Show detailed node views only on E-Ink builds
#ifdef USE_EINK #ifdef USE_EINK
@ -3509,6 +3514,7 @@ void Screen::setFrames(FrameFocus focus)
normalFrames[numframes++] = drawHopSignalScreen; normalFrames[numframes++] = drawHopSignalScreen;
normalFrames[numframes++] = drawDistanceScreen; normalFrames[numframes++] = drawDistanceScreen;
#endif #endif
normalFrames[numframes++] = drawNodeListWithCompasses; normalFrames[numframes++] = drawNodeListWithCompasses;
normalFrames[numframes++] = drawCompassAndLocationScreen; normalFrames[numframes++] = drawCompassAndLocationScreen;
normalFrames[numframes++] = drawLoRaFocused; normalFrames[numframes++] = drawLoRaFocused;