From 3fbe7fd8b21df6528bc8df19b7cad7458b5e32f7 Mon Sep 17 00:00:00 2001 From: Jason P Date: Fri, 5 Sep 2025 20:44:32 -0500 Subject: [PATCH 1/3] BaseUI Updates (#7787) * Account for low resolution wide screen OLEDs * Allow picking of Device Role and new Display Formatter for Device Role * Add remainder of client roles to display formatter * Don't update the role unless you pick a value * Mascots are fun * Fix warnings during compile time * Improve some menus * Mascots need to work everywhere * Update Chirpy image * Fix Trunk * Update protobufs * Add date to Clock screen * Analog clocks love dates too * Finalize date moves for analog clock --- .vscode/settings.json | 5 ++ protobufs | 2 +- src/graphics/Screen.cpp | 10 ++++ src/graphics/Screen.h | 2 + src/graphics/SharedUIDisplay.cpp | 4 ++ src/graphics/draw/ClockRenderer.cpp | 25 ++++++++++ src/graphics/draw/DebugRenderer.cpp | 47 ++++++++++++++++-- src/graphics/draw/DebugRenderer.h | 3 ++ src/graphics/draw/MenuHandler.cpp | 77 +++++++++++++++++++++++++---- src/graphics/draw/MenuHandler.h | 3 ++ src/graphics/draw/UIRenderer.cpp | 44 ++++++++++++++++- src/graphics/images.h | 72 +++++++++++++++++++++++++++ 12 files changed, 277 insertions(+), 17 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 81deca8f9..a54386544 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,10 @@ }, "[powershell]": { "editor.defaultFormatter": "ms-vscode.powershell" + }, + "files.associations": { + "deque": "cpp", + "string": "cpp", + "vector": "cpp" } } diff --git a/protobufs b/protobufs index 945b796a9..27d9a99bd 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 945b796a982f38171a9e0d28b5c8b1f7d53c5cd1 +Subproject commit 27d9a99bd03efe35f91cafd7116c2386be5e26a1 diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 807cb5867..0c88ed0a6 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -982,6 +982,11 @@ void Screen::setFrames(FrameFocus focus) indicatorIcons.push_back(digital_icon_clock); } #endif + if (!hiddenFrames.chirpy) { + fsi.positions.chirpy = numframes; + normalFrames[numframes++] = graphics::DebugRenderer::drawChirpy; + indicatorIcons.push_back(small_chirpy); + } #if HAS_WIFI && !defined(ARCH_PORTDUINO) if (!hiddenFrames.wifi && isWifiAvailable()) { @@ -1150,6 +1155,9 @@ void Screen::toggleFrameVisibility(const std::string &frameName) if (frameName == "show_favorites") { hiddenFrames.show_favorites = !hiddenFrames.show_favorites; } + if (frameName == "chirpy") { + hiddenFrames.chirpy = !hiddenFrames.chirpy; + } } bool Screen::isFrameHidden(const std::string &frameName) const @@ -1178,6 +1186,8 @@ bool Screen::isFrameHidden(const std::string &frameName) const return hiddenFrames.clock; if (frameName == "show_favorites") return hiddenFrames.show_favorites; + if (frameName == "chirpy") + return hiddenFrames.chirpy; return false; } diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 85dcfaf69..262ba4175 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -669,6 +669,7 @@ class Screen : public concurrency::OSThread uint8_t nodelist_distance = 255; uint8_t nodelist_bearings = 255; uint8_t clock = 255; + uint8_t chirpy = 255; uint8_t firstFavorite = 255; uint8_t lastFavorite = 255; uint8_t lora = 255; @@ -698,6 +699,7 @@ class Screen : public concurrency::OSThread #endif bool lora = false; bool show_favorites = false; + bool chirpy = true; } hiddenFrames; /// Try to start drawing ASAP diff --git a/src/graphics/SharedUIDisplay.cpp b/src/graphics/SharedUIDisplay.cpp index 13691665a..0f32b0896 100644 --- a/src/graphics/SharedUIDisplay.cpp +++ b/src/graphics/SharedUIDisplay.cpp @@ -16,6 +16,10 @@ void determineResolution(int16_t screenheight, int16_t screenwidth) isHighResolution = true; } + if (screenwidth > 128 && screenheight <= 64) { + isHighResolution = false; + } + // Special case for Heltec Wireless Tracker v1.1 if (screenwidth == 160 && screenheight == 80) { isHighResolution = false; diff --git a/src/graphics/draw/ClockRenderer.cpp b/src/graphics/draw/ClockRenderer.cpp index 08466662c..d046bda6f 100644 --- a/src/graphics/draw/ClockRenderer.cpp +++ b/src/graphics/draw/ClockRenderer.cpp @@ -191,6 +191,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 const char *titleStr = ""; // === Header === graphics::drawCommonHeader(display, x, y, titleStr, true); + int line = 0; #ifdef T_WATCH_S3 if (nimbleBluetooth && nimbleBluetooth->isConnected()) { @@ -294,11 +295,21 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1 display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2, isPM ? "pm" : "am"); } + #ifndef USE_EINK xOffset = (isHighResolution) ? 18 : 10; display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset, secondString); #endif + + // Display GPS derived date + char datetimeStr[25]; + UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false); + char fullLine[40]; + snprintf(fullLine, sizeof(fullLine), "%s", datetimeStr); + yOffset = (isHighResolution) ? 12 : 1; + display->drawString(startingHourMinuteTextX + timeStringWidth - display->getStringWidth(fullLine), + getTextPositions(display)[line] + yOffset, fullLine); } void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y) @@ -314,6 +325,7 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 const char *titleStr = ""; // === Header === graphics::drawCommonHeader(display, x, y, titleStr, true); + int line = 0; #ifdef T_WATCH_S3 if (nimbleBluetooth && nimbleBluetooth->isConnected()) { @@ -511,6 +523,19 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 // draw second hand display->drawLine(centerX, centerY, secondX, secondY); #endif + + display->setFont(FONT_SMALL); + // Display GPS derived date + char datetimeStr[25]; + UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false); + char fullLine[40]; + if (isHighResolution) { + snprintf(fullLine, sizeof(fullLine), "%s", datetimeStr); + } else { + snprintf(fullLine, sizeof(fullLine), "%s", &datetimeStr[2]); + } + display->drawString(display->getWidth() - 1 - display->getStringWidth(fullLine), getTextPositions(display)[line], + fullLine); } } diff --git a/src/graphics/draw/DebugRenderer.cpp b/src/graphics/draw/DebugRenderer.cpp index 61e919208..fb35134fd 100644 --- a/src/graphics/draw/DebugRenderer.cpp +++ b/src/graphics/draw/DebugRenderer.cpp @@ -395,8 +395,18 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int textWidth = display->getStringWidth(shortnameble); int nameX = (SCREEN_WIDTH - textWidth); display->drawString(nameX, getTextPositions(display)[line++], shortnameble); - // === Second Row: Radio Preset === + + // === Second Row: Role === + auto role = DisplayFormatters::getDeviceRole(config.device.role); + char device_role[25]; + snprintf(device_role, sizeof(device_role), "Role: %s", role); + textWidth = display->getStringWidth(device_role); + nameX = (SCREEN_WIDTH - textWidth) / 2; + display->drawString(nameX, getTextPositions(display)[line++], device_role); + + // === Third Row: Radio Preset === auto mode = DisplayFormatters::getModemPresetDisplayName(config.lora.modem_preset, false, config.lora.use_preset); + char regionradiopreset[25]; const char *region = myRegion ? myRegion->name : NULL; if (region != nullptr) { @@ -410,7 +420,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, nameX = (SCREEN_WIDTH - textWidth) / 2; display->drawString(nameX, getTextPositions(display)[line++], regionradiopreset); - // === Third Row: Frequency / ChanNum === + // === Fourth Row: Frequency / ChanNum === char frequencyslot[35]; char freqStr[16]; float freq = RadioLibInterface::instance->getFreq(); @@ -437,7 +447,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->drawString(nameX, getTextPositions(display)[line++], frequencyslot); #if !defined(M5STACK_UNITC6L) - // === Fourth Row: Channel Utilization === + // === Fifth Row: Channel Utilization === const char *chUtil = "ChUtil:"; char chUtilPercentage[10]; snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent()); @@ -454,7 +464,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int total_line_content_width = (chUtil_x + chutil_bar_width + display->getStringWidth(chUtilPercentage) + extraoffset) / 2; int starting_position = centerofscreen - total_line_content_width; - display->drawString(starting_position, getTextPositions(display)[line++], chUtil); + display->drawString(starting_position, getTextPositions(display)[line], chUtil); // Force 56% or higher to show a full 100% bar, text would still show related percent. if (chutil_percent >= 61) { @@ -491,7 +501,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->fillRect(starting_position + chUtil_x, chUtil_y, fillRight, chutil_bar_height); } - display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[4], + display->drawString(starting_position + chUtil_x + chutil_bar_width + extraoffset, getTextPositions(display)[line++], chUtilPercentage); #endif } @@ -655,6 +665,33 @@ void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x } #endif } + +// **************************** +// * Chirpy Screen * +// **************************** +void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + display->clear(); + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + int line = 1; + int iconX = SCREEN_WIDTH - chirpy_width - (chirpy_width / 3); + int iconY = (SCREEN_HEIGHT - chirpy_height) / 2; + int textX_offset = 10; + if (isHighResolution) { + iconX = SCREEN_WIDTH - chirpy_width_hirez - (chirpy_width_hirez / 3); + iconY = (SCREEN_HEIGHT - chirpy_height_hirez) / 2; + textX_offset = textX_offset * 4; + display->drawXbm(iconX, iconY, chirpy_width_hirez, chirpy_height_hirez, chirpy_hirez); + } else { + display->drawXbm(iconX, iconY, chirpy_width, chirpy_height, chirpy); + } + + int textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("Hello") / 2); + display->drawString(textX, getTextPositions(display)[line++], "Hello"); + textX = (display->getWidth() / 2) - textX_offset - (display->getStringWidth("World!") / 2); + display->drawString(textX, getTextPositions(display)[line++], "World!"); +} } // namespace DebugRenderer } // namespace graphics #endif \ No newline at end of file diff --git a/src/graphics/draw/DebugRenderer.h b/src/graphics/draw/DebugRenderer.h index 3382e931d..563a6c1ce 100644 --- a/src/graphics/draw/DebugRenderer.h +++ b/src/graphics/draw/DebugRenderer.h @@ -33,6 +33,9 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, // System screen display void drawSystemScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + +// Chirpy screen display +void drawChirpy(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); } // namespace DebugRenderer } // namespace graphics diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 48e7e808b..975fc7c0a 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -28,17 +28,19 @@ uint8_t test_count = 0; void menuHandler::loraMenu() { - static const char *optionsArray[] = {"Back", "Region Picker"}; - enum optionsNumbers { Back = 0, lora_picker = 1 }; + static const char *optionsArray[] = {"Back", "Region Picker", "Device Role"}; + enum optionsNumbers { Back = 0, lora_picker = 1, device_role_picker = 2 }; BannerOverlayOptions bannerOptions; bannerOptions.message = "LoRa Actions"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; + bannerOptions.optionsCount = 3; bannerOptions.bannerCallback = [](int selected) -> void { if (selected == Back) { // No action } else if (selected == lora_picker) { menuHandler::menuQueue = menuHandler::lora_picker; + } else if (selected == device_role_picker) { + menuHandler::menuQueue = menuHandler::device_role_picker; } }; screen->showOverlayBanner(bannerOptions); @@ -141,6 +143,40 @@ void menuHandler::LoraRegionPicker(uint32_t duration) screen->showOverlayBanner(bannerOptions); } +void menuHandler::DeviceRolePicker() +{ + static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"}; + enum optionsNumbers { + Back = 0, + devicerole_client = 1, + devicerole_clientmute = 2, + devicerole_lostandfound = 3, + devicerole_tracker = 4 + }; + BannerOverlayOptions bannerOptions; + bannerOptions.message = "Device Role"; + bannerOptions.optionsArrayPtr = optionsArray; + bannerOptions.optionsCount = 5; + bannerOptions.bannerCallback = [](int selected) -> void { + if (selected == Back) { + menuHandler::menuQueue = menuHandler::lora_Menu; + screen->runNow(); + return; + } else if (selected == devicerole_client) { + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + } else if (selected == devicerole_clientmute) { + config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE; + } else if (selected == devicerole_lostandfound) { + config.device.role = meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND; + } else if (selected == devicerole_tracker) { + config.device.role = meshtastic_Config_DeviceConfig_Role_TRACKER; + } + service->reloadConfig(SEGMENT_CONFIG); + rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000); + }; + screen->showOverlayBanner(bannerOptions); +} + void menuHandler::TwelveHourPicker() { static const char *optionsArray[] = {"Back", "12-hour", "24-hour"}; @@ -1038,16 +1074,33 @@ void menuHandler::traceRouteMenu() void menuHandler::testMenu() { - static const char *optionsArray[] = {"Back", "Number Picker"}; + enum optionsNumbers { Back, NumberPicker, ShowChirpy }; + static const char *optionsArray[4] = {"Back"}; + static int optionsEnumArray[4] = {Back}; + int options = 1; + + optionsArray[options] = "Number Picker"; + optionsEnumArray[options++] = NumberPicker; + + optionsArray[options] = screen->isFrameHidden("chirpy") ? "Show Chirpy" : "Hide Chirpy"; + optionsEnumArray[options++] = ShowChirpy; + BannerOverlayOptions bannerOptions; - std::string message = "Test to Run?\n"; - bannerOptions.message = message.c_str(); + bannerOptions.message = "Hidden Test Menu"; bannerOptions.optionsArrayPtr = optionsArray; - bannerOptions.optionsCount = 2; + bannerOptions.optionsCount = options; + bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.bannerCallback = [](int selected) -> void { - if (selected == 1) { + if (selected == NumberPicker) { menuQueue = number_test; screen->runNow(); + } else if (selected == ShowChirpy) { + screen->toggleFrameVisibility("chirpy"); + screen->setFrames(Screen::FOCUS_SYSTEM); + + } else { + menuQueue = system_base_menu; + screen->runNow(); } }; screen->showOverlayBanner(bannerOptions); @@ -1306,7 +1359,7 @@ void menuHandler::FrameToggles_menu() bannerOptions.optionsEnumPtr = optionsEnumArray; bannerOptions.InitialSelected = lastSelectedIndex; // Use index, not enum value - bannerOptions.bannerCallback = [optionsEnumArray, options](int selected) mutable -> void { + bannerOptions.bannerCallback = [options](int selected) mutable -> void { // Find the index of selected in optionsEnumArray int idx = 0; for (; idx < options; ++idx) { @@ -1365,9 +1418,15 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display) switch (menuQueue) { case menu_none: break; + case lora_Menu: + loraMenu(); + break; case lora_picker: LoraRegionPicker(); break; + case device_role_picker: + DeviceRolePicker(); + break; case no_timeout_lora_picker: LoraRegionPicker(0); break; diff --git a/src/graphics/draw/MenuHandler.h b/src/graphics/draw/MenuHandler.h index 4e7e02173..1bfdf128f 100644 --- a/src/graphics/draw/MenuHandler.h +++ b/src/graphics/draw/MenuHandler.h @@ -9,7 +9,9 @@ class menuHandler public: enum screenMenus { menu_none, + lora_Menu, lora_picker, + device_role_picker, no_timeout_lora_picker, TZ_picker, twelve_hour_picker, @@ -46,6 +48,7 @@ class menuHandler static void OnboardMessage(); static void LoraRegionPicker(uint32_t duration = 30000); static void loraMenu(); + static void DeviceRolePicker(); static void handleMenuSwitch(OLEDDisplay *display); static void showConfirmationBanner(const char *message, std::function onConfirm); static void clockMenu(); diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index cd88d2f3d..8df3ee517 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -1019,6 +1019,45 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU display->drawString(x, getTextPositions(display)[line++] + 2, latStr); #else snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7); + // === Second Row: Last GPS Fix === + if (gpsStatus->getLastFixMillis() > 0) { + uint32_t delta = (millis() - gpsStatus->getLastFixMillis()) / 1000; // seconds since last fix + uint32_t days = delta / 86400; + uint32_t hours = (delta % 86400) / 3600; + uint32_t mins = (delta % 3600) / 60; + uint32_t secs = delta % 60; + + char buf[32]; +#if defined(USE_EINK) + // E-Ink: skip seconds, show only days/hours/mins + if (days > 0) { + snprintf(buf, sizeof(buf), " Last: %ud %uh", days, hours); + } else if (hours > 0) { + snprintf(buf, sizeof(buf), " Last: %uh %um", hours, mins); + } else { + snprintf(buf, sizeof(buf), " Last: %um", mins); + } +#else + // Non E-Ink: include seconds where useful + if (days > 0) { + snprintf(buf, sizeof(buf), "Last: %ud %uh", days, hours); + } else if (hours > 0) { + snprintf(buf, sizeof(buf), "Last: %uh %um", hours, mins); + } else if (mins > 0) { + snprintf(buf, sizeof(buf), "Last: %um %us", mins, secs); + } else { + snprintf(buf, sizeof(buf), "Last: %us", secs); + } +#endif + + display->drawString(0, getTextPositions(display)[line++], buf); + } else { + display->drawString(0, getTextPositions(display)[line++], "Last: ?"); + } + + // === Third Row: Latitude === + char latStr[32]; + snprintf(latStr, sizeof(latStr), "Lat: %.5f", geoCoord.getLatitude() * 1e-7); display->drawString(x, getTextPositions(display)[line++], latStr); #endif @@ -1029,6 +1068,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU display->drawString(x, getTextPositions(display)[line++] + 4, lonStr); #else snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7); + snprintf(lonStr, sizeof(lonStr), "Lon: %.5f", geoCoord.getLongitude() * 1e-7); display->drawString(x, getTextPositions(display)[line++], lonStr); // === Fifth Row: Altitude === @@ -1037,9 +1077,9 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU ? ourNode->position.altitude : geoCoord.getAltitude(); if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { - snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); + snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), "Alt: %.0fft", geoCoord.getAltitude() * METERS_TO_FEET); } else { - snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), " Alt: %.0im", geoCoord.getAltitude()); + snprintf(DisplayLineTwo, sizeof(DisplayLineTwo), "Alt: %.0im", geoCoord.getAltitude()); } display->drawString(x, getTextPositions(display)[line++], DisplayLineTwo); #endif diff --git a/src/graphics/images.h b/src/graphics/images.h index fd9a2db0f..72dda7886 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -290,6 +290,78 @@ const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, #ifdef M5STACK_UNITC6L #include "img/icon_small.xbm" #else +#define chirpy_width 38 +#define chirpy_height 50 +static unsigned char chirpy[] = { + 0xfe, 0xff, 0xff, 0xff, 0xdf, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, + 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00, + 0x00, 0x00, 0xe0, 0x81, 0xff, 0xff, 0x7f, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xcf, 0x7f, + 0xfe, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, + 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, + 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, + 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0x81, 0xff, + 0xff, 0x7f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0xc3, 0x00, 0xe0, 0x01, 0x00, 0xc3, + 0x00, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0xc0, 0x30, 0x03, 0xe0, 0x01, 0xc0, 0x30, 0x03, + 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, + 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, + 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf}; + +#define chirpy_width_hirez 76 +#define chirpy_height_hirez 100 +static unsigned char chirpy_hirez[] = { + 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, + 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, + 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, + 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, + 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, + 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, + 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, + 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, + 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, + 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, + 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, + 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, + 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, + 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, + 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, + 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, + 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0x3f, 0xc0, 0xff, 0x0f, 0xf0, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, + 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xf0, 0xff, 0x3f, 0xfc, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0x00, 0xfc, 0x03, 0xf0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, + 0x00, 0xfc, 0x03, 0xc0, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, + 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, + 0x00, 0x00, 0x0f, 0xf0, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, + 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xc0, + 0x03, 0xfc, 0x03, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, + 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xf0, 0x00, 0x0f, + 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, + 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x3c, 0xc0, 0x03, 0x3c, 0x00, + 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, + 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x0f, 0xf0, 0x00, 0xf0, 0x00, 0x00, 0xfc, + 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, + 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, 0xc0, 0x03, 0x3c, 0x00, 0xc0, 0x03, 0x00, 0xfc, 0x03, 0x00, + 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, + 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0xf0, 0x00, 0x0f, 0x00, 0x00, 0x0f, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xf3, 0xfc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf3}; + +#define chirpy_small_image_width 8 +#define chirpy_small_image_height 8 +static unsigned char small_chirpy[] = {0x7f, 0x41, 0x55, 0x55, 0x55, 0x55, 0x41, 0x7f}; + #include "img/icon.xbm" #endif static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning"); \ No newline at end of file From e20a91b94542ba59645523cd1d560a8c95f18b5c Mon Sep 17 00:00:00 2001 From: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com> Date: Thu, 4 Sep 2025 23:33:02 -0400 Subject: [PATCH 2/3] Added Last Coordinate counter to Position screen (#7865) Adding a counter to show the last time a GPS coordinate was detected to ensure the user is aware how long since the coordinate updated or to identify any errors. --- src/GPSStatus.h | 9 ++++++ src/graphics/draw/UIRenderer.cpp | 55 ++++++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/GPSStatus.h b/src/GPSStatus.h index 4b7997935..a1a9f2c56 100644 --- a/src/GPSStatus.h +++ b/src/GPSStatus.h @@ -22,6 +22,9 @@ class GPSStatus : public Status meshtastic_Position p = meshtastic_Position_init_default; + /// Time of last valid GPS fix (millis since boot) + uint32_t lastFixMillis = 0; + public: GPSStatus() { statusType = STATUS_TYPE_GPS; } @@ -83,6 +86,9 @@ class GPSStatus : public Status uint32_t getNumSatellites() const { return p.sats_in_view; } + /// Return millis() when the last GPS fix occurred (0 = never) + uint32_t getLastFixMillis() const { return lastFixMillis; } + bool matches(const GPSStatus *newStatus) const { #ifdef GPS_DEBUG @@ -114,6 +120,9 @@ class GPSStatus : public Status if (isDirty) { if (hasLock) { + // Record time of last valid GPS fix + lastFixMillis = millis(); + // In debug logs, identify position by @timestamp:stage (stage 3 = notify) LOG_DEBUG("New GPS pos@%x:3 lat=%f lon=%f alt=%d pdop=%.2f track=%.2f speed=%.2f sats=%d", p.timestamp, p.latitude_i * 1e-7, p.longitude_i * 1e-7, p.altitude, p.PDOP * 1e-2, p.ground_track * 1e-5, diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index 8df3ee517..e76a39398 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -1000,17 +1000,54 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU // If GPS is off, no need to display these parts if (strcmp(displayLine, "GPS off") != 0 && strcmp(displayLine, "No GPS") != 0) { - - // === Second 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); + /* MUST BE MOVED TO CLOCK SCREEN + // === Second 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); #if !defined(M5STACK_UNITC6L) - display->drawString(0, getTextPositions(display)[line++], fullLine); + display->drawString(0, getTextPositions(display)[line++], fullLine); #endif + */ + + // === Second Row: Last GPS Fix === + if (gpsStatus->getLastFixMillis() > 0) { + uint32_t delta = (millis() - gpsStatus->getLastFixMillis()) / 1000; // seconds since last fix + uint32_t days = delta / 86400; + uint32_t hours = (delta % 86400) / 3600; + uint32_t mins = (delta % 3600) / 60; + uint32_t secs = delta % 60; + + char buf[32]; +#if defined(USE_EINK) + // E-Ink: skip seconds, show only days/hours/mins + if (days > 0) { + snprintf(buf, sizeof(buf), " Last: %ud %uh", days, hours); + } else if (hours > 0) { + snprintf(buf, sizeof(buf), " Last: %uh %um", hours, mins); + } else { + snprintf(buf, sizeof(buf), " Last: %um", mins); + } +#else + // Non E-Ink: include seconds where useful + if (days > 0) { + snprintf(buf, sizeof(buf), " Last: %ud %uh", days, hours); + } else if (hours > 0) { + snprintf(buf, sizeof(buf), " Last: %uh %um", hours, mins); + } else if (mins > 0) { + snprintf(buf, sizeof(buf), " Last: %um %us", mins, secs); + } else { + snprintf(buf, sizeof(buf), " Last: %us", secs); + } +#endif + + display->drawString(0, getTextPositions(display)[line++], buf); + } else { + display->drawString(0, getTextPositions(display)[line++], " Last: ?"); + } // === Third Row: Latitude === char latStr[32]; From 6a92358b6883f6dc86e9c456b45662732fd8b45e Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 19 Sep 2025 07:22:23 -0500 Subject: [PATCH 3/3] Fix --- src/graphics/draw/UIRenderer.cpp | 40 -------------------------------- 1 file changed, 40 deletions(-) diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index e76a39398..5623c9026 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -1056,45 +1056,6 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU display->drawString(x, getTextPositions(display)[line++] + 2, latStr); #else snprintf(latStr, sizeof(latStr), " Lat: %.5f", geoCoord.getLatitude() * 1e-7); - // === Second Row: Last GPS Fix === - if (gpsStatus->getLastFixMillis() > 0) { - uint32_t delta = (millis() - gpsStatus->getLastFixMillis()) / 1000; // seconds since last fix - uint32_t days = delta / 86400; - uint32_t hours = (delta % 86400) / 3600; - uint32_t mins = (delta % 3600) / 60; - uint32_t secs = delta % 60; - - char buf[32]; -#if defined(USE_EINK) - // E-Ink: skip seconds, show only days/hours/mins - if (days > 0) { - snprintf(buf, sizeof(buf), " Last: %ud %uh", days, hours); - } else if (hours > 0) { - snprintf(buf, sizeof(buf), " Last: %uh %um", hours, mins); - } else { - snprintf(buf, sizeof(buf), " Last: %um", mins); - } -#else - // Non E-Ink: include seconds where useful - if (days > 0) { - snprintf(buf, sizeof(buf), "Last: %ud %uh", days, hours); - } else if (hours > 0) { - snprintf(buf, sizeof(buf), "Last: %uh %um", hours, mins); - } else if (mins > 0) { - snprintf(buf, sizeof(buf), "Last: %um %us", mins, secs); - } else { - snprintf(buf, sizeof(buf), "Last: %us", secs); - } -#endif - - display->drawString(0, getTextPositions(display)[line++], buf); - } else { - display->drawString(0, getTextPositions(display)[line++], "Last: ?"); - } - - // === Third Row: Latitude === - char latStr[32]; - snprintf(latStr, sizeof(latStr), "Lat: %.5f", geoCoord.getLatitude() * 1e-7); display->drawString(x, getTextPositions(display)[line++], latStr); #endif @@ -1105,7 +1066,6 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU display->drawString(x, getTextPositions(display)[line++] + 4, lonStr); #else snprintf(lonStr, sizeof(lonStr), " Lon: %.5f", geoCoord.getLongitude() * 1e-7); - snprintf(lonStr, sizeof(lonStr), "Lon: %.5f", geoCoord.getLongitude() * 1e-7); display->drawString(x, getTextPositions(display)[line++], lonStr); // === Fifth Row: Altitude ===