initial bloom filter to optimize mesh

This commit is contained in:
medentem 2024-12-23 12:08:01 -06:00
parent ef81f1e704
commit 4718aa7904
9 changed files with 375 additions and 15 deletions

View File

@ -0,0 +1,95 @@
#include "CoverageFilter.h"
#include <functional> // std::hash
CoverageFilter::CoverageFilter()
{
bits_.fill(0);
}
void CoverageFilter::add(NodeNum item)
{
// For k=2, we just do two separate hash functions.
size_t idx1 = hash1(item);
size_t idx2 = hash2(item);
setBit(idx1);
setBit(idx2);
}
bool CoverageFilter::check(NodeNum item) const
{
// Check both hash positions. If either bit is 0, item is definitely not in.
size_t idx1 = hash1(item);
if (!testBit(idx1))
return false;
size_t idx2 = hash2(item);
if (!testBit(idx2))
return false;
// Otherwise, it might be in (false positive possible).
return true;
}
void CoverageFilter::merge(const CoverageFilter &other)
{
// Bitwise OR the bits.
for (size_t i = 0; i < BLOOM_FILTER_SIZE_BYTES; i++) {
bits_[i] |= other.bits_[i];
}
}
void CoverageFilter::clear()
{
bits_.fill(0);
}
// ------------------------
// Private / Helper Methods
// ------------------------
void CoverageFilter::setBit(size_t index)
{
if (index >= BLOOM_FILTER_SIZE_BITS)
return; // out-of-range check
size_t byteIndex = index / 8;
uint8_t bitMask = 1 << (index % 8);
bits_[byteIndex] |= bitMask;
}
bool CoverageFilter::testBit(size_t index) const
{
if (index >= BLOOM_FILTER_SIZE_BITS)
return false;
size_t byteIndex = index / 8;
uint8_t bitMask = 1 << (index % 8);
return (bits_[byteIndex] & bitMask) != 0;
}
/**
* Very simplistic hash: combine item with a seed and use std::hash
*/
size_t CoverageFilter::hash1(NodeNum value)
{
static const uint64_t seed1 = 0xDEADBEEF;
uint64_t combined = value ^ (seed1 + (value << 6) + (value >> 2));
// Use standard library hash on that combined value
std::hash<uint64_t> hasher;
uint64_t hashOut = hasher(combined);
// Map to [0..127]
return static_cast<size_t>(hashOut % BLOOM_FILTER_SIZE_BITS);
}
size_t CoverageFilter::hash2(NodeNum value)
{
static const uint64_t seed2 = 0xBADC0FFE;
uint64_t combined = value ^ (seed2 + (value << 5) + (value >> 3));
std::hash<uint64_t> hasher;
uint64_t hashOut = hasher(combined);
return static_cast<size_t>(hashOut % BLOOM_FILTER_SIZE_BITS);
}

71
src/mesh/CoverageFilter.h Normal file
View File

@ -0,0 +1,71 @@
#include "MeshTypes.h"
#include <array>
#include <cstddef>
#include <cstdint>
/**
* CoverageFilter:
* A simplified Bloom filter container designed to store coverage information,
* such as which node IDs are "probably covered" by a packet or route.
*
* Here is the worst case False Postiive Rate based on the constraints defined.
* False Positive Rate = (1-e^(-kn/m))^k
* False Positive Rate: k=2 (2 hash functions, 2 bits flipped), n=60 (20 nodes per hop), m=128 bits
* False Positive Rate = 37%
*/
class CoverageFilter
{
public:
/**
* Default constructor: Initialize the bit array to all zeros.
*/
CoverageFilter();
/**
* Insert an item (e.g., nodeID) into the bloom filter.
* This sets multiple bits (in this example, 2).
* @param item: A node identifier to add to the filter.
*/
void add(NodeNum item);
/**
* Check if the item might be in the bloom filter.
* Returns true if likely present; false if definitely not present.
* (False positives possible, false negatives are not.)
*/
bool check(NodeNum item) const;
/**
* Merge (bitwise OR) another CoverageFilter into this one.
* i.e., this->bits = this->bits OR other.bits
*/
void merge(const CoverageFilter &other);
/**
* Clear all bits (optional utility).
* This makes the filter empty again (no items).
*/
void clear();
/**
* Access the underlying bits array for reading/writing,
* e.g., if you want to store it in a packet header or Protobuf.
*/
const std::array<uint8_t, BLOOM_FILTER_SIZE_BYTES> &getBits() const { return bits_; }
void setBits(const std::array<uint8_t, BLOOM_FILTER_SIZE_BYTES> &newBits) { bits_ = newBits; }
private:
// The underlying bit array: 128 bits => 16 bytes
std::array<uint8_t, BLOOM_FILTER_SIZE_BYTES> bits_;
// Helper to set a bit at a given index [0..127].
void setBit(size_t index);
// Helper to check if a bit is set.
bool testBit(size_t index) const;
// Two example hash functions for demonstration.
// TODO: consider MurmurHash, xxHash, etc.
static size_t hash1(NodeNum value);
static size_t hash2(NodeNum value);
};

