mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-29 08:18:12 +00:00
feat(companion): follow live contacts — nav, geo-alert, ETA, quick-share
Builds on live location sharing: - Nearby "Navigate" now follows a moving live target: the navigate view re-reads the shared position every second and re-locates the entry by name for non-contact senders, so it no longer freezes or drops out (previously only refreshed for stored contacts entered via detail). - NavView gains a closing-speed / ETA line (EtaTracker), shown in the Nearby navigate view while approaching. - Geo Alert target can be a live contact, not just a waypoint: the engine re-reads the contact's latest [LOC] position each evaluation (keyed by pubkey prefix) so the geofence follows a moving person; alert wording switches to Near/Away. The picker cycles waypoints + verified live contacts (shown with an '@' prefix). Schema bumped 0xC0DE0014 -> 0x15. - Nearby marks a position-broadcasting node with the map's ♦ diamond (was a cryptic '*'/'~' prefix) and spells out "Sharing pos:" in detail. - Home Map page Hold-Enter is a one-shot position share: pushes to the Live Share target while auto-sharing is on, else opens the picker. Verified: WioTrackerL1_companion_solo_dual builds clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -394,6 +394,10 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
// → 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;
|
||||
// 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
|
||||
@@ -590,6 +594,8 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
|
||||
file.write((uint8_t *)_prefs.geo_alert_label, sizeof(_prefs.geo_alert_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));
|
||||
|
||||
// Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL.
|
||||
uint32_t sentinel = NodePrefs::SCHEMA_SENTINEL;
|
||||
|
||||
@@ -78,6 +78,19 @@ public:
|
||||
return n;
|
||||
}
|
||||
|
||||
// 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
|
||||
// moving contact.
|
||||
const Entry* activeByKey(const uint8_t* key, uint32_t now) const {
|
||||
if (!key) return nullptr;
|
||||
for (int i = 0; i < CAPACITY; i++) {
|
||||
if (!isActive(i, now)) continue;
|
||||
const Entry& e = _e[i];
|
||||
if (e.verified && memcmp(e.key, key, KEY_LEN) == 0) return &e;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
Entry _e[CAPACITY] = {};
|
||||
|
||||
|
||||
@@ -244,9 +244,14 @@ struct NodePrefs { // persisted to file
|
||||
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)
|
||||
int32_t geo_alert_lon_1e6; // target longitude (1e6-scaled)
|
||||
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)
|
||||
// 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
|
||||
|
||||
// Trail auto-pause — when tracking, automatically freeze the trail (timer +
|
||||
// sampling) after the device has sat still for this long, and resume on the
|
||||
@@ -310,7 +315,7 @@ struct NodePrefs { // persisted to file
|
||||
// adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so
|
||||
// older saves are detected on load and skipped (zero-init defaults kept).
|
||||
// High 24 bits identify the file format; low byte is the schema revision.
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE0014;
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE0015;
|
||||
|
||||
// Bit-index for each home page. Used by page_order (entries store bit+1) and
|
||||
// by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "../NodePrefs.h"
|
||||
#include "../Waypoint.h"
|
||||
#include "../LiveTrack.h"
|
||||
#include "icons.h" // drawList (shared scrolling-list helper)
|
||||
|
||||
class GeoAlertScreen : public UIScreen {
|
||||
@@ -43,12 +44,14 @@ public:
|
||||
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
|
||||
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);
|
||||
@@ -118,27 +121,48 @@ public:
|
||||
_task->resetGeoAlert();
|
||||
}
|
||||
|
||||
// Cycle the target through the saved waypoints, snapshotting the chosen one's
|
||||
// coordinate + label into prefs so the alert is independent of later edits.
|
||||
// Cycle the target through saved waypoints (static) and verified live contacts
|
||||
// (a person sharing [LOC] over a DM), snapshotting the chosen one into prefs.
|
||||
// A waypoint stores coord+label; a contact stores the pubkey prefix so the
|
||||
// engine can follow its live position, plus the name and last-known coord.
|
||||
void cycleTarget(int dir) {
|
||||
WaypointStore& wp = _task->waypoints();
|
||||
int total = wp.count();
|
||||
if (total == 0) { _task->showAlert("Mark a waypoint first", 1400); return; }
|
||||
WaypointStore& wp = _task->waypoints();
|
||||
LiveTrackStore& lt = _task->liveTrack();
|
||||
uint32_t now = rtc_clock.getCurrentTime();
|
||||
|
||||
// Locate the current target among the waypoints (by coordinate match).
|
||||
struct T { uint8_t kind; int idx; }; // kind 0=waypoint, 1=live contact (slot)
|
||||
T list[32]; int n = 0;
|
||||
for (int i = 0; i < wp.count() && n < 32; i++) list[n++] = { 0, i };
|
||||
for (int i = 0; i < LiveTrackStore::CAPACITY && n < 32; i++)
|
||||
if (lt.isActive(i, now) && lt.slotAt(i).verified) list[n++] = { 1, i };
|
||||
if (n == 0) { _task->showAlert("No waypoints / live", 1400); return; }
|
||||
|
||||
// Locate the current target in the combined list.
|
||||
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; }
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (list[i].kind == 0 && _prefs->geo_alert_target_kind == 0) {
|
||||
const Waypoint& w = wp.at(list[i].idx);
|
||||
if (w.lat_1e6 == _prefs->geo_alert_lat_1e6 && w.lon_1e6 == _prefs->geo_alert_lon_1e6) { cur = i; break; }
|
||||
} else if (list[i].kind == 1 && _prefs->geo_alert_target_kind == 1) {
|
||||
if (memcmp(lt.slotAt(list[i].idx).key, _prefs->geo_alert_key, LiveTrackStore::KEY_LEN) == 0) { cur = i; break; }
|
||||
}
|
||||
}
|
||||
int nx = (cur < 0) ? (dir >= 0 ? 0 : total - 1)
|
||||
: ((cur + (dir >= 0 ? 1 : total - 1)) % total);
|
||||
int nx = (cur < 0) ? (dir >= 0 ? 0 : n - 1) : ((cur + (dir >= 0 ? 1 : n - 1)) % n);
|
||||
|
||||
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);
|
||||
if (list[nx].kind == 0) {
|
||||
const Waypoint& w = wp.at(list[nx].idx);
|
||||
_prefs->geo_alert_target_kind = 0;
|
||||
_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);
|
||||
} else {
|
||||
const LiveTrackStore::Entry& e = lt.slotAt(list[nx].idx);
|
||||
_prefs->geo_alert_target_kind = 1;
|
||||
memcpy(_prefs->geo_alert_key, e.key, LiveTrackStore::KEY_LEN);
|
||||
_prefs->geo_alert_lat_1e6 = e.lat_1e6;
|
||||
_prefs->geo_alert_lon_1e6 = e.lon_1e6;
|
||||
snprintf(_prefs->geo_alert_label, sizeof(_prefs->geo_alert_label), "%s", e.name);
|
||||
}
|
||||
_prefs->geo_alert_has_target = 1;
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
@@ -12,14 +12,42 @@
|
||||
|
||||
namespace navview {
|
||||
|
||||
// Closing-speed / ETA estimator. The caller keeps one instance per navigation
|
||||
// session and passes it to draw(), which feeds it the live target distance each
|
||||
// frame. Speed is the rate the gap closes (smoothed), so it works for both a
|
||||
// fixed waypoint and a moving live contact; ETA = remaining distance / closing
|
||||
// speed, shown only while actually approaching.
|
||||
struct EtaTracker {
|
||||
float prev_km = -1.0f;
|
||||
uint32_t prev_ms = 0;
|
||||
float closing_kmh = 0.0f; // > 0 = gap shrinking (approaching)
|
||||
bool have = false;
|
||||
void reset() { prev_km = -1.0f; have = false; closing_kmh = 0.0f; }
|
||||
void update(float dist_km, uint32_t now_ms) {
|
||||
if (prev_km >= 0.0f && now_ms > prev_ms) {
|
||||
float dt_h = (now_ms - prev_ms) / 3600000.0f;
|
||||
if (dt_h > 0.0f) {
|
||||
float inst = (prev_km - dist_km) / dt_h; // +approaching, -receding
|
||||
closing_kmh = have ? (closing_kmh * 0.6f + inst * 0.4f) : inst; // light EMA
|
||||
have = true;
|
||||
}
|
||||
}
|
||||
prev_km = dist_km;
|
||||
prev_ms = now_ms;
|
||||
}
|
||||
};
|
||||
|
||||
// own_valid : is there a usable GPS fix for the device right now
|
||||
// cog_valid : is there a stable course-over-ground (UITask::currentCourse)
|
||||
// eta : optional closing-speed/ETA tracker; when supplied a third line is
|
||||
// shown with the approach speed and estimated time of arrival
|
||||
inline void draw(DisplayDriver& d,
|
||||
bool own_valid, int32_t own_lat, int32_t own_lon,
|
||||
int32_t tgt_lat, int32_t tgt_lon,
|
||||
const char* label,
|
||||
bool cog_valid, int cog_deg,
|
||||
bool imperial) {
|
||||
bool imperial,
|
||||
EtaTracker* eta = nullptr) {
|
||||
const int cx = d.width() / 2;
|
||||
const int hdr = d.headerH();
|
||||
|
||||
@@ -52,6 +80,23 @@ inline void draw(DisplayDriver& d,
|
||||
if (cog_valid) snprintf(line, sizeof(line), "Hdg: %d %s", cog_deg, geo::bearingCardinal(cog_deg));
|
||||
else snprintf(line, sizeof(line), "Hdg: --");
|
||||
d.drawTextLeftAlign(2, y, line); y += step;
|
||||
|
||||
// Optional ETA / closing-speed line. Approaching only — a receding or
|
||||
// stationary target shows "ETA: --" rather than a misleading time.
|
||||
if (eta) {
|
||||
eta->update(dist_km, millis());
|
||||
if (eta->have && eta->closing_kmh > 0.3f) {
|
||||
int secs = (int)(dist_km / eta->closing_kmh * 3600.0f);
|
||||
float spd = imperial ? eta->closing_kmh * 0.621371f : eta->closing_kmh;
|
||||
char eta_s[12];
|
||||
if (secs < 3600) snprintf(eta_s, sizeof(eta_s), "%dm", (secs + 59) / 60);
|
||||
else snprintf(eta_s, sizeof(eta_s), "%dh%02dm", secs / 3600, (secs % 3600) / 60);
|
||||
snprintf(line, sizeof(line), "ETA: %s @%d%s", eta_s, (int)(spd + 0.5f), imperial ? "mph" : "kmh");
|
||||
} else {
|
||||
snprintf(line, sizeof(line), "ETA: --");
|
||||
}
|
||||
d.drawTextLeftAlign(2, y, line); y += step;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace navview
|
||||
|
||||
@@ -58,6 +58,7 @@ class NearbyScreen : public UIScreen {
|
||||
int _scroll;
|
||||
bool _detail;
|
||||
bool _nav = false; // full-screen navigate-to-node view (over detail)
|
||||
navview::EtaTracker _nav_eta; // closing-speed/ETA for the navigate view
|
||||
int32_t _own_lat, _own_lon;
|
||||
bool _own_gps;
|
||||
|
||||
@@ -68,6 +69,7 @@ class NearbyScreen : public UIScreen {
|
||||
unsigned long _detail_refresh_ms;
|
||||
unsigned long _list_refresh_ms = 0;
|
||||
static const unsigned long DETAIL_REFRESH_MS = 10000UL;
|
||||
static const unsigned long NAV_REFRESH_MS = 1000UL; // navigate view tracks a moving target
|
||||
static const unsigned long TIME_LIST_REFRESH_MS = 3000UL;
|
||||
|
||||
// ── live-scan state ──────────────────────────────────────────────────────────
|
||||
@@ -447,7 +449,7 @@ class NearbyScreen : public UIScreen {
|
||||
switch (a) {
|
||||
case ACT_NAV: {
|
||||
const Entry* e = selected();
|
||||
if (e && (e->lat_e6 != 0 || e->lon_e6 != 0)) _nav = true;
|
||||
if (e && (e->lat_e6 != 0 || e->lon_e6 != 0)) { _nav = true; _nav_eta.reset(); }
|
||||
else _task->showAlert("No node GPS", 1000);
|
||||
break;
|
||||
}
|
||||
@@ -493,7 +495,7 @@ class NearbyScreen : public UIScreen {
|
||||
fmtAge(age, sizeof(age), e.lastmod);
|
||||
// For a live [LOC] row, label the timestamp as a position share and note
|
||||
// whether the sender's identity is verified (DM) or name-only (channel).
|
||||
if (e.is_live) snprintf(buf, sizeof(buf), "Loc: %s %s", age, e.live_verified ? "(DM)" : "(chan~)");
|
||||
if (e.is_live) snprintf(buf, sizeof(buf), "Sharing pos: %s %s", age, e.live_verified ? "(DM)" : "(chan)");
|
||||
else snprintf(buf, sizeof(buf), "Seen: %s", age);
|
||||
display.drawTextEllipsized(2, hdr + step * 4, display.width() - 4, buf);
|
||||
}
|
||||
@@ -573,16 +575,33 @@ public:
|
||||
int render(DisplayDriver& display) override {
|
||||
display.setTextSize(1);
|
||||
|
||||
// periodic refresh in stored-detail — preserve selected contact by idx
|
||||
if (_detail && _source == SRC_STORED &&
|
||||
millis() - _detail_refresh_ms >= DETAIL_REFRESH_MS) {
|
||||
int saved = (_sel < _count) ? _entries[_sel].contact_idx : -1;
|
||||
// Periodic refresh of the selected entry while in detail or navigate view,
|
||||
// preserving the selection across the list rebuild. Navigate refreshes
|
||||
// faster so a moving live target (a contact sharing [LOC]) tracks smoothly.
|
||||
// Re-selection keys on contact index for stored nodes, and on name for a
|
||||
// non-contact live sender — without the latter, navigating to someone who
|
||||
// only shares on a channel would drop out on the first refresh.
|
||||
unsigned long refresh_due = _nav ? NAV_REFRESH_MS : DETAIL_REFRESH_MS;
|
||||
if ((_detail || _nav) && _source == SRC_STORED &&
|
||||
millis() - _detail_refresh_ms >= refresh_due) {
|
||||
int saved_idx = (_sel < _count) ? _entries[_sel].contact_idx : -1;
|
||||
bool saved_live = (_sel < _count) && _entries[_sel].is_live && saved_idx < 0;
|
||||
char saved_name[sizeof(_entries[0].name)]; saved_name[0] = '\0';
|
||||
if (_sel < _count) {
|
||||
strncpy(saved_name, _entries[_sel].name, sizeof(saved_name) - 1);
|
||||
saved_name[sizeof(saved_name) - 1] = '\0';
|
||||
}
|
||||
refreshStored();
|
||||
bool found = false;
|
||||
if (saved >= 0)
|
||||
if (saved_idx >= 0) {
|
||||
for (int i = 0; i < _count; i++)
|
||||
if (_entries[i].contact_idx == saved) { _sel = i; found = true; break; }
|
||||
if (!found) { _detail = false; _nav = false; } // contact gone — drop both views
|
||||
if (_entries[i].contact_idx == saved_idx) { _sel = i; found = true; break; }
|
||||
} else if (saved_live && saved_name[0]) {
|
||||
for (int i = 0; i < _count; i++)
|
||||
if (_entries[i].is_live && strncmp(_entries[i].name, saved_name, sizeof(saved_name) - 1) == 0)
|
||||
{ _sel = i; found = true; break; }
|
||||
}
|
||||
if (!found) { _detail = false; _nav = false; } // node/share gone — drop both views
|
||||
_detail_refresh_ms = millis();
|
||||
}
|
||||
|
||||
@@ -591,7 +610,7 @@ public:
|
||||
const Entry& e = _entries[_sel];
|
||||
int cog; bool cogv = _task->currentCourse(cog);
|
||||
navview::draw(display, _own_gps, _own_lat, _own_lon,
|
||||
e.lat_e6, e.lon_e6, e.name, cogv, cog, useImperial());
|
||||
e.lat_e6, e.lon_e6, e.name, cogv, cog, useImperial(), &_nav_eta);
|
||||
return 1000;
|
||||
}
|
||||
|
||||
@@ -655,18 +674,20 @@ public:
|
||||
display.drawSelectionRow(0, y - 1, display.width() - reserve, item_h - 1, sel);
|
||||
|
||||
char filt[32];
|
||||
int tx = 2;
|
||||
if (e.is_live) {
|
||||
// Mark a live [LOC] row: '*' = verified DM share, '~' = channel share.
|
||||
char nm[34];
|
||||
snprintf(nm, sizeof(nm), "%s%s", e.live_verified ? "*" : "~", e.name);
|
||||
display.translateUTF8ToBlocks(filt, nm, sizeof(filt));
|
||||
} else {
|
||||
display.translateUTF8ToBlocks(filt, e.name, sizeof(filt));
|
||||
// Diamond marker = this node is broadcasting its position ([LOC]),
|
||||
// the same marker the map uses for a live-tracked contact. DM-verified
|
||||
// vs channel-only is spelled out in the detail view.
|
||||
int iw = ICON_MAP_CONTACT.w * miniIconScale(display);
|
||||
miniIconDrawCentered(display, 2 + iw / 2, y + display.getLineHeight() / 2 - 1, ICON_MAP_CONTACT);
|
||||
tx = 2 + iw + 2;
|
||||
}
|
||||
display.translateUTF8ToBlocks(filt, e.name, sizeof(filt));
|
||||
if (_source == SRC_SCAN && !e.name[0]) { // unknown node → "[Type]"
|
||||
snprintf(filt, sizeof(filt), "[%s]", typeName(e.type));
|
||||
}
|
||||
display.drawTextEllipsized(2, y, dist_col - 4, filt);
|
||||
display.drawTextEllipsized(tx, y, dist_col - tx - 2, filt);
|
||||
|
||||
display.setColor(sel ? DisplayDriver::DARK : DisplayDriver::LIGHT);
|
||||
char right[10];
|
||||
|
||||
@@ -1225,6 +1225,10 @@ public:
|
||||
_task->gotoDashboardConfig();
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_CONTEXT_MENU && _page == HomePage::MAP) {
|
||||
_task->quickShareMyLocation();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -2129,10 +2133,20 @@ void UITask::loop() {
|
||||
// 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;
|
||||
int32_t tlat, tlon;
|
||||
if (_node_prefs->geo_alert_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());
|
||||
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;
|
||||
}
|
||||
int32_t lat, lon;
|
||||
if (!currentLocation(lat, lon)) return false;
|
||||
dist_m = geo::haversineKm(lat, lon, _node_prefs->geo_alert_lat_1e6,
|
||||
_node_prefs->geo_alert_lon_1e6) * 1000.0f;
|
||||
dist_m = geo::haversineKm(lat, lon, tlat, tlon) * 1000.0f;
|
||||
radius_m = (float)NodePrefs::geoAlertRadiusMeters(_node_prefs->geo_alert_radius_idx);
|
||||
return true;
|
||||
}
|
||||
@@ -2160,8 +2174,12 @@ void UITask::evaluateGeoAlert() {
|
||||
|
||||
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;
|
||||
char msg[40];
|
||||
snprintf(msg, sizeof(msg), arrived ? "Arrived: %s" : "Left: %s", lbl);
|
||||
// "Near/Away" reads naturally for a moving person; "Arrived/Left" for a place.
|
||||
snprintf(msg, sizeof(msg),
|
||||
arrived ? (person ? "Near: %s" : "Arrived: %s")
|
||||
: (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");
|
||||
@@ -2277,6 +2295,22 @@ bool UITask::sendLocationShare(int32_t lat, int32_t lon) {
|
||||
return the_mesh.sendMessage(*c, rtc_clock.getCurrentTime(), 0, text, expected_ack, est_timeout) > 0;
|
||||
}
|
||||
|
||||
// One-shot "share my position" from the home Map page (Hold Enter). When live
|
||||
// sharing is already on, push an immediate [LOC] to the same target; otherwise
|
||||
// hand a [LOC] message to the recipient picker so the user chooses where it
|
||||
// goes (no accidental broadcast to a default channel).
|
||||
void UITask::quickShareMyLocation() {
|
||||
int32_t lat, lon;
|
||||
if (!currentLocation(lat, lon)) { showAlert("No GPS fix", 1000); return; }
|
||||
if (_node_prefs && _node_prefs->loc_share_enabled && sendLocationShare(lat, lon)) {
|
||||
showAlert("Position shared", 900);
|
||||
return;
|
||||
}
|
||||
char text[40];
|
||||
snprintf(text, sizeof(text), LOCATION_MSG_TAG "%.5f,%.5f", lat / 1e6, lon / 1e6);
|
||||
shareToMessage(text);
|
||||
}
|
||||
|
||||
void UITask::saveWaypoints() {
|
||||
DataStore* ds = the_mesh.getDataStore();
|
||||
if (!ds) return;
|
||||
|
||||
@@ -179,6 +179,7 @@ public:
|
||||
void gotoQuickMsgScreen();
|
||||
void openContactDM(const ContactInfo& ci);
|
||||
void shareToMessage(const char* text); // open Messages pre-loaded to share `text`
|
||||
void quickShareMyLocation(); // Home Map Hold-Enter: one-shot position share
|
||||
void pickLocShareTarget(); // open Messages to choose the live-share target
|
||||
int getRecentDMContacts(uint8_t out[][NodePrefs::FAVOURITE_PREFIX_LEN], int max) const;
|
||||
void gotoToolsScreen();
|
||||
|
||||
Reference in New Issue
Block a user