From eb15055d8323bf988c55827ca971077577f9001f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 01:19:07 +0000 Subject: [PATCH] feat(BaseUI): show 'GPS Time Only' when GNSS has time but no position fix The position frame's drawGpsCoordinates() only distinguished 'No GPS present' / 'No GPS Lock' / coordinates, so a GNSS that had decoded valid time but no fix displayed identically to a cold chip. - GPSStatus: add per-acquisition hasTime flag (5th ctor param, accessor, matches() term, updateStatus() copy) - GPS::runOnce(): publish immediately on the gotTime rising edge so the flag reaches observers on the time-only path, which previously never published; done directly rather than via the end-of-loop block so fixHoldEnds is preserved and hold/power behavior is unchanged. Safe without a location: PositionModule ignores invalid positions. gotTime is already cleared on each GPS_ACTIVE entry, so the state is not sticky across acquisitions. - UIRenderer::drawGpsCoordinates(): the 'No GPS Lock' line becomes 'GPS Time Only' when time is valid. The drawGps() header renderer is intentionally untouched (its branches need separate de-clobbering work). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01CcrasD4QsatunDreANDgCx --- src/GPSStatus.h | 9 +++++++-- src/gps/GPS.cpp | 9 +++++++-- src/graphics/draw/UIRenderer.cpp | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/GPSStatus.h b/src/GPSStatus.h index 25c7b10394c..2a384c8b5cb 100644 --- a/src/GPSStatus.h +++ b/src/GPSStatus.h @@ -17,6 +17,7 @@ class GPSStatus : public Status bool hasLock = false; // default to false, until we complete our first read bool isConnected = false; // Do we have a GPS we are talking to + bool hasTime = false; // GPS has decoded a valid time this acquisition, even without a position fix bool isPowerSaving = false; // Are we in power saving state @@ -29,11 +30,12 @@ class GPSStatus : public Status GPSStatus() { statusType = STATUS_TYPE_GPS; } // preferred method - GPSStatus(bool hasLock, bool isConnected, bool isPowerSaving, const meshtastic_Position &pos) : Status() + GPSStatus(bool hasLock, bool isConnected, bool isPowerSaving, const meshtastic_Position &pos, bool hasTime = false) : Status() { this->hasLock = hasLock; this->isConnected = isConnected; this->isPowerSaving = isPowerSaving; + this->hasTime = hasTime; // all-in-one struct copy this->p = pos; @@ -50,6 +52,8 @@ class GPSStatus : public Status bool getIsPowerSaving() const { return isPowerSaving; } + bool getHasTime() const { return hasTime; } + int32_t getLatitude() const { if (config.position.fixed_position) { @@ -91,7 +95,7 @@ class GPSStatus : public Status #ifdef GPS_DEBUG LOG_DEBUG("GPSStatus.match() new pos@%x to old pos@%x", newStatus->p.timestamp, p.timestamp); #endif - return (newStatus->hasLock != hasLock || newStatus->isConnected != isConnected || + return (newStatus->hasLock != hasLock || newStatus->isConnected != isConnected || newStatus->hasTime != hasTime || newStatus->isPowerSaving != isPowerSaving || newStatus->p.latitude_i != p.latitude_i || newStatus->p.longitude_i != p.longitude_i || newStatus->p.altitude != p.altitude || newStatus->p.altitude_hae != p.altitude_hae || newStatus->p.PDOP != p.PDOP || @@ -112,6 +116,7 @@ class GPSStatus : public Status initialized = true; hasLock = newStatus->hasLock; isConnected = newStatus->isConnected; + hasTime = newStatus->hasTime; p = newStatus->p; diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index dcd765c9e85..928e8261450 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -1446,7 +1446,7 @@ void GPS::publishUpdate() LOG_DEBUG("Publish pos@%x:2, hasVal=%d, Sats=%d, GPSlock=%d", p.timestamp, hasValidLocation, p.sats_in_view, hasLock()); // Notify any status instances that are observing us - const meshtastic::GPSStatus status = meshtastic::GPSStatus(hasValidLocation, isConnected(), isPowerSaving(), p); + const meshtastic::GPSStatus status = meshtastic::GPSStatus(hasValidLocation, isConnected(), isPowerSaving(), p, gotTime); newStatus.notifyObservers(&status); if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) { positionModule->handleNewPosition(); @@ -1502,7 +1502,7 @@ int32_t GPS::runOnce() // gps_update_interval is faster than the position broadcast interval so there's a // fresh position ready when the device wants to broadcast one on the mesh. // - // 1. Got a time for the first time --> set the time, don't publish. + // 1. Got a time for the first time --> set the time, publish so the UI can show the time-only state. // 2. Got a lock for the first time // --> If gps_update_interval is <= 10s --> publishUpdate // --> Otherwise, hold for MIN(gps_update_interval - GPS_UPDATE_ALWAYS_ON_THRESHOLD_MS, 20s) @@ -1536,6 +1536,11 @@ int32_t GPS::runOnce() // 1. Got a time for the first time this cycle if (!gotTime && lookForTime()) { // Note: we count on this && short-circuiting and not resetting the RTC time gotTime = true; + // Publish immediately (rather than via the block below, which would clear fixHoldEnds) so the + // time-only state reaches the UI without waiting for a location. Safe without a valid location: + // PositionModule::handleNewPosition ignores invalid positions. + shouldPublish = true; + publishUpdate(); } // 2. Got a lock for the first time, or 3. Got a lock after turning back on diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index b8ead27066e..b82b850f9de 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -582,7 +582,7 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, } } else if (!gps->getHasLock() && !config.position.fixed_position) { if (strcmp(mode, "line1") == 0) { - strcpy(displayLine, "No GPS Lock"); + strcpy(displayLine, gps->getHasTime() ? "GPS Time Only" : "No GPS Lock"); display->drawString(x, y, displayLine); } } else {