View File

@ -58,23 +58,39 @@ bool FloodingRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
if (!isToUs(p) && (p->hop_limit > 0) && !isFromUs(p)) {
if (p->id != 0) {
if (isRebroadcaster()) {
meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it
CoverageFilter incomingCoverage;
loadCoverageFilterFromPacket(p, incomingCoverage);
tosend->hop_limit--; // bump down the hop count
float forwardProb = calculateForwardProbability(incomingCoverage);
float rnd = static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
if (rnd <= forwardProb) {
meshtastic_MeshPacket *tosend = packetPool.allocCopy(*p); // keep a copy because we will be sending it
tosend->hop_limit--; // bump down the hop count
#if USERPREFS_EVENT_MODE
if (tosend->hop_limit > 2) {
// if we are "correcting" the hop_limit, "correct" the hop_start by the same amount to preserve hops away.
tosend->hop_start -= (tosend->hop_limit - 2);
tosend->hop_limit = 2;
}
if (tosend->hop_limit > 2) {
// if we are "correcting" the hop_limit, "correct" the hop_start by the same amount to preserve hops away.
tosend->hop_start -= (tosend->hop_limit - 2);
tosend->hop_limit = 2;
}
#endif
LOG_INFO("Rebroadcast received floodmsg");
// Note: we are careful to resend using the original senders node id
// We are careful not to call our hooked version of send() - because we don't want to check this again
Router::send(tosend);
LOG_INFO("Rebroadcasting packet ID=0x%x with ForwardProb=%.2f", p->id, forwardProb);
return true;
CoverageFilter updatedCoverage = incomingCoverage;
mergeMyCoverage(updatedCoverage);
storeCoverageFilterInPacket(updatedCoverage, tosend);
// Note: we are careful to resend using the original senders node id
// We are careful not to call our hooked version of send() - because we don't want to check this again
Router::send(tosend);
return true;
} else {
LOG_DEBUG("No rebroadcast: Random number %f > Forward Probability %f", rnd, forwardProb);
}
} else {
LOG_DEBUG("No rebroadcast: Role = CLIENT_MUTE or Rebroadcast Mode = NONE");
}
@ -99,4 +115,62 @@ void FloodingRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtas
// handle the packet as normal
Router::sniffReceived(p, c);
}
void FloodingRouter::loadCoverageFilterFromPacket(const meshtastic_MeshPacket *p, CoverageFilter &filter)
{
// If packet has coverage bytes (16 bytes), copy them into filter
// e.g. p->coverage_filter is a 16-byte array in your packet struct
std::array<uint8_t, BLOOM_FILTER_SIZE_BYTES> bits;
memcpy(bits.data(), p->coverage_filter.bytes, BLOOM_FILTER_SIZE_BYTES);
filter.setBits(bits);
}
void FloodingRouter::storeCoverageFilterInPacket(const CoverageFilter &filter, meshtastic_MeshPacket *p)
{
auto bits = filter.getBits();
memcpy(p->coverage_filter.bytes, bits.data(), BLOOM_FILTER_SIZE_BYTES);
}
void FloodingRouter::mergeMyCoverage(CoverageFilter &coverage)
{
// Retrieve recent direct neighbors within the time window
std::vector<NodeNum> recentNeighbors = nodeDB->getDistinctRecentDirectNeighborIds(RECENCY_THRESHOLD_MINUTES * 60);
for (auto &nodeId : recentNeighbors) {
coverage.add(nodeId);
}
}
float FloodingRouter::calculateForwardProbability(const CoverageFilter &incoming)
{
// Retrieve recent direct neighbors within the time window
std::vector<NodeNum> recentNeighbors = nodeDB->getDistinctRecentDirectNeighborIds(RECENCY_THRESHOLD_MINUTES * 60);
if (recentNeighbors.empty()) {
// No neighbors to add coverage for
LOG_DEBUG("No recent direct neighbors to add coverage for.");
return 0.0f;
}
// Count how many neighbors are NOT yet in the coverage
int uncovered = 0;
for (auto nodeId : recentNeighbors) {
if (!incoming.check(nodeId)) {
uncovered++;
}
}
// Calculate coverage ratio
float coverageRatio = static_cast<float>(uncovered) / static_cast<float>(recentNeighbors.size());
// Calculate forwarding probability
float forwardProb = BASE_FORWARD_PROB + (coverageRatio * COVERAGE_SCALE_FACTOR);
// Clamp probability between 0 and 1
forwardProb = std::min(std::max(forwardProb, 0.0f), 1.0f);
LOG_DEBUG("CoverageRatio=%.2f, ForwardProb=%.2f (Uncovered=%d, Total=%zu)", coverageRatio, forwardProb, uncovered,
recentNeighbors.size());
return forwardProb;
}

View File

@ -1,5 +1,6 @@
#pragma once
#include "CoverageFilter.h"
#include "PacketHistory.h"
#include "Router.h"
@ -35,6 +36,14 @@ class FloodingRouter : public Router, protected PacketHistory
* @return true if rebroadcasted */
bool perhapsRebroadcast(const meshtastic_MeshPacket *p);
void loadCoverageFilterFromPacket(const meshtastic_MeshPacket *p, CoverageFilter &filter);
void storeCoverageFilterInPacket(const CoverageFilter &filter, meshtastic_MeshPacket *p);
void mergeMyCoverage(CoverageFilter &coverage);
float calculateForwardProbability(const CoverageFilter &incoming);
public:
/**
* Constructor

View File

@ -40,6 +40,27 @@ enum RxSource {
/// We normally just use max 3 hops for sending reliable messages
#define HOP_RELIABLE 3
// Maximum number of neighbors a node adds to the Bloom filter per hop
#define MAX_NEIGHBORS_PER_HOP 20
// Size of the Bloom filter in bytes (128 bits)
#define BLOOM_FILTER_SIZE_BYTES 16
// Size of the Bloom filter in bits (128 bits)
#define BLOOM_FILTER_SIZE_BITS (BLOOM_FILTER_SIZE_BYTES * 8)
// Number of hash functions to use in the Bloom filter
#define NUM_HASH_FUNCTIONS 2
// Base forwarding probability - never drop below this value
#define BASE_FORWARD_PROB 0.1f
// Coverage scaling factor
#define COVERAGE_SCALE_FACTOR 0.9f
// Recency threshold in minutes
#define RECENCY_THRESHOLD_MINUTES 5
typedef int ErrorCode;
/// Alloc and free packets to our global, ISR safe pool

View File

@ -807,6 +807,82 @@ void NodeDB::clearLocalPosition()
setLocalPosition(meshtastic_Position_init_default);
}
/**
* @brief Retrieves a list of distinct recent direct neighbor NodeNums.
*
* Filters out:
* - The local node itself.
* - Ignored nodes.
* - Nodes not within the direct neighbor (hops_away == 0).
* - Nodes heard via MQTT.
* - Nodes not heard within the specified time window.
*
* @param timeWindowSecs The time window in seconds to consider a node as "recently heard."
* @return std::vector<NodeNum> A vector containing the NodeNums of recent direct neighbors.
*/
std::vector<NodeNum> NodeDB::getDistinctRecentDirectNeighborIds(uint32_t timeWindowSecs)
{
uint32_t now = getTime();
NodeNum localNode = getNodeNum();
// Temporary vector to hold neighbors with their SNR for sorting
std::vector<std::pair<NodeNum, float>> neighborsWithSnr;
neighborsWithSnr.reserve(MAX_NEIGHBORS_PER_HOP); // Reserve space to avoid multiple reallocations
for (size_t i = 0; i < numMeshNodes; ++i) {
const meshtastic_NodeInfoLite &node = meshNodes->at(i);
// Skip our own node entry
if (node.num == localNode) {
continue;
}
// Skip ignored nodes
if (node.is_ignored) {
continue;
}
// Check if this node is a direct neighbor (hops_away == 0)
if (!node.has_hops_away || node.hops_away != 0) {
continue;
}
// Skip nodes heard via MQTT
if (node.via_mqtt) {
continue;
}
// Check if the node was heard recently within the time window
if (node.last_heard > 0 && (now - node.last_heard <= timeWindowSecs)) {
neighborsWithSnr.emplace_back(node.num, node.snr);
}
}
LOG_DEBUG("Found %zu candidates before limiting.", neighborsWithSnr.size());
// If the number of candidates exceeds MAX_NEIGHBORS_PER_HOP, select the top N based on SNR
if (neighborsWithSnr.size() > MAX_NEIGHBORS_PER_HOP) {
// Use nth_element to partially sort the vector, bringing the top N SNRs to the front
std::nth_element(neighborsWithSnr.begin(), neighborsWithSnr.begin() + MAX_NEIGHBORS_PER_HOP, neighborsWithSnr.end(),
[](const std::pair<NodeNum, float> &a, const std::pair<NodeNum, float> &b) {
return a.second > b.second; // Sort in descending order of SNR
});
// Resize to keep only the top N neighbors
neighborsWithSnr.resize(MAX_NEIGHBORS_PER_HOP);
}
// Extract NodeNums from the sorted and limited list
std::vector<NodeNum> recentNeighbors;
recentNeighbors.reserve(neighborsWithSnr.size());
for (const auto &pair : neighborsWithSnr) {
recentNeighbors.push_back(pair.first);
}
LOG_DEBUG("Returning %zu recent direct neighbors within %u seconds.", recentNeighbors.size(), timeWindowSecs);
return recentNeighbors;
}
void NodeDB::cleanupMeshDB()
{
int newPos = 0, removed = 0;
@ -1500,4 +1576,4 @@ void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, co
LOG_ERROR("A critical failure occurred, portduino is exiting");
exit(2);
#endif
}
}

