ESP32 recordings contain sensing_update events with empty CSI; dashboard training parses 0 frames and falls back to live history
Reproduced on image digest sha256:48a71081b6df942530dda4e63bfb9c0aa6c5aa9f5022e9815e176ac8bcac9658, created 2026-08-22.
Summary
I am trying to collect room-specific ESP32 CSI recordings through the RuView dashboard and train from the selected recordings.
The recording files are created successfully and are large/non-empty, but they contain sensing_update JSON events where every node has:
"amplitude": [],
"subcarrier_count": 0
When I select those recordings and click Start Training, the backend can read the files but fails to parse every line as a training frame. It then falls back to 100 in-memory live frames, trains for 20 epochs, early-stops, and produces 0 PCK / 0 OKS.
Is this expected for the current latest image? What supported ESP32 firmware/image/configuration combination produces .csi.jsonl records compatible with training_api?
Environment
- Docker image reference:
ruvnet/wifi-densepose:latest - Resolved image digest:
ruvnet/wifi-densepose@sha256:48a71081b6df942530dda4e63bfb9c0aa6c5aa9f5022e9815e176ac8bcac9658 - Image created:
2026-08-22T22:14:50.126177885Z - Container name:
ruview - Data source:
CSI_SOURCE=esp32 - HTTP API/UI: port 3000
- WebSocket: port 3001
- ESP32 CSI UDP ingress: port 5005
- UDP bind:
0.0.0.0:5005 - UDP source allowlist: enabled
- Persistent data mount:
/opt/stacks/ruvnet-wifi-densepose/data -> /app/data - Training recordings: 8 JSONL recordings, approximately 973 MB total
- Nodes observed in live API: IDs 1, 3, 4, 5, and 6
Expected behavior
Based on the documented recording/training workflow, I expected:
POST /api/v1/recording/start/ dashboard Start Recording to persist usable CSI frames.- Recordings to contain non-empty CSI/amplitude or raw-subcarrier data.
- Selected recordings to be parsed by the dashboard training workflow.
- Training to use the selected recording dataset rather than a 100-frame live-history fallback.
- A clear UI/API error if no selected recording can be parsed.
Actual behavior
1. Recording files are created but use an unexpected shape
Example first line from a recording, with the long payload truncated:
{
"type": "sensing_update",
"timestamp": 1787540226.629,
"source": "esp32",
"tick": 191299,
"nodes": [
{
"node_id": 6,
"rssi_dbm": -66.0,
"position": [2.0, 0.0, 1.5],
"amplitude": [],
"subcarrier_count": 0,
"sync": {
"is_leader": false,
"is_valid": false,
"sequence": 68560,
"csi_fps_ema": 35.54
},
"node_inference": {
"classification": "present_moving",
"confidence": 0.4
}
}
]
}
The original recordings are saved as:
data/recordings/rec_<timestamp>.jsonl
The dashboard training loader looked for:
data/recordings/rec_<timestamp>.csi.jsonl
I created temporary symlinks from the expected .csi.jsonl filenames to the existing .jsonl files only to diagnose whether this was a filename issue. This allowed the loader to open the files, but parsing still failed for every line.
2. Trainer parses zero frames from all selected recordings
Relevant training logs:
INFO sensing_server::training_api: Real supervised training started: 100 epochs, lr=0.0003, lambda=0.0001
INFO sensing_server::training_api: Loaded recording rec_1787540226: 9709 lines, 0 frames, 9709 parse errors
INFO sensing_server::training_api: Loaded recording rec_1787541090: 3730 lines, 0 frames, 3730 parse errors
INFO sensing_server::training_api: Loaded recording rec_1787541233: 3826 lines, 0 frames, 3826 parse errors
INFO sensing_server::training_api: Loaded recording rec_1787541676: 5702 lines, 0 frames, 5702 parse errors
INFO sensing_server::training_api: Loaded recording rec_1787540550: 9820 lines, 0 frames, 9820 parse errors
INFO sensing_server::training_api: Loaded recording rec_1787537286: 3847 lines, 0 frames, 3847 parse errors
INFO sensing_server::training_api: Loaded recording rec_1787541366: 9371 lines, 0 frames, 9371 parse errors
INFO sensing_server::training_api: No recordings found for dataset_ids; falling back to live frame_history
INFO sensing_server::training_api: Loaded 100 frames for training
INFO sensing_server::training_api: Features extracted: 100 samples, 396 features/sample, 51 targets/sample
INFO sensing_server::training_api: Train/val split: 80 train, 20 val
That is 46,005 JSONL lines across the selected recordings, with:
0 parsed training frames
46,005 parse errors
3. Training finishes, but only from fallback frames
INFO sensing_server::training_api: Epoch 20/100: loss=50678.782466, [email protected]=0.0000, val_mse=61536.2854, best_pck_torso_h=0.0000@0, patience=0
INFO sensing_server::training_api: Early stopping at epoch 20 (best=0, [email protected]=0.0000)
INFO sensing_server::training_api: Trained model saved: data/models/trained-supervised-...rvf (20247 params, [email protected]=0.0000)
INFO sensing_server::training_api: Real supervised training finished: phase=completed
/api/v1/train/status then reports:
{
"active": false,
"best_epoch": 0,
"best_pck": 0.0,
"enabled": true,
"epoch": 20,
"eta_secs": 0,
"patience_remaining": 0,
"phase": "completed",
"total_epochs": 100,
"train_loss": 50678.782466,
"val_oks": 0.0,
"val_pck": 0.0
}
Live sensing state
The server appears to receive ESP32-related node traffic:
INFO sensing_server: UDP: 0.0.0.0:5005 (ESP32 CSI)
INFO sensing_server: Source: esp32
INFO sensing_server: Data source: esp32 (udp_receiver=true, simulator=false, wifi=false)
INFO sensing_server: UDP data plane security: bind 0.0.0.0:5005 — ROUTABLE, source allowlist enforced
INFO sensing_server: UDP listening on 0.0.0.0:5005 for ESP32, MediaTek, Qualcomm CSI, and RTL8720F radar frames
However, /api/v1/sensing/latest shows every listed node with empty CSI fields. Example:
{
"node_id": 5,
"rssi_dbm": -53.0,
"amplitude": [],
"subcarrier_count": 0,
"sync": {
"csi_fps_ema": 29.48,
"csi_fps_samples": 5190,
"is_valid": false,
"staleness_ms": 752
}
}
Other relevant live API observations:
frame_rate_hz: 0.0
sync.is_valid: false
The API still reports presence/motion and estimated persons, but all returned pose keypoints have confidence: 0.0, so I do not consider that output valid.
Fusion warnings
The server repeatedly logs multistatic-fusion failures:
WARN sensing_server::engine_bridge:
fusion error: Timestamp spread 71158 us exceeds guard interval 60000 us
WARN sensing_server::engine_bridge:
fusion error: Timestamp spread 102982 us exceeds guard interval 60000 us
WARN sensing_server::engine_bridge:
fusion error: Timestamp spread 123242 us exceeds guard interval 60000 us
All observed nodes have sync.is_valid: false.
I understand this timing problem may affect fused inference quality, but it does not appear to explain why each recording line has empty amplitudes and cannot deserialize as a training frame.
Questions
- Is the dashboard recording endpoint currently expected to write
sensing_updateJSONL records, or should it write raw CSI/training-frame.csi.jsonlrecords? - Is the
.jsonlversus.csi.jsonlfilename mismatch known/fixed in a newer image or commit? - Why does the training UI silently fall back to
frame_historywhen all selected recordings parse as zero frames, instead of reporting a failure? - Which ESP32 firmware version/configuration and
ruvnet/wifi-denseposeimage tag are known to produce non-empty:
and recordings compatible withnodes[].amplitude nodes[].subcarrier_counttraining_api? - Is there a documented raw-CSI capture endpoint or mode distinct from the dashboard Start Recording action?
- Is
--calibratethe recommended workflow for single-room presence/motion calibration while the supervised pose-training path requires a separate MM-Fi/Wi-Pose or paired/labelled dataset workflow? - Are the repeated timestamp-spread fusion errors and
sync.is_valid: falseexpected with the current ESP32 firmware, and what is the supported procedure to establish valid multi-node synchronization?
Additional notes
- I have preserved the original recordings and model files and can provide a redacted first-line sample, file statistics, or additional non-secret logs if needed.
- I will not attach API tokens,
.envcontents, or raw network credentials. - The original recordings are approximately 973 MB total, so I have not attached them directly.
Source: ruvnet/RuView