From 85ef06b6be12ae7986f487187e89ff0006038aa5 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:24:24 +0200 Subject: [PATCH] feat(map): packet flow shows all traffic - kinds in colours, a legend, its own nodes - Every packet heard is drawn, by kind in fixed colours: advert, DM, channel, request, ack, other, sent. One that can't be placed on the map is a ring at your own position. - A legend pill at the top counts each kind over the last minute. - On a map without node markers, a flow brings its own nodes (a dot and the name), fading with it. - Copies of one packet (the same packet hash) share one fade, so a path goes out as a whole, not segment by segment. - 32 flows kept on ESP32 (12 on nRF). Co-Authored-By: Claude Opus 5.5 --- docs/solo/contacts.md | 8 +- examples/companion_radio/ui-core/PacketFlow.h | 153 ++++++++++++++---- examples/companion_radio/ui-lvgl/MapScreen.h | 115 +++++++++++-- examples/companion_radio/ui-lvgl/UITask.h | 1 + .../companion_radio/ui-new/NearbyScreen.h | 4 +- 5 files changed, 237 insertions(+), 44 deletions(-) diff --git a/docs/solo/contacts.md b/docs/solo/contacts.md index 1c423103..97ac0ee9 100644 --- a/docs/solo/contacts.md +++ b/docs/solo/contacts.md @@ -43,8 +43,12 @@ back out, **Enter** opens its details and **Hold Enter** its options. > [!NOTE] > **Wio Tracker L2:** **Packet flow** is in the map's options (On the map). -> Each packet runs along its line as a dot and fades, coloured by kind: -> adverts blue, direct messages green, channels amber, the rest violet. +> Every packet heard shows. One that can be placed runs along its line as a +> dot and fades. One that can't (its sender or repeaters have no position) +> is a ring around you. Colours by kind: adverts blue, direct messages +> green, channels amber, requests violet, acks and paths teal, the rest +> pink, your own white. The pill at the top counts each kind over the last +> minute. > [!NOTE] > **Wio Tracker L2:** the **Nodes** app. Filter chips at the top, the sort in diff --git a/examples/companion_radio/ui-core/PacketFlow.h b/examples/companion_radio/ui-core/PacketFlow.h index b9866ef2..0275c3ce 100644 --- a/examples/companion_radio/ui-core/PacketFlow.h +++ b/examples/companion_radio/ui-core/PacketFlow.h @@ -14,9 +14,14 @@ // sender's one-byte hash (matched like a hop, to any contact); // - its end: this device (the GPS, else the position set in Privacy). // Points without a position are skipped, so a flow may be shorter than its -// path or none at all (a group message from a node in direct range). -// A direct packet heard on its way elsewhere carries the hops still ahead: -// a pulse at the next one. +// path. A packet nothing of which can be placed (a group message from a node +// in direct range, a sender with no position) is still a pulse here: every +// packet heard shows. A direct packet heard on its way elsewhere carries the +// hops still ahead: a pulse at the next one (here when it isn't known). +// Each kind is counted over the last minute (count()), for a legend. +// One packet is often heard several times (direct, then via each repeater): +// its flows share one fading clock, restarted by every copy (t_fade), so the +// whole picture of it goes at once. // // Off (NodePrefs::flow_show 0) nothing is resolved: a contact scan per hop is // cheap, but not free on every packet. @@ -25,19 +30,53 @@ class PacketFlow { public: - static const int MAX_FLOWS = 12; +#if defined(ESP32) + static const int MAX_FLOWS = 32; // a busy mesh: a few seconds of it +#else + static const int MAX_FLOWS = 12; // the nRF52's heap is short +#endif static const int MAX_PTS = 10; // origin, up to 8 repeaters, this device - enum Kind : uint8_t { K_ADVERT, K_MSG, K_GROUP, K_OTHER, K_SENT, K_AHEAD }; + // What a packet is (its payload type), and one of our own. + enum Kind : uint8_t { K_ADVERT, K_MSG, K_GROUP, K_REQ, K_ACK, K_OTHER, K_SENT, K_COUNT }; struct Flow { uint32_t t_ms; // when it was heard + uint32_t t_fade; // when its fading clock starts: the last copy of the same packet heard + uint32_t id; // the packet (its hash): copies heard via other repeaters go together uint8_t kind; uint8_t n; // points (1 for a pulse) bool origin; // point 0 is the sender (else the first repeater placed) + bool here; // a pulse here: nothing of it could be placed int32_t lat[MAX_PTS], lon[MAX_PTS]; + int16_t node[MAX_PTS]; // the contact at each point (getContactByIdx), -1 this device / unknown }; + static uint8_t kindOf(uint8_t type) { + switch (type) { + case PAYLOAD_TYPE_ADVERT: return K_ADVERT; + case PAYLOAD_TYPE_TXT_MSG: return K_MSG; + case PAYLOAD_TYPE_GRP_TXT: + case PAYLOAD_TYPE_GRP_DATA: return K_GROUP; + case PAYLOAD_TYPE_REQ: + case PAYLOAD_TYPE_RESPONSE: + case PAYLOAD_TYPE_ANON_REQ: return K_REQ; + case PAYLOAD_TYPE_ACK: + case PAYLOAD_TYPE_PATH: return K_ACK; + default: return K_OTHER; // trace, control, multipart, raw + } + } + + // Packets of `kind` heard (K_SENT: sent) in the last minute. + int count(uint8_t kind) const { + if (kind >= K_COUNT) return 0; + const uint32_t now = millis() / BUCKET_MS; + int n = 0; + for (int b = 0; b < BUCKETS; b++) + if (now - _bucket_at[b] < BUCKETS) n += _cnt[b][kind]; + return n; + } + void begin(NodePrefs* prefs, CourseEngine* course, SensorManager* sensors) { _prefs = prefs; _course = course; _sensors = sensors; } @@ -58,17 +97,23 @@ public: f.t_ms = millis(); f.n = 0; f.origin = false; + f.here = false; + f.id = idOf(pkt); const uint8_t type = pkt->getPayloadType(); - f.kind = type == PAYLOAD_TYPE_ADVERT ? K_ADVERT - : type == PAYLOAD_TYPE_TXT_MSG ? K_MSG - : type == PAYLOAD_TYPE_GRP_TXT || type == PAYLOAD_TYPE_GRP_DATA ? K_GROUP : K_OTHER; + f.kind = kindOf(type); + tally(f.kind); const uint8_t hs = pkt->getPathHashSize(), hc = pkt->getPathHashCount(); + int32_t ola, olo; + const bool own = ownPosition(ola, olo); - if (pkt->isRouteDirect()) { // on its way elsewhere: a pulse at the next hop + // On its way elsewhere: a pulse at the next hop. (A trace's path is its + // SNRs, not hashes.) + if (pkt->isRouteDirect() || type == PAYLOAD_TYPE_TRACE) { int32_t lat, lon; - if (hc == 0 || !placeHash(pkt->path, hs, false, 0, 0, false, lat, lon)) return; - f.kind = K_AHEAD; - add(f, lat, lon); + int16_t idx; + if (type != PAYLOAD_TYPE_TRACE && hc > 0 && placeHash(pkt->path, hs, false, 0, 0, false, lat, lon, idx)) add(f, lat, lon, idx); + else if (own) { add(f, ola, olo); f.here = true; } + else return; push(f); return; } @@ -76,28 +121,36 @@ public: // Back from this device: each hash nearest the point after it, which // it was heard by -- the last repeater is the one heard here. int32_t lat[MAX_PTS], lon[MAX_PTS]; + int16_t node[MAX_PTS]; int k = MAX_PTS; - int32_t ola, olo; - const bool own = ownPosition(ola, olo); - if (own) { k--; lat[k] = ola; lon[k] = olo; } + if (own) { k--; lat[k] = ola; lon[k] = olo; node[k] = -1; } for (int i = hc - 1; i >= 0 && k > 1; i--) { int32_t a, b; + int16_t idx; const bool near = k < MAX_PTS; - if (placeHash(&pkt->path[i * hs], hs, true, near ? lat[k] : 0, near ? lon[k] : 0, near, a, b)) { - k--; lat[k] = a; lon[k] = b; + if (placeHash(&pkt->path[i * hs], hs, true, near ? lat[k] : 0, near ? lon[k] : 0, near, a, b, idx)) { + k--; lat[k] = a; lon[k] = b; node[k] = idx; } } int32_t a, b; - if (k > 0 && origin(pkt, k < MAX_PTS, k < MAX_PTS ? lat[k] : 0, k < MAX_PTS ? lon[k] : 0, a, b)) { - k--; lat[k] = a; lon[k] = b; + int16_t idx; + if (k > 0 && origin(pkt, k < MAX_PTS, k < MAX_PTS ? lat[k] : 0, k < MAX_PTS ? lon[k] : 0, a, b, idx)) { + k--; lat[k] = a; lon[k] = b; node[k] = idx; f.origin = true; } - for (int i = k; i < MAX_PTS; i++) add(f, lat[i], lon[i]); - if (f.n >= 2) push(f); + for (int i = k; i < MAX_PTS; i++) add(f, lat[i], lon[i], node[i]); + if (f.n < 2) { // only here, or nothing: a pulse here + if (!own) return; + f.n = 0; + add(f, ola, olo); + f.here = true; + } + push(f); } void onSent(const mesh::Packet* pkt) { if (!enabled() || !pkt) return; + tally(K_SENT); int32_t lat, lon; if (!ownPosition(lat, lon)) return; Flow f; @@ -105,19 +158,42 @@ public: f.kind = K_SENT; f.n = 0; f.origin = true; + f.here = false; + f.id = idOf(pkt); add(f, lat, lon); push(f); } - void clear() { _n = 0; _head = 0; _seq++; } + void clear() { _n = 0; _head = 0; _seq++; memset(_cnt, 0, sizeof(_cnt)); } private: - static void add(Flow& f, int32_t lat, int32_t lon) { - if (f.n > 0 && f.lat[f.n - 1] == lat && f.lon[f.n - 1] == lon) return; // the same spot twice - f.lat[f.n] = lat; f.lon[f.n] = lon; f.n++; + static const int BUCKETS = 6; // the minute, in 10 s steps + static const uint32_t BUCKET_MS = 10000; + + void tally(uint8_t kind) { + const uint32_t now = millis() / BUCKET_MS; + const int b = now % BUCKETS; + if (_bucket_at[b] != now) { _bucket_at[b] = now; memset(_cnt[b], 0, sizeof(_cnt[b])); } + if (_cnt[b][kind] < 0xFFFF) _cnt[b][kind]++; } - void push(const Flow& f) { + static void add(Flow& f, int32_t lat, int32_t lon, int16_t node = -1) { + if (f.n > 0 && f.lat[f.n - 1] == lat && f.lon[f.n - 1] == lon) return; // the same spot twice + f.lat[f.n] = lat; f.lon[f.n] = lon; f.node[f.n] = node; f.n++; + } + + static uint32_t idOf(const mesh::Packet* pkt) { + uint8_t h[MAX_HASH_SIZE]; + pkt->calculatePacketHash(h); + uint32_t id; + memcpy(&id, h, sizeof(id)); + return id; + } + + void push(Flow f) { + f.t_fade = f.t_ms; + for (int i = 0; i < _n; i++) // the earlier copies of it: their fade starts again with this one + if (_flows[i].id == f.id) _flows[i].t_fade = f.t_ms; _flows[_head] = f; _head = (_head + 1) % MAX_FLOWS; if (_n < MAX_FLOWS) _n++; @@ -134,9 +210,13 @@ private: // Where the packet came from, when it says (a one-byte hash: the contact // nearest the point after it, when there is one). - static bool origin(const mesh::Packet* pkt, bool near, int32_t nlat, int32_t nlon, int32_t& lat, int32_t& lon) { + static bool origin(const mesh::Packet* pkt, bool near, int32_t nlat, int32_t nlon, int32_t& lat, int32_t& lon, + int16_t& node) { const uint8_t type = pkt->getPayloadType(); + node = -1; if (type == PAYLOAD_TYPE_ADVERT) { + ContactInfo* c = the_mesh.lookupContactByPubKey(pkt->payload, PUB_KEY_SIZE); + if (c) node = indexOf(c->id.pub_key); const int app = PUB_KEY_SIZE + 4 + SIGNATURE_SIZE; // key, timestamp, signature, then the app data if (pkt->payload_len > app) { AdvertDataParser p(&pkt->payload[app], pkt->payload_len - app); @@ -145,13 +225,12 @@ private: return true; } } - ContactInfo* c = the_mesh.lookupContactByPubKey(pkt->payload, PUB_KEY_SIZE); if (c && (c->gps_lat || c->gps_lon)) { lat = c->gps_lat; lon = c->gps_lon; return true; } return false; } if ((type == PAYLOAD_TYPE_TXT_MSG || type == PAYLOAD_TYPE_REQ || type == PAYLOAD_TYPE_RESPONSE || type == PAYLOAD_TYPE_PATH) && pkt->payload_len >= 2) - return placeHash(&pkt->payload[1], 1, false, nlat, nlon, near, lat, lon); // [dest hash, src hash, ...] + return placeHash(&pkt->payload[1], 1, false, nlat, nlon, near, lat, lon, node); // [dest hash, src hash, ...] return false; } @@ -159,8 +238,9 @@ private: // repeaters only when `repeaters`; with several, the nearest to (nlat, nlon) // when `near`, else the first. static bool placeHash(const uint8_t* hash, uint8_t size, bool repeaters, int32_t nlat, int32_t nlon, bool near, - int32_t& lat, int32_t& lon) { + int32_t& lat, int32_t& lon, int16_t& node) { bool found = false; + node = -1; float best = 0; for (int idx = MAX_ANON_CONTACTS; ; idx++) { ContactInfo c; @@ -168,16 +248,27 @@ private: if (repeaters && c.type != ADV_TYPE_REPEATER) continue; if ((!c.gps_lat && !c.gps_lon) || memcmp(c.id.pub_key, hash, size) != 0) continue; float d = near ? geo::haversineKm(nlat, nlon, c.gps_lat, c.gps_lon) : 0; - if (!found || d < best) { best = d; lat = c.gps_lat; lon = c.gps_lon; found = true; } + if (!found || d < best) { best = d; lat = c.gps_lat; lon = c.gps_lon; node = (int16_t)idx; found = true; } if (!near) break; } return found; } + // 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++) { + ContactInfo c; + if (!the_mesh.getContactByIdx(idx, c)) return -1; + if (memcmp(c.id.pub_key, key, PUB_KEY_SIZE) == 0) return (int16_t)idx; + } + } + NodePrefs* _prefs = nullptr; CourseEngine* _course = nullptr; SensorManager* _sensors = nullptr; Flow _flows[MAX_FLOWS]; int _head = 0, _n = 0; uint32_t _seq = 0; + uint16_t _cnt[BUCKETS][K_COUNT] = {}; + uint32_t _bucket_at[BUCKETS] = {}; }; diff --git a/examples/companion_radio/ui-lvgl/MapScreen.h b/examples/companion_radio/ui-lvgl/MapScreen.h index d0fd33ca..b1d7b118 100644 --- a/examples/companion_radio/ui-lvgl/MapScreen.h +++ b/examples/companion_radio/ui-lvgl/MapScreen.h @@ -271,19 +271,40 @@ static bool s_view_ok = false; static const uint32_t FLOW_LIFE = 7000; // ms a flow stays on the map static const uint32_t FLOW_TRAVEL = 1500; // ms the dot takes from end to end static const uint32_t PULSE_MS = 1400; +static bool s_flow_nodes = false; // no node markers on this map: a flow draws its own, while it lasts static lv_area_t s_flow_area; // what the flows covered at the last tick static bool s_flow_area_on = false; +// Fixed colours, not the accent: a kind looks the same whatever the theme. static uint32_t flowColour(uint8_t kind) { switch (kind) { case PacketFlow::K_ADVERT: return 0x4FC3F7; // blue - case PacketFlow::K_MSG: return theme::OK; // green - case PacketFlow::K_GROUP: return theme::ACCENT; + case PacketFlow::K_MSG: return 0x6FCF6F; // green + case PacketFlow::K_GROUP: return 0xFFB000; // amber + case PacketFlow::K_REQ: return 0xB39DDB; // violet + case PacketFlow::K_ACK: return 0x80CBC4; // teal case PacketFlow::K_SENT: return theme::TEXT; - case PacketFlow::K_AHEAD: return theme::TEXT_MUTED; - default: return 0xB39DDB; // violet + default: return 0xF48FB1; // pink: trace, control ... } } +static const char* const FLOW_NAMES[PacketFlow::K_COUNT] = { "Adv", "DM", "Ch", "Req", "Ack", "Other", "Sent" }; +static lv_obj_t* s_flow_legend = nullptr; // top centre: each kind's count over the last minute + +// "Adv 3 Ch 5 ...", each in its colour; hidden with nothing to count. +static void flowLegendRefresh() { + if (!s_flow_legend) return; + char t[160]; + int o = 0; + for (int k = 0; s_flow && s_flow->enabled() && k < PacketFlow::K_COUNT; k++) { + const int n = s_flow->count(k); + if (n > 0 && o < (int)sizeof(t) - 24) + o += snprintf(t + o, sizeof(t) - o, "%s#%06lX %s %d#", o ? " " : "", (unsigned long)flowColour(k), FLOW_NAMES[k], n); + } + t[o] = '\0'; + if (!o) { lv_obj_add_flag(s_flow_legend, LV_OBJ_FLAG_HIDDEN); return; } + setText(s_flow_legend, t); + lv_obj_remove_flag(s_flow_legend, LV_OBJ_FLAG_HIDDEN); +} static void flowPx(int32_t lat, int32_t lon, int32_t& x, int32_t& y) { double fx = lonToTileX(lon / 1e6, s_view_z) * TILE_PX - s_view_left; @@ -299,7 +320,7 @@ static bool flowArea(lv_area_t& r) { bool any = false; for (int i = 0; i < s_flow->count(); i++) { const PacketFlow::Flow& f = s_flow->at(i); - if (now - f.t_ms >= FLOW_LIFE) break; // newest first: the rest are older + if (now - f.t_fade >= FLOW_LIFE) continue; // (an older one may live on with a newer copy) for (int k = 0; k < f.n; k++) { int32_t x, y; flowPx(f.lat[k], f.lon[k], x, y); @@ -307,10 +328,71 @@ static bool flowArea(lv_area_t& r) { r.x1 = min(r.x1, x); r.y1 = min(r.y1, y); r.x2 = max(r.x2, x); r.y2 = max(r.y2, y); } } - if (any) { r.x1 -= 28; r.y1 -= 28; r.x2 += 28; r.y2 += 28; } + if (any) { r.x1 -= 28; r.y1 -= 28; r.x2 += s_flow_nodes ? 120 : 28; r.y2 += 28; } // (a node's name to its right) return any; } +// On a map without node markers, the nodes a flow passes -- its sender and +// repeaters, a dot and the name -- shown while it lasts and fading with it, +// each once however many flows share it (the most visible one counts). +static void drawFlowNodes(lv_layer_t* layer, const lv_area_t& a, uint32_t now) { + struct Node { int16_t idx; lv_opa_t opa; int32_t x, y; uint8_t kind; }; + Node nodes[24]; + int nn = 0; + for (int i = 0; i < s_flow->count(); i++) { // newest first + const PacketFlow::Flow& f = s_flow->at(i); + const uint32_t age = now - (f.n == 1 ? f.t_ms : f.t_fade); // a pulse's node: as long as the pulse + const uint32_t life = f.n == 1 ? PULSE_MS : FLOW_LIFE; + if (age >= life) continue; + const lv_opa_t opa = (lv_opa_t)(age < FLOW_TRAVEL ? 255 : 255 - 255 * (age - FLOW_TRAVEL) / (life - FLOW_TRAVEL + 1)); + for (int k = 0; k < f.n; k++) { + if (f.node[k] < 0) continue; + int j = 0; + while (j < nn && nodes[j].idx != f.node[k]) j++; + if (j < nn) { if (opa > nodes[j].opa) nodes[j].opa = opa; continue; } + if (nn == (int)(sizeof(nodes) / sizeof(nodes[0]))) continue; + int32_t x, y; + flowPx(f.lat[k], f.lon[k], x, y); + nodes[nn++] = { f.node[k], opa, a.x1 + x, a.y1 + y, f.kind }; + } + } + lv_draw_rect_dsc_t dot, pill; + lv_draw_rect_dsc_init(&dot); + dot.radius = LV_RADIUS_CIRCLE; + dot.border_color = lv_color_hex(theme::BG); + dot.border_width = 2; + lv_draw_rect_dsc_init(&pill); + pill.bg_color = lv_color_hex(theme::BG); + pill.radius = 4; + lv_draw_label_dsc_t ld; + lv_draw_label_dsc_init(&ld); + ld.font = THEME_FONT_SMALL; + ld.color = lv_color_hex(theme::TEXT); + ld.text_local = true; + const int32_t lh = lv_font_get_line_height(THEME_FONT_SMALL); + for (int j = 0; j < nn; j++) { + const Node& n = nodes[j]; + if (n.x < a.x1 - 120 || n.x > a.x2 + 8 || n.y < a.y1 - 20 || n.y > a.y2 + 20) continue; + dot.bg_color = lv_color_hex(flowColour(n.kind)); + dot.bg_opa = dot.border_opa = n.opa; + lv_area_t da = { n.x - 5, n.y - 5, n.x + 5, n.y + 5 }; + lv_draw_rect(layer, &dot, &da); + ContactInfo c; + if (!the_mesh.getContactByIdx(n.idx, c) || !c.name[0]) continue; + char name[20]; + snprintf(name, sizeof(name), "%s", c.name); + lv_point_t sz; + lv_text_get_size(&sz, name, THEME_FONT_SMALL, 0, 0, LV_COORD_MAX, LV_TEXT_FLAG_NONE); + lv_area_t pa = { n.x + 8, n.y - lh / 2 - 1, n.x + 8 + sz.x + 9, n.y + lh / 2 + 1 }; + pill.bg_opa = (lv_opa_t)(n.opa * 4 / 5); + lv_draw_rect(layer, &pill, &pa); + lv_area_t ta = { pa.x1 + 5, pa.y1 + 1, pa.x2 - 4, pa.y2 }; + ld.text = name; + ld.opa = n.opa; + lv_draw_label(layer, &ld, &ta); + } +} + static void drawFlows(lv_layer_t* layer, const lv_area_t& a) { if (!s_flow || !s_view_ok || !s_flow->enabled()) return; const uint32_t now = millis(); @@ -334,8 +416,9 @@ static void drawFlows(lv_layer_t* layer, const lv_area_t& a) { dot.border_opa = LV_OPA_COVER; for (int i = s_flow->count() - 1; i >= 0; i--) { // oldest first: the newest on top const PacketFlow::Flow& f = s_flow->at(i); - const uint32_t age = now - f.t_ms; - if (age >= FLOW_LIFE) continue; + const uint32_t age = now - f.t_ms; // the dot's run + const uint32_t fade = now - f.t_fade; // the fading: with every copy of the packet + if (fade >= FLOW_LIFE) continue; const lv_color_t col = lv_color_hex(flowColour(f.kind)); int32_t px[PacketFlow::MAX_PTS], py[PacketFlow::MAX_PTS]; for (int k = 0; k < f.n; k++) { flowPx(f.lat[k], f.lon[k], px[k], py[k]); px[k] += a.x1; py[k] += a.y1; } @@ -359,7 +442,7 @@ static void drawFlows(lv_layer_t* layer, const lv_area_t& a) { const float t = age >= FLOW_TRAVEL ? 1.0f : (float)age / FLOW_TRAVEL; float left = total * t; ln.color = col; - ln.opa = (lv_opa_t)(age < FLOW_TRAVEL ? 255 : 255 - 215 * (age - FLOW_TRAVEL) / (FLOW_LIFE - FLOW_TRAVEL)); + ln.opa = (lv_opa_t)(fade < FLOW_TRAVEL ? 255 : 255 - 215 * (fade - FLOW_TRAVEL) / (FLOW_LIFE - FLOW_TRAVEL)); halo.opa = (lv_opa_t)(ln.opa * 2 / 3); int32_t hx = px[0], hy = py[0]; // the dot int32_t ex_[PacketFlow::MAX_PTS], ey_[PacketFlow::MAX_PTS]; @@ -391,6 +474,7 @@ static void drawFlows(lv_layer_t* layer, const lv_area_t& a) { lv_draw_arc(layer, &ring); } } + if (s_flow_nodes) drawFlowNodes(layer, a, now); } static const int MARK_D = 12; // marker dot diameter @@ -750,6 +834,14 @@ void UITask::buildMap() { lv_obj_set_ext_click_area(_map_dl_pill, 6); lv_obj_add_event_cb(_map_dl_pill, onMapDownload, LV_EVENT_CLICKED, NULL); lv_obj_add_flag(_map_dl_pill, LV_OBJ_FLAG_HIDDEN); + mapview::s_flow_legend = mapPill(mapview::s_going, ""); // packet flow: what's been heard + lv_label_set_recolor(mapview::s_flow_legend, true); + lv_obj_set_style_max_width(mapview::s_flow_legend, 212, 0); // a busy minute wraps to a second line + lv_obj_set_style_text_align(mapview::s_flow_legend, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_radius(mapview::s_flow_legend, 8, 0); + lv_obj_add_flag(mapview::s_flow_legend, LV_OBJ_FLAG_HIDDEN); + lv_obj_add_event_cb(mapview::s_flow_legend, [](lv_event_t*) { mapview::s_flow_legend = nullptr; }, LV_EVENT_DELETE, NULL); + mapview::flowLegendRefresh(); // Bottom left, one pill about the map itself: the map data credit (the // licences require it on the map; a tap shows the full line, also in // Settings > About), a bar one grid step long with its distance, the zoom. @@ -981,7 +1073,9 @@ void UITask::rebuildMapMarkers() { mapview::s_mark_prev = mapview::s_mark_count; mapview::s_mark_count = 0; if (_map_nav) rebuildNavMarkers(); - if (_map_nodes || !_map_nav) rebuildNodeMarkers(); + const bool nodes = _map_nodes || !_map_nav; + if (nodes) rebuildNodeMarkers(); + mapview::s_flow_nodes = !nodes; // the flows bring theirs mapview::s_mark_prev = 0; } @@ -1202,6 +1296,7 @@ void UITask::mapFlowTick() { using namespace mapview; if (!s_mark_layer || (int32_t)(millis() - _next_flow_ms) < 0) return; _next_flow_ms = millis() + 50; + if (++_flow_legend_tick >= 10) { _flow_legend_tick = 0; flowLegendRefresh(); } // twice a second lv_area_t r; bool now_on = flowArea(r); if (!now_on && !s_flow_area_on) return; diff --git a/examples/companion_radio/ui-lvgl/UITask.h b/examples/companion_radio/ui-lvgl/UITask.h index 48f077eb..75a16bc8 100644 --- a/examples/companion_radio/ui-lvgl/UITask.h +++ b/examples/companion_radio/ui-lvgl/UITask.h @@ -656,6 +656,7 @@ private: uint32_t _route_shown = 0xFFFFFFFF; // what routeRebuild() drew (routeSig()) uint32_t _next_route_ms = 0; uint32_t _next_flow_ms = 0; // the map's packet flow repaint + uint8_t _flow_legend_tick = 0; // its legend, every 10th repaint bool _route_pick = false; // the nodes map taps repeaters into the route lv_obj_t* _route_bar = nullptr; lv_obj_t* _route_lbl = nullptr; diff --git a/examples/companion_radio/ui-new/NearbyScreen.h b/examples/companion_radio/ui-new/NearbyScreen.h index ab41f599..1aec5553 100644 --- a/examples/companion_radio/ui-new/NearbyScreen.h +++ b/examples/companion_radio/ui-new/NearbyScreen.h @@ -287,6 +287,7 @@ class NearbyScreen : public UIScreen, protected NearbyModel { 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 + static const uint32_t FLOW_HERE_MS = 3000; // and a ring here, for one placed nowhere else 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 @@ -945,7 +946,8 @@ class NearbyScreen : public UIScreen, protected NearbyModel { 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 (f.n == 1) { // a packet nothing of which could be placed: a ring here, briefly + if (f.here && millis() - f.t_ms > FLOW_HERE_MS) continue; if (inside(px[0], py[0])) gfx::drawCircle(d, px[0], py[0], 6 * s); continue; }