From 3779bb765542d4674b56f8289caa635ace19150b Mon Sep 17 00:00:00 2001 From: arsen Date: Sun, 17 May 2026 13:30:08 +0700 Subject: [PATCH] =?UTF-8?q?feat(adr-108):=20NVS=20persistence=20of=20gain-?= =?UTF-8?q?lock=20=E2=80=94=20reboot=20ready=20in=200.5s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-108: after the first successful gain-lock on FW, save the AGC and FFT median values to NVS (namespace "csi_cfg", keys "gl_agc" / "gl_fft"). On every subsequent boot the FW loads them and immediately calls phy_force_rx_gain / phy_fft_scale_force without waiting 300 packets (~3-12 s) for fresh calibration. Mechanics: rv_gain_load_from_nvs / rv_gain_save_to_nvs — small NVS helpers in the gain-lock module. rv_gain_lock_process — `s_nvs_checked` static gate triggers a one- shot load on the first packet after boot. If a saved AGC ≥ MIN_SAFE_AGC is found, lock immediately + mark locked. Otherwise fall through to the existing 300-packet sampler. Existing lock branch — after the median + force_*, save to NVS so the next boot has the values. Verified live: second OTA → 44 Hz raw CSI at WS in the first 3-s sample after boot (was ~5-12 s gap before). Both nodes flashed via WiFi (no USB), no MIN_SAFE_AGC skip in operator's deployment (AGC=44). Tradeoff: NVS values are tied to sensor location + AP MAC + channel + antenna. If the operator moves the sensor or swaps the AP, stale values may be slightly off-optimal until they re-trigger calibration. Today: erase NVS keys via console; future: dedicated FW endpoint. --- firmware/esp32-csi-node/main/csi_collector.c | 73 ++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/firmware/esp32-csi-node/main/csi_collector.c b/firmware/esp32-csi-node/main/csi_collector.c index bd31109d..74dbdedf 100644 --- a/firmware/esp32-csi-node/main/csi_collector.c +++ b/firmware/esp32-csi-node/main/csi_collector.c @@ -21,6 +21,8 @@ #include "esp_log.h" #include "esp_wifi.h" #include "esp_timer.h" +#include "nvs.h" +#include "nvs_flash.h" #include "sdkconfig.h" /* ADR-060: Access the global NVS config for MAC filter and channel override. */ @@ -83,6 +85,51 @@ typedef struct { } rv_phy_rx_ctrl_t; extern void phy_fft_scale_force(bool force_en, int8_t force_value); extern void phy_force_rx_gain(int force_en, int force_value); + +/* ── ADR-108: NVS persistence of gain-lock values ──────────────── + * After the first successful gain-lock, save AGC/FFT medians into NVS + * (namespace "csi_cfg", keys "gl_agc"/"gl_fft"). On subsequent boots + * the FW loads them and immediately forces the gain — reboot → CSI + * ready in ~0.5 s instead of ~3 s waiting for 300 calibration packets. + * + * Stored values are tied to: this sensor location + this AP MAC + + * this channel + this antenna orientation. If any of those change, + * the saved values may be wrong — but harmless: the WiFi PHY will + * just receive slightly off-optimal CSI until the operator triggers + * a re-calibration (today: clear NVS, reboot; future: dedicated REST). + */ +#define RV_GAIN_NVS_NS "csi_cfg" +#define RV_GAIN_NVS_K_AGC "gl_agc" +#define RV_GAIN_NVS_K_FFT "gl_fft" + +static esp_err_t rv_gain_load_from_nvs(uint8_t *agc_out, int8_t *fft_out) +{ + nvs_handle_t h; + esp_err_t err = nvs_open(RV_GAIN_NVS_NS, NVS_READONLY, &h); + if (err != ESP_OK) return err; + uint8_t agc = 0; + int8_t fft = 0; + err = nvs_get_u8(h, RV_GAIN_NVS_K_AGC, &agc); + if (err == ESP_OK) err = nvs_get_i8(h, RV_GAIN_NVS_K_FFT, &fft); + nvs_close(h); + if (err == ESP_OK) { *agc_out = agc; *fft_out = fft; } + return err; +} + +static void rv_gain_save_to_nvs(uint8_t agc, int8_t fft) +{ + nvs_handle_t h; + esp_err_t err = nvs_open(RV_GAIN_NVS_NS, NVS_READWRITE, &h); + if (err != ESP_OK) { + ESP_LOGW("csi_collector", "gain-lock NVS save: nvs_open failed: %s", + esp_err_to_name(err)); + return; + } + nvs_set_u8(h, RV_GAIN_NVS_K_AGC, agc); + nvs_set_i8(h, RV_GAIN_NVS_K_FFT, fft); + nvs_commit(h); + nvs_close(h); +} #define RV_GAIN_CAL_PACKETS 300u #define RV_GAIN_MIN_SAFE_AGC 30u /* < 30 → forcing freezes RX. */ static uint8_t s_agc_samples[RV_GAIN_CAL_PACKETS]; @@ -103,6 +150,28 @@ static int rv_cmp_i8(const void *a, const void *b) { static void rv_gain_lock_process(const wifi_csi_info_t *info) { if (s_gain_locked || info == NULL) return; + + /* ADR-108: short-circuit calibration if previous values are in NVS. */ + static bool s_nvs_checked = false; + if (!s_nvs_checked) { + s_nvs_checked = true; + uint8_t agc = 0; int8_t fft = 0; + if (rv_gain_load_from_nvs(&agc, &fft) == ESP_OK && + agc >= RV_GAIN_MIN_SAFE_AGC) + { + phy_fft_scale_force(true, fft); + phy_force_rx_gain(1, (int)agc); + s_gain_agc_value = agc; + s_gain_fft_value = fft; + s_gain_locked = true; + ESP_LOGI("csi_collector", + "gain-lock RESTORED from NVS: AGC=%u FFT=%d " + "(0-packet calibration; clear NVS to recalibrate)", + (unsigned)agc, (int)fft); + return; + } + } + const rv_phy_rx_ctrl_t *phy = (const rv_phy_rx_ctrl_t *)info; if (s_gain_pkt_count < RV_GAIN_CAL_PACKETS) { @@ -140,6 +209,10 @@ static void rv_gain_lock_process(const wifi_csi_info_t *info) "baseline drift should now collapse.", (unsigned)s_gain_agc_value, (int)s_gain_fft_value, (unsigned)RV_GAIN_CAL_PACKETS); + /* ADR-108: persist for next boot — short-circuit calibration. */ + rv_gain_save_to_nvs(s_gain_agc_value, s_gain_fft_value); + ESP_LOGI(TAG, "gain-lock PERSISTED to NVS (%s/%s, %s)", + RV_GAIN_NVS_NS, RV_GAIN_NVS_K_AGC, RV_GAIN_NVS_K_FFT); } s_gain_locked = true; }