View File

@ -167,6 +167,14 @@ class NodeDB
bool hasValidPosition(const meshtastic_NodeInfoLite *n);
/**
* @brief Retrieves a list of distinct recent direct neighbor NodeNums.
*
* @param timeWindowSecs The time window in seconds to consider a node as "recently heard."
* @return std::vector<NodeNum> A vector containing the NodeNums of recent direct neighbors.
*/
std::vector<NodeNum> getDistinctRecentDirectNeighborIds(uint32_t timeWindowSecs);
private:
uint32_t lastNodeDbSave = 0; // when we last saved our db to flash
/// Find a node in our DB, create an empty NodeInfoLite if missing

View File

@ -10,7 +10,7 @@
#define MAX_TX_QUEUE 16 // max number of packets which can be waiting for transmission
#define MAX_LORA_PAYLOAD_LEN 255 // max length of 255 per Semtech's datasheets on SX12xx
#define MESHTASTIC_HEADER_LENGTH 16
#define MESHTASTIC_HEADER_LENGTH 32
#define MESHTASTIC_PKC_OVERHEAD 12
#define PACKET_FLAGS_HOP_LIMIT_MASK 0x07
@ -43,6 +43,9 @@ typedef struct {
// ***For future use*** Last byte of the NodeNum of the node that will relay/relayed this packet
uint8_t relay_node;
// A 16-byte Bloom filter that tracks coverage of the current node.
std::array<uint8_t, BLOOM_FILTER_SIZE_BYTES> coverage_filter;
} PacketHeader;
/**

View File

@ -690,6 +690,7 @@ typedef struct _meshtastic_MqttClientProxyMessage {
typedef PB_BYTES_ARRAY_T(256) meshtastic_MeshPacket_encrypted_t;
typedef PB_BYTES_ARRAY_T(32) meshtastic_MeshPacket_public_key_t;
typedef PB_BYTES_ARRAY_T(16) meshtastic_MeshPacket_coverage_filter_t;
/* A packet envelope sent/received over the mesh
only payload_variant is sent in the payload portion of the LORA packet.
The other fields are either not sent at all, or sent in the special 16 byte LORA header. */
@ -770,6 +771,8 @@ typedef struct _meshtastic_MeshPacket {
/* Last byte of the node number of the node that will relay/relayed this packet.
Set by the firmware internally, clients are not supposed to set this. */
uint8_t relay_node;
meshtastic_MeshPacket_coverage_filter_t coverage_filter;
} meshtastic_MeshPacket;
/* The bluetooth to device link:
@ -1766,4 +1769,4 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
} /* extern "C" */
#endif
#endif
#endif