mirror of https://github.com/meshcore-dev/MeshCore
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
121 lines
4.7 KiB
121 lines
4.7 KiB
#pragma once
|
|
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
|
|
// Hardware-independent store-and-forward cache of recent public-channel (GRP_TXT)
|
|
// broadcasts. A repeater already relays every channel packet that passes through
|
|
// it; this keeps a bounded copy so a companion that was out of range can pull back
|
|
// what it missed.
|
|
//
|
|
// The repeater holds no channel key, so packets are stored - and later replayed -
|
|
// still encrypted; the requesting client decrypts them with its own channel key.
|
|
// A cached entry's raw[0] is the GRP_TXT channel-hash prefix.
|
|
//
|
|
// Kept free of Arduino / radio dependencies so it can be unit-tested natively
|
|
// (see test/test_channel_history).
|
|
//
|
|
// NUM_ENTRIES : ring-buffer depth (oldest entry evicted when full)
|
|
// MAX_RAW : largest packet payload cached; must be small enough that one entry
|
|
// fits inside a single response packet (see serve()).
|
|
template <int NUM_ENTRIES, int MAX_RAW>
|
|
class ChannelHistory {
|
|
public:
|
|
// per-served-record header: recv_ts(4) + raw_len(1)
|
|
static const int REC_HEADER = 5;
|
|
|
|
ChannelHistory() { clear(); }
|
|
|
|
void clear() {
|
|
memset(_entries, 0, sizeof(_entries));
|
|
_next_idx = 0;
|
|
}
|
|
|
|
// Cache one public-channel payload (caller has already checked it is GRP_TXT).
|
|
// Returns true if newly stored, false if skipped: empty, too large to ever replay
|
|
// in one response, or a duplicate re-flood already held.
|
|
//
|
|
// Dedup is by content: a GRP_TXT payload embeds the sender's encrypted timestamp,
|
|
// so identical bytes mean the very same packet; distinct messages never collide.
|
|
bool capture(const uint8_t* payload, uint16_t payload_len, uint32_t now_ts) {
|
|
if (payload_len < 1 || payload_len > MAX_RAW) return false;
|
|
|
|
for (int k = 0; k < NUM_ENTRIES; k++) {
|
|
const Entry& e = _entries[k];
|
|
if (e.recv_timestamp != 0 && e.raw_len == payload_len
|
|
&& memcmp(e.raw, payload, payload_len) == 0) {
|
|
return false; // already cached
|
|
}
|
|
}
|
|
|
|
Entry& slot = _entries[_next_idx];
|
|
slot.recv_timestamp = now_ts;
|
|
slot.raw_len = (uint8_t)payload_len;
|
|
memcpy(slot.raw, payload, payload_len);
|
|
_next_idx = (_next_idx + 1) % NUM_ENTRIES;
|
|
return true;
|
|
}
|
|
|
|
// Pack matching cached packets into out[], oldest -> newest, for entries strictly
|
|
// newer than since_ts, up to max_count (0 == no explicit cap), stopping early if
|
|
// the next record would not fit in out_cap. Each record is:
|
|
// [recv_ts(4)][raw_len(1)][raw ...]
|
|
// The client pages by re-requesting with since_ts = the newest recv_ts it received.
|
|
// Writes the number of records emitted to out_count; returns bytes written.
|
|
int serve(uint8_t want_hash, uint32_t since_ts, uint8_t max_count,
|
|
uint8_t* out, int out_cap, uint8_t& out_count) const {
|
|
if (max_count == 0) max_count = 255;
|
|
uint8_t count = 0;
|
|
int ofs = 0;
|
|
// _next_idx points at the oldest slot once the ring has wrapped, so walking
|
|
// forward from it yields oldest -> newest; empty slots (recv_timestamp==0) skip.
|
|
for (int k = 0; k < NUM_ENTRIES && count < max_count; k++) {
|
|
const Entry& e = _entries[(_next_idx + k) % NUM_ENTRIES];
|
|
if (e.recv_timestamp == 0 || e.recv_timestamp <= since_ts) continue;
|
|
if (want_hash != 0xFF && e.raw[0] != want_hash) continue; // channel filter
|
|
if (ofs + REC_HEADER + e.raw_len > out_cap) break; // full; client pages for more
|
|
memcpy(&out[ofs], &e.recv_timestamp, 4); ofs += 4;
|
|
out[ofs++] = e.raw_len;
|
|
memcpy(&out[ofs], e.raw, e.raw_len); ofs += e.raw_len;
|
|
count++;
|
|
}
|
|
out_count = count;
|
|
return ofs;
|
|
}
|
|
|
|
// Number of non-empty entries currently held (for diagnostics/debug dumps).
|
|
int countStored() const {
|
|
int n = 0;
|
|
for (int k = 0; k < NUM_ENTRIES; k++) {
|
|
if (_entries[k].recv_timestamp != 0) n++;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
// Read the i-th non-empty entry oldest -> newest (for diagnostics). Returns false
|
|
// past the end. channel_hash / raw_len / recv_ts are optional out-params.
|
|
bool entryAt(int i, uint8_t* channel_hash, uint8_t* raw_len, uint32_t* recv_ts) const {
|
|
int seen = 0;
|
|
for (int k = 0; k < NUM_ENTRIES; k++) {
|
|
const Entry& e = _entries[(_next_idx + k) % NUM_ENTRIES];
|
|
if (e.recv_timestamp == 0) continue;
|
|
if (seen == i) {
|
|
if (channel_hash) *channel_hash = e.raw[0];
|
|
if (raw_len) *raw_len = e.raw_len;
|
|
if (recv_ts) *recv_ts = e.recv_timestamp;
|
|
return true;
|
|
}
|
|
seen++;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private:
|
|
struct Entry {
|
|
uint32_t recv_timestamp; // 0 == empty slot
|
|
uint8_t raw_len;
|
|
uint8_t raw[MAX_RAW]; // GRP_TXT payload, verbatim (still encrypted); raw[0] = channel hash
|
|
};
|
|
Entry _entries[NUM_ENTRIES];
|
|
uint8_t _next_idx;
|
|
};
|
|
|