Files
MeshCore/test/test_radio_profile_scan.py
T

482 lines
23 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/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 STATE_INT_READY 16
#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;
static uint8_t state;
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 {
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_INT_READY) == STATE_RX; }
void beginProfileRetune(bool) {}
void endProfileRetune(bool) {}
bool isReceivingPacket() { return packet; }
bool isPacketPendingOrReceiving() { return packet || (state & STATE_INT_READY); }
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) state |= STATE_INT_READY;
if (blocked==4) 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 && state==STATE_RX);
assert(w._profile_refresh_required==!success);
}
{
RadioLibWrapper w; state=STATE_IDLE;
assert(w.tryRestoreCodingRate(5)==Result::APPLIED);
assert(w.coding_writes==1 && state==STATE_IDLE && !w._profile_refresh_required);
}
{
RadioLibWrapper w; w.failRxStarts=1;
assert(w.tryRestoreCodingRate(5)==Result::FAILED);
assert(w.coding_writes==1 && 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) state=STATE_RX|STATE_INT_READY;
if (reason==3) state=STATE_TX_WAIT;
w.enable();
assert(!w._profile_standby_held && !w.chip.standbyXOSC && w._rx_ps_enabled);
w.busy=w.packet=false; 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()