Files
MeshCore/test/test_radio_profile_scan.py
mikecarper ddf625e9d0 Fix packet, delivery, clock and radio stability regressions
Reject truncated anonymous reply paths and advert metadata before copying,
while preserving timestamp-only empty-password logins. Limit room posting to
read-write/admin ACL roles, retain inbox frames until the requesting transport
accepts them, and lock raw LittleFS traversal against concurrent writes.

Separate ISR event generations from main-loop radio state. Confirm RX/TX IRQs,
detach concrete wrapper callbacks safely, and discard inconsistent LR2021 FIFO
snapshots before rearming. Preserve bridge RX frames through packet-pool
exhaustion and bound ESP-NOW retries and missing-callback recovery. Screen RTC
register layouts without losing DS1307 startup, and ignore poisoned future
contact timestamps during clock bootstrap.

Keep the nRF52 loop stack at 8 KiB while reducing the OTA listing call chain
from 7,832 to 6,056 static bytes. Retain all target names through lossless Full
Companion compression and the canonical size optimizer. Preserve constrained
STM32 filesystems and bind corrected ESP32 UART/OLED budgets to source size
assertions. Document the separately measured preexisting W12 package limits.

Adapted for this branch from meshcore-dev/MeshCore PRs #3521, #3340, #3362,
#2062, #3503, #3038, #3534, #3544 and #3512.

Validation: 1,829 native tests; sanitized parser, inbox, LittleFS, ACL, clock,
RTC, IRQ/FIFO and bridge fault tests; 332 additional Companion cases; real
RAK4631 Full and both repeater profiles, G2 Full/standard, V3 Full and both
constrained STM32 release recipes. Radio-family compile coverage continues
separately; W12 full/portable package guards remain enforced.
2026-10-09 19:56:08 -07:00

485 lines
24 KiB
Python

