* asdf * Implement SphereOfInfluenceModule for traffic management and eviction tracking * Implement Sphere of Influence module for dynamic hop limit adjustment and role-based floor * Update SAMPLING_DENOMINATOR to improve mesh size estimation accuracy * Add debug logging for scale factor estimation and per-hop node counts in SphereOfInfluenceModule * Enable variable hop limits and role-based hop floors in Sphere of Influence module * Respond to copilot review * Disable variable hop limits and role-based hop floors in Sphere of Influence module * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Implement adaptive sampling for unique node ID tracking in Sphere of Influence module * Add state persistence for Sphere of Influence module * Enhance Sphere of Influence module with state management and adaptive sampling adjustments * Refactor hop scaling functionality: remove SphereOfInfluenceModule and introduce HopScalingModule - Deleted SphereOfInfluenceModule.h, consolidating its responsibilities into the new HopScalingModule. - Added HopScalingModule.h and HopScalingModule.cpp to manage hop scaling logic, including eviction tracking and sampling-based mesh size estimation. - Implemented methods for recording evictions and packet senders, estimating scale factors, and computing required hops based on node activity. - Introduced state persistence for hop scaling parameters to maintain continuity across reboots. - Enhanced thread safety and modularity by utilizing concurrency features. * Guard out STM32. Sowwy. * Refactor HopScalingModule: enhance sampling logic and improve state management * Add unit tests for HopScalingModule: implement mock database and various test scenarios * Refactor test output in HopScalingModule tests: replace printf with TEST_MESSAGE for better integration with Unity * Refactor HopScalingModule logging: replace lastStatusMode with descriptive mode names for improved readability * Refactor test_main.cpp: change NodeNum variable to static and improve comments for clarity * Remove unnecessary delay in setup function for improved test performance * Add missing include for MeshTypes in test_main.cpp * Refactor HopScalingModule tests: enhance mesh topology scenarios and improve test clarity * Update HopScalingModule tests: flesh out node scenarios and improve clarity for dense and sparse mesh cases * Fix politeness factor calculations in HopScalingModule and update related test scenarios for clarity. Remove outdated design doc. * Enhance HopScalingModule: add sampled estimate for scaling decisions and refactor initial run state management * Add sample traffic injection for HopScaling tests to enhance sampledEst visibility * Enhance HopScalingModule: adjust windowFraction calculation for early triggers and improve test output formatting * Enhance HopScalingModule: add jitter functionality to sampling denominator and update tests for consistent behavior * Enhance HopScalingModule: implement adaptive sampling denominator adjustment and add reset functionality for tests * Enhance HopScalingTestShim: add test-only clock and window helpers, update injectSampleTraffic for adaptive sampling, and improve scenario summary output * Enhance HopScalingModule: add detailed documentation for functions, improve clarity of jitter and sampling logic, and reset functionality in tests * Enhance HopScalingModule: add evictionEstimate parameter to estimateScaleFactor and update related logging for improved mesh size estimation * Enhance HopScalingModule: adjust effective rolls calculation for improved accuracy, add eviction estimate logic, responding to all copilot review points * Implement CompactHistogram for parallel hop scaling sampling - Added CompactHistogram class to track node hop distances with bitwise sampling. - Integrated CompactHistogram into HopScalingModule for independent packet sampling. - Updated NodeDB to feed both the hop scaling module and the new histogram sampler. - Enhanced HopScalingModule with methods to sample packets for the histogram and retrieve hop distribution statistics. - Implemented tests for CompactHistogram functionality, including sampling, window rolling, and adaptive denominator scaling. - Updated existing tests to validate the integration of the new histogram sampling mechanism. * Enhance CompactHistogram and HopScalingModule: add per-hop distribution functionality, improve time handling for unit tests, and refine test setup for deterministic behavior * CompactHistogram: add mesh size estimation, improve entry replacement logic, and update logging for per-hop distribution * Refactor HopScalingModule and CompactHistogram integration - Removed the suggestedHopFromCompactHistogram function to streamline hop suggestion logic. - Updated HopScalingModule to directly utilize CompactHistogram's internal methods for hop suggestions and sampling. - Enhanced logging in HopScalingModule to provide detailed histogram statistics. - Modified test cases to ensure comprehensive coverage of new histogram behaviors and sampling logic. - Improved node ID distribution in tests to better exercise sampling mechanisms. - Ensured that filtering denominators are held for 12 hours before dropping, enhancing stability in sampling. * Refactor CompactHistogram to support 13-hour activity tracking and introduce politeness regimes - Updated the bitfield structure to accommodate 13-hour seen tracking. - Changed the logic in rollHour() to analyze activity over the last 0-2 hours vs. 1-3 hours for politeness factor calculation. - Introduced three politeness levels: GENEROUS, DEFAULT, and STRICT based on recent activity ratios. - Adjusted filtering and sampling logic to reflect the new 13-hour tracking period. - Updated unit tests to validate new behavior and ensure proper functionality of politeness regimes. * Enhance CompactHistogram and HopScalingModule for improved sampling and decision-making - Introduced a session-specific hash seed in CompactHistogram to reduce bias in node ID sampling. - Updated sampling logic to use hashed node IDs instead of raw IDs for filtering and entry management. - Added histogram rollover tracking in HopScalingModule to ensure proper decision-making after initial data collection. - Adjusted logging to reflect the active state of the histogram and its comparison with NodeDB advisory hops. - Enhanced unit tests to validate new sampling logic and memory layout changes. * Expose hashNodeId for testing in CompactHistogram * Add mesh trend statistics to CompactHistogram for enhanced activity tracking * Implement histogram state persistence in CompactHistogram with save and load functions * Refactor CompactHistogram to improve entry management and enhance rollHour logging * feat: add HopScalingModule for adaptive hop limit recommendations Introduces HopScalingModule, a sampled hop-distance histogram that recommends the minimum hop limit needed to reach ~40 nodes, and automatically reducing the hops as the mesh grows. Key design: - 512-byte packed histogram (128 × 4-byte Record entries) embedded in a new HopScalingModule. - Each Record: 16-bit node hash, 3-bit hop distance, 13-bit seen bitmap - Sampling filter: only nodes where (hash & (denom-1)) == 0 are kept; denominator doubles on overflow and halves when utilisation is low - Hourly rollHour(): tallies per-hop counts, walks scaled buckets to find the minimum hop satisfying TARGET_AFFECTED_NODES (40), applies a politeness extension based on recent/older activity ratio, shifts all seen bitmaps, and persists state to /prefs/hopScalingState.bin - Hop recommendation gated by bootstrap (requires >=1 rollHour before overriding HOP_MAX) - NodeDB calls samplePacketForHistogram() on every non-MQTT rx packet - Module also estimates total mesh size and logs useful information about mesh characteristics. Changes: - src/modules/HopScalingModule.h/.cpp: new module - src/mesh/NodeDB.cpp: wire up samplePacketForHistogram - src/mesh/Router.cpp: consume getLastRequiredHop() - test/test_hop_scaling/: 12-test suite covering all mesh topologies and anticipated operational requirements * test: increase run iterations in sparse to dense transition test * feat: refactor HopScalingModule to use RUNS_PER_HOUR constant and improve logging * feat: enhance HopScalingModule with filtering denominator management and add tests for state transitions * refactor: remove CompactHistogram module and related files * address copilot review comments * Tweak: packet sampling only lora * ove role-based hop floor logic and related definitions into the module - keep it in one place. * Refactor MockNodeDB to use nodeInfoLiteSetBit for MQTT flag setting * Refactor hop scaling parameters and logic to integer maths and put default values in defaults.h. Small flash size reduction, no functional impact. * Update unit test preprocessor directives to PIO_UNIT_TESTING for consistency * refactor: improve test output organization and clarity in hop scaling tests --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
352 lines
18 KiB
C++
352 lines
18 KiB
C++
#pragma once
|
||
|
||
#include "MeshTypes.h"
|
||
#include "concurrency/OSThread.h"
|
||
#include "configuration.h"
|
||
#include "mesh/Default.h"
|
||
#include "mesh/mesh-pb-constants.h"
|
||
#include <algorithm>
|
||
#include <cstdint>
|
||
#include <cstring>
|
||
|
||
#if HAS_VARIABLE_HOPS
|
||
|
||
/**
|
||
* HopScalingModule: Sampled hop-distance histogram for mesh-aware hop limit recommendations.
|
||
*
|
||
* Memory layout: 512 bytes total (128 entries × 4 bytes/entry, no padding)
|
||
* - 16-bit XOR-fold hash of node ID
|
||
* - 3-bit hops away (0–7)
|
||
* - 13-bit hourly seen bitmap
|
||
* All three fields are packed into a single 32-bit Record; sizeof(Record) == 4.
|
||
*
|
||
* Sampling:
|
||
* - A node is added only when passesFilter(hashNodeId(nodeId), samplingDenominator),
|
||
* i.e. (hash16(nodeId) & (samplingDenominator – 1)) == 0 (hash-space subsample, not raw ID)
|
||
* - samplingDenominator starts at 1 (sample all), doubles when the list exceeds FILL_HIGH_PCT
|
||
* - filteringDenominator tracks samplingDenominator upward immediately but does not drop back
|
||
* down until FILTER_DENOM_HOLD_MS (13 h) have elapsed since the last scale-up
|
||
*
|
||
* Hourly rollover (rollHour()):
|
||
* - Summarises per-hop node counts for entries matching filteringDenominator and seen in the
|
||
* last 13 hours
|
||
* - Scales each hop bucket by filteringDenominator and walks the buckets to recommend a hop
|
||
* limit, matching the same algorithm used in HopScalingModule
|
||
* - Shifts the 13-bit seen bitmap left by one slot to open a fresh slot for the new hour;
|
||
* nodes not seen in 13 consecutive hours have all seen bits cleared (stale)
|
||
* - Checks for scale-down: if fewer than FILL_LOW_PCT of capacity pass filteringDenominator,
|
||
* samplingDenominator is halved (filteringDenominator is held until the 13-h lock expires)
|
||
*
|
||
* Thread-safety: all access is single-threaded via the main loop cooperative scheduler.
|
||
*/
|
||
|
||
struct Record {
|
||
uint32_t nodeHash : 16;
|
||
uint32_t hops_away : 3;
|
||
uint32_t seenHoursAgo : 13;
|
||
};
|
||
static_assert(sizeof(Record) == 4);
|
||
|
||
class HopScalingModule : private concurrency::OSThread
|
||
{
|
||
public:
|
||
// -----------------------------------------------------------------------
|
||
// Capacity and memory layout
|
||
// -----------------------------------------------------------------------
|
||
static constexpr size_t CAPACITY = 128;
|
||
static constexpr size_t ENTRY_BYTES = sizeof(Record);
|
||
static constexpr size_t TOTAL_BYTES = CAPACITY * ENTRY_BYTES;
|
||
|
||
// Denominator limits (must be powers of 2)
|
||
static constexpr uint8_t DENOM_MIN = 1;
|
||
static constexpr uint8_t DENOM_MAX = 128;
|
||
|
||
static constexpr uint8_t MAX_HOP = 7;
|
||
|
||
// Fill-level thresholds (percent of CAPACITY)
|
||
static constexpr uint8_t FILL_HIGH_PCT = 80;
|
||
static constexpr uint8_t FILL_LOW_PCT = 20;
|
||
|
||
// How long filteringDenominator is held at an elevated level before it may drop.
|
||
//
|
||
// This value is deliberately equal to the seenHoursAgo window (13 hours / 13 bits).
|
||
// Invariant: every entry that existed when a scale-up fired had seenHoursAgo != 0 at
|
||
// that moment (trimIfNeeded() evicts stale entries before doubling the denominator),
|
||
// so it remains seenInLast13h for at most 13 more rollHour() calls — exactly the
|
||
// hold duration. That means entries from the scale-up event keep counts.total above
|
||
// the scale-down threshold for the entire hold period under normal (active) mesh
|
||
// conditions. On a genuinely quieting mesh the scale-down CAN fire before the hold
|
||
// expires — each firing halves samplingDenominator but filteringDenominator stays
|
||
// elevated, so the hop recommendation correctly stays conservative (MAX_HOP) while
|
||
// the cascade runs. The cascade is bounded at DENOM_MIN (7 halvings from DENOM_MAX);
|
||
// when the hold finally expires, step 5 of rollHour() halves filteringDenominator
|
||
// once per hour (rather than jumping directly to samplingDenominator) until the two
|
||
// converge, giving the hop-walk a gradual, 1-step-per-hour descent.
|
||
static constexpr uint32_t FILTER_DENOM_HOLD_MS = 13UL * 60UL * 60UL * 1000UL; // 13 h (documentation only)
|
||
// Number of rollHour() calls the hold spans — equals the seenHoursAgo window width.
|
||
// filteringDenomHoldRollsRemaining is initialised to this value on scale-up and
|
||
// decremented once per rollHour(); step-down begins when it reaches zero.
|
||
static constexpr uint8_t FILTER_DENOM_HOLD_ROLLS = 13u;
|
||
|
||
// Hop-walk: target cumulative affected-node count when choosing a hop limit
|
||
static constexpr uint16_t TARGET_AFFECTED_NODES = default_hop_scaling_min_target_nodes;
|
||
|
||
// Clamp bounds enforced on min_target_nodes / max_target_nodes
|
||
static constexpr uint16_t MIN_TARGET_NODES_FLOOR = default_hop_scaling_min_target_nodes_floor;
|
||
static constexpr uint16_t MAX_TARGET_NODES_CEILING = default_hop_scaling_max_target_nodes_ceiling;
|
||
static constexpr uint16_t MAX_TARGET_NODES = default_hop_scaling_max_target_nodes;
|
||
|
||
// Politeness factors for the one-hop extension check in the hop walk.
|
||
// Stored as integer numerators over POLITENESS_DENOM (4):
|
||
// politeLimit = min + gap * politeNumer / POLITENESS_DENOM
|
||
// STRICT → min + 25% of gap; DEFAULT → midpoint; GENEROUS → max
|
||
static constexpr uint8_t POLITENESS_DENOM = 4u;
|
||
static constexpr uint8_t POLITENESS_GENEROUS = 4u; // 4/4 = 1.00
|
||
static constexpr uint8_t POLITENESS_DEFAULT = 2u; // 2/4 = 0.50
|
||
static constexpr uint8_t POLITENESS_STRICT = 1u; // 1/4 = 0.25
|
||
|
||
// Activity weight thresholds (ratio of 0-2 h window vs 1-3 h window).
|
||
// Cross-multiply form: recent * ACTIVITY_WEIGHT_SCALE vs older * threshold_numer.
|
||
// GENEROUS if recent*10 < older*9 (ratio < 0.9); STRICT if recent*10 > older*12 (ratio > 1.2)
|
||
static constexpr uint8_t ACTIVITY_WEIGHT_SCALE = 10u;
|
||
static constexpr uint8_t ACTIVITY_WEIGHT_GENEROUS_MAX_NUMER = 9u;
|
||
static constexpr uint8_t ACTIVITY_WEIGHT_STRICT_MIN_NUMER = 12u;
|
||
|
||
// Scheduling: number of 5-minute runOnce() ticks that make up one hourly rollover
|
||
static constexpr uint8_t RUNS_PER_HOUR = 12;
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Types
|
||
// -----------------------------------------------------------------------
|
||
|
||
/// Per-hop node counts produced at each hourly rollover.
|
||
struct PerHopCounts {
|
||
uint16_t perHop[MAX_HOP + 1] = {};
|
||
uint16_t total = 0;
|
||
};
|
||
|
||
/// Mesh activity trend stats produced at each hourly rollover.
|
||
/// All counts are scaled by filteringDenominator (i.e. estimated full-mesh population).
|
||
///
|
||
/// Bitmap interpretation (before the hourly shift): bit 0 = just-completed hour, bit 12 = 12 h ago.
|
||
struct MeshTrendStats {
|
||
/// Estimated node count per hour slot (h=0 is the just-completed hour, h=12 is 12 h ago).
|
||
uint16_t scaledPerHour[13] = {};
|
||
/// Nodes heard only this hour with no prior bitmap history — indicates new arrivals.
|
||
uint16_t newThisHour = 0;
|
||
/// Nodes heard this hour that also appeared in at least one older hour — stable regulars.
|
||
uint16_t returningThisHour = 0;
|
||
/// Nodes heard last hour but silent this hour — potential departures.
|
||
uint16_t lapsedSinceLastHour = 0;
|
||
/// Nodes absent from the last 4 hours but still present in some older hour (5–13 h) — quieting down.
|
||
uint16_t olderThan4h = 0;
|
||
/// Nodes whose only remaining history is the 13th hour (bit 12 only) — about to age out entirely.
|
||
uint16_t agingOut = 0;
|
||
};
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Lifecycle
|
||
// -----------------------------------------------------------------------
|
||
|
||
HopScalingModule();
|
||
~HopScalingModule() = default;
|
||
|
||
/// Reset all entries and state.
|
||
void clear();
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Core API
|
||
// -----------------------------------------------------------------------
|
||
|
||
/// Record a received packet.
|
||
/// Adds or updates an entry when passesFilter(hashNodeId(nodeId), samplingDenominator),
|
||
/// i.e. when the 16-bit XOR-fold hash of the node ID falls in the 1/samplingDenominator
|
||
/// subsample of the hash space. This is NOT a raw nodeId modulo check.
|
||
/// Marks the current hour as seen and updates the stored hop count to the last observed value.
|
||
/// Triggers a trim pass if the list exceeds FILL_HIGH_PCT after the insertion.
|
||
void samplePacketForHistogram(uint32_t nodeId, uint8_t hopCount);
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Accessors
|
||
// -----------------------------------------------------------------------
|
||
|
||
uint8_t getLastRequiredHop() const { return lastRequiredHop; }
|
||
uint8_t getEntryCount() const { return count; }
|
||
uint8_t getFillPercentage() const { return static_cast<uint8_t>((static_cast<uint16_t>(count) * 100u) / CAPACITY); }
|
||
uint8_t getSamplingDenominator() const { return samplingDenominator; }
|
||
uint8_t getFilteringDenominator() const { return filteringDenominator; }
|
||
float getPoliteness() const { return lastPoliteNumer / static_cast<float>(POLITENESS_DENOM); }
|
||
const PerHopCounts &getLastPerHopCounts() const { return lastPerHopCounts; }
|
||
uint8_t getLastSuggestedHop() const { return lastSuggestedHop; }
|
||
const MeshTrendStats &getLastTrendStats() const { return lastTrendStats; }
|
||
|
||
// Compatibility accessors used by tests
|
||
uint8_t getCompactHistogramEntryCount() const { return getEntryCount(); }
|
||
uint8_t getCompactHistogramDenominator() const { return getSamplingDenominator(); }
|
||
uint8_t getCompactHistogramFilterDenominator() const { return getFilteringDenominator(); }
|
||
uint8_t getCompactHistogramSuggestedHop() const { return getLastSuggestedHop(); }
|
||
size_t getCompactHistogramAllSampleCount() const { return getEntryCount(); }
|
||
|
||
/// Force both sampling and filtering denominators to a specific value.
|
||
/// Intended for unit tests that need a deterministic starting denominator.
|
||
void setSamplingDenominator(uint8_t d)
|
||
{
|
||
samplingDenominator = (d < DENOM_MIN) ? DENOM_MIN : (d > DENOM_MAX ? DENOM_MAX : d);
|
||
filteringDenominator = samplingDenominator;
|
||
filteringDenomHoldRollsRemaining = 0;
|
||
}
|
||
|
||
#ifdef PIO_UNIT_TESTING
|
||
// Writable from tests as HopScalingModule::s_testNowMs; drives nowMs() in PIO_UNIT_TESTING builds.
|
||
inline static uint32_t s_testNowMs = 0;
|
||
/// Override the per-session hash seed. Use in tests that need a specific sampling distribution.
|
||
void setHashSeed(uint16_t seed) { hashSeed = seed; }
|
||
uint16_t getHashSeed() const { return hashSeed; }
|
||
/// Expose hashNodeId for tests that need to compute which node IDs pass a given denominator.
|
||
uint16_t hashNodeIdPublic(uint32_t nodeId) const { return hashNodeId(nodeId); }
|
||
#endif
|
||
|
||
protected:
|
||
int32_t runOnce() override;
|
||
|
||
private:
|
||
#ifdef PIO_UNIT_TESTING
|
||
friend class HopScalingTestShim;
|
||
#endif
|
||
|
||
/// Perform hourly rollover.
|
||
/// 1. Tallies per-hop counts for entries matching filteringDenominator and seen in 13 h.
|
||
/// 2. Walks the scaled hop buckets and returns the recommended hop limit.
|
||
/// 3. Logs scaled per-hop counts and recommendation.
|
||
/// 4. Checks for scale-down (< FILL_LOW_PCT of capacity pass filteringDenominator).
|
||
/// 5. Decrements filteringDenomHoldRollsRemaining (if > 0); once it reaches zero, halves
|
||
/// filteringDenominator once toward samplingDenominator per rollHour() call.
|
||
/// 6. Shifts all seen bitmaps left by one hour slot.
|
||
void rollHour();
|
||
// -----------------------------------------------------------------------
|
||
// Persistence
|
||
// -----------------------------------------------------------------------
|
||
|
||
/// Persist the histogram state (entries, denominators, hold-timer) to flash.
|
||
/// No-op on platforms without a filesystem. Performs a full delete-and-rewrite of
|
||
/// the state file on each call; avoid calling more frequently than once per rollHour().
|
||
void saveToDisk() const;
|
||
|
||
/// Restore histogram state from flash. Safe to call even when no file exists.
|
||
/// Call once after construction, before the first rollHour(), to warm-start the
|
||
/// histogram across reboots without waiting 13 hours for data to re-accumulate.
|
||
/// The restored entries are available immediately for sampling, but the first
|
||
/// rollHour() (triggered by the second runOnce() tick) is needed before a warm-start
|
||
/// recommendation replaces the HOP_MAX boot default.
|
||
void loadFromDisk();
|
||
|
||
/// Remove stale entries (seen-bits all zero) and, if the list is still crowded,
|
||
/// double samplingDenominator and filteringDenominator and remove non-matching entries.
|
||
void trimIfNeeded();
|
||
|
||
void logStatusReport(bool didHourlyUpdate) const;
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Histogram storage
|
||
// -----------------------------------------------------------------------
|
||
Record entries[CAPACITY] = {};
|
||
uint8_t count = 0;
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Denominator state
|
||
//
|
||
// Two separate denominators control two distinct gates:
|
||
//
|
||
// samplingDenominator — admission gate. A node is added/updated only when
|
||
// passesFilter(hash, samplingDenominator). Lower value = more permissive =
|
||
// more nodes enter = represents recent mesh state.
|
||
//
|
||
// filteringDenominator — counting gate. The hop-walk tally in rollHour() only
|
||
// counts entries that pass passesFilter(hash, filteringDenominator). It moves
|
||
// up with samplingDenominator immediately (scale-up) but is held at the
|
||
// elevated value for FILTER_DENOM_HOLD_MS (13 h) after any scale-up before it
|
||
// may drop back down (scale-down).
|
||
//
|
||
// Why the estimate is invariant: passesFilter uses a hash-based uniform subsample.
|
||
// For any two powers-of-two denominators D ≤ F, the fraction of D-sampled entries
|
||
// that also pass F is exactly D/F. Therefore:
|
||
// raw_count × F = (total × D/F) × F = total × D
|
||
// The population estimate is the same whether we count with D or with F.
|
||
// The hold period is not about accuracy — it is about stability: it prevents the
|
||
// hop recommendation from reacting to recently-admitted nodes that have not yet
|
||
// accumulated enough seenHoursAgo history to be statistically reliable.
|
||
//
|
||
// denominatorHistory[h] — the filteringDenominator used to both gate and scale
|
||
// hourlyRaw[h]. Invariant: denominatorHistory[h] always equals the
|
||
// filteringDenominator that was active when seenHoursAgo bit h was set.
|
||
// rollHour() advances the array at the very start (before the tally loop), then
|
||
// gates hourlyRaw[h] per-slot by denominatorHistory[h] — each slot's raw count
|
||
// and multiplier are therefore always consistent, even when filteringDenominator
|
||
// changes between rolls (e.g. hold expiry). On scale-up (trimIfNeeded()), the
|
||
// entire array is backfilled uniformly with the new filteringDenominator to
|
||
// preserve the invariant retroactively for all 13 slots. Initialised to
|
||
// DENOM_MIN (1); scaledPerHour slots that draw from a 1 entry are unscaled —
|
||
// correct for a fresh instance with no prior history.
|
||
// -----------------------------------------------------------------------
|
||
uint8_t samplingDenominator = DENOM_MIN;
|
||
uint8_t filteringDenominator = DENOM_MIN;
|
||
uint8_t filteringDenomHoldRollsRemaining = 0; // counts down from FILTER_DENOM_HOLD_ROLLS to 0; step-down fires at 0
|
||
uint8_t denominatorHistory[13] = {};
|
||
uint16_t hashSeed = 0;
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Cached hourly results
|
||
// -----------------------------------------------------------------------
|
||
PerHopCounts lastPerHopCounts = {};
|
||
uint16_t lastScaledPerHop[MAX_HOP + 1] = {};
|
||
uint8_t lastSuggestedHop = MAX_HOP;
|
||
uint8_t lastPoliteNumer = POLITENESS_DEFAULT;
|
||
MeshTrendStats lastTrendStats = {};
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Hop recommendation state
|
||
// -----------------------------------------------------------------------
|
||
uint8_t lastRequiredHop = HOP_MAX;
|
||
uint8_t histogramRollCount = 0;
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Scheduler state
|
||
// -----------------------------------------------------------------------
|
||
bool hasCompletedInitialRun = false;
|
||
uint8_t runsSinceLastHourlyUpdate = 0;
|
||
|
||
// -----------------------------------------------------------------------
|
||
// Inline record helpers
|
||
// -----------------------------------------------------------------------
|
||
|
||
// Record field semantics:
|
||
// nodeHash → XOR-fold of full 32-bit node ID to 16 bits
|
||
// hops_away → hop distance (0–7)
|
||
// seenHoursAgo → 13-bit per-hour seen bitmap
|
||
// bit 0 = seen in the current / most-recent hour
|
||
// bit 12 = seen 12 hours ago
|
||
// Shifts left on each rollHour(); 0 means not seen in 13 h.
|
||
|
||
/// XOR-fold + golden-ratio hash of a 32-bit node ID to 16 bits, mixed with the session seed.
|
||
/// Multiplying by floor(2^32 / φ) gives uniform avalanche; XORing the seed ensures different
|
||
/// devices (or the same device after a clear()) sample a different subset of node IDs.
|
||
/// For seed=0 the function is deterministic, which is used in PIO_UNIT_TESTING builds.
|
||
uint16_t hashNodeId(uint32_t nodeId) const { return static_cast<uint16_t>((nodeId * 2654435761u) >> 16) ^ hashSeed; }
|
||
static bool seenInLast13h(const Record &r) { return r.seenHoursAgo != 0u; }
|
||
static void markCurrentHour(Record &r) { r.seenHoursAgo |= 1u; }
|
||
static void rollSeenBits(Record &r) { r.seenHoursAgo = (r.seenHoursAgo << 1u) & 0x1FFFu; }
|
||
static bool passesFilter(uint16_t nodeHash, uint8_t denom) { return (nodeHash & static_cast<uint16_t>(denom - 1u)) == 0u; }
|
||
|
||
public:
|
||
// Clock — public so tests can share the same timebase via HopScalingModule::s_testNowMs
|
||
#ifdef PIO_UNIT_TESTING
|
||
static uint32_t nowMs() { return s_testNowMs; }
|
||
#else
|
||
static uint32_t nowMs() { return millis(); }
|
||
#endif
|
||
};
|
||
|
||
extern HopScalingModule *hopScalingModule;
|
||
|
||
#endif
|