firmware/src/graphics/draw/UIRenderer.cpp
Ben Meadors 77e7235ce5
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>
2025-05-31 19:31:45 -05:00

1255 lines
52 KiB
C++

#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 <OLEDDisplay.h>
#include <RTC.h>
#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<meshtastic_NodeInfoLite *> 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<size_t>(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