From 697056aa93ca36a925fcc6fb4de6896eb90f3794 Mon Sep 17 00:00:00 2001 From: agessaman Date: Tue, 14 Jul 2026 23:57:17 -0700 Subject: [PATCH] fix(airtime): normalize legacy coding-rate indices AirtimeManager fed the configured coding rate straight into the Semtech symbol formula, so a config using the legacy index form (1..4) would multiply payload symbols by the raw index instead of the 4/5..4/8 denominator and undercount airtime. Normalize through openhop_core's shared coding_rate_denominator() before calculating. --- repeater/airtime.py | 6 +++++- tests/test_airtime.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) 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)