Fix firmware budget regressions and flood alias delimiters

Share nRF52 checked-record reads and command-family lookups without changing
storage formats, error protection, fleet authorization, or delegated scopes.
Use the existing bounded Merkle proof workspace for qualified ESP32 repeaters
while retaining source capacity, wire format, and every enabled feature.
Restore advert tab/CRLF delimiters and expose nested qualification build logs.

Validation: all 1932 native CI test cases; exact canonical RAK4631 Full,
Wio-E5 Mini, Station G3 Full MQTT, and Wireless Tracker Full wrappers;
linked RAK crypto/workspace checks and retained Reader code equivalence;
focused persistence, authorization, source-proof, and RAM policy regressions.
This commit is contained in:
mikecarper committed 2026-10-21 03:02:46 -07:00
1 parent 99724cd1d3
commit db1c4704e0
15 files changed
+370 -130

No files matched your search

+18 -2
View File
@@ -667,7 +667,7 @@ int main() {
}, "board_companion_radio_full")
def test_compact_tables_keep_capacity_and_reserve_active_ota_allocations(self):
for platform, leaf_limit in (("NRF52_PLATFORM", 4096), ("ESP32_PLATFORM", 16384),
for platform, leaf_limit in (("NRF52_PLATFORM", 4096), ("ESP32_PLATFORM", 1536),
("RP2040_PLATFORM", 4096), ("STM32_PLATFORM", 4096)):
with self.subTest(platform=platform):
flags = {"ENABLE_OTA": 1, "MESH_CLIENT_REPEATER_ONLY": 1,
@@ -704,6 +704,22 @@ int main() {
self.assertEqual(alias["components"]["neighbor_history"], 2048 * 9 + 16)
self.assertEqual(alias["components"]["radio_packet_pool"], 10240)
def test_qualified_esp32_repeaters_stream_source_proofs_without_cutting_other_budgets(self):
for psram in (False, True):
flags = {"ENABLE_OTA": 1, "MESH_CLIENT_REPEATER_ONLY": 1,
"WITH_MQTT_BRIDGE": 1, "WIFI_SSID": "test", "MAX_CLIENTS": 32}
if psram:
flags["BOARD_HAS_PSRAM"] = 1
with self.subTest(psram=psram):
legacy = ram.requirements("ESP32_PLATFORM", {
**flags, "OTA_BOUNDED_SOURCE_PROOFS": 0}, "board_repeater")["components"]
selected = ram.requirements("ESP32_PLATFORM", flags, "board_repeater")["components"]
expected = 1536
self.assertEqual(selected["ota_generic_source_buffers"], expected + 2048 + 32)
for name, amount in legacy.items():
if name != "ota_generic_source_buffers":
self.assertEqual(selected[name], amount)
def test_combined_rak_oled_bound_does_not_change_other_display_budgets(self):
defines = {
"RAK_4631": 1, "ETHERNET_ENABLED": 1, "OTA_RAK_AUTO_STORE": 1,
@@ -732,7 +748,7 @@ int main() {
}
full = ram.requirements("ESP32_PLATFORM", defines, "Heltec_v3_repeater_observer_mqtt")
parts = full["components"]
self.assertEqual(full["required_heap_bytes"], 218768)
self.assertEqual(full["required_heap_bytes"], 203920)
self.assertEqual(parts["display_pixels_and_driver"], 2048)
self.assertEqual(parts["uart_bridge_and_driver"], 3072 + 4096)
self.assertEqual(parts["wireless_stacks"], 49152)
+26
View File
@@ -422,6 +422,32 @@ class FleetCommandCryptoTests(unittest.TestCase):
with self.subTest(command=command):
self.assertEqual(self.decode(envelope(command)), "reject")
def test_every_row_family_retains_suffix_bounds_and_detail_permissions(self):
filters = ("flood.filter", "flood.rule", "fr")
rows = filters + ("flood.filter.blacklist", "fb", "flood.moderation", "fm",
"flood.channel.scope", "fs", "flood.channel.scope.require", "fsr")
for family in rows:
for suffix in ("", ".1", ".255"):
command = f"get {family}{suffix}"
with self.subTest(command=command):
self.assertEqual(self.decode(envelope(command)), f"{NOW}\n{NOW + 120}\n{command}")
for suffix in (".0", ".01", ".256", ".9999999999", ".1.secret", "x"):
command = f"get {family}{suffix}"
with self.subTest(command=command):
self.assertEqual(self.decode(envelope(command)), "reject")
for detail in ("sender", "n", "text", "x"):
for suffix in ("", ".1"):
command = f"get {family}{suffix} {detail}"
expected = f"{NOW}\n{NOW + 120}\n{command}" if family in filters and suffix else "reject"
with self.subTest(command=command):
self.assertEqual(self.decode(envelope(command)), expected)
for family in ("flood.channel.data", "flood.channel.data.hops", "flood.max",
"flood.max.unscoped", "flood.max.advert", "radio2.status", "radio2.timing"):
for suffix in (".1", ".255", ".secret"):
command = f"get {family}{suffix}"
with self.subTest(command=command):
self.assertEqual(self.decode(envelope(command)), "reject")
def test_scheduled_radio_commands_have_cross_language_real_signatures(self):
for command in [
"set radioat 910.5,500,5,5,+1,auto",
+22 -7
View File
@@ -23,13 +23,28 @@ static_assert(OTA_PROOFGEN_SCRATCH / 4 == 4095, "odd byte limit lost usable leaf
#endif
"""
base = [os.environ.get("CXX", "g++"), "-std=c++17", "-fsyntax-only", "-x", "c++", "-",
"-I", str(ROOT / "src"), "-DESP32_PLATFORM=1", "-DCOMPANION_RADIO_FULL=1"]
for scratch in (None, 0, 1, 3, 4, 5, 16383, 16384, 16385, 32768):
bounded = scratch is None or 4 <= scratch <= 16384
flags = [] if scratch is None else [f"-DOTA_PROOFGEN_SCRATCH={scratch}"]
with self.subTest(scratch=scratch):
result = subprocess.run([*base, *flags, f"-DEXPECT_BOUNDED={int(bounded)}"],
input=source, text=True, capture_output=True, timeout=30)
"-I", str(ROOT / "src"), "-DESP32_PLATFORM=1"]
for role in ("COMPANION_RADIO_FULL", "MESH_CLIENT_REPEATER_ONLY"):
for scratch in (None, 0, 1, 3, 4, 5, 16383, 16384, 16385, 32768):
bounded = scratch is None or 4 <= scratch <= 16384
flags = [f"-D{role}=1"]
if scratch is not None:
flags.append(f"-DOTA_PROOFGEN_SCRATCH={scratch}")
with self.subTest(role=role, scratch=scratch):
result = subprocess.run([*base, *flags, f"-DEXPECT_BOUNDED={int(bounded)}"],
input=source, text=True, capture_output=True, timeout=30)
self.assertEqual(result.returncode, 0, result.stderr)
# Repeater proof buffers are internal even when the board has PSRAM.
result = subprocess.run([*base, "-DMESH_CLIENT_REPEATER_ONLY=1",
"-DBOARD_HAS_PSRAM=1", "-DEXPECT_BOUNDED=1"],
input=source, text=True, capture_output=True, timeout=30)
self.assertEqual(result.returncode, 0, result.stderr)
for flags in ([], ["-DMESH_CLIENT_REPEATER_ONLY=0"],
["-DCOMPANION_RADIO_FULL=1", "-DBOARD_HAS_PSRAM=1"],
["-DMESH_CLIENT_REPEATER_ONLY=1", "-DOTA_BOUNDED_SOURCE_PROOFS=0"]):
with self.subTest(legacy=flags):
result = subprocess.run([*base, *flags, "-DEXPECT_BOUNDED=0"], input=source,
text=True, capture_output=True, timeout=30)
self.assertEqual(result.returncode, 0, result.stderr)
for scratch in (0, 3, 16385):
with self.subTest(forced_invalid=scratch):
+54
View File
@@ -98,6 +98,60 @@ class OtaSpeedTest(unittest.TestCase):
self.assertEqual(result.returncode, 0, result.stderr)
subprocess.run([str(binary)], check=True)
def test_nrf52_metadata_open_and_short_reads_preserve_committed_speed(self):
mock = (ROOT / "test/fixtures/radio_profiles/mocks/helpers/IdentityStore.h").read_text()
mock = mock.replace("bool fail_write = false;", "bool stat_error=false;\n bool fail_write = false;")
mock = mock.replace("bool mkdir(const char*)", "void _lockFS() {}\n void _unlockFS() {}\n"
" MemoryFS* _getFS() { return this; }\n"
" bool mkdir(const char*)")
mock += ("\nstruct lfs_info {};\nconstexpr int LFS_ERR_NOENT=-2;\n"
"inline int lfs_stat(MemoryFS* fs, const char* path, lfs_info*) {\n"
" if (fs->stat_error) return -5;\n"
" return fs->files.count(path) ? 0 : LFS_ERR_NOENT;\n}\n")
faults = r'''
const auto committed = fs.files;
for (unsigned fault = 0; fault < 3; ++fault) {
fs.stat_error = fault == 0;
fs.fail_read_open = fault == 1;
fs.fail_read_after = fault == 2 ? 4 : -1;
beginSpeedConfig(&fs);
assert(speedFactor() == 1.0f); // Unreadable is not a corrupt image to replace.
command("set ota.speed 2", "ERR");
assert(fs.files == committed);
fs.stat_error = fs.fail_read_open = false;
fs.fail_read_after = -1;
beginSpeedConfig(&fs);
assert(speedFactor() == 1.125f);
}
fs.files["/ota_speed"].pop_back(); // A complete, wrong-sized image is invalid.
beginSpeedConfig(&fs);
assert(speedFactor() == 1.0f);
command("set ota.speed 2", "OK ota.speed=2x (saved)");
beginSpeedConfig(&fs);
assert(speedFactor() == 2.0f);
'''
source = HARNESS.replace(" assert(speedFactor() == 1.125f);\n}",
" assert(speedFactor() == 1.125f);\n" + faults + "}")
compiler = shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler)
with tempfile.TemporaryDirectory() as directory:
work = Path(directory)
(work / "helpers").mkdir()
(work / "helpers/IdentityStore.h").write_text(mock)
cpp = work / "speed.cpp"
cpp.write_text(source)
binary = work / "speed"
result = subprocess.run([
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", "-Wno-sign-compare",
"-DENABLE_OTA=1", "-DNRF52_PLATFORM=1", "-I", directory,
"-I", str(ROOT / "test/fixtures/radio_profiles/mocks"),
"-I", str(ROOT / "test/mocks"), "-I", str(ROOT / "src"),
str(ROOT / "src/helpers/ota/OtaSpeedConfig.cpp"), str(cpp),
"-o", str(binary),
], text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
subprocess.run([str(binary)], check=True)
def test_shared_role_wiring_and_idle_context_access(self):
common = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
companion = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
+48 -1
View File
@@ -237,6 +237,23 @@ void run(const char* command,char* reply) {
else strcpy(reply,"unhandled");
}
int main() {
// Preserve the advert CLI's whitespace delimiters for both full names and
// aliases, returning the original bytes for the caller's own validation.
struct Boundary {const char* command;const char* suffix;};
for(const auto& row : {
Boundary{"get flood.advert", ""}, Boundary{"get fa", ""},
Boundary{"get flood.advert\t2\r\n", "\t2\r\n"},
Boundary{"get fa\t2\r\n", "\t2\r\n"},
Boundary{"get flood.advert\r\n", "\r\n"},
Boundary{"get fa\n", "\n"}, Boundary{"get fa 2", " 2"},
Boundary{"get fa.2", ".2"}}) {
const char* suffix=mesh::cli::floodCommandArguments(row.command,"get flood.advert");
assert(suffix==row.command+strlen(row.command)-strlen(row.suffix));
assert(!strcmp(suffix,row.suffix));
}
for(const char* command : {"get flood.advertisement", "get fax", "get fa/2",
"get fa=2", "get fa\v2", "get fa\f2", "get\tfa 2"})
assert(!mesh::cli::floodCommandArguments(command,"get flood.advert"));
struct Test {const char* command;const char* suffix;const char* action;};
for(const auto& row : {
Test{"get fm.2",".2","formatFloodGroupModeration"},
@@ -287,11 +304,41 @@ int main() {
def test_shorthand_uses_the_existing_filter_manager_permissions(self):
source = (ROOT/'examples/simple_repeater/MyMesh.cpp').read_text()
helpers = '\n'.join(method(source, signature) for signature in [
'static bool commandFamilyMatches(', 'static bool isCommonManagerReadOnlyAllowed(',
'static bool commandFamilyMatches(', 'static bool commandListMatches(',
'static bool isCommonManagerReadOnlyAllowed(',
'static bool isRegionMgrAllowed(', 'static bool isFilterMgrAllowed('])
code = (f'#include "{(ROOT/"src/helpers/FloodCommandAliases.h").as_posix()}"\n'
'#include <cstring>\n#include <cassert>\n#include <initializer_list>\n') + helpers + r'''
int main() {
// Every common namespace retains both manager roles; sensitive get namespaces
// and partial command names stay denied. Clock deliberately requires equality.
for (const char* cmd : {"ver", "board", "neighbors", "sensor get", "sensor list",
"get role", "get public.key", "get clock.sync", "get battery.alert",
"get rx.watchdog", "get recent.repeater", "get recent.repeaters",
"get outpath", "get altpath", "clock", " get public.key"}) {
assert(isFilterMgrAllowed(cmd));
assert(isRegionMgrAllowed(cmd));
}
for (const char* cmd : {"clock secret", "version", "get rolepassword", "get public.keys",
"get admin.password", "get guest.password", "get wifi.password", "get bridge.secret"}) {
assert(!isFilterMgrAllowed(cmd));
assert(!isRegionMgrAllowed(cmd));
}
for (const char* cmd : {"get repeat", "get loop.detect", "get flood.max",
"get flood.channel.data", "get flood.advert", "get flood.filter",
"get flood.filter.blacklist", "get flood.moderation", "set flood.filter",
"del flood.filter", "set flood.filter.blacklist", "del flood.filter.blacklist",
"set flood.moderation", "del flood.moderation", "clear flood.advert",
"set flood.channel.data", "set flood.max", "set loop.detect", "set repeat"}) {
assert(isFilterMgrAllowed(cmd));
assert(!isRegionMgrAllowed(cmd));
}
for (const char* cmd : {"region", "get flood.channel.scope", "set flood.channel.scope",
"del flood.channel.scope", "get flood.channel.scope.require",
"set flood.channel.scope.require", "del flood.channel.scope.require"}) {
assert(isRegionMgrAllowed(cmd));
assert(!isFilterMgrAllowed(cmd));
}
for (const char* cmd : {"get fr", "get fr.3", "set fr.3 any m=bc d", "del fr.3", "del fr all",
"set fr.3 any c=public sender=^Alice$ text=BEACON len=1-80 prob=50 drop",
"set flood.rule.3 type=grp_txt channel=public sender=^Alice$ text=BEACON drop",