feat(map): packet flow - what the radio hears, as lines between nodes

Every packet heard runs from its origin through the repeaters with a
position to this device; a packet sent is a ring. ui-core/PacketFlow.h
resolves them (UiCore::flow, MyMesh logRx/logTx -> Listener). ui-lvgl
draws them on the map's marker layer and repaints only their area while
they move; ui-new draws the last minute on the Nodes map ("Flow" in its
options). Setting: Map options > On the map > Packet flow, CLI map.flow.
Off by default. Sim-checked only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-14 12:50:43 +02:00
co-authored by Claude Opus 5.5
parent 120ddfc830
commit a8440c890e
15 changed files with 478 additions and 7 deletions
@@ -25,6 +25,22 @@ static inline void drawLine(DisplayDriver& d, int x0, int y0, int x1, int y1) {
}
}
// The part of a line inside a box (x, y, w, h), Liang-Barsky; nothing when
// it misses the box. For lines whose ends may lie far off the screen.
static inline void drawLineClipped(DisplayDriver& d, int x0, int y0, int x1, int y1, int bx, int by, int bw, int bh) {
float t0 = 0, t1 = 1;
const float dx = (float)(x1 - x0), dy = (float)(y1 - y0);
const float p[4] = { -dx, dx, -dy, dy };
const float q[4] = { (float)(x0 - bx), (float)(bx + bw - 1 - x0), (float)(y0 - by), (float)(by + bh - 1 - y0) };
for (int i = 0; i < 4; i++) {
if (p[i] == 0) { if (q[i] < 0) return; continue; }
float r = q[i] / p[i];
if (p[i] < 0) { if (r > t1) return; if (r > t0) t0 = r; }
else { if (r < t0) return; if (r < t1) t1 = r; }
}
drawLine(d, x0 + (int)(t0 * dx), y0 + (int)(t0 * dy), x0 + (int)(t1 * dx), y0 + (int)(t1 * dy));
}
// Midpoint-circle outline.
static inline void drawCircle(DisplayDriver& d, int cx, int cy, int r) {
int x = r, y = 0, err = 1 - r;
+54 -2
View File
@@ -263,7 +263,7 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
// ── action-menu actions (matched by id, not by row index) ────────────────────
enum Action : uint8_t { ACT_NAV, ACT_PING, ACT_WAYPOINT, ACT_LOCATOR,
ACT_ADD, ACT_DELETE, ACT_FAV, ACT_PIN, ACT_ADMIN, ACT_SORT, ACT_SCAN, ACT_MAP,
ACT_TELEM, ACT_RESET_PATH, ACT_SHARE, ACT_ROUTE };
ACT_TELEM, ACT_RESET_PATH, ACT_SHARE, ACT_ROUTE, ACT_FLOW };
// Set by UITask::pickAdminTarget() (Tools > Admin, which is remote-only):
// while true, ENTER on an eligible row (a stored repeater/room contact) hands
@@ -286,6 +286,8 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
bool _detail;
bool _nav = false; // full-screen navigate-to-node view (over detail)
bool _map = false; // the nodes with a position on a map (under detail)
static const uint32_t FLOW_SHOW_MS = 60000; // packet flow lines stay this long
uint32_t _flow_seen = 0;
int _map_zoom = 0; // 0: all of them in view; each step halves it, on the selected
navview::EtaTracker _nav_eta; // closing-speed/ETA for the navigate view
unsigned long _detail_refresh_ms;
@@ -552,6 +554,7 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
}
char _fav_label[12]; // "Fav: ON" / "Fav: OFF" -- rewritten in place by L/R
char _flow_label[12]; // "Flow: ON" / "Flow: OFF" -- the map's packet flow
char _pin_label[24]; // "Pin to dial" / "Unpin (slot N)" -- _menu stores the pointer
char _telem_label[20]; // "Telem: ..." -- rewritten in place by L/R
@@ -598,6 +601,10 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
};
if (stored && !_map && positioned(0, 1) >= 0) add("Map", ACT_MAP);
if (_map) { // packet flow: what the radio hears, as lines (ui-core/PacketFlow.h)
buildFlowLabel();
addValue(_flow_label, ACT_FLOW);
}
if (has_gps) add("Navigate", ACT_NAV);
if (has_key) add("Ping", ACT_PING);
if (has_gps) add("Save waypoint", ACT_WAYPOINT);
@@ -645,6 +652,11 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
// Advance the value on the menu's value rows (Sort, Fav, Telem). Sort and Fav
// are two-state, so LEFT and RIGHT do the same there; Telem steps by `dir`.
void buildFlowLabel() {
NodePrefs* p = _task->getNodePrefs();
snprintf(_flow_label, sizeof(_flow_label), "Flow: %s", p && p->flow_show ? "ON" : "OFF");
}
void cycleMenuValue(int i, int dir = 1) {
if (i < 0 || i >= _menu_action_count) return;
if (_menu_actions[i] == ACT_SORT) {
@@ -653,6 +665,13 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
refresh();
} else if (_menu_actions[i] == ACT_FAV) {
toggleFavSelected();
} else if (_menu_actions[i] == ACT_FLOW) {
NodePrefs* p = _task->getNodePrefs();
if (!p) return;
p->flow_show = p->flow_show ? 0 : 1;
the_mesh.savePrefs();
if (!p->flow_show) _task->core().flow.clear();
buildFlowLabel();
} else if (_menu_actions[i] == ACT_TELEM) {
const Entry* e = selected();
ContactInfo* ci = e ? the_mesh.lookupContactByPubKey(e->pub_key, PUB_KEY_SIZE) : nullptr;
@@ -733,6 +752,7 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
break;
}
case ACT_SORT: break; // value rows -- see cycleMenuValue()
case ACT_FLOW: break;
case ACT_SCAN: enterScan(); break;
case ACT_MAP: {
_map = true; _map_zoom = 0;
@@ -916,6 +936,25 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
if (!inside(x, y)) continue;
miniIconDrawCentered(d, x, y, _entries[i].is_live ? ICON_MAP_CONTACT : ICON_MAP_DOT);
}
// Packet flow (ui-core/PacketFlow.h): the lines of the packets heard in
// the last minute, the newest drawn twice as thick; a ring for a packet
// sent, or heard on its way to a repeater.
const PacketFlow& fl = _task->core().flow;
for (int i = 0; fl.enabled() && i < fl.count(); i++) {
const PacketFlow::Flow& f = fl.at(i);
if (millis() - f.t_ms > FLOW_SHOW_MS) break; // newest first
int px[PacketFlow::MAX_PTS], py[PacketFlow::MAX_PTS];
for (int k = 0; k < f.n; k++) project(f.lat[k], f.lon[k], px[k], py[k]);
if (f.n == 1) {
if (inside(px[0], py[0])) gfx::drawCircle(d, px[0], py[0], 6 * s);
continue;
}
const bool bold = i < 2;
for (int k = 1; k < f.n; k++) {
gfx::drawLineClipped(d, px[k - 1], py[k - 1], px[k], py[k], ax, ay, aw, ah);
if (bold) gfx::drawLineClipped(d, px[k - 1] + 1, py[k - 1], px[k] + 1, py[k], ax, ay, aw, ah);
}
}
if (_own_gps) { int x, y; project(_own_lat, _own_lon, x, y); if (inside(x, y)) miniIconDrawCentered(d, x, y, ICON_MAP_CURRENT); }
if (target) { int x, y; project(tla, tlo, x, y); if (inside(x, y)) miniIconDrawCentered(d, x, y, ICON_MAP_TARGET); }
const int r = 4 * s;
@@ -1093,7 +1132,9 @@ public:
_detail_refresh_ms = millis();
}
renderMap(display);
return renderActivePopup(display) ? 50 : 2000;
if (renderActivePopup(display)) return 50;
const PacketFlow& fl = _task->core().flow; // new lines show within a second
return fl.enabled() && (fl.seq() != _flow_seen || fl.active(FLOW_SHOW_MS)) ? (_flow_seen = fl.seq(), 1000) : 2000;
}
// ── list view ────────────────────────────────────────────────────────────
@@ -1220,6 +1261,17 @@ public:
return (mq_delay > 0 && mq_delay < ret) ? mq_delay : ret;
}
// The nodes map, from outside the list (the sim harness).
bool openMap() {
int i = positioned(0, 1);
if (i < 0) return false;
_sel = i;
_detail = false; // the map is drawn from the list
_map = true; _map_zoom = 0;
_detail_refresh_ms = millis();
return true;
}
// Path / trace for a contact, from outside the list (the sim harness).
bool openRoute(const uint8_t* pub_key) {
int i = findContact(pub_key);
@@ -256,6 +256,10 @@ void UITask::openAdminFor(const ContactInfo& ci, bool from_picker) {
setCurrScreen(admin_screen); // runs AdminScreen::onShow()'s reset first
((AdminScreen*)admin_screen)->startFor(ci, from_picker);
}
bool UITask::openNodesMap() {
setCurrScreen(nearby_screen);
return ((NearbyScreen*)nearby_screen)->openMap();
}
void UITask::openRouteFor(const uint8_t* pub_key) {
setCurrScreen(nearby_screen); // its onShow() reset first
((NearbyScreen*)nearby_screen)->openRoute(pub_key);
+1
View File
@@ -331,6 +331,7 @@ public:
void openAdminFor(const ContactInfo& ci, bool from_picker); // canonical Admin entry for a specific target (Nodes' Hold-Enter menu or the picker above)
void openRouteFor(const uint8_t* pub_key); // Nodes > a contact > Path / trace (sim harness)
void openMessagesList() { gotoMessagesScreen(); } // sim harness
bool openNodesMap(); // sim harness: Nodes > Map
void gotoNearbyScreen();
void gotoDashboardConfig();
void gotoAutoAdvertScreen();