Reduce radio preamble validation overhead with equivalent math

This commit is contained in:
mikecarper committed 2026-10-03 18:31:46 -07:00
1 parent a619c78553
commit 645c8a1119
3 files changed
+95 -9

No files matched your search

+14 -3
View File
@@ -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.
+69
View File
@@ -1,13 +1,82 @@
#include <RadioProfiles.h>
#include <cassert>
#include <cmath>
#include <limits>
#include <cstring>
#include <utility>
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<float>::infinity()),
std::nextafter(upper, std::numeric_limits<float>::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<float>::quiet_NaN(),
std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::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<float>::infinity()),
std::numeric_limits<float>::min(), std::numeric_limits<float>::max(),
std::numeric_limits<float>::denorm_min(),
std::numeric_limits<float>::quiet_NaN(),
std::numeric_limits<float>::infinity(),
-std::numeric_limits<float>::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);
+12 -6
View File
@@ -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()