Files
MeshCore/test/test_radio_receive_contract.py
T
mikecarper 73c5f572eb Apply busy radio settings and recover service starts automatically
Save valid busy RX, fast-switch and Companion tuple changes once, then
apply the latest accepted intent at a safe radio gap. Preserve checked-save
rollback, runtime RXEN intent, TempRadio ownership and exact remote reply
delivery. Repair failed preamble compensation without hiding uncertain saves.

Retry WebConfig, MQTT and NimBLE startup failures automatically. Preserve
explicit off choices, OTA ownership and the latest WiFi credentials through
cooperative shutdown. Add regression coverage and update command guides.

Validation: focused host transaction, rollback, ownership and lifecycle suites;
Full RAK4631, Heltec V4 R8 TFT and RC32 packages; representative room, sensor,
LR1110, LR2021, SX1276, STM32WL and ESP-NOW builds. Linked RAK crypto and OTA
workspace checks pass; its complete OTA image uses 712644/712704 bytes.
2026-10-25 15:20:55 -07:00

1051 lines
46 KiB
Python

#!/usr/bin/env python3
"""Execute production CAD, receive re-arm, calibration and mode-watchdog methods."""
from pathlib import Path
import os
import re
import subprocess
import sys
import tempfile
import unittest
from cpp_source import body
ROOT = Path(__file__).resolve().parents[1]
def method(text, signature):
"""Keep the shared legacy API without double-counting C++ alternatives."""
return body(text, signature)
HARNESS = r'''
#include <cassert>
#include <cstdio>
#include <cstdint>
#include <initializer_list>
#include <RadioProfiles.h>
#include <helpers/radiolib/RadioInterruptEvents.h>
#include <helpers/radiolib/NoiseFloorEstimator.h>
#include <helpers/radiolib/RXPowerSaving.h>
#include <helpers/TelemetryHistoryConfig.h>
#ifndef MESH_RECEIVE_HARNESS_HAS_DISPATCHER
namespace mesh { enum class RadioParamApplyResult : uint8_t { APPLIED, BUSY, FAILED }; }
#endif
#define STATE_IDLE 0
#define STATE_RX 1
#define STATE_TX_WAIT 3
#define RADIOLIB_ERR_NONE 0
#define RADIOLIB_CHANNEL_FREE -15
#define RADIOLIB_ERR_UNKNOWN -16
#define RADIOLIB_ERR_RX_TIMEOUT -6
#define MAX_TRANS_UNIT 255
#define MESH_DEBUG_PRINTLN(...) ((void)0)
@NF_CONSTANTS@
static uint32_t now_ms = 0;
uint32_t millis() { return now_ms; }
uint32_t micros() { return now_ms * 1000UL; }
// The dedicated profile-clock contract exercises the production clock helpers.
// This receive-state harness only needs their portable single-process behavior.
void syncProfileClock(bool) {}
uint32_t profileTimestamp(bool) { return micros(); }
void noInterrupts() {}
void interrupts() {}
void yield() { ++now_ms; }
struct Board {
unsigned n_cad_busy = 0, services = 0;
void serviceWatchdog() { ++services; }
};
struct Radio {
RadioInterruptEvents* events;
int scan_result = RADIOLIB_CHANNEL_FREE, start_result = 0;
unsigned scans = 0;
int startChannelScan() { ++scans; return start_result; }
int getChannelScanResult() {
if (scan_result != RADIOLIB_ERR_UNKNOWN) events->record();
return scan_result;
}
};
struct RadioLibWrapper {
@SETTLE_CONSTANT@
uint8_t state = STATE_RX;
RadioInterruptEvents events;
bool hasPendingRadioInterrupt() const { return events.pending(); }
bool claimRadioInterrupt() { return events.claim(); }
bool _cw_active = false;
bool _profile_repair_required = false;
bool serviceCarrierWave() { return false; } // dedicated CW harness owns this path
mesh::RadioProfiles _profiles;
uint32_t _profile_visit_stamp = 0;
uint32_t _profile_scan_generation[2] = {};
uint8_t _active_profile = 0;
bool _profile_rxps_suspended = false, _profile_saved_rxps = false;
void serviceProfileScan() {} // separate profile-scan harness exercises tuning
Board board; Board* _board = &board;
Radio radio{&events}; Radio* _radio = &radio;
bool _rx_ps_enabled = false, _rx_ps_armed = false, _rx_ps_continuous_fallback = false;
bool _rx_hold_continuous = false;
bool _nf_calib_active = false, _nf_refresh_requested = true, _noise_floor_valid = true,
_noise_floor_secondary_valid = true;
bool _nf_window_armed = false, _nf_lazy_refresh = false, _nf_refresh_attempted = false;
uint32_t _nf_published_at[2] = {}, _nf_published_generation[2] = {};
uint8_t _nf_published_valid = 0;
bool _cad_enabled = true, packet = false, busy = false, inject_arm_irq = false;
bool _rx_boosted_gain_valid = false, _rx_boosted_gain_applied = false;
bool _cur_rx_boosted_gain = false, gain_apply_success = true, physical_gain = false;
bool _wd_last_busy = false;
uint8_t _startrx_fails = 0, _rx_mode_failures = 0;
uint32_t _rx_retry_at = 0;
uint16_t _rx_retry_delay_ms = 0;
bool _rx_retry_pending = false;
uint32_t _rx_mode_checked_at = 0, _rx_ps_rx_us = 50000, _rx_ps_sleep_us = 50000;
uint32_t _rx_ps_eff_rx_us = 50000, _rx_ps_eff_sleep_us = 50000;
int16_t _rx_ps_last_error = RADIOLIB_ERR_NONE;
RxPowerSavingArmRetryState _rx_ps_arm_retry;
unsigned long _nf_last_calib = 0, _nf_calib_deadline = 0, _nf_sample_from = 0;
unsigned long _wd_last_transition = 0, _wd_stuck_thresh = 0, _wd_observe_ms = 0;
int16_t _threshold = 0, _noise_floor = -105;
int32_t _noise_floor_centi_dbm = -10500, _noise_floor_secondary_centi_dbm = -10600;
int arm_result = 0, chip_mode = 1;
float rssi = -100;
unsigned arms = 0, stops = 0, soft = 0, hard = 0, reads = 0, mode_reads = 0;
uint32_t n_wd_soft = 0, n_wd_hard = 0;
unsigned calibration_arms = 0, gain_changes = 0, agc_resets = 0;
bool inject_mode_irq = false;
NoiseFloorEstimator _floor_estimator;
NoiseFloorEstimator _secondary_floor_estimator;
NoiseFloorEstimator& profileFloorEstimator(uint8_t profile) {
return profile == 1 ? _secondary_floor_estimator : _floor_estimator;
}
uint16_t profilePreamble(uint8_t profile) const { return _profiles.preamble(profile, 32); }
RadioLibWrapper() { state = STATE_RX; now_ms = 100; }
bool isChipBusy() { return busy; }
bool isReceivingPacket() { return packet; }
float getCurrentRSSI() { ++reads; return rssi; }
int8_t readReceiveMode() {
++mode_reads;
if (inject_mode_irq) events.record();
return chip_mode;
}
bool recoverRadio(bool use_hard) { use_hard ? ++hard : ++soft; return true; }
void rxPsWatchdogCheck() {}
unsigned long getEstAirtimeFor(int) { return 10; }
void stopReceiveDutyCycle() { ++stops; _rx_ps_armed = false; }
void doResetAGC() { ++agc_resets; }
bool applyRxBoostedGainMode(bool enabled) {
++gain_changes; physical_gain = enabled; return gain_apply_success;
}
uint8_t beginReconfigure() { return 0; } // counted SPI/RX boundaries are separate
void endReconfigure(bool) {}
bool supportsRxPowerSaving() const { return true; }
bool supportsRxBoostedGainMode() const { return true; }
int startReceiveMode() {
++arms;
if (_nf_calib_active) ++calibration_arms;
if (!arm_result) _rx_ps_armed = _rx_ps_enabled && !_rx_ps_continuous_fallback && !_nf_calib_active;
if (_rx_ps_armed) {
_rx_ps_eff_rx_us = _rx_ps_rx_us;
_rx_ps_eff_sleep_us = _rx_ps_sleep_us;
}
if (inject_arm_irq) events.record();
return arm_result;
}
int16_t performChannelScan() { return performChannelScanWithTimeout(2500); }
int16_t performChannelScanWithTimeout(unsigned long);
bool isPacketPendingOrReceiving();
bool isChannelActive();
bool isReceivingPassive(int);
bool setRxPowerSaving(bool, uint32_t, uint32_t);
mesh::RadioParamApplyResult trySetRxPowerSaving(bool, uint32_t, uint32_t);
bool setRxBoostedGainMode(bool);
mesh::RadioParamApplyResult trySetRxBoostedGainMode(bool);
void startRecv();
void checkReceiveMode(uint32_t);
void loop();
void triggerNoiseFloorCalibrate(int);
void requestRestartRecv();
void noiseFloorCalibCheck(unsigned long);
void endNoiseFloorCalib(unsigned long);
void requestNoiseFloorRefresh();
void requestPeriodicNoiseFloorRefresh();
void recalibrateNoiseFloor();
bool resetAGCIfIdle(bool refresh_floor = true);
bool tryResetAGC();
bool getNoiseFloorWakeDelay(uint32_t, uint32_t&) const;
void resetAGC();
@RECOVERY_GETTER@
};
@METHODS@
struct LegacyRadio {
unsigned resets = 0;
virtual void resetAGC() { ++resets; }
@LEGACY_TRY_RESET@
@LEGACY_RECOVERY_GETTER@
};
// Public Dispatcher/passive callers share the same bounded routine policy.
static void routineRequest(RadioLibWrapper& w, unsigned path, int margin = 7) {
if (path == 0) w.triggerNoiseFloorCalibrate(margin);
else if (path == 1) {
const bool active = w.isReceivingPassive(margin);
assert(active == (!w._noise_floor_valid || (margin > 0 && w.rssi - w._noise_floor >= margin)));
} else w.requestPeriodicNoiseFloorRefresh();
}
static void selectMode(RadioLibWrapper& w, unsigned mode) {
const uint32_t period = mode == 2 ? RX_POWERSAVING_CONTINUOUS_FALLBACK_US : 50000U;
assert(w.setRxPowerSaving(mode != 0, period, period));
assert(w._rx_ps_enabled == (mode != 0));
assert(w._rx_ps_continuous_fallback == (mode == 2));
}
static void periodicRefreshContracts() {
static_assert(NF_RXPS_REFRESH_INTERVAL_MS == 60000UL, "Routine RXPS refresh must rest one minute");
static_assert(NF_CALIB_INTERVAL_MS == 60000UL, "Failed routine refresh must rest one minute");
static_assert(NF_LAZY_REFRESH_INTERVAL_MS == 300000UL, "Successful floor refresh becomes lazy");
unsigned cases = 0;
for (unsigned path : {0U, 1U, 2U}) for (unsigned mode : {0U, 1U, 2U})
for (uint32_t last : {10000U, 0xfffffff0U}) for (bool valid : {false, true})
for (uint32_t age : {0U, 1U, 1999U, 2000U, 59999U, 60000U}) {
RadioLibWrapper w; selectMode(w, mode);
w._nf_refresh_requested = false; w._noise_floor_valid = valid;
w._nf_last_calib = last; w._threshold = 42;
now_ms = last + age;
routineRequest(w, path);
const uint32_t interval = mode == 1 ? NF_RXPS_REFRESH_INTERVAL_MS : NF_CALIB_INTERVAL_MS;
const bool expected = age >= interval;
assert(w._nf_refresh_requested == expected);
assert(w._threshold == (path == 0 ? 7 : 42)); // threshold changes never wait
assert(w.arms == 0 && w.stops == 0 && w.radio.scans == 0);
++cases;
}
// Invalid startup bypasses the cooldown. A failed, timestamped attempt does
// not: it remains conservatively busy without immediately reopening RX.
for (unsigned path : {0U, 1U, 2U}) {
RadioLibWrapper w; selectMode(w, 1);
w._noise_floor_valid = false; w._nf_last_calib = 0; w._nf_refresh_requested = false;
now_ms = 1; routineRequest(w, path); assert(w._nf_refresh_requested);
w._noise_floor_valid = true; w._nf_refresh_requested = false;
now_ms = NF_RXPS_REFRESH_INTERVAL_MS - 1; routineRequest(w, path);
assert(!w._nf_refresh_requested); // zero timestamp alone is not startup
++now_ms; routineRequest(w, path); assert(w._nf_refresh_requested);
++cases;
}
// Select the current receive mode, not the mode of the last completed batch.
for (unsigned path : {0U, 1U, 2U}) for (unsigned from : {0U, 1U, 2U})
for (unsigned to : {0U, 1U, 2U}) {
RadioLibWrapper w; selectMode(w, from);
w._nf_last_calib = 10000; w._nf_refresh_requested = false;
now_ms = 20000; selectMode(w, to); routineRequest(w, path);
assert(!w._nf_refresh_requested);
now_ms = 10000 + NF_RXPS_REFRESH_INTERVAL_MS - 1; routineRequest(w, path);
assert(!w._nf_refresh_requested);
++now_ms; routineRequest(w, path); assert(w._nf_refresh_requested);
++cases;
}
// A periodic request may join, but must not reset or extend, a partial block.
for (unsigned path : {0U, 1U, 2U}) {
RadioLibWrapper w; selectMode(w, 1); w._rx_ps_armed = true;
w._nf_last_calib = 100; w._nf_refresh_requested = false;
now_ms = 100 + NF_RXPS_REFRESH_INTERVAL_MS;
routineRequest(w, path); w.loop(); w.startRecv();
now_ms += NF_CALIB_SETTLE_MS; w.loop();
assert(w._nf_calib_active && w._floor_estimator.count() == 1);
assert(w._secondary_floor_estimator.add(-90, now_ms));
const auto deadline = w._nf_calib_deadline;
for (unsigned repeat = 0; repeat < 5; ++repeat) {
routineRequest(w, path, 9);
assert(w._nf_refresh_requested && w._floor_estimator.count() == 1);
assert(w._secondary_floor_estimator.count() == 1 && w._nf_calib_deadline == deadline);
}
++cases;
}
// Passive busy/packet/CW ownership still avoids SPI reads, CAD and refresh.
for (unsigned owned : {0U, 1U, 2U, 3U}) {
RadioLibWrapper w; selectMode(w, 1); w._nf_refresh_requested = false;
w._noise_floor_valid = false; now_ms = 100000;
if (owned == 0) w._cw_active = true;
if (owned == 1) w.busy = true;
if (owned == 2) w.packet = true;
if (owned == 3) w.events.record();
assert(w.isReceivingPassive(7) && !w._nf_refresh_requested);
assert(w.reads == 0 && w.arms == 0 && w.radio.scans == 0);
++cases;
}
{
RadioLibWrapper w; selectMode(w, 1); w._nf_refresh_requested = false;
w._nf_last_calib = now_ms;
assert(w.isReceivingPassive(3)); // published floor still gates interference
assert(!w.isReceivingPassive(6) && !w._nf_refresh_requested);
++cases;
}
{
RadioLibWrapper w;
mesh::RadioProfileConfig second;
second.params.freq = 910.5; second.params.bw = 62.5; second.params.sf = 7; second.params.cr = 5;
second.mode = mesh::RadioProfileMode::Rx;
w._profiles.primary = second.params; w._profiles.setSecondary(second, false);
w._noise_floor_valid = w._noise_floor_secondary_valid = false;
w._nf_last_calib = now_ms; w._nf_refresh_requested = false;
assert(!w.isReceivingPassive(7) && !w._nf_refresh_requested && w.reads == 0);
w.triggerNoiseFloorCalibrate(9); // dual initialization still bypasses the single-profile cooldown
assert(w._nf_refresh_requested && w._threshold == 9);
++cases;
}
// Forced gain/tuning and AGC remain prompt, independent of routine cooldown.
for (unsigned forced : {0U, 1U, 2U}) {
RadioLibWrapper w; selectMode(w, 1); w._rx_ps_armed = true;
now_ms = 100000; w._nf_last_calib = now_ms; w._nf_refresh_requested = false;
assert(w._floor_estimator.add(-30, now_ms));
assert(w._secondary_floor_estimator.add(-30, now_ms));
++now_ms;
if (forced == 0) w.recalibrateNoiseFloor();
if (forced == 1) assert(w.setRxBoostedGainMode(true));
if (forced == 2) w.resetAGC();
assert(w._nf_refresh_requested && w._nf_last_calib == 0);
assert(w._floor_estimator.count() == 0 && w._secondary_floor_estimator.count() == 0);
assert(w._noise_floor_valid == (forced == 2));
assert(w._noise_floor_centi_dbm == -10500);
assert(w.gain_changes == (forced == 1 ? 1U : 0U));
assert(w.agc_resets == (forced == 2 ? 1U : 0U));
// AGC already schedules the normal receive re-arm; calibration follows it.
if (w.state == STATE_IDLE) w.startRecv();
w.loop(); assert(w._nf_calib_active);
++cases;
}
// A failed initial block rests one minute; an accepted baseline rests five.
// Quiet and busy windows are exercised through the actual loop/re-arm path.
for (bool busy : {false, true}) for (unsigned path : {0U, 1U}) {
RadioLibWrapper w; selectMode(w, 1); w._rx_ps_armed = true;
w._noise_floor_valid = false; w._nf_refresh_requested = false; w.busy = busy;
const uint32_t start = now_ms;
// The passive busy guard correctly returns without a new request. Seed
// the initial startup demand via the actual Dispatcher method instead.
w.triggerNoiseFloorCalibrate(7);
for (; w._nf_last_calib == 0; ++now_ms) {
assert(uint32_t(now_ms - start) <= NF_CALIB_TIMEOUT_MS + 1);
if (uint32_t(now_ms - start) % NF_CALIB_INTERVAL_MS == 0) {
if (path == 0) w.triggerNoiseFloorCalibrate(7);
else routineRequest(w, path);
}
w.loop(); if (w.state == STATE_IDLE) w.startRecv();
}
const uint32_t finished = w._nf_last_calib;
assert(w.calibration_arms == 1 && w._rx_ps_armed && !w._nf_refresh_requested);
assert(w._noise_floor_valid == !busy);
if (busy) assert(w.reads == 0 && w._noise_floor_centi_dbm == -10500);
// A quiet passive caller reads RSSI, but those reads do not open windows.
const uint32_t interval = busy ? NF_RXPS_REFRESH_INTERVAL_MS : NF_LAZY_REFRESH_INTERVAL_MS;
for (; uint32_t(now_ms - finished) < interval; ++now_ms) {
if (uint32_t(now_ms - finished) % NF_CALIB_INTERVAL_MS == 0) {
if (path == 0) w.triggerNoiseFloorCalibrate(7);
else routineRequest(w, path);
}
w.loop(); if (w.state == STATE_IDLE) w.startRecv();
assert(w.calibration_arms == 1 && !w._nf_calib_active && !w._nf_refresh_requested);
}
// A timestamped failed attempt is not the invalid-startup exception.
w.busy = false; routineRequest(w, path); w.loop();
if (w.state == STATE_IDLE) w.startRecv();
assert(w._nf_calib_active && w.calibration_arms == 2);
++cases;
}
std::printf("PASS: routine noise-floor cooldown (%u cases)\n", cases);
}
static void agcAdmissionContracts() {
{
using Result = mesh::RadioParamApplyResult;
RadioLibWrapper w;
w._rx_boosted_gain_valid = w._rx_boosted_gain_applied = true;
w.gain_apply_success = false;
assert(w.trySetRxBoostedGainMode(true) == Result::FAILED);
assert(!w._rx_boosted_gain_applied && w._rx_boosted_gain_valid && !w._cur_rx_boosted_gain);
assert(w.physical_gain); // Failure deliberately models a partial chip write.
const auto failed_writes = w.gain_changes;
w.gain_apply_success = true;
assert(w.trySetRxBoostedGainMode(false) == Result::APPLIED);
assert(w._rx_boosted_gain_applied && w._rx_boosted_gain_valid && !w._cur_rx_boosted_gain);
assert(!w.physical_gain);
assert(w.gain_changes == failed_writes + 1);
const auto repaired_writes = w.gain_changes;
assert(w.trySetRxBoostedGainMode(false) == Result::APPLIED);
assert(w.gain_changes == repaired_writes); // Verified matching gain is a true no-op.
w._cw_active = true;
assert(w.trySetRxBoostedGainMode(false) == Result::BUSY);
assert(w.gain_changes == repaired_writes);
}
unsigned cases = 0;
for (unsigned mode : {0U, 1U, 2U}) for (unsigned owned : {0U, 1U, 2U, 3U, 4U}) {
if (mode == 1 && owned == 1) continue; // known RXPS sleep has an explicit wake path
RadioLibWrapper w; selectMode(w, mode);
w._rx_ps_armed = mode == 1;
w._rx_boosted_gain_valid = w._rx_boosted_gain_applied = w._cur_rx_boosted_gain = true;
w._nf_last_calib = now_ms;
w._nf_calib_deadline = now_ms + 800;
w._nf_sample_from = now_ms + NF_CALIB_SETTLE_MS;
assert(w._floor_estimator.add(-30, now_ms));
assert(w._secondary_floor_estimator.add(-30, now_ms));
if (owned == 0) w._cw_active = true;
if (owned == 1) w.busy = true; // unknown/continuous BUSY must defer
if (owned == 2) w.events.record();
if (owned == 3) w.packet = true;
if (owned == 4) w.state = STATE_TX_WAIT;
const auto state = w.state, armed = uint8_t(w._rx_ps_armed);
const auto deadline = w._nf_calib_deadline, sample_from = w._nf_sample_from;
unsigned admitted = 0;
for (unsigned retry = 0; retry < 3; ++retry) {
if (w.tryResetAGC()) ++admitted;
// Legacy AGC deliberately retains its historical ability to reset while
// BUSY. The adaptive try API defers outside known RXPS sleep.
if (owned != 1) w.resetAGC();
assert(admitted == 0 && w.agc_resets == 0 && w.gain_changes == 0);
assert(w.state == state && w._rx_ps_armed == bool(armed));
assert(w._nf_last_calib == now_ms && w._nf_refresh_requested);
assert(w._nf_calib_deadline == deadline && w._nf_sample_from == sample_from);
assert(w._floor_estimator.count() == 1 && w._secondary_floor_estimator.count() == 1);
assert(w._noise_floor_valid && w._noise_floor_secondary_valid);
assert(w._noise_floor_centi_dbm == -10500 && w._noise_floor_secondary_centi_dbm == -10600);
if (owned == 2) assert(w.hasPendingRadioInterrupt()); // never consume another owner's IRQ
}
w._cw_active = w.busy = w.packet = false;
w.claimRadioInterrupt(); w.state = STATE_RX;
assert(w.tryResetAGC()); ++admitted;
assert(admitted == 1 && w.agc_resets == 1 && w.gain_changes == 1);
assert(w.state == STATE_IDLE && w._nf_refresh_requested && w._nf_last_calib == 0);
assert(w._floor_estimator.count() == 0 && w._secondary_floor_estimator.count() == 0);
++cases;
}
{
RadioLibWrapper w; selectMode(w, 1);
w._rx_ps_armed = true; w.busy = true;
bool admitted = false;
// A valid 1 ms listen / 49 ms sleep cycle can remain BUSY at every
// phase-25 poll. Known RXPS must use the legacy explicit wake sequence.
for (unsigned retry = 0; retry < 10000 && !admitted; ++retry) {
now_ms = 25 + retry * 50;
admitted = w.tryResetAGC();
}
assert(admitted && w.agc_resets == 1 && w.state == STATE_IDLE);
assert(w._nf_refresh_requested && w._noise_floor_valid);
assert(w.reads == 0 && w.radio.scans == 0);
++cases;
}
for (unsigned mode : {0U, 1U, 2U}) {
RadioLibWrapper w; selectMode(w, mode);
w.busy = true; w._nf_refresh_requested = false; w._nf_last_calib = now_ms;
assert(!w.tryResetAGC() && w.agc_resets == 0 && !w._nf_refresh_requested);
w.resetAGC();
assert(w.agc_resets == 1 && w.state == STATE_IDLE);
assert(w._nf_refresh_requested && w._nf_last_calib == 0);
++cases;
}
// Admission reports that the sequence ran, not that the hardware is healthy.
// Both public APIs preserve independent published floors/validity and the
// active window's bound while invalidating samples from the old AGC path.
for (unsigned mode : {0U, 1U, 2U}) for (bool primary : {false, true})
for (bool secondary : {false, true}) for (bool active_window : {false, true})
for (bool legacy : {false, true}) {
RadioLibWrapper w; selectMode(w, mode);
w._rx_ps_armed = mode == 1 && !active_window;
w._rx_boosted_gain_valid = w._rx_boosted_gain_applied = w._cur_rx_boosted_gain = true;
w._noise_floor_valid = primary; w._noise_floor_secondary_valid = secondary;
w._nf_calib_active = active_window; w._nf_last_calib = now_ms;
w._nf_calib_deadline = now_ms + 800;
w.n_wd_soft = 2; w.n_wd_hard = 3;
assert(w.getRecoveryCount() == 5);
assert(w._floor_estimator.add(-30, now_ms));
assert(w._secondary_floor_estimator.add(-30, now_ms));
++now_ms;
if (legacy) w.resetAGC();
else assert(w.tryResetAGC());
assert(w.agc_resets == 1 && w.gain_changes == 1 && w.arms == 0);
assert(w.state == STATE_IDLE && w._nf_refresh_requested && w._nf_last_calib == 0);
assert(w._noise_floor_valid == primary && w._noise_floor_secondary_valid == secondary);
assert(w._noise_floor == -105 && w._noise_floor_centi_dbm == -10500);
assert(w._noise_floor_secondary_centi_dbm == -10600);
assert(w._floor_estimator.count() == 0 && w._secondary_floor_estimator.count() == 0);
assert(w._nf_sample_from == now_ms + (w._rx_ps_armed ? 0 : NF_CALIB_SETTLE_MS));
assert(w._nf_calib_active == active_window);
assert(w._nf_calib_deadline == (active_window ? now_ms + NF_CALIB_TIMEOUT_MS : 0));
assert(w.getRecoveryCount() == 5); // AGC attempts do not inflate watchdog recovery counters
++cases;
}
{
RadioLibWrapper w;
assert(w.getRecoveryCount() == 0);
w.n_wd_soft = 1; assert(w.getRecoveryCount() == 1);
w.n_wd_hard = 2; assert(w.getRecoveryCount() == 3);
++cases;
}
{
LegacyRadio radio;
assert(radio.tryResetAGC() && radio.resets == 1);
assert(radio.getRecoveryCount() == 0);
++cases;
}
std::printf("PASS: AGC admission (%u cases)\n", cases);
}
int main() {
periodicRefreshContracts();
agcAdmissionContracts();
// A completed packet, current frame, or TX owns the radio; CAD cannot erase it.
for (int owned : {1, 2, 3}) {
RadioLibWrapper w;
if (owned == 1) w.events.record();
if (owned == 2) w.packet = true;
if (owned == 3) w.state = STATE_TX_WAIT;
assert(w.isChannelActive());
assert(w.radio.scans == 0 && w.arms == 0);
}
for (int result : {RADIOLIB_CHANNEL_FREE, 1, -5, RADIOLIB_ERR_UNKNOWN}) {
for (bool ps : {false, true}) {
RadioLibWrapper w;
w._rx_ps_enabled = w._rx_ps_armed = ps;
w.radio.scan_result = result;
const bool active = w.isChannelActive();
assert(active == (result != RADIOLIB_CHANNEL_FREE));
assert(w.stops == unsigned(ps));
assert(w.arms == 1 && w.state == STATE_RX);
assert(w._rx_ps_armed == ps);
if (result == RADIOLIB_ERR_UNKNOWN) {
assert(now_ms == 2600 && w.board.services == 2);
}
}
}
{
RadioLibWrapper w;
w.radio.start_result = -5;
assert(w.isChannelActive());
assert(w.arms == 1 && w.state == STATE_RX);
}
{
RadioLibWrapper w;
w.arm_result = -5;
assert(w.isChannelActive()); // a free CAD with failed RX restoration is not success
assert(w.state == STATE_IDLE);
}
{
RadioLibWrapper w;
w.inject_arm_irq = true;
assert(w.isChannelActive());
assert(w.state == STATE_RX && w.hasPendingRadioInterrupt()); // preserves the new packet
}
// A quiet bounded window completes in ~3.2 s and powersaving resumes.
for (bool ps : {false, true}) {
RadioLibWrapper w;
w._rx_ps_enabled = w._rx_ps_armed = ps;
w._noise_floor_valid = false;
w._nf_last_calib = 0;
for (; now_ms < 4000; ++now_ms) {
w.loop();
if (w.state == STATE_IDLE) w.startRecv();
}
assert(!w._nf_refresh_requested && !w._nf_calib_active);
assert(w._noise_floor_valid && w._noise_floor_centi_dbm == -10000);
assert(w._rx_ps_armed == ps);
assert(w.reads == 64); // no unnecessary SPI RSSI reads between accepted samples
}
// Each receive re-arm needs fresh frontend settling before sampling RSSI.
{
RadioLibWrapper w;
w.startRecv();
for (; now_ms < 107; ++now_ms) w.loop();
assert(w.reads == 0);
w.loop();
assert(w.reads == 1);
now_ms = 200;
w.startRecv(); // CAD/TX re-arm must settle again, retaining earlier samples
for (; now_ms < 207; ++now_ms) w.loop();
assert(w.reads == 1 && w._floor_estimator.count() == 1);
w.loop();
assert(w.reads == 2 && w._floor_estimator.count() == 2);
}
// Repeated scheduled requests must not reset a partial block.
{
RadioLibWrapper w;
for (; now_ms < 4100; ++now_ms) {
if (now_ms % 2000 == 0) w.requestNoiseFloorRefresh();
w.loop();
}
assert(w._noise_floor_centi_dbm == -10125);
}
// After dual-profile initialization, the Dispatcher's two-second
// maintenance tick updates the threshold but must not restart calibration.
// A profile whose normal visit is under 7 ms gets one new bounded 7 ms
// sample set after fifteen minutes; a naturally long profile does not.
{
RadioLibWrapper w;
mesh::RadioProfileConfig second;
second.params.freq = 910.5; second.params.bw = 62.5; second.params.sf = 7; second.params.cr = 5;
second.mode = mesh::RadioProfileMode::RxTx;
w._profiles.primary = second.params;
w._profiles.setSecondary(second, false);
w._nf_refresh_requested = false;
w._noise_floor_valid = w._noise_floor_secondary_valid = true;
w.triggerNoiseFloorCalibrate(7);
assert(!w._nf_refresh_requested && w._threshold == 7);
w._noise_floor_secondary_valid = false;
w.triggerNoiseFloorCalibrate(8);
assert(w._nf_refresh_requested && w._threshold == 8);
}
{
RadioLibWrapper w;
mesh::RadioProfileConfig second;
second.params.freq = 910.5; second.params.bw = 500; second.params.sf = 5; second.params.cr = 5;
second.mode = mesh::RadioProfileMode::Rx;
w._profiles.primary = second.params;
w._profiles.setSecondary(second, false);
w._nf_refresh_requested = false;
w._noise_floor_valid = w._noise_floor_secondary_valid = true;
w._nf_last_calib = now_ms;
now_ms += NF_FAST_PROFILE_REFRESH_INTERVAL_MS - 1;
w.triggerNoiseFloorCalibrate(7);
assert(!w._nf_refresh_requested);
++now_ms;
w.triggerNoiseFloorCalibrate(7);
assert(w._nf_refresh_requested);
}
{
RadioLibWrapper w;
mesh::RadioProfileConfig second;
second.params.freq = 910.5; second.params.bw = 62.5; second.params.sf = 7; second.params.cr = 5;
second.mode = mesh::RadioProfileMode::Rx;
w._profiles.primary = second.params;
w._profiles.setSecondary(second, false);
w._nf_refresh_requested = false;
w._noise_floor_valid = w._noise_floor_secondary_valid = true;
w._nf_last_calib = now_ms;
now_ms += NF_FAST_PROFILE_REFRESH_INTERVAL_MS;
w.triggerNoiseFloorCalibrate(7);
assert(!w._nf_refresh_requested);
}
now_ms = 100; // subsequent timing tests use their original short timeline
// R1 and R2 collect distinct RSSI populations across alternating visits;
// publishing one must never blend the other channel into its floor.
{
RadioLibWrapper w;
mesh::RadioProfileConfig second;
second.params.freq = 910.5; second.params.bw = 62.5; second.params.sf = 7; second.params.cr = 5;
second.mode = mesh::RadioProfileMode::RxTx;
w._profiles.primary = second.params;
w._profiles.setSecondary(second, false);
w._profile_scan_generation[0] = w._profiles.generation[0];
w._profile_scan_generation[1] = w._profiles.generation[1];
w._profile_rxps_suspended = true;
w._noise_floor_valid = w._noise_floor_secondary_valid = false;
w._nf_refresh_requested = true;
w._nf_sample_from = 0;
for (unsigned i = 0; i < NoiseFloorEstimator::SAMPLE_COUNT; ++i) {
w._active_profile = 0; w.rssi = -100; w.loop(); now_ms += 50;
w._active_profile = 1; w.rssi = -90; w.loop(); now_ms += 50;
}
w.loop(); // publish after both profile estimators complete
assert(!w._nf_refresh_requested && w._noise_floor_centi_dbm == -10000);
assert(w._noise_floor_secondary_centi_dbm == -9000);
}
// Hardware standby RSSI is not background noise, even if software says RX.
{
RadioLibWrapper w;
w._floor_estimator.add(-100, 0);
w.chip_mode = 0;
w.rssi = -127;
for (; now_ms < 4100; ++now_ms) w.loop();
assert(w._noise_floor_centi_dbm == -10500);
assert(w._floor_estimator.count() == 0 && w.reads == 0);
assert(w.mode_reads == 80); // rejected samples are also limited to 20 Hz
w.chip_mode = 1;
w.rssi = -100;
for (; now_ms < 8100; ++now_ms) w.loop();
assert(w.reads == 64 && !w._nf_refresh_requested);
assert(w._noise_floor_centi_dbm == -10125);
}
// A BUSY chip or a packet arriving during the mode probe must block RSSI I/O.
for (bool busy : {false, true}) {
RadioLibWrapper w;
w.busy = busy;
w.inject_mode_irq = !busy;
w.loop();
assert(w.reads == 0);
assert(w.mode_reads == (busy ? 0U : 1U));
}
// Families without a mode probe retain the existing RSSI sampling path.
{
RadioLibWrapper w;
w.chip_mode = -1;
w.loop();
assert(w.reads == 1);
}
// Busy continuous RX times out without changing the published floor.
{
RadioLibWrapper w;
w.packet = true;
for (; now_ms <= NoiseFloorEstimator::WINDOW_TIMEOUT_MS + 101; ++now_ms) w.loop();
assert(!w._nf_refresh_requested);
assert(w._noise_floor_centi_dbm == -10500);
assert(w.reads == 0);
}
{
RadioLibWrapper w;
w._floor_estimator.add(-30, 100);
w.resetAGC();
assert(w._noise_floor_valid && w._noise_floor_centi_dbm == -10500);
assert(w._floor_estimator.count() == 0);
}
// Unknown/SPI-busy/sleep and packet ownership cannot trigger false recovery.
for (int skip : {0,1,2,3,4}) {
RadioLibWrapper w;
w.chip_mode = 0;
if (skip == 0) w.chip_mode = -1;
if (skip == 1) w.busy = true;
if (skip == 2) w._rx_ps_armed = true;
if (skip == 3) w.packet = true;
if (skip == 4) w.events.record();
for (uint32_t t : {10000U,20000U,30000U}) w.checkReceiveMode(t);
assert(w.soft == 0 && w.hard == 0);
}
{
RadioLibWrapper w;
w.chip_mode = 0;
w.checkReceiveMode(10000); assert(w.soft == 0);
w.checkReceiveMode(20000); assert(w.soft == 1);
w.checkReceiveMode(30000); assert(w.hard == 1);
w.chip_mode = 1; w.checkReceiveMode(40000);
w.chip_mode = 0; w.checkReceiveMode(50000);
assert(w.soft == 1 && w.hard == 1);
}
{
RadioLibWrapper w;
w._rx_mode_checked_at = 0xFFFFF000U;
w.checkReceiveMode(0x10); assert(w._rx_mode_checked_at == 0xFFFFF000U);
w.checkReceiveMode(0x2000); assert(w._rx_mode_checked_at == 0x2000);
}
}
'''
def transition_harness(source=None):
"""Build the shared actual-method harness, including its counted I/O adapters."""
if source is None:
source = (ROOT / 'src/helpers/radiolib/RadioLibWrappers.cpp').read_text()
header = (ROOT / 'src/helpers/radiolib/RadioLibWrappers.h').read_text()
dispatcher = (ROOT / 'src/Dispatcher.h').read_text()
rxps = (ROOT / 'src/helpers/radiolib/RXPowerSaving.h').read_text()
names = [('int16_t', 'performChannelScanWithTimeout'), ('bool', 'isPacketPendingOrReceiving'),
('bool', 'isChannelActive'), ('bool', 'isReceivingPassive'),
('bool', 'setRxPowerSaving'), ('bool', 'setRxBoostedGainMode'),
('mesh::RadioParamApplyResult', 'trySetRxPowerSaving'),
('mesh::RadioParamApplyResult', 'trySetRxBoostedGainMode'),
('void', 'startRecv'), ('void', 'checkReceiveMode'),
('void', 'loop'), ('void', 'triggerNoiseFloorCalibrate'),
('void', 'requestRestartRecv'), ('void', 'noiseFloorCalibCheck'),
('void', 'endNoiseFloorCalib'), ('void', 'requestNoiseFloorRefresh'),
('void', 'requestPeriodicNoiseFloorRefresh'), ('void', 'recalibrateNoiseFloor'),
('bool', 'resetAGCIfIdle'), ('bool', 'tryResetAGC'), ('void', 'resetAGC'),
('bool', 'getNoiseFloorWakeDelay')]
methods = '\n'.join(method(source, f'{kind} RadioLibWrapper::{name}(')
for kind, name in names)
constants = '\n'.join(re.findall(r'^#define NF_\w+\s+[^\n]+', source, re.MULTILINE))
constants += '\n' + re.search(
r'^#define RX_POWERSAVING_CONTINUOUS_FALLBACK_US\s+[^\n]+', rxps,
re.MULTILINE).group(0)
settle_start = header.index('#if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LLCC68)')
settle = header[settle_start:header.index('#endif', settle_start) + len('#endif')]
return (HARNESS.replace('@METHODS@', methods).replace('@NF_CONSTANTS@', constants)
.replace('@SETTLE_CONSTANT@', settle)
.replace('@RECOVERY_GETTER@', method(header, 'uint32_t getRecoveryCount() const').replace('override', ''))
.replace('@LEGACY_TRY_RESET@', method(dispatcher, 'virtual bool tryResetAGC('))
.replace('@LEGACY_RECOVERY_GETTER@', method(dispatcher, 'virtual uint32_t getRecoveryCount(')))
class RadioReceiveContractTest(unittest.TestCase):
def test_preamble_margin_and_receive_deadlines(self):
radio_source = (ROOT / 'src/helpers/radiolib/RadioLibWrappers.cpp').read_text()
timing = method(radio_source, 'PacketMillis RadioLibWrapper::calcMaxPacketMillis(')
sx1262 = method((ROOT / 'src/helpers/radiolib/CustomSX1262.h').read_text(),
'bool isReceiving()')
lr1110 = method((ROOT / 'src/helpers/radiolib/CustomLR1110.h').read_text(),
'bool isReceiving()')
source = r'''
#include <cassert>
#include <cstdint>
#include <initializer_list>
#define MAX_TRANS_UNIT 255
#define MESH_DEBUG_PRINTLN(...) ((void)0)
#define RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED 4U
#define RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID 8U
#define RADIOLIB_SX126X_IRQ_HEADER_VALID 16U
#define RADIOLIB_SX126X_IRQ_HEADER_ERR 32U
#define RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED 16U
#define RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID 32U
#define RADIOLIB_LR11X0_IRQ_HEADER_ERR 64U
static uint32_t now_ms;
uint32_t millis() { return now_ms; }
struct PacketMillis { uint32_t preambleMillis, payloadMillis; };
struct Chip { uint32_t getTimeOnAir(int) { return 1000000; } };
struct RadioLibWrapper {
Chip chip;
Chip* _radio = &chip;
PacketMillis calcMaxPacketMillis(uint8_t, float, uint8_t, uint16_t);
};
@TIMING@
struct ReceiverState {
uint32_t _preambleMillis, _maxPayloadMillis;
uint32_t _activityAt = 0, irq = 0;
bool _headerSeen = false;
explicit ReceiverState(PacketMillis p)
: _preambleMillis(p.preambleMillis), _maxPayloadMillis(p.payloadMillis) {}
bool isChipBusy() { return false; }
uint32_t getIrqFlags() { return irq; }
uint32_t getIrqStatus() { return irq; }
void clearIrqFlags(uint32_t flags) { irq &= ~flags; }
void clearIrqState(uint32_t flags) { irq &= ~flags; }
};
struct SX1262Receiver : ReceiverState {
using ReceiverState::ReceiverState;
@SX1262@
};
struct LR1110Receiver : ReceiverState {
using ReceiverState::ReceiverState;
@LR1110@
};
template <typename Receiver>
void checkReceive(PacketMillis limits, uint32_t preamble, uint32_t header,
uint32_t error, bool preserve_header_error) {
// A latched preamble with no header expires despite repeated polling.
// Check both normal uptime and a millis() rollover during the hold.
for (uint32_t start : {100U, 0xFFFFFFF0U}) {
Receiver rx(limits);
now_ms = start;
assert(!rx.isReceiving());
rx.irq = preamble;
assert(rx.isReceiving());
for (uint32_t elapsed : {1U, limits.preambleMillis - 1, limits.preambleMillis}) {
now_ms = start + elapsed;
assert(rx.isReceiving());
}
now_ms = start + limits.preambleMillis + 1;
assert(!rx.isReceiving() && !(rx.irq & preamble));
// A subsequent packet gets a fresh hold, not the abandoned packet's timer.
++now_ms;
rx.irq = preamble;
assert(rx.isReceiving());
now_ms += limits.preambleMillis;
rx.irq |= header;
assert(rx.isReceiving());
now_ms += limits.payloadMillis;
assert(rx.isReceiving());
++now_ms;
assert(!rx.isReceiving() && !(rx.irq & (preamble | header)));
}
{
// A completed packet releases the scan immediately when its IRQs are consumed.
Receiver rx(limits);
now_ms = 100;
rx.irq = preamble;
assert(rx.isReceiving());
++now_ms;
rx.irq |= header;
assert(rx.isReceiving());
++now_ms;
rx.irq = 0;
assert(!rx.isReceiving());
rx.irq = preamble;
assert(rx.isReceiving());
}
{
// Header failure releases the hold; LR1110's recovery path owns its error IRQ.
Receiver rx(limits);
now_ms = 100;
rx.irq = preamble;
assert(rx.isReceiving());
++now_ms;
rx.irq |= error;
assert(!rx.isReceiving());
assert(bool(rx.irq & error) == preserve_header_error);
}
}
int main() {
RadioLibWrapper radio;
struct Case {
uint8_t sf;
float bw;
uint8_t cr;
uint16_t preamble;
uint32_t hold_ms, payload_ms;
};
// Independent timing examples: configured preamble + four symbols + the
// existing sync/header allowance. Payload limits retain their old values.
const Case cases[] = {
{7, 62.5f, 5, 32, 99, 1456},
{7, 125, 5, 32, 50, 1528},
{7, 500, 5, 64, 21, 1569},
{8, 500, 5, 88, 54, 1518},
{9, 500, 5, 32, 50, 1528},
{5, 500, 5, 128, 10, 1586},
{6, 125, 8, 16, 18, 985},
{7, 62.5f, 8, 120, 280, 730},
{7, 125, 8, 65535, 67125, 4000},
};
for (const Case& c : cases) {
const auto limits = radio.calcMaxPacketMillis(c.sf, c.bw, c.cr, c.preamble);
assert(limits.preambleMillis == c.hold_ms);
assert(limits.payloadMillis == c.payload_ms);
checkReceive<SX1262Receiver>(limits, RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED,
RADIOLIB_SX126X_IRQ_HEADER_VALID, RADIOLIB_SX126X_IRQ_HEADER_ERR, false);
checkReceive<LR1110Receiver>(limits, RADIOLIB_LR11X0_IRQ_PREAMBLE_DETECTED,
RADIOLIB_LR11X0_IRQ_SYNC_WORD_HEADER_VALID, RADIOLIB_LR11X0_IRQ_HEADER_ERR, true);
}
}
'''
with tempfile.TemporaryDirectory(prefix='meshcore-preamble-hold-') as tmp:
cpp, binary = Path(tmp) / 'test.cpp', Path(tmp) / 'test'
cpp.write_text(source.replace('@TIMING@', timing)
.replace('@SX1262@', sx1262).replace('@LR1110@', lr1110))
result = subprocess.run([os.environ.get('CXX', 'c++'), '-std=c++17', '-O1',
'-Wall', '-Wextra', str(cpp), '-o', str(binary)], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(binary)], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
def test_real_rxps_setter_preserves_intent_and_rejects_bad_values(self):
common = (ROOT / 'src/helpers/CommonCLI.cpp').read_text()
setter = method(common, 'if (memcmp(config, "radio.rxps ", 11) == 0)')
calculator = method(common, 'bool CommonCLI::calculateRxPowerSavingLevel(')
source = r'''
#include <cassert>
#include <cstring>
#include <initializer_list>
#include <helpers/CLICommandUtils.h>
#include <helpers/radiolib/RXPowerSaving.h>
namespace mesh { enum class RadioParamApplyResult : uint8_t { APPLIED, BUSY, FAILED }; }
struct NodePrefs {
uint8_t rx_powersaving_enabled = 1, rx_ps_level = 6, rx_ps_preamble = 32, sf = 8;
uint32_t rx_ps_rx_us = 40000, rx_ps_sleep_us = 32000;
float bw = 62.5;
};
struct Callbacks {
bool accept = true;
unsigned calls = 0;
bool setRxPowerSaving(bool, uint32_t, uint32_t) { ++calls; return accept; }
bool supportsRxPowerSaving() const { return true; }
};
struct CommonCLI {
NodePrefs* _prefs;
Callbacks* _callbacks;
unsigned saves = 0;
static constexpr uint8_t RxPowerSaving = 2;
uint8_t _rx_settings_pending = 0;
uint32_t _rx_settings_retry_at = 0;
mesh::RadioParamApplyResult applyRxSetting(uint8_t) {
return _callbacks->setRxPowerSaving(_prefs->rx_powersaving_enabled,
_prefs->rx_ps_rx_us, _prefs->rx_ps_sleep_us)
? mesh::RadioParamApplyResult::APPLIED : mesh::RadioParamApplyResult::FAILED;
}
bool saveRxSetting(uint8_t, mesh::RadioParamApplyResult result, char*) {
if (result == mesh::RadioParamApplyResult::FAILED) return false;
++saves; return true;
}
void restoreRxSetting(uint8_t, mesh::RadioParamApplyResult, uint8_t, uint32_t) {}
void savePrefs() { ++saves; }
bool calculateRxPowerSavingLevel(uint32_t, uint8_t, float, uint32_t, uint32_t*, uint32_t*);
void set(const char* config, char* reply);
};
void appendRxPowerSavingAdjustmentNote(char*, const NodePrefs*, uint8_t, float) {}
@CALCULATOR@
void CommonCLI::set(const char* config, char* reply) { @SETTER@ }
int main() {
NodePrefs p;
Callbacks callbacks;
CommonCLI cli{&p, &callbacks};
char reply[160] = {};
cli.set("radio.rxps level 8 preamble 32", reply);
assert(p.rx_powersaving_enabled && p.rx_ps_level == 8 && p.rx_ps_preamble == 32);
const uint32_t rx = p.rx_ps_rx_us, sleep = p.rx_ps_sleep_us;
cli.set("radio.rxps off \t", reply);
assert(!p.rx_powersaving_enabled && p.rx_ps_level == 8 && p.rx_ps_preamble == 32);
assert(p.rx_ps_rx_us == rx && p.rx_ps_sleep_us == sleep && cli.saves == 2);
for (const char* command : {"radio.rxps level 264 preamble 32",
"radio.rxps level 8 preamble 288", "radio.rxps level 4294967296",
"radio.rxps 32 1000", "radio.rxps max extra"}) {
cli.set(command, reply);
assert(cli.saves == 2 && callbacks.calls == 2);
assert(!p.rx_powersaving_enabled && p.rx_ps_level == 8 && p.rx_ps_preamble == 32);
}
cli.set("radio.rxps 12345 67890", reply);
assert(p.rx_powersaving_enabled && p.rx_ps_level == 0 && p.rx_ps_preamble == 0);
assert(p.rx_ps_rx_us == 12345 && p.rx_ps_sleep_us == 67890 && cli.saves == 3);
callbacks.accept = false;
cli.set("radio.rxps max", reply);
assert(p.rx_ps_level == 0 && p.rx_ps_rx_us == 12345 && cli.saves == 3);
callbacks.accept = true;
cli.set("radio.rxps max", reply);
assert(p.rx_ps_level == 8 && p.rx_ps_preamble == 16 && cli.saves == 4);
}
'''
with tempfile.TemporaryDirectory(prefix='meshcore-rxps-cli-') as tmp:
cpp, binary = Path(tmp) / 'test.cpp', Path(tmp) / 'test'
cpp.write_text(source.replace('@CALCULATOR@', calculator).replace('@SETTER@', setter))
result = subprocess.run([os.environ.get('CXX', 'c++'), '-std=c++17', '-O1',
'-I', str(ROOT / 'test/mocks'), '-I', str(ROOT / 'src'),
str(cpp), '-o', str(binary)], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(binary)], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
def run_production_transitions(self, source=None, expect_failure=False, phase_failure=False):
harness = transition_harness(source)
with tempfile.TemporaryDirectory(prefix='meshcore-radio-transitions-') as tmp:
cpp = Path(tmp) / 'test.cpp'
binary = Path(tmp) / 'test'
# An inherited production comment uses an em dash. Keep generated
# test files ASCII without changing any executable source token.
cpp.write_text(harness.replace('\u2014', '-'), encoding='ascii')
result = subprocess.run([os.environ.get('CXX','c++'), '-std=c++17', '-O1',
'-Wall', '-Wextra', '-DUSE_SX1262=1',
*(['-fsanitize=address,undefined', '-fno-sanitize-recover=all',
'-fno-pie', '-no-pie'] if sys.platform.startswith('linux') else []),
'-I', str(ROOT / 'src'), str(cpp), '-o', str(binary)],
capture_output=True, text=True, timeout=60)
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=20)
if expect_failure:
self.assertNotEqual(result.returncode, 0, 'unthrottled negative control escaped the regression')
if phase_failure:
self.assertIn('agcAdmissionContracts', result.stderr)
self.assertIn('admitted && w.agc_resets == 1', result.stderr)
else:
self.assertIn('periodicRefreshContracts', result.stderr)
self.assertIn('w._nf_refresh_requested == expected', result.stderr)
else:
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn('PASS: routine noise-floor cooldown (262 cases)', result.stdout)
self.assertIn('PASS: AGC admission (68 cases)', result.stdout)
def test_production_transitions(self):
self.run_production_transitions()
def test_busy_rxps_sleep_phase_lock_negative_control(self):
source = (ROOT / 'src/helpers/radiolib/RadioLibWrappers.cpp').read_text()
body = method(source, 'bool RadioLibWrapper::tryResetAGC(')
self.assertEqual(body.count('_rx_ps_armed || '), 1)
unguarded = body.replace('_rx_ps_armed || ', '')
self.run_production_transitions(source.replace(body, unguarded),
expect_failure=True, phase_failure=True)
def test_unthrottled_periodic_callers_and_helper_negative_controls(self):
source = (ROOT / 'src/helpers/radiolib/RadioLibWrappers.cpp').read_text()
for caller in ('triggerNoiseFloorCalibrate', 'isReceivingPassive', 'requestPeriodicNoiseFloorRefresh'):
with self.subTest(caller=caller):
kind = 'bool' if caller == 'isReceivingPassive' else 'void'
body = method(source, f'{kind} RadioLibWrapper::{caller}(')
if caller == 'requestPeriodicNoiseFloorRefresh':
unthrottled = body[:body.index('{') + 1] + '\n requestNoiseFloorRefresh();\n}'
else:
self.assertEqual(body.count('requestPeriodicNoiseFloorRefresh();'), 1)
unthrottled = body.replace('requestPeriodicNoiseFloorRefresh();', 'requestNoiseFloorRefresh();')
self.run_production_transitions(source.replace(body, unthrottled), expect_failure=True)
if __name__ == '__main__':
unittest.main()