2025-05-30 19:11:13 -05:00
|
|
|
#include "UIRenderer.h"
|
|
|
|
|
#include "GPSStatus.h"
|
|
|
|
|
#include "configuration.h"
|
|
|
|
|
#include "gps/GeoCoord.h"
|
|
|
|
|
#include "graphics/ScreenFonts.h"
|
|
|
|
|
#include "graphics/SharedUIDisplay.h"
|
|
|
|
|
#include "graphics/images.h"
|
|
|
|
|
#include <OLEDDisplay.h>
|
|
|
|
|
|
|
|
|
|
#if !MESHTASTIC_EXCLUDE_GPS
|
|
|
|
|
|
|
|
|
|
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 drawGPSpowerstat(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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-30 20:50:24 -05:00
|
|
|
// 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);
|
|
|
|
|
}
|
2025-05-30 19:11:13 -05:00
|
|
|
|
2025-05-30 20:50:24 -05:00
|
|
|
return display->getStringWidth(buf);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace UIRenderer
|
2025-05-30 19:11:13 -05:00
|
|
|
} // namespace graphics
|
|
|
|
|
|
|
|
|
|
#endif // !MESHTASTIC_EXCLUDE_GPS
|