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:
liquidraver
2026-04-17 10:24:12 +02:00
parent cba81af196
commit ba4b86752d
17 changed files with 1909 additions and 1864 deletions
+699 -691
View File
File diff suppressed because it is too large Load Diff
+22 -4
View File
@@ -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;
+7
View File
@@ -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;
+8 -4
View File
@@ -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
+9
View File
@@ -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
+22 -22
View File
@@ -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.
+5 -6
View File
@@ -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).
+80 -80
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -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