"""Exercise production profile transitions with a controllable physical radio."""
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <cassert>
#include <initializer_list>
#include <RadioProfiles.h>
#include <helpers/radiolib/RadioInterruptEvents.h>
#include <helpers/radiolib/RXPowerSaving.h>
#include <helpers/radiolib/NoiseFloorEstimator.h>
#define RADIOLIB_ERR_NONE 0
#define STATE_IDLE 0
#define STATE_RX 1
#define STATE_TX_WAIT 3
#define NF_CALIB_SETTLE_MS 7UL
#define MESH_DEBUG_PRINTLN(...) ((void)0)
namespace mesh { enum class RadioParamApplyResult { APPLIED, BUSY, FAILED }; }
static uint64_t elapsed_us;
uint32_t micros() { return (uint32_t)elapsed_us; }
static bool profile_clock_active=false;
void syncProfileClock(bool dual_profile) { profile_clock_active=dual_profile; }
uint32_t profileTimestamp(bool) { return micros(); }
uint32_t profileElapsedUs(uint32_t since, uint32_t now, bool) { return now - since; }
uint32_t millis() { return (uint32_t)(elapsed_us / 1000); }
struct Chip { bool standbyXOSC=false; int standby() { return 0; } };
using CustomSX1262 = Chip;
struct RadioLibWrapper {
uint8_t state = STATE_RX;
RadioInterruptEvents events;
bool hasPendingRadioInterrupt() const { return events.pending(); }
bool claimRadioInterrupt() { return events.claim(); }
bool _cw_active=false;
Chip chip; Chip* _radio = &chip;
mesh::RadioProfiles _profiles;
bool _params_valid=true, packet=false, busy=false, fail=false;
bool _rx_ps_enabled=true, _rx_ps_armed=true, _rx_ps_continuous_fallback=false;
bool _profile_saved_rxps=false, _profile_rxps_suspended=false;
bool _profile_standby_held=false, _saved_standby_xosc=false;
bool _nf_calib_active=false, _nf_refresh_requested=false, _noise_floor_valid=true, _noise_floor_secondary_valid=true, _profile_refresh_required=false;
unsigned long _nf_last_calib=0, _nf_calib_deadline=0, _nf_sample_from=0;
NoiseFloorEstimator _floor_estimator;
NoiseFloorEstimator _secondary_floor_estimator;
static constexpr uint32_t NoiseFloorSettleMillis=7UL;
NoiseFloorEstimator& profileFloorEstimator(uint8_t profile) {
return profile == 1 ? _secondary_floor_estimator : _floor_estimator;
}
uint32_t _rx_ps_rx_us=50000, _rx_ps_sleep_us=50000;
uint8_t _active_profile=0, _cur_sf=7, _cur_cr=5;
uint32_t _profile_generation=1, _profile_visit_stamp=0, _profile_retry_at=0;
uint32_t _profile_scan_generation[2]={};
uint16_t _physical_preamble=32;
float _cur_freq=909.5, _cur_bw=62.5;
unsigned applies=0, failRxStarts=0, switch_apply_us=1200;
unsigned coding_writes=0;
bool coding_success=true;
bool setCodingRate(uint8_t) { ++coding_writes; return coding_success; }
RadioLibWrapper() {
state=STATE_RX; elapsed_us=100000;
auto& p=_profiles.primary;
p.freq=_cur_freq; p.bw=_cur_bw; p.sf=_cur_sf; p.cr=_cur_cr;
_profile_visit_stamp=profileTimestamp(_profiles.enabled());
}
bool isChipBusy() { return busy; }
bool isInRecvMode() const { return state == STATE_RX; }
void beginProfileRetune(bool) {}
void endProfileRetune(bool) {}
bool isReceivingPacket() { return packet; }
bool isPacketPendingOrReceiving() { return packet || hasPendingRadioInterrupt(); }
bool supportsRxPowerSaving() { return true; }
void startRecv() {
if (failRxStarts) { --failRxStarts; state=STATE_IDLE; return; }
if (_profile_rxps_suspended) assert(chip.standbyXOSC && !_rx_ps_enabled);
state=STATE_RX; _profile_visit_stamp=profileTimestamp(_profiles.enabled()); _rx_ps_armed=_rx_ps_enabled;
}
void stopReceiveDutyCycle() { _rx_ps_armed=false; }
bool applyParams(float f,float,uint8_t,uint8_t) { ++applies; elapsed_us+=switch_apply_us; return !fail || f==909.5f; }
void cacheParams(float f,float b,uint8_t s,uint8_t c) { _cur_freq=f;_cur_bw=b;_cur_sf=s;_cur_cr=c; }
bool restoreAfterDeepInit() { return true; }
void recalibrateNoiseFloor() { _noise_floor_valid=false; }
bool validateProfile(const mesh::RadioProfileParams& p) const { return mesh::RadioProfiles::valid(p); }
uint16_t profilePreamble(uint8_t n) const { return _profiles.preamble(n,32); }
uint8_t beginReconfigure();
void endReconfigure(bool);
void setProfileStandbyWarm(bool);
void serviceProfileScan();
mesh::RadioParamApplyResult tuneProfile(uint8_t);
mesh::RadioParamApplyResult prepareTransmitProfile(uint8_t);
mesh::RadioParamApplyResult prepareTransmitProfile(uint8_t, bool);
mesh::RadioParamApplyResult tryRestoreCodingRate(uint8_t);
mesh::RadioParamApplyResult trySetParams(float,float,uint8_t,uint8_t,const uint32_t* = nullptr);
mesh::RadioParamApplyResult trySetPrimaryParams(const mesh::RadioProfileParams&,bool,const uint32_t* = nullptr);
void enable() {
mesh::RadioProfileConfig second;
second.params=_profiles.primary;second.params.freq=910.5;second.params.bw=500;
second.mode=mesh::RadioProfileMode::RxTx;_profiles.setSecondary(second,false);
serviceProfileScan();
}
};
@METHODS@
int main() {
using Result=mesh::RadioParamApplyResult;
{
RadioLibWrapper w;
w.enable();
assert(profile_clock_active);
w._cw_active=true;
w._profiles.setSecondary({},false);
w.serviceProfileScan();
assert(!profile_clock_active); // even a carrier-wave early return stops RTC2
}
{
RadioLibWrapper w; w.enable();
assert(w._profiles.switchTestReady());
assert(w._profiles.switch_test_samples[0]==4 && w._profiles.switch_test_samples[1]==4);
assert(w._profiles.switchBudgetUs()==1320); // measured 1200 us plus 10%, no nominal floor
assert(w._profiles.preamble(0,32)>=32 && w._profiles.preamble(0,32)<=128);
assert(w._profiles.preamble(1,32)>=32 && w._profiles.preamble(1,32)<=128);
assert(w._profiles.preamble(0,32)%8==0 && w._profiles.preamble(1,32)%8==0);
}
{
RadioLibWrapper w; w._profiles.primary.bw=w._cur_bw=500; w.switch_apply_us=8428;
w.enable();
assert(w._profiles.switchTestReady() && w._profiles.switchBudgetUs()==9271);
assert(w._profiles.preamble(0,32)==128 && w._profiles.preamble(1,32)<=128);
assert(w._profiles.chirpTiming().preamble[0]>128);
}
{
RadioLibWrapper w; w.packet=true; w.enable();
assert(!w._profiles.switchTestReady() && w._profiles.switch_test_samples[0]==0);
w.packet=false; w.serviceProfileScan(); assert(w._profiles.switchTestReady());
w._profiles.setSecondary({},false);
assert(!w._profiles.switchTestReady());
}
{
RadioLibWrapper w; w.fail=true; w.enable();
assert(w._profiles.switch_failures==1 && w._profiles.switch_test_samples[0]==0);
w.fail=false; elapsed_us+=1000000; w.serviceProfileScan();
assert(w._profiles.switchTestReady());
}
for (unsigned blocked=0; blocked<5; ++blocked) {
RadioLibWrapper w;
w._cw_active = blocked==0; w.busy = blocked==1; w.packet = blocked==2;
if (blocked==3) w.events.record();
if (blocked==4) w.state = STATE_TX_WAIT;
assert(w.tryRestoreCodingRate(5)==Result::BUSY && w.coding_writes==0);
}
for (bool success : {false,true}) {
RadioLibWrapper w; w.coding_success=success;
assert(w.tryRestoreCodingRate(5)==(success?Result::APPLIED:Result::FAILED));
assert(w.coding_writes==1 && w.state==STATE_RX);
assert(w._profile_refresh_required==!success);
}
{
RadioLibWrapper w; w.state=STATE_IDLE;
assert(w.tryRestoreCodingRate(5)==Result::APPLIED);
assert(w.coding_writes==1 && w.state==STATE_IDLE && !w._profile_refresh_required);
}
{
RadioLibWrapper w; w.failRxStarts=1;
assert(w.tryRestoreCodingRate(5)==Result::FAILED);
assert(w.coding_writes==1 && w.state==STATE_IDLE && w._profile_refresh_required);
// The normal receive re-arm and profile refresh can recover after a
// transient restart failure; a later coding-rate restore then succeeds.
w.startRecv(); w.serviceProfileScan();
assert(w.isInRecvMode() && !w._profile_refresh_required);
assert(w.tryRestoreCodingRate(5)==Result::APPLIED);
assert(w.coding_writes==2 && w.isInRecvMode() && !w._profile_refresh_required);
}
{
// Force only admits a reply on an active RX-only profile. It cannot
// enable a disabled profile or erase an in-progress reception.
RadioLibWrapper w; w.enable();
w._profiles.secondary.mode=mesh::RadioProfileMode::Rx;
assert(w.prepareTransmitProfile(1)==Result::FAILED);
assert(w.prepareTransmitProfile(1,false)==Result::FAILED);
w.packet=true;
assert(w.prepareTransmitProfile(1,true)==Result::BUSY);
w.packet=false; w.busy=true;
assert(w.prepareTransmitProfile(1,true)==Result::BUSY);
w.busy=false;
assert(w.prepareTransmitProfile(1,true)==Result::APPLIED);
assert(w._active_profile==1 && w._profiles.secondary.mode==mesh::RadioProfileMode::Rx);
assert(w.prepareTransmitProfile(1)==Result::FAILED);
w._profiles.setSecondary({},false);
assert(w.prepareTransmitProfile(1,true)==Result::FAILED);
assert(w.prepareTransmitProfile(2,true)==Result::FAILED);
}
{
// Applying the tuple is not success if RX resume failed. Roll back the
// profile and caches, and request recovery if rollback RX also fails.
for (unsigned failures: {1u,2u}) {
RadioLibWrapper w; w.enable(); w.failRxStarts=failures;
const auto switches=w._profiles.switches;
const auto generation=w._profile_generation;
assert(w.tuneProfile(1)==Result::FAILED);
assert(w._active_profile==0 && w._cur_freq==909.5f && w._cur_bw==62.5f);
assert(w._profile_generation==generation && w._profiles.switches==switches);
assert(w._profiles.switch_failures==1);
assert(w.isInRecvMode()==(failures==1));
assert(w._profile_refresh_required==(failures==2));
}
}
{
// A primary command must not publish a new profile if restarting RX
// fails. Restore the active tuple (which may be radio2) and RXPS policy.
for (unsigned initial=0; initial<4; ++initial) {
const bool secondary=(initial & 1), powersaving=(initial & 2);
for (bool primary_api: {false,true}) {
for (unsigned failures: {1u,2u}) {
RadioLibWrapper w;
w._rx_ps_enabled=w._rx_ps_armed=powersaving;
if (secondary) { w.enable(); assert(w.tuneProfile(1)==Result::APPLIED); }
const auto primary=w._profiles.primary;
const auto generation=w._profiles.generation[0];
const auto active=w._active_profile;
const auto physical_generation=w._profile_generation;
const auto freq=w._cur_freq, bw=w._cur_bw;
const auto preamble=w._physical_preamble;
const auto rxps=w._rx_ps_enabled, saved_rxps=w._profile_saved_rxps;
const auto rx_us=w._rx_ps_rx_us, sleep_us=w._rx_ps_sleep_us;
const auto fallback=w._rx_ps_continuous_fallback;
auto requested=primary; requested.freq=910.25; requested.preamble=64;
const uint32_t timings[]={100000,200000};
w.failRxStarts=failures;
const auto result=primary_api ? w.trySetPrimaryParams(requested,true,timings)
: w.trySetParams(requested.freq,requested.bw,requested.sf,requested.cr,timings);
assert(result==Result::FAILED);
assert(w._profiles.primary==primary && !w._profiles.primary_temporary);
assert(w._profiles.generation[0]==generation && w._profile_generation==physical_generation);
assert(w._active_profile==active && w._cur_freq==freq && w._cur_bw==bw);
assert(w._physical_preamble==preamble);
assert(w._rx_ps_enabled==rxps && w._profile_saved_rxps==saved_rxps);
assert(w._rx_ps_rx_us==rx_us && w._rx_ps_sleep_us==sleep_us);
assert(w._rx_ps_continuous_fallback==fallback);
assert(w.isInRecvMode()==(failures==1));
assert(w._profile_refresh_required==(failures==2));
if (!w.isInRecvMode()) w.startRecv(); // normal receive recovery
assert(w.trySetPrimaryParams(requested,true,timings)==Result::APPLIED);
assert(w._profiles.primary==requested && w._profiles.primary_temporary);
assert(w._profiles.generation[0]==generation+1);
assert(w._profile_generation==generation+1 && w._active_profile==0);
assert(w.isInRecvMode() && !w._profile_refresh_required);
assert(w._rx_ps_rx_us==timings[0] && w._rx_ps_sleep_us==timings[1]);
assert(secondary ? w._profile_saved_rxps : w._rx_ps_enabled);
}
}
}
}
{
RadioLibWrapper w; w.enable();
w._nf_refresh_requested=false; // exercise the ordinary, post-calibration cadence
assert(w._profile_rxps_suspended && !w._rx_ps_enabled && !w._rx_ps_armed);
assert(w.chip.standbyXOSC && w._profile_standby_held && !w._saved_standby_xosc);
w.setProfileStandbyWarm(true); // repeated requests must not overwrite the saved RC policy
elapsed_us+=w._profiles.listenUs(0)-1;w.serviceProfileScan();assert(w._active_profile==0);
elapsed_us++;w.serviceProfileScan();assert(w._active_profile==1);
elapsed_us+=w._profiles.listenUs(1)-1;w.serviceProfileScan();assert(w._active_profile==1);
elapsed_us++;w.serviceProfileScan();assert(w._active_profile==0);
elapsed_us+=w._profiles.listenUs(0);w.serviceProfileScan();assert(w._active_profile==1);
const auto generation=w._profile_generation;
w.packet=true;elapsed_us+=100000;w.serviceProfileScan();assert(w._active_profile==1);
assert(w.prepareTransmitProfile(0)==Result::BUSY);
w._profiles.setSecondary({},false);w.serviceProfileScan();
assert(w._active_profile==1 && w._profile_generation==generation);
assert(w.chip.standbyXOSC); // cannot change oscillator policy during a packet
w.packet=false;w.serviceProfileScan();
assert(w._active_profile==0 && w._rx_ps_enabled && !w._profile_rxps_suspended);
assert(w._rx_ps_armed);
assert(!w.chip.standbyXOSC && !w._profile_standby_held);
w.serviceProfileScan(); assert(!w._profile_refresh_required);
}
{
RadioLibWrapper w;
w._profiles.primary.bw=500; w._cur_bw=500;
w.enable();
w._profiles.secondary.params.bw=62.5;
++w._profiles.generation[1];
w.serviceProfileScan();assert(w._active_profile==1); // slower channel first
w._nf_refresh_requested=false; // ordinary cadence after the replacement baseline
elapsed_us+=w._profiles.listenUs(1)-1;w.serviceProfileScan();assert(w._active_profile==1);
elapsed_us++;w.serviceProfileScan();assert(w._active_profile==0);
}
{
// SX126x profiles need at most a 7ms settle. A shorter visit is extended
// only to that bound, never to the former 20ms calibration bound.
RadioLibWrapper w;
w._nf_refresh_requested=true;
w.enable();
const auto primary_visit=w._profiles.listenUs(0);
const auto calibrated_visit=primary_visit < 7000 ? 7000 : primary_visit;
elapsed_us += calibrated_visit - 1; w.serviceProfileScan(); assert(w._active_profile==0);
elapsed_us++; w.serviceProfileScan(); assert(w._active_profile==1);
}
{
// Between spaced noise samples, even a very short profile must keep its
// normal visit. Calibration may not turn every fast scan hop into 7ms.
RadioLibWrapper w;
w._nf_refresh_requested=true;
w.enable();
w._floor_estimator.add(-100, millis());
w._active_profile=0;
w._profile_visit_stamp=profileTimestamp(w._profiles.enabled());
const auto primary_visit=w._profiles.listenUs(0);
elapsed_us += primary_visit - 1; w.serviceProfileScan(); assert(w._active_profile==0);
elapsed_us++; w.serviceProfileScan(); assert(w._active_profile==1);
}
{
RadioLibWrapper w;w.enable();w.fail=true;
elapsed_us+=100000;w.serviceProfileScan();
assert(w._profiles.switch_failures==1 && w._active_profile==0);
assert(w.chip.standbyXOSC && w._profile_standby_held);
const auto applies=w.applies;
for (int i=0;i<10;++i) w.serviceProfileScan();
assert(w.applies==applies); // failed target is not hammered every loop
elapsed_us+=1001000;w.fail=false;w.serviceProfileScan();assert(w._active_profile==1);
}
{
RadioLibWrapper w;
// Zero is the inactive retry timer even after 24.8 days of uptime.
elapsed_us=uint64_t(0xf0000000UL)*1000;w.enable();
assert(w._profile_rxps_suspended);
elapsed_us+=100000;w.serviceProfileScan();assert(w._profiles.switches>=1);
}
{
RadioLibWrapper w;auto primary=w._profiles.primary;
auto temp=primary;temp.freq=910.5;temp.preamble=64;
w.packet=true;
assert(w.trySetPrimaryParams(temp,true)==Result::BUSY);
assert(w._profiles.primary==primary && !w._profiles.primary_temporary);
w.packet=false;w.fail=true;
assert(w.trySetPrimaryParams(temp,true)==Result::FAILED);
assert(w._profiles.primary==primary && w._physical_preamble==32);
w.fail=false;
assert(w.trySetPrimaryParams(temp,true)==Result::APPLIED);
assert(w._profiles.primary==temp && w._physical_preamble==64);
assert(w._profile_generation==w._profiles.generation[0]);
}
{
RadioLibWrapper w; w.chip.standbyXOSC=true; w._rx_ps_enabled=false; w._rx_ps_armed=false;
w.enable(); assert(w._saved_standby_xosc);
w._profiles.setSecondary({},false); w.serviceProfileScan();
assert(w.chip.standbyXOSC && !w._profile_standby_held && !w._rx_ps_enabled);
w.setProfileStandbyWarm(false); assert(w.chip.standbyXOSC);
}
for (bool preamble_only : {false,true}) {
RadioLibWrapper w; w.enable();
const auto original=w._profiles;
auto requested=original.primary;
if (preamble_only) requested.preamble=64;
assert(w.trySetPrimaryParams(requested,!preamble_only)==Result::APPLIED);
assert(w._profiles.primary==requested && w._profiles.primary_temporary==!preamble_only);
assert(w._profiles.generation[0]==original.generation[0]+1);
assert(!w._profiles.switchTestReady() && w._profiles.switchBudgetUs()==0);
assert(w._profiles.switch_test_samples[0]==0 && w._profiles.switch_test_samples[1]==0);
w.serviceProfileScan(); assert(w._profiles.switchTestReady());
}
{
RadioLibWrapper w; w.enable();
const auto original=w._profiles;
assert(w.trySetPrimaryParams(original.primary,false)==Result::APPLIED);
assert(w._profiles.generation[0]==original.generation[0]);
assert(w._profiles.switchTestReady() && w._profiles.switchBudgetUs()==original.switchBudgetUs());
for (unsigned i=0;i<2;++i) {
assert(w._profiles.switch_test_samples[i]==original.switch_test_samples[i]);
assert(w._profiles.switch_test_max_us[i]==original.switch_test_max_us[i]);
}
}
for (bool preamble_only : {false,true}) for (bool busy : {false,true}) {
RadioLibWrapper w; w.enable();
const auto original=w._profiles;
const auto original_preamble=w._physical_preamble;
auto requested=original.primary;
if (preamble_only) requested.preamble=64;
w.packet=busy;
if (!busy) w.failRxStarts=1;
assert(w.trySetPrimaryParams(requested,!preamble_only)==(busy?Result::BUSY:Result::FAILED));
assert(w._profiles.primary==original.primary && !w._profiles.primary_temporary);
assert(w._profiles.generation[0]==original.generation[0]);
assert(w._physical_preamble==original_preamble);
assert(w._profiles.switchTestReady() && w._profiles.switchBudgetUs()==original.switchBudgetUs());
for (unsigned i=0;i<2;++i) {
assert(w._profiles.switch_test_samples[i]==original.switch_test_samples[i]);
assert(w._profiles.switch_test_max_us[i]==original.switch_test_max_us[i]);
}
}
for (int reason=0;reason<4;++reason) {
RadioLibWrapper w;
if (reason==0) w.busy=true;
if (reason==1) w.packet=true;
if (reason==2) { w.state=STATE_RX; w.events.record(); }
if (reason==3) w.state=STATE_TX_WAIT;
w.enable();
assert(!w._profile_standby_held && !w.chip.standbyXOSC && w._rx_ps_enabled);
w.busy=w.packet=false; w.events.claim(); w.state=STATE_RX; w.serviceProfileScan();
assert(w.chip.standbyXOSC && w._profile_rxps_suspended);
w._profiles.setSecondary({},false); w.busy=true; w.serviceProfileScan();
assert(w.chip.standbyXOSC && w._profile_rxps_suspended);
w.busy=false; w.serviceProfileScan();
assert(!w.chip.standbyXOSC && !w._profile_rxps_suspended && w._rx_ps_enabled);
}
}
'''
class ProfileScanTest(unittest.TestCase):
def test_production_scan_and_transitions(self):
source=(ROOT/'src/helpers/radiolib/RadioLibWrappers.cpp').read_text()
names=[('uint8_t','beginReconfigure'),('void','endReconfigure'),
('void','serviceProfileScan')]+[('mesh::RadioParamApplyResult',name) for name in
['tuneProfile','prepareTransmitProfile','tryRestoreCodingRate','trySetParams','trySetPrimaryParams']]
methods='\n'.join(method(source,f'{kind} RadioLibWrapper::{name}(') for kind,name in names)
methods+='\n'+method(source,
'mesh::RadioParamApplyResult RadioLibWrapper::prepareTransmitProfile(uint8_t profile, bool')
wrapper=(ROOT/'src/helpers/radiolib/CustomSX1262Wrapper.h').read_text()
methods+='\n'+method(wrapper,'void setProfileStandbyWarm(').replace(
'void setProfileStandbyWarm(bool enabled) override',
'void RadioLibWrapper::setProfileStandbyWarm(bool enabled)')
with tempfile.TemporaryDirectory() as folder:
cpp=Path(folder)/'test.cpp'; exe=Path(folder)/'test.exe'
(Path(folder)/'Arduino.h').write_text('#pragma once\n#include <cstdint>\n#include <cmath>\n')
cpp.write_text(HARNESS.replace('@METHODS@',methods))
result=subprocess.run([os.environ.get('CXX','g++'),'-std=c++17','-Wall','-Wextra',
'-I',folder,'-I',str(ROOT/'src'),str(cpp),'-o',str(exe)],capture_output=True,text=True)
self.assertEqual(result.returncode,0,result.stderr)
result=subprocess.run([str(exe)],capture_output=True,text=True)
self.assertEqual(result.returncode,0,result.stderr)
def test_warm_oscillator_is_not_used_before_tcxo_reinitialization(self):
source=(ROOT/'src/helpers/radiolib/CustomSX1262.h').read_text()
begin=method(source,'int16_t begin(')
harness=r'''
#include <cassert>
#include <cstdint>
#define RADIOLIB_SX126X_SYNC_WORD_PRIVATE 0x12
#define RADIOLIB_ERR_NONE 0
struct SX1262 {
bool standbyXOSC=false;
int16_t beginResult=0, calibrationResult=0;
unsigned calibrations=0;
int16_t begin(float,float,uint8_t,uint8_t,uint8_t,int8_t,uint16_t,float,bool) {
assert(!standbyXOSC); // every init command must use RC until TCXO is configured
return beginResult;
}
};
struct CustomSX1262: SX1262 {
bool _coldStandby=false;
bool _tcxoWakePending=true;
struct { bool invalidated=false; void invalidate() { invalidated=true; } } _profileSwitch;
int16_t applyMeshCoreTcxoDelay() {
assert(!standbyXOSC); ++calibrations; return calibrationResult;
}
@BEGIN@
};
int main() {
for (bool warm: {false,true}) {
for (int failure=0; failure<3; ++failure) {
CustomSX1262 radio; radio.standbyXOSC=warm;
radio.beginResult=failure==1 ? -1 : 0;
radio.calibrationResult=failure==2 ? -2 : 0;
assert(radio.begin()==-failure);
assert(radio._profileSwitch.invalidated);
assert(radio._coldStandby);
assert(!radio._tcxoWakePending);
assert(radio.standbyXOSC==warm);
assert(radio.calibrations==(failure==1 ? 0u : 1u));
}
}
}
'''
with tempfile.TemporaryDirectory() as folder:
cpp=Path(folder)/'test.cpp'; exe=Path(folder)/'test.exe'
cpp.write_text('#include <initializer_list>\n'+harness.replace('@BEGIN@',begin))
result=subprocess.run([os.environ.get('CXX','g++'),'-std=c++17','-Wall','-Wextra',
str(cpp),'-o',str(exe)],capture_output=True,text=True)
self.assertEqual(result.returncode,0,result.stderr)
result=subprocess.run([str(exe)],capture_output=True,text=True)
self.assertEqual(result.returncode,0,result.stderr)
if __name__=='__main__': unittest.main()