mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
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:
@@ -451,7 +451,7 @@ A separate **Actions** toggle, nested under Commands (Commands must be ON for Ac
|
|||||||
| ----------------- | -------------------------------------------------------------------- |
|
| ----------------- | -------------------------------------------------------------------- |
|
||||||
| `!buzz [seconds]` | Sounds the buzzer as a find-me signal — default 5s, capped at 30s. Sounds even if the buzzer is muted in Settings (that's the point of a find-me signal). |
|
| `!buzz [seconds]` | Sounds the buzzer as a find-me signal — default 5s, capped at 30s. Sounds even if the buzzer is muted in Settings (that's the point of a find-me signal). |
|
||||||
| `!gps on` / `!gps off` | Enables/disables GPS, same effect as the Home page's GPS toggle. |
|
| `!gps on` / `!gps off` | Enables/disables GPS, same effect as the Home page's GPS toggle. |
|
||||||
| `!gps fix` | Single-shot location: turns GPS on if it wasn't already, waits for a stabilised fix (≥8 satellites, averaged over 10s), sends the position, then restores GPS to whatever state it was in before. Replies in two parts — an immediate `GPS: acquiring fix...` ack, then the position (or `GPS: no fix (timeout)` / a partial fix) as a follow-up message up to 90s later. Only one `!gps fix` can be in flight at a time; a second one gets `GPS: fix already pending`. |
|
| `!gps fix` | Single-shot location: turns GPS on if it wasn't already, waits for a stabilised fix (HDOP ≤ 2.0, or ≥8 satellites on GPS hardware that doesn't report HDOP, averaged over 10s), sends the position, then restores GPS to whatever state it was in before. Replies in two parts — an immediate `GPS: acquiring fix...` ack, then the position (or `GPS: no fix (timeout)` / a partial fix) as a follow-up message up to 90s later. Only one `!gps fix` can be in flight at a time; a second one gets `GPS: fix already pending`. |
|
||||||
| `!advert` | Sends an advert immediately, same as the Home page's manual advert action. |
|
| `!advert` | Sends an advert immediately, same as the Home page's manual advert action. |
|
||||||
|
|
||||||
Actions combine with Commands and each other in one message the same way — `!batt !gps on` answers with `4.10V | GPS: on` in a single reply. With Actions OFF for a target, `!buzz`/`!gps`/`!gps fix`/`!advert` are silently ignored (no reply, no effect) exactly like any other unrecognised command, and `!help`'s reply doesn't mention them.
|
Actions combine with Commands and each other in one message the same way — `!batt !gps on` answers with `4.10V | GPS: on` in a single reply. With Actions OFF for a target, `!buzz`/`!gps`/`!gps fix`/`!advert` are silently ignored (no reply, no effect) exactly like any other unrecognised command, and `!help`'s reply doesn't mention them.
|
||||||
|
|||||||
@@ -338,6 +338,7 @@ private:
|
|||||||
void tickLocFix(); // ticked every loop() while _loc_fix.active
|
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 startLocFix(uint8_t dest_type, const uint8_t* pub_key, uint8_t channel_idx);
|
||||||
void sendLocFixResult(const char* msg);
|
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 botTriggerMatches(const char* trigger, const char* body, bool allow_wildcard) const;
|
||||||
bool botInQuietHours() const; // true when auto-replies should stay silent
|
bool botInQuietHours() const; // true when auto-replies should stay silent
|
||||||
bool botDmAllowed(const uint8_t* pubkey); // per-contact DM throttle: ok to reply?
|
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()
|
// !gps fix state -- one global slot (one physical GPS): botCommandReply()
|
||||||
// rejects a second request outright while one is active, so this never
|
// rejects a second request outright while one is active, so this never
|
||||||
// needs to be an array. See tickLocFix()/startLocFix() in MyMeshBot.h.
|
// 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_AVERAGE_MS = 10000; // once ready, keep averaging this long
|
||||||
static const uint32_t LOCFIX_TIMEOUT_MS = 90000; // hard stop covering both phases
|
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)
|
enum { LOCFIX_DEST_CONTACT = 0, LOCFIX_DEST_CHANNEL = 1 }; // CONTACT covers DM and room alike (both reply via sendMessage)
|
||||||
|
|||||||
@@ -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.
|
// !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
|
// Phase 1 (acquire): wait for a valid fix good enough to trust -- HDOP (fix
|
||||||
// satellites. Phase 2 (average): once that's first met, keep summing
|
// geometry) when the provider exposes it, satellite count as a cruder
|
||||||
// node_lat/node_lon for LOCFIX_AVERAGE_MS more -- only on ticks where the
|
// fallback when it doesn't (see isLocFixReady()). Phase 2 (average): once
|
||||||
// threshold still holds, so a momentary drop below LOCFIX_MIN_SATS just skips
|
// that's first met, keep summing node_lat/node_lon for LOCFIX_AVERAGE_MS more
|
||||||
// a sample instead of aborting the whole wait. A hard LOCFIX_TIMEOUT_MS
|
// -- 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
|
// 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
|
// result is a plain failure (no stale-cache fallback -- a cached node_lat/lon
|
||||||
// from long before this request would be actively misleading here).
|
// 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() {
|
void MyMesh::tickLocFix() {
|
||||||
if (!_loc_fix.active) return;
|
if (!_loc_fix.active) return;
|
||||||
|
|
||||||
LocationProvider* loc = sensors.getLocationProvider();
|
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) {
|
if (_loc_fix.averaging_until_ms == 0 && ready) {
|
||||||
_loc_fix.averaging_until_ms = futureMillis(LOCFIX_AVERAGE_MS);
|
_loc_fix.averaging_until_ms = futureMillis(LOCFIX_AVERAGE_MS);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
- **Auto-Reply Bot is renamed to Remote Bot.** It's grown beyond auto-replies into remote device control (Actions, below), so the Tools screen entry and docs now say "Remote Bot" — same screen, same settings, nothing to reconfigure.
|
- **Auto-Reply Bot is renamed to Remote Bot.** It's grown beyond auto-replies into remote device control (Actions, below), so the Tools screen entry and docs now say "Remote Bot" — same screen, same settings, nothing to reconfigure.
|
||||||
- **Remote Bot gains Actions: `!buzz`, `!gps`, `!advert`.** A new **Actions** toggle (per target — Direct/Channel/Room, nested under each target's existing **Commands** toggle) enables three commands that change the device's own behaviour instead of just reporting on it: `!buzz [seconds]` sounds the buzzer as a find-me signal (default 5s, capped at 30s, sounds even if the buzzer is muted), `!gps on`/`!gps off` toggles GPS, and `!advert` sends an immediate advert. Off by default; combines with the existing query commands in one message the same way (`!batt !gps on` → `4.10V | GPS: on`).
|
- **Remote Bot gains Actions: `!buzz`, `!gps`, `!advert`.** A new **Actions** toggle (per target — Direct/Channel/Room, nested under each target's existing **Commands** toggle) enables three commands that change the device's own behaviour instead of just reporting on it: `!buzz [seconds]` sounds the buzzer as a find-me signal (default 5s, capped at 30s, sounds even if the buzzer is muted), `!gps on`/`!gps off` toggles GPS, and `!advert` sends an immediate advert. Off by default; combines with the existing query commands in one message the same way (`!batt !gps on` → `4.10V | GPS: on`).
|
||||||
- **`!gps fix` — single-shot GPS location, no need to leave GPS running.** Turns GPS on (if it wasn't already), waits for a stabilised fix (≥8 satellites, then averages for 10s), sends the position, and restores GPS to whatever state it was in before — up to a 90s timeout, after which it reports a partial fix (if it got at least some samples) or failure. Replies in two parts: an immediate "acquiring fix..." ack, then the actual position as a follow-up message once ready. Only one fix request can be in flight at a time (a second one gets "fix already pending"); same Actions toggle as `!buzz`/`!gps`/`!advert`.
|
- **`!gps fix` — single-shot GPS location, no need to leave GPS running.** Turns GPS on (if it wasn't already), waits for a stabilised fix (HDOP ≤ 2.0 — falls back to ≥8 satellites on GPS hardware that doesn't report HDOP — then averages for 10s), sends the position, and restores GPS to whatever state it was in before — up to a 90s timeout, after which it reports a partial fix (if it got at least some samples) or failure. Replies in two parts: an immediate "acquiring fix..." ack, then the actual position as a follow-up message once ready. Only one fix request can be in flight at a time (a second one gets "fix already pending"); same Actions toggle as `!buzz`/`!gps`/`!advert`.
|
||||||
- **Bot Trigger fields accept multiple phrases.** Pack several trigger words into one Trigger field, comma-separated (`hi,hello there,yo`) — matching any one of them fires the reply, same as before for a single phrase.
|
- **Bot Trigger fields accept multiple phrases.** Pack several trigger words into one Trigger field, comma-separated (`hi,hello there,yo`) — matching any one of them fires the reply, same as before for a single phrase.
|
||||||
- **Remote Bot Actions gain `!gpio1`..`!gpio4`** (Wio Tracker L1 only — 4 otherwise-unused pins). Each pin is independently set to Off/Input/Output (GPIO1/GPIO2 also offer Analog) from a new **Tools › GPIO** screen; `!gpio1 on`/`!gpio1 off` drives an Output pin remotely, a bare `!gpio1` reports the current mode and reading (including a millivolt value in Analog mode). Gated by the same per-target Actions toggle as `!buzz`/`!gps`/`!advert`.
|
- **Remote Bot Actions gain `!gpio1`..`!gpio4`** (Wio Tracker L1 only — 4 otherwise-unused pins). Each pin is independently set to Off/Input/Output (GPIO1/GPIO2 also offer Analog) from a new **Tools › GPIO** screen; `!gpio1 on`/`!gpio1 off` drives an Output pin remotely, a bare `!gpio1` reports the current mode and reading (including a millivolt value in Analog mode). Gated by the same per-target Actions toggle as `!buzz`/`!gps`/`!advert`.
|
||||||
- **Optional CardKB support (Wio Tracker L1, Grove I2C) — full keyboard-only navigation.** Plug an M5Stack CardKB into the Grove connector and it's auto-detected at boot — no setting to flip. Typing goes straight into the message/name field instead of navigating the on-screen keyboard grid; arrows and Esc work as expected everywhere else too (including inside the placeholder and accent popups). Enter acts like the physical centre button (advances the on-screen grid selection) rather than submitting, so **Fn+Enter** sends the message/confirms the field from anywhere, **Fn+`<letter>`** opens that letter's accent popup directly (no arrow-hunting needed — handy for Polish/Czech/etc. diacritics), and **Fn+Tab** opens the Hold-Enter equivalent (Shift-lock, clear field) for whatever's selected; plain **Tab** still opens the Hold-Enter context menu everywhere else (message reply/navigate, Bot/Admin/Repeater menus, …).
|
- **Optional CardKB support (Wio Tracker L1, Grove I2C) — full keyboard-only navigation.** Plug an M5Stack CardKB into the Grove connector and it's auto-detected at boot — no setting to flip. Typing goes straight into the message/name field instead of navigating the on-screen keyboard grid; arrows and Esc work as expected everywhere else too (including inside the placeholder and accent popups). Enter acts like the physical centre button (advances the on-screen grid selection) rather than submitting, so **Fn+Enter** sends the message/confirms the field from anywhere, **Fn+`<letter>`** opens that letter's accent popup directly (no arrow-hunting needed — handy for Polish/Czech/etc. diacritics), and **Fn+Tab** opens the Hold-Enter equivalent (Shift-lock, clear field) for whatever's selected; plain **Tab** still opens the Hold-Enter context menu everywhere else (message reply/navigate, Bot/Admin/Repeater menus, …).
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ public:
|
|||||||
virtual long getLongitude() = 0;
|
virtual long getLongitude() = 0;
|
||||||
virtual long getAltitude() = 0;
|
virtual long getAltitude() = 0;
|
||||||
virtual long satellitesCount() = 0;
|
virtual long satellitesCount() = 0;
|
||||||
|
// Horizontal Dilution of Precision, in tenths (11 == HDOP 1.1) -- lower is
|
||||||
|
// better, and a much more direct read on fix quality than satellite count
|
||||||
|
// alone (few satellites in good geometry can beat many in poor geometry).
|
||||||
|
// -1 means this provider doesn't expose it; callers fall back to
|
||||||
|
// satellitesCount() in that case.
|
||||||
|
virtual long getHDOP() { return -1; }
|
||||||
virtual bool isValid() = 0;
|
virtual bool isValid() = 0;
|
||||||
virtual long getTimestamp() = 0;
|
virtual long getTimestamp() = 0;
|
||||||
virtual void sendSentence(const char * sentence);
|
virtual void sendSentence(const char * sentence);
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ public :
|
|||||||
return alt;
|
return alt;
|
||||||
}
|
}
|
||||||
long satellitesCount() override { return nmea.getNumSatellites(); }
|
long satellitesCount() override { return nmea.getNumSatellites(); }
|
||||||
|
long getHDOP() override { return nmea.getHDOP(); }
|
||||||
bool isValid() override { return nmea.isValid(); }
|
bool isValid() override { return nmea.isValid(); }
|
||||||
|
|
||||||
long getTimestamp() override {
|
long getTimestamp() override {
|
||||||
|
|||||||
Reference in New Issue
Block a user