2026-06-02 23:51:08 +02:00
|
|
|
#pragma once
|
|
|
|
|
// Shared geographic helpers used by the navigation features (Nearby Nodes,
|
|
|
|
|
// Waypoints, trail course-over-ground). Coordinates are fixed-point degrees
|
|
|
|
|
// scaled by 1e6 (the same representation used by the GPS provider and the
|
|
|
|
|
// contact/trail records). All functions are pure and header-inline.
|
|
|
|
|
|
|
|
|
|
#include <Arduino.h>
|
|
|
|
|
#include <math.h>
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
#include <ctype.h>
|
2026-06-02 23:51:08 +02:00
|
|
|
|
|
|
|
|
#ifndef M_PI
|
|
|
|
|
#define M_PI 3.14159265358979323846
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
namespace geo {
|
|
|
|
|
|
|
|
|
|
// Great-circle distance between two 1e6-scaled lat/lon points, in kilometres.
|
|
|
|
|
static inline float haversineKm(int32_t lat1, int32_t lon1, int32_t lat2, int32_t lon2) {
|
|
|
|
|
static const float DEG2RAD = (float)M_PI / 180.0f;
|
|
|
|
|
float la1 = lat1 * (1e-6f * DEG2RAD);
|
|
|
|
|
float la2 = lat2 * (1e-6f * DEG2RAD);
|
|
|
|
|
float dla = (lat2 - lat1) * (1e-6f * DEG2RAD);
|
|
|
|
|
float dlo = (lon2 - lon1) * (1e-6f * DEG2RAD);
|
|
|
|
|
float a = sinf(dla/2)*sinf(dla/2) + cosf(la1)*cosf(la2)*sinf(dlo/2)*sinf(dlo/2);
|
|
|
|
|
return 6371.0f * 2.0f * asinf(sqrtf(a));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initial bearing from point 1 to point 2, degrees 0..359 (0 = north).
|
|
|
|
|
static inline int bearingDeg(int32_t lat1, int32_t lon1, int32_t lat2, int32_t lon2) {
|
|
|
|
|
static const float DEG2RAD = (float)M_PI / 180.0f;
|
|
|
|
|
float la1 = lat1 * (1e-6f * DEG2RAD);
|
|
|
|
|
float la2 = lat2 * (1e-6f * DEG2RAD);
|
|
|
|
|
float dlo = (lon2 - lon1) * (1e-6f * DEG2RAD);
|
|
|
|
|
float b = atan2f(sinf(dlo)*cosf(la2),
|
|
|
|
|
cosf(la1)*sinf(la2) - sinf(la1)*cosf(la2)*cosf(dlo)) * (180.0f / (float)M_PI);
|
|
|
|
|
if (b < 0.0f) b += 360.0f;
|
|
|
|
|
return (int)(b + 0.5f) % 360;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 8-point cardinal label for a bearing in degrees.
|
|
|
|
|
static inline const char* bearingCardinal(int deg) {
|
|
|
|
|
static const char* dirs[] = { "N","NE","E","SE","S","SW","W","NW" };
|
|
|
|
|
return dirs[((deg + 22) % 360) / 45];
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-04 00:44:56 +02:00
|
|
|
// Human distance string. Metric: "850m" / "2.3km" / "140km". Imperial:
|
|
|
|
|
// "850ft" / "2.3mi" / "140mi" (feet below ~1000 ft, then miles).
|
|
|
|
|
static inline void fmtDist(char* buf, int n, float km, bool imperial) {
|
|
|
|
|
if (imperial) {
|
|
|
|
|
float ft = km * 3280.84f;
|
|
|
|
|
if (ft < 1000.0f) { snprintf(buf, n, "%dft", (int)(ft + 0.5f)); return; }
|
|
|
|
|
float mi = km * 0.621371f;
|
|
|
|
|
if (mi < 100.0f) snprintf(buf, n, "%.1fmi", mi);
|
|
|
|
|
else snprintf(buf, n, "%dmi", (int)(mi + 0.5f));
|
|
|
|
|
} else {
|
|
|
|
|
if (km < 1.0f) snprintf(buf, n, "%dm", (int)(km * 1000 + 0.5f));
|
|
|
|
|
else if (km < 100.0f) snprintf(buf, n, "%.1fkm", km);
|
|
|
|
|
else snprintf(buf, n, "%dkm", (int)(km + 0.5f));
|
|
|
|
|
}
|
2026-06-02 23:51:08 +02: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>
2026-06-25 19:04:21 +02:00
|
|
|
// Compact age tag for a timestamp, e.g. "12s" / "5m" / "3h" / "2d" — sized to
|
|
|
|
|
// sit inline after a name (unlike a full "X ago" sentence). Empty string for
|
|
|
|
|
// an unknown (0) or future timestamp. Takes `now` rather than reading the RTC
|
|
|
|
|
// itself, so this stays a pure function like the rest of this file.
|
|
|
|
|
static inline void fmtAgeShort(char* buf, int n, uint32_t now, uint32_t lastmod) {
|
|
|
|
|
if (lastmod == 0 || now < lastmod) { buf[0] = '\0'; return; }
|
|
|
|
|
uint32_t age = now - lastmod;
|
|
|
|
|
if (age < 60) snprintf(buf, n, "%us", (unsigned)age);
|
|
|
|
|
else if (age < 3600) snprintf(buf, n, "%um", (unsigned)(age / 60));
|
|
|
|
|
else if (age < 86400) snprintf(buf, n, "%uh", (unsigned)(age / 3600));
|
|
|
|
|
else snprintf(buf, n, "%ud", (unsigned)(age / 86400));
|
|
|
|
|
}
|
|
|
|
|
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
// Tag marking a shared waypoint inside a message: "[WAY]<lat>,<lon> <label>".
|
|
|
|
|
// A plain {loc} expansion ("<lat>,<lon>") parses too — the tag just adds intent
|
|
|
|
|
// and a label, and keeps the text readable on apps/firmware that don't know it.
|
|
|
|
|
#define WAYPOINT_MSG_TAG "[WAY]"
|
|
|
|
|
|
|
|
|
|
// Scan a message for an embedded "lat,lon" location (decimal degrees, the same
|
|
|
|
|
// text {loc} emits). If a WAYPOINT_MSG_TAG precedes it, the trailing text is
|
|
|
|
|
// taken as the label. Returns true and fills lat/lon (1e6-scaled) on a valid,
|
|
|
|
|
// in-range coordinate; label (optional) gets the trimmed tag suffix or "".
|
|
|
|
|
static inline bool parseLatLon(const char* text, int32_t& lat_1e6, int32_t& lon_1e6,
|
|
|
|
|
char* label = nullptr, int label_n = 0) {
|
|
|
|
|
if (label && label_n > 0) label[0] = '\0';
|
|
|
|
|
if (!text) return false;
|
|
|
|
|
|
|
|
|
|
const char* tag = strstr(text, WAYPOINT_MSG_TAG);
|
|
|
|
|
const char* scan = tag ? tag + strlen(WAYPOINT_MSG_TAG) : text;
|
|
|
|
|
|
|
|
|
|
for (const char* p = scan; *p; p++) {
|
|
|
|
|
if (*p != '-' && *p != '+' && *p != '.' && !isdigit((unsigned char)*p)) continue;
|
|
|
|
|
char* end = nullptr;
|
|
|
|
|
double la = strtod(p, &end);
|
|
|
|
|
if (end == p) continue;
|
|
|
|
|
if (!memchr(p, '.', end - p)) continue; // require a decimal point (skip "5,6")
|
|
|
|
|
const char* q = end;
|
|
|
|
|
while (*q == ' ') q++;
|
|
|
|
|
if (*q != ',') { p = end - 1; continue; }
|
|
|
|
|
q++;
|
|
|
|
|
while (*q == ' ') q++;
|
|
|
|
|
char* end2 = nullptr;
|
|
|
|
|
double lo = strtod(q, &end2);
|
|
|
|
|
if (end2 == q) continue;
|
|
|
|
|
if (la < -90.0 || la > 90.0 || lo < -180.0 || lo > 180.0) continue;
|
|
|
|
|
|
2026-06-04 15:46:57 +02:00
|
|
|
lat_1e6 = (int32_t)(la * 1e6 + (la < 0 ? -0.5 : 0.5)); // round in double — float loses ~1m at 1e8
|
|
|
|
|
lon_1e6 = (int32_t)(lo * 1e6 + (lo < 0 ? -0.5 : 0.5));
|
feat(nav): navigate to / save a location shared in a message
Locations already travel as plain text ({loc} expands to "lat,lon"), so any
message carrying coordinates can now be acted on. In the fullscreen message
view, the context-menu (Hold Enter) gains Navigate and Save waypoint
entries whenever the message contains a coordinate — alongside Reply.
- geo::parseLatLon: shared scanner for an embedded "lat,lon" (the {loc}
format), with an optional [WAY] tag that supplies a label. Range-checked,
requires a decimal point to avoid matching plain integer pairs. Will also
back the waypoint-share feature later.
- QuickMsgScreen: Navigate opens NavView inline (To/Hdg/distance, honouring
the global Units setting) over the message, back returns to it; Save
waypoint stores the point (label from [WAY], else auto-named). Works in
both DM and channel histories, and on incoming or outgoing messages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:28:17 +02:00
|
|
|
if (label && label_n > 0 && tag) { // label only from a tagged share
|
|
|
|
|
const char* s = end2;
|
|
|
|
|
while (*s == ' ') s++;
|
|
|
|
|
int n = 0;
|
|
|
|
|
while (s[n] && n < label_n - 1) { label[n] = s[n]; n++; }
|
|
|
|
|
while (n > 0 && label[n - 1] == ' ') n--; // trim trailing spaces
|
|
|
|
|
label[n] = '\0';
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-06-25 19:04:21 +02:00
|
|
|
// Tag marking a live position share inside a message: "[LOC]<lat>,<lon>".
|
|
|
|
|
// Distinct from WAYPOINT_MSG_TAG: a waypoint is a static point-of-interest to
|
|
|
|
|
// save, whereas this announces the *sender's own* current position so the
|
|
|
|
|
// receiver can update its bearing/track on that node. Both reuse parseLatLon
|
|
|
|
|
// for the coordinate, and both stay readable on firmware/apps that don't know
|
|
|
|
|
// the tag (it just looks like a coordinate).
|
|
|
|
|
#define LOCATION_MSG_TAG "[LOC]"
|
|
|
|
|
|
|
|
|
|
// True if `text` carries a LOCATION_MSG_TAG share; fills lat/lon (1e6-scaled)
|
|
|
|
|
// from the coordinate that follows it. Returns false for plain text, for a
|
|
|
|
|
// bare coordinate, or for a [WAY] share — only an explicit [LOC] tag counts,
|
|
|
|
|
// so ordinary messages that happen to contain digits don't move anyone's pin.
|
|
|
|
|
static inline bool parseLocShare(const char* text, int32_t& lat_1e6, int32_t& lon_1e6) {
|
|
|
|
|
if (!text) return false;
|
|
|
|
|
const char* tag = strstr(text, LOCATION_MSG_TAG);
|
|
|
|
|
if (!tag) return false;
|
|
|
|
|
return parseLatLon(tag + strlen(LOCATION_MSG_TAG), lat_1e6, lon_1e6);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-02 23:51:08 +02:00
|
|
|
} // namespace geo
|