feat(ui): a route written in a message shown on the map (L2 Route, L1 Show route)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-26 19:46:28 +01:00
co-authored by Claude Opus 5.5
parent 009bde0db4
commit 81c551425e
8 changed files with 301 additions and 11 deletions
+132
View File
@@ -0,0 +1,132 @@
#pragma once
// A route written in a message -- a bot's {path} / !path reply ("3f:a1:c8",
// "3fa1:c802") -- placed on the map: this device, then each hop's repeater,
// then the node the message came from when it has a position. The route a
// reply carries is the way our own message went to the bot, so it starts
// here.
//
// Finding one: two or more hex groups of one width (2 or 4 characters, i.e.
// 1- or 2-byte hashes) joined by ':', standing alone. A clock time (12:30,
// 12:30:45) reads the same, so all-decimal groups that make a time aren't
// one. A single hop ("3f") is too like an ordinary word to tell.
//
// Placing a hop: a repeater with a position whose key starts with the hash
// (PacketFlow::placeHash); of several, the nearest the point before it. A hop
// that can't be placed is left out of the line, still counted and named.
//
// Header-only Core, single TU (see UiCore.h).
class HopRoute {
public:
static const int MAX_HOPS = 16;
static const int MAX_PTS = MAX_HOPS + 2; // here, the hops, the sender
struct Pt {
int32_t lat, lon;
int16_t node; // the contact (getContactByIdx), -1 this device
int8_t hop; // its hop (0-based), -1 here, -2 the sender
};
// The first route in `text`: its hashes (w bytes each) into `hashes`, the
// count into `n`.
static bool find(const char* text, uint8_t* hashes, uint8_t& n, uint8_t& w) {
for (const char* p = text; p && *p; p++) {
if (p != text && (isalnum((unsigned char)p[-1]) || p[-1] == ':')) continue;
int gw = 0;
while (isxdigit((unsigned char)p[gw])) gw++;
if (gw != 2 && gw != 4) continue;
uint8_t k = 0;
bool decimal = true, ok = true;
int vals[3] = {0, 0, 0};
const char* q = p;
for (;;) {
for (int i = 0; i < gw; i++) if (!isxdigit((unsigned char)q[i])) { ok = false; break; }
if (!ok || isalnum((unsigned char)q[gw])) { ok = false; break; }
if (k < MAX_HOPS) {
for (int b = 0; b < gw / 2; b++) hashes[k * (gw / 2) + b] = (uint8_t)((hexv(q[2 * b]) << 4) | hexv(q[2 * b + 1]));
}
for (int i = 0; i < gw; i++) if (!isdigit((unsigned char)q[i])) decimal = false;
if (k < 3 && gw == 2) vals[k] = (q[0] - '0') * 10 + (q[1] - '0');
k++;
q += gw;
if (*q != ':') break;
q++;
}
if (!ok || k < 2) continue;
if (decimal && gw == 2 && k <= 3 && vals[0] <= 23 && vals[1] <= 59 && (k < 3 || vals[2] <= 59)) continue; // a time
n = k > MAX_HOPS ? MAX_HOPS : k;
w = (uint8_t)(gw / 2);
return true;
}
return false;
}
static bool has(const char* text) {
uint8_t h[MAX_HOPS * 2], n, w;
return find(text, h, n, w);
}
// Places the route in `text` (see find()), from (own_lat, own_lon) when
// `own`, to the contact keyed `sender_key` (prefix of 4+ bytes) or named
// `sender_name` when one has a position. False when the text holds none.
bool show(const char* text, bool own, int32_t own_lat, int32_t own_lon,
const uint8_t* sender_key, const char* sender_name) {
if (!find(text, _hash, _n, _w)) return false;
_pts = 0;
_placed = 0;
if (own) addPt(own_lat, own_lon, -1, -1);
for (uint8_t i = 0; i < _n; i++) {
const bool near = _pts > 0;
int32_t la, lo;
int16_t idx;
if (PacketFlow::placeHash(&_hash[i * _w], _w, true, near ? _pt[_pts - 1].lat : 0, near ? _pt[_pts - 1].lon : 0,
near, la, lo, idx)) {
addPt(la, lo, idx, (int8_t)i);
_placed++;
}
}
ContactInfo* c = sender_key ? the_mesh.lookupContactByPubKey(sender_key, 4) : nullptr;
if (!c && sender_name && sender_name[0]) c = byName(sender_name);
if (c && (c->gps_lat || c->gps_lon)) addPt(c->gps_lat, c->gps_lon, -1, -2);
_active = true;
return true;
}
void close() { _active = false; }
bool active() const { return _active; }
uint8_t hops() const { return _n; }
uint8_t placed() const { return _placed; }
int points() const { return _pts; }
const Pt& point(int i) const { return _pt[i]; }
// A hop's name: its repeater's (as contactctl::hopName), else "?3F".
void hopName(int i, char* out, size_t len) const {
contactctl::hopName((uint8_t)((_w - 1) << 6 | _n), _hash, i, out, len);
}
// "3 of 4 hops on the map" (or "4 hops on the map").
void summary(char* out, size_t len) const {
if (_placed == _n) snprintf(out, len, "%u hop%s on the map", (unsigned)_n, _n == 1 ? "" : "s");
else snprintf(out, len, "%u of %u hops on the map", (unsigned)_placed, (unsigned)_n);
}
private:
static int hexv(char c) { return c <= '9' ? c - '0' : (c | 0x20) - 'a' + 10; }
void addPt(int32_t lat, int32_t lon, int16_t node, int8_t hop) {
if (_pts < MAX_PTS) _pt[_pts++] = { lat, lon, node, hop };
}
static ContactInfo* byName(const char* name) {
for (int idx = MAX_ANON_CONTACTS; ; idx++) {
ContactInfo c;
if (!the_mesh.getContactByIdx(idx, c)) return nullptr;
if (strcmp(c.name, name) == 0) return the_mesh.lookupContactByPubKey(c.id.pub_key, PUB_KEY_SIZE);
}
}
uint8_t _hash[MAX_HOPS * 2];
uint8_t _n = 0, _w = 1, _placed = 0;
Pt _pt[MAX_PTS];
int _pts = 0;
bool _active = false;
};
@@ -248,6 +248,7 @@ private:
return false;
}
public: // HopRoute places a route's hops the same way
// A contact with a position whose key starts with `hash` (`size` bytes):
// repeaters only when `repeaters`; with several, the nearest to (nlat, nlon)
// when `near`, else the first.
@@ -268,6 +269,7 @@ private:
return found;
}
private:
// The contact's index (getContactByIdx) by its key; -1 none.
static int16_t indexOf(const uint8_t* key) {
for (int idx = MAX_ANON_CONTACTS; ; idx++) {
@@ -30,6 +30,7 @@
#include "AdminSession.h"
#include "RoomSessions.h"
#include "ContactControl.h"
#include "HopRoute.h" // after ContactControl (hopName)
class UiCore : public MyMesh::Listener {
public:
@@ -71,6 +72,7 @@ public:
PathEditor route; // route to a node: edit, trace out and back, save
CourseEngine course; // GPS position + course-over-ground ring
PacketFlow flow; // packets heard, as lines between nodes for the map
HopRoute hop_route; // a route written in a message ({path}), placed for the map
LiveShareEngine live_share; // [LOC] auto-share session + peers' shared positions
LocatorEngine locator; // active target, geofence crossings, proximity beeper
TrailEngine trail; // GPS trail store, sampling, auto-pause, shutdown save