refactor: rename Geo Alert feature to Locator

Pure rename, no behavior change: GeoAlertScreen -> LocatorScreen (file +
class), NodePrefs geo_alert_* fields -> locator_*, UITask engine functions
(evaluateGeoAlert/fireGeoAlert/geoAlertDistance/geoProximityBeeper) and state
-> evaluateLocator/fireLocator/locatorDistance/locatorProximityBeeper, Tools
menu entry and screen header text -> "Locator", plus matching docs/README
updates. NodePrefs field renames don't affect on-disk layout (DataStore
serializes by position, not name).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
MarekZegare4
2026-06-23 23:37:30 +02:00
parent 3c94aca602
commit 57f5346427
9 changed files with 155 additions and 155 deletions

View File

@@ -51,7 +51,7 @@ All firmware files are published on the [releases page](https://github.com/Marek
- [Screen Lock](./docs/solo_features/screen_lock/screen_lock.md) — lock the device to prevent accidental keypresses, with a lock screen showing time and sensor data
- [Tools Screen](./docs/solo_features/tools_screen/tools_screen.md) — GPS trail & waypoints, compass, nearby nodes (with ping & navigate), ringtone editor, auto-reply bot, auto-advert, live location sharing, geo-alert, diagnostics, repeater
- [Tools Screen](./docs/solo_features/tools_screen/tools_screen.md) — GPS trail & waypoints, compass, nearby nodes (with ping & navigate), ringtone editor, auto-reply bot, auto-advert, live location sharing, locator, diagnostics, repeater
- **Battery saving (radio)** — two optional, independent toggles under Settings Radio:
- **Pwr save** — hardware duty-cycle receive (SX126x `SetRxDutyCycle`): the radio cycles RX↔sleep on its own and wakes on a preamble, cutting average RX current with only a little added receive latency
@@ -99,7 +99,7 @@ Updating to a newer version usually does not require erasing flash unless the re
| [Clock Screen](./docs/solo_features/clock_screen/clock_screen.md) | Clock page, date, configurable data fields |
| [Settings Screen](./docs/solo_features/settings_screen/settings_screen.md) | All settings sections with values and interactions |
| [Screen Lock](./docs/solo_features/screen_lock/screen_lock.md) | Lock/unlock sequence, lock screen, auto-lock |
| [Tools Screen](./docs/solo_features/tools_screen/tools_screen.md) | GPS trail & waypoints, compass, navigation, nearby nodes, ringtone editor, auto-reply bot, auto-advert, live location sharing, geo-alert, diagnostics, repeater |
| [Tools Screen](./docs/solo_features/tools_screen/tools_screen.md) | GPS trail & waypoints, compass, navigation, nearby nodes, ringtone editor, auto-reply bot, auto-advert, live location sharing, locator, diagnostics, repeater |
### Upstream MeshCore

View File

@@ -8,7 +8,7 @@
| :-----------------------: | :-----------------------: |
| ![](./tls_scr_1_oled.png) | ![](./tls_scr_1_eink.png) |
The Tools screen is a hub for GPS trail recording, nearby node browsing, ringtone editing, auto-reply bot, auto-advert, live location sharing, geo-alert, compass, device diagnostics, and repeater mode. Tools are grouped into collapsible **Location** / **Comms** / **System** sections — the same fold-in-place model as Settings; Tools always opens folded back to the section list. Navigate with **UP/DOWN**, press **Enter** on a section header to expand or collapse it, or on a tool to open it.
The Tools screen is a hub for GPS trail recording, nearby node browsing, ringtone editing, auto-reply bot, auto-advert, live location sharing, locator, compass, device diagnostics, and repeater mode. Tools are grouped into collapsible **Location** / **Comms** / **System** sections — the same fold-in-place model as Settings; Tools always opens folded back to the section list. Navigate with **UP/DOWN**, press **Enter** on a section header to expand or collapse it, or on a tool to open it.
---
@@ -225,7 +225,7 @@ The tool holds both directions of sharing in one flat list. Navigate with **UP/D
---
## Geo Alert
## Locator
A single **geofence** that beeps and shows an alert when you cross **into** or **out of** a radius. The target can be a **saved waypoint** (a fixed place — "tell me when I'm back at camp") or a **live contact** (a person sharing their position via Live Share — "alert me when my friend gets near / falls behind"). A waypoint target is a **snapshot** (coordinate + label copied), so it keeps working even if you later edit or delete that waypoint; a contact target follows the person's latest shared position.
@@ -246,7 +246,7 @@ Navigate with **UP/DOWN**, change a value with **LEFT/RIGHT** (or **Enter**); **
**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.
> [!TIP]
> Mark the spot first with **Tools Trail → Hold Enter → Mark here** (or **+ Add by coords**), then set it as the Geo Alert target.
> Mark the spot first with **Tools Trail → Hold Enter → Mark here** (or **+ Add by coords**), then set it as the Locator target.
---

View File

@@ -376,29 +376,29 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
if (_prefs.loc_share_move_idx >= NodePrefs::LOC_SHARE_MOVE_COUNT) _prefs.loc_share_move_idx = 1;
if (_prefs.loc_share_interval_idx >= NodePrefs::LOC_SHARE_INTERVAL_COUNT) _prefs.loc_share_interval_idx = 1;
if (_prefs.loc_share_heartbeat_idx >= NodePrefs::LOC_SHARE_HEARTBEAT_COUNT) _prefs.loc_share_heartbeat_idx = 0;
// → 0xC0DE0013: geo-alert + trail auto-pause. Pre-0x13 files leave stray bytes
// → 0xC0DE0013: locator + trail auto-pause. Pre-0x13 files leave stray bytes
// here; clamp each back to its default so upgraders start with both off.
rd(&_prefs.geo_alert_enabled, sizeof(_prefs.geo_alert_enabled));
rd(&_prefs.geo_alert_has_target, sizeof(_prefs.geo_alert_has_target));
rd(&_prefs.geo_alert_radius_idx, sizeof(_prefs.geo_alert_radius_idx));
rd(&_prefs.geo_alert_mode, sizeof(_prefs.geo_alert_mode));
rd(&_prefs.geo_alert_lat_1e6, sizeof(_prefs.geo_alert_lat_1e6));
rd(&_prefs.geo_alert_lon_1e6, sizeof(_prefs.geo_alert_lon_1e6));
rd(_prefs.geo_alert_label, sizeof(_prefs.geo_alert_label));
rd(&_prefs.locator_enabled, sizeof(_prefs.locator_enabled));
rd(&_prefs.locator_has_target, sizeof(_prefs.locator_has_target));
rd(&_prefs.locator_radius_idx, sizeof(_prefs.locator_radius_idx));
rd(&_prefs.locator_mode, sizeof(_prefs.locator_mode));
rd(&_prefs.locator_lat_1e6, sizeof(_prefs.locator_lat_1e6));
rd(&_prefs.locator_lon_1e6, sizeof(_prefs.locator_lon_1e6));
rd(_prefs.locator_label, sizeof(_prefs.locator_label));
rd(&_prefs.trail_autopause_idx, sizeof(_prefs.trail_autopause_idx));
if (_prefs.geo_alert_enabled > 1) _prefs.geo_alert_enabled = 0;
if (_prefs.geo_alert_has_target > 1) _prefs.geo_alert_has_target = 0;
if (_prefs.geo_alert_radius_idx >= NodePrefs::GEO_ALERT_RADIUS_COUNT) _prefs.geo_alert_radius_idx = 1;
if (_prefs.geo_alert_mode >= NodePrefs::GEO_ALERT_MODE_COUNT) _prefs.geo_alert_mode = 0;
if (_prefs.locator_enabled > 1) _prefs.locator_enabled = 0;
if (_prefs.locator_has_target > 1) _prefs.locator_has_target = 0;
if (_prefs.locator_radius_idx >= NodePrefs::LOCATOR_RADIUS_COUNT) _prefs.locator_radius_idx = 1;
if (_prefs.locator_mode >= NodePrefs::LOCATOR_MODE_COUNT) _prefs.locator_mode = 0;
if (_prefs.trail_autopause_idx >= NodePrefs::TRAIL_AUTOPAUSE_COUNT) _prefs.trail_autopause_idx = 0;
_prefs.geo_alert_label[sizeof(_prefs.geo_alert_label) - 1] = '\0';
// → 0xC0DE0014: geo-alert proximity beeper.
rd(&_prefs.geo_alert_beeper, sizeof(_prefs.geo_alert_beeper));
if (_prefs.geo_alert_beeper > 1) _prefs.geo_alert_beeper = 0;
// → 0xC0DE0015: geo-alert can target a live contact (kind + pubkey prefix).
rd(&_prefs.geo_alert_target_kind, sizeof(_prefs.geo_alert_target_kind));
rd(_prefs.geo_alert_key, sizeof(_prefs.geo_alert_key));
if (_prefs.geo_alert_target_kind > 1) _prefs.geo_alert_target_kind = 0;
_prefs.locator_label[sizeof(_prefs.locator_label) - 1] = '\0';
// → 0xC0DE0014: locator proximity beeper.
rd(&_prefs.locator_beeper, sizeof(_prefs.locator_beeper));
if (_prefs.locator_beeper > 1) _prefs.locator_beeper = 0;
// → 0xC0DE0015: locator can target a live contact (kind + pubkey prefix).
rd(&_prefs.locator_target_kind, sizeof(_prefs.locator_target_kind));
rd(_prefs.locator_key, sizeof(_prefs.locator_key));
if (_prefs.locator_target_kind > 1) _prefs.locator_target_kind = 0;
// Pre-0x10 files leave stray sentinel bytes here, same as a never-configured
// device. Either way there's no valid saved profile, so default to a profile
// in the same band as the companion's own network (_prefs.freq, already read
@@ -586,17 +586,17 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
file.write((uint8_t *)&_prefs.loc_share_move_idx, sizeof(_prefs.loc_share_move_idx));
file.write((uint8_t *)&_prefs.loc_share_interval_idx, sizeof(_prefs.loc_share_interval_idx));
file.write((uint8_t *)&_prefs.loc_share_heartbeat_idx, sizeof(_prefs.loc_share_heartbeat_idx));
file.write((uint8_t *)&_prefs.geo_alert_enabled, sizeof(_prefs.geo_alert_enabled));
file.write((uint8_t *)&_prefs.geo_alert_has_target, sizeof(_prefs.geo_alert_has_target));
file.write((uint8_t *)&_prefs.geo_alert_radius_idx, sizeof(_prefs.geo_alert_radius_idx));
file.write((uint8_t *)&_prefs.geo_alert_mode, sizeof(_prefs.geo_alert_mode));
file.write((uint8_t *)&_prefs.geo_alert_lat_1e6, sizeof(_prefs.geo_alert_lat_1e6));
file.write((uint8_t *)&_prefs.geo_alert_lon_1e6, sizeof(_prefs.geo_alert_lon_1e6));
file.write((uint8_t *)_prefs.geo_alert_label, sizeof(_prefs.geo_alert_label));
file.write((uint8_t *)&_prefs.locator_enabled, sizeof(_prefs.locator_enabled));
file.write((uint8_t *)&_prefs.locator_has_target, sizeof(_prefs.locator_has_target));
file.write((uint8_t *)&_prefs.locator_radius_idx, sizeof(_prefs.locator_radius_idx));
file.write((uint8_t *)&_prefs.locator_mode, sizeof(_prefs.locator_mode));
file.write((uint8_t *)&_prefs.locator_lat_1e6, sizeof(_prefs.locator_lat_1e6));
file.write((uint8_t *)&_prefs.locator_lon_1e6, sizeof(_prefs.locator_lon_1e6));
file.write((uint8_t *)_prefs.locator_label, sizeof(_prefs.locator_label));
file.write((uint8_t *)&_prefs.trail_autopause_idx, sizeof(_prefs.trail_autopause_idx));
file.write((uint8_t *)&_prefs.geo_alert_beeper, sizeof(_prefs.geo_alert_beeper));
file.write((uint8_t *)&_prefs.geo_alert_target_kind, sizeof(_prefs.geo_alert_target_kind));
file.write((uint8_t *)_prefs.geo_alert_key, sizeof(_prefs.geo_alert_key));
file.write((uint8_t *)&_prefs.locator_beeper, sizeof(_prefs.locator_beeper));
file.write((uint8_t *)&_prefs.locator_target_kind, sizeof(_prefs.locator_target_kind));
file.write((uint8_t *)_prefs.locator_key, sizeof(_prefs.locator_key));
// Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL.
uint32_t sentinel = NodePrefs::SCHEMA_SENTINEL;

View File

@@ -79,7 +79,7 @@ public:
}
// Latest active position for a verified (DM/pubkey) share, or null if that
// node hasn't shared recently. Used by the geo-alert engine to follow a
// node hasn't shared recently. Used by the locator engine to follow a
// moving contact.
const Entry* activeByKey(const uint8_t* key, uint32_t now) const {
if (!key) return nullptr;

View File

@@ -235,34 +235,34 @@ struct NodePrefs { // persisted to file
uint8_t loc_share_interval_idx; // min send interval (index into locShareIntervalSecs)
uint8_t loc_share_heartbeat_idx; // stationary heartbeat (index into locShareHeartbeatSecs)
// Geo-alert — a single geofence around a saved point. When enabled the device
// Locator — a single geofence around a saved point. When enabled the device
// watches its own GPS fix and beeps + shows an alert when it crosses into
// (arrive) or out of (leave) the radius. The target coordinate/label is a
// snapshot of a chosen waypoint, so the alert survives the waypoint being
// edited or deleted. Configured from Tools Geo Alert.
uint8_t geo_alert_enabled; // 0=off (default), 1=armed
uint8_t geo_alert_has_target; // 0=no target chosen yet, 1=target set
uint8_t geo_alert_radius_idx; // index into geoAlertRadiusMeters
uint8_t geo_alert_mode; // 0=arrive, 1=leave, 2=both
int32_t geo_alert_lat_1e6; // target latitude (1e6-scaled; last-known for a contact)
int32_t geo_alert_lon_1e6; // target longitude (1e6-scaled; last-known for a contact)
char geo_alert_label[12]; // target name for the alert text (WAYPOINT_LABEL_LEN)
// edited or deleted. Configured from Tools Locator.
uint8_t locator_enabled; // 0=off (default), 1=armed
uint8_t locator_has_target; // 0=no target chosen yet, 1=target set
uint8_t locator_radius_idx; // index into locatorRadiusMeters
uint8_t locator_mode; // 0=arrive, 1=leave, 2=both
int32_t locator_lat_1e6; // target latitude (1e6-scaled; last-known for a contact)
int32_t locator_lon_1e6; // target longitude (1e6-scaled; last-known for a contact)
char locator_label[12]; // target name for the alert text (WAYPOINT_LABEL_LEN)
// Target can be a static waypoint or a live contact: for a contact the engine
// re-reads the latest [LOC] position each evaluation (keyed by pubkey prefix),
// so the geofence follows a moving person ("alert when my friend is near").
uint8_t geo_alert_target_kind; // 0=waypoint (static), 1=live contact
uint8_t geo_alert_key[6]; // contact pubkey prefix when target_kind==1
uint8_t locator_target_kind; // 0=waypoint (static), 1=live contact
uint8_t locator_key[6]; // contact pubkey prefix when target_kind==1
// Trail auto-pause — when tracking, automatically freeze the trail (timer +
// sampling) after the device has sat still for this long, and resume on the
// next real movement. 0 = off. Index into trailAutoPauseSecs.
uint8_t trail_autopause_idx;
// Geo-alert proximity beeper — when on (and the alert is armed with a target),
// Locator proximity beeper — when on (and the alert is armed with a target),
// the device ticks while inside the radius and shortens the gap between ticks
// the closer it gets to the target, like a homing beeper. Independent of the
// discrete arrive/leave alert (geo_alert_mode).
uint8_t geo_alert_beeper; // 0=off (default), 1=on
// discrete arrive/leave alert (locator_mode).
uint8_t locator_beeper; // 0=off (default), 1=on
// Single source of truth for the live-share option tables (shared by the Map
// UI labels and the auto-send engine in UITask).
@@ -282,17 +282,17 @@ struct NodePrefs { // persisted to file
return H[idx < LOC_SHARE_HEARTBEAT_COUNT ? idx : 0];
}
// Geo-alert option tables (shared by the Tools Geo Alert UI labels and the
// Locator option tables (shared by the Tools Locator UI labels and the
// evaluation engine in UITask).
static const uint8_t GEO_ALERT_RADIUS_COUNT = 5;
static uint16_t geoAlertRadiusMeters(uint8_t idx) {
static const uint16_t R[GEO_ALERT_RADIUS_COUNT] = { 50, 100, 250, 500, 1000 };
return R[idx < GEO_ALERT_RADIUS_COUNT ? idx : 1];
static const uint8_t LOCATOR_RADIUS_COUNT = 5;
static uint16_t locatorRadiusMeters(uint8_t idx) {
static const uint16_t R[LOCATOR_RADIUS_COUNT] = { 50, 100, 250, 500, 1000 };
return R[idx < LOCATOR_RADIUS_COUNT ? idx : 1];
}
static const uint8_t GEO_ALERT_MODE_COUNT = 3; // 0=arrive, 1=leave, 2=both
static const char* geoAlertModeLabel(uint8_t m) {
static const char* L[GEO_ALERT_MODE_COUNT] = { "Arrive", "Leave", "Both" };
return L[m < GEO_ALERT_MODE_COUNT ? m : 0];
static const uint8_t LOCATOR_MODE_COUNT = 3; // 0=arrive, 1=leave, 2=both
static const char* locatorModeLabel(uint8_t m) {
static const char* L[LOCATOR_MODE_COUNT] = { "Arrive", "Leave", "Both" };
return L[m < LOCATOR_MODE_COUNT ? m : 0];
}
// Trail auto-pause delays (seconds). 0 = off.

View File

@@ -1,11 +1,11 @@
#pragma once
// Geo-alert config tool. Tools Geo Alert.
// Locator config tool. Tools Locator.
// 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
// lives in UITask::evaluateLocator(). 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.
@@ -15,7 +15,7 @@
#include "../LiveTrack.h"
#include "icons.h" // drawList (shared scrolling-list helper)
class GeoAlertScreen : public UIScreen {
class LocatorScreen : public UIScreen {
UITask* _task;
NodePrefs* _prefs;
bool _dirty = false;
@@ -46,36 +46,36 @@ class GeoAlertScreen : public UIScreen {
}
public:
GeoAlertScreen(UITask* task, NodePrefs* prefs) : _task(task), _prefs(prefs) {}
LocatorScreen(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");
snprintf(buf, n, "%s", (_prefs && _prefs->locator_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)";
if (_prefs && _prefs->locator_has_target) {
const char* nm = _prefs->locator_label[0] ? _prefs->locator_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);
if (_prefs->locator_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);
uint16_t r = NodePrefs::locatorRadiusMeters(_prefs ? _prefs->locator_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));
snprintf(buf, n, "%s", NodePrefs::locatorModeLabel(_prefs ? _prefs->locator_mode : 0));
break;
case K_BEEPER:
snprintf(buf, n, "%s", (_prefs && _prefs->geo_alert_beeper) ? "ON" : "OFF");
snprintf(buf, n, "%s", (_prefs && _prefs->locator_beeper) ? "ON" : "OFF");
break;
default: buf[0] = '\0';
}
@@ -85,7 +85,7 @@ public:
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT);
if (_picking) { renderPicker(display); return 400; }
display.drawCenteredHeader("GEO ALERT");
display.drawCenteredHeader("LOCATOR");
const int valx = display.width() / 2 + 6;
drawList(display, ROW_COUNT, _sel, _scroll, [&](int i, int y, bool sel, int reserve) {
@@ -106,8 +106,8 @@ public:
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)
_prefs->locator_enabled ^= 1;
if (_prefs->locator_enabled && !_prefs->locator_has_target)
_task->showAlert("Pick a target", 1200);
_dirty = true;
break;
@@ -115,23 +115,23 @@ public:
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);
_prefs->locator_radius_idx = (uint8_t)((_prefs->locator_radius_idx
+ (dir >= 0 ? 1 : NodePrefs::LOCATOR_RADIUS_COUNT - 1)) % NodePrefs::LOCATOR_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);
_prefs->locator_mode = (uint8_t)((_prefs->locator_mode
+ (dir >= 0 ? 1 : NodePrefs::LOCATOR_MODE_COUNT - 1)) % NodePrefs::LOCATOR_MODE_COUNT);
_dirty = true;
break;
case K_BEEPER:
_prefs->geo_alert_beeper ^= 1;
_prefs->locator_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();
_task->resetLocator();
}
// Build the selectable target set into _targets: favourites first (the quick
@@ -179,25 +179,25 @@ public:
// 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;
if (_targets[i].kind == 1 && _prefs->locator_target_kind == 1) {
if (memcmp(_targets[i].key, _prefs->locator_key, 6) == 0) return i;
} else if (_targets[i].kind == 0 && _prefs->locator_target_kind == 0) {
if (_targets[i].lat == _prefs->locator_lat_1e6 &&
_targets[i].lon == _prefs->locator_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;
_prefs->locator_target_kind = t.kind;
if (t.kind == 1) memcpy(_prefs->locator_key, t.key, 6);
_prefs->locator_lat_1e6 = t.lat;
_prefs->locator_lon_1e6 = t.lon;
snprintf(_prefs->locator_label, sizeof(_prefs->locator_label), "%s", t.name);
_prefs->locator_has_target = 1;
_dirty = true;
_task->resetGeoAlert();
_task->resetLocator();
}
// LEFT/RIGHT quick-cycle over the same set the picker shows.

View File

@@ -13,7 +13,7 @@ class ToolsScreen : public UIScreen {
UITask* _task;
enum Action {
ACT_NEARBY, ACT_LIVESHARE, ACT_TRAIL, ACT_GEOALERT, ACT_COMPASS,
ACT_NEARBY, ACT_LIVESHARE, ACT_TRAIL, ACT_LOCATOR, ACT_COMPASS,
ACT_BOT, ACT_AUTOADVERT, ACT_REPEATER,
ACT_RINGTONE, ACT_DIAGNOSTICS
};
@@ -44,7 +44,7 @@ class ToolsScreen : public UIScreen {
case ACT_NEARBY: _task->gotoNearbyScreen(); break;
case ACT_LIVESHARE: _task->gotoLiveShareScreen(); break;
case ACT_TRAIL: _task->gotoTrailScreen(); break;
case ACT_GEOALERT: _task->gotoGeoAlertScreen(); break;
case ACT_LOCATOR: _task->gotoLocatorScreen(); break;
case ACT_COMPASS: _task->gotoCompassScreen(); break;
case ACT_BOT: _task->gotoBotScreen(); break;
case ACT_AUTOADVERT: _task->gotoAutoAdvertScreen(); break;
@@ -113,7 +113,7 @@ const ToolsScreen::Tool ToolsScreen::LOCATION_TOOLS[] = {
{ "Nearby Nodes", &ICON_MAP_CONTACT, ACT_NEARBY },
{ "Live Share", &ICON_ADVERT, ACT_LIVESHARE },
{ "Trail", &ICON_TRAIL, ACT_TRAIL },
{ "Geo Alert", &ICON_MAP_WAYPOINT, ACT_GEOALERT },
{ "Locator", &ICON_MAP_WAYPOINT, ACT_LOCATOR },
{ "Compass", &ICON_MAP_NORTH, ACT_COMPASS },
};
const ToolsScreen::Tool ToolsScreen::COMMS_TOOLS[] = {

View File

@@ -124,7 +124,7 @@ static const int QUICK_MSGS_MAX = 10;
#include "DashboardConfigScreen.h"
#include "AutoAdvertScreen.h"
#include "LiveShareScreen.h"
#include "GeoAlertScreen.h"
#include "LocatorScreen.h"
#include "TrailScreen.h"
#include "CompassScreen.h"
#include "DiagnosticsScreen.h"
@@ -1423,7 +1423,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
dashboard_config = new DashboardConfigScreen(this, node_prefs);
auto_advert_screen = new AutoAdvertScreen(this, node_prefs);
live_share_screen = new LiveShareScreen(this, node_prefs);
geo_alert_screen = new GeoAlertScreen(this, node_prefs);
locator_screen = new LocatorScreen(this, node_prefs);
trail_screen = new TrailScreen(this, &_trail);
compass_screen = new CompassScreen(this);
diag_screen = new DiagnosticsScreen(this);
@@ -1494,9 +1494,9 @@ void UITask::gotoLiveShareScreen() {
setCurrScreen(live_share_screen);
}
void UITask::gotoGeoAlertScreen() {
((GeoAlertScreen*)geo_alert_screen)->enter();
setCurrScreen(geo_alert_screen);
void UITask::gotoLocatorScreen() {
((LocatorScreen*)locator_screen)->enter();
setCurrScreen(locator_screen);
}
void UITask::gotoAutoAdvertScreen() {
@@ -2240,70 +2240,70 @@ void UITask::loop() {
}
}
// Geo-alert — beep + alert when the device crosses into / out of the armed
// Locator — beep + alert when the device crosses into / out of the armed
// geofence. Cheap; a few seconds of latency at the boundary is fine.
if ((int32_t)(millis() - _next_geo_alert_ms) >= 0) {
_next_geo_alert_ms = millis() + 3000UL;
evaluateGeoAlert();
if ((int32_t)(millis() - _next_locator_ms) >= 0) {
_next_locator_ms = millis() + 3000UL;
evaluateLocator();
}
// Geo-alert proximity beeper — ticks faster the closer to the target. Runs on
// Locator proximity beeper — ticks faster the closer to the target. Runs on
// its own short cadence (the crossing check above is too coarse for this).
geoProximityBeeper();
locatorProximityBeeper();
}
// Evaluate the single geofence against the current GPS fix. Crossing the radius
// fires fireGeoAlert() according to the configured mode; a hysteresis band on
// fires fireLocator() according to the configured mode; a hysteresis band on
// the "leave" edge stops it chattering at the boundary, and the first reading
// after arming only seeds the inside/outside state (no spurious alert).
// Distance (m) from the current GPS fix to the geo-alert target, plus the
// Distance (m) from the current GPS fix to the locator target, plus the
// configured radius (m). Returns false when no target is set or there's no fix
// — the single place the target-distance maths lives, shared by the crossing
// evaluator and the proximity beeper.
bool UITask::geoAlertDistance(float& dist_m, float& radius_m) const {
if (!_node_prefs || !_node_prefs->geo_alert_has_target) return false;
bool UITask::locatorDistance(float& dist_m, float& radius_m) const {
if (!_node_prefs || !_node_prefs->locator_has_target) return false;
int32_t tlat, tlon;
if (_node_prefs->geo_alert_target_kind == 1) {
if (_node_prefs->locator_target_kind == 1) {
// Live contact: follow the latest [LOC] position. No recent share → no
// distance to evaluate (engine waits rather than using a stale fix).
const LiveTrackStore::Entry* e =
_livetrack.activeByKey(_node_prefs->geo_alert_key, (uint32_t)rtc_clock.getCurrentTime());
_livetrack.activeByKey(_node_prefs->locator_key, (uint32_t)rtc_clock.getCurrentTime());
if (!e) return false;
tlat = e->lat_1e6; tlon = e->lon_1e6;
} else {
tlat = _node_prefs->geo_alert_lat_1e6; tlon = _node_prefs->geo_alert_lon_1e6;
tlat = _node_prefs->locator_lat_1e6; tlon = _node_prefs->locator_lon_1e6;
}
int32_t lat, lon;
if (!currentLocation(lat, lon)) return false;
dist_m = geo::haversineKm(lat, lon, tlat, tlon) * 1000.0f;
radius_m = (float)NodePrefs::geoAlertRadiusMeters(_node_prefs->geo_alert_radius_idx);
radius_m = (float)NodePrefs::locatorRadiusMeters(_node_prefs->locator_radius_idx);
return true;
}
void UITask::evaluateGeoAlert() {
if (!_node_prefs || !_node_prefs->geo_alert_enabled || !_node_prefs->geo_alert_has_target) {
_geo_alert_known = false;
void UITask::evaluateLocator() {
if (!_node_prefs || !_node_prefs->locator_enabled || !_node_prefs->locator_has_target) {
_locator_known = false;
return;
}
float dist, r;
if (!geoAlertDistance(dist, r)) return; // armed but no fix yet — keep state
if (!locatorDistance(dist, r)) return; // armed but no fix yet — keep state
bool inside;
if (!_geo_alert_known) inside = dist <= r; // seed state
else if (_geo_alert_inside) inside = dist <= r * 1.25f; // leave past band
if (!_locator_known) inside = dist <= r; // seed state
else if (_locator_inside) inside = dist <= r * 1.25f; // leave past band
else inside = dist <= r; // arrive at edge
if (_geo_alert_known && inside != _geo_alert_inside) {
uint8_t mode = _node_prefs->geo_alert_mode; // 0=arrive,1=leave,2=both
if (_locator_known && inside != _locator_inside) {
uint8_t mode = _node_prefs->locator_mode; // 0=arrive,1=leave,2=both
bool fire = inside ? (mode == 0 || mode == 2) : (mode == 1 || mode == 2);
if (fire) fireGeoAlert(inside);
if (fire) fireLocator(inside);
}
_geo_alert_inside = inside;
_geo_alert_known = true;
_locator_inside = inside;
_locator_known = true;
}
void UITask::fireGeoAlert(bool arrived) {
const char* lbl = _node_prefs->geo_alert_label[0] ? _node_prefs->geo_alert_label : "target";
bool person = _node_prefs->geo_alert_target_kind == 1;
void UITask::fireLocator(bool arrived) {
const char* lbl = _node_prefs->locator_label[0] ? _node_prefs->locator_label : "target";
bool person = _node_prefs->locator_target_kind == 1;
char msg[40];
// "Near/Away" reads naturally for a moving person; "Arrived/Left" for a place.
snprintf(msg, sizeof(msg),
@@ -2311,37 +2311,37 @@ void UITask::fireGeoAlert(bool arrived) {
: (person ? "Away: %s" : "Left: %s"), lbl);
showAlert(msg, 3000);
if (!isBuzzerQuiet())
playMelody(arrived ? "geoarr:d=8,o=6,b=140:c,e,g" : "geolv:d=8,o=6,b=140:g,e,c");
playMelody(arrived ? "locarr:d=8,o=6,b=140:c,e,g" : "loclv:d=8,o=6,b=140:g,e,c");
}
// Homing beeper: while armed with a target and inside the radius, emit a short
// tick whose interval shrinks linearly with distance — slow at the edge, rapid
// near the centre. Polls distance a few times a second; silent outside the
// radius. The beeper has its own toggle (geo_alert_beeper), so turning it on is
// radius. The beeper has its own toggle (locator_beeper), so turning it on is
// an explicit "I want to hear this" — it deliberately overrides the global
// buzzer mute (playMelody → buzzer.playForced ignores the quiet flag).
void UITask::geoProximityBeeper() {
void UITask::locatorProximityBeeper() {
static const uint32_t BEEP_MIN_MS = 150; // fastest cadence (at the target)
static const uint32_t BEEP_MAX_MS = 2000; // slowest cadence (at the edge)
if (!_node_prefs || !_node_prefs->geo_alert_enabled || !_node_prefs->geo_alert_beeper
|| !_node_prefs->geo_alert_has_target) {
if (!_node_prefs || !_node_prefs->locator_enabled || !_node_prefs->locator_beeper
|| !_node_prefs->locator_has_target) {
return;
}
if ((int32_t)(millis() - _geo_beep_check_ms) < 0) return;
_geo_beep_check_ms = millis() + 250UL;
if ((int32_t)(millis() - _locator_beep_check_ms) < 0) return;
_locator_beep_check_ms = millis() + 250UL;
float dist, r;
if (!geoAlertDistance(dist, r)) return;
if (!locatorDistance(dist, r)) return;
if (dist > r) { // outside the zone: stay quiet, beep on re-entry
_geo_beep_next_ms = millis();
_locator_beep_next_ms = millis();
return;
}
if ((int32_t)(millis() - _geo_beep_next_ms) < 0) return;
if ((int32_t)(millis() - _locator_beep_next_ms) < 0) return;
float frac = (r > 0) ? dist / r : 0; // 0 at centre, 1 at edge
if (frac < 0) frac = 0; else if (frac > 1) frac = 1;
uint32_t interval = BEEP_MIN_MS + (uint32_t)(frac * (BEEP_MAX_MS - BEEP_MIN_MS));
playMelody("geop:d=32,o=7,b=200:c");
_geo_beep_next_ms = millis() + interval;
playMelody("locp:d=32,o=7,b=200:c");
_locator_beep_next_ms = millis() + interval;
}
// Insert a GPS fix into the course-over-ground ring, rejecting gross outliers

View File

@@ -79,7 +79,7 @@ class UITask : public AbstractUITask {
UIScreen* dashboard_config;
UIScreen* auto_advert_screen;
UIScreen* live_share_screen;
UIScreen* geo_alert_screen;
UIScreen* locator_screen;
UIScreen* trail_screen;
UIScreen* compass_screen;
UIScreen* diag_screen;
@@ -106,20 +106,20 @@ class UITask : public AbstractUITask {
bool _trail_pause_has_ref = false;
uint32_t _trail_last_move_ms = 0;
// Geo-alert engine state. _geo_alert_known guards the first evaluation after
// Locator engine state. _locator_known guards the first evaluation after
// arming (initialise inside/outside silently, fire only on later crossings).
uint32_t _next_geo_alert_ms = 0;
bool _geo_alert_inside = false;
bool _geo_alert_known = false;
uint32_t _next_locator_ms = 0;
bool _locator_inside = false;
bool _locator_known = false;
// Proximity beeper: ticks while inside the radius, faster the nearer the
// target. _geo_beep_check_ms throttles the distance poll; _geo_beep_next_ms
// target. _locator_beep_check_ms throttles the distance poll; _locator_beep_next_ms
// is when the next tick is due.
uint32_t _geo_beep_check_ms = 0;
uint32_t _geo_beep_next_ms = 0;
bool geoAlertDistance(float& dist_m, float& radius_m) const;
void evaluateGeoAlert();
void fireGeoAlert(bool arrived);
void geoProximityBeeper();
uint32_t _locator_beep_check_ms = 0;
uint32_t _locator_beep_next_ms = 0;
bool locatorDistance(float& dist_m, float& radius_m) const;
void evaluateLocator();
void fireLocator(bool arrived);
void locatorProximityBeeper();
// Course-over-ground ring — a heading source independent of trail recording.
// Filled from the same periodic GPS poll regardless of _trail.isActive().
@@ -189,11 +189,11 @@ public:
void gotoDashboardConfig();
void gotoAutoAdvertScreen();
void gotoLiveShareScreen();
void gotoGeoAlertScreen();
// Re-arm the geo-alert state machine so the next evaluation initialises
// silently (called by the Geo Alert tool after the target/radius changes,
void gotoLocatorScreen();
// Re-arm the locator state machine so the next evaluation initialises
// silently (called by the Locator tool after the target/radius changes,
// so re-entering the zone doesn't fire on a stale inside/outside state).
void resetGeoAlert() { _geo_alert_known = false; }
void resetLocator() { _locator_known = false; }
void gotoTrailScreen();
void gotoMapScreen(); // opens the Trail screen directly in its Map view
void gotoCompassScreen();