mirror of
https://github.com/meshtastic/firmware.git
synced 2025-09-06 11:39:32 +00:00
initial bloom filter to optimize mesh
This commit is contained in:
parent
ef81f1e704
commit
4718aa7904
95
src/mesh/CoverageFilter.cpp
Normal file
95
src/mesh/CoverageFilter.cpp
Normal 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
71
src/mesh/CoverageFilter.h
Normal 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);
|
||||
};
|
@ -58,6 +58,14 @@ bool FloodingRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
|
||||
if (!isToUs(p) && (p->hop_limit > 0) && !isFromUs(p)) {
|
||||
if (p->id != 0) {
|
||||
if (isRebroadcaster()) {
|
||||
CoverageFilter incomingCoverage;
|
||||
loadCoverageFilterFromPacket(p, incomingCoverage);
|
||||
|
||||
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
|
||||
@ -69,12 +77,20 @@ bool FloodingRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_INFO("Rebroadcast received floodmsg");
|
||||
LOG_INFO("Rebroadcasting packet ID=0x%x with ForwardProb=%.2f", p->id, forwardProb);
|
||||
|
||||
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");
|
||||
}
|
||||
@ -100,3 +116,61 @@ 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;
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
/**
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user