Validate beacon power without stranding saved settings

This commit is contained in:
mikecarper committed 2026-10-16 00:39:07 -07:00
1 parent adbeb2d007
commit d5cc73c43d
15 files changed
+662 -7

No files matched your search

+57
View File
@@ -57,6 +57,62 @@ void put32(uint8_t* bytes, uint32_t value) {
}
void fixCrc(std::vector<uint8_t>& bytes) { put32(bytes.data() + bytes.size() - 4, crc(bytes.data(), bytes.size() - 4)); }
void structuralStorage() {
Fixture mc; mc.mc(1, "Old");
Fixture mt; mt.mt();
for (unsigned fault = 0; fault < 15; ++fault) {
Fixture f;
f.fs.files = fault >= 12 ? mt.fs.files : mc.fs.files;
auto& bytes = f.fs.files.at("/beacons");
uint8_t* record = bytes.data() + 8;
switch (fault) {
case 0: record[10] = 255; break; // Invalid SF.
case 1: record[11] = 4; break; // Invalid coding rate.
case 2: {
const float bw = 123;
std::memcpy(record + 4, &bw, 4); break;
}
case 3: record[8] = 7; record[9] = 0; break;
case 4: { // Valid tuple, but its long preamble overflows safe airtime.
const float bw = 7.8f;
std::memcpy(record + 4, &bw, 4);
record[10] = 12; record[8] = 248; record[9] = 255; break;
}
case 5: put32(record, 0x7fc00000U); break; // NaN frequency.
case 6: record[17] = 255; break; // Malformed UTF-8.
case 7: record[17] = '\n'; break;
case 8: record[21] = 'X'; break; // Hidden bytes after the terminator.
case 9: record[16] = 0; break; // Disabled records must be all zero.
case 10: record[12] = uint8_t(mesh::RadioProfileProtocol::Ethernet); break;
case 11: record[15] = 8; break;
case 12: record[14] = 255; break; // Invalid MT preset.
case 13: record[8] = 32; record[9] = 0; break; // MT preamble is fixed.
case 14: bytes[6] = 0; break; // MT requires durable ID initialization.
}
fixCrc(bytes);
const auto original = f.fs.files;
f.manager.begin(&f.fs, &f.mesh, &f.clock, f.name);
f.cmd("get beacon", false);
f.cmd("set beacon.interval 12", false);
f.cmd("delete beacon all", false);
f.tick(48 * H);
assert(f.mesh.attempts == 0 && f.fs.files == original);
}
// Neither a future schema nor a read error may fall back to an older image.
for (bool unreadable : {false, true}) {
Fixture f;
f.fs.files = mc.fs.files;
f.fs.files["/beacons.bak"] = f.fs.files.at("/beacons");
if (unreadable) f.fs.fail_read_open = true;
else f.fs.files.at("/beacons")[4] = 2;
const auto original = f.fs.files;
f.manager.begin(&f.fs, &f.mesh, &f.clock, f.name);
f.cmd("get beacon", false);
f.tick(48 * H);
assert(f.mesh.attempts == 0 && f.fs.files == original);
}
}
void commands() {
Fixture f;
assert(!f.mesh.wake_enabled && f.fs.files.empty());
@@ -407,6 +463,7 @@ int main(int argc, char** argv) {
else if (!std::strcmp(argv[1], "schedule")) schedule();
else if (!std::strcmp(argv[1], "rolling")) rolling();
else if (!std::strcmp(argv[1], "storage")) storage();
else if (!std::strcmp(argv[1], "structural_storage")) structuralStorage();
else if (!std::strcmp(argv[1], "ids")) ids();
else if (!std::strcmp(argv[1], "mutations")) mutations();
else if (!std::strcmp(argv[1], "stale_backup")) staleBackup();
+90
View File
@@ -0,0 +1,90 @@
#pragma once
#include <RadioProfiles.h>
#include <helpers/radiolib/RadioProfileChip.h>
#include <helpers/radiolib/RadioPowerLimits.h>
#include <algorithm>
#include <cassert>
#include <cstdint>
#define RADIOLIB_ERR_NONE 0
#define RADIOLIB_ERR_INVALID_OUTPUT_POWER -1
#define RADIOLIB_MIN(a, b) std::min(int(a), int(b))
#define RADIOLIB_MAX(a, b) std::max(int(a), int(b))
#define RADIOLIB_CHECK_RANGE(value, low, high, error) \
if ((value) < (low) || (value) > (high)) return (error)
extern unsigned power_hardware_calls;
inline void dacWrite(uint8_t, uint8_t) { ++power_hardware_calls; }
namespace mesh {
struct MainBoard {};
struct Radio {
@RADIO_CAPABILITY@
};
}
struct PhysicalLayer {
bool highFreq = false;
float frequency = 909.5f;
virtual int16_t checkOutputPower(int8_t, int8_t*) = 0;
int16_t setOutputPower(int8_t, bool) { ++power_hardware_calls; return 0; }
};
struct SX1262 : PhysicalLayer {
int16_t checkOutputPower(int8_t, int8_t*) override;
};
struct SX1276 : PhysicalLayer {
int16_t checkOutputPower(int8_t, int8_t*) override;
int16_t checkOutputPower(int8_t, int8_t*, bool);
};
struct SX1272 : PhysicalLayer {
int16_t checkOutputPower(int8_t, int8_t*) override;
int16_t checkOutputPower(int8_t, int8_t*, bool);
};
struct LR1110 : PhysicalLayer {
int16_t checkOutputPower(int8_t, int8_t*) override;
int16_t checkOutputPower(int8_t, int8_t*, bool);
};
struct LR1121 : PhysicalLayer {
int16_t checkOutputPower(int8_t, int8_t*) override;
int16_t checkOutputPower(int8_t, int8_t*, bool);
};
struct LR2021 : PhysicalLayer {
int16_t checkOutputPower(int8_t, int8_t*) override;
};
struct Module {
bool hp = true, lp = true;
bool findRfSwitchMode(int mode) const { return mode == 1 ? hp : lp; }
};
struct STM32WLx : PhysicalLayer {
static constexpr int MODE_TX_HP = 1, MODE_TX_LP = 2;
Module module;
const Module* getMod() const { return &module; }
int16_t checkOutputPower(int8_t, int8_t*) override;
};
@DRIVER_CHECKS@
class RadioLibWrapper : public mesh::Radio {
protected:
PhysicalLayer* _radio;
virtual bool checkBeaconOutputPower(float, int8_t) const;
public:
RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard&) : _radio(&radio) {}
virtual void loop() { ++power_hardware_calls; }
virtual int16_t applyCachedTxPower(int8_t) { ++power_hardware_calls; return 0; }
bool setTxPower(int8_t power) { return applyCachedTxPower(power) == 0; }
bool validateBeaconPower(float, int8_t) const override;
bool validateProfile(const mesh::RadioProfileParams&) const;
bool supportsMeshtext() const { return true; }
bool supportsEthernetTunnel() const { return true; }
};
using CustomSX1276 = SX1276;
using CustomSX1262 = SX1262;
struct CustomSX1276Wrapper : RadioLibWrapper { using RadioLibWrapper::RadioLibWrapper; };
struct CustomSX1262Wrapper : RadioLibWrapper { using RadioLibWrapper::RadioLibWrapper; };
struct IndicatorRadioHal {
unsigned services = 0;
void serviceInterrupt() { ++services; ++power_hardware_calls; }
};
@INDICATOR_WRAPPER@
@DAC_LEVEL@
#define DAC_PA_DEFAULT_DRIVE_DBM 2
#define DAC_PA_TABLE_MAX INT8_MAX
@DAC_WRAPPER@
+251
View File
@@ -0,0 +1,251 @@
#include <Mesh.h>
#include <helpers/BeaconManager.h>
#include <helpers/BeaconFrames.h>
#include <helpers/UTF8Helpers.h>
#include <cstdio>
#include <limits>
unsigned power_hardware_calls = 0;
uint64_t beacon_test_time = 0;
@PRODUCTION_METHODS@
static bool boardAllows(int power, float frequency) {
// Explicit board caps are independent of the chip and calibrated PA range.
(void)power;
(void)frequency;
#ifdef MIN_LORA_TX_POWER
if (power < MIN_LORA_TX_POWER) return false;
#endif
#ifdef MAX_LORA_TX_POWER
if (power > MAX_LORA_TX_POWER) return false;
#endif
return true;
}
static bool driverAllows(int power, float frequency) {
(void)power;
#if defined(TEST_STM32)
(void)frequency;
#ifdef TEST_NO_PA
return false;
#elif defined(TEST_LP_ONLY)
return power >= -17 && power <= 14;
#elif defined(TEST_HP_ONLY)
return power >= -9 && power <= 22;
#else
return power >= -17 && power <= 22;
#endif
#elif MESH_RADIO_PROFILE_CHIP == 1276
(void)frequency;
return (power >= -4 && power <= 17) || power == 20;
#elif MESH_RADIO_PROFILE_CHIP == 1272
(void)frequency;
return power >= -1 && power <= 20;
#elif MESH_RADIO_PROFILE_CHIP == 1110
(void)frequency;
return power >= -17 && power <= 22;
#elif MESH_RADIO_PROFILE_CHIP == 1121
if (frequency > 1000) return power >= -18 && power <= 13;
#ifdef LR1121_FORCE_HP_PA
return power >= -9 && power <= 22;
#else
return power >= -17 && power <= 22;
#endif
#elif MESH_RADIO_PROFILE_CHIP == 2021
return frequency >= 1900 ? power >= -19 && power <= 12 : power >= -9 && power <= 22;
#else
(void)frequency;
return power >= -9 && power <= 22;
#endif
}
static void capability(RadioLibWrapper& wrapper, PhysicalLayer& chip) {
mesh::Radio unsupported;
assert(!unsupported.validateBeaconPower(909.5f, 17));
assert(!wrapper.validateBeaconPower(std::numeric_limits<float>::quiet_NaN(), 10));
for (bool normal_high_band : {false, true}) {
chip.highFreq = normal_high_band;
chip.frequency = normal_high_band ? 2440.0f : 909.5f;
for (float requested : {433.875f, 909.5f, 2440.0f}) {
for (int power = -128; power <= 127; ++power) {
const bool expected = boardAllows(power, requested) && driverAllows(power, requested);
assert(wrapper.validateBeaconPower(requested, int8_t(power)) == expected);
}
assert(chip.frequency == (normal_high_band ? 2440.0f : 909.5f));
assert(chip.highFreq == normal_high_band && power_hardware_calls == 0);
}
}
}
#if !defined(TEST_NO_PA)
static void image(const char* path, std::vector<uint8_t>& bytes, bool write) {
FILE* file = std::fopen(path, write ? "wb" : "rb");
assert(file);
if (write) assert(std::fwrite(bytes.data(), 1, bytes.size(), file) == bytes.size());
else {
bytes.clear();
for (int value; (value = std::fgetc(file)) != EOF;) bytes.push_back(uint8_t(value));
assert(!std::ferror(file));
}
assert(std::fclose(file) == 0);
}
static void upgrade(RadioLibWrapper& wrapper, bool legacy) {
MemoryFS fs;
if (!legacy) {
image("upgrade.beacons", fs.files["/beacons"], false);
image("upgrade.ids", fs.files["/beacon_ids"], false);
}
mesh::Mesh mesh;
mesh._radio = &wrapper;
mesh::RTCClock rtc;
mesh::BeaconManager manager;
manager.begin(&fs, &mesh, &rtc, "Upgrade test");
char reply[256];
const auto command = [&](const char* text) {
assert(manager.handle(text, reply, sizeof(reply)));
assert(std::strncmp(reply, "Err", 3));
};
const auto tick = [&](unsigned hours) {
g_mock_millis += hours * mesh::BeaconManager::HourMillis;
manager.loop(g_mock_millis);
};
if (legacy) {
// These images are produced by the real manager and old Mesh validator,
// not hand-crafted bytes that the old firmware might never have saved.
command("set beacon 1 meshcore 909.5,125,7,5,100 Old MC");
command("set beacon 2 meshcore 909.5,125,7,5,22 Sibling");
command("set beacon 3 meshtext US LongFast 100 Old MT");
tick(6);
assert(mesh.sends.size() == 1 && mesh.sends[0].id != 0);
image("upgrade.beacons", fs.files.at("/beacons"), true);
image("upgrade.ids", fs.files.at("/beacon_ids"), true);
return;
}
const auto original = fs.files;
assert(original.at("/beacons")[6] == 1); // Durable IDs were initialized.
command("get beacon");
command("get beacon 1"); assert(std::strstr(reply, "100") && std::strstr(reply, "Old MC"));
command("get beacon 3"); assert(std::strstr(reply, "100") && std::strstr(reply, "Old MT"));
tick(2); // Unsupported MC consumes no queue attempt or storage write.
assert(mesh.attempts == 0 && mesh.sends.empty() && fs.files == original);
tick(2); // A supported sibling still transmits.
assert(mesh.attempts == 1 && mesh.sends.size() == 1 && mesh.sends[0].slot.power == 22);
assert(fs.files == original);
tick(2); // Unsupported MT must be rejected before durable nextId().
assert(mesh.attempts == 1 && mesh.sends.size() == 1 && fs.files == original);
command("set beacon.interval 12");
command("set beacon 2 meshcore 909.5,125,7,5,20 Updated sibling");
const auto& before = original.at("/beacons");
const auto& after = fs.files.at("/beacons");
for (unsigned index : {0U, 2U})
assert(std::equal(before.begin() + 8 + index * 98, before.begin() + 8 + (index + 1) * 98,
after.begin() + 8 + index * 98));
assert(fs.files.at("/beacon_ids") == original.at("/beacon_ids"));
command("get beacon 3"); assert(std::strstr(reply, "100"));
command("set beacon 1 meshcore 909.5,125,7,5,20 Repaired");
command("delete beacon 3");
assert(fs.files.at("/beacon_ids") == original.at("/beacon_ids"));
manager.begin(&fs, &mesh, &rtc, "Upgrade test");
command("get beacon 1"); assert(std::strstr(reply, "Repaired"));
command("get beacon.interval"); assert(std::strstr(reply, "12"));
tick(1);
assert(mesh.sends.size() == 2 && mesh.sends.back().slot.power == 20);
tick(1);
assert(mesh.sends.size() == 3 && std::strstr(mesh.sends.back().slot.text, "Updated sibling"));
assert(fs.files.at("/beacon_ids") == original.at("/beacon_ids"));
assert(power_hardware_calls == 0);
}
static void manager(RadioLibWrapper& wrapper) {
MemoryFS fs;
mesh::Mesh mesh;
mesh._radio = &wrapper;
mesh::RTCClock rtc;
mesh::BeaconManager manager;
manager.begin(&fs, &mesh, &rtc, "Power test");
char reply[256], command[128];
const float frequency = MESH_RADIO_PROFILE_CHIP == 1268 ? 433.875f : 909.5f;
for (int bad : {-128, 100, 127}) {
std::snprintf(command, sizeof(command), "set beacon 1 meshcore %g,125,7,5,%d Hello", double(frequency), bad);
assert(manager.handle(command, reply, sizeof(reply)));
assert(!std::strncmp(reply, "Err", 3));
assert(fs.files.empty()); // Rejection happens before persistence.
}
int accepted = 22;
while (!driverAllows(accepted, frequency) || !boardAllows(accepted, frequency)) --accepted;
std::snprintf(command, sizeof(command), "set beacon 1 meshcore %g,125,7,5,%d Hello", double(frequency), accepted);
assert(manager.handle(command, reply, sizeof(reply)) && !std::strncmp(reply, "OK", 2));
assert(fs.files.count("/beacons") && power_hardware_calls == 0);
manager.begin(&fs, &mesh, &rtc, "Power test");
g_mock_millis += 2 * mesh::BeaconManager::HourMillis;
manager.loop(g_mock_millis);
assert(mesh.sends.size() == 1 && mesh.sends[0].slot.power == accepted);
assert(power_hardware_calls == 0);
}
#endif
#if MESH_RADIO_PROFILE_CHIP == 1276
static void externalAmplifier() {
mesh::MainBoard board;
CustomSX1276 chip;
const DacPaLevel levels[] = {{10, 30}, {17, 50}, {30, 130}, {33, 225}};
DacPaSX1276Wrapper amplifier(chip, board, 4, levels, 4, 30);
for (int power = -128; power <= 127; ++power)
assert(amplifier.validateBeaconPower(909.5f, int8_t(power))
== (boardAllows(power, 909.5f) && power >= 10 && power <= 30));
DacPaSX1276Wrapper empty(chip, board, 4, nullptr, 0);
assert(!empty.validateBeaconPower(909.5f, 17));
DacPaSX1276Wrapper impossible(chip, board, 4, levels, 4, 9);
assert(!impossible.validateBeaconPower(909.5f, 17));
assert(power_hardware_calls == 0);
}
#endif
int main(int argc, char** argv) {
mesh::MainBoard board;
#if defined(TEST_STM32)
STM32WLx chip;
#ifdef TEST_LP_ONLY
chip.module.hp = false;
#elif defined(TEST_HP_ONLY)
chip.module.lp = false;
#elif defined(TEST_NO_PA)
chip.module.hp = chip.module.lp = false;
#endif
#elif MESH_RADIO_PROFILE_CHIP == 1276
SX1276 chip;
#elif MESH_RADIO_PROFILE_CHIP == 1272
SX1272 chip;
#elif MESH_RADIO_PROFILE_CHIP == 1110
LR1110 chip;
#elif MESH_RADIO_PROFILE_CHIP == 1121
LR1121 chip;
#elif MESH_RADIO_PROFILE_CHIP == 2021
LR2021 chip;
#else
SX1262 chip;
#endif
RadioLibWrapper wrapper(chip, board);
#if !defined(TEST_NO_PA)
if (argc == 2 && !std::strcmp(argv[1], "manager")) { manager(wrapper); return 0; }
if (argc == 2 && !std::strcmp(argv[1], "legacy_write")) { upgrade(wrapper, true); return 0; }
if (argc == 2 && !std::strcmp(argv[1], "upgrade")) { upgrade(wrapper, false); return 0; }
#else
(void)argc; (void)argv;
#endif
capability(wrapper, chip);
#if !defined(TEST_NO_PA)
manager(wrapper);
#endif
#if MESH_RADIO_PROFILE_CHIP == 1276
externalAmplifier();
#endif
#if MESH_RADIO_PROFILE_CHIP == 1262 && !defined(TEST_STM32)
IndicatorRadioHal hal;
IndicatorSX1262Wrapper indicator(chip, board, hal);
assert(indicator.validateBeaconPower(909.5f, 10) == boardAllows(10, 909.5f));
assert(!indicator.validateBeaconPower(909.5f, 100));
assert(hal.services == 0 && power_hardware_calls == 0);
#endif
puts("PASS beacon power capability and persisted CLI validation");
}
+1
View File
@@ -42,6 +42,7 @@ public:
bool valid = true, meshtext = true;
RadioProfiles profiles;
bool validateProfile(const RadioProfileParams& p) const { return valid && RadioProfiles::valid(p); }
bool validateBeaconPower(float, int8_t power) const { return power >= -9 && power <= 22; }
bool supportsMeshtext() const { return meshtext; }
};
class TestClock {
+3
View File
@@ -57,6 +57,9 @@ class BeaconManagerTest(unittest.TestCase):
def test_every_config_short_write_and_recovery(self):
self.run_scenario("storage")
def test_structural_config_corruption_remains_held(self):
self.run_scenario("structural_storage")
def test_durable_meshtext_ids_fail_closed(self):
self.run_scenario("ids")
+167
View File
@@ -0,0 +1,167 @@
"""Run production beacon validation with real read-only RadioLib checkers.
Only the driver hardware boundaries and Mesh queue are replaced. No retune,
power write, DAC write, or Indicator HAL service is allowed during validation.
"""
from pathlib import Path
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from cpp_source import body
ROOT = Path(__file__).resolve().parents[1]
RADIO = ROOT / "src/helpers/radiolib"
class BeaconPowerTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if not cls.compiler:
raise RuntimeError("beacon power regression requires a C++17 compiler")
relative = "src/modules/SX126x/SX1262.cpp"
required = (relative, "src/modules/SX127x/SX1278.cpp", "src/modules/SX127x/SX1272.cpp",
"src/modules/LR11x0/LR1110.cpp", "src/modules/LR11x0/LR1120.cpp",
"src/modules/LR2021/LR2021_config.cpp", "src/modules/SX126x/STM32WLx.cpp")
override = os.environ.get("MESHCORE_RADIOLIB_DIR")
candidates = [Path(override)] if override else sorted((ROOT / ".pio/libdeps").glob("*/RadioLib"))
sdk = next((folder for folder in candidates if all((folder / name).is_file() for name in required)), None)
if sdk is None:
raise RuntimeError("an existing pinned RadioLib checkout is required (no download)")
methods = []
for filename, original, replacement, signatures in (
(relative, "SX1262", "SX1262", ("int16_t SX1262::checkOutputPower(",)),
("src/modules/SX127x/SX1278.cpp", "SX1278", "SX1276",
("int16_t SX1278::checkOutputPower(int8_t power, int8_t* clipped)",
"int16_t SX1278::checkOutputPower(int8_t power, int8_t* clipped, bool")),
("src/modules/SX127x/SX1272.cpp", "SX1272", "SX1272",
("int16_t SX1272::checkOutputPower(int8_t power, int8_t* clipped)",
"int16_t SX1272::checkOutputPower(int8_t power, int8_t* clipped, bool")),
("src/modules/LR11x0/LR1110.cpp", "LR1110", "LR1110",
("int16_t LR1110::checkOutputPower(int8_t power, int8_t* clipped)",
"int16_t LR1110::checkOutputPower(int8_t power, int8_t* clipped, bool")),
("src/modules/LR11x0/LR1120.cpp", "LR1120", "LR1121",
("int16_t LR1120::checkOutputPower(int8_t power, int8_t* clipped)",
"int16_t LR1120::checkOutputPower(int8_t power, int8_t* clipped, bool")),
("src/modules/LR2021/LR2021_config.cpp", "LR2021", "LR2021",
("int16_t LR2021::checkOutputPower(",)),
("src/modules/SX126x/STM32WLx.cpp", "STM32WLx", "STM32WLx",
("int16_t STM32WLx::checkOutputPower(",))):
text = (sdk / filename).read_text(encoding="utf-8")
methods += [body(text, signature).replace(original + "::", replacement + "::")
for signature in signatures]
header = (ROOT / "test/fixtures/beacons/power_radio.h").read_text(encoding="ascii")
header = header.replace("@DRIVER_CHECKS@", "")
header = header.replace("@RADIO_CAPABILITY@", body(
(ROOT / "src/Dispatcher.h").read_text(encoding="ascii"), "virtual bool validateBeaconPower("))
header = header.replace("@INDICATOR_WRAPPER@", body(
(ROOT / "variants/sensecap_indicator-espnow/IndicatorSX1262Wrapper.h").read_text(encoding="ascii"),
"class IndicatorSX1262Wrapper") + ";")
dac = (RADIO / "DacPaSX1276Wrapper.h").read_text(encoding="ascii")
header = header.replace("@DAC_LEVEL@", body(dac, "struct DacPaLevel") + ";")
header = header.replace("@DAC_WRAPPER@", body(dac, "class DacPaSX1276Wrapper") + ";")
mesh = (ROOT / "test/fixtures/beacons/mocks/Mesh.h").read_text(encoding="ascii")
validator = body(mesh, "bool validateTextBeacon(")
mesh = mesh.replace(validator, "bool validateTextBeacon(const BeaconSlot&) const;")
mesh = mesh.replace("#pragma once", '#pragma once\n#include "PowerRadio.h"')
mesh = mesh.replace("class Mesh {\n public:", "class Mesh {\n public:\n ::RadioLibWrapper* _radio = nullptr;")
cls.shared = (RADIO / "RadioLibWrappers.cpp").read_text(encoding="utf-8")
cls.mesh_source = (ROOT / "src/MeshBeacon.cpp").read_text(encoding="ascii")
production = "\n".join(methods) + "\n" + "\n".join(body(cls.shared, signature) for signature in (
"bool RadioLibWrapper::validateBeaconPower(", "bool RadioLibWrapper::checkBeaconOutputPower(",
"bool RadioLibWrapper::validateProfile("))
production += "\nnamespace mesh {\n" + body(cls.mesh_source, "bool Mesh::validateTextBeacon(") + "\n}"
production += "\nnamespace mesh { namespace beacon {\n" + body(
(ROOT / "src/helpers/BeaconFrames.cpp").read_text(encoding="ascii"), "bool validText(") + "\n}}"
cls.fixture = (ROOT / "test/fixtures/beacons/power_test.cpp").read_text(encoding="ascii")
cls.production = production
cls.work = tempfile.TemporaryDirectory(prefix="meshcore-beacon-power-")
cls.addClassCleanup(cls.work.cleanup)
cls.directory = Path(cls.work.name)
(cls.directory / "PowerRadio.h").write_text(header, encoding="ascii")
(cls.directory / "Mesh.h").write_text(mesh, encoding="ascii")
def compile_run(self, name, flags, production=None, scenario=None, negative=False, manager_source=None):
cpp, binary = self.directory / (name + ".cpp"), self.directory / name
cpp.write_text(self.fixture.replace("@PRODUCTION_METHODS@", production or self.production), encoding="ascii")
sanitizers = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
if sys.platform.startswith("linux") else [])
built = subprocess.run([
self.compiler, "-std=c++17", "-O1", "-g", "-Wall", "-Wextra", "-Werror",
"-DMESH_ENABLE_TX_BEACONS=1", "-DMESH_ENABLE_MESHTEXT_CHAT=0",
"-DMESH_ENABLE_MESHTEXT_WIRE=1", "-DLORA_TX_POWER=2", *flags, *sanitizers,
"-I" + str(self.directory), "-I" + str(ROOT / "test/fixtures/beacons/mocks"),
"-I" + str(ROOT / "test/fixtures/radio_profiles/mocks"), "-isystem", str(ROOT / "test/mocks"),
"-I" + str(ROOT / "src"), "-I" + str(ROOT / "src/helpers"),
str(manager_source or ROOT / "src/helpers/BeaconManager.cpp"),
str(ROOT / "src/helpers/MeshtextChat.cpp"), str(cpp), "-o", str(binary)],
capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
result = subprocess.run([str(binary), *([scenario] if scenario else [])],
capture_output=True, text=True, cwd=self.directory, timeout=15)
if negative:
self.assertNotEqual(result.returncode, 0, "old power validation unexpectedly passed")
self.assertIn("Assertion", result.stderr)
else:
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_chip_ranges_prospective_bands_board_caps_and_external_pa(self):
for name, flags in (
("sx1262", ["-DRADIO_CLASS=CustomSX1262"]),
("sx1268", ["-DRADIO_CLASS=CustomSX1268"]),
("llcc68", ["-DRADIO_CLASS=CustomLLCC68"]),
("sx1276", ["-DRADIO_CLASS=CustomSX1276"]),
("sx1272", ["-DUSE_SX1272=1"]),
("lr1110", ["-DRADIO_CLASS=CustomLR1110"]),
("lr1121", ["-DRADIO_CLASS=CustomLR1121"]),
("lr1121_hp", ["-DRADIO_CLASS=CustomLR1121", "-DLR1121_FORCE_HP_PA=1"]),
("lr2021", ["-DRADIO_CLASS=CustomLR2021", "-DUSE_LR2021=1"]),
("stm32_dual", ["-DRADIO_CLASS=CustomSTM32WLx", "-DTEST_STM32=1"]),
("stm32_lp", ["-DRADIO_CLASS=CustomSTM32WLx", "-DTEST_STM32=1", "-DTEST_LP_ONLY=1"]),
("stm32_hp", ["-DRADIO_CLASS=CustomSTM32WLx", "-DTEST_STM32=1", "-DTEST_HP_ONLY=1"]),
("stm32_no_pa", ["-DRADIO_CLASS=CustomSTM32WLx", "-DTEST_STM32=1", "-DTEST_NO_PA=1"]),
("board_cap", ["-DRADIO_CLASS=CustomSX1262", "-DMIN_LORA_TX_POWER=0", "-DMAX_LORA_TX_POWER=13"]),
("dac_cap", ["-DRADIO_CLASS=CustomSX1276", "-DMIN_LORA_TX_POWER=17", "-DMAX_LORA_TX_POWER=30"])):
with self.subTest(chip=name):
self.compile_run(name, flags)
def test_previous_mesh_validator_accepts_impossible_power_before_persistence(self):
guard = "!_radio->validateBeaconPower(slot.radio.freq, slot.power)"
self.assertEqual(self.production.count(guard), 1)
old = self.production.replace(guard,
"(slot.power < -9 || !isLoRaTxPowerValid(slot.power, slot.radio.freq))")
self.compile_run("old_mesh", ["-DRADIO_CLASS=CustomSX1262"], old, "manager", negative=True)
def test_legacy_power_image_remains_queryable_and_repairable(self):
guard = "!_radio->validateBeaconPower(slot.radio.freq, slot.power)"
self.assertEqual(self.production.count(guard), 1)
legacy = self.production.replace(guard,
"(slot.power < -9 || !isLoRaTxPowerValid(slot.power, slot.radio.freq))")
flags = ["-DRADIO_CLASS=CustomSX1262"]
self.compile_run("legacy_image", flags, legacy, "legacy_write")
self.compile_run("upgrade", flags, scenario="upgrade")
# Restoring the former disk-time capability check bricks every beacon
# command on precisely the same actual legacy image.
source = (ROOT / "src/helpers/BeaconManager.cpp").read_text(encoding="ascii")
marker = "if (!decodeSlot(bytes, slot)"
self.assertEqual(source.count(marker), 1)
held = self.directory / "held_manager.cpp"
held.write_text(source.replace(marker,
marker + " || (slot.enabled && !mesh_->validateTextBeacon(slot))"), encoding="ascii")
self.compile_run("held_upgrade", flags, scenario="upgrade", negative=True, manager_source=held)
def test_current_band_driver_check_is_not_a_prospective_band_check(self):
checker = body(self.shared, "bool RadioLibWrapper::checkBeaconOutputPower(")
old = self.production.replace(checker, """bool RadioLibWrapper::checkBeaconOutputPower(
float, int8_t power) const {
return _radio->checkOutputPower(power, nullptr) == RADIOLIB_ERR_NONE;
}""")
self.compile_run("old_band", ["-DRADIO_CLASS=CustomLR1121"], old, negative=True)
if __name__ == "__main__":
unittest.main()
+14
View File
@@ -25,6 +25,8 @@ HARNESS = r'''
#include <cstdio>
#include <initializer_list>
#include <RadioProfiles.h>
#include <helpers/radiolib/RadioProfileChip.h>
#include <helpers/radiolib/RadioPowerLimits.h>
#define MESH_ENABLE_TX_BEACONS 1
#define MESH_ENABLE_MESHTEXT_WIRE 1
#define STATE_IDLE 0
@@ -60,6 +62,7 @@ struct Modem {
int finishTransmit() { ++finishes; return 0; }
int startTransmit(uint8_t*, int) { return tx_result; }
int setPreambleLength(uint16_t value) { preamble = value; return 0; }
int checkOutputPower(int8_t value, int8_t*) { return value >= -9 && value <= 22 ? 0 : -1; }
};
struct RadioLibWrapper {
Modem modem;
@@ -162,6 +165,8 @@ struct RadioLibWrapper {
bool startSendRaw(const uint8_t*, int);
void onSendFinished();
uint32_t getBeaconAirtime(const mesh::RadioProfileParams&, int);
bool validateBeaconPower(float, int8_t) const;
virtual bool checkBeaconOutputPower(float, int8_t) const;
void cacheBeaconRestoreParams();
Result prepareBeaconTransmit(const mesh::RadioProfileParams&, int8_t);
Result restoreBeaconReceive();
@@ -311,6 +316,13 @@ int main() {
assert(w.prepareBeaconTransmit(p, 10) == Result::FAILED && !w._beacon_owned);
++cases;
}
for (int power : {-10, 23, 100, 127}) {
RadioLibWrapper w;
assert(w.prepareBeaconTransmit(beacon(), int8_t(power)) == Result::FAILED);
assert(!w._beacon_owned && !w.applications && !w.power_sets);
assert(!w.modem.standbys && !w.retune_begins && !w.stops);
++cases;
}
std::printf("PASS: %u actual-method beacon radio cases\n", cases);
}
'''
@@ -327,6 +339,8 @@ class BeaconRadioTest(unittest.TestCase):
"bool RadioLibWrapper::startSendRaw(const uint8_t* bytes, int len)",
"void RadioLibWrapper::onSendFinished()",
"uint32_t RadioLibWrapper::getBeaconAirtime(",
"bool RadioLibWrapper::validateBeaconPower(",
"bool RadioLibWrapper::checkBeaconOutputPower(",
"void RadioLibWrapper::cacheBeaconRestoreParams()",
"mesh::RadioParamApplyResult RadioLibWrapper::prepareBeaconTransmit(",
"bool RadioLibWrapper::isBeaconChannelBusy()",