Retain valid noise floors during adaptive AGC maintenance

This commit is contained in:
mikecarper committed 2026-10-17 11:06:10 -07:00
1 parent 55354c7fbe
commit eb4f837969
6 files changed
+78 -13

No files matched your search

+5 -2
View File
@@ -73,8 +73,11 @@ an attempt on a busy channel.
After a successful baseline, ordinary single-profile refresh waits five
minutes. A rejected rise or failed attempt retains the last usable floor and
retries after one minute. Gain, tuning and AGC changes still request immediate
recalibration, so a configured short AGC interval can refresh more frequently.
retries after one minute. Gain/tuning changes and radio recovery still request
immediate recalibration. Adaptive AGC maintenance with an unchanged RF path
retains a valid baseline and its existing refresh deadline; it does not cancel
a pending collection or postpone a failed-sample retry. Legacy fixed-cadence
AGC resets still request recalibration.
Dual-profile scanning keeps its existing timing and calibration policy.
ESP32 timed sleep uses a millisecond timer capped at the next sample deadline.
+15 -6
View File
@@ -1040,10 +1040,10 @@ bool RadioLibWrapper::tryResetAGC() {
// BUSY is expected during RXPS sleep. Reuse the legacy explicit wake/reset
// sequence there; polling only every 50 ms can miss every short RX window.
// Outside known duty cycling, a busy SPI command still defers admission.
return (_rx_ps_armed || !isChipBusy()) && resetAGCIfIdle();
return (_rx_ps_armed || !isChipBusy()) && resetAGCIfIdle(false);
}
bool RadioLibWrapper::resetAGCIfIdle() {
bool RadioLibWrapper::resetAGCIfIdle(bool refresh_floor) {
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned) return false;
#endif
@@ -1053,11 +1053,20 @@ bool RadioLibWrapper::resetAGCIfIdle() {
doResetAGC();
state = STATE_IDLE; // trigger a startReceive()
if (_rx_boosted_gain_valid) applyRxBoostedGainMode(_cur_rx_boosted_gain);
const bool gain_restored = !_rx_boosted_gain_valid
|| applyRxBoostedGainMode(_cur_rx_boosted_gain);
// AGC reset discards stale samples, but the established floor remains usable
// for LBT while the next spaced block is collected. Gain/tuning changes use
// recalibrateNoiseFloor() directly to seed a fresh baseline.
// Adaptive maintenance restores the same RF path. A valid, idle baseline
// keeps its existing refresh deadline instead of spending another complete
// continuous-RX window on every reset. Never cancel a requested collection,
// postpone failed-sample retries, or change dual-profile scanning policy.
if (!refresh_floor && gain_restored && _noise_floor_valid
&& !_profiles.enabled() && !_nf_refresh_requested && !_nf_calib_active) {
return true;
}
// Recovery, legacy resets, changed/unknown paths and pending collections
// retain prompt resampling. Do not mix samples across the analog reset.
const bool previous_valid = _noise_floor_valid;
const bool previous_secondary_valid = _noise_floor_secondary_valid;
recalibrateNoiseFloor();
+1 -1
View File
@@ -242,7 +242,7 @@ protected:
void rxPsWatchdogCheck();
void requestPeriodicNoiseFloorRefresh();
void requestNoiseFloorRefresh();
bool resetAGCIfIdle();
bool resetAGCIfIdle(bool refresh_floor = true);
void requestRestartRecv();
bool isPacketPendingOrReceiving();
void noiseFloorCalibCheck(unsigned long now);
+43
View File
@@ -157,6 +157,49 @@ int main(int argc, char** argv) {
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.
+11 -1
View File
@@ -39,6 +39,7 @@ class NoiseFloorSleepTests(unittest.TestCase):
fixture = (ROOT / 'test/fixtures/noise_floor_sleep.cpp').read_text()
variants = {
'production': getter,
'agc_forces_floor': getter,
'old_batch_veto': getter[:getter.index('{') + 1] + '''
(void)now;
if (!isCalibratingNoiseFloor()) return false;
@@ -55,7 +56,9 @@ class NoiseFloorSleepTests(unittest.TestCase):
for name, implementation in variants.items():
filename = name + '_' + family
cpp, binary = work / (filename + '.cpp'), work / filename
cpp.write_text((harness + implementation + fixture).replace('\u2014', '-'),
variant_harness = harness.replace('resetAGCIfIdle(false)', 'resetAGCIfIdle()') \
if name == 'agc_forces_floor' else harness
cpp.write_text((variant_harness + implementation + fixture).replace('\u2014', '-'),
encoding='ascii')
built = subprocess.run([os.environ.get('CXX', 'g++'), '-std=c++17',
'-O1', '-g', '-Wall', '-Wextra', '-Werror',
@@ -96,6 +99,13 @@ class NoiseFloorSleepTests(unittest.TestCase):
def test_timeout_timestamp_zero_is_not_an_unarmed_or_startup_sentinel(self):
self.run_case('timeout-zero')
def test_adaptive_agc_keeps_valid_floor_cadence_but_not_changed_paths(self):
self.run_case('agc-idle')
def test_old_agc_forced_collection_negative_control(self):
self.run_case('agc-idle', 'agc_forces_floor',
'maintenance must preserve the floor')
def test_old_whole_batch_sleep_veto_negative_control(self):
self.run_case('quiet', 'old_batch_veto', 'pending batch must permit idle')
+3 -3
View File
@@ -80,7 +80,7 @@ struct RadioLibWrapper {
_noise_floor_secondary_valid = true;
bool _nf_window_armed = false, _nf_lazy_refresh = false, _nf_refresh_attempted = false;
bool _cad_enabled = true, packet = false, busy = false, inject_arm_irq = false;
bool _rx_boosted_gain_valid = false, _cur_rx_boosted_gain = false;
bool _rx_boosted_gain_valid = false, _cur_rx_boosted_gain = false, gain_apply_success = true;
bool _wd_last_busy = false;
uint8_t _startrx_fails = 0, _rx_mode_failures = 0;
uint32_t _rx_retry_at = 0;
@@ -117,7 +117,7 @@ struct RadioLibWrapper {
unsigned long getEstAirtimeFor(int) { return 10; }
void stopReceiveDutyCycle() { ++stops; _rx_ps_armed = false; }
void doResetAGC() { ++agc_resets; }
bool applyRxBoostedGainMode(bool) { ++gain_changes; return true; }
bool applyRxBoostedGainMode(bool) { ++gain_changes; return gain_apply_success; }
uint8_t beginReconfigure() { return 0; } // counted SPI/RX boundaries are separate
void endReconfigure(bool) {}
bool supportsRxPowerSaving() const { return true; }
@@ -145,7 +145,7 @@ struct RadioLibWrapper {
void requestNoiseFloorRefresh();
void requestPeriodicNoiseFloorRefresh();
void recalibrateNoiseFloor();
bool resetAGCIfIdle();
bool resetAGCIfIdle(bool refresh_floor = true);
bool tryResetAGC();
bool getNoiseFloorWakeDelay(uint32_t, uint32_t&) const;
void resetAGC();