diff --git a/config.yaml.example b/config.yaml.example index 79b8333..e2cde6c 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -483,10 +483,12 @@ sx1262: is_waveshare: false delays: - # TX delay factor for flood mode (multiplier) + # Flood TX delay: airtime multiplier, NOT seconds. The actual pre-transmit + # delay is a random value in [0, 5 * packet_airtime * factor] seconds. tx_delay_factor: 1.0 - # TX delay factor for direct mode (faster) + # Direct TX delay: same random 0-5*airtime*factor window, usually lower so + # directed traffic clears faster. direct_tx_delay_factor: 0.5 duty_cycle: diff --git a/repeater/engine.py b/repeater/engine.py index eebfd3e..74195f9 100644 --- a/repeater/engine.py +++ b/repeater/engine.py @@ -77,6 +77,14 @@ class RepeaterHandler(BaseHandler): self.max_duplicates_per_packet = 20 self.tx_delay_factor = config.get("delays", {}).get("tx_delay_factor", 1.0) self.direct_tx_delay_factor = config.get("delays", {}).get("direct_tx_delay_factor", 0.5) + # These are airtime multipliers, not seconds: TX delay is a random value in + # [0, 5 * airtime * factor] (matching MeshCore getRetransmitDelay). Flood and + # direct share the formula and differ only in the factor. + logger.info( + "TX delay factors (airtime multipliers, not seconds): " + f"flood random 0-5*airtime*F (F={self.tx_delay_factor}), " + f"direct random 0-5*airtime*F (F={self.direct_tx_delay_factor})" + ) self.use_score_for_tx = config.get("repeater", {}).get("use_score_for_tx", False) self.score_threshold = config.get("repeater", {}).get("score_threshold", 0.3) self.max_flood_hops = config.get("repeater", {}).get("max_flood_hops", 64) diff --git a/repeater/web/api_endpoints.py b/repeater/web/api_endpoints.py index 5663c9d..77b2673 100644 --- a/repeater/web/api_endpoints.py +++ b/repeater/web/api_endpoints.py @@ -3868,8 +3868,8 @@ class APIEndpoints: "bandwidth": 62500, # Bandwidth in Hz (valid: 7.8, 10.4, 15.6, 20.8, 31.25, 41.7, 62.5, 125, 250, 500 kHz) "spreading_factor": 8, # Spreading factor (5-12) "coding_rate": 8, # Coding rate (5-8 for 4/5 to 4/8) - "tx_delay_factor": 1.0, # TX delay factor (0.0-5.0) - "direct_tx_delay_factor": 0.5, # Direct TX delay (0.0-5.0) + "tx_delay_factor": 1.0, # Flood TX delay airtime multiplier (0.0-5.0) + "direct_tx_delay_factor": 0.5, # Direct TX delay airtime multiplier (0.0-5.0) "rx_delay_base": 0.0, # RX delay base (>= 0) "node_name": "MyNode", # Node name "owner_info": "Owner text", # Owner info text diff --git a/tests/test_engine.py b/tests/test_engine.py index 84f0ac0..b38fee4 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -8,6 +8,7 @@ airtime duty-cycle, TX mode (forward/monitor/no_tx), and config reloading. import asyncio import base64 +import logging import time from unittest.mock import AsyncMock, MagicMock, patch @@ -849,6 +850,14 @@ class TestTxDelay: delay = handler._calculate_tx_delay(pkt, snr=0.0) assert delay == 0.0 + def test_startup_log_states_delay_factor_semantics(self, caplog): + with caplog.at_level(logging.INFO, logger="RepeaterHandler"): + _make_handler_with_hash(bytes([LOCAL_HASH])) + assert any( + "TX delay factors" in record.message and "airtime multipliers" in record.message + for record in caplog.records + ) + def test_transport_direct_uses_random_window(self, handler): handler.direct_tx_delay_factor = 0.77 pkt = _make_transport_direct_packet()