diff --git a/src/RadioProfiles.h b/src/RadioProfiles.h index bcebf53d5..7c97bf0d0 100644 --- a/src/RadioProfiles.h +++ b/src/RadioProfiles.h @@ -118,7 +118,7 @@ class RadioProfiles { if (!isfinite(p.freq) || !isfinite(p.bw) || p.freq < 150 || p.freq > 2500 || p.sf < 5 || p.sf > 12 || p.cr < 5 || p.cr > 8 || (p.preamble != 0 && (p.preamble < 8 || p.preamble > MaxPreamble))) return false; - const float bandwidths[] = {7.8f, 10.4f, 15.6f, 20.8f, 31.25f, 41.7f, + 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}; for (float bw : bandwidths) if (fabsf(p.bw - bw) < 0.01f) return true; return false; @@ -126,12 +126,23 @@ class RadioProfiles { static double symbolUs(const RadioProfileParams& p) { return p.bw > 0 && p.sf <= 12 ? (double)(1U << p.sf) * 1000.0 / p.bw : 0; } +#if defined(STM32_PLATFORM) && defined(__GNUC__) && !defined(__clang__) + // Share the complete validation on flash-constrained STM32 targets. + __attribute__((noinline, noclone)) +#endif static bool safePreamble(const RadioProfileParams& p, uint16_t symbols) { const double symbol = symbolUs(p); if (symbol <= 0) return false; const int de = symbol >= 16000 ? 1 : 0; - const double payload = 8 + ceil((8.0 * 255 - 4 * p.sf - + (p.sf <= 6 ? 20 : 28) + 16) / (4 * (p.sf - 2 * de))) * 8; + const int denominator = 4 * (p.sf - 2 * de); + // The payload expression is integral apart from this small rational + // ceiling. Preserve even invalid-SF behavior: a zero denominator was + // infinite (and failed the limit), while signed division rounds a + // negative quotient toward zero, exactly its mathematical ceiling. + if (!denominator) return false; + const int numerator = 8 * 255 - 4 * p.sf + (p.sf <= 6 ? 20 : 28) + 16; + const double payload = 8 + (denominator > 0 + ? (numerator + denominator - 1) / denominator : numerator / denominator) * 8; // RadioLib's microsecond airtime calculation multiplies by four before // dividing. Reject settings that overflow that intermediate for a full // packet, including a retry using coding rate 4/8. diff --git a/test/fixtures/radio_profiles/chirp_math.cpp b/test/fixtures/radio_profiles/chirp_math.cpp index 685f2a2d4..2fcad3fe7 100644 --- a/test/fixtures/radio_profiles/chirp_math.cpp +++ b/test/fixtures/radio_profiles/chirp_math.cpp @@ -1,13 +1,82 @@ #include #include #include +#include +#include #include using Profiles=mesh::RadioProfiles; mesh::RadioProfileParams params(unsigned sf,float bw) { mesh::RadioProfileParams p; p.freq=909.5;p.sf=sf;p.bw=bw;p.cr=5;return p; } +static bool originalSafePreamble(const mesh::RadioProfileParams& p, uint16_t symbols) { + const double symbol = Profiles::symbolUs(p); + if (symbol <= 0) return false; + const int de = symbol >= 16000 ? 1 : 0; + const double payload = 8 + std::ceil((8.0 * 255 - 4 * p.sf + + (p.sf <= 6 ? 20 : 28) + 16) / (4 * (p.sf - 2 * de))) * 8; + return (symbols + (p.sf <= 6 ? 6.25 : 4.25) + payload) * symbol * 4 <= UINT32_MAX; +} +static void checkSafePreamble(unsigned sf, float bw, uint16_t symbols) { + const auto p = params(sf, bw); + assert(Profiles::safePreamble(p, symbols) == originalSafePreamble(p, symbols)); +} int main() { + // Sharing the read-only validation table must preserve every supported + // value and the strict float tolerance on both sides of its boundary. + for (float bw : {7.8f, 10.4f, 15.6f, 20.8f, 31.25f, 41.7f, + 62.5f, 125.0f, 250.0f, 500.0f, 1000.0f, 812.5f, 1625.0f}) { + auto p = params(7, bw); + assert(Profiles::valid(p)); + const float lower = bw - 0.01f, upper = bw + 0.01f; + for (float inside : {std::nextafter(lower, bw), std::nextafter(upper, bw)}) { + p.bw = inside; + assert(std::fabs(inside - bw) < 0.01f && Profiles::valid(p)); + } + for (float outside : {std::nextafter(lower, -std::numeric_limits::infinity()), + std::nextafter(upper, std::numeric_limits::infinity())}) { + p.bw = outside; + assert(std::fabs(outside - bw) >= 0.01f && !Profiles::valid(p)); + } + } + for (float unsupported : {0.0f, 1.0f, 8.0f, 12.0f, 100.0f, 1500.0f, 2000.0f}) + assert(!Profiles::valid(params(7, unsupported))); + for (float nonfinite : {std::numeric_limits::quiet_NaN(), + std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}) { + assert(!Profiles::valid(params(7, nonfinite))); + auto p = params(7, 125); + p.freq = nonfinite; + assert(!Profiles::valid(p)); + } + // Compare the production integer ceiling with the original double formula, + // including invalid SFs where zero/negative denominators were observable. + for (unsigned sf = 0; sf < 256; ++sf) { + const float transition = float(double(1u << (sf & 31)) * 1000 / 16000); + for (float bw : {0.0f, -0.0f, -1.0f, 7.8f, 62.5f, 125.0f, 500.0f, + 1000.0f, transition, std::nextafter(transition, 0.0f), + std::nextafter(transition, std::numeric_limits::infinity()), + std::numeric_limits::min(), std::numeric_limits::max(), + std::numeric_limits::denorm_min(), + std::numeric_limits::quiet_NaN(), + std::numeric_limits::infinity(), + -std::numeric_limits::infinity()}) { + for (uint16_t symbols : {0, 1, 8, 16, 32, 128, 1024, 65528, 65535}) + checkSafePreamble(sf, bw, symbols); + } + } + for (unsigned sf = 0; sf <= 12; ++sf) + for (float bw : {0.001f, 7.8f, 125.0f}) + for (unsigned symbols = 0; symbols <= UINT16_MAX; ++symbols) + checkSafePreamble(sf, bw, uint16_t(symbols)); + uint32_t random = 0x89abcdef; + for (unsigned i = 0; i < 16384; ++i) { + random ^= random << 13; random ^= random >> 17; random ^= random << 5; + float bw; + static_assert(sizeof(bw) == sizeof(random), "Radio bandwidth is a 32-bit float"); + std::memcpy(&bw, &random, sizeof(bw)); + checkSafePreamble((i & 1) ? i % 13 : random & 255, bw, uint16_t(random >> 16)); + } assert(Profiles::MinListenSymbols==4.6); assert(Profiles::SlowListenSymbols==4.6); assert(Profiles::MinFastListenSymbols==4.6); diff --git a/test/test_radio_chirp_math.py b/test/test_radio_chirp_math.py index 6f591735d..ae6d718af 100644 --- a/test/test_radio_chirp_math.py +++ b/test/test_radio_chirp_math.py @@ -1,5 +1,6 @@ """Compile the portable production chirp math, independent of radio hardware.""" from pathlib import Path +import os import shutil import subprocess import tempfile @@ -12,11 +13,16 @@ class RadioChirpMathTest(unittest.TestCase): compiler=shutil.which('g++') or shutil.which('clang++') self.assertIsNotNone(compiler) with tempfile.TemporaryDirectory() as folder: - exe=Path(folder)/'chirps.exe' - done=subprocess.run([compiler,'-std=c++17','-Wall','-Wextra','-I',str(ROOT/'src'), - str(ROOT/'test/fixtures/radio_profiles/chirp_math.cpp'),'-o',str(exe)],capture_output=True,text=True) - self.assertEqual(done.returncode,0,done.stderr) - done=subprocess.run([str(exe)],capture_output=True,text=True) - self.assertEqual(done.returncode,0,done.stderr) + for stm in (False, True): + with self.subTest(stm32_defined=stm): + exe=Path(folder)/('chirps-stm.exe' if stm else 'chirps.exe') + checks=[] if os.name=='nt' else ['-fsanitize=address,undefined', + '-fno-sanitize-recover=all','-fno-pie','-no-pie'] + done=subprocess.run([compiler,'-std=c++17','-O1','-g','-Wall','-Wextra', + *checks,*(['-DSTM32_PLATFORM'] if stm else []),'-I',str(ROOT/'src'), + str(ROOT/'test/fixtures/radio_profiles/chirp_math.cpp'),'-o',str(exe)],capture_output=True,text=True) + self.assertEqual(done.returncode,0,done.stderr) + done=subprocess.run([str(exe)],capture_output=True,text=True) + self.assertEqual(done.returncode,0,done.stderr) if __name__=='__main__':unittest.main()