Compare commits

..

2 Commits

Author SHA1 Message Date
rUv 5789351b78 fix(esp32): add connected-STA self-ping CSI traffic source (#954) (#985)
The ESP32 CSI engine only produces CSI for received OFDM frames (L-LTF/
HT-LTF). On a quiet network — or on a display-enabled build where the
#893 MGMT->MGMT+DATA promiscuous upgrade is skipped (has_display=true) —
the only CSI-eligible frames are sparse beacons (often non-OFDM DSSS),
so wifi_csi_callback can starve to yield=0pps -> DEGRADED -> motion=0
(#521, #954).

Fix (additive): pin a ~50 Hz OFDM unicast floor by pinging the STA's own
DHCP gateway. The router's ICMP echo replies are OFDM frames destined to
this station and drive the CSI engine regardless of promiscuous filter
state or ambient traffic. Mirrors Espressif's esp-csi csi_recv_router
reference. Promiscuous capture (#396/#893) is left fully intact so
multistatic/multi-node sensing still hears other stations' frames.

Reconciles PR #955 (which removed promiscuous entirely and conflicted
with the already-shipped #893 DATA-capture path) into an additive change
on current main.

Verified on ESP32-S3 (N16R8, COM8), ESP-IDF v5.4:
  Promiscuous mode enabled (MGMT-only, RuView#396)
  self-ping started -> 192.168.1.1 @50Hz (CSI OFDM source, fix #521/#954)
  CSI cb #1: len=128 rssi=-40 ch=5
  adaptive_ctrl: state=6 yield=13-19pps motion=1.00 presence>0  (SENSE_ACTIVE)
DEGRADED cleared; CSI yield stable ~15 pps over 60 s.

Co-authored-by: Meraj <merajmehrabi@gmail.com>
2026-06-09 14:43:12 +02:00
rUv b6420ac9ba fix(server): make synthetic CSI opt-in only (sibling fix to #937) (#979)
Background

Issue #937 in the cognitum-v0 appliance repo flagged that the
`cognitum-csi-capture` systemd unit shipped `--simulate` by default,
silently serving synthetic CSI tagged as production telemetry on
`/api/v1/sensor/stream`. That's a textbook trust-eroding pattern — the
single most-cited "where's the real data?" evidence external reviewers
(#943, #934) point at when they call the project AI-slop.

A grep across THIS tree surfaced the exact same anti-pattern in three
places:

  docker/docker-compose.yml:27        # auto (default) — probe ESP32, fall back to simulation
  docker/docker-entrypoint.sh:14      # CSI_SOURCE — data source: auto (default), ...
  main.rs:6435                        info!("No hardware detected, using simulation"); "simulate"

The sensing-server's `auto` source resolver at main.rs:6425-6440
silently fell back to synthetic with only an `info!` log line as the
signal. Downstream consumers calling `/api/v1/sensing/latest` or
`/ws/sensing` had no in-band way to know they were being served fake
data.

Fix

`auto` now refuses to fall back. When neither ESP32 UDP nor host WiFi
is detected, the server logs a clear `error!` explaining the situation
and exits 78 (EX_CONFIG). The error message names the two ways to
proceed: provision real hardware, or set `--source simulated` /
`CSI_SOURCE=simulated` explicitly. Existing operators who already use
`--source simulated` (or its legacy `simulate` alias) are unaffected —
the alias is preserved for back-compat.

Docker entrypoint comment, docker-compose comment, and the Tauri
desktop app's source-default path also updated to reflect the new
posture. The desktop app keeps its `simulated` default because it's
an explicit demo product — the value passed downstream is the
*explicit* `simulated`, not `auto`, so the server tags it correctly
and never lies about its data source.

Validation

  cargo build  -p wifi-densepose-sensing-server --no-default-features
  cargo test   -p wifi-densepose-sensing-server --no-default-features
  → 122 / 122 pass, build clean (existing pre-fix warnings unchanged).

Deployment

⚠ Breaking change for unattended deployments that relied on the
`auto → simulated` silent fallback. That is exactly the failure mode
this PR fixes: pretending to serve real sensing data when the source
is fake. Operators who genuinely want demo mode set
`CSI_SOURCE=simulated` explicitly; the error message and the
docker-compose comment both point them there.
2026-06-08 18:07:39 +02:00
5 changed files with 119 additions and 9 deletions
+5 -2
View File
@@ -24,10 +24,13 @@ services:
environment:
- RUST_LOG=info
# CSI_SOURCE controls the data source for the sensing server.
# Options: auto (default) — probe for ESP32 UDP then fall back to simulation
# Options: auto (default) — probe for ESP32 UDP then host WiFi; **fail
# hard with exit 78 if neither is detected**.
# Synthetic data is no longer a silent fallback
# (issue #937 fix) — operators must opt in.
# esp32 — receive real CSI frames from an ESP32 on UDP port 5005
# wifi — use host Wi-Fi RSSI/scan data (Windows netsh)
# simulated — generate synthetic CSI data (no hardware required)
# simulated — explicitly generate synthetic CSI for demo mode
- CSI_SOURCE=${CSI_SOURCE:-auto}
# MODELS_DIR controls where the server scans for .rvf model files.
# Mount a host directory and set this to make models visible:
+10 -1
View File
@@ -11,7 +11,16 @@
# docker run ruvnet/wifi-densepose:latest --model /app/models/my.rvf
#
# Environment variables:
# CSI_SOURCE — data source: auto (default), esp32, wifi, simulated
# CSI_SOURCE — data source. Valid values:
# auto — try ESP32 then Windows WiFi, **fail-loud if no
# real hardware is detected** (issue #937 fix:
# the server no longer silently falls back to
# synthetic data — that's now opt-in only).
# esp32 — listen for UDP CSI on the configured port.
# wifi — Windows-native WiFi capture.
# simulated — explicit demo mode with synthetic CSI.
# Default is `auto`. Set CSI_SOURCE=simulated when you want
# fake data tagged as such; never set it implicitly.
# MODELS_DIR — directory to scan for .rvf model files (default: data/models)
set -e
@@ -23,6 +23,9 @@
#include "esp_wifi.h"
#include "esp_timer.h"
#include "sdkconfig.h"
#include "esp_netif.h" /* #954: STA gateway lookup for self-ping CSI source */
#include "ping/ping_sock.h" /* #954: esp_ping gateway traffic generator */
#include "lwip/ip_addr.h" /* #954: ip_addr_t target for esp_ping */
/* ADR-060: Access the global NVS config for MAC filter and channel override. */
extern nvs_config_t g_nvs_config;
@@ -365,6 +368,67 @@ static void wifi_promiscuous_cb(void *buf, wifi_promiscuous_pkt_type_t type)
(void)type;
}
/* ---- RuView#521/#954: connected-STA CSI traffic source (additive) ----
*
* The ESP32 CSI engine only produces CSI for received OFDM frames (L-LTF/HT-LTF).
* On a quiet network — or on a display-enabled build where the #893 MGMT->MGMT+DATA
* promiscuous upgrade is skipped (has_display=true) — the only CSI-eligible frames
* are sparse beacons (often non-OFDM DSSS), so wifi_csi_callback can starve to
* yield=0pps -> DEGRADED -> motion/presence=0 (#521, #954).
*
* This guarantees a ~50 Hz OFDM unicast floor by pinging the STA's own gateway:
* the router's ICMP echo replies are OFDM frames destined to this station, which
* drive the CSI engine regardless of promiscuous filter state or ambient traffic.
* It is ADDITIVE — promiscuous capture (#396/#893) is left fully intact so
* multistatic/multi-node sensing still hears other stations' frames. Mirrors
* Espressif's esp-csi csi_recv_router reference.
*/
static esp_ping_handle_t s_self_ping = NULL;
static void csi_ping_cb_noop(esp_ping_handle_t hdl, void *args) { (void)hdl; (void)args; }
static void csi_start_self_ping(void)
{
if (s_self_ping != NULL) {
return; /* already running */
}
esp_netif_t *sta = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
esp_netif_ip_info_t ip;
if (sta == NULL || esp_netif_get_ip_info(sta, &ip) != ESP_OK || ip.gw.addr == 0) {
ESP_LOGW(TAG, "self-ping: no gateway IP yet; CSI relies on ambient frames (#954)");
return;
}
char gw_str[16];
esp_ip4addr_ntoa(&ip.gw, gw_str, sizeof(gw_str));
ip_addr_t target;
memset(&target, 0, sizeof(target));
ipaddr_aton(gw_str, &target);
esp_ping_config_t cfg = ESP_PING_DEFAULT_CONFIG();
cfg.target_addr = target;
cfg.count = ESP_PING_COUNT_INFINITE;
cfg.interval_ms = 20; /* 50 Hz -> ~50 received OFDM replies/sec */
cfg.data_size = 1;
cfg.task_stack_size = 4096;
esp_ping_callbacks_t cbs = {
.cb_args = NULL,
.on_ping_success = csi_ping_cb_noop,
.on_ping_timeout = csi_ping_cb_noop,
.on_ping_end = csi_ping_cb_noop,
};
if (esp_ping_new_session(&cfg, &cbs, &s_self_ping) == ESP_OK && s_self_ping != NULL) {
esp_ping_start(s_self_ping);
ESP_LOGI(TAG, "self-ping started -> %s @50Hz (CSI OFDM source, fix #521/#954)", gw_str);
} else {
ESP_LOGW(TAG, "self-ping: esp_ping_new_session failed");
s_self_ping = NULL;
}
}
void csi_collector_set_node_id(uint8_t node_id)
{
s_node_id = node_id;
@@ -526,6 +590,11 @@ void csi_collector_init(void)
ESP_LOGI(TAG, "CSI collection initialized (node_id=%u, channel=%u)",
(unsigned)s_node_id, (unsigned)csi_channel);
/* RuView#521/#954: start the connected-STA traffic source so the CSI engine
* receives a guaranteed OFDM unicast floor even when promiscuous capture is
* starved (display builds / quiet networks). Additive to #396/#893. */
csi_start_self_ping();
}
/* Accessor for other modules that need the authoritative runtime node_id. */
@@ -108,8 +108,14 @@ pub async fn start_server(
cmd.args(["--log-level", log_level]);
}
// Set data source (default to "simulate" if not specified for demo mode)
let source = config.source.as_deref().unwrap_or("simulate");
// Default to explicit "simulated" demo mode when the desktop user hasn't
// chosen a source — this is the *Tauri demo* app, not a production
// sensing endpoint, so the demo default is correct here. Critically, the
// value passed downstream is the **explicit** "simulated", not "auto",
// which means the sensing-server will tag the data as synthetic in its
// API responses rather than silently fall back (issue #937 fix in
// sensing-server's `auto` handler).
let source = config.source.as_deref().unwrap_or("simulated");
cmd.args(["--source", source]);
// Redirect stdout/stderr to pipes for monitoring
@@ -317,7 +323,7 @@ pub async fn restart_server(
log_level: None,
bind_address: None,
server_path: None,
source: None, // Use default (simulate)
source: None, // Falls through to explicit "simulated" — Tauri demo default.
}
};
@@ -6421,7 +6421,17 @@ async fn main() {
info!(" UI path: {}", args.ui_path.display());
info!(" Source: {}", args.source);
// Auto-detect data source
// Auto-detect data source.
//
// Issue #937 / sibling fix: previously `auto` silently fell back to the
// synthetic data source when no ESP32 or Windows WiFi was reachable, with
// only an `info!` log line as the signal. Downstream API consumers
// (`/api/v1/sensing/latest`, `/ws/sensing`) had no in-band way to know they
// were being served fake CSI tagged as production telemetry. That is the
// exact "where's the real data?" pattern external reviewers (#943, #934)
// cited as the most damaging evidence of the project misrepresenting its
// posture. Synthetic-data is now opt-in only — operators who want demo
// mode must explicitly set `--source simulated` or `CSI_SOURCE=simulated`.
let source = match args.source.as_str() {
"auto" => {
info!("Auto-detecting data source...");
@@ -6432,10 +6442,23 @@ async fn main() {
info!(" Windows WiFi detected");
"wifi"
} else {
info!(" No hardware detected, using simulation");
"simulate"
error!(
"No real CSI source detected. Auto-detection refuses to silently \
fall back to synthetic data because that would expose downstream \
consumers (/api/v1/sensing/latest, /ws/sensing) to fake telemetry \
tagged as production. To run with synthetic data, set the source \
explicitly: --source simulated (or CSI_SOURCE=simulated in Docker). \
To use real hardware: provision an ESP32 to emit CSI on UDP :{} or \
install the Windows WiFi capture driver. See \
https://github.com/ruvnet/RuView/issues/937 for context.",
args.udp_port
);
std::process::exit(78); // EX_CONFIG
}
}
// "simulate" is a synonym for "simulated" (back-compat alias kept so
// existing operators who already opted in don't get broken by this fix).
"simulate" => "simulated",
other => other,
};