From 9e5ce72c8603f2076fb46e9c89d38823ed445b71 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Thu, 24 Sep 2026 23:15:18 +0200 Subject: [PATCH] refactor(ui-core): move locator into LocatorEngine Active target (set/clear/resolve person position), geofence crossing state machine and proximity beeper move from UITask to ui-core/LocatorEngine.h, emitting LocatorCrossed / LocatorBeep events. UiEvent gains a flag and a 24-byte text. tickCore() now runs at the end of UITask::loop(), where the moved engines used to run, so a fresh GPS wake re-seeds the locator before it evaluates. Co-Authored-By: Claude Opus 5.5 --- docs/development/ui-core.md | 4 +- .../companion_radio/ui-core/LocatorEngine.h | 199 ++++++++++++++++++ examples/companion_radio/ui-core/UiCore.h | 4 + examples/companion_radio/ui-core/UiEvents.h | 8 +- .../companion_radio/ui-new/LocatorScreen.h | 4 +- examples/companion_radio/ui-new/UITask.cpp | 165 ++------------- examples/companion_radio/ui-new/UITask.h | 21 +- 7 files changed, 236 insertions(+), 169 deletions(-) create mode 100644 examples/companion_radio/ui-core/LocatorEngine.h diff --git a/docs/development/ui-core.md b/docs/development/ui-core.md index 5d6ecb6c..fc9dfff4 100644 --- a/docs/development/ui-core.md +++ b/docs/development/ui-core.md @@ -35,7 +35,7 @@ interleaved. Classified: - Unread tracking — DM unread table ✅ (`ui-core/DmUnreadTable.h`), room unread (still in `UITask`). - Notifications — `showAlert`, `notify`, `SoundNotifier`, LED (`userLedHandler`), wake-on-message (`checkDisplayOn`, auto-off). - Live share ✅ `ui-core/LiveShareEngine.h` — session timer, movement/heartbeat gate, send + scope guard, peers' `LiveTrackStore` + expiry. -- Locator — geofence state machine, proximity beeper (`evaluateLocator`, `fireLocator`, targets). +- Locator ✅ `ui-core/LocatorEngine.h` — active target, geofence state machine, proximity beeper. - Trail — sampling, auto-pause, low-battery auto-save. - Course over ground + current position ✅ `ui-core/CourseEngine.h`. - Clock tools — alarm / countdown / ring ✅ `ui-core/ClockEngine.h`. @@ -151,7 +151,7 @@ checked in the sim plus on L1 hardware before the next one. 0. **Listener boundary** ✅ (upstream `MyMesh::Listener` ported; `MyMesh` has no UI calls left). 1. **Skeleton** ✅. `examples/companion_radio/ui-core/` with `UiCore.h` (facade), `MessageHistory.h`, `DmUnreadTable.h`. Header-only for now, reached from `ui-new/UITask.cpp` by relative include, so none of the 66 variant `platformio.ini` files that build `ui-new` change. `UITask` heap-allocates one `UiCore` in `begin()` (before the screens, as `MessagesScreen` used to own the history on the heap); `MessagesScreen` binds to `core().history` by reference. When the Core grows real `.cpp` files, compile them through a unity `.cpp` inside each frontend directory. -2. **Engines, one per commit.** Clock tools ✅ (also introduced `ui-core/UiEvents.h`, the Core → frontend event queue; `UITask::tickCore()` runs `UiCore::loop()` and drains it) → ping ✅ → course-over-ground ✅ → live share ✅ → locator → trail → notifications. `UITask` shrinks to screen management + drawing. +2. **Engines, one per commit.** Clock tools ✅ (also introduced `ui-core/UiEvents.h`, the Core → frontend event queue; `UITask::tickCore()` runs `UiCore::loop()` and drains it) → ping ✅ → course-over-ground ✅ → live share ✅ → locator ✅ → trail → notifications. `UITask` shrinks to screen management + drawing. 3. **Flip the interface.** `UiCore` implements `AbstractUITask`; `ui-new`'s `UITask` becomes a frontend fed by events. 4. **Settings schema.** Convert `SettingsScreen` section by section. 5. **`ui-lvgl` skeleton** for L2 + sim target: boot, home, message list/conversation, keyboard. Then screens by priority. diff --git a/examples/companion_radio/ui-core/LocatorEngine.h b/examples/companion_radio/ui-core/LocatorEngine.h new file mode 100644 index 00000000..672b833d --- /dev/null +++ b/examples/companion_radio/ui-core/LocatorEngine.h @@ -0,0 +1,199 @@ +#pragma once +// Locator: the one "active target" the device tracks (a waypoint, or a person +// by 6-byte pubkey prefix -- persisted in NodePrefs::locator_*), the geofence +// crossing state machine around it and the proximity beeper. The target is +// shared by the Locator geofence, the Nav bearing/ETA view and the map. +// +// Output: UiEventType::LocatorCrossed (text = ready-made alert, flag = arrived) +// on an armed crossing, UiEventType::LocatorBeep for each proximity tick. + +#include "../GeoUtils.h" +#include "UiEvents.h" +#include "CourseEngine.h" +#include "LiveShareEngine.h" + +class LocatorEngine { +public: + void begin(NodePrefs* prefs, const CourseEngine* course, const LiveShareEngine* live, + UiEventQueue* events) { + _prefs = prefs; _course = course; _live = live; _events = events; + } + + void loop() { + // Crossing check -- cheap; a few seconds of latency at the boundary is fine. + if ((int32_t)(millis() - _next_eval_ms) >= 0) { + _next_eval_ms = millis() + 3000UL; + evaluate(); + } + // Proximity beeper -- its own short cadence (the crossing check is too coarse). + proximityBeeper(); + } + + // Re-arm the crossing state machine so the next evaluation initialises + // silently (target/radius changed, or a fresh GPS wake may deliver a + // still-settling first fix -- neither must read as a crossing). + void reset() { _known = false; } + + // ── Target ───────────────────────────────────────────────────────────────── + // setTarget() only *defines* the target (fields + re-arm); the caller decides + // when to persist. kind 0 = waypoint (key ignored), 1 = person (key required). + void setTarget(uint8_t kind, const uint8_t* key, int32_t lat, int32_t lon, const char* name) { + if (!_prefs) return; + _prefs->locator_target_kind = kind; + if (kind == 1 && key) memcpy(_prefs->locator_key, key, NodePrefs::FAVOURITE_PREFIX_LEN); + _prefs->locator_lat_1e6 = lat; + _prefs->locator_lon_1e6 = lon; + snprintf(_prefs->locator_label, sizeof(_prefs->locator_label), "%s", name); + _prefs->locator_has_target = 1; + reset(); // re-seed the crossing engine so the change can't fire on a stale state + } + void clearTarget() { + if (!_prefs) return; + _prefs->locator_has_target = 0; + reset(); + } + // If the active target is exactly this waypoint, clear it and persist (called + // from waypoint deletion so the Locator can't keep pointing at a spot that no + // longer exists). + void clearTargetIfWaypoint(int32_t lat_1e6, int32_t lon_1e6) { + if (!_prefs || !_prefs->locator_has_target || _prefs->locator_target_kind != 0) return; + if (_prefs->locator_lat_1e6 != lat_1e6 || _prefs->locator_lon_1e6 != lon_1e6) return; + clearTarget(); + the_mesh.savePrefs(); + } + // Contact removed: drop the target if it was this person. Returns true if + // prefs changed (caller persists). + bool onContactRemoved(const uint8_t* pub_key) { + if (!_prefs || !_prefs->locator_has_target || _prefs->locator_target_kind != 1) return false; + if (memcmp(_prefs->locator_key, pub_key, NodePrefs::FAVOURITE_PREFIX_LEN) != 0) return false; + clearTarget(); + return true; + } + + // One precedence for a person's position — an active [LOC] live share wins, + // else the last-advertised GPS fix. Not everyone keeps live-sharing on, so the + // fallback lets a rarely-updating but stationary node (a repeater, or someone + // who shared a fix once) still work as a target. Optional live/ts report + // freshness for the picker's age tag. + bool resolvePersonPos(const uint8_t* key, int32_t& lat, int32_t& lon, + bool* live = nullptr, uint32_t* ts = nullptr) const { + if (live) *live = false; + if (ts) *ts = 0; + if (!key) return false; + const LiveTrackStore::Entry* e = + _live->track().activeByKey(key, (uint32_t)rtc_clock.getCurrentTime()); + if (e) { + lat = e->lat_1e6; lon = e->lon_1e6; + if (live) *live = true; + if (ts) *ts = e->ts; + return true; + } + ContactInfo* c = the_mesh.lookupContactByPubKey(key, NodePrefs::FAVOURITE_PREFIX_LEN); + if (c && (c->gps_lat != 0 || c->gps_lon != 0)) { + lat = c->gps_lat; lon = c->gps_lon; + if (ts) *ts = c->lastmod; + return true; + } + return false; + } + + // Resolved position of the active target. Gated only on a target being set, + // independent of whether the Locator alert is enabled, so a destination you + // set still shows on the map. + bool activeTargetPos(int32_t& lat, int32_t& lon) const { + if (!_prefs || !_prefs->locator_has_target) return false; + if (_prefs->locator_target_kind == 1) + return resolvePersonPos(_prefs->locator_key, lat, lon); + lat = _prefs->locator_lat_1e6; + lon = _prefs->locator_lon_1e6; + return true; + } + +private: + // Distance (m) from the current GPS fix to the target, plus the configured + // radius (m). False when no target is set or there's no fix. + bool distance(float& dist_m, float& radius_m) const { + int32_t tlat, tlon; + if (!activeTargetPos(tlat, tlon)) return false; + int32_t lat, lon; + if (!_course->currentLocation(lat, lon)) return false; + dist_m = geo::haversineKm(lat, lon, tlat, tlon) * 1000.0f; + radius_m = (float)NodePrefs::locatorRadiusMeters(_prefs->locator_radius_idx); + return true; + } + + // Crossing the radius fires according to the configured mode; a hysteresis + // band on the "leave" edge stops it chattering at the boundary, and the first + // reading after arming only seeds the inside/outside state. + void evaluate() { + if (!_prefs || !_prefs->locator_enabled || !_prefs->locator_has_target) { + _known = false; + return; + } + float dist, r; + if (!distance(dist, r)) return; // armed but no fix yet — keep state + bool inside; + if (!_known) inside = dist <= r; // seed state + else if (_inside) inside = dist <= r * 1.25f; // leave past band + else inside = dist <= r; // arrive at edge + + if (_known && inside != _inside) { + uint8_t mode = _prefs->locator_mode; // 0=arrive,1=leave,2=both + bool fire = inside ? (mode == 0 || mode == 2) : (mode == 1 || mode == 2); + if (fire) fireCrossing(inside); + } + _inside = inside; + _known = true; + } + + void fireCrossing(bool arrived) { + const char* lbl = _prefs->locator_label[0] ? _prefs->locator_label : "target"; + bool person = _prefs->locator_target_kind == 1; + char msg[sizeof(UiEvent::text)]; + // "Near/Away" reads naturally for a moving person; "Arrived/Left" for a place. + snprintf(msg, sizeof(msg), + arrived ? (person ? "Near: %s" : "Arrived: %s") + : (person ? "Away: %s" : "Left: %s"), lbl); + _events->push(UiEventType::LocatorCrossed, msg, arrived); + } + + // Ticks while inside the radius, faster the nearer the target. + void proximityBeeper() { + static const uint32_t BEEP_MIN_MS = 150; // fastest cadence (at the target) + static const uint32_t BEEP_MAX_MS = 2000; // slowest cadence (at the edge) + if (!_prefs || !_prefs->locator_enabled || !_prefs->locator_beeper + || !_prefs->locator_has_target || _prefs->locator_mode == 1) { // leave-only mode: no homing + return; + } + if ((int32_t)(millis() - _beep_check_ms) < 0) return; + _beep_check_ms = millis() + 250UL; + + float dist, r; + if (!distance(dist, r)) return; + if (dist > r) { // outside the zone: stay quiet, beep on re-entry + _beep_next_ms = millis(); + return; + } + if ((int32_t)(millis() - _beep_next_ms) < 0) return; + float frac = (r > 0) ? dist / r : 0; // 0 at centre, 1 at edge + if (frac < 0) frac = 0; else if (frac > 1) frac = 1; + uint32_t interval = BEEP_MIN_MS + (uint32_t)(frac * (BEEP_MAX_MS - BEEP_MIN_MS)); + _events->push(UiEventType::LocatorBeep); + _beep_next_ms = millis() + interval; + } + + NodePrefs* _prefs = nullptr; + const CourseEngine* _course = nullptr; + const LiveShareEngine* _live = nullptr; + UiEventQueue* _events = nullptr; + + // _known guards the first evaluation after arming (initialise inside/outside + // silently, fire only on later crossings). + uint32_t _next_eval_ms = 0; + bool _inside = false; + bool _known = false; + // Beeper: _beep_check_ms throttles the distance poll; _beep_next_ms is when + // the next tick is due. + uint32_t _beep_check_ms = 0; + uint32_t _beep_next_ms = 0; +}; diff --git a/examples/companion_radio/ui-core/UiCore.h b/examples/companion_radio/ui-core/UiCore.h index f8c59778..02b8aa21 100644 --- a/examples/companion_radio/ui-core/UiCore.h +++ b/examples/companion_radio/ui-core/UiCore.h @@ -14,6 +14,7 @@ #include "PingEngine.h" #include "CourseEngine.h" #include "LiveShareEngine.h" +#include "LocatorEngine.h" class UiCore { public: @@ -22,6 +23,7 @@ public: ping.begin(prefs); course.begin(sensors); live_share.begin(prefs, &course, &events); + locator.begin(prefs, &course, &live_share, &events); } // Driven from the frontend's loop(), before it drains `events`. @@ -29,6 +31,7 @@ public: clock.loop(); course.loop(); live_share.loop(); + locator.loop(); } UiEventQueue events; // Core → frontend; drained by the frontend's loop() @@ -38,6 +41,7 @@ public: PingEngine ping; // single in-flight ping + last result CourseEngine course; // GPS position + course-over-ground ring LiveShareEngine live_share; // [LOC] auto-share session + peers' shared positions + LocatorEngine locator; // active target, geofence crossings, proximity beeper // ── Models ──────────────────────────────────────────────────────────────── MessageHistory history; // channel + DM rings, delivery state, channel unread diff --git a/examples/companion_radio/ui-core/UiEvents.h b/examples/companion_radio/ui-core/UiEvents.h index 574478c3..62b87cb4 100644 --- a/examples/companion_radio/ui-core/UiEvents.h +++ b/examples/companion_radio/ui-core/UiEvents.h @@ -12,11 +12,14 @@ enum class UiEventType : uint8_t { ClockAlert, // alarm / countdown fired: wake, show `text`, start the ring melody ClockRingEnded, // ring window elapsed with no dismiss: stop melody, clear alert LiveShareEnded, // live-share session reached its duration and switched itself off + LocatorCrossed, // geofence crossing: show `text`; flag = arrived (else left) + LocatorBeep, // one proximity-beeper tick }; struct UiEvent { UiEventType type; - char text[20]; + bool flag; + char text[24]; }; class UiEventQueue { @@ -25,11 +28,12 @@ public: UiEventQueue() : _head(0), _count(0) {} - void push(UiEventType type, const char* text = nullptr) { + void push(UiEventType type, const char* text = nullptr, bool flag = false) { int pos; if (_count < SIZE) { pos = (_head + _count) % SIZE; _count++; } else { pos = _head; _head = (_head + 1) % SIZE; } // drop oldest _q[pos].type = type; + _q[pos].flag = flag; if (text) { strncpy(_q[pos].text, text, sizeof(_q[pos].text) - 1); _q[pos].text[sizeof(_q[pos].text) - 1] = '\0'; diff --git a/examples/companion_radio/ui-new/LocatorScreen.h b/examples/companion_radio/ui-new/LocatorScreen.h index 0b8cd891..9afe983a 100644 --- a/examples/companion_radio/ui-new/LocatorScreen.h +++ b/examples/companion_radio/ui-new/LocatorScreen.h @@ -5,7 +5,7 @@ // When armed the device beeps / alerts as it crosses into (arrive/near) or out // of (leave/away) the radius. A waypoint target is snapshotted (coord + label); // a person target follows their latest shared position. The crossing engine -// lives in UITask::evaluateLocator(). The Target row's Enter opens a picker +// lives in the UI Core (ui-core/LocatorEngine.h). The Target row's Enter opens a picker // ("None" first — the only way to unset a target once chosen — then your // favourites, offered even with no known position yet so you can arm ahead // of time, then any other contact with a currently-known position: @@ -167,7 +167,7 @@ public: // Add a person candidate to _targets, deduped by pubkey prefix. Freshness is // resolved here for display only: an active [LOC] share wins (live=true), // else the contact's last-advertised position if it has one — the same - // precedence UITask::locatorDistance() uses at evaluation time. When + // precedence LocatorEngine uses at evaluation time. When // `require_position` is false (favourites), a contact with neither is still // added with no position — "arm ahead of time", per the existing feature. bool addPersonTarget(const uint8_t* key, const char* name, bool require_position, bool fav) { diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 8c48ae42..4d5b2899 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1745,6 +1745,14 @@ void UITask::tickCore() { case UiEventType::LiveShareEnded: showAlert("Live share ended", 2500); break; + case UiEventType::LocatorCrossed: + showAlert(ev.text, 3000); + if (!isBuzzerQuiet()) + playMelody(ev.flag ? "locarr:d=8,o=6,b=140:c,e,g" : "loclv:d=8,o=6,b=140:g,e,c"); + break; + case UiEventType::LocatorBeep: + playMelody("locp:d=32,o=7,b=200:c"); + break; default: break; } @@ -2745,9 +2753,6 @@ void UITask::loop() { if (curr) curr->poll(); - // UI Core engines (alarm + countdown, …) run regardless of the current screen - // / display state, so they're driven here (not via the current screen's poll()). - tickCore(); if (_display != NULL && _display->isOn()) { if (_locked && (int32_t)(millis() - _lock_wake_until) >= 0) { @@ -2925,114 +2930,22 @@ void UITask::loop() { } } - // Locator — beep + alert when the device crosses into / out of the armed - // geofence. Cheap; a few seconds of latency at the boundary is fine. - if ((int32_t)(millis() - _next_locator_ms) >= 0) { - _next_locator_ms = millis() + 3000UL; - evaluateLocator(); - } - // Locator proximity beeper — ticks faster the closer to the target. Runs on - // its own short cadence (the crossing check above is too coarse for this). - locatorProximityBeeper(); + // UI Core engines (alarm + countdown, COG, live share, locator, …) run + // regardless of the current screen / display state. Last in the loop, after + // the GPS keep-awake check above (a fresh GPS wake re-seeds the locator first). + tickCore(); } -// Evaluate the single geofence against the current GPS fix. Crossing the radius -// fires fireLocator() according to the configured mode; a hysteresis band on -// the "leave" edge stops it chattering at the boundary, and the first reading -// after arming only seeds the inside/outside state (no spurious alert). -// Distance (m) from the current GPS fix to the locator target, plus the -// configured radius (m). Returns false when no target is set or there's no fix -// — the single place the target-distance maths lives, shared by the crossing -// evaluator and the proximity beeper. -// One precedence for a person's position — an active [LOC] live share wins, -// else the last-advertised GPS fix. Not everyone keeps live-sharing on, so the -// fallback lets a rarely-updating but stationary node (a repeater, or someone -// who shared a fix once) still work as a target. +// Locator (engine in ui-core/LocatorEngine.h). +void UITask::resetLocator() { _core->locator.reset(); } bool UITask::resolvePersonPos(const uint8_t* key, int32_t& lat, int32_t& lon, bool* live, uint32_t* ts) const { - if (live) *live = false; - if (ts) *ts = 0; - if (!key) return false; - const LiveTrackStore::Entry* e = - _core->live_share.track().activeByKey(key, (uint32_t)rtc_clock.getCurrentTime()); - if (e) { - lat = e->lat_1e6; lon = e->lon_1e6; - if (live) *live = true; - if (ts) *ts = e->ts; - return true; - } - ContactInfo* c = the_mesh.lookupContactByPubKey(key, NodePrefs::FAVOURITE_PREFIX_LEN); - if (c && (c->gps_lat != 0 || c->gps_lon != 0)) { - lat = c->gps_lat; lon = c->gps_lon; - if (ts) *ts = c->lastmod; - return true; - } - return false; + return _core->locator.resolvePersonPos(key, lat, lon, live, ts); } - -bool UITask::activeTargetPos(int32_t& lat, int32_t& lon) const { - if (!_node_prefs || !_node_prefs->locator_has_target) return false; - if (_node_prefs->locator_target_kind == 1) - return resolvePersonPos(_node_prefs->locator_key, lat, lon); - lat = _node_prefs->locator_lat_1e6; - lon = _node_prefs->locator_lon_1e6; - return true; -} - -bool UITask::locatorDistance(float& dist_m, float& radius_m) const { - int32_t tlat, tlon; - if (!activeTargetPos(tlat, tlon)) return false; - int32_t lat, lon; - if (!currentLocation(lat, lon)) return false; - dist_m = geo::haversineKm(lat, lon, tlat, tlon) * 1000.0f; - radius_m = (float)NodePrefs::locatorRadiusMeters(_node_prefs->locator_radius_idx); - return true; -} - -void UITask::evaluateLocator() { - if (!_node_prefs || !_node_prefs->locator_enabled || !_node_prefs->locator_has_target) { - _locator_known = false; - return; - } - float dist, r; - if (!locatorDistance(dist, r)) return; // armed but no fix yet — keep state - bool inside; - if (!_locator_known) inside = dist <= r; // seed state - else if (_locator_inside) inside = dist <= r * 1.25f; // leave past band - else inside = dist <= r; // arrive at edge - - if (_locator_known && inside != _locator_inside) { - uint8_t mode = _node_prefs->locator_mode; // 0=arrive,1=leave,2=both - bool fire = inside ? (mode == 0 || mode == 2) : (mode == 1 || mode == 2); - if (fire) fireLocator(inside); - } - _locator_inside = inside; - _locator_known = true; -} - -void UITask::fireLocator(bool arrived) { - const char* lbl = _node_prefs->locator_label[0] ? _node_prefs->locator_label : "target"; - bool person = _node_prefs->locator_target_kind == 1; - char msg[40]; - // "Near/Away" reads naturally for a moving person; "Arrived/Left" for a place. - snprintf(msg, sizeof(msg), - arrived ? (person ? "Near: %s" : "Arrived: %s") - : (person ? "Away: %s" : "Left: %s"), lbl); - showAlert(msg, 3000); - if (!isBuzzerQuiet()) - playMelody(arrived ? "locarr:d=8,o=6,b=140:c,e,g" : "loclv:d=8,o=6,b=140:g,e,c"); -} - +bool UITask::activeTargetPos(int32_t& lat, int32_t& lon) const { return _core->locator.activeTargetPos(lat, lon); } void UITask::setTarget(uint8_t kind, const uint8_t* key, int32_t lat, int32_t lon, const char* name) { - if (!_node_prefs) return; - _node_prefs->locator_target_kind = kind; - if (kind == 1 && key) memcpy(_node_prefs->locator_key, key, NodePrefs::FAVOURITE_PREFIX_LEN); - _node_prefs->locator_lat_1e6 = lat; - _node_prefs->locator_lon_1e6 = lon; - snprintf(_node_prefs->locator_label, sizeof(_node_prefs->locator_label), "%s", name); - _node_prefs->locator_has_target = 1; - resetLocator(); // re-seed the crossing engine so the change can't fire on a stale state + _core->locator.setTarget(kind, key, lat, lon, name); } void UITask::setTargetNow(uint8_t kind, const uint8_t* key, int32_t lat, int32_t lon, const char* name) { @@ -3042,18 +2955,8 @@ void UITask::setTargetNow(uint8_t kind, const uint8_t* key, int32_t lat, int32_t showAlert("Target set", 1200); } -void UITask::clearTarget() { - if (!_node_prefs) return; - _node_prefs->locator_has_target = 0; - resetLocator(); -} - -void UITask::clearTargetIfWaypoint(int32_t lat_1e6, int32_t lon_1e6) { - if (!_node_prefs || !_node_prefs->locator_has_target || _node_prefs->locator_target_kind != 0) return; - if (_node_prefs->locator_lat_1e6 != lat_1e6 || _node_prefs->locator_lon_1e6 != lon_1e6) return; - clearTarget(); - the_mesh.savePrefs(); -} +void UITask::clearTarget() { _core->locator.clearTarget(); } +void UITask::clearTargetIfWaypoint(int32_t lat_1e6, int32_t lon_1e6) { _core->locator.clearTargetIfWaypoint(lat_1e6, lon_1e6); } // CONTRACT: every NodePrefs field that keys on a contact pubkey/prefix is // cleared here, so a removed contact can't leave a dangling reference. If you @@ -3073,11 +2976,7 @@ void UITask::onContactRemoved(const uint8_t* pub_key) { int slot = findFavouriteSlot(pub_key); if (slot >= 0) { clearFavouriteSlot(slot); changed = true; } - if (_node_prefs->locator_has_target && _node_prefs->locator_target_kind == 1 - && memcmp(_node_prefs->locator_key, pub_key, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) { - clearTarget(); - changed = true; - } + if (_core->locator.onContactRemoved(pub_key)) changed = true; // Fail closed rather than guess a new recipient: a contact target that's // gone just turns auto-share off, it doesn't fall back to some other target. if (_node_prefs->loc_share_target_type == 1 @@ -3163,30 +3062,6 @@ void UITask::onChannelRemoved(uint8_t channel_idx) { // radius. The beeper has its own toggle (locator_beeper), so turning it on is // an explicit "I want to hear this" — it deliberately overrides the global // buzzer mute (playMelody → buzzer.playForced ignores the quiet flag). -void UITask::locatorProximityBeeper() { - static const uint32_t BEEP_MIN_MS = 150; // fastest cadence (at the target) - static const uint32_t BEEP_MAX_MS = 2000; // slowest cadence (at the edge) - if (!_node_prefs || !_node_prefs->locator_enabled || !_node_prefs->locator_beeper - || !_node_prefs->locator_has_target || _node_prefs->locator_mode == 1) { // leave-only mode: no homing - return; - } - if ((int32_t)(millis() - _locator_beep_check_ms) < 0) return; - _locator_beep_check_ms = millis() + 250UL; - - float dist, r; - if (!locatorDistance(dist, r)) return; - if (dist > r) { // outside the zone: stay quiet, beep on re-entry - _locator_beep_next_ms = millis(); - return; - } - if ((int32_t)(millis() - _locator_beep_next_ms) < 0) return; - float frac = (r > 0) ? dist / r : 0; // 0 at centre, 1 at edge - if (frac < 0) frac = 0; else if (frac > 1) frac = 1; - uint32_t interval = BEEP_MIN_MS + (uint32_t)(frac * (BEEP_MAX_MS - BEEP_MIN_MS)); - playMelody("locp:d=32,o=7,b=200:c"); - _locator_beep_next_ms = millis() + interval; -} - bool UITask::currentCourse(int& deg_out) const { return _core->course.currentCourse(deg_out); } bool UITask::currentLocation(int32_t& lat, int32_t& lon) const { return _core->course.currentLocation(lat, lon); } diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index ae1b0f71..ebed2792 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -127,21 +127,6 @@ class UITask : public AbstractUITask { bool _trail_pause_has_ref = false; uint32_t _trail_last_move_ms = 0; - // Locator engine state. _locator_known guards the first evaluation after - // arming (initialise inside/outside silently, fire only on later crossings). - uint32_t _next_locator_ms = 0; - bool _locator_inside = false; - bool _locator_known = false; - // Proximity beeper: ticks while inside the radius, faster the nearer the - // target. _locator_beep_check_ms throttles the distance poll; _locator_beep_next_ms - // is when the next tick is due. - uint32_t _locator_beep_check_ms = 0; - uint32_t _locator_beep_next_ms = 0; - bool locatorDistance(float& dist_m, float& radius_m) const; - void evaluateLocator(); - void fireLocator(bool arrived); - void locatorProximityBeeper(); - // Runs the UI Core engines and reacts to their events (alert overlay, buzzer, // display wake). Driven from loop() regardless of the current screen. void tickCore(); @@ -301,7 +286,7 @@ public: // Re-arm the locator state machine so the next evaluation initialises // silently (called by the Locator tool after the target/radius changes, // so re-entering the zone doesn't fire on a stale inside/outside state). - void resetLocator() { _locator_known = false; } + void resetLocator(); // The one "active target" the device tracks — shared by the Locator geofence, // the Nav bearing/ETA view and (future) the map focus, so every entry point // sets the same thing. kind 0 = waypoint (key ignored), 1 = person (key @@ -335,13 +320,13 @@ public: // prefers an active [LOC] live share, falls back to their last-advertised // GPS fix. Returns false when neither is known. Optional live/ts report // freshness for the picker's age tag. One precedence, used by both the - // Locator engine (locatorDistance) and the target picker. + // Locator engine (ui-core/LocatorEngine.h) and the target picker. bool resolvePersonPos(const uint8_t* key, int32_t& lat, int32_t& lon, bool* live = nullptr, uint32_t* ts = nullptr) const; // Resolved position of the active target — a waypoint's coords, or a person // via resolvePersonPos(). Gated only on a target being set, independent of // whether the Locator alert is enabled, so a destination you set still shows - // on the map. Used by locatorDistance() and the map renderers. + // on the map. Used by the Locator engine and the map renderers. bool activeTargetPos(int32_t& lat, int32_t& lon) const; void gotoTrailScreen(); void gotoMapScreen(); // opens the Trail screen directly in its Map view