feat: TX delay update calculation with route-aware random window and update tests

This commit is contained in:
Rightup
2026-07-13 22:39:11 +01:00
parent e2172c2b73
commit a0eb0fc38c
2 changed files with 56 additions and 27 deletions
+12 -16
View File
@@ -1067,22 +1067,18 @@ class RepeaterHandler(BaseHandler):
packet_len = packet.get_raw_length()
airtime_ms = self.airtime_mgr.calculate_airtime(packet_len)
route_type = packet.header & PH_ROUTE_MASK
if packet.is_route_flood():
delay_factor = self.tx_delay_factor
elif packet.is_route_direct():
delay_factor = self.direct_tx_delay_factor
else:
delay_factor = self.direct_tx_delay_factor
# Base delay calculations
# this part took me along time to get right well i hope i got it right ;-)
if route_type == ROUTE_TYPE_FLOOD:
# Flood packets: random(0-5) * (airtime * 52/50 / 2) * tx_delay_factor
# This creates collision avoidance with tunable delay
base_delay_ms = (airtime_ms * 52 / 50) / 2.0 # From C++ implementation
random_mult = secrets.randbelow(5001) / 1000.0
delay_ms = base_delay_ms * random_mult * self.tx_delay_factor
delay_s = delay_ms / 1000.0
else: # DIRECT
# Direct packets: use direct_tx_delay_factor (already in seconds)
# direct_tx_delay_factor is stored as seconds in config
delay_s = self.direct_tx_delay_factor
# MeshCore uses the same random window for flood and direct variants:
# choose a value from 0 through 5 * airtime * factor.
base_delay_ms = airtime_ms * delay_factor
random_mult = secrets.randbelow(5001) / 1000.0
delay_s = (base_delay_ms * random_mult) / 1000.0
# Apply score-based delay adjustment ONLY if delay >= 50ms threshold
# (matching C++ reactive behavior in Dispatcher::calcRxDelay)
@@ -1104,7 +1100,7 @@ class RepeaterHandler(BaseHandler):
if logger.isEnabledFor(logging.DEBUG):
logger.debug(
f"Route={'FLOOD' if route_type == ROUTE_TYPE_FLOOD else 'DIRECT'}, "
f"Route={'FLOOD' if packet.is_route_flood() else 'DIRECT'}, "
f"len={packet_len}B, airtime={airtime_ms:.1f}ms, delay={delay_s:.3f}s"
)
+44 -11
View File
@@ -584,7 +584,7 @@ class TestPacketScore:
class TestTxDelay:
"""TX delay: flood random, direct fixed, score adjustment, cap."""
"""TX delay: route-aware random window, score adjustment, cap."""
def test_flood_delay_non_negative(self, handler):
pkt = _make_flood_packet()
@@ -597,13 +597,42 @@ class TestTxDelay:
delay = handler._calculate_tx_delay(pkt, snr=0.0)
assert delay <= 5.0
def test_direct_delay_uses_factor(self, handler):
handler.direct_tx_delay_factor = 1.23
pkt = _make_direct_packet()
delay = handler._calculate_tx_delay(pkt, snr=0.0)
# Direct packets use direct_tx_delay_factor directly (in seconds)
# Score adjustment may change it, but base should be 1.23 when score is off
assert delay == pytest.approx(1.23, abs=0.01)
@pytest.mark.parametrize(
"builder, factor_attr",
[
(_make_flood_packet, "tx_delay_factor"),
(_make_transport_flood_packet, "tx_delay_factor"),
(_make_direct_packet, "direct_tx_delay_factor"),
(_make_transport_direct_packet, "direct_tx_delay_factor"),
],
)
def test_route_variants_use_random_window(self, handler, builder, factor_attr):
setattr(handler, factor_attr, 1.23)
pkt = builder()
expected = handler.airtime_mgr.calculate_airtime(pkt.get_raw_length()) * 1.23 * 5 / 1000.0
with patch("repeater.engine.secrets.randbelow", return_value=5000):
delay = handler._calculate_tx_delay(pkt, snr=0.0)
assert delay == pytest.approx(expected, rel=0.001, abs=0.001)
@pytest.mark.parametrize(
"builder, factor_attr",
[
(_make_flood_packet, "tx_delay_factor"),
(_make_transport_flood_packet, "tx_delay_factor"),
(_make_direct_packet, "direct_tx_delay_factor"),
(_make_transport_direct_packet, "direct_tx_delay_factor"),
],
)
def test_route_variants_can_delay_to_zero(self, handler, builder, factor_attr):
setattr(handler, factor_attr, 1.23)
pkt = builder()
with patch("repeater.engine.secrets.randbelow", return_value=0):
delay = handler._calculate_tx_delay(pkt, snr=0.0)
assert delay == 0.0
def test_score_adjustment_reduces_delay(self, handler):
handler.use_score_for_tx = True
@@ -634,11 +663,15 @@ class TestTxDelay:
delay = handler._calculate_tx_delay(pkt, snr=0.0)
assert delay == 0.0
def test_transport_direct_uses_direct_delay(self, handler):
def test_transport_direct_uses_random_window(self, handler):
handler.direct_tx_delay_factor = 0.77
pkt = _make_transport_direct_packet()
delay = handler._calculate_tx_delay(pkt, snr=0.0)
assert delay == pytest.approx(0.77, abs=0.01)
expected = handler.airtime_mgr.calculate_airtime(pkt.get_raw_length()) * 0.77 * 5 / 1000.0
with patch("repeater.engine.secrets.randbelow", return_value=5000):
delay = handler._calculate_tx_delay(pkt, snr=0.0)
assert delay == pytest.approx(expected, rel=0.001, abs=0.001)
# ===================================================================