Files
HaloKeymind/src/helpers/SimpleMeshTables.h
T

547 lines
18 KiB
C++

#pragma once
#include <Mesh.h>
#if ARDUINO
#include <Arduino.h>
#endif
#ifdef ESP32
#include <FS.h>
// TFT_eSPI (pulled in by the tracker variants' display driver) defines
// FS_NO_GLOBALS, which suppresses FS.h's own `using fs::File`. Without this,
// File never reaches global scope and every TU that reaches FS.h through
// TFT_eSPI first fails with "'File' has not been declared" -- here and in
// simple_repeater/MyMesh.h. Restore exactly what FS.h would have done.
// Cannot use fs::File explicitly instead: File is also the global type on the
// nRF52/RP2040 paths, which have no fs namespace.
#if defined(FS_NO_GLOBALS)
using fs::File;
#endif
#endif
#define MAX_PACKET_HASHES (128+32)
#ifndef MAX_PACKET_ACKS
#define MAX_PACKET_ACKS 64
#endif
#if MAX_PACKET_ACKS < 1
#error "MAX_PACKET_ACKS must be at least 1"
#endif
#define ACK_VALID_BYTES ((MAX_PACKET_ACKS + 7) / 8)
#define MAX_ROUTE_HASH_BYTES 3
inline bool routeHashPrefixesOverlap(const uint8_t* a, uint8_t a_len,
const uint8_t* b, uint8_t b_len) {
if (a == NULL || b == NULL || a_len == 0 || b_len == 0
|| a_len > MAX_ROUTE_HASH_BYTES || b_len > MAX_ROUTE_HASH_BYTES) {
return false;
}
uint8_t compare_len = a_len < b_len ? a_len : b_len;
return memcmp(a, b, compare_len) == 0;
}
class SimpleMeshTables : public mesh::MeshTables {
public:
struct RecentRepeaterInfo {
// Identity and link quality for a next-hop path prefix.
uint8_t prefix[MAX_ROUTE_HASH_BYTES];
uint8_t prefix_len;
int8_t snr_x4;
uint32_t last_heard_millis;
};
private:
uint8_t _hashes[MAX_PACKET_HASHES*MAX_HASH_SIZE];
int _next_idx;
uint8_t _ack_hashes[MAX_PACKET_ACKS*MAX_HASH_SIZE];
uint8_t _ack_valid[ACK_VALID_BYTES];
int _next_ack_idx;
uint32_t _direct_dups, _flood_dups;
RecentRepeaterInfo* _recent_repeaters;
int _max_recent_repeaters;
int _recent_repeater_count;
bool hasSeenHash(const uint8_t* hash) const {
const uint8_t* sp = _hashes;
for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) {
if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) {
return true;
}
}
return false;
}
void storeHash(const uint8_t* hash) {
memcpy(&_hashes[_next_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE);
_next_idx = (_next_idx + 1) % MAX_PACKET_HASHES;
}
bool isDedicatedAckPacket(const mesh::Packet* packet) const {
if (packet->getPayloadType() == PAYLOAD_TYPE_ACK) {
return packet->payload_len >= sizeof(uint32_t);
}
return packet->getPayloadType() == PAYLOAD_TYPE_MULTIPART
&& packet->payload_len >= sizeof(uint32_t) + 1
&& (packet->payload[0] & 0x0F) == PAYLOAD_TYPE_ACK;
}
bool isAckSlotValid(int idx) const {
return (_ack_valid[idx >> 3] & (uint8_t)(1U << (idx & 7))) != 0;
}
void setAckSlotValid(int idx, bool valid) {
uint8_t mask = (uint8_t)(1U << (idx & 7));
if (valid) {
_ack_valid[idx >> 3] |= mask;
} else {
_ack_valid[idx >> 3] &= (uint8_t)~mask;
}
}
bool hasSeenAckHash(const uint8_t* hash) const {
for (int i = 0; i < MAX_PACKET_ACKS; i++) {
if (isAckSlotValid(i)
&& memcmp(&_ack_hashes[i*MAX_HASH_SIZE], hash, MAX_HASH_SIZE) == 0) {
return true;
}
}
return false;
}
void storeAckHash(const uint8_t* hash) {
if (hasSeenAckHash(hash)) return;
memcpy(&_ack_hashes[_next_ack_idx*MAX_HASH_SIZE], hash, MAX_HASH_SIZE);
setAckSlotValid(_next_ack_idx, true);
_next_ack_idx = (_next_ack_idx + 1) % MAX_PACKET_ACKS;
}
void clearAckHash(const uint8_t* hash) {
for (int i = 0; i < MAX_PACKET_ACKS; i++) {
if (isAckSlotValid(i)
&& memcmp(&_ack_hashes[i*MAX_HASH_SIZE], hash, MAX_HASH_SIZE) == 0) {
setAckSlotValid(i, false);
return;
}
}
}
int8_t weightedSnrX4RoundUp(int8_t curr_snr_x4, int8_t new_snr_x4) const {
// Keep existing SNR heavier than a single new sample: 75% existing + 25% new.
int32_t weighted_sum = ((int32_t)curr_snr_x4 * 3) + (int32_t)new_snr_x4;
int32_t blended = weighted_sum / 4; // truncates toward zero
// "Round up" means ceil(), which only differs from truncation for positive remainders.
if (weighted_sum > 0 && (weighted_sum % 4) != 0) {
blended++;
}
if (blended > 127) {
blended = 127;
} else if (blended < -128) {
blended = -128;
}
return (int8_t)blended;
}
bool extractRecentRepeater(const mesh::Packet* packet, uint8_t* prefix, uint8_t& prefix_len) const {
// Learn repeater prefixes only from packet shapes that expose a trustworthy repeater ID.
// For flood traffic, the last path entry is the repeater we directly heard.
if (packet->isRouteFlood() && packet->getPathHashCount() > 0) {
prefix_len = packet->getPathHashSize();
if (prefix_len > MAX_ROUTE_HASH_BYTES) {
prefix_len = MAX_ROUTE_HASH_BYTES;
}
const uint8_t* last_hop = &packet->path[(packet->getPathHashCount() - 1) * packet->getPathHashSize()];
memcpy(prefix, last_hop, prefix_len);
return true;
}
// If there is no flood path to inspect, fall back to payload-derived identities.
if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->payload_len >= PUB_KEY_SIZE) {
memcpy(prefix, packet->payload, MAX_ROUTE_HASH_BYTES);
prefix_len = MAX_ROUTE_HASH_BYTES;
return true;
}
if (packet->getPayloadType() == PAYLOAD_TYPE_CONTROL
&& packet->isRouteDirect()
&& packet->getPathHashCount() == 0
&& packet->payload_len >= 6 + MAX_ROUTE_HASH_BYTES
&& (packet->payload[0] & 0xF0) == 0x90) {
memcpy(prefix, &packet->payload[6], MAX_ROUTE_HASH_BYTES);
prefix_len = MAX_ROUTE_HASH_BYTES;
return true;
}
return false;
}
bool recentRepeaterComesBefore(const RecentRepeaterInfo& a, int a_idx,
const RecentRepeaterInfo& b, int b_idx) const {
if (a.prefix_len != b.prefix_len) {
return a.prefix_len > b.prefix_len; // 3-byte prefixes, then 2-byte, then 1-byte.
}
if (a.snr_x4 != b.snr_x4) {
return a.snr_x4 > b.snr_x4; // Highest SNR first within each prefix length.
}
int cmp = memcmp(a.prefix, b.prefix, a.prefix_len);
if (cmp != 0) {
return cmp < 0;
}
return a_idx < b_idx;
}
bool recentRepeaterMatchesFilter(const RecentRepeaterInfo& info,
const uint8_t* search_prefix,
uint8_t search_prefix_len) const {
if (search_prefix_len == 0) return true;
return search_prefix != NULL && search_prefix_len <= MAX_ROUTE_HASH_BYTES
&& routeHashPrefixesOverlap(info.prefix, info.prefix_len,
search_prefix, search_prefix_len);
}
const RecentRepeaterInfo* getRecentRepeaterBySortedIdxFiltered(
int idx_wanted, const uint8_t* search_prefix,
uint8_t search_prefix_len) const {
if (_max_recent_repeaters == 0 || idx_wanted < 0
|| search_prefix_len > MAX_ROUTE_HASH_BYTES
|| (search_prefix_len > 0 && search_prefix == NULL)) {
return NULL;
}
const RecentRepeaterInfo* last = NULL;
int last_idx = -1;
for (int rank = 0; rank <= idx_wanted; rank++) {
const RecentRepeaterInfo* best = NULL;
int best_idx = -1;
for (int i = 0; i < _recent_repeater_count; i++) {
const RecentRepeaterInfo* info = &_recent_repeaters[i];
if (!recentRepeaterMatchesFilter(*info, search_prefix,
search_prefix_len)) {
continue;
}
if (last != NULL && !recentRepeaterComesBefore(*last, last_idx, *info, i)) {
continue;
}
if (best == NULL || recentRepeaterComesBefore(*info, i, *best, best_idx)) {
best = info;
best_idx = i;
}
}
if (best == NULL) return NULL;
last = best;
last_idx = best_idx;
}
return last;
}
void recordRecentRepeater(const mesh::Packet* packet) {
if (_max_recent_repeaters == 0) {
return;
}
uint8_t prefix[MAX_ROUTE_HASH_BYTES] = {0};
uint8_t prefix_len = 0;
if (!extractRecentRepeater(packet, prefix, prefix_len) || prefix_len == 0) {
return;
}
setRecentRepeater(prefix, prefix_len, packet->_snr);
}
public:
// Recent-repeater storage is supplied only by repeater firmware. Keeping it
// external makes this class layout identical in every translation unit;
// role-local feature macros must never change a C++ class definition.
SimpleMeshTables(RecentRepeaterInfo* recent_repeaters = NULL, int max_recent_repeaters = 0)
: _recent_repeaters(recent_repeaters),
_max_recent_repeaters(recent_repeaters != NULL && max_recent_repeaters > 0
? max_recent_repeaters : 0),
_recent_repeater_count(0) {
memset(_hashes, 0, sizeof(_hashes));
_next_idx = 0;
memset(_ack_hashes, 0, sizeof(_ack_hashes));
memset(_ack_valid, 0, sizeof(_ack_valid));
_next_ack_idx = 0;
_direct_dups = _flood_dups = 0;
if (_max_recent_repeaters > 0) {
memset(_recent_repeaters, 0, _max_recent_repeaters * sizeof(RecentRepeaterInfo));
}
}
#ifdef ESP32
void restoreFrom(File f) {
f.read(_hashes, sizeof(_hashes));
f.read((uint8_t *) &_next_idx, sizeof(_next_idx));
// ACKs are short-lived transport state and are intentionally not persisted.
memset(_ack_hashes, 0, sizeof(_ack_hashes));
memset(_ack_valid, 0, sizeof(_ack_valid));
_next_ack_idx = 0;
// Recent repeater entries are intentionally not restored across boots.
// This avoids struct-layout migration issues and keeps stale path quality
// stats from persisting indefinitely.
clearRecentRepeaters();
}
void saveTo(File f) {
f.write(_hashes, sizeof(_hashes));
f.write((const uint8_t *) &_next_idx, sizeof(_next_idx));
}
#endif
bool wasSeen(const mesh::Packet* packet) override {
if (isDedicatedAckPacket(packet)) {
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
if (hasSeenAckHash(hash)) {
if (packet->isRouteDirect()) {
_direct_dups++;
} else {
_flood_dups++;
}
return true;
}
return false;
}
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
if (hasSeenHash(hash)) {
if (packet->isRouteDirect()) {
_direct_dups++; // keep some stats
} else {
_flood_dups++;
}
return true;
}
return false;
}
void markSeen(const mesh::Packet* packet) override {
if (isDedicatedAckPacket(packet)) {
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
storeAckHash(hash);
return;
}
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
if (!hasSeenHash(hash)) {
storeHash(hash);
recordRecentRepeater(packet);
}
}
void markSent(const mesh::Packet* packet) override {
if (isDedicatedAckPacket(packet)) {
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
storeAckHash(hash);
return;
}
// Outbound packets must be marked as already-sent without teaching the recent-heard cache about ourselves.
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
if (!hasSeenHash(hash)) {
storeHash(hash);
}
}
void clear(const mesh::Packet* packet) override {
if (isDedicatedAckPacket(packet)) {
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
clearAckHash(hash);
return;
}
uint8_t hash[MAX_HASH_SIZE];
packet->calculatePacketHash(hash);
uint8_t* sp = _hashes;
for (int i = 0; i < MAX_PACKET_HASHES; i++, sp += MAX_HASH_SIZE) {
if (memcmp(hash, sp, MAX_HASH_SIZE) == 0) {
memset(sp, 0, MAX_HASH_SIZE);
break;
}
}
}
uint32_t getNumDirectDups() const { return _direct_dups; }
uint32_t getNumFloodDups() const { return _flood_dups; }
bool setRecentRepeater(const uint8_t* prefix, uint8_t prefix_len, int8_t snr_x4,
bool snr_locked = false, bool bypass_allow_filter = false) {
(void)snr_locked;
(void)bypass_allow_filter;
if (_max_recent_repeaters == 0) {
return false;
}
if (prefix == NULL || prefix_len == 0) {
return false;
}
if (prefix_len > MAX_ROUTE_HASH_BYTES) {
prefix_len = MAX_ROUTE_HASH_BYTES;
}
int oldest_idx = 0;
#if ARDUINO
const uint32_t now = millis();
uint32_t oldest_age = 0;
bool have_oldest = false;
#endif
// Occupied entries are kept packed at the front of the externally supplied
// array. Normal traffic therefore scans only learned repeaters, not the
// full 512/2048-slot capacity. Keep exact prefixes distinct so a 1-byte
// path prefix does not collapse 2/3-byte repeaters sharing its first byte.
for (int i = 0; i < _recent_repeater_count; i++) {
RecentRepeaterInfo& existing = _recent_repeaters[i];
if (existing.prefix_len != prefix_len || memcmp(existing.prefix, prefix, prefix_len) != 0) {
#if ARDUINO
uint32_t age = (uint32_t)(now - existing.last_heard_millis);
if (!have_oldest || age > oldest_age) {
oldest_age = age;
oldest_idx = i;
have_oldest = true;
}
#endif
continue;
}
existing.snr_x4 = weightedSnrX4RoundUp(existing.snr_x4, snr_x4);
#if ARDUINO
existing.last_heard_millis = now;
#else
existing.last_heard_millis = 0;
#endif
return true;
}
// Non-Arduino tests have no monotonic clock, so a full table retains the
// historical deterministic fallback of evicting slot zero.
int slot_idx;
if (_recent_repeater_count < _max_recent_repeaters) {
slot_idx = _recent_repeater_count++;
} else {
slot_idx = oldest_idx;
}
RecentRepeaterInfo& slot = _recent_repeaters[slot_idx];
memset(slot.prefix, 0, sizeof(slot.prefix));
memcpy(slot.prefix, prefix, prefix_len);
slot.prefix_len = prefix_len;
slot.snr_x4 = snr_x4;
#if ARDUINO
slot.last_heard_millis = now;
#else
slot.last_heard_millis = 0;
#endif
return true;
}
bool decrementRecentRepeaterSnrX4(const uint8_t* prefix, uint8_t prefix_len, uint8_t amount_x4 = 1) {
if (_max_recent_repeaters == 0) {
return false;
}
if (prefix == NULL || prefix_len == 0 || amount_x4 == 0) {
return false;
}
if (prefix_len > MAX_ROUTE_HASH_BYTES) {
prefix_len = MAX_ROUTE_HASH_BYTES;
}
RecentRepeaterInfo* existing = const_cast<RecentRepeaterInfo*>(findRecentRepeaterByHash(prefix, prefix_len));
if (existing != NULL) {
int16_t lowered = (int16_t)existing->snr_x4 - (int16_t)amount_x4;
if (lowered < -128) lowered = -128;
existing->snr_x4 = (int8_t)lowered;
return true;
}
return false;
}
int getRecentRepeaterCount() const {
return _recent_repeater_count;
}
const RecentRepeaterInfo* getRecentRepeaterBySortedIdx(int idx_wanted) const {
return getRecentRepeaterBySortedIdxFiltered(idx_wanted, NULL, 0);
}
int getRecentRepeaterMatchingCount(const uint8_t* search_prefix,
uint8_t search_prefix_len) const {
if (_max_recent_repeaters == 0 || search_prefix == NULL
|| search_prefix_len == 0
|| search_prefix_len > MAX_ROUTE_HASH_BYTES) {
return 0;
}
int count = 0;
for (int i = 0; i < _recent_repeater_count; i++) {
if (recentRepeaterMatchesFilter(_recent_repeaters[i], search_prefix,
search_prefix_len)) {
count++;
}
}
return count;
}
const RecentRepeaterInfo* getRecentRepeaterMatchingBySortedIdx(
const uint8_t* search_prefix, uint8_t search_prefix_len,
int idx_wanted) const {
if (search_prefix == NULL || search_prefix_len == 0) return NULL;
return getRecentRepeaterBySortedIdxFiltered(
idx_wanted, search_prefix, search_prefix_len);
}
const RecentRepeaterInfo* findRecentRepeaterByHash(const uint8_t* hash, uint8_t hash_len) const {
if (_max_recent_repeaters == 0) {
return NULL;
}
if (hash == NULL || hash_len == 0) {
return NULL;
}
// Prefer exact matches. If none exists, fall back to the longest overlapping
// prefix, using highest SNR to break ties.
const RecentRepeaterInfo* best = NULL;
for (int i = 0; i < _recent_repeater_count; i++) {
const RecentRepeaterInfo* info = &_recent_repeaters[i];
if (info->prefix_len == hash_len && memcmp(info->prefix, hash, hash_len) == 0) {
return info;
}
if (routeHashPrefixesOverlap(info->prefix, info->prefix_len, hash, hash_len)) {
if (best == NULL || info->prefix_len > best->prefix_len
|| (info->prefix_len == best->prefix_len && info->snr_x4 > best->snr_x4)) {
best = info;
}
}
}
return best;
}
void clearRecentRepeaters() {
if (_max_recent_repeaters > 0) {
memset(_recent_repeaters, 0, _max_recent_repeaters * sizeof(RecentRepeaterInfo));
}
_recent_repeater_count = 0;
}
int expireRecentRepeaters(uint32_t now_millis, uint32_t max_age_millis) {
if (_max_recent_repeaters == 0) {
return 0;
}
int expired = 0;
for (int i = 0; i < _recent_repeater_count; ) {
RecentRepeaterInfo& info = _recent_repeaters[i];
if ((uint32_t)(now_millis - info.last_heard_millis) > max_age_millis) {
_recent_repeater_count--;
if (i != _recent_repeater_count) {
info = _recent_repeaters[_recent_repeater_count];
}
memset(&_recent_repeaters[_recent_repeater_count], 0, sizeof(RecentRepeaterInfo));
expired++;
} else {
i++;
}
}
return expired;
}
void resetStats() { _direct_dups = _flood_dups = 0; }
};