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
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:
@@ -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:** 4–6 hours (hardware 1h, firmware 1h, software 1h, calibration 1–3h)
|
||||
|
||||
**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 30–60 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.0–8.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.6–0.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.*
|
||||
Reference in New Issue
Block a user