mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-29 00:08:12 +00:00
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>
172 lines
6.0 KiB
C++
172 lines
6.0 KiB
C++
#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;
|
||
}
|
||
};
|