mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-20 12:32:26 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,7 @@ interleaved. Classified:
|
|||||||
- Unread tracking — DM unread table ✅ (`ui-core/DmUnreadTable.h`), room unread (still in `UITask`).
|
- 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).
|
- 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.
|
- 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.
|
- Trail — sampling, auto-pause, low-battery auto-save.
|
||||||
- Course over ground + current position ✅ `ui-core/CourseEngine.h`.
|
- Course over ground + current position ✅ `ui-core/CourseEngine.h`.
|
||||||
- Clock tools — alarm / countdown / ring ✅ `ui-core/ClockEngine.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).
|
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.
|
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.
|
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.
|
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.
|
5. **`ui-lvgl` skeleton** for L2 + sim target: boot, home, message list/conversation, keyboard. Then screens by priority.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "PingEngine.h"
|
#include "PingEngine.h"
|
||||||
#include "CourseEngine.h"
|
#include "CourseEngine.h"
|
||||||
#include "LiveShareEngine.h"
|
#include "LiveShareEngine.h"
|
||||||
|
#include "LocatorEngine.h"
|
||||||
|
|
||||||
class UiCore {
|
class UiCore {
|
||||||
public:
|
public:
|
||||||
@@ -22,6 +23,7 @@ public:
|
|||||||
ping.begin(prefs);
|
ping.begin(prefs);
|
||||||
course.begin(sensors);
|
course.begin(sensors);
|
||||||
live_share.begin(prefs, &course, &events);
|
live_share.begin(prefs, &course, &events);
|
||||||
|
locator.begin(prefs, &course, &live_share, &events);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Driven from the frontend's loop(), before it drains `events`.
|
// Driven from the frontend's loop(), before it drains `events`.
|
||||||
@@ -29,6 +31,7 @@ public:
|
|||||||
clock.loop();
|
clock.loop();
|
||||||
course.loop();
|
course.loop();
|
||||||
live_share.loop();
|
live_share.loop();
|
||||||
|
locator.loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
UiEventQueue events; // Core → frontend; drained by the frontend's loop()
|
UiEventQueue events; // Core → frontend; drained by the frontend's loop()
|
||||||
@@ -38,6 +41,7 @@ public:
|
|||||||
PingEngine ping; // single in-flight ping + last result
|
PingEngine ping; // single in-flight ping + last result
|
||||||
CourseEngine course; // GPS position + course-over-ground ring
|
CourseEngine course; // GPS position + course-over-ground ring
|
||||||
LiveShareEngine live_share; // [LOC] auto-share session + peers' shared positions
|
LiveShareEngine live_share; // [LOC] auto-share session + peers' shared positions
|
||||||
|
LocatorEngine locator; // active target, geofence crossings, proximity beeper
|
||||||
|
|
||||||
// ── Models ────────────────────────────────────────────────────────────────
|
// ── Models ────────────────────────────────────────────────────────────────
|
||||||
MessageHistory history; // channel + DM rings, delivery state, channel unread
|
MessageHistory history; // channel + DM rings, delivery state, channel unread
|
||||||
|
|||||||
@@ -12,11 +12,14 @@ enum class UiEventType : uint8_t {
|
|||||||
ClockAlert, // alarm / countdown fired: wake, show `text`, start the ring melody
|
ClockAlert, // alarm / countdown fired: wake, show `text`, start the ring melody
|
||||||
ClockRingEnded, // ring window elapsed with no dismiss: stop melody, clear alert
|
ClockRingEnded, // ring window elapsed with no dismiss: stop melody, clear alert
|
||||||
LiveShareEnded, // live-share session reached its duration and switched itself off
|
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 {
|
struct UiEvent {
|
||||||
UiEventType type;
|
UiEventType type;
|
||||||
char text[20];
|
bool flag;
|
||||||
|
char text[24];
|
||||||
};
|
};
|
||||||
|
|
||||||
class UiEventQueue {
|
class UiEventQueue {
|
||||||
@@ -25,11 +28,12 @@ public:
|
|||||||
|
|
||||||
UiEventQueue() : _head(0), _count(0) {}
|
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;
|
int pos;
|
||||||
if (_count < SIZE) { pos = (_head + _count) % SIZE; _count++; }
|
if (_count < SIZE) { pos = (_head + _count) % SIZE; _count++; }
|
||||||
else { pos = _head; _head = (_head + 1) % SIZE; } // drop oldest
|
else { pos = _head; _head = (_head + 1) % SIZE; } // drop oldest
|
||||||
_q[pos].type = type;
|
_q[pos].type = type;
|
||||||
|
_q[pos].flag = flag;
|
||||||
if (text) {
|
if (text) {
|
||||||
strncpy(_q[pos].text, text, sizeof(_q[pos].text) - 1);
|
strncpy(_q[pos].text, text, sizeof(_q[pos].text) - 1);
|
||||||
_q[pos].text[sizeof(_q[pos].text) - 1] = '\0';
|
_q[pos].text[sizeof(_q[pos].text) - 1] = '\0';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// When armed the device beeps / alerts as it crosses into (arrive/near) or out
|
// 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);
|
// of (leave/away) the radius. A waypoint target is snapshotted (coord + label);
|
||||||
// a person target follows their latest shared position. The crossing engine
|
// 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
|
// ("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
|
// favourites, offered even with no known position yet so you can arm ahead
|
||||||
// of time, then any other contact with a currently-known position:
|
// 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
|
// Add a person candidate to _targets, deduped by pubkey prefix. Freshness is
|
||||||
// resolved here for display only: an active [LOC] share wins (live=true),
|
// 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
|
// 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
|
// `require_position` is false (favourites), a contact with neither is still
|
||||||
// added with no position — "arm ahead of time", per the existing feature.
|
// 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) {
|
bool addPersonTarget(const uint8_t* key, const char* name, bool require_position, bool fav) {
|
||||||
|
|||||||
@@ -1745,6 +1745,14 @@ void UITask::tickCore() {
|
|||||||
case UiEventType::LiveShareEnded:
|
case UiEventType::LiveShareEnded:
|
||||||
showAlert("Live share ended", 2500);
|
showAlert("Live share ended", 2500);
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -2745,9 +2753,6 @@ void UITask::loop() {
|
|||||||
|
|
||||||
if (curr) curr->poll();
|
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 (_display != NULL && _display->isOn()) {
|
||||||
if (_locked && (int32_t)(millis() - _lock_wake_until) >= 0) {
|
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
|
// UI Core engines (alarm + countdown, COG, live share, locator, …) run
|
||||||
// its own short cadence (the crossing check above is too coarse for this).
|
// regardless of the current screen / display state. Last in the loop, after
|
||||||
locatorProximityBeeper();
|
// 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
|
// Locator (engine in ui-core/LocatorEngine.h).
|
||||||
// fires fireLocator() according to the configured mode; a hysteresis band on
|
void UITask::resetLocator() { _core->locator.reset(); }
|
||||||
// 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.
|
|
||||||
bool UITask::resolvePersonPos(const uint8_t* key, int32_t& lat, int32_t& lon,
|
bool UITask::resolvePersonPos(const uint8_t* key, int32_t& lat, int32_t& lon,
|
||||||
bool* live, uint32_t* ts) const {
|
bool* live, uint32_t* ts) const {
|
||||||
if (live) *live = false;
|
return _core->locator.resolvePersonPos(key, lat, lon, live, ts);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
bool UITask::activeTargetPos(int32_t& lat, int32_t& lon) const { return _core->locator.activeTargetPos(lat, lon); }
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
void UITask::setTarget(uint8_t kind, const uint8_t* key, int32_t lat, int32_t lon, const char* name) {
|
void UITask::setTarget(uint8_t kind, const uint8_t* key, int32_t lat, int32_t lon, const char* name) {
|
||||||
if (!_node_prefs) return;
|
_core->locator.setTarget(kind, key, lat, lon, name);
|
||||||
_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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::setTargetNow(uint8_t kind, const uint8_t* key, int32_t lat, int32_t lon, const char* 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);
|
showAlert("Target set", 1200);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UITask::clearTarget() {
|
void UITask::clearTarget() { _core->locator.clearTarget(); }
|
||||||
if (!_node_prefs) return;
|
void UITask::clearTargetIfWaypoint(int32_t lat_1e6, int32_t lon_1e6) { _core->locator.clearTargetIfWaypoint(lat_1e6, lon_1e6); }
|
||||||
_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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// CONTRACT: every NodePrefs field that keys on a contact pubkey/prefix is
|
// 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
|
// 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);
|
int slot = findFavouriteSlot(pub_key);
|
||||||
if (slot >= 0) { clearFavouriteSlot(slot); changed = true; }
|
if (slot >= 0) { clearFavouriteSlot(slot); changed = true; }
|
||||||
|
|
||||||
if (_node_prefs->locator_has_target && _node_prefs->locator_target_kind == 1
|
if (_core->locator.onContactRemoved(pub_key)) changed = true;
|
||||||
&& memcmp(_node_prefs->locator_key, pub_key, NodePrefs::FAVOURITE_PREFIX_LEN) == 0) {
|
|
||||||
clearTarget();
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
// Fail closed rather than guess a new recipient: a contact target that's
|
// 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.
|
// gone just turns auto-share off, it doesn't fall back to some other target.
|
||||||
if (_node_prefs->loc_share_target_type == 1
|
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
|
// 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
|
// an explicit "I want to hear this" — it deliberately overrides the global
|
||||||
// buzzer mute (playMelody → buzzer.playForced ignores the quiet flag).
|
// 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::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); }
|
bool UITask::currentLocation(int32_t& lat, int32_t& lon) const { return _core->course.currentLocation(lat, lon); }
|
||||||
|
|
||||||
|
|||||||
@@ -127,21 +127,6 @@ class UITask : public AbstractUITask {
|
|||||||
bool _trail_pause_has_ref = false;
|
bool _trail_pause_has_ref = false;
|
||||||
uint32_t _trail_last_move_ms = 0;
|
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,
|
// Runs the UI Core engines and reacts to their events (alert overlay, buzzer,
|
||||||
// display wake). Driven from loop() regardless of the current screen.
|
// display wake). Driven from loop() regardless of the current screen.
|
||||||
void tickCore();
|
void tickCore();
|
||||||
@@ -301,7 +286,7 @@ public:
|
|||||||
// Re-arm the locator state machine so the next evaluation initialises
|
// Re-arm the locator state machine so the next evaluation initialises
|
||||||
// silently (called by the Locator tool after the target/radius changes,
|
// 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).
|
// 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 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
|
// 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
|
// 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
|
// prefers an active [LOC] live share, falls back to their last-advertised
|
||||||
// GPS fix. Returns false when neither is known. Optional live/ts report
|
// 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
|
// 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 resolvePersonPos(const uint8_t* key, int32_t& lat, int32_t& lon,
|
||||||
bool* live = nullptr, uint32_t* ts = nullptr) const;
|
bool* live = nullptr, uint32_t* ts = nullptr) const;
|
||||||
// Resolved position of the active target — a waypoint's coords, or a person
|
// Resolved position of the active target — a waypoint's coords, or a person
|
||||||
// via resolvePersonPos(). Gated only on a target being set, independent of
|
// via resolvePersonPos(). Gated only on a target being set, independent of
|
||||||
// whether the Locator alert is enabled, so a destination you set still shows
|
// 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;
|
bool activeTargetPos(int32_t& lat, int32_t& lon) const;
|
||||||
void gotoTrailScreen();
|
void gotoTrailScreen();
|
||||||
void gotoMapScreen(); // opens the Trail screen directly in its Map view
|
void gotoMapScreen(); // opens the Trail screen directly in its Map view
|
||||||
|
|||||||
Reference in New Issue
Block a user