mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-27 15:28:11 +00:00
feat(geo-alert): pick target from a list (favourites first)
Enter on the Target row now opens a picker instead of only cycling: it lists favourites first (the quick path), then active verified live senders, then waypoints — UP/DOWN + Enter to choose. This lets you arm an alert against a person ahead of time by pubkey, without waiting for them to share a [LOC] (the previous cycle only offered currently-active live senders, so a person you hadn't received a fresh share from couldn't be selected at all). LEFT/RIGHT still quick-cycles the same unified set. Verified: WioTrackerL1_companion_solo_dual builds clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -232,14 +232,14 @@ Navigate with **UP/DOWN**, change a value with **LEFT/RIGHT** (or **Enter**); **
|
||||
| Setting | Options | Notes |
|
||||
| ------- | -------------------------------- | -------------------------------------------------------------------------------------- |
|
||||
| Alert | ON / OFF | Master switch. Enabling without a target prompts you to pick one. |
|
||||
| Target | waypoints + live contacts | **LEFT/RIGHT** (or **Enter**) cycles through your waypoints and any verified (DM) live contacts; a contact target is shown with an `@` prefix. Shows `none` until set. |
|
||||
| Target | person or waypoint | **Enter** opens a picker — **favourites** first, then any active live senders, then waypoints; **UP/DOWN** + **Enter** to choose. **LEFT/RIGHT** quick-cycles the same set in place. A person is shown with an `@` prefix. Shows `none` until set. |
|
||||
| Radius | 50 / 100 / 250 / 500 m / 1 km | Geofence size. |
|
||||
| Mode | Arrive / Leave / Both | Which crossing fires the alert — entering the radius, leaving it, or both. |
|
||||
| Beeper | ON / OFF | Optional homing tone (see below). |
|
||||
|
||||
**Crossing alert.** When armed with a target, the device watches its own GPS fix and fires the alert (a short melody plus an on-screen message) the moment you cross the radius, according to **Mode**. The wording adapts to the target — `Arrived` / `Left` for a waypoint, `Near` / `Away` for a person. The edge has a little hysteresis so a fix hovering right on the boundary doesn't chatter, and the first reading after arming only seeds the in/out state — it won't fire spuriously just because you armed it while already inside.
|
||||
|
||||
**Following a person.** With a **live-contact** target, the geofence tracks the distance *between you and them* using their latest shared position, so it works even while both of you move. If they stop sharing (their share goes stale), evaluation pauses until a fresh position arrives — it never alerts on an outdated fix. Only contacts whose shares are **DM-verified** can be targeted (a channel share carries no stable identity to lock onto).
|
||||
**Following a person.** Pick a **favourite** (or contact / active live sender) as the target and the geofence tracks the distance *between you and them* using their latest shared position, so it works even while both of you move. You can arm it **ahead of time** — choosing a favourite locks onto their identity (pubkey), and the alert starts working as soon as they share a position. If they stop sharing (their share goes stale), evaluation pauses until a fresh position arrives — it never alerts on an outdated fix. A person must share over a **DM** for it to follow them (a channel share carries no stable identity to lock onto).
|
||||
|
||||
**Proximity beeper.** With **Beeper** on, the device also ticks while you're inside the radius and **shortens the gap between ticks the closer you get to the target** — slow near the edge, rapid near the centre — like a homing beeper guiding you to the exact spot. It's silent outside the radius. Because the beeper is its own opt-in toggle, turning it on **overrides the global buzzer mute** (**Settings › Sound › Buzzer**) — it's an explicit "I want to hear this". It works independently of the arrive/leave crossing alert (which does follow the mute), so you can use either or both.
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#pragma once
|
||||
// Geo-alert config tool. Tools › Geo Alert.
|
||||
// A single geofence around a saved waypoint: when armed, the device beeps and
|
||||
// shows an alert as it crosses into (arrive) or out of (leave) the radius. The
|
||||
// target is snapshotted from a waypoint (coord + label), so the alert keeps
|
||||
// working even if that waypoint is later edited or deleted. The crossing engine
|
||||
// itself lives in UITask::evaluateGeoAlert().
|
||||
// A single geofence whose target is either a saved waypoint (a place) or a
|
||||
// person — a favourite/contact or a live [LOC] sender, keyed by pubkey prefix.
|
||||
// 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::evaluateGeoAlert(). The Target row's Enter opens a picker
|
||||
// (favourites first, then active live senders, then waypoints); LEFT/RIGHT
|
||||
// quick-cycles the same set.
|
||||
// Included by UITask.cpp after LiveShareScreen.h.
|
||||
|
||||
#include "../NodePrefs.h"
|
||||
@@ -19,6 +22,15 @@ class GeoAlertScreen : public UIScreen {
|
||||
int _sel = 0;
|
||||
int _scroll = 0;
|
||||
|
||||
// Target picker (Enter on the Target row): a flat selectable list of people
|
||||
// and places, rebuilt from favourites / live senders / waypoints on open.
|
||||
bool _picking = false;
|
||||
int _pick_sel = 0, _pick_scroll = 0;
|
||||
struct Target { uint8_t kind; int32_t lat, lon; uint8_t key[6]; char name[20]; }; // kind 0=waypoint 1=person
|
||||
static const int TARGET_MAX = 24;
|
||||
Target _targets[TARGET_MAX];
|
||||
int _target_n = 0;
|
||||
|
||||
enum Kind : uint8_t { K_ENABLE, K_TARGET, K_RADIUS, K_MODE, K_BEEPER };
|
||||
struct Row { Kind kind; const char* label; };
|
||||
static const int ROW_COUNT = 5;
|
||||
@@ -36,7 +48,7 @@ class GeoAlertScreen : public UIScreen {
|
||||
public:
|
||||
GeoAlertScreen(UITask* task, NodePrefs* prefs) : _task(task), _prefs(prefs) {}
|
||||
|
||||
void enter() { _dirty = false; _sel = 0; _scroll = 0; }
|
||||
void enter() { _dirty = false; _sel = 0; _scroll = 0; _picking = false; }
|
||||
|
||||
void valueLabel(Kind k, char* buf, int n) {
|
||||
switch (k) {
|
||||
@@ -72,6 +84,7 @@ public:
|
||||
int render(DisplayDriver& display) override {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
if (_picking) { renderPicker(display); return 400; }
|
||||
display.drawCenteredHeader("GEO ALERT");
|
||||
|
||||
const int valx = display.width() / 2 + 6;
|
||||
@@ -121,53 +134,112 @@ public:
|
||||
_task->resetGeoAlert();
|
||||
}
|
||||
|
||||
// Cycle the target through saved waypoints (static) and verified live contacts
|
||||
// (a person sharing [LOC] over a DM), snapshotting the chosen one into prefs.
|
||||
// A waypoint stores coord+label; a contact stores the pubkey prefix so the
|
||||
// engine can follow its live position, plus the name and last-known coord.
|
||||
void cycleTarget(int dir) {
|
||||
WaypointStore& wp = _task->waypoints();
|
||||
// Build the selectable target set into _targets: favourites first (the quick
|
||||
// path you pin ahead of time), then active verified live senders not already
|
||||
// pinned, then saved waypoints. A person is keyed by pubkey prefix so the
|
||||
// engine follows their live position even before/independent of a [LOC].
|
||||
void buildTargets() {
|
||||
_target_n = 0;
|
||||
for (int i = 0; i < NodePrefs::FAVOURITES_COUNT && _target_n < TARGET_MAX; i++) {
|
||||
const uint8_t* pre = _prefs->favourite_contacts[i];
|
||||
bool empty = true;
|
||||
for (int b = 0; b < NodePrefs::FAVOURITE_PREFIX_LEN; b++) if (pre[b]) { empty = false; break; }
|
||||
if (empty) continue;
|
||||
ContactInfo* c = the_mesh.lookupContactByPubKey(pre, NodePrefs::FAVOURITE_PREFIX_LEN);
|
||||
if (!c) continue;
|
||||
Target& t = _targets[_target_n++];
|
||||
t.kind = 1; t.lat = t.lon = 0;
|
||||
memcpy(t.key, pre, 6);
|
||||
snprintf(t.name, sizeof(t.name), "%s", c->name);
|
||||
}
|
||||
LiveTrackStore& lt = _task->liveTrack();
|
||||
uint32_t now = rtc_clock.getCurrentTime();
|
||||
for (int i = 0; i < LiveTrackStore::CAPACITY && _target_n < TARGET_MAX; i++) {
|
||||
if (!lt.isActive(i, now) || !lt.slotAt(i).verified) continue;
|
||||
const LiveTrackStore::Entry& e = lt.slotAt(i);
|
||||
bool dup = false;
|
||||
for (int j = 0; j < _target_n; j++)
|
||||
if (_targets[j].kind == 1 && memcmp(_targets[j].key, e.key, 6) == 0) { dup = true; break; }
|
||||
if (dup) continue;
|
||||
Target& t = _targets[_target_n++];
|
||||
t.kind = 1; t.lat = e.lat_1e6; t.lon = e.lon_1e6;
|
||||
memcpy(t.key, e.key, 6);
|
||||
snprintf(t.name, sizeof(t.name), "%s", e.name);
|
||||
}
|
||||
WaypointStore& wp = _task->waypoints();
|
||||
for (int i = 0; i < wp.count() && _target_n < TARGET_MAX; i++) {
|
||||
const Waypoint& w = wp.at(i);
|
||||
Target& t = _targets[_target_n++];
|
||||
t.kind = 0; t.lat = w.lat_1e6; t.lon = w.lon_1e6;
|
||||
memset(t.key, 0, 6);
|
||||
snprintf(t.name, sizeof(t.name), "%s", w.label);
|
||||
}
|
||||
}
|
||||
|
||||
struct T { uint8_t kind; int idx; }; // kind 0=waypoint, 1=live contact (slot)
|
||||
T list[32]; int n = 0;
|
||||
for (int i = 0; i < wp.count() && n < 32; i++) list[n++] = { 0, i };
|
||||
for (int i = 0; i < LiveTrackStore::CAPACITY && n < 32; i++)
|
||||
if (lt.isActive(i, now) && lt.slotAt(i).verified) list[n++] = { 1, i };
|
||||
if (n == 0) { _task->showAlert("No waypoints / live", 1400); return; }
|
||||
|
||||
// Locate the current target in the combined list.
|
||||
int cur = -1;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (list[i].kind == 0 && _prefs->geo_alert_target_kind == 0) {
|
||||
const Waypoint& w = wp.at(list[i].idx);
|
||||
if (w.lat_1e6 == _prefs->geo_alert_lat_1e6 && w.lon_1e6 == _prefs->geo_alert_lon_1e6) { cur = i; break; }
|
||||
} else if (list[i].kind == 1 && _prefs->geo_alert_target_kind == 1) {
|
||||
if (memcmp(lt.slotAt(list[i].idx).key, _prefs->geo_alert_key, LiveTrackStore::KEY_LEN) == 0) { cur = i; break; }
|
||||
// Index of the currently-configured target within _targets, or -1.
|
||||
int currentTargetIndex() const {
|
||||
for (int i = 0; i < _target_n; i++) {
|
||||
if (_targets[i].kind == 1 && _prefs->geo_alert_target_kind == 1) {
|
||||
if (memcmp(_targets[i].key, _prefs->geo_alert_key, 6) == 0) return i;
|
||||
} else if (_targets[i].kind == 0 && _prefs->geo_alert_target_kind == 0) {
|
||||
if (_targets[i].lat == _prefs->geo_alert_lat_1e6 &&
|
||||
_targets[i].lon == _prefs->geo_alert_lon_1e6) return i;
|
||||
}
|
||||
}
|
||||
int nx = (cur < 0) ? (dir >= 0 ? 0 : n - 1) : ((cur + (dir >= 0 ? 1 : n - 1)) % n);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (list[nx].kind == 0) {
|
||||
const Waypoint& w = wp.at(list[nx].idx);
|
||||
_prefs->geo_alert_target_kind = 0;
|
||||
_prefs->geo_alert_lat_1e6 = w.lat_1e6;
|
||||
_prefs->geo_alert_lon_1e6 = w.lon_1e6;
|
||||
snprintf(_prefs->geo_alert_label, sizeof(_prefs->geo_alert_label), "%s", w.label);
|
||||
} else {
|
||||
const LiveTrackStore::Entry& e = lt.slotAt(list[nx].idx);
|
||||
_prefs->geo_alert_target_kind = 1;
|
||||
memcpy(_prefs->geo_alert_key, e.key, LiveTrackStore::KEY_LEN);
|
||||
_prefs->geo_alert_lat_1e6 = e.lat_1e6;
|
||||
_prefs->geo_alert_lon_1e6 = e.lon_1e6;
|
||||
snprintf(_prefs->geo_alert_label, sizeof(_prefs->geo_alert_label), "%s", e.name);
|
||||
}
|
||||
void applyTarget(const Target& t) {
|
||||
_prefs->geo_alert_target_kind = t.kind;
|
||||
if (t.kind == 1) memcpy(_prefs->geo_alert_key, t.key, 6);
|
||||
_prefs->geo_alert_lat_1e6 = t.lat;
|
||||
_prefs->geo_alert_lon_1e6 = t.lon;
|
||||
snprintf(_prefs->geo_alert_label, sizeof(_prefs->geo_alert_label), "%s", t.name);
|
||||
_prefs->geo_alert_has_target = 1;
|
||||
_dirty = true;
|
||||
_task->resetGeoAlert();
|
||||
}
|
||||
|
||||
// LEFT/RIGHT quick-cycle over the same set the picker shows.
|
||||
void cycleTarget(int dir) {
|
||||
buildTargets();
|
||||
if (_target_n == 0) { _task->showAlert("No favs / waypoints", 1400); return; }
|
||||
int cur = currentTargetIndex();
|
||||
int nx = (cur < 0) ? (dir >= 0 ? 0 : _target_n - 1)
|
||||
: ((cur + (dir >= 0 ? 1 : _target_n - 1)) % _target_n);
|
||||
applyTarget(_targets[nx]);
|
||||
}
|
||||
|
||||
void openPicker() {
|
||||
buildTargets();
|
||||
if (_target_n == 0) { _task->showAlert("No favs / waypoints", 1400); return; }
|
||||
int cur = currentTargetIndex();
|
||||
_pick_sel = (cur >= 0) ? cur : 0;
|
||||
_pick_scroll = 0;
|
||||
_picking = true;
|
||||
}
|
||||
|
||||
void renderPicker(DisplayDriver& display) {
|
||||
display.drawCenteredHeader("PICK TARGET");
|
||||
drawList(display, _target_n, _pick_sel, _pick_scroll, [&](int i, int y, bool sel, int reserve) {
|
||||
display.drawSelectionRow(0, y - 1, display.width() - reserve, display.lineStep() - 1, sel);
|
||||
char row[28];
|
||||
// '@' marks a person; a plain name is a waypoint.
|
||||
if (_targets[i].kind == 1) snprintf(row, sizeof(row), "@%s", _targets[i].name);
|
||||
else snprintf(row, sizeof(row), "%s", _targets[i].name);
|
||||
display.drawTextEllipsized(2, y, display.width() - 2 - reserve, row);
|
||||
});
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
// Target picker takes all input while open.
|
||||
if (_picking) {
|
||||
if (c == KEY_UP) { _pick_sel = (_pick_sel > 0) ? _pick_sel - 1 : _target_n - 1; return true; }
|
||||
if (c == KEY_DOWN) { _pick_sel = (_pick_sel < _target_n - 1) ? _pick_sel + 1 : 0; return true; }
|
||||
if (c == KEY_ENTER) { applyTarget(_targets[_pick_sel]); _picking = false; return true; }
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { _picking = false; return true; }
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) {
|
||||
if (_dirty) { the_mesh.savePrefs(); _dirty = false; } // engine re-seeded per edit
|
||||
_task->gotoToolsScreen();
|
||||
@@ -177,7 +249,10 @@ public:
|
||||
if (c == KEY_DOWN) { moveSel(+1); return true; }
|
||||
if (keyIsPrev(c)) { activate(-1); return true; }
|
||||
if (keyIsNext(c)) { activate(+1); return true; }
|
||||
if (c == KEY_ENTER) { activate(+1); return true; }
|
||||
if (c == KEY_ENTER) {
|
||||
if (rows(_sel).kind == K_TARGET) { openPicker(); return true; } // full list picker
|
||||
activate(+1); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user