Correct chip-specific radio profile validation and airtime

Retain supported LR2021 bandwidths and reject frequency gaps. Enable LR1121 high-band bandwidth selection and account for its forced LDRO in profile and beacon airtime.

Cover all eight chip families with production-method regression tests.
This commit is contained in:
mikecarper committed 2026-10-19 18:49:15 -07:00
1 parent 25299a77b3
commit ca67494f8c
5 files changed
+140 -11

No files matched your search

+1
View File
@@ -618,6 +618,7 @@ jobs:
test_sx1262_modem_cache test_esp32_retune_priority test_tdeck_spi_clock_timing
test_radio_chirp_math
test_lr1110_profile_switch test_lr1110_packet_setter_cache
test_radio_profile_validation_backport
- name: Verify radio lab collectors and compiled experiment contracts
run: |
+3 -3
View File
@@ -147,13 +147,13 @@ class RadioProfiles {
return profile == 1 ? secondary.params : primary;
}
static bool valid(const RadioProfileParams& p) {
if (!isfinite(p.freq) || !isfinite(p.bw) || p.freq < 150 || p.freq > 2500
if (!isfinite(p.freq) || !isfinite(p.bw) || p.freq < 137 || p.freq > 2500
|| p.sf < 5 || p.sf > 12 || p.cr < 5 || p.cr > 8
|| (p.protocol != RadioProfileProtocol::MeshCore && p.protocol != RadioProfileProtocol::Meshtext
&& p.protocol != RadioProfileProtocol::Ethernet)
|| (p.preamble != 0 && (p.preamble < 8 || p.preamble > MaxPreamble))) return false;
static constexpr float bandwidths[] = {7.8f, 10.4f, 15.6f, 20.8f, 31.25f, 41.7f,
62.5f, 125, 250, 500, 1000, 812.5f, 1625};
static constexpr float bandwidths[] = {7.8f, 10.4f, 15.6f, 20.8f, 31.25f, 41.67f, 41.7f,
62.5f, 83.34f, 101.56f, 125, 203.125f, 250, 406.25f, 500, 812.5f, 1000, 1625};
for (float bw : bandwidths) if (fabsf(p.bw - bw) < 0.01f) return true;
return false;
}
+1 -1
View File
@@ -90,7 +90,7 @@ protected:
bool applyParams(float freq, float bw, uint8_t sf, uint8_t cr) override {
if (((CustomLR1121 *)_radio)->setFrequency(freq) != RADIOLIB_ERR_NONE
|| ((CustomLR1121 *)_radio)->setSpreadingFactor(sf) != RADIOLIB_ERR_NONE
|| ((CustomLR1121 *)_radio)->setBandwidth(bw) != RADIOLIB_ERR_NONE
|| ((CustomLR1121 *)_radio)->setBandwidth(bw, bw == 203.125f || bw == 406.25f || bw == 812.5f) != RADIOLIB_ERR_NONE
|| ((CustomLR1121 *)_radio)->setCodingRate(cr) != RADIOLIB_ERR_NONE
|| !updatePreamble(sf, bw)) return false;
PacketMillis pm = calcMaxPacketMillis(sf, bw, cr, preambleLengthForParams(sf, bw));
+43 -7
View File
@@ -465,22 +465,40 @@ bool RadioLibWrapper::validateProfile(const mesh::RadioProfileParams& p) const {
#else
if (p.protocol == mesh::RadioProfileProtocol::Ethernet) return false;
#endif
#if MESH_RADIO_PROFILE_CHIP != 1110 && MESH_RADIO_PROFILE_CHIP != 1121 && MESH_RADIO_PROFILE_CHIP != 2021 && MESH_RADIO_PROFILE_CHIP != 68 && MESH_RADIO_PROFILE_CHIP != 1272
static constexpr float bandwidths[] = {7.8f, 10.4f, 15.6f, 20.8f, 31.25f, 41.7f, 62.5f, 125, 250, 500};
bool supported = false;
for (float bw : bandwidths) if (fabsf(p.bw - bw) < 0.01f) supported = true;
if (!supported) return false;
#endif
#if MESH_RADIO_PROFILE_CHIP == 1276
return p.freq >= 137 && p.freq <= 1020 && p.sf >= 7 && p.bw <= 500;
#elif MESH_RADIO_PROFILE_CHIP == 1272
return p.freq >= 860 && p.freq <= 1020 && p.sf >= 7 && p.bw >= 125 && p.bw <= 500;
return p.freq >= 860 && p.freq <= 1020 && p.sf >= 7
&& (p.bw == 125 || p.bw == 250 || p.bw == 500);
#elif MESH_RADIO_PROFILE_CHIP == 68
return p.freq >= 150 && p.freq <= 960
&& ((p.bw == 125 && p.sf <= 9) || (p.bw == 250 && p.sf <= 10)
|| (p.bw == 500 && p.sf <= 11));
#elif MESH_RADIO_PROFILE_CHIP == 2021
return (p.freq <= 1090 || p.freq >= 1900) && p.bw >= 31.25 && p.bw <= 1000;
static constexpr float bandwidths[] = {31.25f, 41.67f, 41.7f, 62.5f, 83.34f, 101.56f,
125, 203.125f, 250, 406.25f, 500, 812.5f, 1000};
bool supported = false;
for (float bw : bandwidths) if (fabsf(p.bw - bw) < 0.01f) supported = true;
return ((p.freq >= 150 && p.freq <= 1090) || (p.freq >= 1900 && p.freq <= 2200)
|| (p.freq >= 2400 && p.freq <= 2500)) && supported;
#elif MESH_RADIO_PROFILE_CHIP == 1110
return p.freq <= 960 && p.bw >= 62.5 && p.bw <= 500;
return p.freq >= 150 && p.freq <= 960
&& (p.bw == 62.5 || p.bw == 125 || p.bw == 250 || p.bw == 500);
#elif MESH_RADIO_PROFILE_CHIP == 1121
return ((p.freq >= 150 && p.freq <= 960) || (p.freq >= 1900 && p.freq <= 2200)
|| (p.freq >= 2400 && p.freq <= 2500))
&& (p.bw == 62.5 || p.bw == 125 || p.bw == 250 || p.bw == 500
|| (p.freq >= 1900 && (p.bw == 203.125f || p.bw == 406.25f || p.bw == 812.5f)));
#elif MESH_RADIO_PROFILE_CHIP == 1268
return p.freq >= 410 && p.freq <= 810 && p.bw <= 500;
#else
return p.freq <= 960 && p.bw <= 500;
return p.freq >= 150 && p.freq <= 960 && p.bw <= 500;
#endif
}
@@ -613,7 +631,11 @@ uint32_t RadioLibWrapper::getBeaconAirtime(const mesh::RadioProfileParams& p, in
? 16 : (p.preamble ? p.preamble : 32);
if (!mesh::RadioProfiles::safePreamble(p, preamble)) return 0;
const double symbol = mesh::RadioProfiles::symbolUs(p);
const int de = symbol >= 16000 ? 1 : 0;
const int de = symbol >= 16000
#if MESH_RADIO_PROFILE_CHIP == 1121
|| (p.freq > 1000 && p.sf > 10)
#endif
? 1 : 0;
const double coded = ceil((8.0 * length - 4 * p.sf + (p.sf <= 6 ? 20 : 28) + 16)
/ (4 * (p.sf - 2 * de)));
const double payload = 8 + (coded > 0 ? coded * p.cr : 0);
@@ -935,17 +957,31 @@ bool RadioLibWrapper::getProfileIdleDelay(uint32_t& delay_ms) const {
}
uint32_t RadioLibWrapper::getProfileAirtime(uint8_t profile, int len, uint8_t cr) {
if (profile > 1 || len < 0 || len > MAX_TRANS_UNIT) return 0;
if (!_profiles.enabled() && profile == 0 && cr == 0) return getEstAirtimeFor(len);
const auto& p = _profiles.params(profile);
if (!validateProfile(p)) return 0;
const double symbol = mesh::RadioProfiles::symbolUs(p);
if (symbol <= 0 || len < 0) return 0;
if (cr < 5 || cr > 8) cr = p.cr;
const int de = symbol >= 16000 ? 1 : 0;
const int de = symbol >= 16000
#if MESH_RADIO_PROFILE_CHIP == 1121
// RadioLib 7.8.1 forces SX128x-compatible LDRO for high-band SF11/12.
|| (p.freq > 1000 && p.sf > 10)
#endif
? 1 : 0;
const double coded = ceil((8.0 * len - 4 * p.sf + (p.sf <= 6 ? 20 : 28) + 16)
/ (4 * (p.sf - 2 * de)));
const double payload_symbols = 8 + (coded > 0 ? coded * cr : 0);
const double suffix = p.sf <= 6 ? 6.25 : 4.25;
return (uint32_t)ceil((profilePreamble(profile) + suffix + payload_symbols) * symbol / 1000.0);
const double airtime = ceil((profilePreamble(profile) + suffix + payload_symbols) * symbol / 1000.0);
const uint32_t rounded = airtime > UINT32_MAX ? UINT32_MAX : uint32_t(airtime);
#if MESH_RADIO_PROFILE_CHIP == 1110 || MESH_RADIO_PROFILE_CHIP == 1121
// Preserve these wrappers' operational floor even for an inactive tuple.
return rounded < 200 ? 200 : rounded;
#else
return rounded;
#endif
}
bool RadioLibWrapper::setParams(float freq, float bw, uint8_t sf, uint8_t cr,
@@ -0,0 +1,92 @@
"""Exercise production profile validation and per-tuple airtime on each chip."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
from cpp_source import body
ROOT = Path(__file__).resolve().parents[1]
class ProfileValidationBackportTests(unittest.TestCase):
def test_supported_tuples_protocol_guards_and_operational_airtime(self):
source = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
methods = "\n".join(body(source, signature).replace("RadioLibWrapper::", "").replace("uint8_t cr)", "uint8_t cr = 0)")
for signature in ("bool RadioLibWrapper::validateProfile(",
"uint32_t RadioLibWrapper::getProfileAirtime(",
"uint32_t RadioLibWrapper::getBeaconAirtime("))
harness = r'''
#include <cassert>
#include <cmath>
#include <RadioProfiles.h>
#define MAX_TRANS_UNIT 255
#define MESH_ENABLE_ETHERNET_WIRE 1
struct RadioLibWrapper {
mesh::RadioProfiles _profiles;
bool foreign=false;
bool supportsMeshtext()const{return foreign;}
bool supportsEthernetTunnel()const{return foreign;}
uint16_t profilePreamble(uint8_t)const{return 32;}
uint32_t getEstAirtimeFor(int){return 777;}
@METHODS@
};
int main(){
RadioLibWrapper w;
mesh::RadioProfileParams p;p.freq=MESH_RADIO_PROFILE_CHIP==1268?433:915;p.bw=125;p.sf=8;p.cr=5;
assert(w.validateProfile(p));
p.protocol=mesh::RadioProfileProtocol::Meshtext;assert(!w.validateProfile(p));
w.foreign=true;assert(w.validateProfile(p));
p.protocol=mesh::RadioProfileProtocol::Ethernet;assert(w.validateProfile(p));
w.foreign=false;assert(!w.validateProfile(p));p.protocol=mesh::RadioProfileProtocol::MeshCore;
p.freq=137;
assert(w.validateProfile(p)==(MESH_RADIO_PROFILE_CHIP==1276));
p.freq=149;assert(w.validateProfile(p)==(MESH_RADIO_PROFILE_CHIP==1276));
p.freq=915;p.bw=406.25f;
assert(w.validateProfile(p)==(MESH_RADIO_PROFILE_CHIP==2021));
p.freq=2400;
assert(w.validateProfile(p)==(MESH_RADIO_PROFILE_CHIP==1121||MESH_RADIO_PROFILE_CHIP==2021));
p.freq=2300;assert(!w.validateProfile(p));
p.freq=MESH_RADIO_PROFILE_CHIP==1268?433:915;p.bw=125;p.sf=8;
w._profiles.primary=p;
assert(w.getProfileAirtime(0,10)==777); // Preserve the existing live-tuple path.
w._profiles.secondary.params=p;w._profiles.secondary.mode=mesh::RadioProfileMode::Rx;
const auto short_air=w.getProfileAirtime(0,1);
#if MESH_RADIO_PROFILE_CHIP==1110||MESH_RADIO_PROFILE_CHIP==1121
assert(short_air==200);
#else
assert(short_air<200);
#endif
#if MESH_RADIO_PROFILE_CHIP==1121
p.freq=2400;p.bw=812.5;p.sf=12;w._profiles.primary=p;
// Pinned LR11xx forces high-band LDRO: 51 coded blocks * CR5, plus overhead.
const auto expected=uint32_t(ceil((32+4.25+8+51*5)*4096.0/812.5));
assert(w.getProfileAirtime(0,255)==expected);
assert(w.getBeaconAirtime(p,255)==expected);
#endif
assert(w.getProfileAirtime(2,10)==0&&w.getProfileAirtime(0,-1)==0);
assert(w.getProfileAirtime(0,256)==0);
for(float bandwidth:{31.25f,41.67f,41.7f,62.5f,83.34f,101.56f,125.0f,203.125f,250.0f,406.25f,500.0f,812.5f,1000.0f}) {
p.freq=915;p.bw=bandwidth;
#if MESH_RADIO_PROFILE_CHIP==2021
assert(w.validateProfile(p));
#endif
}
}
'''.replace("#include <cassert>", "#include <cassert>\n#include <initializer_list>").replace("@METHODS@", methods)
with tempfile.TemporaryDirectory(prefix="profile-validation-") as folder:
cpp = Path(folder) / "test.cpp"
binary = Path(folder) / "test.exe"
cpp.write_text(harness)
for chip in (1262, 1268, 1276, 1272, 68, 1110, 1121, 2021):
with self.subTest(chip=chip):
flags = [] if os.name == "nt" else ["-fsanitize=address,undefined", "-fno-pie", "-no-pie"]
built = subprocess.run([os.environ.get("CXX", "g++"), "-std=c++11", *flags,
"-DMESH_RADIO_PROFILE_CHIP="+str(chip), "-I", str(ROOT / "src"),
str(cpp), "-o", str(binary)], capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stderr)
ran = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
self.assertEqual(ran.returncode, 0, ran.stderr)
if __name__ == "__main__": unittest.main()