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.
This commit is contained in:
agessaman
2026-07-14 23:57:17 -07:00
parent 74528af3ae
commit 697056aa93
2 changed files with 20 additions and 1 deletions
+5 -1
View File
@@ -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
+15
View File
@@ -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)