diff --git a/docs/solo_features/tools_screen/tools_screen.md b/docs/solo_features/tools_screen/tools_screen.md index 12f7d4b1..a4b56f6a 100644 --- a/docs/solo_features/tools_screen/tools_screen.md +++ b/docs/solo_features/tools_screen/tools_screen.md @@ -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). | | `!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. | 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. diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index c186c46c..1c8186c1 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -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) diff --git a/examples/companion_radio/MyMeshBot.h b/examples/companion_radio/MyMeshBot.h index 4fdf6f3e..8b8db6c6 100644 --- a/examples/companion_radio/MyMeshBot.h +++ b/examples/companion_radio/MyMeshBot.h @@ -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); diff --git a/release-notes.md b/release-notes.md index edf4917e..b9ed4d31 100644 --- a/release-notes.md +++ b/release-notes.md @@ -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. - **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. - **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+``** 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, …). diff --git a/src/helpers/sensors/LocationProvider.h b/src/helpers/sensors/LocationProvider.h index 81d08652..08d531a8 100644 --- a/src/helpers/sensors/LocationProvider.h +++ b/src/helpers/sensors/LocationProvider.h @@ -14,6 +14,12 @@ public: virtual long getLongitude() = 0; virtual long getAltitude() = 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 long getTimestamp() = 0; virtual void sendSentence(const char * sentence); diff --git a/src/helpers/sensors/MicroNMEALocationProvider.h b/src/helpers/sensors/MicroNMEALocationProvider.h index eec466d3..f18a31ed 100644 --- a/src/helpers/sensors/MicroNMEALocationProvider.h +++ b/src/helpers/sensors/MicroNMEALocationProvider.h @@ -120,6 +120,7 @@ public : return alt; } long satellitesCount() override { return nmea.getNumSatellites(); } + long getHDOP() override { return nmea.getHDOP(); } bool isValid() override { return nmea.isValid(); } long getTimestamp() override {