Files
MeshCore-Solo/examples/companion_radio/ui-new/GeoAlertScreen.h
MarekZegare4 440a222e4b 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>
2026-06-22 13:45:47 +02:00

259 lines
10 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#pragma once
// Geo-alert config tool. Tools Geo Alert.
// 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"
#include "../Waypoint.h"
#include "../LiveTrack.h"
#include "icons.h" // drawList (shared scrolling-list helper)
class GeoAlertScreen : public UIScreen {
UITask* _task;
NodePrefs* _prefs;
bool _dirty = false;
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;
static Row rows(int i) {
static const Row R[ROW_COUNT] = {
{ K_ENABLE, "Alert" },
{ K_TARGET, "Target" },
{ K_RADIUS, "Radius" },
{ K_MODE, "Mode" },
{ K_BEEPER, "Beeper" },
};
return R[i];
}
public:
GeoAlertScreen(UITask* task, NodePrefs* prefs) : _task(task), _prefs(prefs) {}
void enter() { _dirty = false; _sel = 0; _scroll = 0; _picking = false; }
void valueLabel(Kind k, char* buf, int n) {
switch (k) {
case K_ENABLE:
snprintf(buf, n, "%s", (_prefs && _prefs->geo_alert_enabled) ? "ON" : "OFF");
break;
case K_TARGET:
if (_prefs && _prefs->geo_alert_has_target) {
const char* nm = _prefs->geo_alert_label[0] ? _prefs->geo_alert_label : "(unnamed)";
// '@' prefix marks a live contact target (a moving person) vs a waypoint.
if (_prefs->geo_alert_target_kind == 1) snprintf(buf, n, "@%s", nm);
else snprintf(buf, n, "%s", nm);
} else {
snprintf(buf, n, "none");
}
break;
case K_RADIUS: {
uint16_t r = NodePrefs::geoAlertRadiusMeters(_prefs ? _prefs->geo_alert_radius_idx : 1);
if (r < 1000) snprintf(buf, n, "%um", (unsigned)r);
else snprintf(buf, n, "%.1fkm", r / 1000.0f);
break;
}
case K_MODE:
snprintf(buf, n, "%s", NodePrefs::geoAlertModeLabel(_prefs ? _prefs->geo_alert_mode : 0));
break;
case K_BEEPER:
snprintf(buf, n, "%s", (_prefs && _prefs->geo_alert_beeper) ? "ON" : "OFF");
break;
default: buf[0] = '\0';
}
}
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;
drawList(display, ROW_COUNT, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
Row r = rows(i);
display.drawSelectionRow(0, y - 1, display.width() - reserve, display.lineStep() - 1, sel);
display.setCursor(4, y);
display.print(r.label);
char val[24];
valueLabel(r.kind, val, sizeof(val));
if (val[0]) display.drawTextEllipsized(valx, y, display.width() - valx - reserve, val);
});
return 500;
}
void moveSel(int dir) { _sel = (_sel + dir + ROW_COUNT) % ROW_COUNT; }
void activate(int dir) {
if (!_prefs) return;
switch (rows(_sel).kind) {
case K_ENABLE:
_prefs->geo_alert_enabled ^= 1;
if (_prefs->geo_alert_enabled && !_prefs->geo_alert_has_target)
_task->showAlert("Pick a target", 1200);
_dirty = true;
break;
case K_TARGET:
cycleTarget(dir);
break;
case K_RADIUS:
_prefs->geo_alert_radius_idx = (uint8_t)((_prefs->geo_alert_radius_idx
+ (dir >= 0 ? 1 : NodePrefs::GEO_ALERT_RADIUS_COUNT - 1)) % NodePrefs::GEO_ALERT_RADIUS_COUNT);
_dirty = true;
break;
case K_MODE:
_prefs->geo_alert_mode = (uint8_t)((_prefs->geo_alert_mode
+ (dir >= 0 ? 1 : NodePrefs::GEO_ALERT_MODE_COUNT - 1)) % NodePrefs::GEO_ALERT_MODE_COUNT);
_dirty = true;
break;
case K_BEEPER:
_prefs->geo_alert_beeper ^= 1;
_dirty = true;
break;
}
// Re-seed the crossing engine after any change so editing the target/radius
// while armed can't fire a stale arrive/leave before the screen is closed.
_task->resetGeoAlert();
}
// 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);
}
}
// 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;
}
}
return -1;
}
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();
return true;
}
if (c == KEY_UP) { moveSel(-1); return true; }
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) {
if (rows(_sel).kind == K_TARGET) { openPicker(); return true; } // full list picker
activate(+1); return true;
}
return false;
}
};