From a0eb0fc38c57bf1f06545190f08db19079c3f809 Mon Sep 17 00:00:00 2001 From: Rightup Date: Mon, 13 Jul 2026 22:39:11 +0100 Subject: [PATCH] feat: TX delay update calculation with route-aware random window and update tests --- repeater/engine.py | 28 ++++++++++------------ tests/test_engine.py | 55 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 56 insertions(+), 27 deletions(-) diff --git a/repeater/engine.py b/repeater/engine.py index 80280af..4bb3578 100644 --- a/repeater/engine.py +++ b/repeater/engine.py @@ -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" ) diff --git a/tests/test_engine.py b/tests/test_engine.py index 713f707..3b93cc9 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -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) # ===================================================================