feat(BaseUI): show 'GPS Time Only' when GNSS has time but no position fix (#11361)

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).


Claude-Session: https://claude.ai/code/session_01CcrasD4QsatunDreANDgCx

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jonathan Bennett
2026-08-10 02:39:40 +00:00
committed by GitHub
co-authored by Claude
parent 2e958821f9
commit 512154ff97
3 files changed
+15 -5

No files matched your search

+7 -2
View File
@@ -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;
+7 -2
View File
@@ -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
+1 -1
View File
@@ -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 {