feat(gps): use HDOP for !gps fix readiness, satellite count as fallback

Satellite count alone is a poor proxy for fix quality -- few satellites
in good geometry can beat many in poor geometry. LocationProvider now
exposes getHDOP() (default -1 = unsupported); MicroNMEA implements it.
isLocFixReady() prefers HDOP <= 2.0 when available, falling back to the
old >=8 satellite threshold for providers that don't report it (e.g.
RAK12500/u-blox).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-07-24 22:49:17 +02:00
parent 2c656f5af7
commit 7b6ae8e2cb
6 changed files with 29 additions and 9 deletions

View File

@@ -338,6 +338,7 @@ private:
void tickLocFix(); // ticked every loop() while _loc_fix.active
void startLocFix(uint8_t dest_type, const uint8_t* pub_key, uint8_t channel_idx);
void sendLocFixResult(const char* msg);
static bool isLocFixReady(LocationProvider* loc); // HDOP if the provider has it, else satellite count
bool botTriggerMatches(const char* trigger, const char* body, bool allow_wildcard) const;
bool botInQuietHours() const; // true when auto-replies should stay silent
bool botDmAllowed(const uint8_t* pubkey); // per-contact DM throttle: ok to reply?
@@ -442,7 +443,11 @@ private:
// !gps fix state -- one global slot (one physical GPS): botCommandReply()
// rejects a second request outright while one is active, so this never
// needs to be an array. See tickLocFix()/startLocFix() in MyMeshBot.h.
static const int LOCFIX_MIN_SATS = 8; // readiness threshold
// Readiness: HDOP (tenths, lower=better) when the provider exposes it --
// 20 == HDOP 2.0, the usual "good fix" cutoff -- else satellite count as a
// cruder fallback (see LocationProvider::getHDOP()'s -1 = "not available").
static const int LOCFIX_MAX_HDOP = 20;
static const int LOCFIX_MIN_SATS = 8; // readiness threshold (HDOP-less fallback)
static const uint32_t LOCFIX_AVERAGE_MS = 10000; // once ready, keep averaging this long
static const uint32_t LOCFIX_TIMEOUT_MS = 90000; // hard stop covering both phases
enum { LOCFIX_DEST_CONTACT = 0, LOCFIX_DEST_CHANNEL = 1 }; // CONTACT covers DM and room alike (both reply via sendMessage)

View File

@@ -522,19 +522,27 @@ void MyMesh::startLocFix(uint8_t dest_type, const uint8_t* pub_key, uint8_t chan
}
// !gps fix state machine, ticked every MyMesh::loop() while _loc_fix.active.
// Phase 1 (acquire): wait for a valid fix with at least LOCFIX_MIN_SATS
// satellites. Phase 2 (average): once that's first met, keep summing
// node_lat/node_lon for LOCFIX_AVERAGE_MS more -- only on ticks where the
// threshold still holds, so a momentary drop below LOCFIX_MIN_SATS just skips
// a sample instead of aborting the whole wait. A hard LOCFIX_TIMEOUT_MS
// Phase 1 (acquire): wait for a valid fix good enough to trust -- HDOP (fix
// geometry) when the provider exposes it, satellite count as a cruder
// fallback when it doesn't (see isLocFixReady()). Phase 2 (average): once
// that's first met, keep summing node_lat/node_lon for LOCFIX_AVERAGE_MS more
// -- only on ticks where the threshold still holds, so a momentary dip just
// skips a sample instead of aborting the whole wait. A hard LOCFIX_TIMEOUT_MS
// deadline covers both phases; on timeout with zero samples collected the
// result is a plain failure (no stale-cache fallback -- a cached node_lat/lon
// from long before this request would be actively misleading here).
bool MyMesh::isLocFixReady(LocationProvider* loc) {
if (!loc || !loc->isValid()) return false;
long hdop = loc->getHDOP();
if (hdop >= 0) return hdop <= LOCFIX_MAX_HDOP; // preferred: real fix-quality signal
return loc->satellitesCount() >= LOCFIX_MIN_SATS; // provider has no HDOP -- cruder fallback
}
void MyMesh::tickLocFix() {
if (!_loc_fix.active) return;
LocationProvider* loc = sensors.getLocationProvider();
bool ready = loc && loc->isValid() && loc->satellitesCount() >= LOCFIX_MIN_SATS;
bool ready = isLocFixReady(loc);
if (_loc_fix.averaging_until_ms == 0 && ready) {
_loc_fix.averaging_until_ms = futureMillis(LOCFIX_AVERAGE_MS);