Merge pull request #3200 from F4FPR/gps-diag-command

Add generic GPS UART and NMEA diagnostics
This commit is contained in:
fdlamotte
2026-09-26 13:34:34 -04:00
committed by GitHub
3 changed files with 109 additions and 1 deletions
+14
View File
@@ -355,6 +355,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;
+12
View File
@@ -1,6 +1,8 @@
#pragma once
#include "Mesh.h"
#include <stddef.h>
#include <stdio.h>
class LocationProvider {
@@ -22,4 +24,14 @@ public:
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);
}
};
@@ -4,6 +4,7 @@
#include <MicroNMEA.h>
#include <RTClib.h>
#include <helpers/RefCountedDigitalPin.h>
#include <string.h>
#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);
}
};