mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-30 16:58:12 +00:00
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>
259 lines
10 KiB
C++
259 lines
10 KiB
C++
#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;
|
||
}
|
||
};
|