Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot] 21e7d14abb chore: update vendor submodules to latest upstream 2026-06-04 01:12:00 +00:00
6 changed files with 11 additions and 61 deletions
@@ -21,7 +21,6 @@
#include "esp_wifi.h"
#include "esp_mac.h"
#include "esp_timer.h"
#include "esp_idf_version.h"
#include "freertos/FreeRTOS.h"
#include "freertos/timers.h"
#include <string.h>
@@ -145,27 +144,11 @@ static void on_recv(const uint8_t *src_mac, const uint8_t *data, int len)
}
}
/* Issue #944: ESP-IDF v6.0 changed `esp_now_send_cb_t` from
* void (*)(const uint8_t *mac, esp_now_send_status_t status)
* to
* void (*)(const esp_now_send_info_t *tx_info, esp_now_send_status_t status)
* Both signatures ignore the address-side argument here — we only inspect
* `status` to bump the TX-fail counter — so the body is identical; only the
* function-pointer type differs. ESP_IDF_VERSION_MAJOR is the canonical guard.
*/
#if ESP_IDF_VERSION_MAJOR >= 6
static void on_send(const esp_now_send_info_t *tx_info, esp_now_send_status_t status)
{
(void)tx_info;
if (status != ESP_NOW_SEND_SUCCESS) s_tx_fail++;
}
#else
static void on_send(const uint8_t *mac, esp_now_send_status_t status)
{
(void)mac;
if (status != ESP_NOW_SEND_SUCCESS) s_tx_fail++;
}
#endif
static void beacon_timer_cb(TimerHandle_t t)
{
@@ -276,13 +276,6 @@ pub struct FieldNormalMode {
pub geometry_hash: u64,
/// Baseline eigenvalue count above Marcenko-Pastur threshold (empty-room).
pub baseline_eigenvalue_count: usize,
/// Baseline noise variance estimate (median of bottom-half positive
/// eigenvalues from the calibration covariance). Persisted so that
/// `estimate_occupancy` can anchor its Marcenko-Pastur threshold to the
/// calibration noise floor instead of letting it drift with the
/// per-window sample size. Defaults to 0.0 in the diagonal-fallback path.
/// Issue #942.
pub baseline_noise_var: f64,
}
/// Body perturbation extracted from a CSI observation.
@@ -511,11 +504,7 @@ impl FieldModel {
let baseline: Vec<Vec<f64>> = self.link_stats.iter().map(|ls| ls.mean_vector()).collect();
// --- True eigenvalue decomposition (with diagonal fallback) ---
// Returns: (energies, modes, baseline_count, baseline_noise_var).
// The noise_var slot is 0.0 in the diagonal-fallback paths; the
// estimation hot path treats 0.0 as "no anchored noise floor" and
// falls back to per-window noise_var, preserving pre-#942 behavior.
let (mode_energies, environmental_modes, baseline_eig_count, baseline_noise_var) =
let (mode_energies, environmental_modes, baseline_eig_count) =
if let Some(ref cov_sum) = self.covariance_sum {
if self.covariance_count > 1 {
// Compute sample covariance from raw outer products:
@@ -599,28 +588,23 @@ impl FieldModel {
let baseline_count =
eigenvalues.iter().filter(|&&ev| ev > mp_threshold).count();
(energies, modes, baseline_count, noise_var)
(energies, modes, baseline_count)
}
Err(_) => {
// Fallback to diagonal approximation on SVD failure
let (e, m, b) =
diagonal_fallback(&self.link_stats, n_sc, n_modes);
(e, m, b, 0.0_f64)
diagonal_fallback(&self.link_stats, n_sc, n_modes)
}
}
// When eigenvalue feature is disabled, use diagonal fallback
#[cfg(not(feature = "eigenvalue"))]
{
let (e, m, b) = diagonal_fallback(&self.link_stats, n_sc, n_modes);
(e, m, b, 0.0_f64)
diagonal_fallback(&self.link_stats, n_sc, n_modes)
}
} else {
let (e, m, b) = diagonal_fallback(&self.link_stats, n_sc, n_modes);
(e, m, b, 0.0_f64)
diagonal_fallback(&self.link_stats, n_sc, n_modes)
}
} else {
let (e, m, b) = diagonal_fallback(&self.link_stats, n_sc, n_modes);
(e, m, b, 0.0_f64)
diagonal_fallback(&self.link_stats, n_sc, n_modes)
};
// Compute variance explained using the same centered covariance as modes.
@@ -664,7 +648,6 @@ impl FieldModel {
calibrated_at_us: timestamp_us,
geometry_hash,
baseline_eigenvalue_count: baseline_eig_count,
baseline_noise_var,
};
self.modes = Some(field_mode);
@@ -811,7 +794,7 @@ impl FieldModel {
// Marcenko-Pastur noise estimate: median of POSITIVE eigenvalues
// in the bottom half. Excludes zeros from rank-deficient matrices
// (common when n_subcarriers > n_frames, e.g. 56 subcarriers / 50 frames).
let local_noise_var = {
let noise_var = {
let mut positive: Vec<f64> =
eigenvalues.iter().copied().filter(|&e| e > 1e-10).collect();
positive.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
@@ -824,22 +807,6 @@ impl FieldModel {
return Ok(0); // All zero eigenvalues — can't estimate
}
};
// Issue #942: anchor the noise floor to the calibration's noise_var
// when it's available. Per-window noise_var drifts with sample size —
// a short estimation window can produce a small local_noise_var that
// inflates `significant` and breaks the test_estimate_occupancy_noise_only
// invariant. The max of (calibration noise, local noise) keeps the
// threshold from collapsing on small windows while still letting the
// per-window noise dominate when it's the larger estimate. Falls back
// to local_noise_var when baseline_noise_var == 0 (diagonal-fallback
// calibration path, or pre-#942 stored modes).
let noise_var = if modes.baseline_noise_var > 0.0 {
local_noise_var.max(modes.baseline_noise_var)
} else {
local_noise_var
};
let ratio = n as f64 / count as f64;
let mp_threshold = noise_var * (1.0 + ratio.sqrt()).powi(2);
+1 -1