ruvnet / ruvnet/RuView

ESP32 recordings contain sensing_update events with empty CSI; dashboard training parses 0 frames and falls back to live history

Open
#1,703 7 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Rust
Stars
94.5k
Forks
12.5k
Avg merge
21h 27m
Merged PRs (30d)
43

Description

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:

  1. POST /api/v1/recording/start / dashboard Start Recording to persist usable CSI frames.
  2. Recordings to contain non-empty CSI/amplitude or raw-subcarrier data.
  3. Selected recordings to be parsed by the dashboard training workflow.
  4. Training to use the selected recording dataset rather than a 100-frame live-history fallback.
  5. 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, pck_torso_h@0.2=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, pck_torso_h@0.2=0.0000)
INFO sensing_server::training_api: Trained model saved: data/models/trained-supervised-...rvf (20247 params, pck_torso_h@0.2=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

  1. Is the dashboard recording endpoint currently expected to write sensing_update JSONL records, or should it write raw CSI/training-frame .csi.jsonl records?
  2. Is the .jsonl versus .csi.jsonl filename mismatch known/fixed in a newer image or commit?
  3. Why does the training UI silently fall back to frame_history when all selected recordings parse as zero frames, instead of reporting a failure?
  4. Which ESP32 firmware version/configuration and ruvnet/wifi-densepose image tag are known to produce non-empty:
    nodes[].amplitude
    nodes[].subcarrier_count
    
    and recordings compatible with training_api?
  5. Is there a documented raw-CSI capture endpoint or mode distinct from the dashboard Start Recording action?
  6. Is --calibrate the 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?
  7. Are the repeated timestamp-spread fusion errors and sync.is_valid: false expected 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, .env contents, or raw network credentials.
  • The original recordings are approximately 973 MB total, so I have not attached them directly.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing the /api/v1/recording/start output into training_api, comparing the recorded sensing_update JSONL shape and .jsonl/.csi.jsonl lookup with the parser expectations. Check the selected-recording path and its frame_history fallback, then verify the behavior using the reported recordings and /api/v1/train/status. Done means compatible recording data is parsed or the UI/API clearly reports why training cannot proceed.

Written by the indexing model from the issue text.

Assessment

Tech stack
docker, rust
Domain
api, backend, embedded-iot, machine-learning
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.