Files
ratdeck/src/hal/GPSManager.cpp
T
DeFiDude 7bf1be2724 Fix GPS time not showing without satellite lock (LoRa-only boot)
Two-layer fix:
- NMEAParser: parse time fields from RMC sentences even when status
  is void ('V'). The MIA-M10Q's battery-backed RTC provides time in
  every sentence — we were discarding it along with the void location.
- GPSManager: allow first time sync without satellite lock. Subsequent
  syncs still require sats>0 for accuracy. Year/epoch validation still
  guards against garbage data from uninitialized modules.

Previously, time only appeared after WiFi connection (NTP via
configTzTime). Now the clock shows within seconds of boot using the
GPS module's RTC, then corrects when satellites lock.
2026-03-28 15:16:57 -06:00

221 lines
7.4 KiB
C++

#include "GPSManager.h"
#if HAS_GPS
#include <sys/time.h>
#include <time.h>
constexpr uint32_t GPSManager::BAUD_RATES[];
void GPSManager::begin() {
if (_running) return;
// Restore last-known time from NVS on boot
restoreTimeFromNVS();
// Start baud rate auto-detection: try first rate
_baudDetected = false;
_baudAttemptIdx = 0;
_baudAttemptStart = millis();
_serial.begin(BAUD_RATES[0], SERIAL_8N1, GPS_RX, GPS_TX);
Serial.printf("[GPS] UART started, trying %lu baud (GPIO RX=%d TX=%d)\n",
(unsigned long)BAUD_RATES[0], GPS_RX, GPS_TX);
_running = true;
}
void GPSManager::loop() {
if (!_running) return;
uint32_t prevSentences = _parser.sentencesParsed();
// Read available bytes (up to 64 per call to stay non-blocking)
int count = 0;
while (_serial.available() && count < 64) {
char c = _serial.read();
_parser.feed(c);
count++;
}
// Baud rate auto-detection: if no valid sentence after timeout, try next rate
if (!_baudDetected) {
if (_parser.sentencesParsed() > 0) {
_baudDetected = true;
_detectedBaud = BAUD_RATES[_baudAttemptIdx];
Serial.printf("[GPS] Baud detected: %lu (%lu sentences parsed)\n",
(unsigned long)_detectedBaud, (unsigned long)_parser.sentencesParsed());
} else if (millis() - _baudAttemptStart >= BAUD_DETECT_TIMEOUT_MS) {
_baudAttemptIdx++;
if (_baudAttemptIdx >= BAUD_RATE_COUNT) {
// Exhausted all baud rates — keep last one active, will retry on next begin()
Serial.println("[GPS] No valid NMEA data at any baud rate");
_baudDetected = true; // Stop retrying
_detectedBaud = BAUD_RATES[BAUD_RATE_COUNT - 1];
} else {
_serial.end();
delay(10);
_serial.begin(BAUD_RATES[_baudAttemptIdx], SERIAL_8N1, GPS_RX, GPS_TX);
_baudAttemptStart = millis();
Serial.printf("[GPS] Trying %lu baud...\n",
(unsigned long)BAUD_RATES[_baudAttemptIdx]);
}
}
}
// Check for new time data
NMEAData& d = _parser.data();
if (d.timeUpdated) {
d.timeUpdated = false;
// Prefer satellite-verified time (sats > 0), but allow the module's
// battery-backed RTC for the first sync so the clock appears on boot
// even without satellite lock (e.g., indoors). Year/epoch validation
// in syncSystemTime() guards against garbage data.
bool hasSatFix = (d.satellites > 0);
if (hasSatFix) _lastFixMs = millis();
bool allowSync = hasSatFix || (_timeSyncCount == 0);
if (d.timeValid && allowSync && (millis() - _lastTimeSyncMs >= TIME_SYNC_INTERVAL_MS || _timeSyncCount == 0)) {
syncSystemTime();
// Persist time to NVS so reboots without WiFi/GPS have approximate time
persistToNVS();
_lastPersistMs = millis();
}
}
// Check for new location data
if (d.locationUpdated) {
d.locationUpdated = false;
_locationValid = d.locationValid;
_lastFixMs = millis();
}
// Stale fix detection
if (_timeValid && (millis() - _lastFixMs >= FIX_TIMEOUT_MS)) {
// Don't clear _timeValid — system clock is still set and accurate.
// Only clear location validity since position may have changed.
_locationValid = false;
}
}
void GPSManager::stop() {
if (!_running) return;
// Persist current data before stopping
if (_timeValid || _locationValid) {
persistToNVS();
}
_serial.end();
_running = false;
_baudDetected = false;
_locationValid = false;
Serial.println("[GPS] Stopped");
}
uint32_t GPSManager::fixAgeMs() const {
if (_lastFixMs == 0) return UINT32_MAX;
return millis() - _lastFixMs;
}
void GPSManager::syncSystemTime() {
const NMEAData& d = _parser.data();
if (!d.timeValid) return;
// Sanity check year range (reject spoofed/garbage data)
if (d.year < 2024 || d.year > 2030) {
Serial.printf("[GPS] Rejected time: year %d out of range\n", d.year);
return;
}
// Convert GPS UTC date/time to Unix epoch using arithmetic
// (avoids mktime() TZ contamination and timegm() unavailability on ESP32)
auto daysFromCivil = [](int y, int m, int d) -> int32_t {
// Converts civil date to days since 1970-01-01 (Howard Hinnant algorithm)
y -= (m <= 2);
int era = (y >= 0 ? y : y - 399) / 400;
unsigned yoe = (unsigned)(y - era * 400);
unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
return era * 146097 + (int32_t)doe - 719468;
};
int32_t days = daysFromCivil(d.year, d.month, d.day);
time_t epoch = (time_t)days * 86400 + d.hour * 3600 + d.minute * 60 + d.second;
if (epoch < 1700000000) {
Serial.printf("[GPS] Rejected time: epoch %ld too low\n", (long)epoch);
return;
}
struct timeval tv = {};
tv.tv_sec = epoch;
tv.tv_usec = 0;
settimeofday(&tv, nullptr);
_timeValid = true;
_lastTimeSyncMs = millis();
_timeSyncCount++;
// Set TZ so localtime() returns correct local time (with DST if applicable)
setenv("TZ", _posixTZ, 1);
tzset();
Serial.printf("[GPS] System time synced: %04d-%02d-%02d %02d:%02d:%02d UTC (sync #%lu, sats=%d)\n",
d.year, d.month, d.day, d.hour, d.minute, d.second,
(unsigned long)_timeSyncCount, (int)d.satellites);
}
void GPSManager::restoreTimeFromNVS() {
Preferences prefs;
if (!prefs.begin(NVS_NAMESPACE, true)) return; // read-only
int64_t storedEpoch = prefs.getLong64("epoch", 0);
prefs.end();
if (storedEpoch > 1700000000) {
struct timeval tv = {};
tv.tv_sec = (time_t)storedEpoch;
tv.tv_usec = 0;
settimeofday(&tv, nullptr);
// Set TZ
setenv("TZ", _posixTZ, 1);
tzset();
time_t now = time(nullptr);
struct tm* local = localtime(&now);
if (local) {
Serial.printf("[GPS] Restored time from NVS: %04d-%02d-%02d %02d:%02d (stale, approximate)\n",
local->tm_year + 1900, local->tm_mon + 1, local->tm_mday,
local->tm_hour, local->tm_min);
}
// Note: _timeValid stays false — this is approximate/stale time.
// The system clock is set so timestamps work, but the GPS indicator
// won't show as "fixed" until a real satellite fix arrives.
}
}
void GPSManager::persistToNVS() {
time_t now = time(nullptr);
if (now < 1700000000) return; // Don't persist if we don't have real time
Preferences prefs;
if (!prefs.begin(NVS_NAMESPACE, false)) return; // read-write
prefs.putLong64("epoch", (int64_t)now);
const NMEAData& d = _parser.data();
if (d.locationValid) {
// Store as integers (microdegrees) to avoid float NVS issues
prefs.putLong("lat_ud", (int32_t)(d.latitude * 1000000.0));
prefs.putLong("lon_ud", (int32_t)(d.longitude * 1000000.0));
prefs.putLong("alt_cm", (int32_t)(d.altitude * 100.0));
}
prefs.end();
}
void GPSManager::setPosixTZ(const char* tz) {
strncpy(_posixTZ, tz, sizeof(_posixTZ) - 1);
_posixTZ[sizeof(_posixTZ) - 1] = '\0';
}
#endif // HAS_GPS