diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index b318bb58e8..b8fd6d567c 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -344,6 +344,20 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re } else { strcpy(reply, "gps provider not found"); } + } else if (strcmp(command, "gps diag") == 0) { + LocationProvider * l = _sensors->getLocationProvider(); + if (l != NULL) { + const char* setting = _sensors->getSettingByKey("gps"); + bool requested = setting != NULL && strcmp(setting, "1") == 0; + int prefix_len = snprintf(reply, 160, "req:%u ", requested ? 1U : 0U); + if (prefix_len < 0 || prefix_len >= 160) { + reply[159] = 0; + } else { + l->formatDiagnostics(reply + prefix_len, 160 - prefix_len); + } + } else { + strcpy(reply, "gps provider not found"); + } } else if (memcmp(command, "gps setloc", 10) == 0) { _prefs->node_lat = _sensors->node_lat; _prefs->node_lon = _sensors->node_lon; diff --git a/src/helpers/sensors/LocationProvider.h b/src/helpers/sensors/LocationProvider.h index 81d08652ed..a70ae8ca6f 100644 --- a/src/helpers/sensors/LocationProvider.h +++ b/src/helpers/sensors/LocationProvider.h @@ -1,6 +1,8 @@ #pragma once #include "Mesh.h" +#include +#include class LocationProvider { @@ -22,4 +24,14 @@ class LocationProvider { virtual void stop() = 0; virtual void loop() = 0; virtual bool isEnabled() = 0; + + // Format compact diagnostics in the caller-provided buffer. Providers that + // have more information (for example UART counters) can override this. + virtual void formatDiagnostics(char* out, size_t out_size) { + if (out_size == 0) return; + snprintf(out, out_size, "en:%u sat:%ld fix:%u", + isEnabled() ? 1U : 0U, + satellitesCount(), + isValid() ? 1U : 0U); + } }; diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index b66cb11458..0a6ebc1daa 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -4,6 +4,7 @@ #include #include #include +#include #ifndef GPS_EN #ifdef PIN_GPS_EN @@ -51,6 +52,32 @@ class MicroNMEALocationProvider : public LocationProvider { unsigned long _last_time_sync = 0; static const unsigned long TIME_SYNC_INTERVAL = 1800000; // Re-sync every 30 minutes + // Observation-only diagnostics. These fields do not change GPS power, + // timing, reset behaviour, or parser input. + uint32_t _uart_bytes = 0; + uint32_t _nmea_checksum_ok = 0; + uint32_t _nmea_checksum_bad = 0; + uint32_t _begin_calls = 0; + uint32_t _stop_calls = 0; + unsigned long _last_uart_ms = 0; + unsigned long _last_nmea_ms = 0; + unsigned long _last_fix_ms = 0; + bool _uart_seen = false; + bool _nmea_seen = false; + bool _fix_seen = false; + + static uint32_t ageMs(unsigned long timestamp) { + return (uint32_t)(millis() - timestamp); + } + + static void formatAge(char* out, size_t out_size, bool seen, unsigned long timestamp) { + if (!seen) { + snprintf(out, out_size, "never"); + } else { + snprintf(out, out_size, "%lu", (unsigned long)ageMs(timestamp)); + } + } + public : MicroNMEALocationProvider(Stream& ser, mesh::RTCClock* clock = NULL, int pin_reset = GPS_RESET, int pin_en = GPS_EN,RefCountedDigitalPin* peripher_power=NULL) : nmea(_nmeaBuffer, sizeof(_nmeaBuffer)), _clock(clock), _gps_serial(&ser), _peripher_power(peripher_power), _pin_reset(pin_reset), _pin_en(pin_en) { @@ -76,6 +103,7 @@ public : } void begin() override { + _begin_calls++; claim(); if (_pin_en != -1) { digitalWrite(_pin_en, GPS_EN_ACTIVE); @@ -94,6 +122,7 @@ public : } void stop() override { + _stop_calls++; if (_pin_en != -1) { digitalWrite(_pin_en, !GPS_EN_ACTIVE); } @@ -137,10 +166,36 @@ public : while (_gps_serial->available()) { char c = _gps_serial->read(); + _uart_bytes++; + _last_uart_ms = millis(); + _uart_seen = true; #ifdef GPS_NMEA_DEBUG Serial.print(c); #endif - nmea.process(c); + bool parsed = nmea.process(c); + + // MicroNMEA leaves the completed sentence in its buffer after the + // first line terminator. CRLF therefore counts once: CR observes the + // sentence, then LF clears the now-empty buffer. + if ((c == '\0' || c == '\r' || c == '\n') && nmea.getSentence()[0] != '\0') { + if (MicroNMEA::testChecksum(nmea.getSentence())) { + _nmea_checksum_ok++; + _last_nmea_ms = millis(); + _nmea_seen = true; + + // Refresh fix age only when a newly parsed GGA/RMC sentence + // carries a valid fix. Other valid NMEA sentences must not + // make an old position look fresh. + const char* message_id = nmea.getMessageID(); + if (parsed && isValid() && + (strcmp(message_id, "GGA") == 0 || strcmp(message_id, "RMC") == 0)) { + _last_fix_ms = millis(); + _fix_seen = true; + } + } else { + _nmea_checksum_bad++; + } + } } if (!isValid()) time_valid = 0; @@ -163,4 +218,31 @@ public : } } } + + void formatDiagnostics(char* out, size_t out_size) override { + if (out_size == 0) return; + + char uart_age[11]; + char nmea_age[11]; + char fix_age[11]; + formatAge(uart_age, sizeof(uart_age), _uart_seen, _last_uart_ms); + formatAge(nmea_age, sizeof(nmea_age), _nmea_seen, _last_nmea_ms); + formatAge(fix_age, sizeof(fix_age), _fix_seen, _last_fix_ms); + + // Compact enough for CommonCLI's 160-byte reply buffer, including the + // "req:" prefix added by CommonCLI. + snprintf(out, out_size, + "en:%u ub:%lu ua:%s ok:%lu bad:%lu na:%s sat:%ld fix:%u fa:%s bc:%lu sc:%lu", + isEnabled() ? 1U : 0U, + (unsigned long)_uart_bytes, + uart_age, + (unsigned long)_nmea_checksum_ok, + (unsigned long)_nmea_checksum_bad, + nmea_age, + satellitesCount(), + isValid() ? 1U : 0U, + fix_age, + (unsigned long)_begin_calls, + (unsigned long)_stop_calls); + } };