mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-02 19:02:00 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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] = {};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user