mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-08-01 17:56:12 +00:00
feat(companion): geo-alert geofence + trail auto-pause
Geo Alert (Tools › Geo Alert): a single geofence around a snapshotted waypoint. Beeps + alerts on crossing into (arrive) / out of (leave) the radius per mode, with edge hysteresis and silent first-eval seeding. Adds a proximity Beeper that ticks faster the closer to the target while inside the radius — independent of the discrete arrive/leave alert. Trail auto-pause (Trail › Settings): freezes the trail timer + sampling after the device sits still for a configurable delay, resuming on the next real movement. New paused state in TrailStore (distinct from Stop); Summary shows "paused". Persisted via new NodePrefs fields; schema bumped 0xC0DE0012 → 0xC0DE0014 with clamps so upgraders start with both features off. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1d2f2beaba
commit
408dfd494b
@@ -0,0 +1,171 @@
|
||||
#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().
|
||||
// Included by UITask.cpp after LiveShareScreen.h.
|
||||
|
||||
#include "../NodePrefs.h"
|
||||
#include "../Waypoint.h"
|
||||
#include "icons.h" // scrollIndicatorReserve / drawScrollIndicator
|
||||
|
||||
class GeoAlertScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
NodePrefs* _prefs;
|
||||
bool _dirty = false;
|
||||
int _sel = 0;
|
||||
int _scroll = 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; }
|
||||
|
||||
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 && _prefs->geo_alert_label[0])
|
||||
snprintf(buf, n, "%s", _prefs->geo_alert_label);
|
||||
else if (_prefs && _prefs->geo_alert_has_target)
|
||||
snprintf(buf, n, "(unnamed)");
|
||||
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);
|
||||
display.drawCenteredHeader("GEO ALERT");
|
||||
|
||||
const int y0 = display.listStart();
|
||||
const int step = display.lineStep();
|
||||
|
||||
int vis = (display.height() - y0) / step;
|
||||
if (vis < 1) vis = 1;
|
||||
if (_sel < _scroll) _scroll = _sel;
|
||||
if (_sel >= _scroll + vis) _scroll = _sel - vis + 1;
|
||||
if (_scroll < 0) _scroll = 0;
|
||||
|
||||
const int reserve = scrollIndicatorReserve(display, ROW_COUNT, vis);
|
||||
const int valx = display.width() / 2 + 6;
|
||||
|
||||
for (int i = _scroll; i < ROW_COUNT && i < _scroll + vis; i++) {
|
||||
int y = y0 + (i - _scroll) * step;
|
||||
Row r = rows(i);
|
||||
bool sel = (i == _sel);
|
||||
display.drawSelectionRow(0, y - 1, display.width() - reserve, step - 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);
|
||||
}
|
||||
|
||||
drawScrollIndicator(display, y0, vis * step, ROW_COUNT, vis, _scroll);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Cycle the target through the saved waypoints, snapshotting the chosen one's
|
||||
// coordinate + label into prefs so the alert is independent of later edits.
|
||||
void cycleTarget(int dir) {
|
||||
WaypointStore& wp = _task->waypoints();
|
||||
int total = wp.count();
|
||||
if (total == 0) { _task->showAlert("Mark a waypoint first", 1400); return; }
|
||||
|
||||
// Locate the current target among the waypoints (by coordinate match).
|
||||
int cur = -1;
|
||||
if (_prefs->geo_alert_has_target) {
|
||||
for (int i = 0; i < total; i++)
|
||||
if (wp.at(i).lat_1e6 == _prefs->geo_alert_lat_1e6 &&
|
||||
wp.at(i).lon_1e6 == _prefs->geo_alert_lon_1e6) { cur = i; break; }
|
||||
}
|
||||
int nx = (cur < 0) ? (dir >= 0 ? 0 : total - 1)
|
||||
: ((cur + (dir >= 0 ? 1 : total - 1)) % total);
|
||||
|
||||
const Waypoint& w = wp.at(nx);
|
||||
_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);
|
||||
_prefs->geo_alert_has_target = 1;
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) {
|
||||
if (_dirty) { the_mesh.savePrefs(); _task->resetGeoAlert(); _dirty = false; }
|
||||
_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) { activate(+1); return true; }
|
||||
return false;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user