From e0442880e18264e770cc1be785f7ceccf710429f Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 19 Oct 2026 20:36:16 -0700 Subject: [PATCH] Keep Full RAK Companion within flash using integer airtime blocks --- src/helpers/radiolib/RadioLibWrappers.cpp | 16 ++++-- test/fixtures/trace_profile_timing.cpp | 1 + test/test_clock_transit_profile_timing.py | 1 + test/test_forwarding_profile_timing.py | 2 + .../test_radio_profile_validation_backport.py | 57 ++++++++++++++++++- 5 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 1261ef414..fdd575cb2 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -674,9 +674,11 @@ uint32_t RadioLibWrapper::getBeaconAirtime(const mesh::RadioProfileParams& p, in || (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); + // Round the small integer bit/block ratio without floating-point division. + const int numerator = 8 * length - 4 * p.sf + (p.sf <= 6 ? 20 : 28) + 16; + const int denominator = 4 * (p.sf - 2 * de); + const int coded = numerator > 0 ? (numerator + denominator - 1) / denominator : 0; + const double payload = 8 + coded * p.cr; return static_cast(ceil((preamble + (p.sf <= 6 ? 6.25 : 4.25) + payload) * symbol / 1000.0)); } @@ -1013,9 +1015,11 @@ uint32_t RadioLibWrapper::getProfileAirtime(uint8_t profile, int len, uint8_t cr || (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); + // Round the small integer bit/block ratio without floating-point division. + const int numerator = 8 * len - 4 * p.sf + (p.sf <= 6 ? 20 : 28) + 16; + const int denominator = 4 * (p.sf - 2 * de); + const int coded = numerator > 0 ? (numerator + denominator - 1) / denominator : 0; + const double payload_symbols = 8 + coded * cr; const double suffix = p.sf <= 6 ? 6.25 : 4.25; const double airtime = ceil((profilePreamble(profile) + suffix + payload_symbols) * symbol / 1000.0); const uint32_t rounded = airtime > UINT32_MAX ? UINT32_MAX : uint32_t(airtime); diff --git a/test/fixtures/trace_profile_timing.cpp b/test/fixtures/trace_profile_timing.cpp index a1e5ba8b2..f735409d7 100644 --- a/test/fixtures/trace_profile_timing.cpp +++ b/test/fixtures/trace_profile_timing.cpp @@ -18,6 +18,7 @@ class RadioLibWrapper { public: mesh::RadioProfiles _profiles; + bool validateProfile(const mesh::RadioProfileParams& p) const { return mesh::RadioProfiles::valid(p); } uint16_t profilePreamble(uint8_t profile) { return _profiles.preamble(profile, 32); } uint32_t getEstAirtimeFor(int) { return 77; } uint32_t getProfileAirtime(uint8_t, int, uint8_t = 0); diff --git a/test/test_clock_transit_profile_timing.py b/test/test_clock_transit_profile_timing.py index a6def221c..50f17f7d4 100644 --- a/test/test_clock_transit_profile_timing.py +++ b/test/test_clock_transit_profile_timing.py @@ -32,6 +32,7 @@ HARNESS = r''' class RadioLibWrapper { public: mesh::RadioProfiles _profiles; + bool validateProfile(const mesh::RadioProfileParams& p) const { return mesh::RadioProfiles::valid(p); } uint16_t profilePreamble(uint8_t profile) { return _profiles.preamble(profile, 32); } diff --git a/test/test_forwarding_profile_timing.py b/test/test_forwarding_profile_timing.py index a8cf5535a..e20fd378f 100644 --- a/test/test_forwarding_profile_timing.py +++ b/test/test_forwarding_profile_timing.py @@ -155,9 +155,11 @@ class ForwardingProfileTimingTests(unittest.TestCase): #include #include #include +#include class RadioLibWrapper { public: mesh::RadioProfiles _profiles; + bool validateProfile(const mesh::RadioProfileParams& p) const { return mesh::RadioProfiles::valid(p); } unsigned legacy_calls = 0; uint32_t getEstAirtimeFor(int length) { ++legacy_calls; return uint32_t(17 * length + 3); } uint16_t profilePreamble(uint8_t profile) { return _profiles.params(profile).preamble; } diff --git a/test/test_radio_profile_validation_backport.py b/test/test_radio_profile_validation_backport.py index e3f5d1a8f..0b5c8c415 100644 --- a/test/test_radio_profile_validation_backport.py +++ b/test/test_radio_profile_validation_backport.py @@ -27,10 +27,34 @@ struct RadioLibWrapper { bool foreign=false; bool supportsMeshtext()const{return foreign;} bool supportsEthernetTunnel()const{return foreign;} - uint16_t profilePreamble(uint8_t)const{return 32;} + uint16_t profilePreamble(uint8_t profile)const{ + const auto& p=_profiles.params(profile); + return p.protocol==mesh::RadioProfileProtocol::Meshtext?16:(p.preamble?p.preamble:32); + } uint32_t getEstAirtimeFor(int){return 777;} @METHODS@ }; +// Preserve the previous floating-point formula as a regression oracle for the +// integer block-count optimization, including the high-band LDRO exception. +double referenceAirtime(const mesh::RadioProfileParams& p,int length,uint8_t cr,uint16_t preamble){ + const double symbol=double(1U<=16000 +#if MESH_RADIO_PROFILE_CHIP==1121 + ||(p.freq>1000&&p.sf>10) +#endif + )?1:0; + const double blocks=ceil((8.0*length-4*p.sf+(p.sf<=6?20:28)+16)/(4*(p.sf-2*de))); + return ceil((preamble+(p.sf<=6?6.25:4.25)+8+(blocks>0?blocks*cr:0))*symbol/1000.0); +} +uint32_t referenceProfileAirtime(const mesh::RadioProfileParams& p,int length,uint8_t cr,uint16_t preamble){ + const double airtime=referenceAirtime(p,length,cr,preamble); + const uint32_t rounded=airtime>UINT32_MAX?UINT32_MAX:uint32_t(airtime); +#if MESH_RADIO_PROFILE_CHIP==1110||MESH_RADIO_PROFILE_CHIP==1121 + return rounded<200?200:rounded; +#else + return rounded; +#endif +} 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; @@ -72,6 +96,37 @@ int main(){ assert(w.validateProfile(p)); #endif } + // Exercise every packet length and coding rate, including SF5/6 overhead, + // zero-length negative numerators, narrow-band LDRO and high-band SF11/12. + // The tolerance edge and long preamble also cover the largest airtimes. + for(float frequency:{MESH_RADIO_PROFILE_CHIP==1268?433.0f:915.0f,2400.0f}) + for(float bandwidth:{7.791f,7.8f,31.25f,62.5f,125.0f,500.0f,812.5f}) + for(unsigned sf=5;sf<=12;++sf) + for(unsigned cr=5;cr<=8;++cr) + for(unsigned preamble:{0,8,32,65528}) { + p.freq=frequency;p.bw=bandwidth;p.sf=sf;p.cr=cr;p.preamble=preamble; + p.protocol=mesh::RadioProfileProtocol::MeshCore; + if(!w.validateProfile(p))continue; + w._profiles.primary=p; + for(int length=0;length<=255;++length) { + const auto expected=referenceProfileAirtime(p,length,cr,w.profilePreamble(0)); + assert(w.getProfileAirtime(0,length)==expected); + assert(w.getProfileAirtime(0,length,cr)==expected); + assert(expected<35000000UL); + const uint16_t beacon_preamble=p.preamble?p.preamble:32; + const auto beacon_expected=length&&mesh::RadioProfiles::safePreamble(p,beacon_preamble) + ?uint32_t(referenceAirtime(p,length,cr,beacon_preamble)):0; + assert(w.getBeaconAirtime(p,length)==beacon_expected); + } + assert(w.getProfileAirtime(0,255,4)==w.getProfileAirtime(0,255)); + assert(w.getProfileAirtime(0,255,9)==w.getProfileAirtime(0,255)); + } + // Meshtext fixes the preamble to 16 even when a long explicit value is saved. + w.foreign=true;p.freq=MESH_RADIO_PROFILE_CHIP==1268?433:915; + p.bw=125;p.sf=8;p.cr=5;p.preamble=65528; + p.protocol=mesh::RadioProfileProtocol::Meshtext;w._profiles.primary=p; + assert(w.getProfileAirtime(0,255)==referenceProfileAirtime(p,255,p.cr,16)); + assert(w.getBeaconAirtime(p,255)==uint32_t(referenceAirtime(p,255,p.cr,16))); } '''.replace("#include ", "#include \n#include ").replace("@METHODS@", methods) with tempfile.TemporaryDirectory(prefix="profile-validation-") as folder: