chore: import upstream snapshot with attribution
Firmware QEMU Tests (ADR-061) / QEMU Test (edge-tier1) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (full-adr060) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (tdm-3node) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / Swarm Test (ADR-062) (push) Has been skipped
npm packages / tools/ruview-mcp (node 22) (push) Failing after 1s
nvsim-server → ghcr.io / build-and-publish (push) Failing after 1s
ruview-swarm CI guard / tests (full+train) (push) Failing after 2s
Bench Regression Guard / bench compile-verify (--no-run) (push) Failing after 0s
Bench Regression Guard / bench fast-run (informational, non-gating) (push) Has been skipped
Firmware CI / Verify version.txt matches release tag (push) Has been skipped
Dashboard a11y + cross-browser / a11y (push) Failing after 0s
nvsim Dashboard → GitHub Pages / build-and-deploy (push) Failing after 2s
Firmware CI / Build firmware (esp32s3 / 4mb) (push) Failing after 15s
Firmware QEMU Tests (ADR-061) / Build Espressif QEMU (push) Failing after 1s
Firmware QEMU Tests (ADR-061) / Fuzz Testing (ADR-061 Layer 6) (push) Failing after 1s
Continuous Deployment / Pre-deployment Checks (push) Has been skipped
Firmware CI / Build firmware (esp32c6 / c6-4mb) (push) Failing after 15s
Firmware CI / Build firmware (esp32s3 / 8mb) (push) Failing after 15s
Firmware QEMU Tests (ADR-061) / QEMU Test (boundary-max) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (boundary-min) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (default) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / QEMU Test (edge-tier0) (push) Has been skipped
Firmware QEMU Tests (ADR-061) / NVS Matrix Generation (push) Failing after 1s
Security Scanning / Security Policy Compliance (push) Failing after 0s
Security Scanning / Dependency Vulnerability Scan (push) Failing after 0s
Security Scanning / Static Application Security Testing (push) Failing after 1s
Security Scanning / Infrastructure Security Scan (push) Failing after 1s
Security Scanning / Secret Scanning (push) Failing after 1s
npm packages / harness/ruview (node 22) (push) Failing after 17s
Security Scanning / License Compliance Scan (push) Failing after 1s
Security Scanning / Container Security Scan (push) Failing after 4s
three.js demos → GitHub Pages / build-and-deploy (push) Failing after 1s
Verify Pipeline Determinism / Verify Pipeline Determinism (3.11) (push) Failing after 1s
Fix-Marker Regression Guard / Verify fix markers (push) Failing after 1s
ADR-115 MQTT integration tests / mqtt-integration (push) Failing after 1s
npm packages / harness/ruview (node 20) (push) Failing after 1s
npm packages / tools/ruview-mcp (node 20) (push) Failing after 1s
npm packages / tools/ruview-cli (node 20) (push) Failing after 1s
npm packages / tools/ruview-cli (node 22) (push) Failing after 1s
BFLD MQTT Integration / cargo test --features mqtt (live mosquitto) (push) Failing after 29s
ruview-swarm CI guard / build train_marl bin (push) Failing after 2s
ruview-swarm CI guard / clippy (-D warnings, --no-deps) (push) Failing after 3s
ruview-swarm CI guard / tests (ruflo) (push) Failing after 1s
ruview-swarm CI guard / tests (train) (push) Failing after 2s
ruview-swarm CI guard / tests (default) (push) Failing after 2s
Point Cloud Viewer → GitHub Pages / build-and-deploy (push) Failing after 8s
ruview-swarm CI guard / ITAR / publish guard (push) Failing after 0s
wifi-densepose sensing-server → Docker Hub + ghcr.io / build · push · smoke-test (push) Failing after 1s
Continuous Deployment / Deploy to Production (push) Has been cancelled
Continuous Deployment / Rollback Deployment (push) Has been cancelled
Continuous Deployment / Post-deployment Monitoring (push) Has been cancelled
Continuous Deployment / Notify Deployment Status (push) Has been cancelled
Continuous Deployment / Deploy to Staging (push) Has been cancelled
Security Scanning / Security Report (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 11:59:54 +08:00
commit 9740bc64c9
2636 changed files with 1330642 additions and 0 deletions
+917
View File
@@ -0,0 +1,917 @@
# ESP32 CSI to Cognitum Seed Pretraining Pipeline
A beginner-friendly tutorial for collecting WiFi CSI data with ESP32 nodes
and building a pre-trained model using the Cognitum Seed edge intelligence appliance.
**Estimated time:** 1 hour (setup 20 min, data collection 30 min, verification 10 min)
**What you will build:** A self-supervised pretraining dataset stored on a
Cognitum Seed, containing 8-dimensional feature vectors extracted from live
WiFi Channel State Information. The Seed's RVF vector store, kNN search, and
witness chain turn raw radio signals into a searchable, cryptographically
attested knowledge base -- no cameras or manual labeling required.
**Who this is for:** Makers, embedded engineers, and ML practitioners who want
to experiment with WiFi-based human sensing. No Rust knowledge is needed; the
entire workflow uses Python and pre-built firmware binaries.
---
## Table of Contents
1. [Prerequisites](#1-prerequisites)
2. [Hardware Setup](#2-hardware-setup)
3. [Running the Bridge](#3-running-the-bridge)
4. [Data Collection Protocol](#4-data-collection-protocol)
5. [Monitoring Progress](#5-monitoring-progress)
6. [Understanding the Feature Vectors](#6-understanding-the-feature-vectors)
7. [Using the Pre-trained Data](#7-using-the-pre-trained-data)
8. [Troubleshooting](#8-troubleshooting)
9. [Next Steps](#9-next-steps)
---
## 1. Prerequisites
### Hardware
| Item | Quantity | Approx. Cost | Notes |
|------|----------|-------------|-------|
| ESP32-S3 (8MB flash) | 2 | ~$9 each | Must be S3 variant -- original ESP32 and C3 are not supported (single-core, cannot run CSI DSP) |
| Cognitum Seed (Pi Zero 2 W) | 1 | ~$15 | Available at [cognitum.one](https://cognitum.one) |
| USB-C data cables | 3 | ~$3 each | Must be **data** cables, not charge-only |
**Total cost: ~$36**
### Software
Install these on your host laptop/desktop (Windows, macOS, or Linux):
```bash
# Python 3.10 or later
python --version
# Expected: Python 3.10.x or later
# esptool for flashing firmware
pip install esptool
# pyserial for serial monitoring (optional but useful)
pip install pyserial
```
> **Tip:** You do not need the Rust toolchain for this tutorial. The ESP32
> firmware is distributed as pre-built binaries, and the bridge script is
> pure Python.
### Firmware
Download the v0.5.4 firmware binaries from the GitHub releases page:
```
esp32-csi-node.bin -- Main firmware (8MB flash)
bootloader.bin -- Bootloader
partition-table.bin -- Partition table
ota_data_initial.bin -- OTA data
```
### Network
All devices must be on the same WiFi network. You will need:
- Your WiFi SSID and password
- Your host laptop's local IP address (e.g., `192.168.1.20`)
Find your host IP:
```bash
# Windows
ipconfig | findstr "IPv4"
# macOS / Linux
ip addr show | grep "inet " | grep -v 127.0.0.1
```
---
## 2. Hardware Setup
### Physical Layout
```
┌─────────────────────────────────────────────────┐
│ Room │
│ │
│ [ESP32 #1] [ESP32 #2] │
│ node_id=1 node_id=2 │
│ on shelf on desk │
│ ~1.5m high ~0.8m high │
│ │
│ 3-5 meters apart │
│ │
│ [Cognitum Seed] │
│ on table, USB to laptop │
│ │
│ [Host Laptop] │
│ running bridge script │
└─────────────────────────────────────────────────┘
```
> **Tip:** Place the two ESP32 nodes 3-5 meters apart at different heights.
> This gives the multi-node pipeline spatial diversity, which improves the
> quality of cross-viewpoint features.
### Step 2.1: Connect and Verify the Cognitum Seed
Plug the Cognitum Seed into your laptop using a USB **data** cable.
Wait 30-60 seconds for it to boot. Then verify connectivity:
```bash
curl -sk https://169.254.42.1:8443/api/v1/status
```
Expected output (abbreviated):
```json
{
"device_id": "ecaf97dd-fc90-4b0e-b0e7-e9f896b9fbb6",
"total_vectors": 0,
"epoch": 1,
"dimension": 8,
"uptime_secs": 45
}
```
> **Note:** The `-sk` flags tell curl to use HTTPS (`-s` silent, `-k` skip
> TLS certificate verification). The Seed uses a self-signed certificate.
You can also open `https://169.254.42.1:8443/guide` in a browser (accept
the self-signed certificate warning) to see the Seed's setup guide.
### Step 2.2: Pair the Seed
Pairing generates a bearer token that authorizes write access. Pairing can
only be initiated from the USB interface (169.254.42.1), not from WiFi -- this
is a security feature.
```bash
curl -sk -X POST https://169.254.42.1:8443/api/v1/pair \
-H "Content-Type: application/json" \
-d '{"client_name": "wifi-densepose-tutorial"}'
```
Expected output:
```json
{
"token": "seed_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"expires": null,
"permissions": ["read", "write", "admin"]
}
```
Save this token -- you will need it for every bridge command:
```bash
export SEED_TOKEN="seed_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```
> **Warning:** Treat the token like a password. Do not commit it to git or
> share it publicly.
### Step 2.3: Flash ESP32 #1
Connect the first ESP32-S3 to your laptop via USB. Identify its serial port:
```bash
# Windows -- look for "Silicon Labs" or "CP210x" in Device Manager
# or run:
python -m serial.tools.list_ports
# macOS
ls /dev/tty.usb*
# Linux
ls /dev/ttyUSB* /dev/ttyACM*
```
Flash the firmware (replace `COM9` with your port):
```bash
esptool.py --chip esp32s3 --port COM9 --baud 460800 \
write_flash \
0x0 bootloader.bin \
0x8000 partition-table.bin \
0xd000 ota_data_initial.bin \
0x10000 esp32-csi-node.bin
```
Expected output (last lines):
```
Writing at 0x000f4000... (100 %)
Wrote 978432 bytes (...)
Hash of data verified.
Leaving...
Hard resetting via RTS pin...
```
### Step 2.4: Provision ESP32 #1
Tell the ESP32 which WiFi network to join and where to send data:
```bash
python firmware/esp32-csi-node/provision.py \
--port COM9 \
--ssid "YourWiFi" \
--password "YourPassword" \
--target-ip 192.168.1.20 \
--target-port 5006 \
--node-id 1
```
Replace:
- `COM9` with your actual serial port
- `YourWiFi` / `YourPassword` with your WiFi credentials
- `192.168.1.20` with your host laptop's IP address
Expected output:
```
Writing NVS partition (24576 bytes) at offset 0x9000...
Provisioning complete. Reset the device to apply.
```
> **Important:** The `--target-ip` is your **host laptop**, not the Seed.
> The bridge script runs on your laptop and forwards vectors to the Seed
> via HTTPS.
### Step 2.5: Verify ESP32 #1 Is Streaming
After provisioning, the ESP32 resets and begins streaming. Verify with a
quick UDP listener:
```bash
python -c "
import socket, struct
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('0.0.0.0', 5006))
sock.settimeout(10)
print('Listening on UDP 5006 for 10 seconds...')
count = 0
try:
while True:
data, addr = sock.recvfrom(2048)
magic = struct.unpack_from('<I', data)[0]
names = {0xC5110001: 'CSI_RAW', 0xC5110002: 'VITALS', 0xC5110003: 'FEATURES'}
name = names.get(magic, f'UNKNOWN(0x{magic:08X})')
count += 1
if count <= 5:
print(f' Packet {count}: {name} from {addr[0]} ({len(data)} bytes)')
except socket.timeout:
pass
sock.close()
print(f'Received {count} packets total')
"
```
Expected output:
```
Listening on UDP 5006 for 10 seconds...
Packet 1: VITALS from 192.168.1.105 (32 bytes)
Packet 2: FEATURES from 192.168.1.105 (48 bytes)
Packet 3: VITALS from 192.168.1.105 (32 bytes)
Packet 4: FEATURES from 192.168.1.105 (48 bytes)
Packet 5: VITALS from 192.168.1.105 (32 bytes)
Received 20 packets total
```
If you see 0 packets, check the [Troubleshooting](#8-troubleshooting) section.
### Step 2.6: Flash and Provision ESP32 #2
Repeat steps 2.3-2.5 for the second ESP32, using `--node-id 2`:
```bash
# Flash (replace COM8 with your port)
esptool.py --chip esp32s3 --port COM8 --baud 460800 \
write_flash \
0x0 bootloader.bin \
0x8000 partition-table.bin \
0xd000 ota_data_initial.bin \
0x10000 esp32-csi-node.bin
# Provision
python firmware/esp32-csi-node/provision.py \
--port COM8 \
--ssid "YourWiFi" \
--password "YourPassword" \
--target-ip 192.168.1.20 \
--target-port 5006 \
--node-id 2
```
### Step 2.7: Verify Both Nodes
Run the UDP listener again. You should see packets from two different IPs:
```
Packet 1: FEATURES from 192.168.1.105 (48 bytes) <-- node 1
Packet 2: FEATURES from 192.168.1.104 (48 bytes) <-- node 2
Packet 3: VITALS from 192.168.1.105 (32 bytes)
Packet 4: VITALS from 192.168.1.104 (32 bytes)
```
---
## 3. Running the Bridge
The bridge script (`scripts/seed_csi_bridge.py`) listens for UDP packets
from the ESP32 nodes, batches them, and ingests them into the Seed's RVF
vector store via HTTPS.
### Basic Start
```bash
python scripts/seed_csi_bridge.py \
--seed-url https://169.254.42.1:8443 \
--token "$SEED_TOKEN" \
--udp-port 5006 \
--batch-size 10
```
Expected output:
```
12:00:01 [INFO] Connected to Seed ecaf97dd — 0 vectors, epoch 1, dim 8
12:00:01 [INFO] Listening on UDP port 5006 (batch size: 10, flush interval: 10s)
12:00:11 [INFO] Ingested 10 vectors (epoch=2, witness=a3b7c9d2e4f6...)
12:00:21 [INFO] Ingested 10 vectors (epoch=3, witness=f1e2d3c4b5a6...)
```
### Bridge Flags Explained
| Flag | Default | Description |
|------|---------|-------------|
| `--seed-url` | `https://169.254.42.1:8443` | Seed HTTPS endpoint (USB link-local) |
| `--token` | `$SEED_TOKEN` env var | Bearer token from pairing step |
| `--udp-port` | `5006` | UDP port to listen for ESP32 packets |
| `--batch-size` | `10` | Number of vectors per ingest call |
| `--flush-interval` | `10` | Maximum seconds between flushes (time-based batching) |
| `--validate` | off | After each batch, run kNN query + PIR comparison |
| `--stats` | off | Print Seed stats and exit (no bridge loop) |
| `--compact` | off | Trigger store compaction and exit |
| `--allowed-sources` | none | Comma-separated IPs to accept (anti-spoofing) |
| `-v` / `--verbose` | off | Log every received packet |
### Recommended: Validation Mode
For your first data collection, enable `--validate` so the bridge verifies
each batch against the Seed's kNN index:
```bash
python scripts/seed_csi_bridge.py \
--seed-url https://169.254.42.1:8443 \
--token "$SEED_TOKEN" \
--udp-port 5006 \
--batch-size 10 \
--validate
```
With validation enabled, you will see additional output after each batch:
```
12:00:11 [INFO] Ingested 10 vectors (epoch=2, witness=a3b7c9d2...)
12:00:11 [INFO] Validation: kNN distance=0.000000 (exact match)
12:00:11 [INFO] PIR=LOW CSI_presence=0.14 (absent) -- agreement 100.0% (1/1)
```
### Recommended: Source IP Filtering
If you are on a shared network, restrict the bridge to only accept packets
from your ESP32 nodes:
```bash
python scripts/seed_csi_bridge.py \
--token "$SEED_TOKEN" \
--udp-port 5006 \
--batch-size 10 \
--allowed-sources "192.168.1.104,192.168.1.105"
```
---
## 4. Data Collection Protocol
Collect 6 scenarios, 5 minutes each, for a total of 30 minutes of data.
With 2 nodes at 1 Hz each, each scenario produces ~600 feature vectors.
> **Before you begin:** Make sure the bridge is running (Section 3). Leave
> the terminal open and start a new terminal for the commands below.
### Scenario 1: Empty Room (5 min)
This establishes the baseline -- what the room looks like with no one in it.
```bash
echo "=== SCENARIO 1: EMPTY ROOM ==="
echo "Leave the room now. Data collection starts in 10 seconds."
sleep 10
echo "Recording for 5 minutes... ($(date))"
sleep 300
echo "Done. You may re-enter the room."
```
**What to do:** Leave the room. Close the door if possible. Stay out for
the full 5 minutes.
### Scenario 2: One Person Stationary (5 min)
```bash
echo "=== SCENARIO 2: 1 PERSON STATIONARY ==="
echo "Sit at a desk or chair. Stay still. Breathe normally."
sleep 300
echo "Done."
```
**What to do:** Sit at a desk roughly between the two ESP32 nodes. Stay
still. Breathe normally. Do not use your phone (arm movement adds noise).
### Scenario 3: One Person Walking (5 min)
```bash
echo "=== SCENARIO 3: 1 PERSON WALKING ==="
echo "Walk around the room at a normal pace."
sleep 300
echo "Done."
```
**What to do:** Walk around the room in varied paths. Go near each ESP32
node at least once. Walk at a normal pace -- not too fast, not too slow.
### Scenario 4: One Person Varied Activity (5 min)
```bash
echo "=== SCENARIO 4: 1 PERSON VARIED ==="
echo "Move around: stand, sit, wave arms, turn in place."
sleep 300
echo "Done."
```
**What to do:** Mix activities. Stand up, sit down, wave your arms, turn
around, reach for a shelf, crouch down. The goal is to capture a variety of
body positions and motions.
### Scenario 5: Two People (5 min)
```bash
echo "=== SCENARIO 5: TWO PEOPLE ==="
echo "Two people in the room, both moving around."
sleep 300
echo "Done."
```
**What to do:** Have a second person enter the room. Both people should
move around naturally -- walking, sitting, standing at different positions.
### Scenario 6: Transitions (5 min)
```bash
echo "=== SCENARIO 6: TRANSITIONS ==="
echo "Enter and exit the room repeatedly."
sleep 300
echo "Done."
```
**What to do:** Walk in and out of the room several times. Pause for
30-60 seconds inside, then leave for 30-60 seconds. This teaches the model
what state transitions look like.
### Expected Data Volume
After all 6 scenarios:
| Metric | Expected |
|--------|----------|
| Total time | 30 minutes |
| Vectors per node | ~1,800 |
| Total vectors (2 nodes) | ~3,600 |
| RVF store size | ~150 KB |
| Witness chain entries | ~360+ |
---
## 5. Monitoring Progress
### Check Seed Stats
At any time, open a new terminal and run:
```bash
python scripts/seed_csi_bridge.py --token "$SEED_TOKEN" --stats
```
Expected output (after completing all 6 scenarios):
```
=== Seed Status ===
Device ID: ecaf97dd-fc90-4b0e-b0e7-e9f896b9fbb6
Total vectors: 3612
Epoch: 362
Dimension: 8
Uptime: 3845s
=== Witness Chain ===
Valid: True
Chain length: 1747
Head: a3b7c9d2e4f6g8h1i2j3k4l5m6n7...
=== Boundary Analysis ===
Fragility score: 0.42
Boundary count: 6
=== Coherence Profile ===
phase_count: 6
current_phase: 5
coherence: 0.87
=== kNN Graph Stats ===
nodes: 3612
edges: 18060
avg_degree: 5.0
```
> **What to look for:**
> - `Total vectors` should grow by ~2 per second (1 per node per second)
> - `Valid: True` on the witness chain means no data tampering
> - `Fragility score` rises during transitions and drops during stable
> scenarios -- this is normal and expected
> - `phase_count` should roughly correspond to the number of distinct
> scenarios the Seed has observed
### Verify kNN Quality
Query the Seed for the 5 nearest neighbors to a "someone present" vector:
```bash
curl -sk -X POST https://169.254.42.1:8443/api/v1/store/query \
-H "Authorization: Bearer $SEED_TOKEN" \
-H "Content-Type: application/json" \
-d '{"vector": [0.8, 0.5, 0.5, 0.6, 0.5, 0.25, 0.0, 0.6], "k": 5}'
```
Expected output:
```json
{
"results": [
{"id": 2847193655, "distance": 0.023},
{"id": 1038476291, "distance": 0.031},
{"id": 3719284651, "distance": 0.045},
{"id": 928374651, "distance": 0.052},
{"id": 1847293746, "distance": 0.068}
]
}
```
Low distances (< 0.1) indicate the query vector is similar to stored
vectors -- the store contains meaningful data.
### Verify Witness Chain
The witness chain is a SHA-256 hash chain that proves no vectors were
tampered with after ingestion:
```bash
curl -sk -X POST https://169.254.42.1:8443/api/v1/witness/verify \
-H "Authorization: Bearer $SEED_TOKEN"
```
Expected output:
```json
{
"valid": true,
"chain_length": 1747,
"head": "a3b7c9d2e4f6..."
}
```
> **Warning:** If `valid` is `false`, the witness chain has been broken.
> This means data was modified outside the normal ingest path. Discard
> the dataset and re-collect.
---
## 6. Understanding the Feature Vectors
Each ESP32 node extracts an 8-dimensional feature vector once per second
from the 100 Hz CSI processing pipeline. Every dimension is normalized to
the range 0.0 to 1.0.
### Feature Dimension Table
| Dim | Name | Raw Source | Normalization | Range | Example Values |
|-----|------|-----------|---------------|-------|----------------|
| 0 | Presence score | `presence_score` | `/ 15.0`, clamped | 0.0 -- 1.0 | Empty: 0.01-0.05, Occupied: 0.19-1.0 |
| 1 | Motion energy | `motion_energy` | `/ 10.0`, clamped | 0.0 -- 1.0 | Still: 0.05-0.15, Walking: 0.3-0.8 |
| 2 | Breathing rate | `breathing_bpm` | `/ 30.0`, clamped | 0.0 -- 1.0 | Normal: 0.5-0.8 (15-24 BPM), At rest: 0.67-1.0 (20-34 BPM observed) |
| 3 | Heart rate | `heartrate_bpm` | `/ 120.0`, clamped | 0.0 -- 1.0 | Resting: 0.50-0.67 (60-80 BPM), Active: 0.63-0.83 (75-99 BPM observed) |
| 4 | Phase variance | Welford variance | Mean of top-K subcarriers | 0.0 -- 1.0 | Stable: 0.1-0.3, Disturbed: 0.5-0.9 |
| 5 | Person count | `n_persons / 4.0` | Clamped to [0, 1] | 0.0 -- 1.0 | 0 people: 0.0, 1 person: 0.25, 2 people: 0.5 |
| 6 | Fall detected | Binary flag | 1.0 if fall, else 0.0 | 0.0 or 1.0 | Normal: 0.0, Fall event: 1.0 |
| 7 | RSSI | `(rssi + 100) / 100` | Clamped to [0, 1] | 0.0 -- 1.0 | Close: 0.57-0.66 (-43 to -34 dBm), Far: 0.28-0.40 (-72 to -60 dBm) |
### How to Read a Feature Vector
Example vector from live validation:
```
[0.99, 0.47, 0.67, 0.63, 0.50, 0.25, 0.00, 0.57]
```
Reading this:
- **0.99** (dim 0, presence) -- Strong presence detected
- **0.47** (dim 1, motion) -- Moderate motion (slow walking or fidgeting)
- **0.67** (dim 2, breathing) -- 20.1 BPM (0.67 x 30), normal at-rest breathing
- **0.63** (dim 3, heart rate) -- 75.6 BPM (0.63 x 120), normal resting heart rate
- **0.50** (dim 4, phase variance) -- Placeholder (future use)
- **0.25** (dim 5, person count) -- 1 person (0.25 x 4 = 1)
- **0.00** (dim 6, fall) -- No fall detected
- **0.57** (dim 7, RSSI) -- RSSI of -43 dBm ((0.57 x 100) - 100), strong signal
### Packet Format
The feature vector is transmitted as a 48-byte binary packet with magic
number `0xC5110003`:
```
Offset Size Type Field
------ ---- ------- ----------------
0 4 uint32 magic (0xC5110003)
4 1 uint8 node_id
5 1 uint8 reserved
6 2 uint16 sequence number
8 8 int64 timestamp (microseconds since boot)
16 32 float[8] feature vector (8 x 4 bytes)
------ ----
Total: 48 bytes
```
---
## 7. Using the Pre-trained Data
After collecting 30 minutes of data, the Seed holds ~3,600 feature vectors
organized as a kNN graph with witness chain attestation.
### Query for Similar States
Find vectors similar to "one person sitting quietly":
```bash
curl -sk -X POST https://169.254.42.1:8443/api/v1/store/query \
-H "Authorization: Bearer $SEED_TOKEN" \
-H "Content-Type: application/json" \
-d '{"vector": [0.8, 0.1, 0.6, 0.6, 0.5, 0.25, 0.0, 0.5], "k": 10}'
```
Find vectors similar to "empty room":
```bash
curl -sk -X POST https://169.254.42.1:8443/api/v1/store/query \
-H "Authorization: Bearer $SEED_TOKEN" \
-H "Content-Type: application/json" \
-d '{"vector": [0.05, 0.02, 0.0, 0.0, 0.3, 0.0, 0.0, 0.5], "k": 10}'
```
### Environment Fingerprinting
The Seed's boundary analysis detects regime changes in the vector space.
When someone enters or leaves the room, the fragility score spikes:
```bash
curl -sk https://169.254.42.1:8443/api/v1/boundary
```
```json
{
"fragility_score": 0.42,
"boundary_count": 6
}
```
A `fragility_score` above 0.3 indicates the environment is in or near a
transition state. The `boundary_count` roughly corresponds to the number
of distinct "states" (scenarios) the Seed has observed.
### Export Vectors
To export all vectors for offline analysis or training:
```bash
curl -sk https://169.254.42.1:8443/api/v1/store/export \
-H "Authorization: Bearer $SEED_TOKEN" \
-o pretrain-vectors.rvf
```
The exported `.rvf` file contains the raw vector data and can be loaded
by the Rust training pipeline (`wifi-densepose-train` crate) or converted
to NumPy arrays for Python-based training.
### Compact the Store
For long-running deployments, run compaction daily to keep the store
within the Seed's memory budget:
```bash
python scripts/seed_csi_bridge.py --token "$SEED_TOKEN" --compact
```
```
Triggering store compaction...
Compaction result: {
"vectors_before": 3612,
"vectors_after": 3200,
"bytes_freed": 16544
}
```
### Use with the Sensing Server
Start a recording session to capture the raw CSI frames alongside the
feature vectors (the sensing-server provides the recording API):
```bash
# Start the recording (5 minutes)
curl -X POST http://localhost:3000/api/v1/recording/start \
-H "Content-Type: application/json" \
-d '{"session_name":"pretrain-1p-still","label":"1p-still","duration_secs":300}'
```
The recording saves `.csi.jsonl` files that the `wifi-densepose-train`
crate can load for full contrastive pretraining (see ADR-070).
---
## 8. Troubleshooting
### ESP32 Won't Connect to WiFi
**Symptoms:** No packets received, ESP32 serial output shows repeated
"WiFi: Connecting..." messages.
**Fixes:**
1. Verify SSID and password are correct (re-provision if needed)
2. Make sure you are on a 2.4 GHz network (ESP32 does not support 5 GHz)
3. Move the ESP32 closer to the access point
4. Check the serial output for the exact error:
```bash
python -m serial.tools.miniterm COM9 115200
```
Look for lines like `wifi:connected` or `wifi:reason 201` (wrong password).
### Bridge Shows 0 Packets
**Symptoms:** Bridge starts but never logs "Ingested" messages.
**Fixes:**
1. Make sure the ESP32's `--target-ip` matches your laptop's IP
2. Check that `--target-port` matches `--udp-port` on the bridge (default: 5006)
3. Check your firewall -- UDP port 5006 must be open for inbound traffic
4. Run the UDP listener test from Section 2.5 to confirm raw packets arrive
5. If using `--allowed-sources`, make sure the ESP32 IP addresses are listed
### Seed Returns 401 Unauthorized
**Symptoms:** Bridge logs `HTTP Error 401` on ingest.
**Fixes:**
1. Make sure `$SEED_TOKEN` is set correctly: `echo $SEED_TOKEN`
2. Re-pair the Seed if the token was lost (Section 2.2)
3. Verify the token works with a status query:
```bash
curl -sk -H "Authorization: Bearer $SEED_TOKEN" \
https://169.254.42.1:8443/api/v1/store/graph/stats
```
### NaN Values in Features
**Symptoms:** Bridge logs `Dropping feature packet: features[X]=nan (NaN/inf)`.
**Fixes:**
- This is expected during the first few seconds after ESP32 boot while the
DSP pipeline initializes. The bridge automatically drops NaN/inf packets.
- If NaN persists beyond 10 seconds, reflash the firmware -- the DSP state
may be corrupted.
### ENOMEM on ESP32 Boot
**Symptoms:** Serial output shows `E (xxx) heap: alloc failed` or
`ENOMEM` errors.
**Fixes:**
1. If using a 4MB flash ESP32-S3, use the 4MB partition table and
sdkconfig (see `sdkconfig.defaults.4mb`)
2. Reduce buffer sizes by setting edge tier to 1 during provisioning:
```bash
python firmware/esp32-csi-node/provision.py \
--port COM9 --edge-tier 1 \
--ssid "YourWiFi" --password "YourPassword" \
--target-ip 192.168.1.20 --node-id 1
```
### Seed Not Reachable at 169.254.42.1
**Symptoms:** `curl` to `169.254.42.1:8443` times out.
**Fixes:**
1. Ensure you are using a **data** USB cable (charge-only cables lack data pins)
2. Wait 60 seconds after plugging in for the Seed to fully boot
3. Check the USB network interface appeared on your host:
```bash
# Windows
ipconfig | findstr "169.254"
# macOS / Linux
ip addr show | grep "169.254"
```
4. If the Seed is on WiFi instead, use its WiFi IP (e.g., `192.168.1.109`):
```bash
python scripts/seed_csi_bridge.py \
--seed-url https://192.168.1.109:8443 \
--token "$SEED_TOKEN"
```
### Bridge Ingest Failures (Connection Reset)
**Symptoms:** Periodic `Ingest failed` messages, then recovery.
**Fixes:**
- The bridge retries once automatically (2-second delay). Occasional failures
are normal when the Seed is rebuilding its kNN graph.
- If failures are frequent (>10% of batches), increase `--batch-size` to
reduce the number of HTTPS calls:
```bash
python scripts/seed_csi_bridge.py --token "$SEED_TOKEN" --batch-size 20
```
---
## 9. Next Steps
### Full Contrastive Pretraining (ADR-070)
This tutorial covers Phase 1 (data collection) of the pretraining pipeline
defined in [ADR-070](../adr/ADR-070-self-supervised-pretraining.md). The
remaining phases are:
- **Phase 2: Contrastive pretraining** -- Train a TCN encoder using temporal
coherence and multi-node consistency as self-supervised signals
- **Phase 3: Downstream heads** -- Attach task-specific heads (presence,
person count, activity, vital signs) using weak labels from the Seed's
PIR sensor and scenario boundaries
- **Phase 4: Package and distribute** -- Export as ONNX model weights for
distribution in GitHub releases
### Architecture Documentation
- [ADR-069: ESP32 CSI to Cognitum Seed Pipeline](../adr/ADR-069-cognitum-seed-csi-pipeline.md) --
Full architecture of the bridge pipeline
- [ADR-070: Self-Supervised Pretraining](../adr/ADR-070-self-supervised-pretraining.md) --
Complete pretraining pipeline design
### Multi-Node Mesh
Scale to 3-4 ESP32 nodes for better spatial coverage. Each node gets a
unique `--node-id` and all target the same host laptop. The Seed's kNN
graph naturally clusters vectors by node and sensing state.
### Cognitum Seed Resources
- [cognitum.one](https://cognitum.one) -- Hardware and firmware information
- Seed API: 98 HTTPS endpoints with bearer token authentication
- MCP proxy: 114 tools accessible via JSON-RPC 2.0 for AI assistant integration
### Rust Training Pipeline
For users with the Rust toolchain, the `wifi-densepose-train` crate
provides the full training pipeline with RuVector integration:
```bash
cd v2
cargo run -p wifi-densepose-train -- \
--data pretrain-vectors.rvf \
--epochs 50 \
--output pretrained-encoder.onnx
```
@@ -0,0 +1,466 @@
# Pi 5 + Hailo Cluster: Building a Cognitive RF Observer with rvcsi
A field-tested tutorial for turning a 4-node Raspberry Pi 5 cluster into a
multistatic Wi-Fi CSI cognitive RF observer that learns room states,
predicts the next one, and flags anomalies — entirely from radio.
**Estimated time:** 46 hours (hardware 1h, firmware 1h, software 1h, calibration 13h)
**What you will build:** A self-learning 4-node cluster that captures Wi-Fi
Channel State Information from a stable RF beacon, encodes each frame into a
128-dimensional fingerprint on an on-device Hailo-8 NPU, clusters those
fingerprints into discrete room states with stable IDs across runs, models
state transitions with a 2nd-order Markov chain (with measurable predictive
skill above chance), and persists everything to a queryable brain corpus on
a workstation. The whole thing runs over Tailscale and is operated through
a single CLI with **34 subcommands**.
**Who this is for:** RF engineers, smart-home hackers, security researchers,
and ML/embedded folks comfortable with Linux + systemd. No specific signal-
processing background required — but you do need patience for hardware
quirks (nexmon_csi cross-compile is a known dead end; see step 3).
> **The TL;DR**: 4× Pi 5 + 2× Hailo-8 → CSI → 128-d embeddings → cosine
> k-means with warm-start → 2nd-order Markov → SQLite brain → 34-subcommand
> operator CLI. Production-grade signal: 39% top-1 ceiling on next-state
> prediction (16× chance baseline), continuous fleet/drift/anomaly
> monitoring, and a 12-category time-series corpus.
> **About the name "rvcsi" in this tutorial.** When this tutorial was
> first written, the cluster's per-Pi capture services were named with
> an `rvcsi` prefix (`cog-rvcsi-stream`, `cog-rvcsi-correlator`) as
> branding only — the actual code was Python and didn't depend on the
> upstream [`ruvnet/rvcsi`](https://github.com/ruvnet/rvcsi) Rust
> runtime. **As of 2026-05-13**, the v0-appliance project has accepted
> [ADR-207](https://github.com/ruvnet/v0-appliance/blob/main/docs/adr/ADR-207-rvcsi-library-integration.md)
> (rvCSI library integration — Option D) and shipped a Rust binary
> `cog-rvcsi-pi` built on rvcsi-runtime 0.3 that replaces the three
> Python services. The cutover is per-Pi, operator-driven, with
> one-command rollback (`scripts/rvcsi-pi/install-rvcsi-pi.sh` and
> `uninstall-rvcsi-pi.sh`). A given cluster may be running either
> stack while migration is in progress; the schema and operator
> surface are unchanged across the cutover. See ADR-207's
> Implementation log for the current state.
---
## Table of Contents
1. [Prerequisites](#1-prerequisites)
2. [Architecture overview](#2-architecture-overview)
3. [Per-node firmware: nexmon_csi on Pi 5](#3-per-node-firmware-nexmon_csi-on-pi-5)
4. [Per-node services](#4-per-node-services)
5. [Workstation pipeline](#5-workstation-pipeline)
6. [Calibration: getting from raw CSI to room states](#6-calibration-getting-from-raw-csi-to-room-states)
7. [Operating the cluster: the cog-query CLI](#7-operating-the-cluster-the-cog-query-cli)
8. [What you can measure](#8-what-you-can-measure)
9. [Troubleshooting](#9-troubleshooting)
10. [Next steps](#10-next-steps)
---
## 1. Prerequisites
### Hardware
| Item | Quantity | Approx. cost | Notes |
|------|----------|--------------|-------|
| Raspberry Pi 5 (8GB) | 4 | ~$80 each | 4GB works but tight under sustained load |
| Hailo-8 M.2 HAT (AI Kit) | 2 | ~$110 each | Only 2 needed — encoder is split across cluster-1 + cluster-2 |
| MicroSD (64GB, A2) | 4 | ~$10 each | A2 class strongly recommended for sustained writes |
| USB-C PD power supply (27W) | 4 | ~$12 each | Pi 5 draws 5A at full Hailo load |
| Active cooler | 4 | ~$5 each | Cluster-2 sustains thermal load — passive will throttle |
| Workstation (≥16GB RAM, Linux) | 1 | — | Hosts the brain HTTP service + clusterer + anomaly daemon |
| Stable Wi-Fi beacon | 1 | — | Any AP on the same 5 GHz channel. We use ch.149/80MHz. Stability matters more than identity. |
**Total parts cost:** ~$580 plus workstation.
> **Important:** All 4 Pi 5s must use the on-board `bcm43455c0` radio. USB
> Wi-Fi adapters with otherwise-similar chipsets **will not** work — nexmon's
> firmware patches are silicon-specific. See ADR-206 § "USB Wi-Fi dongle
> rabbit-hole" for the painful version of that lesson.
### Software prerequisites
| Component | Version | Notes |
|-----------|---------|-------|
| Pi OS Bookworm (Lite) | 64-bit, kernel 6.6+ | Use the Lite image — Desktop slows boot and burns SD writes |
| Tailscale | ≥1.60 | Mesh networking across the cluster |
| Rust toolchain | 1.78+ on workstation, 1.78+ on each Pi | For ruvector + adapter binaries |
| Python 3.11+ | system Python on workstation | numpy required |
| systemd-user | already present | Workstation timers run as user units |
---
## 2. Architecture overview
```
┌─ workstation (Linux, ≥16GB) ──────────────────┐
│ │
│ brain HTTP (SQLite, port 9876) │
│ ↑↑ │
│ ┌──┴┴──────────────────────────────────┐ │
│ │ rfmem-tail ← ingests live brain │ │
│ │ rfmem-recall → posts category= │ │
│ │ rfmem-recall when │ │
│ │ current state ≈ past │ │
│ │ rfmem-anomaly → 13-axis detector, │ │
│ │ posts rfmem-anomaly & │ │
│ │ rfmem-state-transition │ │
│ │ cog-rfmem-states (timer, hourly) │ │
│ │ re-clusters w/ warm-start│ │
│ │ cog-rfmem-insights (timer, nightly) │ │
│ │ writes rfmem-insights │ │
│ │ cog-rfmem-drift-check (timer, 05:00) │ │
│ │ audits cluster file state│ │
│ └───────────────────────────────────────┘ │
│ │
│ cog-query (CLI, 34 subcommands, 4 JSON modes)│
└────────────────────────────────────────────────┘
Tailscale mesh ──────────┴───────────────────────────────┐
↓ ↓ ↓
┌─ cluster-1 (Hailo) ┐ ┌─ cluster-2 (Hailo + fusion) ┐ ┌─ cluster-3 ┐ ┌─ v0 ┐
│ cog-csi-emitter │ │ cog-csi-emitter │ │ same as │ │ same│
│ cog-csi-adapter │ │ cog-csi-adapter │ │ cluster-1 │ │ as │
│ cog-rvcsi-stream │ │ cog-rvcsi-stream │ │ minus │ │ c-3 │
│ cog-hailo-encoder │ │ cog-hailo-encoder │ │ Hailo & │ │ │
│ │ │ cog-rvcsi-correlator (fusion)│ │ correlator │ │ │
└────────────────────┘ └─────────────────────────────┘ └────────────┘ └─────┘
4 svc 5 svc 3 svc 3 svc
└─────────────────────── 15 expected services total ──────────────────────┘
```
**Why this split?** Multistatic fusion (combining CSI from 4 spatial vantage
points into a single weighted observation) is computationally cheap but
benefits from being on **one** node so the other three only do capture +
encode. Hailo-8 is the bottleneck cost, so we put two on the cluster
(one for redundancy, one for the fusion node) and let `cluster-3` + `v0`
run as pure capture sensors.
---
## 3. Per-node firmware: nexmon_csi on Pi 5
**Critical lesson learned (saved you a week):** the workstation x86_64
cross-compile path for nexmon_csi on Pi 5 **does not work**. The 39-hunk
patch series applies cleanly on a native Pi 5 ARM build, and fails in
subtle ways elsewhere.
The recipe that works:
```bash
# On each Pi 5 (not the workstation):
sudo apt update && sudo apt install -y \
raspberrypi-kernel-headers bc bison flex libssl-dev make \
gcc gawk qpdf cmake build-essential libpcap-dev clang gcc-arm-none-eabi
git clone https://github.com/seemoo-lab/nexmon.git ~/nexmon
cd ~/nexmon
source setup_env.sh
make
cd patches
git clone https://github.com/seemoo-lab/nexmon_csi.git
cd nexmon_csi
# Apply the Pi-5-friendly patch series — all 39 hunks should apply clean
# on native ARM. If you see "Hunk #N FAILED", you are almost certainly
# cross-compiling from x86_64. Stop. Build on the Pi.
./install.sh
# Switch on:
sudo mcp # 'monitor capability provisioning' — enable
sudo nexutil -Iwlan0 -s500 -b -l34 -v<86-char base64 capture filter>
```
> **Pi 5 kernel gotcha:** Pi OS Bookworm ships two kernels — `kernel8.img`
> (4K pages) and `kernel_2712.img` (16K pages, Pi 5 only). nexmon_csi
> currently builds clean against `kernel8.img`. Add `kernel=kernel8.img`
> to `/boot/firmware/config.txt` if you've switched. **After the switch,
> SSH by hostname via Tailscale** — host keys + DHCP gotchas otherwise.
> **Clock-skew first-boot trap:** Pi 5 has no RTC. First-boot apt will
> reject "future-dated" `Release` files. Patch your firstboot to wait for
> `systemd-timesyncd` before running `apt-get`.
The complete commands + full troubleshooting matrix is in the
[detailed gist](https://gist.github.com/ruvnet/88e7b053c41cb4f4af7a7ec4af873017) — section "Firmware: nexmon_csi on Pi 5".
---
## 4. Per-node services
Each cluster Pi runs a small fixed set of systemd services. Per-host
topology:
| Service | cluster-1 | cluster-2 | cluster-3 | v0 |
|---|:--:|:--:|:--:|:--:|
| `cog-csi-emitter` (raw CSI capture from nexmon) | ✓ | ✓ | ✓ | ✓ |
| `cog-csi-adapter` (Rust binary; CSI → 256-byte float frames) | ✓ | ✓ | ✓ | ✓ |
| `cog-rvcsi-stream` (publishes frames to rvcsi-correlator) | ✓ | ✓ | ✓ | ✓ |
| `cog-hailo-encoder` (frames → 128-d fingerprints on Hailo-8) | ✓ | ✓ | — | — |
| `cog-rvcsi-correlator` (multistatic fusion across 4 nodes) | — | ✓ | — | — |
| **Expected service count** | **4** | **5** | **3** | **3** |
The topology is encoded in the workstation's `cog-query fleet-status`
subcommand, which compares per-host expected services against live
`systemctl is-active` results. A flat-service check would falsely flag
cluster-3 and v0 as degraded (they have neither Hailo nor the correlator
— that's by design).
> **rvcsi cutover (ADR-207 Option D, 2026-05-13).** The three services
> `cog-csi-emitter`, `cog-csi-adapter`, and `cog-rvcsi-stream` are
> being consolidated into one Rust binary `cog-rvcsi-pi` built on
> [rvcsi-runtime](https://crates.io/crates/rvcsi-runtime). The new
> binary holds the same per-Pi role and the same expected-service
> count from the operator's view (`fleet-status` already understands
> both layouts). Deploy with
> `bash scripts/rvcsi-pi/install-rvcsi-pi.sh <pi-host>`; revert with
> `scripts/rvcsi-pi/uninstall-rvcsi-pi.sh`. The cutover is per-Pi,
> not flag-day — mixed Python/Rust clusters are supported. The Hailo
> encoder + correlator stay Python in this phase; their Rust ports
> are tracked as follow-on ADRs.
All unit files + the install script are in the
[detailed gist](https://gist.github.com/ruvnet/88e7b053c41cb4f4af7a7ec4af873017) — section "Per-node systemd units".
---
## 5. Workstation pipeline
The workstation runs ten user-mode units (3 daemons, 7 timers):
| Unit | Type | Cadence | Purpose |
|---|---|---|---|
| `cog-rfmem-tail` | daemon | continuous | Ingests live brain entries into the workstation mirror |
| `cog-rfmem-recall` | daemon | continuous | kNN-matches current fingerprint vs persisted ones, posts `rfmem-recall` |
| `cog-rfmem-anomaly` | daemon | continuous | 13-axis anomaly detector, posts `rfmem-anomaly` + `rfmem-state-transition` |
| `cog-rfmem-indexer` | timer | every 5 min | Updates HNSW index for kNN |
| `cog-rfmem-compress` | timer | hourly | Compresses old brain entries |
| `cog-rfmem-daily` | timer | nightly 04:00 | Per-day stats roll-up (`rfmem-daily`) |
| `cog-rfmem-states` | timer | hourly | Re-runs cosine k-means w/ warm-start (`rfmem-state-summary`) |
| `cog-rfmem-insights` | timer | nightly 04:55 | NL synthesis, posts `rfmem-insights` |
| `cog-rfmem-drift-check` | timer | nightly 05:00 | Audits cluster file/unit drift, posts `rfmem-drift` |
| `cog-rfmem-mirror` | timer | hourly | Mirrors cluster-2 brain → workstation read-replica |
Install in one shot:
```bash
git clone https://github.com/<your-fork>/v0-appliance.git
cd v0-appliance
bash scripts/rfmem/install-workstation.sh
```
The installer is **idempotent** — rerunning is safe and only enables
units that aren't yet enabled. It also wires a git post-commit hook
that auto-deploys + auto-smoke-tests on every commit touching
`scripts/rfmem/`. That closes the "I edited the repo but forgot to
deploy" gap that bit us repeatedly in early development.
---
## 6. Calibration: getting from raw CSI to room states
This is the longest step but largely passive — let it run.
### 6.1 Walk the room
For 3060 minutes after the cluster is live, walk through every room you
want recognized. Sit, stand, move between rooms, repeat. The encoder is
learning to map "what the room looks like in CSI" into 128-d vectors;
diversity here matters more than total time.
### 6.2 First clustering pass
```bash
# Force-trigger the clusterer (it normally fires hourly):
systemctl --user start cog-rfmem-states.service
python3 scripts/rfmem/cog-query.py states
```
Output looks like:
```
=== rfmem-states — k=16, n=12,847 ===
state #0 π=0.184 dwell=42.3s centroid_drift=0.012 (default)
state #1 π=0.121 dwell=18.1s centroid_drift=0.003
state #4 π=0.087 dwell=29.6s centroid_drift=0.041
...
```
**Stable IDs across runs.** The warm-start k-means recipe matches new
centroids to the prior run's centroids by cosine similarity before
assigning IDs. This means state #4 stays state #4 between hourly runs —
otherwise downstream Markov transitions would scramble after every
re-cluster.
### 6.3 Let the Markov chain build
After a few thousand transitions (a few hours of activity), check:
```bash
python3 scripts/rfmem/cog-query.py prediction-accuracy
```
You should see something like:
```
=== prediction-accuracy — training-set top-1 ceilings ===
1st-order: 37.1% (16x chance baseline of 6.25%)
2nd-order: 39.4% (16x chance baseline of 6.25%, 1.06x gain over 1st)
```
The 2nd-order chain beats 1st-order because it conditions on the
**previous** state as well as the current one. Self-loops are excluded
from the argmax (a transition is by definition a state change).
### 6.4 Verify the room learned itself
```bash
python3 scripts/rfmem/cog-query.py insights
```
Reads like:
```
The cluster has observed 446,231 fingerprints, clustering them into
16 discrete RF states. The room exhibits moderately diverse (stationary
entropy 0.82/1.0). State #4 is the dominant 'default' state (π=0.214);
state #13 is the rarest baseline (π=0.018).
Prediction skill (last hour, 2nd-order): top-1 12.4% (1.98x chance),
top-3 31.0% (1.65x chance, 412 transitions) (training-set ceiling
39.4% — operating @ 31% of capacity).
```
That "operating @ 31% of capacity" line is the operational efficiency:
how close live performance is to the model's theoretical ceiling. Big
gap = the room is being noisy in ways the static cluster model doesn't
capture. Small gap = you're near SOTA for this static model.
---
## 7. Operating the cluster: the cog-query CLI
A single CLI binary with **34 subcommands** + 4 machine-readable JSON
modes. Practical ones (full list in the gist):
| Subcommand | What it does |
|---|---|
| `summary --hours 1` | Bird's-eye view of last hour: anomalies, transitions, recall hits |
| `top-events --hours 24 --limit 5` | Highest-info events in window (combines novelty + tier + recency) |
| `top-events --json` | Same, agent-consumable |
| `insights` | Natural-language synthesis (paragraph) — what the cluster thinks |
| `insights --json` | Same, structured |
| `insights --post` | Same, persisted to brain as `rfmem-insights` |
| `stats` | Corpus: per-category counts, dimensions, vector counts |
| `motion` | Recent motion events |
| `anomalies --sort info` | Anomalies sorted by composite info score (1.08.0) |
| `circadian` | 24-hour bin of activity — does the room have a daily rhythm? |
| `by-state` | Per-state metrics (dwell, σ-baseline, novelty distribution) |
| `markov` | Top transitions by frequency, both 1st + 2nd-order |
| `transitions --sort novelty` | Rare/surprising transitions |
| `dwell-times` | How long the room stays in each state |
| `prediction-accuracy` | 1st + 2nd-order top-1 ceilings |
| `baseline-drift` | Has the noise floor shifted? (slow change) |
| `centroid-drift` | Has any state's RF signature materially changed? |
| `fleet-status` | Per-host expected-service liveness check |
| `fleet-status --json` | Same, agent-consumable |
| `fleet-status --post` | Same, persisted to brain as `rfmem-fleet` (heartbeat) |
| `check-drift` | Workstation/cluster file + unit drift audit |
| `replica-status` | Hourly cluster-2 → workstation mirror health |
### The fleet-health triad
Three subcommands cover the operator's full health picture:
- `check-drift` — file content drift (what's deployed vs what's in git)
- `replica-status` — workstation mirror lag (last successful sync)
- `fleet-status` — service liveness across the 4 Pis (topology-aware)
If all three are green, the cluster is healthy. If any one fires, you
have a concrete starting point.
---
## 8. What you can measure
After a week of runtime, you can answer questions like:
- **"What's the room's most common 'baseline' state?"** → `states` shows
the π-dominant cluster ID.
- **"Did anything weird happen last night?"** → `anomalies --sort info
--hours 12` sorts by combined-information score (novelty × tier × state-
rarity × calmness).
- **"How predictable is the room?"** → `insights` reports stationary
entropy (0.0 = single state, 1.0 = uniform). Most rooms land 0.60.9.
- **"What's the most novel transition ever observed?"** → `transitions
--sort novelty --limit 1`. We've seen transitions with
`transition_p=0.0000` — never observed before in 446k+ embeddings.
- **"Is the room changing slowly?"** → `centroid-drift` flags states
whose 128-d signature has moved > 0.05 cosine distance since the prior
clusterer run. Common cause: a piece of furniture moved.
- **"What's the daily rhythm?"** → `circadian` bins activity by hour.
Most rooms show clear morning/evening peaks.
---
## 9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| `nexmon_csi` build fails with FAILED hunks | Cross-compiling from x86_64 | Build on the Pi natively |
| Pi 5 stops booting after kernel switch | Wrong `kernel=` in `/boot/firmware/config.txt` | Use `kernel=kernel8.img` |
| First boot fails on `apt update` | No RTC → clock skew, apt rejects "future-dated" Release files | Wait for `systemd-timesyncd` in firstboot |
| `cog-rfmem-now` times out | Workstation daemon swap-thrashing | Bump `MemoryMax=` in unit file (we run 1G) |
| `fleet-status` shows DEGRADED on cluster-3 / v0 | Topology unaware (old version) | Update to latest — per-host expected-services |
| Cluster-2 Hailo encoder silent | `cp -r` made encoder a directory, not a file | `install -m 0755` instead |
| 2nd-order Markov top-1 = 0% | Self-loop dominates argmax | Zero out self-loop before `.argmax()` |
| State IDs change between runs | No warm-start k-means | Update clusterer to match new centroids to prior run by cosine |
| HardFaults during embedded N6 bring-up | (Different topic, see [ADR-027](../adr/) for STM32N6 startup notes) | — |
---
## 10. Next steps
Once your cluster is producing stable predictions and clean fleet health,
the natural directions are:
1. **Cross-room correlation** — train a second cluster in another room
and feed both into the workstation. The brain already supports
multiple namespaces.
2. **Active sensing** — instead of passively observing whatever beacon is
present, drive your own (e.g., dedicated 5 GHz beacon AP at fixed
power). Eliminates upstream variability.
3. **Vital signs** — the RuView project has companion code for extracting
heart-rate and breathing from CSI; the 128-d encoder output is a
reasonable input feature.
4. **Federated training** — multiple physical sites publishing to a shared
brain. Each site keeps its own clusters; transitions are the shared
vocabulary.
5. **Push to upstream RuView** — if your cluster develops capabilities not
in this tutorial (you'll know by the time you've written the README),
send a PR.
---
## Reference material
- **[Detailed cookbook gist (all commands, configs, unit files)](https://gist.github.com/ruvnet/88e7b053c41cb4f4af7a7ec4af873017)**
- **[ADR-206: nexmon_csi on Pi 5 cluster](https://github.com/ruvnet/v0-appliance/blob/main/docs/adr/ADR-206-nexmon-csi-on-pi-5-cluster.md)** — the engineering decision record
with full rationale, including the painful-but-instructive failures
- **[v0-appliance repo](https://github.com/ruvnet/v0-appliance)** — the
source of truth for `scripts/rfmem/` operator tooling
- **[seemoo-lab/nexmon_csi](https://github.com/seemoo-lab/nexmon_csi)** —
upstream CSI capture firmware
- **[Hailo-8 documentation](https://hailo.ai/products/hailo-8/)** — NPU
reference
---
*This tutorial was built against the v0.5.0-cognitive-rf-observer milestone
of `v0-appliance`. The cluster has been running continuously for 6+ weeks
of development with 446k+ fingerprints observed, 16 stable RF states, and
a 2nd-order Markov model operating at 31% of its 39.4% theoretical
top-1 ceiling. SOTA is a moving target — but this is a real, working
cognitive RF observer that you can reproduce.*