diff --git a/repeater/airtime.py b/repeater/airtime.py index 69ee3f3..b9e8141 100644 --- a/repeater/airtime.py +++ b/repeater/airtime.py @@ -3,6 +3,8 @@ import math import time from typing import Tuple +from openhop_core.protocol.packet_utils import coding_rate_denominator + logger = logging.getLogger("AirtimeManager") @@ -55,7 +57,9 @@ class AirtimeManager: """ sf = spreading_factor or self.spreading_factor bw_hz = bandwidth_hz or self.bandwidth - cr = coding_rate or self.coding_rate + # Configs and radio drivers use either the denominator form (5..8) or + # the legacy index form (1..4); normalize before the Semtech formula. + cr = coding_rate_denominator(coding_rate or self.coding_rate) preamble_len = preamble_len or self.preamble_length crc = 1 if crc_enabled else 0 h = 0 if explicit_header else 1 # H=0 for explicit, H=1 for implicit diff --git a/tests/test_airtime.py b/tests/test_airtime.py index cf61537..8f005bf 100644 --- a/tests/test_airtime.py +++ b/tests/test_airtime.py @@ -97,6 +97,21 @@ def test_long_range_preset_has_higher_airtime_than_fast_preset_for_same_payload( assert long_mgr.calculate_airtime(payload_len) > fast_mgr.calculate_airtime(payload_len) +def test_legacy_coding_rate_index_matches_denominator_form(): + """A config using the legacy index form (1..4) must compute the same + airtime as the equivalent denominator form (5..8), not undercount + payload symbols by multiplying with the raw index.""" + for index, denom in ((1, 5), (2, 6), (3, 7), (4, 8)): + by_index = _make_mgr(sf=10, bw_hz=250000, cr=index, preamble=8) + by_denom = _make_mgr(sf=10, bw_hz=250000, cr=denom, preamble=8) + for payload_len in (16, 64, 128): + assert math.isclose( + by_index.calculate_airtime(payload_len), + by_denom.calculate_airtime(payload_len), + rel_tol=1e-9, + ) + + @pytest.mark.parametrize("_title,sf,bw_hz,cr", ALL_PRESETS, ids=ALL_PRESET_IDS) def test_can_transmit_blocks_after_budget_exhausted_for_each_preset(_title, sf, bw_hz, cr): mgr = _make_mgr(sf, bw_hz, cr, preamble=8, max_airtime_per_minute=600)