From 4718aa79048272a5c7c6054dd68d7292acfe1bd6 Mon Sep 17 00:00:00 2001 From: medentem Date: Mon, 23 Dec 2024 12:08:01 -0600 Subject: [PATCH] initial bloom filter to optimize mesh --- src/mesh/CoverageFilter.cpp | 95 ++++++++++++++++++++++++ src/mesh/CoverageFilter.h | 71 ++++++++++++++++++ src/mesh/FloodingRouter.cpp | 98 ++++++++++++++++++++++--- src/mesh/FloodingRouter.h | 9 +++ src/mesh/MeshTypes.h | 21 ++++++ src/mesh/NodeDB.cpp | 78 +++++++++++++++++++- src/mesh/NodeDB.h | 8 ++ src/mesh/RadioInterface.h | 5 +- src/mesh/generated/meshtastic/mesh.pb.h | 5 +- 9 files changed, 375 insertions(+), 15 deletions(-) create mode 100644 src/mesh/CoverageFilter.cpp create mode 100644 src/mesh/CoverageFilter.h diff --git a/src/mesh/CoverageFilter.cpp b/src/mesh/CoverageFilter.cpp new file mode 100644 index 000000000..7e6ae62fd --- /dev/null +++ b/src/mesh/CoverageFilter.cpp @@ -0,0 +1,95 @@ +#include "CoverageFilter.h" + +#include // 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 hasher; + uint64_t hashOut = hasher(combined); + + // Map to [0..127] + return static_cast(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 hasher; + uint64_t hashOut = hasher(combined); + + return static_cast(hashOut % BLOOM_FILTER_SIZE_BITS); +} \ No newline at end of file diff --git a/src/mesh/CoverageFilter.h b/src/mesh/CoverageFilter.h new file mode 100644 index 000000000..f116de762 --- /dev/null +++ b/src/mesh/CoverageFilter.h @@ -0,0 +1,71 @@ +#include "MeshTypes.h" +#include +#include +#include + +/** + * 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 &getBits() const { return bits_; } + void setBits(const std::array &newBits) { bits_ = newBits; } + + private: + // The underlying bit array: 128 bits => 16 bytes + std::array 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); +}; \ No newline at end of file diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp index e29c596df..a49b55ccc 100644 --- a/src/mesh/FloodingRouter.cpp +++ b/src/mesh/FloodingRouter.cpp @@ -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(rand()) / static_cast(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 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 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 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(uncovered) / static_cast(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; } \ No newline at end of file diff --git a/src/mesh/FloodingRouter.h b/src/mesh/FloodingRouter.h index 52614f391..cf412239d 100644 --- a/src/mesh/FloodingRouter.h +++ b/src/mesh/FloodingRouter.h @@ -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 diff --git a/src/mesh/MeshTypes.h b/src/mesh/MeshTypes.h index 1d6bd342d..2cd669356 100644 --- a/src/mesh/MeshTypes.h +++ b/src/mesh/MeshTypes.h @@ -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 diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 2af85e4f5..e6704a903 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -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 A vector containing the NodeNums of recent direct neighbors. + */ +std::vector NodeDB::getDistinctRecentDirectNeighborIds(uint32_t timeWindowSecs) +{ + uint32_t now = getTime(); + NodeNum localNode = getNodeNum(); + + // Temporary vector to hold neighbors with their SNR for sorting + std::vector> 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 &a, const std::pair &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 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 -} +} \ No newline at end of file diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index 7e51a1240..b2b2185a0 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -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 A vector containing the NodeNums of recent direct neighbors. + */ + std::vector 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 diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index 89a4c7087..48fa50a21 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -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 coverage_filter; } PacketHeader; /** diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 14ed76f70..faf0da5d7 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -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 \ No newline at end of file