Validate beacon power without stranding saved settings
This commit is contained in:
1 parent
adbeb2d007
commit
d5cc73c43d
15 files changed
+662
-7
No files matched your search
@@ -422,6 +422,7 @@ jobs:
|
||||
'--storage-dir', '.pio/libdeps/radio-status-test'], check=True)
|
||||
PY
|
||||
python3 -B test/test_sx126x_receive_mode.py
|
||||
python3 -B test/test_beacon_power.py -v
|
||||
|
||||
- name: Verify temporary-radio timing and exact reply delivery
|
||||
run: |
|
||||
|
||||
@@ -40,11 +40,23 @@ Meshtext takes an explicit `us`, `eu868`, `eu433`, or `anz` region, a recognized
|
||||
preset, and dBm. Its frequency is calculated using the stock preset channel;
|
||||
its preamble is fixed at 16 symbols. Invalid modem, region, power, or text
|
||||
settings are rejected. Use settings permitted for your hardware and location.
|
||||
Power is checked against the driver's range at the beacon frequency and any
|
||||
explicit board limits. External-PA drivers interpret it in antenna dBm using
|
||||
their calibration range; the normal receive profile's power is unchanged.
|
||||
|
||||
Each message body is limited to 80 bytes and is prefixed with the node name.
|
||||
Settings are saved separately from ordinary preferences and channels.
|
||||
Configuration changes do not immediately transmit a beacon.
|
||||
|
||||
A structurally valid saved slot that the current board or firmware cannot
|
||||
transmit remains available through `get beacon`. Its scheduled opportunity is
|
||||
skipped, while supported slots continue normally. Use `set beacon` to repair
|
||||
that slot or `delete beacon` to remove it; unsupported Meshtext slots do not
|
||||
consume packet IDs. This also permits repairing an out-of-range power saved
|
||||
by older firmware. Corrupt images retain the existing safe backup recovery;
|
||||
without a valid backup they remain held. Unreadable or unknown-version files
|
||||
remain held without transmitting or overwriting them.
|
||||
|
||||
## Radio ownership and limits
|
||||
|
||||
These are ordinary channel text messages, not MeshCore identity advertisements
|
||||
|
||||
@@ -94,6 +94,9 @@ public:
|
||||
virtual bool supportsMeshtext() const { return false; }
|
||||
virtual bool supportsEthernetTunnel() const { return false; }
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
// Check the requested antenna power at the prospective beacon frequency.
|
||||
// Validation must not retune the radio or alter its current power setting.
|
||||
virtual bool validateBeaconPower(float, int8_t) const { return false; }
|
||||
virtual RadioParamApplyResult prepareBeaconTransmit(const RadioProfileParams&, int8_t) {
|
||||
return RadioParamApplyResult::FAILED;
|
||||
}
|
||||
|
||||
+1
-2
@@ -3,7 +3,6 @@
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
|
||||
#include <helpers/BeaconFrames.h>
|
||||
#include <helpers/radiolib/RadioPowerLimits.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
@@ -12,7 +11,7 @@ bool Mesh::validateTextBeacon(const BeaconSlot& slot) const {
|
||||
if (!beacon::validText(slot.text) || slot.hops > 7
|
||||
|| !RadioProfiles::valid(slot.radio)
|
||||
|| !RadioProfiles::safePreamble(slot.radio, slot.radio.preamble ? slot.radio.preamble : 32)
|
||||
|| slot.power < -9 || !isLoRaTxPowerValid(slot.power, slot.radio.freq)
|
||||
|| !_radio->validateBeaconPower(slot.radio.freq, slot.power)
|
||||
|| !_radio->validateProfile(slot.radio)) return false;
|
||||
if (slot.radio.protocol == RadioProfileProtocol::MeshCore) return true;
|
||||
#if MESH_ENABLE_MESHTEXT_WIRE
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <Mesh.h>
|
||||
#include <helpers/FilePresence.h>
|
||||
#include <helpers/MeshtextChat.h>
|
||||
#include <helpers/UTF8Helpers.h>
|
||||
#include <Arduino.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
@@ -75,12 +76,15 @@ bool decodeSlot(const uint8_t bytes[RecordSize], BeaconSlot& slot) {
|
||||
slot.power = int8_t(bytes[13]); slot.preset = bytes[14]; slot.hops = bytes[15];
|
||||
slot.enabled = true;
|
||||
memcpy(slot.text, bytes + 17, BeaconSlot::TextCapacity);
|
||||
const size_t text_length = strlen(slot.text);
|
||||
// Reject noncanonical trailing data instead of carrying hidden text bytes.
|
||||
for (size_t i = strlen(slot.text) + 1; i < BeaconSlot::TextCapacity; ++i)
|
||||
for (size_t i = text_length + 1; i < BeaconSlot::TextCapacity; ++i)
|
||||
if (slot.text[i]) return false;
|
||||
for (const unsigned char* c = reinterpret_cast<const unsigned char*>(slot.text); *c; ++c)
|
||||
if (*c < 32 || *c == 127) return false;
|
||||
return isfinite(slot.radio.freq) && isfinite(slot.radio.bw)
|
||||
return validUtf8PrefixLength(slot.text, text_length) == text_length
|
||||
&& RadioProfiles::valid(slot.radio)
|
||||
&& RadioProfiles::safePreamble(slot.radio, slot.radio.preamble ? slot.radio.preamble : 32)
|
||||
&& (slot.radio.protocol != RadioProfileProtocol::Meshtext
|
||||
|| (slot.preset <= uint8_t(mt::Preset::LongSlow) && slot.radio.preamble == 16));
|
||||
}
|
||||
@@ -218,7 +222,9 @@ BeaconManager::ReadResult BeaconManager::readConfig(
|
||||
}
|
||||
crc = crcUpdate(crc, bytes, sizeof(bytes));
|
||||
BeaconSlot slot;
|
||||
if (!decodeSlot(bytes, slot) || (slot.enabled && (!mesh_ || !mesh_->validateTextBeacon(slot)))
|
||||
// Disk validity is independent of the current board's radio capabilities.
|
||||
// Preserve unsupported slots so an upgrade can still query/repair them.
|
||||
if (!decodeSlot(bytes, slot)
|
||||
|| (slot.enabled && slot.radio.protocol == RadioProfileProtocol::Meshtext && !header[6]))
|
||||
valid = false;
|
||||
if (output) output[i] = slot;
|
||||
@@ -403,7 +409,9 @@ void BeaconManager::loop(uint32_t now) {
|
||||
if (hour > 0 && handled_hour_ != hour) {
|
||||
const unsigned index = unsigned((hour - 1) % SlotCount);
|
||||
BeaconSlot& slot = slots_[index];
|
||||
if (!slot.enabled) handled_hour_ = hour;
|
||||
// A retained slot may be unsupported by this board or firmware. Skip it
|
||||
// before allocating a durable Meshtext packet ID or entering the TX queue.
|
||||
if (!slot.enabled || !mesh_->validateTextBeacon(slot)) handled_hour_ = hour;
|
||||
else {
|
||||
uint64_t guard = last_send_ && elapsed_ - last_send_ < HourMillis
|
||||
? HourMillis - (elapsed_ - last_send_) : 0;
|
||||
|
||||
@@ -139,6 +139,12 @@ public:
|
||||
}
|
||||
|
||||
protected:
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
bool checkBeaconOutputPower(float frequency_mhz, int8_t dbm) const override {
|
||||
(void)frequency_mhz;
|
||||
return dbm >= _min_dbm && dbm <= _max_dbm;
|
||||
}
|
||||
#endif
|
||||
// Emit a gain-control code. Override for a PA driven by PWM or an external
|
||||
// DAC instead of the MCU's own.
|
||||
virtual void writeGainControl(uint8_t code) {
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#include "RadioLibWrappers.h"
|
||||
#include "RadioInterruptEvents.h"
|
||||
#include "RadioProfileChip.h"
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
#include "RadioPowerLimits.h"
|
||||
#endif
|
||||
#include <new>
|
||||
#if defined(ESP32_PLATFORM)
|
||||
#include <esp_heap_caps.h>
|
||||
@@ -559,6 +562,39 @@ mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) {
|
||||
}
|
||||
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
bool RadioLibWrapper::validateBeaconPower(float frequency_mhz, int8_t power) const {
|
||||
return isfinite(frequency_mhz) && mesh::isLoRaTxPowerValid(power, frequency_mhz)
|
||||
&& checkBeaconOutputPower(frequency_mhz, power);
|
||||
}
|
||||
|
||||
bool RadioLibWrapper::checkBeaconOutputPower(float frequency_mhz, int8_t power) const {
|
||||
(void)frequency_mhz;
|
||||
#if MESH_RADIO_PROFILE_CHIP == 1276
|
||||
// SX127x's setter automatically selects RFO below 2 dBm. Its two-argument
|
||||
// checker assumes PA_BOOST, so use the same selection as setOutputPower().
|
||||
return static_cast<SX1276*>(_radio)->checkOutputPower(power, nullptr, power < 2)
|
||||
== RADIOLIB_ERR_NONE;
|
||||
#elif MESH_RADIO_PROFILE_CHIP == 1272
|
||||
return static_cast<SX1272*>(_radio)->checkOutputPower(power, nullptr, power < 2)
|
||||
== RADIOLIB_ERR_NONE;
|
||||
#elif MESH_RADIO_PROFILE_CHIP == 1121
|
||||
// RadioLib's checker uses its current band. Validate the prospective band
|
||||
// without changing the cached frequency or triggering a hardware retune.
|
||||
if (frequency_mhz > 1000.0f) 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 mesh::lr2021::isHighBand(frequency_mhz)
|
||||
? power >= -19 && power <= 12 : power >= -9 && power <= 22;
|
||||
#else
|
||||
// These RadioLib checkers inspect configuration only; no SPI is performed.
|
||||
return _radio->checkOutputPower(power, nullptr) == RADIOLIB_ERR_NONE;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t RadioLibWrapper::getBeaconAirtime(const mesh::RadioProfileParams& p, int length) {
|
||||
if (length <= 0 || length > MAX_TRANS_UNIT || !validateProfile(p)) return 0;
|
||||
const uint16_t preamble = p.protocol == mesh::RadioProfileProtocol::Meshtext
|
||||
@@ -596,7 +632,8 @@ mesh::RadioParamApplyResult RadioLibWrapper::prepareBeaconTransmit(
|
||||
_hil_beacon_tx_frequency_mhz = 0;
|
||||
if (!mesh::hilTransmitFrequencyAllowed(p.freq)) return Result::FAILED;
|
||||
#endif
|
||||
if (!_params_valid || !_dbm_valid || !getBeaconAirtime(p, MAX_TRANS_UNIT)) return Result::FAILED;
|
||||
if (!_params_valid || !_dbm_valid || !getBeaconAirtime(p, MAX_TRANS_UNIT)
|
||||
|| !validateBeaconPower(p.freq, power)) return Result::FAILED;
|
||||
if (isCalibratingNoiseFloor()) return Result::BUSY;
|
||||
|
||||
beginProfileRetune(isInRecvMode() && !_rx_ps_armed && !_rx_ps_enabled);
|
||||
|
||||
@@ -272,6 +272,11 @@ protected:
|
||||
virtual int16_t applyCachedTxPower(int8_t dbm) {
|
||||
return _radio->setOutputPower(dbm);
|
||||
}
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
// External amplifiers override this in antenna dBm. The public capability
|
||||
// still checks the board's explicit limits before consulting this hook.
|
||||
virtual bool checkBeaconOutputPower(float frequency_mhz, int8_t dbm) const;
|
||||
#endif
|
||||
virtual bool applyRxBoostedGainMode(bool) { return false; }
|
||||
virtual int16_t enterCarrierWave() { return _radio->transmitDirect(); }
|
||||
virtual bool validateMorseFrequency(float frequency_mhz) const {
|
||||
@@ -409,6 +414,7 @@ public:
|
||||
mesh::RadioParamApplyResult prepareTransmitProfile(uint8_t profile) override;
|
||||
mesh::RadioParamApplyResult prepareTransmitProfile(uint8_t profile, bool reply_rx_override) override;
|
||||
#if MESH_ENABLE_TX_BEACONS
|
||||
bool validateBeaconPower(float frequency_mhz, int8_t power) const override;
|
||||
mesh::RadioParamApplyResult prepareBeaconTransmit(const mesh::RadioProfileParams& params,
|
||||
int8_t power) override;
|
||||
mesh::RadioParamApplyResult restoreBeaconReceive() override;
|
||||
|
||||
+57
@@ -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();
|
||||
|
||||
Vendored
+90
@@ -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@
|
||||
Vendored
+251
@@ -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");
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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()",
|
||||
|
||||
Reference in new issue
Block a user