This page takes you from an unopened box to a live dashboard showing who's in the room, their breathing rate, heart beat, motion — and how far you can honestly push skeletal pose. Every command below is quoted verbatim from RuView's own docs and code, not paraphrased.
✓ Verified against RuView main @ 3d7530f0 (v1701) · committed 2026-06-12 09:09 EDT · this page written 2026-06-12
Don't jump ahead. Each phase has a "you'll know it worked when…" check. If a check fails, stop and fix it — the later phases can't compensate.
| # | Phase | Time | You'll know it worked when… |
|---|---|---|---|
| 1 | Know your brain: Seed vs V0 | 5 min | You know which box you have and what runs where |
| 2 | Prepare the network | 10 min | Reserved IPs; you know your 2.4 GHz network name |
| 3 | Run RuView on simulated data | 2 min | curl /health returns "status":"ok" |
| 4 | Flash the ESP32 nodes | 15 min/node first time | Serial monitor shows the node booting |
| 5 | Provision each node | 2 min/node | tcpdump shows ~10+ pkts/sec on UDP 5005 |
| 6 | Point RuView at real CSI | 5 min | Observatory badge flips DEMO → LIVE |
| 7 | Calibrate the empty room | 1–2 min | room-status says baseline: fresh |
| 8 | Enroll & train your room | ~10 min | Six specialists report ✓ trained |
| 9 | Give the Seed a memory | 15 min | Bridge --stats shows vectors accumulating |
The single biggest beginner confusion. Three machines can be involved, and each has a different job.
| Machine | What it is | Its job in this walkthrough |
|---|---|---|
| Cognitum One Seed | Pi Zero 2 W appliance | The memory: persistent vector store, kNN search, witness chain, MCP tools for AI assistants. It does not run the heavy CSI processing. |
| Cognitum V0 appliance | Pi 5 + Hailo-8 | The always-on LAN core: can run the RuView sensing server itself, plus its calibration dashboard on port 9000. |
| Your Mac/PC | Whatever you have | Flashing the nodes (always), and running the sensing server + Seed bridge (if you don't have a V0). 4 GB RAM minimum, 8 GB recommended; ~2–5 GB disk. |
Mental model: ESP32 nodes are the senses (they capture WiFi-CSI), the sensing server — on your laptop or the V0 — is the perception (it turns CSI into people, breathing, pose), and the Seed is the memory (it stores what was perceived, tamper-evidently, and lets AI agents query it). You can complete phases 1–8 with no Seed at all, then add it in phase 9.
Here's the thing nobody tells you up front: your sensors never stream their raw data to the Seed either way. Raw CSI is a firehose (~50+ packets/sec/node) and the Seed is a Pi Zero — it physically can't run the perception pipeline. Raw CSI always goes to the sensing server on a capable machine (your laptop, or the V0). The real choice is what happens after perception:
| Network-only (no Seed) | Seed-connected | |
|---|---|---|
| What you get | Everything live: dashboards, vitals, pose, alerts — full real-time experience | All of that, plus a permanent record: each second's 8-number room summary stored with a tamper-evident witness chain |
| What happens to history | Forgotten instantly. The server keeps no database — close it and yesterday never happened | Queryable forever: "was anyone in the living room at 3am?" becomes answerable — by you or your AI assistant over MCP |
| What must stay on | The laptop/PC running the server | Same, plus the Seed (~2 W) — and currently a small bridge script on the host (an upstream limitation) |
| Extra capability | — | Seed's own sensors (PIR, temp/humidity) become free ground-truth signals — they make camera-free pose training meaningfully better (10 signals vs 3) |
| Best for | First week: learning, placing nodes, calibrating | Anything you actually want to use: elder care, sleep trends, security, training models |
Our advice: start network-only (phases 1–8) — fewer moving parts while you learn. Add the Seed (phase 10) the day you catch yourself wanting history. The one decision to get right early: provision nodes to a reserved, stable IP, because every node carries that address in its flash. Moving the server later means re-provisioning each node over USB (a 2-minute chore × 7 nodes — avoidable by choosing the stable machine, like a V0, up front). If you own a V0, skip the dilemma: it runs the perception and the always-on role in one box.
ESP32-S3 and C6 nodes only see 2.4 GHz; the C5 is the dual-band exception. If your router broadcasts one combined name, that's fine — but if 2.4 and 5 GHz have separate names, note the 2.4 GHz one. Bonus for C6 nodes: if your router supports WiFi 6 (802.11ax) on 2.4 GHz, enable it — the C6 then captures 242-tone high-fidelity CSI instead of 64-tone (4× the spectral detail). (And if you're wondering why your laptop's far fancier WiFi can't just do the sensing itself: every WiFi chip computes the needed CSI internally, but only the ESP32's maker exposes it — your OS keeps that door locked, which is why these little boards exist at all.)
In your router's DHCP settings, reserve a fixed IP for the machine that will run the sensing server (your laptop or the V0). The nodes are provisioned with this exact IP — if it changes, every node goes silent and you'll think the hardware died. Reserve the Seed's IP too while you're there.
The nodes stream to UDP 5005 on the sensing-server machine. macOS will prompt to allow it; on Linux check ufw; on Windows allow the app through Defender Firewall.
Counter-intuitive but right: get the software working on simulated data first. Then when real hardware misbehaves, you know the problem is the hardware side, not the software side.
docker run -p 3000:3000 -p 3001:3001 ruvnet/wifi-densepose:latest
# then open http://localhost:3000 in your browser
You'll see a Three.js view with a 3D body skeleton, signal heatmap, phase plot and a vitals panel — all from synthetic CSI. The image is multi-arch (Intel/AMD + Apple Silicon).
curl http://localhost:3000/health # Expected: {"status":"ok","source":"simulated","clients":0} curl http://localhost:3000/api/v1/sensing/latest # latest frame curl http://localhost:3000/api/v1/vital-signs # breathing + heart rate curl http://localhost:3000/api/v1/pose/current # 17 COCO keypoints
git clone https://github.com/ruvnet/RuView.git cd RuView/v2 cargo build --release # Rust 1.85+ recommended cargo test --workspace --no-default-features # optional: runs 1,400+ tests # binary lands at target/release/sensing-server
Port gotcha that bites everyone: Docker publishes 3000/3001, but the source binary defaults to 8080/8765. Running from source, pass the ports and UI path explicitly: --http-port 3000 --ws-port 3001 --ui-path ../ui.
Download the prebuilt binaries from RuView Releases ↗. As of today, use v0.7.1-esp32 or newer (fixes a CSI-starvation bug and a heart-rate bug stuck near 45 BPM); v0.8.0-esp32 adds the C6's 242-tone WiFi-6 CSI. The easiest path is still asking Claude Code with the RuView plugin to do this — below is what happens under the hood.
# your serial port: /dev/cu.usbmodem… on macOS, COM7-style on Windows
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
write-flash --flash-mode dio --flash-size 8MB --flash-freq 80m \
0x0 bootloader.bin 0x8000 partition-table.bin \
0xf000 ota_data_initial.bin 0x20000 esp32-csi-node.bin
4 MB boards (S3 SuperMini): use the -4mb binaries from the release and --flash-size 4MB, with partition-table-4mb.bin at 0x8000 and esp32-csi-node-4mb.bin at 0x20000.
Build with current ESP-IDF (the v0.8.0-esp32 release notes say v5.5.2 for full HE-LTF; the in-repo README still documents v5.4 — check the release notes), or grab the C6 binaries from v0.8.0-esp32:
cd firmware/esp32-csi-node
idf.py set-target esp32c6
idf.py build # ~1.0 MB binary
idf.py -p COM6 flash
Then watch it boot with idf.py -p COM6 monitor — you want to see lines like:
I (353) main: ESP32-C6 CSI Node (ADR-018 / ADR-110) — Node ID: 1
I (413) c6_ts: init done: channel=15 EUI=… leader=yes(candidate)
I (463) wifi: mac_version:HAL_MAC_ESP32AX_761 ← the 802.11ax MAC is loaded
Know before you buy/flash: the C6 has no OTA (single partition) — every reflash is over USB. And the C6 build has no mmWave fusion or display support; it's the precision CSI instrument, the S3 is the workhorse.
If nothing happens when you flash: 95% of the time it's a charge-only USB-C cable. The other 5%: hold the board's BOOT button while plugging in to force download mode. (MR60 XIAO: you must open the case and use the inner USB-C port — the edge port is power-only.)
Provisioning writes settings into the node's flash (NVS) — no reflash needed to change them later. One script handles S3 and C6 alike: firmware/esp32-csi-node/provision.py.
python firmware/esp32-csi-node/provision.py --port COM7 \ --ssid "YourWiFi" --password "YourPassword" \ --target-ip 192.168.1.20 --node-id 1
--target-ip is the reserved IP of the sensing-server machine from phase 02. --node-id must be unique per node (1, 2, 3…7) — it's the only way the server tells your nodes apart; there is no discovery handshake. Re-running the script merges new settings, it doesn't wipe old ones.
So nodes take turns transmitting instead of jamming each other:
python firmware/esp32-csi-node/provision.py --port COM7 --tdm-slot 0 --tdm-total 7
python firmware/esp32-csi-node/provision.py --port COM8 --tdm-slot 1 --tdm-total 7
# …and so on through slot 6 for a 7-node arraypython firmware/esp32-csi-node/provision.py --port COM7 \ --ssid "YourWiFi" --password "YourPassword" --target-ip 192.168.1.20 \ --edge-tier 2
Tier 2 makes each node compute presence, breathing, heart-rate, motion and fall flags on the chip (~33 KB RAM) and stream them alongside raw CSI — this is also what the Seed ingests in phase 09.
Mesh beacon authentication (HMAC-SHA256, ADR-032) exists in the firmware but has no provisioning flag yet — skip this; it is not user-configurable today.
sudo tcpdump -ni any udp port 5005 # expect a steady packet stream from each node's IP# Docker: docker run --rm -e CSI_SOURCE=esp32 \ -p 3000:3000 -p 3001:3001 -p 5005:5005/udp \ ruvnet/wifi-densepose:latest # From source (e.g. on the V0): ./target/release/sensing-server --source esp32 --udp-port 5005 \ --http-port 3000 --ws-port 3001 --ui-path ../ui
Only one process can own UDP 5005. If you ran a standalone aggregator for testing, stop it first — the sensing server is the aggregator behind the dashboard, API and WebSocket.
curl http://localhost:3000/health # "source" should no longer say simulated curl http://localhost:3000/api/v1/nodes # every node-id you provisioned, with RSSI + freshness curl http://localhost:3000/api/v1/sensing/latest
| View | URL | What it's for |
|---|---|---|
| Dashboard | http://localhost:3000/ui/index.html | Signal field, vitals, live skeleton |
| Observatory | http://localhost:3000/ui/observatory.html | Cinematic room view + per-node status — your array health screen |
The Observatory auto-detects the live server: its HUD badge flips from DEMO to LIVE. That badge is your single source of truth — if it says DEMO, you're looking at canned data, not your home.
Out of the box the system has never seen your room. Calibration records the empty-room RF fingerprint that everything else is measured against. This is the step almost everyone skips, and it's why their presence detection flickers.
wifi-densepose calibrate --udp-port 5005 --duration-s 60 --output baseline.bin # you must NOT be in the room
The CLI ships in the RuView build (wifi-densepose-cli crate). On a V0 appliance you can instead press Calibrate on its dashboard at http://cognitum-v0:9000. Pets count as "not empty" — shut the cat out.
Recalibrate after: moving furniture, moving/adding any node, or when room-status reports baseline drift. It's deliberate that this never happens automatically — the system can't verify the room is empty by itself, and a baseline captured with someone in the room silently poisons everything downstream.
This is the out-of-box training process you've been looking for. No camera, no dataset downloads — you are the training data. (ADR-151's four-stage flow.)
wifi-densepose enroll --room-id living-room # → "Stand still in view of the sensor…" [✓ anchor accepted: coherence 0.91] # → "Sit down…" [✗ low SNR, retrying] # …walk, lie down, leave the room, etc. — 8 anchor poses total
Each anchor has a quality gate — if your WiFi was noisy during one, it just asks again. Do this once per room.
wifi-densepose train-room --enrollment ./enrollment.json --output ./room-bank.json
Instead of one big model, RuView trains six small statistical specialists calibrated to your room — breathing, heartbeat, restlessness, posture, presence and anomaly. All six always train (there's no flag to pick a subset). They run in microseconds, fully on-device.
wifi-densepose room-status --bank ./room-bank.json # baseline: fresh (drift 0.04 < 0.20) # breathing ✓ trained conf p50 0.88 # heartbeat ✓ trained conf p50 0.71 # posture ✓ 3 prototypes (stand/sit/lie) # anomaly ✓ · presence ✓ · restlessness ✓
The dashboard's skeleton works out of the box — but know what you're looking at, and what it takes to make it real. The training answer everyone asks: calibration is 60 seconds, enrollment ~4 minutes, real pose needs one ~40-minute camera-teacher session + ~1 hour of training — never hours or days of empty-room recording. And it's a 17-point skeleton by physics and by benchmark standard: a 2.4 GHz wave is ~12.5 cm long, so fingers and faces (most of MediaPipe's extra 16 points) are below what the radio can resolve — finer grain comes from sensor fusion (LD6004 Z-axis, 60 GHz radar, camera point-cloud), not more WiFi keypoints.
The default 17-keypoint figure is signal-derived — limbs animated from motion energy, not learned keypoint inference. Great for "is something moving and roughly where," not for "is their left arm raised." The dashboard labels this honestly ("Signal-Derived vs Model Inference").
RuView can train a pose model by fusing up to 10 weak signals — PIR, temperature, humidity, cross-node RSSI triangulation, vibration footsteps, door reed switch, kNN clusters from the Seed's vector store and more:
# with a Cognitum Seed connected (all 10 signals): node scripts/train-camera-free.js \ --data data/recordings/pretrain-*.csi.jsonl \ --seed-url https://169.254.42.1:8443 --seed-token "$SEED_TOKEN" # without a Seed (CSI-only, 3 signals — still works): node scripts/train-camera-free.js \ --data data/recordings/pretrain-*.csi.jsonl --no-seed
Output: an 82.8 KB model (8 KB quantized) with full-skeleton predictions and per-node adapters. Expect coarse-but-plausible poses, not precision.
A webcam + MediaPipe records real poses paired with CSI for 35–40 minutes while you move through varied activities; then the camera goes away forever and the model runs on WiFi alone. (Intriguing hardware option: the XIAO ESP32S3 Sense ↗ is an S3 CSI node with a camera on board — teacher and student in one $27 part, though RuView's documented flow uses a host webcam.)
pip install mediapipe opencv-python python scripts/calibrate-camera-room.py # 5 min, two checkerboards — makes the model survive relocation # Terminal 1 + Terminal 2, simultaneously, 40 minutes: python scripts/record-csi-udp.py --duration 2400 python scripts/collect-ground-truth.py --duration 2400 --preview \ --calibration data/calibration/camera-room.json node scripts/align-ground-truth.js --gt data/ground-truth/*.jsonl --csi data/recordings/csi-*.csi.jsonl node scripts/train-wiflow-supervised.js --data data/paired/*.jsonl --scale small --epochs 50 node scripts/eval-wiflow.js --model models/wiflow-supervised/wiflow-v1.json --data data/paired/*.jsonl
Reality check, from Ruv's own changelog: a previously cited 92.9% accuracy figure was retracted (it came from a flawed protocol). Change activity every 1–2 minutes during capture — 40 minutes of sitting trains a model that only knows sitting. A 5-minute session is not enough, period.
About the pretrained models on Hugging Face: ruvnet/wifi-densepose-pretrained is real (a CSI encoder + presence head, 82.3% held-out triplet accuracy) and ruvnet/wifi-densepose-mmfi-pose reports 82.69% torso-PCK@20 on the MM-Fi benchmark. But the live sensing server's --model flag can't read the published JSONL format yet (a tracked gap) — so for the live dashboard, run without --model and use the weights from the Python/training side. Don't burn an evening on this; it's upstream's gap, not your mistake.
Everything so far is real-time and forgotten. The Seed adds the persistent part: every second, an 8-number summary of the room (presence, motion, breathing, heart rate, phase variance, person count, fall flag, signal strength) is stored with a tamper-evident witness chain — and becomes queryable by AI assistants over MCP.
# Plug the Seed into your laptop via USB — it appears as a network device at 169.254.42.1 curl -sk -X POST https://169.254.42.1:8443/api/v1/pair/window # opens a 30-second pairing window curl -sk -X POST https://169.254.42.1:8443/api/v1/pair \ -H 'Content-Type: application/json' -d '{"client_name":"my-laptop"}' # SAVE THE RETURNED TOKEN — it is shown exactly once
Pairing is USB-only by design — nobody on your WiFi can pair with your Seed.
python firmware/esp32-csi-node/provision.py --port COM9 \ --ssid "YourWiFi" --password "secret" \ --target-ip 192.168.1.20 --target-port 5006 --node-id 1
--target-ip here is your laptop (where the bridge runs) — not the Seed. The bridge is the middleman; current limitation is that it runs on a host machine, not on the Seed itself. Nodes need --edge-tier 2 (phase 05) to produce the feature packets.
export SEED_TOKEN="your-pairing-token" python scripts/seed_csi_bridge.py \ --seed-url https://169.254.42.1:8443 --token "$SEED_TOKEN" \ --udp-port 5006 --batch-size 10 --validate
python scripts/seed_csi_bridge.py --token "$SEED_TOKEN" --stats # vector counts, ingest rate python scripts/seed_csi_bridge.py --token "$SEED_TOKEN" --compact # run periodically — Seed works best under ~100K vectors
From here, AI assistants (Claude, etc.) can query your home's sensing history through the Seed's MCP proxy — "was anyone in the living room at 3am?" becomes an answerable question, with a cryptographic audit trail behind the answer.
Work top-down — each row assumes the rows above it pass.
| Symptom | Cause & fix |
|---|---|
| Flashing fails / no serial port | Charge-only USB-C cable (swap it), or wrong port on the MR60 (use the inner one), or hold BOOT while plugging in. |
| Node won't join WiFi | Wrong band — S3/C6 can't see 5 GHz networks. Re-provision with the 2.4 GHz SSID. |
| tcpdump shows nothing on 5005 | Wrong --target-ip (must be the sensing-server machine, and reserved in the router), or the firewall is eating UDP 5005. |
| Packets arrive but /api/v1/nodes is empty | Another process owns UDP 5005 (stop the standalone aggregator), or Docker is missing -p 5005:5005/udp and -e CSI_SOURCE=esp32. |
| Observatory stuck on DEMO | You opened the file directly — open it through the server: http://localhost:3000/ui/observatory.html. |
| Presence flickers when the room is empty | You skipped calibration (phase 07), or the baseline is stale — recalibrate with the room genuinely empty. |
| Vitals show 0 BPM | Subject must be fairly stationary and within range; give the MR60 ~60 s warm-up; check --edge-tier 2 was provisioned. |
| Heart rate pinned at ~45 BPM | Old firmware — that exact bug was fixed in v0.7.1-esp32. Reflash. |
| C6 CSI looks low-resolution | Firmware built with IDF older than v5.5.2 (per the v0.8.0-esp32 release notes), or your AP isn't doing 802.11ax on 2.4 GHz — both downgrade you to 64-tone CSI. |
| Seed ingest fails with 401 | The pairing token is shown once — re-pair over USB (30-second window) and update $SEED_TOKEN. |
Where these commands come from: RuView's user-guide.md ↗, ADR-069 (Seed pipeline), ADR-135 (calibration), ADR-151 (room training) — all read from main @ 3d7530f0 (v1701), committed 2026-06-12. RuView moves fast (multiple releases a day); if something here drifts, the repo wins and we'll re-verify this page against it.