mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-08-04 03:06:12 +00:00
feat(companion): live location sharing, Locator geofencing, trail auto-pause
Squash merge of feat/location-beacon-alerts-autopause (v1.21). Features: - Live Location Sharing — broadcast position as movement-gated [LOC] messages to a channel or contact; live shares show as map pins with distance/bearing in Nearby Nodes and a status-bar indicator. [LOC] is parsed in DMs, channel messages and room messages; DM shares name the sender. - Locator (geofence) — arm a geofence around a saved waypoint or a person (live [LOC] or last-known position), alert on arrive/leave or near/far, with an optional homing beeper (gated to arrive/both modes). Arm from Tools > Locator, Nearby Nodes, or Waypoints; target picker lists favourites first, clearable via a "None" entry. Active target is drawn as a flag on the map. - One active target shared across Locator / Navigate / Map via a single resolver (resolvePersonPos / activeTargetPos) that prefers a live [LOC] share over the last-advertised GPS fix. - Follow live contacts — Navigate to a live-sharing contact follows them as they move and adds an ETA line; quick-share your own position from the Map. - Map & status-bar upgrades — home mini-map gets a north marker, scale tick, and a connected trail line (was disconnected dots); status line shows tracked-node count, an arrow + distance to the active Locator/Nav target (falling back to the nearest live-tracked contact); GPS fix icon in the top status bar, shown only on GPS boards with GPS enabled. - Trail auto-pause — recording freezes on stops (banking elapsed time, breaking the map line across the idle gap) and resumes on movement without ending the session. - Streaming trail simplification — GPS points are simplified in-stream via a fixed-corridor (Reumann-Witkam) pass tuned for fidelity: straight runs collapse to their endpoints, curves stay bounded to the Min-dist tolerance, so the 512-point buffer covers a far longer route than a flat point budget would suggest. - Collapsible Tools (Location / Comms / System sections, fold-in-place like Settings) and page-indicator icons on the home carousel. - Waypoint coordinate editor — add a waypoint by scroll-editing lat/lon digit by digit. Fixes: - Critical: low-heap hang and contact loss on RAM-tight builds. Halved message-history scrollback rings (recovering ~14 KB free heap) and made contacts/channels/prefs persistence atomic (temp file + rename), so an interrupted save can no longer corrupt or wipe the store. - Serial.write() bounded so a stalled USB host can't hang the device. - Nearby Nodes: live [LOC] senders respect the type filter, sort by shared position, and the list refreshes so live shares bubble up. - Map: live contacts are labelled before waypoints. - GPS status icon hidden when GPS is off in Settings. - Splash screen no longer truncates a pre-release tag's own dash (e.g. v1.21-rc1) when stripping the build's commit-hash suffix. - Null-guarded the Locator target picker; clamped loc-share channel index on load. Under the hood: - -Os size optimisation on the e-ink and GAT562 30S solo envs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c935287627
commit
57774d41f3
@@ -13,6 +13,7 @@
|
||||
#include "KeyboardWidget.h"
|
||||
#include "PopupMenu.h"
|
||||
#include "NavView.h"
|
||||
#include "DigitEditor.h"
|
||||
|
||||
class WaypointsView {
|
||||
UITask* _task;
|
||||
@@ -30,14 +31,18 @@ class WaypointsView {
|
||||
int32_t _mark_lat = 0, _mark_lon = 0; // pending new-mark position
|
||||
uint32_t _mark_ts = 0;
|
||||
|
||||
// ADD form — type a waypoint by coordinates. The keyboard has no comma or
|
||||
// minus, so lat/lon are entered as magnitude + a hemisphere toggle (LEFT/RIGHT).
|
||||
char _add_lat[12] = "", _add_lon[12] = ""; // magnitude strings
|
||||
// ADD form — enter a waypoint by coordinates. Lat/Lon are numeric, so they use
|
||||
// the digit-by-digit scroll editor (same widget as the radio frequency field)
|
||||
// rather than the text keyboard: Enter opens the editor on the magnitude,
|
||||
// LEFT/RIGHT on the row toggles the hemisphere (N/S, E/W). Only the Label field
|
||||
// uses the keyboard.
|
||||
float _add_lat_mag = 0.0f, _add_lon_mag = 0.0f; // magnitude (degrees)
|
||||
char _add_label[WAYPOINT_LABEL_LEN] = "";
|
||||
bool _add_lat_neg = false; // true = S
|
||||
bool _add_lon_neg = false; // true = W
|
||||
int _add_sel = 0; // 0=Lat 1=Lon 2=Label 3=Save
|
||||
int _kb_field = -1; // which ADD field the keyboard edits (-1 = label/rename)
|
||||
int _kb_field = -1; // ADD label edit (>=0) vs mark/rename (-1)
|
||||
DigitEditor _add_editor; // active while editing a Lat/Lon magnitude
|
||||
|
||||
bool useImperial() const { return _task && _task->useImperial(); }
|
||||
bool ownPos(int32_t& lat, int32_t& lon) const { return _task->currentLocation(lat, lon); }
|
||||
@@ -53,38 +58,23 @@ class WaypointsView {
|
||||
// Add a waypoint by coordinates (no GPS fix needed). Opens the ADD form.
|
||||
void openAddForm() {
|
||||
if (_task->waypoints().full()) { _task->showAlert("Waypoints full", 1000); return; }
|
||||
_add_lat[0] = _add_lon[0] = _add_label[0] = '\0';
|
||||
_add_lat_mag = _add_lon_mag = 0.0f;
|
||||
_add_label[0] = '\0';
|
||||
_add_lat_neg = _add_lon_neg = false;
|
||||
_add_sel = 0;
|
||||
_add_editor.active = false;
|
||||
_mode = ADD;
|
||||
}
|
||||
|
||||
// Store the just-typed value into the focused ADD field.
|
||||
// Store the just-typed label (the only ADD field that still uses the keyboard).
|
||||
void applyAddField(const char* buf) {
|
||||
char* dst = (_kb_field == 0) ? _add_lat
|
||||
: (_kb_field == 1) ? _add_lon : _add_label;
|
||||
int cap = (_kb_field == 2) ? WAYPOINT_LABEL_LEN : (int)sizeof(_add_lat);
|
||||
strncpy(dst, buf, cap - 1);
|
||||
dst[cap - 1] = '\0';
|
||||
}
|
||||
|
||||
// Parse a numeric field: true only when the whole string is a number (so a
|
||||
// stray letter from the full keyboard can't silently read as 0).
|
||||
static bool parseNum(const char* s, double& out) {
|
||||
if (!s || !s[0]) return false;
|
||||
char* end = nullptr;
|
||||
out = strtod(s, &end);
|
||||
if (end == s) return false; // nothing numeric parsed
|
||||
while (*end == ' ') end++;
|
||||
return *end == '\0'; // no trailing non-numeric chars
|
||||
strncpy(_add_label, buf, sizeof(_add_label) - 1);
|
||||
_add_label[sizeof(_add_label) - 1] = '\0';
|
||||
}
|
||||
|
||||
// Validate the ADD form and save the waypoint.
|
||||
void commitAddForm() {
|
||||
double la, lo;
|
||||
if (!parseNum(_add_lat, la) || !parseNum(_add_lon, lo)) {
|
||||
_task->showAlert("Lat/Lon invalid", 1200); return;
|
||||
}
|
||||
double la = _add_lat_mag, lo = _add_lon_mag;
|
||||
if (_add_lat_neg) la = -la;
|
||||
if (_add_lon_neg) lo = -lo;
|
||||
if (la < -90.0 || la > 90.0 || lo < -180.0 || lo > 180.0) {
|
||||
@@ -134,12 +124,22 @@ class WaypointsView {
|
||||
int y = top + i * step;
|
||||
bool sel = (i == _add_sel);
|
||||
display.drawSelectionRow(0, y - 1, display.width(), step - 1, sel);
|
||||
char row[28];
|
||||
if (i == 0) snprintf(row, sizeof(row), "Lat: %c %s", _add_lat_neg ? 'S' : 'N', _add_lat[0] ? _add_lat : "--");
|
||||
else if (i == 1) snprintf(row, sizeof(row), "Lon: %c %s", _add_lon_neg ? 'W' : 'E', _add_lon[0] ? _add_lon : "--");
|
||||
else if (i == 2) snprintf(row, sizeof(row), "Label: %s", _add_label[0] ? _add_label : "(auto)");
|
||||
else snprintf(row, sizeof(row), "[Save]");
|
||||
display.setCursor(2, y); display.print(row);
|
||||
// While editing a magnitude, draw the row prefix + the digit editor so the
|
||||
// place under the cursor is highlighted; otherwise draw the plain value.
|
||||
if ((i == 0 || i == 1) && sel && _add_editor.active) {
|
||||
char prefix[10];
|
||||
if (i == 0) snprintf(prefix, sizeof(prefix), "Lat: %c ", _add_lat_neg ? 'S' : 'N');
|
||||
else snprintf(prefix, sizeof(prefix), "Lon: %c ", _add_lon_neg ? 'W' : 'E');
|
||||
display.setCursor(2, y); display.print(prefix);
|
||||
_add_editor.render(display, 2 + (int)strlen(prefix) * display.getCharWidth(), y);
|
||||
} else {
|
||||
char row[28];
|
||||
if (i == 0) snprintf(row, sizeof(row), "Lat: %c %.5f", _add_lat_neg ? 'S' : 'N', _add_lat_mag);
|
||||
else if (i == 1) snprintf(row, sizeof(row), "Lon: %c %.5f", _add_lon_neg ? 'W' : 'E', _add_lon_mag);
|
||||
else if (i == 2) snprintf(row, sizeof(row), "Label: %s", _add_label[0] ? _add_label : "(auto)");
|
||||
else snprintf(row, sizeof(row), "[Save]");
|
||||
display.setCursor(2, y); display.print(row);
|
||||
}
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
}
|
||||
@@ -274,7 +274,7 @@ public:
|
||||
if (_sel >= wpListCount()) _sel = wpListCount() - 1;
|
||||
if (_sel < 0) _sel = 0;
|
||||
}
|
||||
} else { // Send (share in a message)
|
||||
} else if (sel == 2) { // Send (share in a message)
|
||||
if (wi >= 0 && wi < _task->waypoints().count()) {
|
||||
const Waypoint& w = _task->waypoints().at(wi);
|
||||
double lat = w.lat_1e6 / 1000000.0, lon = w.lon_1e6 / 1000000.0;
|
||||
@@ -283,13 +283,27 @@ public:
|
||||
else snprintf(text, sizeof(text), WAYPOINT_MSG_TAG "%.5f,%.5f", lat, lon);
|
||||
_task->shareToMessage(text); // hands off to the Messages screen
|
||||
}
|
||||
} else { // Set as target
|
||||
if (wi >= 0 && wi < _task->waypoints().count()) {
|
||||
const Waypoint& w = _task->waypoints().at(wi);
|
||||
_task->setTargetNow(0, nullptr, w.lat_1e6, w.lon_1e6, w.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ADD form: type lat/lon (magnitude) + hemisphere toggle + label.
|
||||
// ADD form: scroll-edit lat/lon (magnitude) + hemisphere toggle + label.
|
||||
if (_mode == ADD) {
|
||||
// The digit editor, while open, consumes all input (UP/DOWN change the
|
||||
// digit, LEFT/RIGHT move the cursor, Enter commits, Cancel aborts).
|
||||
if (_add_editor.active) {
|
||||
if (_add_editor.handleInput(c) == DigitEditor::DONE) {
|
||||
if (_add_sel == 0) _add_lat_mag = _add_editor.value;
|
||||
else _add_lon_mag = _add_editor.value;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_CANCEL) { _mode = OFF; return true; }
|
||||
if (c == KEY_UP) { _add_sel = (_add_sel > 0) ? _add_sel - 1 : 3; return true; }
|
||||
if (c == KEY_DOWN) { _add_sel = (_add_sel < 3) ? _add_sel + 1 : 0; return true; }
|
||||
@@ -299,8 +313,8 @@ public:
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_ENTER) {
|
||||
if (_add_sel == 0) { _kb_field = 0; openKb(_add_lat, 11); }
|
||||
else if (_add_sel == 1) { _kb_field = 1; openKb(_add_lon, 11); }
|
||||
if (_add_sel == 0) _add_editor.begin(_add_lat_mag, 0.0f, 90.0f, 2, 5);
|
||||
else if (_add_sel == 1) _add_editor.begin(_add_lon_mag, 0.0f, 180.0f, 3, 5);
|
||||
else if (_add_sel == 2) { _kb_field = 2; openKb(_add_label, WAYPOINT_LABEL_LEN - 1); }
|
||||
else { commitAddForm(); }
|
||||
return true;
|
||||
@@ -327,13 +341,14 @@ public:
|
||||
else _mode = NAV; // a waypoint / Trail-start row
|
||||
return true;
|
||||
}
|
||||
// Rename/Delete/Send apply to saved waypoints only — not Trail-start or Add.
|
||||
// Rename/Delete/Send/Locator apply to saved waypoints only — not Trail-start or Add.
|
||||
if (c == KEY_CONTEXT_MENU && !selIsStart() && _sel != n &&
|
||||
wpIndex() >= 0 && wpIndex() < _task->waypoints().count()) {
|
||||
_ctx.begin("Waypoint", 3);
|
||||
_ctx.begin("Waypoint", 4);
|
||||
_ctx.addItem("Rename");
|
||||
_ctx.addItem("Delete");
|
||||
_ctx.addItem("Send");
|
||||
_ctx.addItem("Set as target");
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user