233 lines
11 KiB
C++
233 lines
11 KiB
C++
|
|
static const char* scenario;
|
|
#define CHECK(expression, diagnostic) do { if (!(expression)) { \
|
|
std::fprintf(stderr, "FAIL %s: %s (line %d)\n", scenario, diagnostic, __LINE__); \
|
|
return 1; } } while (0)
|
|
|
|
static void prepare(RadioLibWrapper& w, unsigned mode, uint32_t start = 100) {
|
|
now_ms = start;
|
|
w._rx_ps_enabled = mode != 0;
|
|
w._rx_ps_continuous_fallback = mode == 2;
|
|
w._rx_ps_armed = mode == 1;
|
|
w._noise_floor_valid = false;
|
|
w._nf_refresh_requested = true;
|
|
w._nf_last_calib = 0;
|
|
w.startRecv();
|
|
w.loop();
|
|
if (w.state == STATE_IDLE) w.startRecv();
|
|
}
|
|
|
|
int main(int argc, char** argv) {
|
|
if (argc != 2) return 2;
|
|
scenario = argv[1];
|
|
if (!std::strcmp(scenario, "quiet")) {
|
|
for (unsigned mode : {0U, 1U, 2U}) {
|
|
RadioLibWrapper w;
|
|
prepare(w, mode);
|
|
const uint32_t start = now_ms;
|
|
uint32_t eligible_ms = 0;
|
|
unsigned iterations = 0;
|
|
while (w._nf_refresh_requested || w._nf_calib_active) {
|
|
CHECK(++iterations < 200, "deadline service must remain bounded");
|
|
uint32_t wait = UINT32_MAX;
|
|
CHECK(w.getNoiseFloorWakeDelay(now_ms, wait), "active batch lost wake deadline");
|
|
CHECK(wait > 0 || w._floor_estimator.complete(),
|
|
"pending batch must permit idle between samples");
|
|
CHECK(wait <= NoiseFloorEstimator::SAMPLE_INTERVAL_MS,
|
|
"sample deadline exceeded original spacing");
|
|
eligible_ms += wait;
|
|
now_ms += wait;
|
|
w.loop();
|
|
if (w.state == STATE_IDLE) w.startRecv();
|
|
}
|
|
CHECK(w.reads == NoiseFloorEstimator::SAMPLE_COUNT,
|
|
"idle scheduling changed accepted sample count");
|
|
CHECK(w._noise_floor_valid && w._noise_floor_centi_dbm == -10000,
|
|
"idle scheduling changed published estimator value");
|
|
CHECK(w._rx_ps_armed == (mode == 1), "calibration did not restore receive policy");
|
|
CHECK(eligible_ms >= 3150 && uint32_t(now_ms - start) < 3300,
|
|
"quiet collection must retain original quality and bounded cadence");
|
|
}
|
|
} else if (!std::strcmp(scenario, "rollover")) {
|
|
for (uint32_t start : {100U, UINT32_MAX - 3U, uint32_t(0U - NF_CALIB_SETTLE_MS)}) {
|
|
RadioLibWrapper w;
|
|
prepare(w, 0, start);
|
|
uint32_t wait = 0;
|
|
CHECK(w.reads == 0, "RX settling must precede every first sample");
|
|
CHECK(w.getNoiseFloorWakeDelay(now_ms, wait) && wait == NF_CALIB_SETTLE_MS,
|
|
"settle deadline must survive rollover, including timestamp zero");
|
|
now_ms += wait - 1;
|
|
w.loop();
|
|
CHECK(w.reads == 0 && w.getNoiseFloorWakeDelay(now_ms, wait) && wait == 1,
|
|
"sample accepted before settle deadline");
|
|
++now_ms;
|
|
CHECK(w.getNoiseFloorWakeDelay(now_ms, wait) && wait == 0,
|
|
"exact sample boundary must request service");
|
|
w.loop();
|
|
CHECK(w.reads == 1 && w.getNoiseFloorWakeDelay(now_ms, wait) && wait == 50,
|
|
"next sample spacing must survive rollover");
|
|
now_ms += 49;
|
|
w.loop();
|
|
CHECK(w.reads == 1, "early sample changed original sample spacing");
|
|
++now_ms;
|
|
w.loop();
|
|
CHECK(w.reads == 2, "next sample deadline not serviced");
|
|
}
|
|
} else if (!std::strcmp(scenario, "queries")) {
|
|
RadioLibWrapper w;
|
|
prepare(w, 1);
|
|
uint32_t wait = 0;
|
|
const unsigned reads = w.reads, modes = w.mode_reads;
|
|
const unsigned busy = w.busy_probes, packets = w.packet_probes;
|
|
const auto count = w._floor_estimator.count();
|
|
for (unsigned i = 0; i < 100; ++i)
|
|
CHECK(w.getNoiseFloorWakeDelay(now_ms, wait) && wait > 0,
|
|
"query must preserve pending sample deadline");
|
|
CHECK(w.reads == reads && w.mode_reads == modes && w.busy_probes == busy
|
|
&& w.packet_probes == packets && w._floor_estimator.count() == count,
|
|
"deadline query must not access hardware or mutate samples");
|
|
w.events.record();
|
|
CHECK(w.getNoiseFloorWakeDelay(now_ms, wait) && w.hasPendingRadioInterrupt(),
|
|
"deadline query consumed radio IRQ");
|
|
now_ms += NF_CALIB_SETTLE_MS;
|
|
w.loop();
|
|
CHECK(w.hasPendingRadioInterrupt() && w.reads == reads,
|
|
"pending packet IRQ must retain ownership during sampling service");
|
|
w.claimRadioInterrupt();
|
|
now_ms += NoiseFloorEstimator::SAMPLE_INTERVAL_MS;
|
|
w.inject_mode_irq = true;
|
|
w.loop();
|
|
CHECK(w.hasPendingRadioInterrupt() && w.reads == reads,
|
|
"IRQ during status probe must prevent RSSI read and remain pending");
|
|
} else if (!std::strcmp(scenario, "rejected")) {
|
|
for (bool busy : {false, true}) for (uint32_t start : {100U, UINT32_MAX - 17U}) {
|
|
RadioLibWrapper w;
|
|
prepare(w, 1, start);
|
|
w._noise_floor_valid = true;
|
|
w.packet = !busy;
|
|
w.busy = busy;
|
|
unsigned iterations = 0;
|
|
while (w._nf_refresh_requested || w._nf_calib_active) {
|
|
CHECK(++iterations < 1000, "rejected-sample retries must not hot poll");
|
|
uint32_t wait = 0;
|
|
CHECK(w.getNoiseFloorWakeDelay(now_ms, wait), "rejected sample lost timeout wake");
|
|
CHECK(wait > 0, "rejected sample must permit bounded idle before retry");
|
|
now_ms += wait;
|
|
w.loop();
|
|
if (w.state == STATE_IDLE) w.startRecv();
|
|
}
|
|
CHECK(w.reads == 0 && w._noise_floor_centi_dbm == -10500,
|
|
"rejected reads must retain published floor");
|
|
CHECK(uint32_t(now_ms - start) <= NoiseFloorEstimator::WINDOW_TIMEOUT_MS + 50,
|
|
"idle must not extend calibration timeout");
|
|
}
|
|
} else if (!std::strcmp(scenario, "lazy")) {
|
|
for (unsigned path : {0U, 1U, 2U}) for (bool accepted : {false, true})
|
|
for (uint32_t start : {1000U, UINT32_MAX - 3149U}) {
|
|
RadioLibWrapper w;
|
|
now_ms = start;
|
|
for (unsigned i = 0; i < NoiseFloorEstimator::SAMPLE_COUNT; ++i) {
|
|
now_ms = start + i * NoiseFloorEstimator::SAMPLE_INTERVAL_MS;
|
|
CHECK(w._floor_estimator.add(accepted ? -105 : -70, now_ms),
|
|
"policy fixture must use original complete spaced blocks");
|
|
}
|
|
w.loop();
|
|
CHECK(!w._nf_refresh_requested && w._nf_refresh_attempted,
|
|
"complete publish attempt must retire calibration");
|
|
CHECK(w._nf_lazy_refresh == accepted && w._noise_floor_valid,
|
|
"held rise must not earn slow refresh merely because old floor is valid");
|
|
const uint32_t finish = now_ms;
|
|
const uint32_t interval = accepted ? NF_LAZY_REFRESH_INTERVAL_MS : NF_CALIB_INTERVAL_MS;
|
|
for (uint32_t age : {1U, interval - 1}) {
|
|
now_ms = finish + age;
|
|
if (path == 0) w.triggerNoiseFloorCalibrate(7);
|
|
else if (path == 1) w.isReceivingPassive(7);
|
|
else w.requestPeriodicNoiseFloorRefresh();
|
|
CHECK(!w._nf_refresh_requested, "routine caller ignored lazy/retry cooldown");
|
|
}
|
|
now_ms = finish + interval;
|
|
if (path == 0) w.triggerNoiseFloorCalibrate(7);
|
|
else if (path == 1) w.isReceivingPassive(7);
|
|
else w.requestPeriodicNoiseFloorRefresh();
|
|
CHECK(w._nf_refresh_requested, "routine refresh missed its selected cadence");
|
|
w._nf_refresh_requested = false;
|
|
w._nf_lazy_refresh = true;
|
|
w._nf_last_calib = now_ms;
|
|
w.recalibrateNoiseFloor();
|
|
CHECK(w._nf_refresh_requested && !w._nf_lazy_refresh && !w._noise_floor_valid,
|
|
"forced tuning/gain baseline must bypass lazy routine cadence");
|
|
}
|
|
} else if (!std::strcmp(scenario, "agc-idle")) {
|
|
for (unsigned mode : {0U, 1U, 2U}) for (bool lazy : {false, true})
|
|
for (uint32_t last : {0U, 100U, UINT32_MAX - 2000U}) {
|
|
RadioLibWrapper w;
|
|
w._rx_ps_enabled = mode != 0;
|
|
w._rx_ps_continuous_fallback = mode == 2;
|
|
w._rx_ps_armed = mode == 1;
|
|
w._noise_floor_valid = true;
|
|
w._nf_refresh_requested = w._nf_calib_active = false;
|
|
w._nf_lazy_refresh = lazy;
|
|
w._nf_refresh_attempted = true;
|
|
w._nf_last_calib = last;
|
|
const uint32_t interval = lazy ? NF_LAZY_REFRESH_INTERVAL_MS : NF_CALIB_INTERVAL_MS;
|
|
for (uint32_t age : {1000U, 2000U, interval - 1}) {
|
|
now_ms = last + age;
|
|
w.requestPeriodicNoiseFloorRefresh();
|
|
CHECK(w.tryResetAGC(), "idle maintenance must run the reset sequence");
|
|
CHECK(!w._nf_refresh_requested && w._noise_floor_valid
|
|
&& w._nf_lazy_refresh == lazy && w._nf_last_calib == last,
|
|
"maintenance must preserve the floor and original refresh deadline");
|
|
w.startRecv();
|
|
CHECK(w._rx_ps_armed == (mode == 1) && !w._nf_calib_active,
|
|
"maintenance must return immediately to the selected RX policy");
|
|
}
|
|
now_ms = last + interval;
|
|
w.requestPeriodicNoiseFloorRefresh();
|
|
CHECK(w._nf_refresh_requested, "maintenance postponed a routine refresh or failed retry");
|
|
}
|
|
for (unsigned reason : {0U, 1U, 2U, 3U, 4U, 5U}) {
|
|
RadioLibWrapper w;
|
|
w._nf_refresh_requested = w._nf_calib_active = false;
|
|
w._noise_floor_valid = true;
|
|
w._nf_last_calib = now_ms;
|
|
if (reason == 0) w._noise_floor_valid = false;
|
|
if (reason == 1) w._nf_refresh_requested = true;
|
|
if (reason == 2) w._nf_calib_active = true;
|
|
if (reason == 3) w._profiles.secondary.mode = mesh::RadioProfileMode::RxTx;
|
|
if (reason == 5) { w._rx_boosted_gain_valid = true; w.gain_apply_success = false; }
|
|
if (reason == 4) w.resetAGC(); // Recovery/legacy uses the forced API.
|
|
else CHECK(w.tryResetAGC(), "idle reset was unexpectedly refused");
|
|
CHECK(w._nf_refresh_requested && w._nf_last_calib == 0,
|
|
"startup, pending, dual-profile, failed gain and recovery floors must still refresh");
|
|
}
|
|
} else if (!std::strcmp(scenario, "timeout-zero")) {
|
|
RadioLibWrapper w;
|
|
// A legitimate 30-second timeout lands exactly on uint32_t zero.
|
|
const uint32_t start = uint32_t(0U - NoiseFloorEstimator::WINDOW_TIMEOUT_MS);
|
|
prepare(w, 0, start);
|
|
w.packet = true;
|
|
w._noise_floor_valid = false;
|
|
CHECK(w._nf_window_armed && uint32_t(w._nf_calib_deadline) == 0,
|
|
"wrapped timeout must retain explicit armed state");
|
|
now_ms = UINT32_MAX;
|
|
w.loop();
|
|
uint32_t wait = 0;
|
|
CHECK(w._nf_refresh_requested && w.getNoiseFloorWakeDelay(now_ms, wait) && wait == 1,
|
|
"last timeout millisecond must remain idle eligible");
|
|
now_ms = 0;
|
|
CHECK(w.getNoiseFloorWakeDelay(now_ms, wait) && wait == 0,
|
|
"wrapped timeout must request exact-boundary service");
|
|
w.loop();
|
|
CHECK(!w._nf_refresh_requested && w._nf_refresh_attempted && w._nf_last_calib == 0,
|
|
"timeout at timestamp zero must retire failed attempt");
|
|
w.packet = false;
|
|
now_ms = NF_CALIB_INTERVAL_MS - 1;
|
|
w.requestPeriodicNoiseFloorRefresh();
|
|
CHECK(!w._nf_refresh_requested, "timestamp-zero failure must not masquerade as startup");
|
|
++now_ms;
|
|
w.requestPeriodicNoiseFloorRefresh();
|
|
CHECK(w._nf_refresh_requested, "timestamp-zero failure missed bounded retry");
|
|
} else return 2;
|
|
std::printf("PASS: %s\n", scenario);
|
|
}
|