mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-02 15:53:56 +00:00
acw: airtime-scale jitter caps, companion surroundings awareness
- flood retransmit jitter now capped at min(2000ms, 6·airtime) instead of fixed 2000ms — spreads tighter at SF7, unchanged at SF8 - reactive per-dupe backoff cap now min(2000ms, 12·airtime), keeps semantic of "push past ~12 relay slots" - contention ring 16 → 24 for 50-neighbor hilltops - companions passively track heard floods (warms EMA without forwarding) and spread their own TX by up to min(1000ms, 3·airtime), hopefully fixing repeaters missing companion's first transmission config cleanup: - move BLE TX buffer bumps (ACL_TX=12 etc.) from zephcore_common.conf to esp32_common.conf — the Espressif blob needs them, nRF doesn't, and the bumps were overflowing nRF52840 RAM - remove CONFIG_ZEPHCORE_MAX_CONTACTS=510 overrides from 5 nRF52840 companion boards; Kconfig default of 350 fits with comfortable margin (wio prod: 91% → 79% RAM)
This commit is contained in:
+699
-691
File diff suppressed because it is too large
Load Diff
@@ -1173,10 +1173,13 @@ void CompanionMesh::onRawDataRecv(mesh::Packet *packet)
|
||||
uint32_t CompanionMesh::getRetransmitDelay(const mesh::Packet *packet)
|
||||
{
|
||||
float factor = getContentionTracker().getFloodDelayFactor();
|
||||
uint32_t t = (uint32_t)(_radio->getEstAirtimeFor(
|
||||
packet->getPathByteLen() + packet->payload_len + 2) * factor);
|
||||
uint32_t max_jitter = 5 * t;
|
||||
/* Cap jitter to 2000ms to avoid excessive latency in very dense areas.
|
||||
uint32_t airtime = _radio->getEstAirtimeFor(
|
||||
packet->getPathByteLen() + packet->payload_len + 2);
|
||||
uint32_t max_jitter = (uint32_t)(5 * airtime * factor);
|
||||
/* Airtime-scaled ceiling: never exceed ~6 airtimes of spread. */
|
||||
uint32_t airtime_cap = 6 * airtime;
|
||||
if (max_jitter > airtime_cap) max_jitter = airtime_cap;
|
||||
/* Absolute cap: avoid excessive latency in very dense areas.
|
||||
* Reactive backoff will fine-tune further if needed. */
|
||||
if (max_jitter > 2000) max_jitter = 2000;
|
||||
/* Floor: give downstream nodes time to finish RX processing
|
||||
@@ -1193,6 +1196,21 @@ uint32_t CompanionMesh::getDirectRetransmitDelay(const mesh::Packet *packet)
|
||||
return 20 + getRNG()->nextInt(0, t / 10 + 1);
|
||||
}
|
||||
|
||||
uint32_t CompanionMesh::getInitialFloodJitter(const mesh::Packet *packet)
|
||||
{
|
||||
float factor = getContentionTracker().getFloodDelayFactor();
|
||||
uint32_t airtime = _radio->getEstAirtimeFor(
|
||||
packet->getPathByteLen() + packet->payload_len + 2);
|
||||
uint32_t max_jitter = (uint32_t)(5 * airtime * factor);
|
||||
/* Companion spreads less aggressively than a repeater: half the
|
||||
* airtime ceiling, and a tighter absolute cap (1000ms vs 2000ms). */
|
||||
uint32_t airtime_cap = 3 * airtime;
|
||||
if (max_jitter > airtime_cap) max_jitter = airtime_cap;
|
||||
if (max_jitter > 1000) max_jitter = 1000;
|
||||
if (max_jitter == 0) return 0;
|
||||
return getRNG()->nextInt(0, max_jitter + 1);
|
||||
}
|
||||
|
||||
uint8_t CompanionMesh::getDutyCyclePercent() const
|
||||
{
|
||||
return (uint8_t)prefs.airtime_factor;
|
||||
|
||||
@@ -261,6 +261,13 @@ protected:
|
||||
/* Dispatcher tuning (uses prefs) */
|
||||
uint32_t getRetransmitDelay(const mesh::Packet *packet) override;
|
||||
uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override;
|
||||
|
||||
/* Companion doesn't forward, but needs surroundings awareness so its
|
||||
* initial TX spreads with local contention. Enables passive EMA
|
||||
* warming and adaptive initial-flood jitter. */
|
||||
bool passivelyTrackFloods() const override { return true; }
|
||||
uint32_t getInitialFloodJitter(const mesh::Packet *packet) override;
|
||||
|
||||
uint8_t getDutyCyclePercent() const override;
|
||||
uint8_t getExtraAckTransmitCount() const override;
|
||||
|
||||
|
||||
@@ -522,10 +522,14 @@ void RepeaterMesh::logTxFail(mesh::Packet* pkt, int len) {
|
||||
|
||||
uint32_t RepeaterMesh::getRetransmitDelay(const mesh::Packet* packet) {
|
||||
float factor = getContentionTracker().getFloodDelayFactor();
|
||||
uint32_t t = (uint32_t)(_radio->getEstAirtimeFor(
|
||||
packet->getPathByteLen() + packet->payload_len + 2) * factor);
|
||||
uint32_t max_jitter = 5 * t;
|
||||
/* Cap jitter to 2000ms to avoid excessive latency in very dense areas.
|
||||
uint32_t airtime = _radio->getEstAirtimeFor(
|
||||
packet->getPathByteLen() + packet->payload_len + 2);
|
||||
uint32_t max_jitter = (uint32_t)(5 * airtime * factor);
|
||||
/* Airtime-scaled ceiling: never exceed ~6 airtimes of spread
|
||||
* (keeps SF7/narrow-BW configs from wasting time in oversized jitter windows). */
|
||||
uint32_t airtime_cap = 6 * airtime;
|
||||
if (max_jitter > airtime_cap) max_jitter = airtime_cap;
|
||||
/* Absolute cap: avoid excessive latency in very dense areas.
|
||||
* Reactive backoff will fine-tune further if needed. */
|
||||
if (max_jitter > 2000) max_jitter = 2000;
|
||||
/* Floor: give downstream nodes time to finish RX processing
|
||||
|
||||
@@ -15,6 +15,15 @@ CONFIG_BT_PRIVACY=y
|
||||
# BLE thread stacks — ESP32 software BLE controller needs larger stacks
|
||||
CONFIG_BT_TX_PROCESSOR_STACK_SIZE=2048
|
||||
|
||||
# BLE TX buffers — the Espressif BLE controller blob bursts more aggressively
|
||||
# than the nRF softdevice and will deadlock the system workqueue if ACL TX
|
||||
# buffers run dry (bt_hci_cmd_alloc(K_FOREVER) blocks the cooperative syswq).
|
||||
# EVT_RX must strictly exceed ACL_TX (enforced by BUILD_ASSERT in buf.h).
|
||||
CONFIG_BT_BUF_ACL_TX_COUNT=12
|
||||
CONFIG_BT_L2CAP_TX_BUF_COUNT=12
|
||||
CONFIG_BT_CONN_TX_MAX=12
|
||||
CONFIG_BT_BUF_EVT_RX_COUNT=14
|
||||
|
||||
# ========== Heap ==========
|
||||
# ESP32 BLE stack requires larger heap (override zephcore_common 2KB default)
|
||||
CONFIG_HEAP_MEM_POOL_SIZE=32768
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
# Production Config - Common to ALL boards (Companion & Repeater)
|
||||
# Build: west build -b <board> zephcore --pristine -- \
|
||||
# -DEXTRA_CONF_FILE="boards/common/prod.conf"
|
||||
#
|
||||
# Disables logging for smaller binary and lower power consumption.
|
||||
# USB CDC still works for CLI commands on repeaters.
|
||||
|
||||
# Disable logging subsystem (saves ~62KB flash, ~2-5mA power)
|
||||
CONFIG_LOG=n
|
||||
|
||||
# Disable asserts (saves flash, removes CMake warning)
|
||||
CONFIG_ASSERT=n
|
||||
|
||||
# Disable SEGGER RTT (saves ~4KB RAM ring buffer + init code)
|
||||
CONFIG_USE_SEGGER_RTT=n
|
||||
|
||||
# Disable thread name strings (saves flash — only useful for debug)
|
||||
CONFIG_THREAD_NAME=n
|
||||
|
||||
# Max contacts: Kconfig default is 350 (safe for 100KB ExtraFS).
|
||||
# Do NOT set here — EXTRA_CONF_FILE overrides board.conf, which would
|
||||
# prevent boards with more storage (Wio QSPI=510) from raising the limit.
|
||||
# Production Config - Common to ALL boards (Companion & Repeater)
|
||||
# Build: west build -b <board> zephcore --pristine -- \
|
||||
# -DEXTRA_CONF_FILE="boards/common/prod.conf"
|
||||
#
|
||||
# Disables logging for smaller binary and lower power consumption.
|
||||
# USB CDC still works for CLI commands on repeaters.
|
||||
|
||||
# Disable logging subsystem (saves ~62KB flash, ~2-5mA power)
|
||||
CONFIG_LOG=n
|
||||
|
||||
# Disable asserts (saves flash, removes CMake warning)
|
||||
CONFIG_ASSERT=n
|
||||
|
||||
# Disable SEGGER RTT (saves ~4KB RAM ring buffer + init code)
|
||||
CONFIG_USE_SEGGER_RTT=n
|
||||
|
||||
# Disable thread name strings (saves flash — only useful for debug)
|
||||
CONFIG_THREAD_NAME=n
|
||||
|
||||
# Max contacts: Kconfig default is 350 — fits nRF52840 RAM with ~10% headroom
|
||||
# and is safe for 100KB ExtraFS. RAM-limited boards (xiao_nrf54l15=450,
|
||||
# xiao_esp32c3=300) override in their own board.conf. Do not set here.
|
||||
|
||||
@@ -93,12 +93,11 @@ CONFIG_BT_BUF_ACL_RX_SIZE=251
|
||||
CONFIG_BT_BUF_ACL_TX_SIZE=251
|
||||
CONFIG_BT_L2CAP_TX_MTU=247
|
||||
|
||||
# BLE TX buffers — default 3 is too low, causes system workqueue deadlock
|
||||
# when BLE activity bursts (DLE + param update + ATT) exhaust all buffers
|
||||
# and bt_hci_cmd_alloc(K_FOREVER) blocks the cooperative syswq forever.
|
||||
CONFIG_BT_BUF_ACL_TX_COUNT=12
|
||||
CONFIG_BT_L2CAP_TX_BUF_COUNT=12
|
||||
CONFIG_BT_CONN_TX_MAX=12
|
||||
# BLE TX buffers — platform-specific. ESP32 gets a large bump in
|
||||
# esp32_common.conf to prevent syswq deadlock during BLE bursts (DLE +
|
||||
# param update + ATT exhausting buffers and bt_hci_cmd_alloc(K_FOREVER)
|
||||
# blocking the cooperative syswq). nRF52 uses Zephyr defaults — RAM is
|
||||
# tight on nRF52840 and the nRF controller rarely deadlocks.
|
||||
|
||||
# BLE RX thread stack — default 1200 too small for ATT handlers + logging + asserts
|
||||
CONFIG_BT_RX_STACK_SIZE=2048
|
||||
|
||||
@@ -10,5 +10,3 @@ CONFIG_ZEPHCORE_BOARD_NAME="SenseCAP Solar"
|
||||
CONFIG_BT_DIS_MODEL_NUMBER_STR="Seeed SenseCAP Solar"
|
||||
|
||||
CONFIG_ZEPHCORE_SD_FWID=0x0123
|
||||
|
||||
CONFIG_ZEPHCORE_MAX_CONTACTS=510
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
# Elecrow ThinkNode M1 (nRF52840 + SX1262 + SSD1681 EPD)
|
||||
# Board-specific configuration
|
||||
#
|
||||
# Hardware:
|
||||
# - nRF52840 SoC, SX1262 LoRa (22dBm), 1.54" SSD1681 e-paper (200x200)
|
||||
# - GPS module on UART0, MX25R1635F 2MB QSPI flash
|
||||
# - Battery ADC on AIN2 (P0.04), 150K+150K divider
|
||||
# - Buzzer on P0.06, two buttons, GPS hardware switch
|
||||
# - LEDs: GREEN=P1.04, BLUE=P0.14
|
||||
|
||||
# Board identification (matches Arduino MeshCore variant name)
|
||||
CONFIG_ZEPHCORE_BOARD_NAME="ThinkNode M1"
|
||||
|
||||
# Device Information Service model name
|
||||
CONFIG_BT_DIS_MODEL_NUMBER_STR="Elecrow ThinkNode-M1"
|
||||
|
||||
# SoftDevice firmware ID (ThinkNode M1 bootloader uses S140 v6)
|
||||
CONFIG_ZEPHCORE_SD_FWID=0x00B6
|
||||
|
||||
# Contacts — QSPI (2MB) provides ample storage for more contacts
|
||||
CONFIG_ZEPHCORE_MAX_CONTACTS=510
|
||||
|
||||
# ========== RAM budget ==========
|
||||
# SSD1681 200x200 CFB framebuffer needs 5000 bytes from k_malloc.
|
||||
# Default heap (2048) is sized for SSD1306 128x64 (1024 bytes).
|
||||
CONFIG_HEAP_MEM_POOL_SIZE=6144
|
||||
|
||||
# Shrink RTT buffer to free RAM (4096 → 1024, saves 3KB).
|
||||
# Only used during J-Link debugging, 1KB is Zephyr's default.
|
||||
CONFIG_SEGGER_RTT_BUFFER_SIZE_UP=1024
|
||||
# Elecrow ThinkNode M1 (nRF52840 + SX1262 + SSD1681 EPD)
|
||||
# Board-specific configuration
|
||||
#
|
||||
# Hardware:
|
||||
# - nRF52840 SoC, SX1262 LoRa (22dBm), 1.54" SSD1681 e-paper (200x200)
|
||||
# - GPS module on UART0, MX25R1635F 2MB QSPI flash
|
||||
# - Battery ADC on AIN2 (P0.04), 150K+150K divider
|
||||
# - Buzzer on P0.06, two buttons, GPS hardware switch
|
||||
# - LEDs: GREEN=P1.04, BLUE=P0.14
|
||||
|
||||
# Board identification (matches Arduino MeshCore variant name)
|
||||
CONFIG_ZEPHCORE_BOARD_NAME="ThinkNode M1"
|
||||
|
||||
# Device Information Service model name
|
||||
CONFIG_BT_DIS_MODEL_NUMBER_STR="Elecrow ThinkNode-M1"
|
||||
|
||||
# SoftDevice firmware ID (ThinkNode M1 bootloader uses S140 v6)
|
||||
CONFIG_ZEPHCORE_SD_FWID=0x00B6
|
||||
|
||||
# ========== RAM budget ==========
|
||||
# SSD1681 200x200 CFB framebuffer needs 5000 bytes from k_malloc.
|
||||
# Default heap (2048) is sized for SSD1306 128x64 (1024 bytes).
|
||||
CONFIG_HEAP_MEM_POOL_SIZE=6144
|
||||
|
||||
# Shrink RTT buffer to free RAM (4096 → 1024, saves 3KB).
|
||||
# Only used during J-Link debugging, 1KB is Zephyr's default.
|
||||
CONFIG_SEGGER_RTT_BUFFER_SIZE_UP=1024
|
||||
|
||||
@@ -18,6 +18,3 @@ CONFIG_BT_DIS_MODEL_NUMBER_STR="Elecrow ThinkNode-M6"
|
||||
|
||||
# SoftDevice firmware ID (ThinkNode M6 bootloader uses S140 v6, same as M1/M3)
|
||||
CONFIG_ZEPHCORE_SD_FWID=0x00B6
|
||||
|
||||
# Contacts — QSPI (2MB) provides ample storage for more contacts
|
||||
CONFIG_ZEPHCORE_MAX_CONTACTS=510
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
# Wio Tracker L1 (nRF52840 + SX1262)
|
||||
# Board-specific configuration - pins and unique features only
|
||||
#
|
||||
# Hardware:
|
||||
# - SX1262 LoRa on SPI0 (P1.14 CS, P1.07 RST, P1.10 BUSY, P0.07 DIO1, P1.08 RXEN)
|
||||
# - L76KB GPS on UART0 (P0.26/P0.27)
|
||||
# - P25Q16H 2MB QSPI flash
|
||||
# - Battery ADC on AIN7 (P0.31), enable on P0.04
|
||||
# - SH1106 OLED on I2C0 (P0.05/P0.06, addr 0x3D)
|
||||
# - Joystick + user button + buzzer (P1.00)
|
||||
|
||||
# Board identification (matches Arduino variant name)
|
||||
CONFIG_ZEPHCORE_BOARD_NAME="Wio Tracker L1"
|
||||
|
||||
# Device Information Service model name
|
||||
CONFIG_BT_DIS_MODEL_NUMBER_STR="Wio Tracker L1"
|
||||
|
||||
# SoftDevice firmware ID (s140 v7.3.0 — Seeed bootloader)
|
||||
CONFIG_ZEPHCORE_SD_FWID=0x0123
|
||||
|
||||
# Contacts — QSPI (2MB) provides ample storage for more contacts
|
||||
CONFIG_ZEPHCORE_MAX_CONTACTS=510
|
||||
# Wio Tracker L1 (nRF52840 + SX1262)
|
||||
# Board-specific configuration - pins and unique features only
|
||||
#
|
||||
# Hardware:
|
||||
# - SX1262 LoRa on SPI0 (P1.14 CS, P1.07 RST, P1.10 BUSY, P0.07 DIO1, P1.08 RXEN)
|
||||
# - L76KB GPS on UART0 (P0.26/P0.27)
|
||||
# - P25Q16H 2MB QSPI flash
|
||||
# - Battery ADC on AIN7 (P0.31), enable on P0.04
|
||||
# - SH1106 OLED on I2C0 (P0.05/P0.06, addr 0x3D)
|
||||
# - Joystick + user button + buzzer (P1.00)
|
||||
|
||||
# Board identification (matches Arduino variant name)
|
||||
CONFIG_ZEPHCORE_BOARD_NAME="Wio Tracker L1"
|
||||
|
||||
# Device Information Service model name
|
||||
CONFIG_BT_DIS_MODEL_NUMBER_STR="Wio Tracker L1"
|
||||
|
||||
# SoftDevice firmware ID (s140 v7.3.0 — Seeed bootloader)
|
||||
CONFIG_ZEPHCORE_SD_FWID=0x0123
|
||||
|
||||
@@ -7,8 +7,6 @@ CONFIG_BT_DIS_MODEL_NUMBER_STR="Seeed XIAO nRF52840"
|
||||
|
||||
CONFIG_ZEPHCORE_SD_FWID=0x0123
|
||||
|
||||
CONFIG_ZEPHCORE_MAX_CONTACTS=510
|
||||
|
||||
# Default debug (logging.conf) sends LOG_* to RTT only. Plain XIAO is usually
|
||||
# flashed/debugged over USB without an SWD J-Link — enable UART backend so
|
||||
# LOG_* appears on the CDC ACM serial port (same as zephyr,console).
|
||||
|
||||
@@ -1,80 +1,80 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Adaptive Contention Window — EMA-based flood retransmit delay
|
||||
*
|
||||
* Counts neighbor retransmit dupes within a 10s window per packet.
|
||||
* Dupe counts feed a rolling EMA that drives an adaptive delay factor.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
class Packet;
|
||||
|
||||
class ContentionTracker {
|
||||
public:
|
||||
ContentionTracker();
|
||||
|
||||
/* FNV-1a 32-bit hash for ring buffer correlation (not dedup SHA256). */
|
||||
static uint32_t computePacketHash32(const Packet *pkt);
|
||||
|
||||
void trackRetransmit(uint32_t hash32, uint32_t now_ms);
|
||||
|
||||
/* Returns true if packet matched a tracked retransmit (dupe recorded). */
|
||||
bool recordDupeIfTracked(uint32_t hash32, uint32_t now_ms);
|
||||
|
||||
/* Returns backoff_multiplier * airtime, clamped by remaining headroom.
|
||||
* Returns 0 when hard cap reached or backoff disabled. */
|
||||
uint16_t getReactiveHeadroom(uint32_t hash32, uint32_t airtime_ms) const;
|
||||
|
||||
void addReactiveExtension(uint32_t hash32, uint16_t added_ms);
|
||||
|
||||
/* Finalize expired entries into EMA. */
|
||||
void tick(uint32_t now_ms);
|
||||
|
||||
float getContentionEstimate() const;
|
||||
|
||||
/* sqrt curve: MIN_FLOOD_FACTOR + FLOOD_SCALE * sqrt(est), cap 2.0.
|
||||
* Returns 0.5 during warmup. */
|
||||
float getFloodDelayFactor() const;
|
||||
|
||||
bool isWarmedUp() const { return _finalized_count >= WARMUP_PACKETS; }
|
||||
|
||||
void setBackoffMultiplier(float m) { _backoff_multiplier = m; }
|
||||
float getBackoffMultiplier() const { return _backoff_multiplier; }
|
||||
|
||||
private:
|
||||
static constexpr int RING_SIZE = 16; /* max concurrent tracked retransmits */
|
||||
static constexpr uint32_t WINDOW_MS = 10000; /* dupe observation window; covers SF12 2-hop */
|
||||
static constexpr int EMA_SHIFT = 3; /* alpha = 1/8 */
|
||||
static constexpr int WARMUP_PACKETS = 4; /* min samples before EMA is trusted */
|
||||
static constexpr float MIN_FLOOD_FACTOR = 0.05f; /* floor: near-zero delay in quiet networks */
|
||||
static constexpr float FLOOD_SCALE = 0.170f; /* (0.5 - 0.05) / sqrt(15) */
|
||||
static constexpr float MAX_FLOOD_FACTOR = 2.0f; /* ceiling: 2x base airtime */
|
||||
static constexpr float DEFAULT_BACKOFF_MULT = 0.5f; /* half-airtime per dupe heard */
|
||||
static constexpr uint32_t REACTIVE_HARD_CAP_MS = 2000; /* max cumulative reactive extension */
|
||||
static constexpr uint32_t STALE_MS = 300000; /* 5 min: reset EMA if no traffic */
|
||||
|
||||
struct Entry {
|
||||
uint32_t hash32;
|
||||
uint32_t first_seen_ms;
|
||||
uint8_t dupe_count;
|
||||
uint16_t reactive_added_ms;
|
||||
bool active;
|
||||
};
|
||||
|
||||
Entry _ring[RING_SIZE];
|
||||
int _next_idx;
|
||||
uint32_t _ema_x256;
|
||||
int _finalized_count;
|
||||
uint32_t _last_retransmit_ms;
|
||||
float _backoff_multiplier;
|
||||
|
||||
void finalizeEntry(int idx);
|
||||
int findEntry(uint32_t hash32) const;
|
||||
};
|
||||
|
||||
} /* namespace mesh */
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Adaptive Contention Window — EMA-based flood retransmit delay
|
||||
*
|
||||
* Counts neighbor retransmit dupes within a 10s window per packet.
|
||||
* Dupe counts feed a rolling EMA that drives an adaptive delay factor.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
class Packet;
|
||||
|
||||
class ContentionTracker {
|
||||
public:
|
||||
ContentionTracker();
|
||||
|
||||
/* FNV-1a 32-bit hash for ring buffer correlation (not dedup SHA256). */
|
||||
static uint32_t computePacketHash32(const Packet *pkt);
|
||||
|
||||
void trackRetransmit(uint32_t hash32, uint32_t now_ms);
|
||||
|
||||
/* Returns true if packet matched a tracked retransmit (dupe recorded). */
|
||||
bool recordDupeIfTracked(uint32_t hash32, uint32_t now_ms);
|
||||
|
||||
/* Returns backoff_multiplier * airtime, clamped by remaining headroom.
|
||||
* Returns 0 when hard cap reached or backoff disabled. */
|
||||
uint16_t getReactiveHeadroom(uint32_t hash32, uint32_t airtime_ms) const;
|
||||
|
||||
void addReactiveExtension(uint32_t hash32, uint16_t added_ms);
|
||||
|
||||
/* Finalize expired entries into EMA. */
|
||||
void tick(uint32_t now_ms);
|
||||
|
||||
float getContentionEstimate() const;
|
||||
|
||||
/* sqrt curve: MIN_FLOOD_FACTOR + FLOOD_SCALE * sqrt(est), cap 2.0.
|
||||
* Returns 0.5 during warmup. */
|
||||
float getFloodDelayFactor() const;
|
||||
|
||||
bool isWarmedUp() const { return _finalized_count >= WARMUP_PACKETS; }
|
||||
|
||||
void setBackoffMultiplier(float m) { _backoff_multiplier = m; }
|
||||
float getBackoffMultiplier() const { return _backoff_multiplier; }
|
||||
|
||||
private:
|
||||
static constexpr int RING_SIZE = 24; /* max concurrent tracked retransmits */
|
||||
static constexpr uint32_t WINDOW_MS = 10000; /* dupe observation window; covers SF12 2-hop */
|
||||
static constexpr int EMA_SHIFT = 3; /* alpha = 1/8 */
|
||||
static constexpr int WARMUP_PACKETS = 4; /* min samples before EMA is trusted */
|
||||
static constexpr float MIN_FLOOD_FACTOR = 0.05f; /* floor: near-zero delay in quiet networks */
|
||||
static constexpr float FLOOD_SCALE = 0.170f; /* (0.5 - 0.05) / sqrt(15) */
|
||||
static constexpr float MAX_FLOOD_FACTOR = 2.0f; /* ceiling: 2x base airtime */
|
||||
static constexpr float DEFAULT_BACKOFF_MULT = 0.5f; /* half-airtime per dupe heard */
|
||||
static constexpr uint32_t REACTIVE_HARD_CAP_MS = 2000; /* max cumulative reactive extension */
|
||||
static constexpr uint32_t STALE_MS = 300000; /* 5 min: reset EMA if no traffic */
|
||||
|
||||
struct Entry {
|
||||
uint32_t hash32;
|
||||
uint32_t first_seen_ms;
|
||||
uint8_t dupe_count;
|
||||
uint16_t reactive_added_ms;
|
||||
bool active;
|
||||
};
|
||||
|
||||
Entry _ring[RING_SIZE];
|
||||
int _next_idx;
|
||||
uint32_t _ema_x256;
|
||||
int _finalized_count;
|
||||
uint32_t _last_retransmit_ms;
|
||||
float _backoff_multiplier;
|
||||
|
||||
void finalizeEntry(int idx);
|
||||
int findEntry(uint32_t hash32) const;
|
||||
};
|
||||
|
||||
} /* namespace mesh */
|
||||
|
||||
+107
-101
@@ -1,101 +1,107 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* ZephCore Mesh - routing protocol layer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mesh/Dispatcher.h>
|
||||
#include <mesh/ContentionTracker.h>
|
||||
#ifdef CONFIG_ZEPHCORE_APC
|
||||
#include <mesh/PowerController.h>
|
||||
#endif
|
||||
#include <mesh/RTC.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
struct GroupChannel {
|
||||
uint8_t hash[PATH_HASH_SIZE];
|
||||
uint8_t secret[PUB_KEY_SIZE];
|
||||
};
|
||||
|
||||
class MeshTables {
|
||||
public:
|
||||
virtual bool hasSeen(const Packet *packet) = 0;
|
||||
virtual void clear(const Packet *packet) = 0;
|
||||
};
|
||||
|
||||
class Mesh : public Dispatcher {
|
||||
RNG *_rng;
|
||||
RTCClock *_rtc;
|
||||
MeshTables *_tables;
|
||||
|
||||
void removeSelfFromPath(Packet *packet);
|
||||
void routeDirectRecvAcks(Packet *packet, uint32_t delay_millis);
|
||||
DispatcherAction forwardMultipartDirect(Packet *pkt);
|
||||
|
||||
protected:
|
||||
ContentionTracker _contention;
|
||||
ContentionTracker& getContentionTracker() { return _contention; }
|
||||
const ContentionTracker& getContentionTracker() const { return _contention; }
|
||||
#ifdef CONFIG_ZEPHCORE_APC
|
||||
PowerController _power_ctrl;
|
||||
PowerController& getPowerController() { return _power_ctrl; }
|
||||
const PowerController& getPowerController() const { return _power_ctrl; }
|
||||
#endif
|
||||
void extendPendingRetransmit(uint32_t hash32);
|
||||
|
||||
DispatcherAction onRecvPacket(Packet *pkt) override;
|
||||
virtual uint32_t getCADFailRetryDelay() const override;
|
||||
virtual DispatcherAction routeRecvPacket(Packet *packet);
|
||||
virtual bool filterRecvFloodPacket(Packet *packet) { return false; }
|
||||
virtual bool allowPacketForward(const Packet *packet);
|
||||
virtual uint32_t getRetransmitDelay(const Packet *packet);
|
||||
virtual uint32_t getDirectRetransmitDelay(const Packet *packet) { return 0; }
|
||||
virtual uint8_t getExtraAckTransmitCount() const { return 0; }
|
||||
virtual int searchPeersByHash(const uint8_t *hash) { (void)hash; return 0; }
|
||||
virtual void getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) { (void)dest_secret; (void)peer_idx; }
|
||||
virtual void onPeerDataRecv(Packet *packet, uint8_t type, int sender_idx, const uint8_t *secret, uint8_t *data, size_t len) { (void)packet; (void)type; (void)sender_idx; (void)secret; (void)data; (void)len; }
|
||||
virtual void onTraceRecv(Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) { (void)packet; (void)tag; (void)auth_code; (void)flags; (void)path_snrs; (void)path_hashes; (void)path_len; }
|
||||
virtual bool onPeerPathRecv(Packet *packet, int sender_idx, const uint8_t *secret, uint8_t *path, uint8_t path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len) { (void)packet; (void)sender_idx; (void)secret; (void)path; (void)path_len; (void)extra_type; (void)extra; (void)extra_len; return false; }
|
||||
virtual void onAdvertRecv(Packet *packet, const Identity &id, uint32_t timestamp, const uint8_t *app_data, size_t app_data_len) { (void)packet; (void)id; (void)timestamp; (void)app_data; (void)app_data_len; }
|
||||
virtual void onAnonDataRecv(Packet *packet, const uint8_t *secret, const Identity &sender, uint8_t *data, size_t len) { (void)packet; (void)secret; (void)sender; (void)data; (void)len; }
|
||||
virtual void onPathRecv(Packet *packet, Identity &sender, uint8_t *path, uint8_t path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len) { (void)packet; (void)sender; (void)path; (void)path_len; (void)extra_type; (void)extra; (void)extra_len; }
|
||||
virtual void onControlDataRecv(Packet *packet) { (void)packet; }
|
||||
virtual void onRawDataRecv(Packet *packet) { (void)packet; }
|
||||
virtual int searchChannelsByHash(const uint8_t *hash, GroupChannel channels[], int max_matches) { (void)hash; (void)channels; (void)max_matches; return 0; }
|
||||
virtual void onGroupDataRecv(Packet *packet, uint8_t type, const GroupChannel &channel, uint8_t *data, size_t len) { (void)packet; (void)type; (void)channel; (void)data; (void)len; }
|
||||
virtual void onAckRecv(Packet *packet, uint32_t ack_crc) { (void)packet; (void)ack_crc; }
|
||||
|
||||
public:
|
||||
Mesh(Radio &radio, MillisecondClock &ms, RNG &rng, RTCClock &rtc, PacketManager &mgr, MeshTables &tables);
|
||||
void begin();
|
||||
void loop();
|
||||
void maintenanceLoop();
|
||||
|
||||
LocalIdentity self_id;
|
||||
|
||||
RNG *getRNG() const { return _rng; }
|
||||
RTCClock *getRTCClock() const { return _rtc; }
|
||||
MeshTables *getTables() const { return _tables; }
|
||||
|
||||
Packet *createAdvert(const LocalIdentity &id, const uint8_t *app_data = nullptr, size_t app_data_len = 0);
|
||||
Packet *createAck(uint32_t ack_crc);
|
||||
Packet *createMultiAck(uint32_t ack_crc, uint8_t remaining);
|
||||
Packet *createControlData(const uint8_t *data, size_t len);
|
||||
Packet *createDatagram(uint8_t type, const Identity &dest, const uint8_t *secret, const uint8_t *data, size_t len);
|
||||
Packet *createAnonDatagram(uint8_t type, const LocalIdentity &sender, const Identity &dest, const uint8_t *secret, const uint8_t *data, size_t data_len);
|
||||
Packet *createGroupDatagram(uint8_t type, const GroupChannel &channel, const uint8_t *data, size_t data_len);
|
||||
Packet *createPathReturn(const Identity &dest, const uint8_t *secret, const uint8_t *path, uint8_t path_len, uint8_t extra_type, const uint8_t *extra, size_t extra_len);
|
||||
Packet *createPathReturn(const uint8_t *dest_hash, const uint8_t *secret, const uint8_t *path, uint8_t path_len, uint8_t extra_type, const uint8_t *extra, size_t extra_len);
|
||||
Packet *createRawData(const uint8_t *data, size_t len);
|
||||
Packet *createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags = 0);
|
||||
|
||||
void sendFlood(Packet *packet, uint32_t delay_millis = 0, uint8_t path_hash_size = 1);
|
||||
void sendFlood(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis = 0, uint8_t path_hash_size = 1);
|
||||
void sendDirect(Packet *packet, const uint8_t *path, uint8_t path_len, uint32_t delay_millis = 0);
|
||||
void sendZeroHop(Packet *packet, uint32_t delay_millis = 0);
|
||||
void sendZeroHop(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis = 0);
|
||||
};
|
||||
|
||||
} /* namespace mesh */
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* ZephCore Mesh - routing protocol layer
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <mesh/Dispatcher.h>
|
||||
#include <mesh/ContentionTracker.h>
|
||||
#ifdef CONFIG_ZEPHCORE_APC
|
||||
#include <mesh/PowerController.h>
|
||||
#endif
|
||||
#include <mesh/RTC.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
struct GroupChannel {
|
||||
uint8_t hash[PATH_HASH_SIZE];
|
||||
uint8_t secret[PUB_KEY_SIZE];
|
||||
};
|
||||
|
||||
class MeshTables {
|
||||
public:
|
||||
virtual bool hasSeen(const Packet *packet) = 0;
|
||||
virtual void clear(const Packet *packet) = 0;
|
||||
};
|
||||
|
||||
class Mesh : public Dispatcher {
|
||||
RNG *_rng;
|
||||
RTCClock *_rtc;
|
||||
MeshTables *_tables;
|
||||
|
||||
void removeSelfFromPath(Packet *packet);
|
||||
void routeDirectRecvAcks(Packet *packet, uint32_t delay_millis);
|
||||
DispatcherAction forwardMultipartDirect(Packet *pkt);
|
||||
|
||||
protected:
|
||||
ContentionTracker _contention;
|
||||
ContentionTracker& getContentionTracker() { return _contention; }
|
||||
const ContentionTracker& getContentionTracker() const { return _contention; }
|
||||
#ifdef CONFIG_ZEPHCORE_APC
|
||||
PowerController _power_ctrl;
|
||||
PowerController& getPowerController() { return _power_ctrl; }
|
||||
const PowerController& getPowerController() const { return _power_ctrl; }
|
||||
#endif
|
||||
void extendPendingRetransmit(uint32_t hash32);
|
||||
|
||||
DispatcherAction onRecvPacket(Packet *pkt) override;
|
||||
virtual uint32_t getCADFailRetryDelay() const override;
|
||||
virtual DispatcherAction routeRecvPacket(Packet *packet);
|
||||
virtual bool filterRecvFloodPacket(Packet *packet) { return false; }
|
||||
virtual bool allowPacketForward(const Packet *packet);
|
||||
virtual uint32_t getRetransmitDelay(const Packet *packet);
|
||||
virtual uint32_t getDirectRetransmitDelay(const Packet *packet) { return 0; }
|
||||
/* Passive contention tracking: if true, track heard floods we don't forward
|
||||
* (warms the contention EMA on nodes that don't relay, e.g. companions). */
|
||||
virtual bool passivelyTrackFloods() const { return false; }
|
||||
/* Added to caller-supplied delay on every sendFlood. Default 0 (repeater
|
||||
* behavior). Companion overrides to spread its initial TX adaptively. */
|
||||
virtual uint32_t getInitialFloodJitter(const Packet *packet) { (void)packet; return 0; }
|
||||
virtual uint8_t getExtraAckTransmitCount() const { return 0; }
|
||||
virtual int searchPeersByHash(const uint8_t *hash) { (void)hash; return 0; }
|
||||
virtual void getPeerSharedSecret(uint8_t *dest_secret, int peer_idx) { (void)dest_secret; (void)peer_idx; }
|
||||
virtual void onPeerDataRecv(Packet *packet, uint8_t type, int sender_idx, const uint8_t *secret, uint8_t *data, size_t len) { (void)packet; (void)type; (void)sender_idx; (void)secret; (void)data; (void)len; }
|
||||
virtual void onTraceRecv(Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) { (void)packet; (void)tag; (void)auth_code; (void)flags; (void)path_snrs; (void)path_hashes; (void)path_len; }
|
||||
virtual bool onPeerPathRecv(Packet *packet, int sender_idx, const uint8_t *secret, uint8_t *path, uint8_t path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len) { (void)packet; (void)sender_idx; (void)secret; (void)path; (void)path_len; (void)extra_type; (void)extra; (void)extra_len; return false; }
|
||||
virtual void onAdvertRecv(Packet *packet, const Identity &id, uint32_t timestamp, const uint8_t *app_data, size_t app_data_len) { (void)packet; (void)id; (void)timestamp; (void)app_data; (void)app_data_len; }
|
||||
virtual void onAnonDataRecv(Packet *packet, const uint8_t *secret, const Identity &sender, uint8_t *data, size_t len) { (void)packet; (void)secret; (void)sender; (void)data; (void)len; }
|
||||
virtual void onPathRecv(Packet *packet, Identity &sender, uint8_t *path, uint8_t path_len, uint8_t extra_type, uint8_t *extra, uint8_t extra_len) { (void)packet; (void)sender; (void)path; (void)path_len; (void)extra_type; (void)extra; (void)extra_len; }
|
||||
virtual void onControlDataRecv(Packet *packet) { (void)packet; }
|
||||
virtual void onRawDataRecv(Packet *packet) { (void)packet; }
|
||||
virtual int searchChannelsByHash(const uint8_t *hash, GroupChannel channels[], int max_matches) { (void)hash; (void)channels; (void)max_matches; return 0; }
|
||||
virtual void onGroupDataRecv(Packet *packet, uint8_t type, const GroupChannel &channel, uint8_t *data, size_t len) { (void)packet; (void)type; (void)channel; (void)data; (void)len; }
|
||||
virtual void onAckRecv(Packet *packet, uint32_t ack_crc) { (void)packet; (void)ack_crc; }
|
||||
|
||||
public:
|
||||
Mesh(Radio &radio, MillisecondClock &ms, RNG &rng, RTCClock &rtc, PacketManager &mgr, MeshTables &tables);
|
||||
void begin();
|
||||
void loop();
|
||||
void maintenanceLoop();
|
||||
|
||||
LocalIdentity self_id;
|
||||
|
||||
RNG *getRNG() const { return _rng; }
|
||||
RTCClock *getRTCClock() const { return _rtc; }
|
||||
MeshTables *getTables() const { return _tables; }
|
||||
|
||||
Packet *createAdvert(const LocalIdentity &id, const uint8_t *app_data = nullptr, size_t app_data_len = 0);
|
||||
Packet *createAck(uint32_t ack_crc);
|
||||
Packet *createMultiAck(uint32_t ack_crc, uint8_t remaining);
|
||||
Packet *createControlData(const uint8_t *data, size_t len);
|
||||
Packet *createDatagram(uint8_t type, const Identity &dest, const uint8_t *secret, const uint8_t *data, size_t len);
|
||||
Packet *createAnonDatagram(uint8_t type, const LocalIdentity &sender, const Identity &dest, const uint8_t *secret, const uint8_t *data, size_t data_len);
|
||||
Packet *createGroupDatagram(uint8_t type, const GroupChannel &channel, const uint8_t *data, size_t data_len);
|
||||
Packet *createPathReturn(const Identity &dest, const uint8_t *secret, const uint8_t *path, uint8_t path_len, uint8_t extra_type, const uint8_t *extra, size_t extra_len);
|
||||
Packet *createPathReturn(const uint8_t *dest_hash, const uint8_t *secret, const uint8_t *path, uint8_t path_len, uint8_t extra_type, const uint8_t *extra, size_t extra_len);
|
||||
Packet *createRawData(const uint8_t *data, size_t len);
|
||||
Packet *createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags = 0);
|
||||
|
||||
void sendFlood(Packet *packet, uint32_t delay_millis = 0, uint8_t path_hash_size = 1);
|
||||
void sendFlood(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis = 0, uint8_t path_hash_size = 1);
|
||||
void sendDirect(Packet *packet, const uint8_t *path, uint8_t path_len, uint32_t delay_millis = 0);
|
||||
void sendZeroHop(Packet *packet, uint32_t delay_millis = 0);
|
||||
void sendZeroHop(Packet *packet, uint16_t *transport_codes, uint32_t delay_millis = 0);
|
||||
};
|
||||
|
||||
} /* namespace mesh */
|
||||
|
||||
+164
-161
@@ -1,161 +1,164 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Adaptive Contention Window — dupe-counting based delay estimation
|
||||
*/
|
||||
|
||||
#include <mesh/ContentionTracker.h>
|
||||
#include <mesh/Packet.h>
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
ContentionTracker::ContentionTracker()
|
||||
: _next_idx(0), _ema_x256(0), _finalized_count(0),
|
||||
_last_retransmit_ms(0), _backoff_multiplier(DEFAULT_BACKOFF_MULT)
|
||||
{
|
||||
memset(_ring, 0, sizeof(_ring));
|
||||
}
|
||||
|
||||
/* FNV-1a over payload_type + first 8 payload bytes */
|
||||
uint32_t ContentionTracker::computePacketHash32(const Packet *pkt)
|
||||
{
|
||||
uint32_t h = 0x811c9dc5u; /* FNV-1a offset basis */
|
||||
uint8_t t = pkt->getPayloadType();
|
||||
h = (h ^ t) * 0x01000193u;
|
||||
int n = pkt->payload_len < 8 ? pkt->payload_len : 8;
|
||||
for (int i = 0; i < n; i++) {
|
||||
h = (h ^ pkt->payload[i]) * 0x01000193u;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
int ContentionTracker::findEntry(uint32_t hash32) const
|
||||
{
|
||||
for (int i = 0; i < RING_SIZE; i++) {
|
||||
if (_ring[i].active && _ring[i].hash32 == hash32) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void ContentionTracker::finalizeEntry(int idx)
|
||||
{
|
||||
if (!_ring[idx].active) return;
|
||||
|
||||
uint32_t sample_x256 = (uint32_t)_ring[idx].dupe_count << 8;
|
||||
|
||||
int32_t diff = (int32_t)sample_x256 - (int32_t)_ema_x256;
|
||||
|
||||
if (_finalized_count < WARMUP_PACKETS) {
|
||||
/* Warmup: seed EMA with fast convergence */
|
||||
if (_finalized_count == 0) {
|
||||
_ema_x256 = sample_x256;
|
||||
} else {
|
||||
_ema_x256 = (uint32_t)((int32_t)_ema_x256 + (diff >> 1));
|
||||
}
|
||||
} else {
|
||||
_ema_x256 = (uint32_t)((int32_t)_ema_x256 + (diff >> EMA_SHIFT));
|
||||
}
|
||||
|
||||
_finalized_count++;
|
||||
_ring[idx].active = false;
|
||||
}
|
||||
|
||||
void ContentionTracker::trackRetransmit(uint32_t hash32, uint32_t now_ms)
|
||||
{
|
||||
_last_retransmit_ms = now_ms;
|
||||
|
||||
/* Evict oldest if ring slot occupied */
|
||||
if (_ring[_next_idx].active) {
|
||||
finalizeEntry(_next_idx);
|
||||
}
|
||||
|
||||
Entry &e = _ring[_next_idx];
|
||||
e.hash32 = hash32;
|
||||
e.first_seen_ms = now_ms;
|
||||
e.dupe_count = 0;
|
||||
e.reactive_added_ms = 0;
|
||||
e.active = true;
|
||||
|
||||
_next_idx = (_next_idx + 1) % RING_SIZE;
|
||||
}
|
||||
|
||||
bool ContentionTracker::recordDupeIfTracked(uint32_t hash32, uint32_t now_ms)
|
||||
{
|
||||
int idx = findEntry(hash32);
|
||||
if (idx < 0) return false;
|
||||
|
||||
Entry &e = _ring[idx];
|
||||
|
||||
if (now_ms - e.first_seen_ms > WINDOW_MS) {
|
||||
finalizeEntry(idx);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.dupe_count < 255) {
|
||||
e.dupe_count++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t ContentionTracker::getReactiveHeadroom(uint32_t hash32, uint32_t airtime_ms) const
|
||||
{
|
||||
int idx = findEntry(hash32);
|
||||
if (idx < 0) return 0;
|
||||
|
||||
uint32_t per_dupe = (uint32_t)(_backoff_multiplier * (float)airtime_ms);
|
||||
if (per_dupe == 0) return 0;
|
||||
|
||||
/* Hard cap: REACTIVE_HARD_CAP_MS total extension per packet */
|
||||
if (_ring[idx].reactive_added_ms >= REACTIVE_HARD_CAP_MS) return 0;
|
||||
|
||||
uint32_t remaining = REACTIVE_HARD_CAP_MS - _ring[idx].reactive_added_ms;
|
||||
if (per_dupe > remaining) per_dupe = remaining;
|
||||
return per_dupe > 0xFFFF ? 0xFFFF : (uint16_t)per_dupe;
|
||||
}
|
||||
|
||||
void ContentionTracker::addReactiveExtension(uint32_t hash32, uint16_t added_ms)
|
||||
{
|
||||
int idx = findEntry(hash32);
|
||||
if (idx < 0) return;
|
||||
|
||||
uint32_t total = (uint32_t)_ring[idx].reactive_added_ms + added_ms;
|
||||
_ring[idx].reactive_added_ms = total > 0xFFFF ? 0xFFFF : (uint16_t)total;
|
||||
}
|
||||
|
||||
void ContentionTracker::tick(uint32_t now_ms)
|
||||
{
|
||||
for (int i = 0; i < RING_SIZE; i++) {
|
||||
if (_ring[i].active && now_ms - _ring[i].first_seen_ms > WINDOW_MS) {
|
||||
finalizeEntry(i);
|
||||
}
|
||||
}
|
||||
|
||||
/* Decay EMA toward 0 if no retransmit in STALE_MS */
|
||||
if (_last_retransmit_ms != 0 && now_ms - _last_retransmit_ms > STALE_MS) {
|
||||
if (_ema_x256 > 0) {
|
||||
_ema_x256 -= _ema_x256 >> EMA_SHIFT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float ContentionTracker::getContentionEstimate() const
|
||||
{
|
||||
return (float)_ema_x256 / 256.0f;
|
||||
}
|
||||
|
||||
float ContentionTracker::getFloodDelayFactor() const
|
||||
{
|
||||
if (!isWarmedUp()) return 0.5f; /* conservative default before warmup */
|
||||
|
||||
float est = getContentionEstimate();
|
||||
if (est <= 0.0f) return MIN_FLOOD_FACTOR;
|
||||
|
||||
float factor = MIN_FLOOD_FACTOR + FLOOD_SCALE * sqrtf(est);
|
||||
if (factor > MAX_FLOOD_FACTOR) factor = MAX_FLOOD_FACTOR;
|
||||
return factor;
|
||||
}
|
||||
|
||||
} /* namespace mesh */
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* Adaptive Contention Window — dupe-counting based delay estimation
|
||||
*/
|
||||
|
||||
#include <mesh/ContentionTracker.h>
|
||||
#include <mesh/Packet.h>
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
|
||||
ContentionTracker::ContentionTracker()
|
||||
: _next_idx(0), _ema_x256(0), _finalized_count(0),
|
||||
_last_retransmit_ms(0), _backoff_multiplier(DEFAULT_BACKOFF_MULT)
|
||||
{
|
||||
memset(_ring, 0, sizeof(_ring));
|
||||
}
|
||||
|
||||
/* FNV-1a over payload_type + first 8 payload bytes */
|
||||
uint32_t ContentionTracker::computePacketHash32(const Packet *pkt)
|
||||
{
|
||||
uint32_t h = 0x811c9dc5u; /* FNV-1a offset basis */
|
||||
uint8_t t = pkt->getPayloadType();
|
||||
h = (h ^ t) * 0x01000193u;
|
||||
int n = pkt->payload_len < 8 ? pkt->payload_len : 8;
|
||||
for (int i = 0; i < n; i++) {
|
||||
h = (h ^ pkt->payload[i]) * 0x01000193u;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
int ContentionTracker::findEntry(uint32_t hash32) const
|
||||
{
|
||||
for (int i = 0; i < RING_SIZE; i++) {
|
||||
if (_ring[i].active && _ring[i].hash32 == hash32) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void ContentionTracker::finalizeEntry(int idx)
|
||||
{
|
||||
if (!_ring[idx].active) return;
|
||||
|
||||
uint32_t sample_x256 = (uint32_t)_ring[idx].dupe_count << 8;
|
||||
|
||||
int32_t diff = (int32_t)sample_x256 - (int32_t)_ema_x256;
|
||||
|
||||
if (_finalized_count < WARMUP_PACKETS) {
|
||||
/* Warmup: seed EMA with fast convergence */
|
||||
if (_finalized_count == 0) {
|
||||
_ema_x256 = sample_x256;
|
||||
} else {
|
||||
_ema_x256 = (uint32_t)((int32_t)_ema_x256 + (diff >> 1));
|
||||
}
|
||||
} else {
|
||||
_ema_x256 = (uint32_t)((int32_t)_ema_x256 + (diff >> EMA_SHIFT));
|
||||
}
|
||||
|
||||
_finalized_count++;
|
||||
_ring[idx].active = false;
|
||||
}
|
||||
|
||||
void ContentionTracker::trackRetransmit(uint32_t hash32, uint32_t now_ms)
|
||||
{
|
||||
_last_retransmit_ms = now_ms;
|
||||
|
||||
/* Evict oldest if ring slot occupied */
|
||||
if (_ring[_next_idx].active) {
|
||||
finalizeEntry(_next_idx);
|
||||
}
|
||||
|
||||
Entry &e = _ring[_next_idx];
|
||||
e.hash32 = hash32;
|
||||
e.first_seen_ms = now_ms;
|
||||
e.dupe_count = 0;
|
||||
e.reactive_added_ms = 0;
|
||||
e.active = true;
|
||||
|
||||
_next_idx = (_next_idx + 1) % RING_SIZE;
|
||||
}
|
||||
|
||||
bool ContentionTracker::recordDupeIfTracked(uint32_t hash32, uint32_t now_ms)
|
||||
{
|
||||
int idx = findEntry(hash32);
|
||||
if (idx < 0) return false;
|
||||
|
||||
Entry &e = _ring[idx];
|
||||
|
||||
if (now_ms - e.first_seen_ms > WINDOW_MS) {
|
||||
finalizeEntry(idx);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (e.dupe_count < 255) {
|
||||
e.dupe_count++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint16_t ContentionTracker::getReactiveHeadroom(uint32_t hash32, uint32_t airtime_ms) const
|
||||
{
|
||||
int idx = findEntry(hash32);
|
||||
if (idx < 0) return 0;
|
||||
|
||||
uint32_t per_dupe = (uint32_t)(_backoff_multiplier * (float)airtime_ms);
|
||||
if (per_dupe == 0) return 0;
|
||||
|
||||
/* Effective cap: ~12 relay-slots (airtime-scaled), absolute ceiling REACTIVE_HARD_CAP_MS */
|
||||
uint32_t effective_cap = 12 * airtime_ms;
|
||||
if (effective_cap > REACTIVE_HARD_CAP_MS) effective_cap = REACTIVE_HARD_CAP_MS;
|
||||
|
||||
if (_ring[idx].reactive_added_ms >= effective_cap) return 0;
|
||||
|
||||
uint32_t remaining = effective_cap - _ring[idx].reactive_added_ms;
|
||||
if (per_dupe > remaining) per_dupe = remaining;
|
||||
return per_dupe > 0xFFFF ? 0xFFFF : (uint16_t)per_dupe;
|
||||
}
|
||||
|
||||
void ContentionTracker::addReactiveExtension(uint32_t hash32, uint16_t added_ms)
|
||||
{
|
||||
int idx = findEntry(hash32);
|
||||
if (idx < 0) return;
|
||||
|
||||
uint32_t total = (uint32_t)_ring[idx].reactive_added_ms + added_ms;
|
||||
_ring[idx].reactive_added_ms = total > 0xFFFF ? 0xFFFF : (uint16_t)total;
|
||||
}
|
||||
|
||||
void ContentionTracker::tick(uint32_t now_ms)
|
||||
{
|
||||
for (int i = 0; i < RING_SIZE; i++) {
|
||||
if (_ring[i].active && now_ms - _ring[i].first_seen_ms > WINDOW_MS) {
|
||||
finalizeEntry(i);
|
||||
}
|
||||
}
|
||||
|
||||
/* Decay EMA toward 0 if no retransmit in STALE_MS */
|
||||
if (_last_retransmit_ms != 0 && now_ms - _last_retransmit_ms > STALE_MS) {
|
||||
if (_ema_x256 > 0) {
|
||||
_ema_x256 -= _ema_x256 >> EMA_SHIFT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float ContentionTracker::getContentionEstimate() const
|
||||
{
|
||||
return (float)_ema_x256 / 256.0f;
|
||||
}
|
||||
|
||||
float ContentionTracker::getFloodDelayFactor() const
|
||||
{
|
||||
if (!isWarmedUp()) return 0.5f; /* conservative default before warmup */
|
||||
|
||||
float est = getContentionEstimate();
|
||||
if (est <= 0.0f) return MIN_FLOOD_FACTOR;
|
||||
|
||||
float factor = MIN_FLOOD_FACTOR + FLOOD_SCALE * sqrtf(est);
|
||||
if (factor > MAX_FLOOD_FACTOR) factor = MAX_FLOOD_FACTOR;
|
||||
return factor;
|
||||
}
|
||||
|
||||
} /* namespace mesh */
|
||||
|
||||
+723
-719
File diff suppressed because it is too large
Load Diff
+17
-17
@@ -1,17 +1,17 @@
|
||||
manifest:
|
||||
remotes:
|
||||
- name: zephyrproject-rtos
|
||||
url-base: https://github.com/zephyrproject-rtos
|
||||
|
||||
projects:
|
||||
- name: hal_espressif
|
||||
remote: zephyrproject-rtos
|
||||
revision: b7953b8019361d09e613f7011d2ccc41b984d087
|
||||
path: modules/hal/espressif
|
||||
- name: zephyr
|
||||
remote: zephyrproject-rtos
|
||||
revision: 684c9e8f32e4373a21098559f748f06915f950c9
|
||||
import: true
|
||||
|
||||
self:
|
||||
path: zephcore
|
||||
manifest:
|
||||
remotes:
|
||||
- name: zephyrproject-rtos
|
||||
url-base: https://github.com/zephyrproject-rtos
|
||||
|
||||
projects:
|
||||
- name: hal_espressif
|
||||
remote: zephyrproject-rtos
|
||||
revision: b7953b8019361d09e613f7011d2ccc41b984d087
|
||||
path: modules/hal/espressif
|
||||
- name: zephyr
|
||||
remote: zephyrproject-rtos
|
||||
revision: 684c9e8f32e4373a21098559f748f06915f950c9
|
||||
import: true
|
||||
|
||||
self:
|
||||
path: zephcore
|
||||
|
||||
Reference in New Issue
Block a user