mirror of
https://github.com/ruvnet/RuView
synced 2026-06-30 13:43:18 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5402b070f6 |
@@ -1 +0,0 @@
|
||||
{"intelligence":7,"timestamp":1774922079152}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"running": true,
|
||||
"startedAt": "2026-03-09T15:26:00.921Z",
|
||||
"startedAt": "2026-02-28T15:54:19.353Z",
|
||||
"workers": {
|
||||
"map": {
|
||||
"runCount": 49,
|
||||
@@ -8,16 +8,16 @@
|
||||
"failureCount": 0,
|
||||
"averageDurationMs": 1.2857142857142858,
|
||||
"lastRun": "2026-02-28T16:13:19.194Z",
|
||||
"nextRun": "2026-03-09T15:56:00.928Z",
|
||||
"nextRun": "2026-02-28T16:28:19.195Z",
|
||||
"isRunning": false
|
||||
},
|
||||
"audit": {
|
||||
"runCount": 45,
|
||||
"runCount": 44,
|
||||
"successCount": 0,
|
||||
"failureCount": 45,
|
||||
"failureCount": 44,
|
||||
"averageDurationMs": 0,
|
||||
"lastRun": "2026-03-09T15:43:00.933Z",
|
||||
"nextRun": "2026-03-09T15:38:00.914Z",
|
||||
"lastRun": "2026-02-28T16:20:19.184Z",
|
||||
"nextRun": "2026-02-28T16:30:19.185Z",
|
||||
"isRunning": false
|
||||
},
|
||||
"optimize": {
|
||||
@@ -26,7 +26,7 @@
|
||||
"failureCount": 34,
|
||||
"averageDurationMs": 0,
|
||||
"lastRun": "2026-02-28T16:23:19.387Z",
|
||||
"nextRun": "2026-03-09T15:45:00.915Z",
|
||||
"nextRun": "2026-02-28T16:18:19.361Z",
|
||||
"isRunning": false
|
||||
},
|
||||
"consolidate": {
|
||||
@@ -35,7 +35,7 @@
|
||||
"failureCount": 0,
|
||||
"averageDurationMs": 0.6521739130434783,
|
||||
"lastRun": "2026-02-28T16:05:19.091Z",
|
||||
"nextRun": "2026-03-09T16:02:00.918Z",
|
||||
"nextRun": "2026-02-28T16:35:19.054Z",
|
||||
"isRunning": false
|
||||
},
|
||||
"testgaps": {
|
||||
@@ -44,8 +44,8 @@
|
||||
"failureCount": 27,
|
||||
"averageDurationMs": 0,
|
||||
"lastRun": "2026-02-28T16:08:19.369Z",
|
||||
"nextRun": "2026-03-09T15:54:00.920Z",
|
||||
"isRunning": false
|
||||
"nextRun": "2026-02-28T16:22:19.355Z",
|
||||
"isRunning": true
|
||||
},
|
||||
"predict": {
|
||||
"runCount": 0,
|
||||
@@ -64,8 +64,8 @@
|
||||
},
|
||||
"config": {
|
||||
"autoStart": false,
|
||||
"logDir": "/Users/cohen/GitHub/ruvnet/RuView/.claude-flow/logs",
|
||||
"stateFile": "/Users/cohen/GitHub/ruvnet/RuView/.claude-flow/daemon-state.json",
|
||||
"logDir": "/home/user/wifi-densepose/.claude-flow/logs",
|
||||
"stateFile": "/home/user/wifi-densepose/.claude-flow/daemon-state.json",
|
||||
"maxConcurrent": 2,
|
||||
"workerTimeoutMs": 300000,
|
||||
"resourceThresholds": {
|
||||
@@ -131,5 +131,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"savedAt": "2026-03-09T15:43:00.933Z"
|
||||
"savedAt": "2026-02-28T16:23:19.387Z"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
166
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"timestamp": "2026-03-06T13:17:27.368Z",
|
||||
"mode": "local",
|
||||
"checks": {
|
||||
"envFilesProtected": true,
|
||||
"gitIgnoreExists": true,
|
||||
"noHardcodedSecrets": true
|
||||
},
|
||||
"riskLevel": "low",
|
||||
"recommendations": [],
|
||||
"note": "Install Claude Code CLI for AI-powered security analysis"
|
||||
}
|
||||
+13
-13
@@ -6,7 +6,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" pre-bash",
|
||||
"command": "node .claude/helpers/hook-handler.cjs pre-bash",
|
||||
"timeout": 5000
|
||||
}
|
||||
]
|
||||
@@ -18,7 +18,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" post-edit",
|
||||
"command": "node .claude/helpers/hook-handler.cjs post-edit",
|
||||
"timeout": 10000
|
||||
}
|
||||
]
|
||||
@@ -29,7 +29,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" route",
|
||||
"command": "node .claude/helpers/hook-handler.cjs route",
|
||||
"timeout": 10000
|
||||
}
|
||||
]
|
||||
@@ -40,12 +40,12 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" session-restore",
|
||||
"command": "node .claude/helpers/hook-handler.cjs session-restore",
|
||||
"timeout": 15000
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/auto-memory-hook.mjs\" import",
|
||||
"command": "node .claude/helpers/auto-memory-hook.mjs import",
|
||||
"timeout": 8000
|
||||
}
|
||||
]
|
||||
@@ -56,7 +56,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" session-end",
|
||||
"command": "node .claude/helpers/hook-handler.cjs session-end",
|
||||
"timeout": 10000
|
||||
}
|
||||
]
|
||||
@@ -67,7 +67,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/auto-memory-hook.mjs\" sync",
|
||||
"command": "node .claude/helpers/auto-memory-hook.mjs sync",
|
||||
"timeout": 10000
|
||||
}
|
||||
]
|
||||
@@ -79,11 +79,11 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" compact-manual"
|
||||
"command": "node .claude/helpers/hook-handler.cjs compact-manual"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" session-end",
|
||||
"command": "node .claude/helpers/hook-handler.cjs session-end",
|
||||
"timeout": 5000
|
||||
}
|
||||
]
|
||||
@@ -93,11 +93,11 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" compact-auto"
|
||||
"command": "node .claude/helpers/hook-handler.cjs compact-auto"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" session-end",
|
||||
"command": "node .claude/helpers/hook-handler.cjs session-end",
|
||||
"timeout": 6000
|
||||
}
|
||||
]
|
||||
@@ -108,7 +108,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" status",
|
||||
"command": "node .claude/helpers/hook-handler.cjs status",
|
||||
"timeout": 3000
|
||||
}
|
||||
]
|
||||
@@ -117,7 +117,7 @@
|
||||
},
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/statusline.cjs\""
|
||||
"command": "node .claude/helpers/statusline.cjs"
|
||||
},
|
||||
"permissions": {
|
||||
"allow": [
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"enabledMcpjsonServers": [
|
||||
"claude-flow"
|
||||
],
|
||||
"enableAllProjectMcpServers": true
|
||||
}
|
||||
@@ -62,32 +62,6 @@ jobs:
|
||||
bandit-report.json
|
||||
safety-report.json
|
||||
|
||||
# Rust Workspace Tests
|
||||
rust-tests:
|
||||
name: Rust Workspace Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
v2/target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('v2/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
- name: Run Rust tests
|
||||
working-directory: v2
|
||||
run: cargo test --workspace --no-default-features
|
||||
|
||||
# Unit and Integration Tests
|
||||
test:
|
||||
name: Tests
|
||||
@@ -209,7 +183,7 @@ jobs:
|
||||
docker-build:
|
||||
name: Docker Build & Test
|
||||
runs-on: ubuntu-latest
|
||||
needs: [code-quality, test, rust-tests]
|
||||
needs: [code-quality, test]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -308,29 +282,28 @@ jobs:
|
||||
notify:
|
||||
name: Notify
|
||||
runs-on: ubuntu-latest
|
||||
needs: [code-quality, test, rust-tests, performance-test, docker-build, docs]
|
||||
needs: [code-quality, test, performance-test, docker-build, docs]
|
||||
if: always()
|
||||
# GitHub Actions does not allow `secrets.X` directly in step-level `if:`
|
||||
# expressions — only `env.X`. Promote the secret to env at job scope so
|
||||
# the gating expression below is parseable.
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
steps:
|
||||
- name: Notify Slack on success
|
||||
if: ${{ env.SLACK_WEBHOOK_URL != '' && needs.code-quality.result == 'success' && needs.test.result == 'success' && needs.docker-build.result == 'success' }}
|
||||
if: ${{ secrets.SLACK_WEBHOOK_URL != '' && needs.code-quality.result == 'success' && needs.test.result == 'success' && needs.docker-build.result == 'success' }}
|
||||
uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: success
|
||||
channel: '#ci-cd'
|
||||
text: '✅ CI pipeline completed successfully for ${{ github.ref }}'
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
- name: Notify Slack on failure
|
||||
if: ${{ env.SLACK_WEBHOOK_URL != '' && (needs.code-quality.result == 'failure' || needs.test.result == 'failure' || needs.docker-build.result == 'failure') }}
|
||||
if: ${{ secrets.SLACK_WEBHOOK_URL != '' && (needs.code-quality.result == 'failure' || needs.test.result == 'failure' || needs.docker-build.result == 'failure') }}
|
||||
uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: failure
|
||||
channel: '#ci-cd'
|
||||
text: '❌ CI pipeline failed for ${{ github.ref }}'
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: github.ref == 'refs/heads/main' && needs.docker-build.result == 'success'
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
name: Desktop Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'desktop-v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to release (e.g., 0.4.0)'
|
||||
required: true
|
||||
default: '0.4.0'
|
||||
attach_to_existing:
|
||||
description: 'Attach to existing release tag (leave empty to create new)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
build-macos:
|
||||
name: Build macOS
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target: [aarch64-apple-darwin, x86_64-apple-darwin]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: v2/crates/wifi-densepose-desktop/ui
|
||||
run: npm ci
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: v2/crates/wifi-densepose-desktop/ui
|
||||
run: npm run build
|
||||
|
||||
- name: Install Tauri CLI
|
||||
run: cargo install tauri-cli --version "^2.0.0"
|
||||
|
||||
- name: Build Tauri app
|
||||
working-directory: v2/crates/wifi-densepose-desktop
|
||||
run: cargo tauri build --target ${{ matrix.target }}
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
|
||||
- name: Get architecture name
|
||||
id: arch
|
||||
run: |
|
||||
if [ "${{ matrix.target }}" = "aarch64-apple-darwin" ]; then
|
||||
echo "arch=arm64" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "arch=x64" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Package macOS app
|
||||
run: |
|
||||
cd v2/target/${{ matrix.target }}/release/bundle/macos
|
||||
zip -r "RuView-Desktop-${{ github.event.inputs.version || '0.4.0' }}-macos-${{ steps.arch.outputs.arch }}.zip" "RuView Desktop.app"
|
||||
|
||||
- name: Upload macOS artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ruview-macos-${{ steps.arch.outputs.arch }}
|
||||
path: v2/target/${{ matrix.target }}/release/bundle/macos/*.zip
|
||||
|
||||
build-windows:
|
||||
name: Build Windows
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: v2/crates/wifi-densepose-desktop/ui
|
||||
run: npm ci
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: v2/crates/wifi-densepose-desktop/ui
|
||||
run: npm run build
|
||||
|
||||
- name: Install Tauri CLI
|
||||
run: cargo install tauri-cli --version "^2.0.0"
|
||||
|
||||
- name: Build Tauri app
|
||||
working-directory: v2/crates/wifi-densepose-desktop
|
||||
run: cargo tauri build
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
|
||||
- name: Upload Windows MSI artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ruview-windows-msi
|
||||
path: v2/target/release/bundle/msi/*.msi
|
||||
|
||||
- name: Upload Windows NSIS artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ruview-windows-nsis
|
||||
path: v2/target/release/bundle/nsis/*.exe
|
||||
|
||||
create-release:
|
||||
name: Create Release
|
||||
needs: [build-macos, build-windows]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
|
||||
- name: List artifacts
|
||||
run: find artifacts -type f
|
||||
|
||||
- name: Create or Update Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: RuView Desktop v${{ github.event.inputs.version || '0.4.0' }}
|
||||
tag_name: ${{ github.event.inputs.attach_to_existing || format('desktop-v{0}', github.event.inputs.version || '0.4.0') }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: ${{ github.event.inputs.attach_to_existing == '' }}
|
||||
files: |
|
||||
artifacts/**/*.zip
|
||||
artifacts/**/*.msi
|
||||
artifacts/**/*.exe
|
||||
artifacts/**/*.dmg
|
||||
body: |
|
||||
## RuView Desktop v${{ github.event.inputs.version || '0.4.0' }}
|
||||
|
||||
WiFi-based human pose estimation desktop application.
|
||||
|
||||
### Downloads
|
||||
|
||||
| Platform | Architecture | Download |
|
||||
|----------|--------------|----------|
|
||||
| macOS | Apple Silicon (M1/M2/M3) | `RuView-Desktop-*-macos-arm64.zip` |
|
||||
| macOS | Intel | `RuView-Desktop-*-macos-x64.zip` |
|
||||
| Windows | x64 | `RuView-Desktop-*.msi` or `RuView-Desktop-*.exe` |
|
||||
|
||||
### Installation
|
||||
|
||||
**macOS:**
|
||||
1. Download the appropriate `.zip` file for your Mac
|
||||
2. Extract the zip file
|
||||
3. Move `RuView Desktop.app` to your Applications folder
|
||||
4. Right-click and select "Open" (first time only, to bypass Gatekeeper)
|
||||
|
||||
**Windows:**
|
||||
1. Download the `.msi` installer
|
||||
2. Run the installer
|
||||
3. Launch RuView Desktop from the Start menu
|
||||
|
||||
### Requirements
|
||||
- macOS 11.0+ (Big Sur or later)
|
||||
- Windows 10/11 (64-bit)
|
||||
@@ -12,50 +12,31 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ESP32-S3 Firmware (${{ matrix.variant }})
|
||||
name: Build ESP32-S3 Firmware
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: espressif/idf:v5.4
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- variant: 8mb
|
||||
sdkconfig: sdkconfig.defaults
|
||||
partition_table_name: partitions_display.csv
|
||||
size_limit_kb: 1100
|
||||
artifact_app: esp32-csi-node.bin
|
||||
artifact_pt: partition-table.bin
|
||||
- variant: 4mb
|
||||
sdkconfig: sdkconfig.defaults.4mb
|
||||
partition_table_name: partitions_4mb.csv
|
||||
size_limit_kb: 1100
|
||||
artifact_app: esp32-csi-node-4mb.bin
|
||||
artifact_pt: partition-table-4mb.bin
|
||||
image: espressif/idf:v5.2
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build firmware (${{ matrix.variant }})
|
||||
- name: Build firmware
|
||||
working-directory: firmware/esp32-csi-node
|
||||
run: |
|
||||
. $IDF_PATH/export.sh
|
||||
if [ "${{ matrix.variant }}" != "8mb" ]; then
|
||||
cp "${{ matrix.sdkconfig }}" sdkconfig.defaults
|
||||
fi
|
||||
idf.py set-target esp32s3
|
||||
idf.py build
|
||||
|
||||
- name: Verify binary size (< ${{ matrix.size_limit_kb }} KB gate)
|
||||
- name: Verify binary size (< 950 KB gate)
|
||||
working-directory: firmware/esp32-csi-node
|
||||
run: |
|
||||
BIN=build/esp32-csi-node.bin
|
||||
SIZE=$(stat -c%s "$BIN")
|
||||
MAX=$((${{ matrix.size_limit_kb }} * 1024))
|
||||
MAX=$((950 * 1024))
|
||||
echo "Binary size: $SIZE bytes ($(( SIZE / 1024 )) KB)"
|
||||
echo "Size limit: $MAX bytes (${{ matrix.size_limit_kb }} KB)"
|
||||
echo "Size limit: $MAX bytes (950 KB — includes Tier 3 WASM runtime)"
|
||||
if [ "$SIZE" -gt "$MAX" ]; then
|
||||
echo "::error::Firmware binary exceeds ${{ matrix.size_limit_kb }} KB size gate ($SIZE > $MAX)"
|
||||
echo "::error::Firmware binary exceeds 950 KB size gate ($SIZE > $MAX)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Binary size OK: $SIZE <= $MAX"
|
||||
@@ -66,27 +47,31 @@ jobs:
|
||||
ERRORS=0
|
||||
BIN=build/esp32-csi-node.bin
|
||||
|
||||
# Check binary exists and is non-empty.
|
||||
if [ ! -s "$BIN" ]; then
|
||||
echo "::error::Binary not found or empty"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check partition table magic (0xAA50 at offset 0).
|
||||
PT=build/partition_table/partition-table.bin
|
||||
if [ -f "$PT" ]; then
|
||||
MAGIC=$(od -A n -t x1 -N 2 "$PT" | tr -d ' ')
|
||||
MAGIC=$(xxd -l2 -p "$PT")
|
||||
if [ "$MAGIC" != "aa50" ]; then
|
||||
echo "::warning::Partition table magic mismatch: $MAGIC (expected aa50)"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check bootloader exists.
|
||||
BL=build/bootloader/bootloader.bin
|
||||
if [ ! -s "$BL" ]; then
|
||||
echo "::warning::Bootloader binary missing or empty"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
|
||||
NONZERO=$(od -A n -t x1 -N 1024 "$BIN" | tr -d ' f\n' | wc -c)
|
||||
# Verify non-zero data in binary (not all 0xFF padding).
|
||||
NONZERO=$(xxd -l 1024 -p "$BIN" | tr -d 'f' | wc -c)
|
||||
if [ "$NONZERO" -lt 100 ]; then
|
||||
echo "::error::Binary appears to be mostly padding (non-zero chars: $NONZERO)"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
@@ -98,27 +83,18 @@ jobs:
|
||||
echo "Flash image integrity verified"
|
||||
fi
|
||||
|
||||
- name: Stage release binaries with variant-specific names
|
||||
working-directory: firmware/esp32-csi-node
|
||||
run: |
|
||||
mkdir -p release-staging
|
||||
cp build/esp32-csi-node.bin release-staging/${{ matrix.artifact_app }}
|
||||
cp build/partition_table/partition-table.bin release-staging/${{ matrix.artifact_pt }}
|
||||
if [ "${{ matrix.variant }}" = "8mb" ]; then
|
||||
cp build/bootloader/bootloader.bin release-staging/bootloader.bin
|
||||
cp build/ota_data_initial.bin release-staging/ota_data_initial.bin
|
||||
fi
|
||||
ls -la release-staging/
|
||||
|
||||
- name: Check QEMU ESP32-S3 support status
|
||||
run: |
|
||||
echo "::notice::ESP32-S3 QEMU support is experimental in ESP-IDF v5.4. "
|
||||
echo "Full smoke testing requires QEMU 8.2+ with xtensa-esp32s3 target."
|
||||
echo "See: https://github.com/espressif/qemu/wiki"
|
||||
|
||||
- name: Upload firmware artifact (${{ matrix.variant }})
|
||||
- name: Upload firmware artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: esp32-csi-node-firmware-${{ matrix.variant }}
|
||||
path: firmware/esp32-csi-node/release-staging/
|
||||
retention-days: 90
|
||||
name: esp32-csi-node-firmware
|
||||
path: |
|
||||
firmware/esp32-csi-node/build/esp32-csi-node.bin
|
||||
firmware/esp32-csi-node/build/bootloader/bootloader.bin
|
||||
firmware/esp32-csi-node/build/partition_table/partition-table.bin
|
||||
retention-days: 30
|
||||
|
||||
@@ -1,370 +0,0 @@
|
||||
name: Firmware QEMU Tests (ADR-061)
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'firmware/**'
|
||||
- 'scripts/qemu-esp32s3-test.sh'
|
||||
- 'scripts/validate_qemu_output.py'
|
||||
- 'scripts/generate_nvs_matrix.py'
|
||||
- 'scripts/qemu_swarm.py'
|
||||
- 'scripts/swarm_health.py'
|
||||
- 'scripts/swarm_presets/**'
|
||||
- '.github/workflows/firmware-qemu.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'firmware/**'
|
||||
- 'scripts/qemu-esp32s3-test.sh'
|
||||
- 'scripts/validate_qemu_output.py'
|
||||
- 'scripts/generate_nvs_matrix.py'
|
||||
- 'scripts/qemu_swarm.py'
|
||||
- 'scripts/swarm_health.py'
|
||||
- 'scripts/swarm_presets/**'
|
||||
- '.github/workflows/firmware-qemu.yml'
|
||||
|
||||
env:
|
||||
IDF_VERSION: "v5.4"
|
||||
QEMU_REPO: "https://github.com/espressif/qemu.git"
|
||||
QEMU_BRANCH: "esp-develop"
|
||||
|
||||
jobs:
|
||||
build-qemu:
|
||||
name: Build Espressif QEMU
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cache QEMU build
|
||||
id: cache-qemu
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /opt/qemu-esp32
|
||||
# Include date component so cache refreshes monthly when branch updates
|
||||
key: qemu-esp32s3-${{ env.QEMU_BRANCH }}-v5
|
||||
restore-keys: |
|
||||
qemu-esp32s3-${{ env.QEMU_BRANCH }}-
|
||||
|
||||
- name: Install QEMU build dependencies
|
||||
if: steps.cache-qemu.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
git build-essential ninja-build pkg-config \
|
||||
libglib2.0-dev libpixman-1-dev libslirp-dev \
|
||||
libgcrypt20-dev \
|
||||
python3 python3-venv
|
||||
|
||||
- name: Clone and build Espressif QEMU
|
||||
if: steps.cache-qemu.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
git clone --depth 1 -b "$QEMU_BRANCH" "$QEMU_REPO" /tmp/qemu-esp
|
||||
cd /tmp/qemu-esp
|
||||
mkdir build && cd build
|
||||
../configure \
|
||||
--target-list=xtensa-softmmu \
|
||||
--prefix=/opt/qemu-esp32 \
|
||||
--enable-slirp \
|
||||
--disable-werror
|
||||
ninja -j$(nproc)
|
||||
ninja install
|
||||
|
||||
- name: Verify QEMU binary
|
||||
run: |
|
||||
file_size() { stat -c%s "$1" 2>/dev/null || stat -f%z "$1" 2>/dev/null || wc -c < "$1"; }
|
||||
/opt/qemu-esp32/bin/qemu-system-xtensa --version
|
||||
echo "QEMU binary size: $(file_size /opt/qemu-esp32/bin/qemu-system-xtensa) bytes"
|
||||
|
||||
- name: Upload QEMU artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: qemu-esp32
|
||||
path: /opt/qemu-esp32/
|
||||
retention-days: 7
|
||||
|
||||
qemu-test:
|
||||
name: QEMU Test (${{ matrix.nvs_config }})
|
||||
needs: build-qemu
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: espressif/idf:v5.4
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
nvs_config:
|
||||
- default
|
||||
- full-adr060
|
||||
- edge-tier0
|
||||
- edge-tier1
|
||||
- tdm-3node
|
||||
- boundary-max
|
||||
- boundary-min
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download QEMU artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: qemu-esp32
|
||||
path: /opt/qemu-esp32
|
||||
|
||||
- name: Make QEMU executable
|
||||
run: chmod +x /opt/qemu-esp32/bin/qemu-system-xtensa
|
||||
|
||||
- name: Verify QEMU works
|
||||
run: /opt/qemu-esp32/bin/qemu-system-xtensa --version
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
. $IDF_PATH/export.sh
|
||||
pip install esptool esp-idf-nvs-partition-gen
|
||||
|
||||
- name: Set target ESP32-S3
|
||||
working-directory: firmware/esp32-csi-node
|
||||
run: |
|
||||
. $IDF_PATH/export.sh
|
||||
idf.py set-target esp32s3
|
||||
|
||||
- name: Build firmware (mock CSI mode)
|
||||
working-directory: firmware/esp32-csi-node
|
||||
run: |
|
||||
. $IDF_PATH/export.sh
|
||||
idf.py \
|
||||
-D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.qemu" \
|
||||
build
|
||||
|
||||
- name: Generate NVS matrix
|
||||
run: |
|
||||
. $IDF_PATH/export.sh
|
||||
python3 scripts/generate_nvs_matrix.py \
|
||||
--output-dir firmware/esp32-csi-node/build/nvs_matrix \
|
||||
--only ${{ matrix.nvs_config }}
|
||||
|
||||
- name: Create merged flash image
|
||||
working-directory: firmware/esp32-csi-node
|
||||
run: |
|
||||
. $IDF_PATH/export.sh
|
||||
|
||||
# Determine merge_bin arguments
|
||||
OTA_ARGS=""
|
||||
if [ -f build/ota_data_initial.bin ]; then
|
||||
OTA_ARGS="0xf000 build/ota_data_initial.bin"
|
||||
fi
|
||||
|
||||
python3 -m esptool --chip esp32s3 merge_bin \
|
||||
-o build/qemu_flash.bin \
|
||||
--flash_mode dio --flash_freq 80m --flash_size 8MB \
|
||||
--fill-flash-size 8MB \
|
||||
0x0 build/bootloader/bootloader.bin \
|
||||
0x8000 build/partition_table/partition-table.bin \
|
||||
$OTA_ARGS \
|
||||
0x20000 build/esp32-csi-node.bin
|
||||
|
||||
file_size() { stat -c%s "$1" 2>/dev/null || stat -f%z "$1" 2>/dev/null || wc -c < "$1"; }
|
||||
echo "Flash image size: $(file_size build/qemu_flash.bin) bytes"
|
||||
|
||||
- name: Inject NVS partition
|
||||
if: matrix.nvs_config != 'default'
|
||||
working-directory: firmware/esp32-csi-node
|
||||
run: |
|
||||
NVS_BIN="build/nvs_matrix/nvs_${{ matrix.nvs_config }}.bin"
|
||||
if [ -f "$NVS_BIN" ]; then
|
||||
file_size() { stat -c%s "$1" 2>/dev/null || stat -f%z "$1" 2>/dev/null || wc -c < "$1"; }
|
||||
echo "Injecting NVS: $NVS_BIN ($(file_size "$NVS_BIN") bytes)"
|
||||
dd if="$NVS_BIN" of=build/qemu_flash.bin \
|
||||
bs=1 seek=$((0x9000)) conv=notrunc 2>/dev/null
|
||||
else
|
||||
echo "WARNING: NVS binary not found: $NVS_BIN"
|
||||
fi
|
||||
|
||||
- name: Run QEMU smoke test
|
||||
env:
|
||||
QEMU_PATH: /opt/qemu-esp32/bin/qemu-system-xtensa
|
||||
QEMU_TIMEOUT: "90"
|
||||
run: |
|
||||
echo "Starting QEMU (timeout: ${QEMU_TIMEOUT}s)..."
|
||||
|
||||
timeout "$QEMU_TIMEOUT" "$QEMU_PATH" \
|
||||
-machine esp32s3 \
|
||||
-nographic \
|
||||
-drive file=firmware/esp32-csi-node/build/qemu_flash.bin,if=mtd,format=raw \
|
||||
-serial mon:stdio \
|
||||
-nic user,model=open_eth,net=10.0.2.0/24 \
|
||||
-no-reboot \
|
||||
2>&1 | tee firmware/esp32-csi-node/build/qemu_output.log || true
|
||||
|
||||
echo "QEMU finished. Log size: $(wc -l < firmware/esp32-csi-node/build/qemu_output.log) lines"
|
||||
|
||||
- name: Validate QEMU output
|
||||
run: |
|
||||
python3 scripts/validate_qemu_output.py \
|
||||
firmware/esp32-csi-node/build/qemu_output.log
|
||||
|
||||
- name: Upload test logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: qemu-logs-${{ matrix.nvs_config }}
|
||||
path: |
|
||||
firmware/esp32-csi-node/build/qemu_output.log
|
||||
firmware/esp32-csi-node/build/nvs_matrix/
|
||||
retention-days: 14
|
||||
|
||||
fuzz-test:
|
||||
name: Fuzz Testing (ADR-061 Layer 6)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install clang
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y clang
|
||||
|
||||
- name: Build fuzz targets
|
||||
working-directory: firmware/esp32-csi-node/test
|
||||
run: make all CC=clang
|
||||
|
||||
- name: Run serialize fuzzer (60s)
|
||||
working-directory: firmware/esp32-csi-node/test
|
||||
run: make run_serialize FUZZ_DURATION=60 || echo "FUZZER_CRASH=serialize" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Run edge enqueue fuzzer (60s)
|
||||
working-directory: firmware/esp32-csi-node/test
|
||||
run: make run_edge FUZZ_DURATION=60 || echo "FUZZER_CRASH=edge" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Run NVS config fuzzer (60s)
|
||||
working-directory: firmware/esp32-csi-node/test
|
||||
run: make run_nvs FUZZ_DURATION=60 || echo "FUZZER_CRASH=nvs" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Check for crashes
|
||||
working-directory: firmware/esp32-csi-node/test
|
||||
run: |
|
||||
CRASHES=$(find . -type f \( -name "crash-*" -o -name "oom-*" -o -name "timeout-*" \) 2>/dev/null | wc -l)
|
||||
echo "Crash artifacts found: $CRASHES"
|
||||
if [ "$CRASHES" -gt 0 ] || [ -n "${FUZZER_CRASH:-}" ]; then
|
||||
echo "::error::Fuzzer found $CRASHES crash/oom/timeout artifacts. FUZZER_CRASH=${FUZZER_CRASH:-none}"
|
||||
ls -la crash-* oom-* timeout-* 2>/dev/null
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload fuzz artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fuzz-crashes
|
||||
path: |
|
||||
firmware/esp32-csi-node/test/crash-*
|
||||
firmware/esp32-csi-node/test/oom-*
|
||||
firmware/esp32-csi-node/test/timeout-*
|
||||
retention-days: 30
|
||||
|
||||
nvs-matrix-validate:
|
||||
name: NVS Matrix Generation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install NVS generator
|
||||
run: pip install esp-idf-nvs-partition-gen
|
||||
|
||||
- name: Generate all 14 NVS configs
|
||||
run: |
|
||||
python3 scripts/generate_nvs_matrix.py \
|
||||
--output-dir build/nvs_matrix
|
||||
|
||||
- name: Verify all binaries generated
|
||||
run: |
|
||||
EXPECTED=14
|
||||
ACTUAL=$(find build/nvs_matrix -type f -name "nvs_*.bin" 2>/dev/null | wc -l)
|
||||
echo "Generated $ACTUAL / $EXPECTED NVS binaries"
|
||||
ls -la build/nvs_matrix/
|
||||
|
||||
if [ "$ACTUAL" -lt "$EXPECTED" ]; then
|
||||
echo "::error::Only $ACTUAL of $EXPECTED NVS binaries generated"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Verify binary sizes
|
||||
run: |
|
||||
file_size() { stat -c%s "$1" 2>/dev/null || stat -f%z "$1" 2>/dev/null || wc -c < "$1"; }
|
||||
for f in build/nvs_matrix/nvs_*.bin; do
|
||||
SIZE=$(file_size "$f")
|
||||
if [ "$SIZE" -ne 24576 ]; then
|
||||
echo "::error::$f has unexpected size $SIZE (expected 24576)"
|
||||
exit 1
|
||||
fi
|
||||
echo " OK: $(basename $f) ($SIZE bytes)"
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ADR-062: QEMU Swarm Configurator Test
|
||||
#
|
||||
# Runs a lightweight 3-node swarm (ci_matrix preset) under QEMU to validate
|
||||
# multi-node orchestration, TDM slot coordination, and swarm-level health
|
||||
# assertions. Uses the pre-built QEMU binary from the build-qemu job and the
|
||||
# firmware built by qemu-test.
|
||||
#
|
||||
# The CI runner is non-root, so TAP bridge networking is unavailable.
|
||||
# The orchestrator (qemu_swarm.py) detects this and falls back to SLIRP
|
||||
# user-mode networking, which is sufficient for the ci_matrix preset.
|
||||
# ---------------------------------------------------------------------------
|
||||
swarm-test:
|
||||
name: Swarm Test (ADR-062)
|
||||
needs: [build-qemu]
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: espressif/idf:v5.4
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download QEMU artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: qemu-esp32
|
||||
path: /opt/qemu-esp32
|
||||
|
||||
- name: Make QEMU executable
|
||||
run: chmod +x /opt/qemu-esp32/bin/qemu-system-xtensa
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
. $IDF_PATH/export.sh
|
||||
pip install pyyaml esptool esp-idf-nvs-partition-gen
|
||||
|
||||
- name: Build firmware for swarm
|
||||
working-directory: firmware/esp32-csi-node
|
||||
run: |
|
||||
. $IDF_PATH/export.sh
|
||||
idf.py set-target esp32s3
|
||||
idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.qemu" build
|
||||
python3 -m esptool --chip esp32s3 merge_bin \
|
||||
-o build/qemu_flash.bin \
|
||||
--flash_mode dio --flash_freq 80m --flash_size 8MB \
|
||||
--fill-flash-size 8MB \
|
||||
0x0 build/bootloader/bootloader.bin \
|
||||
0x8000 build/partition_table/partition-table.bin \
|
||||
0x20000 build/esp32-csi-node.bin
|
||||
|
||||
- name: Run swarm smoke test
|
||||
run: |
|
||||
. $IDF_PATH/export.sh
|
||||
EXIT_CODE=0
|
||||
python3 scripts/qemu_swarm.py --preset ci_matrix \
|
||||
--qemu-path /opt/qemu-esp32/bin/qemu-system-xtensa \
|
||||
--output-dir build/swarm-results || EXIT_CODE=$?
|
||||
# Exit 0=PASS, 1=WARN (acceptable in CI without real hardware)
|
||||
if [ "$EXIT_CODE" -gt 1 ]; then
|
||||
echo "Swarm test failed with exit code $EXIT_CODE"
|
||||
exit "$EXIT_CODE"
|
||||
fi
|
||||
timeout-minutes: 10
|
||||
|
||||
- name: Upload swarm results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: swarm-results
|
||||
path: |
|
||||
build/swarm-results/
|
||||
retention-days: 14
|
||||
@@ -377,11 +377,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [sast, dependency-scan, container-scan, iac-scan, secret-scan, license-scan, compliance-check]
|
||||
if: always()
|
||||
# Promote secret to env-scope so the gating `if:` on the Slack-notify
|
||||
# step below is parseable (GitHub Actions rejects `secrets.X` in
|
||||
# step-level `if:` expressions).
|
||||
env:
|
||||
SECURITY_SLACK_WEBHOOK_URL: ${{ secrets.SECURITY_SLACK_WEBHOOK_URL }}
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -407,11 +402,8 @@ jobs:
|
||||
name: security-summary
|
||||
path: security-summary.md
|
||||
|
||||
# GitHub Actions does not allow `secrets.X` in step-level `if:` —
|
||||
# use env.X instead. Inherits SECURITY_SLACK_WEBHOOK_URL from the
|
||||
# job-level env block (added below).
|
||||
- name: Notify security team on critical findings
|
||||
if: ${{ env.SECURITY_SLACK_WEBHOOK_URL != '' && (needs.sast.result == 'failure' || needs.dependency-scan.result == 'failure' || needs.container-scan.result == 'failure') }}
|
||||
if: ${{ secrets.SECURITY_SLACK_WEBHOOK_URL != '' && (needs.sast.result == 'failure' || needs.dependency-scan.result == 'failure' || needs.container-scan.result == 'failure') }}
|
||||
uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: failure
|
||||
@@ -423,7 +415,7 @@ jobs:
|
||||
Workflow: ${{ github.workflow }}
|
||||
Please review the security scan results immediately.
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ env.SECURITY_SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SECURITY_SLACK_WEBHOOK_URL }}
|
||||
|
||||
- name: Create security issue on critical findings
|
||||
if: needs.sast.result == 'failure' || needs.dependency-scan.result == 'failure'
|
||||
|
||||
@@ -4,16 +4,16 @@ on:
|
||||
push:
|
||||
branches: [ main, master, 'claude/**' ]
|
||||
paths:
|
||||
- 'archive/v1/src/core/**'
|
||||
- 'archive/v1/src/hardware/**'
|
||||
- 'archive/v1/data/proof/**'
|
||||
- 'v1/src/core/**'
|
||||
- 'v1/src/hardware/**'
|
||||
- 'v1/data/proof/**'
|
||||
- '.github/workflows/verify-pipeline.yml'
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
paths:
|
||||
- 'archive/v1/src/core/**'
|
||||
- 'archive/v1/src/hardware/**'
|
||||
- 'archive/v1/data/proof/**'
|
||||
- 'v1/src/core/**'
|
||||
- 'v1/src/hardware/**'
|
||||
- 'v1/data/proof/**'
|
||||
- '.github/workflows/verify-pipeline.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -37,19 +37,19 @@ jobs:
|
||||
- name: Install pinned dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r archive/v1/requirements-lock.txt
|
||||
pip install -r v1/requirements-lock.txt
|
||||
|
||||
- name: Verify reference signal is reproducible
|
||||
run: |
|
||||
echo "=== Regenerating reference signal ==="
|
||||
python archive/v1/data/proof/generate_reference_signal.py
|
||||
python v1/data/proof/generate_reference_signal.py
|
||||
echo ""
|
||||
echo "=== Checking data file matches committed version ==="
|
||||
# The regenerated file should be identical to the committed one
|
||||
# (We compare the metadata file since data file is large)
|
||||
python -c "
|
||||
import json, hashlib
|
||||
with open('archive/v1/data/proof/sample_csi_meta.json') as f:
|
||||
with open('v1/data/proof/sample_csi_meta.json') as f:
|
||||
meta = json.load(f)
|
||||
assert meta['is_synthetic'] == True, 'Metadata must mark signal as synthetic'
|
||||
assert meta['numpy_seed'] == 42, 'Seed must be 42'
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
echo "=== Scanning for unseeded np.random usage in production code ==="
|
||||
# Search for np.random calls without a seed in production code
|
||||
# Exclude test files, proof data generators, and known parser placeholders
|
||||
VIOLATIONS=$(grep -rn "np\.random\." archive/v1/src/ \
|
||||
VIOLATIONS=$(grep -rn "np\.random\." v1/src/ \
|
||||
--include="*.py" \
|
||||
--exclude-dir="__pycache__" \
|
||||
| grep -v "np\.random\.RandomState" \
|
||||
|
||||
+1
-26
@@ -23,14 +23,6 @@ rust-port/wifi-densepose-rs/data/recordings/
|
||||
nvs.bin
|
||||
nvs_config.csv
|
||||
nvs_provision.bin
|
||||
firmware/esp32-csi-node/nvs_seed.csv
|
||||
firmware/esp32-csi-node/nvs_seed.bin
|
||||
firmware/esp32-csi-node/nvs_config.bin
|
||||
firmware/esp32-csi-node/nvs_wifi.bin
|
||||
firmware/esp32-csi-node/nvs.bin
|
||||
# Catch any other NVS binaries/CSVs with credentials
|
||||
**/nvs_*.bin
|
||||
**/nvs_*.csv
|
||||
|
||||
# Working artifacts that should not land in root
|
||||
/*.wasm
|
||||
@@ -234,21 +226,4 @@ v1/src/sensing/mac_wifi
|
||||
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||
# refer to https://docs.cursor.com/context/ignore-files
|
||||
.cursorignore
|
||||
.cursorindexingignore
|
||||
|
||||
# Claude Flow runtime artifacts (auto-generated, machine-specific)
|
||||
**/daemon.pid
|
||||
**/pending-insights.jsonl
|
||||
**/vectors.db
|
||||
**/memory.db
|
||||
**/.claude-flow/sessions/session-*.json
|
||||
**/.claude-flow/sessions/current.json
|
||||
|
||||
# Node modules (should use npm ci, not committed)
|
||||
**/node_modules/
|
||||
|
||||
# Local build scripts
|
||||
firmware/esp32-csi-node/build_firmware.batdata/
|
||||
models/
|
||||
demo_pointcloud.ply
|
||||
demo_splats.json
|
||||
.cursorindexingignore
|
||||
Binary file not shown.
Vendored
-49
@@ -1,49 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "QEMU ESP32-S3 Debug",
|
||||
"type": "cppdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/firmware/esp32-csi-node/build/esp32-csi-node.elf",
|
||||
"cwd": "${workspaceFolder}/firmware/esp32-csi-node",
|
||||
"MIMode": "gdb",
|
||||
"miDebuggerPath": "xtensa-esp-elf-gdb",
|
||||
"miDebuggerServerAddress": "localhost:1234",
|
||||
"setupCommands": [
|
||||
{
|
||||
"description": "Set remote hardware breakpoint limit (ESP32-S3 has 2)",
|
||||
"text": "set remote hardware-breakpoint-limit 2",
|
||||
"ignoreFailures": false
|
||||
},
|
||||
{
|
||||
"description": "Set remote hardware watchpoint limit (ESP32-S3 has 2)",
|
||||
"text": "set remote hardware-watchpoint-limit 2",
|
||||
"ignoreFailures": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "QEMU ESP32-S3 Debug (attach)",
|
||||
"type": "cppdbg",
|
||||
"request": "attach",
|
||||
"program": "${workspaceFolder}/firmware/esp32-csi-node/build/esp32-csi-node.elf",
|
||||
"cwd": "${workspaceFolder}/firmware/esp32-csi-node",
|
||||
"MIMode": "gdb",
|
||||
"miDebuggerPath": "xtensa-esp-elf-gdb",
|
||||
"miDebuggerServerAddress": "localhost:1234",
|
||||
"setupCommands": [
|
||||
{
|
||||
"description": "Set remote hardware breakpoint limit (ESP32-S3 has 2)",
|
||||
"text": "set remote hardware-breakpoint-limit 2",
|
||||
"ignoreFailures": false
|
||||
},
|
||||
{
|
||||
"description": "Set remote hardware watchpoint limit (ESP32-S3 has 2)",
|
||||
"text": "set remote hardware-watchpoint-limit 2",
|
||||
"ignoreFailures": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+2
-334
@@ -7,339 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- **Ghost skeletons in live UI with multi-node ESP32 setups** (#420, ADR-082) —
|
||||
`tracker_bridge::tracker_to_person_detections` documented itself as filtering
|
||||
to `is_alive()` tracks but in fact passed every non-Terminated track to the
|
||||
WebSocket stream. `Lost` tracks — kept inside `reid_window` for
|
||||
re-identification but not currently observed — were rendering as phantom
|
||||
skeletons, accumulating to 22-24 with 3 nodes × 10 Hz CSI while
|
||||
`estimated_persons` correctly reported 1. Added
|
||||
`PoseTracker::confirmed_tracks()` (Tentative + Active only) and rewired the
|
||||
bridge to use it. Lost tracks remain in the tracker for re-ID; they just
|
||||
no longer ship to the UI. Regression test:
|
||||
`test_lost_tracks_excluded_from_bridge_output`.
|
||||
- **Rust workspace build with `--no-default-features` on Windows** (#366, #415) —
|
||||
`wifi-densepose-mat`, `wifi-densepose-sensing-server`, and `wifi-densepose-train`
|
||||
all depended on `wifi-densepose-signal` with default features enabled, which
|
||||
pulled `ndarray-linalg` → `openblas-src` → vcpkg/system-BLAS through the entire
|
||||
workspace. `--no-default-features` at the workspace root then could not opt out
|
||||
of BLAS, breaking `cargo build` / `cargo test` on Windows without vcpkg. All
|
||||
three consumers now declare `wifi-densepose-signal = { ..., default-features = false }`,
|
||||
so `cargo test --workspace --no-default-features` builds cleanly without
|
||||
vcpkg/openblas. Validated: 1,538 tests pass, 0 fail, 8 ignored.
|
||||
- **`signal` test `test_estimate_occupancy_noise_only` failed without `eigenvalue`** —
|
||||
The test unwrapped the `NotCalibrated` stub returned when the BLAS-backed
|
||||
`estimate_occupancy` is compiled out. Gated with `#[cfg(feature = "eigenvalue")]`
|
||||
so it only runs when the real implementation is available.
|
||||
|
||||
## [v0.6.2-esp32] — 2026-04-20
|
||||
|
||||
Firmware release cutting ADR-081 and the Timer Svc stack fix discovered during
|
||||
on-hardware validation. Cut from `main` at commit pointing to this entry.
|
||||
Tested on ESP32-S3 (QFN56 rev v0.2, MAC `3c:0f:02:e9:b5:f8`), 30 s continuous
|
||||
run: no crashes, 149 `rv_feature_state_t` emissions (~5 Hz), medium/slow ticks
|
||||
firing cleanly, HEALTH mesh packets sent.
|
||||
|
||||
### Fixed
|
||||
- **Firmware: Timer Svc stack overflow on ADR-081 fast loop** — `emit_feature_state()` runs inside the FreeRTOS Timer Svc task via the fast-loop callback; it calls `stream_sender` network I/O which pushes past the ESP-IDF 2 KiB default timer stack and panics ~1 s after boot. Bumped `CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH` to 8 KiB in `sdkconfig.defaults`, `sdkconfig.defaults.template`, and `sdkconfig.defaults.4mb`. Follow-up (tracked separately): move heavy work out of the timer daemon into a dedicated worker task.
|
||||
- **Firmware: `adaptive_controller.c` implicit declaration** (#404) — `fast_loop_cb` called `emit_feature_state()` before its static definition, triggering `-Werror=implicit-function-declaration`. Added a forward declaration above the first use.
|
||||
|
||||
### Changed
|
||||
- **CI: firmware build matrix (8MB + 4MB)** — `firmware-ci.yml` now matrix-builds both the default 8MB (`sdkconfig.defaults`) and 4MB SuperMini (`sdkconfig.defaults.4mb`) variants, uploading distinct artifacts and producing variant-named release binaries (`esp32-csi-node.bin` / `esp32-csi-node-4mb.bin`, `partition-table.bin` / `partition-table-4mb.bin`).
|
||||
|
||||
### Added
|
||||
- **ADR-081: Adaptive CSI Mesh Firmware Kernel** — New 5-layer architecture
|
||||
(Radio Abstraction Layer / Adaptive Controller / Mesh Sensing Plane /
|
||||
On-device Feature Extraction / Rust handoff) that reframes the existing
|
||||
ESP32 firmware modules as components of a chipset-agnostic kernel. ADR
|
||||
in `docs/adr/ADR-081-adaptive-csi-mesh-firmware-kernel.md`. Goal: swap
|
||||
one radio family for another without changing the Rust signal /
|
||||
ruvector / train / mat crates.
|
||||
- **Firmware: radio abstraction vtable (`rv_radio_ops_t`)** — New
|
||||
`firmware/esp32-csi-node/main/rv_radio_ops.{h}` defines the
|
||||
chipset-agnostic ops (init, set_channel, set_mode, set_csi_enabled,
|
||||
set_capture_profile, get_health), profile enum
|
||||
(`RV_PROFILE_PASSIVE_LOW_RATE` / `ACTIVE_PROBE` / `RESP_HIGH_SENS` /
|
||||
`FAST_MOTION` / `CALIBRATION`), and health snapshot struct.
|
||||
`rv_radio_ops_esp32.c` provides the ESP32 binding wrapping
|
||||
`csi_collector` + `esp_wifi_*`. A second binding (mock or alternate
|
||||
chipset) is the portability acceptance test for ADR-081.
|
||||
- **Firmware: `rv_feature_state_t` packet (magic `0xC5110006`)** — New
|
||||
60-byte compact per-node sensing state (packed, verified by
|
||||
`_Static_assert`) in `firmware/esp32-csi-node/main/rv_feature_state.h`:
|
||||
motion, presence, respiration BPM/conf, heartbeat BPM/conf, anomaly
|
||||
score, env-shift score, node coherence, quality flags, IEEE CRC32.
|
||||
Replaces raw ADR-018 CSI as the default upstream stream (~99.7%
|
||||
bandwidth reduction: 300 B/s at 5 Hz vs. ~100 KB/s raw).
|
||||
- **Firmware: mock radio ops binding for QEMU** — New
|
||||
`firmware/esp32-csi-node/main/rv_radio_ops_mock.c`, compiled only when
|
||||
`CONFIG_CSI_MOCK_ENABLED`. Satisfies ADR-081's portability acceptance
|
||||
test: a second `rv_radio_ops_t` binding compiles and runs against the
|
||||
same controller + mesh-plane code as the ESP32 binding.
|
||||
- **Firmware: feature-state emitter wired into controller fast loop** —
|
||||
`adaptive_controller.c` now emits one 60-byte `rv_feature_state_t` per
|
||||
fast tick (default 200 ms → 5 Hz), pulling from the latest edge vitals
|
||||
and controller observation. This is the first end-to-end Layer 4/5
|
||||
path for ADR-081.
|
||||
- **Firmware: `csi_collector_get_pkt_yield_per_sec()` /
|
||||
`_get_send_fail_count()` accessors** — Expose the CSI callback rate
|
||||
and UDP send-failure counter so the ESP32 radio ops binding can
|
||||
populate `rv_radio_health_t.pkt_yield_per_sec` and `.send_fail_count`,
|
||||
closing the adaptive controller's observation loop.
|
||||
- **Firmware: host-side unit test suite for ADR-081 pure logic** — New
|
||||
`firmware/esp32-csi-node/tests/host/` (Makefile + 2 test files + shim
|
||||
`esp_err.h`). Exercises `adaptive_controller_decide()` (9 test cases:
|
||||
degraded gate on pkt-yield collapse + coherence loss, anomaly > motion,
|
||||
motion → SENSE_ACTIVE, aggressive cadence, stable presence →
|
||||
RESP_HIGH_SENS, empty-room default, hysteresis, NULL safety) and
|
||||
`rv_feature_state_*` helpers (size assertion, IEEE CRC32 known
|
||||
vectors, determinism, receiver-side verification). 33/33 assertions
|
||||
pass. Benchmarks: decide() 3.2 ns/call, CRC32(56 B) 614 ns/pkt
|
||||
(87 MB/s), full finalize() 616 ns/call. Pure function
|
||||
`adaptive_controller_decide()` extracted to
|
||||
`adaptive_controller_decide.c` so the firmware build and the host
|
||||
tests share a single source-of-truth implementation.
|
||||
- **Scripts: `validate_qemu_output.py` ADR-081 checks** — Validator
|
||||
(invoked by ADR-061 `scripts/qemu-esp32s3-test.sh` in CI) gains three
|
||||
checks for adaptive controller boot line, mock radio ops
|
||||
registration, and slow-loop heartbeat, so QEMU runs regression-gate
|
||||
Layer 1/2 presence.
|
||||
- **Firmware: ADR-081 Layer 3 mesh sensing plane** — New
|
||||
`firmware/esp32-csi-node/main/rv_mesh.{h,c}` defines 4 node roles
|
||||
(Anchor / Observer / Fusion relay / Coordinator), 7 on-wire message
|
||||
types (TIME_SYNC, ROLE_ASSIGN, CHANNEL_PLAN, CALIBRATION_START,
|
||||
FEATURE_DELTA, HEALTH, ANOMALY_ALERT), 3 authorization classes
|
||||
(None / HMAC-SHA256-session / Ed25519-batch), `rv_node_status_t`
|
||||
(28 B), `rv_anomaly_alert_t` (28 B), `rv_time_sync_t`,
|
||||
`rv_role_assign_t`, `rv_channel_plan_t`, `rv_calibration_start_t`.
|
||||
Pure-C encoder/decoder (`rv_mesh_encode()` / `rv_mesh_decode()`) with
|
||||
16-byte envelope + payload + IEEE CRC32 trailer; convenience encoders
|
||||
for each message type. Controller now emits `HEALTH` every slow-loop
|
||||
tick (30 s default) and `ANOMALY_ALERT` on state transitions to ALERT
|
||||
or DEGRADED. Host tests: `test_rv_mesh` exercises 27 assertions
|
||||
covering roundtrip, bad magic, truncation, CRC flipping, oversize
|
||||
payload rejection, and encode+decode throughput (1.0 μs/roundtrip
|
||||
on host).
|
||||
- **Rust: ADR-081 Layer 1/3 mirror module** — New
|
||||
`crates/wifi-densepose-hardware/src/radio_ops.rs` mirrors the
|
||||
firmware-side `rv_radio_ops_t` vtable as the Rust `RadioOps` trait
|
||||
(init, set_channel, set_mode, set_csi_enabled, set_capture_profile,
|
||||
get_health) and provides `MockRadio` for offline testing.
|
||||
Also mirrors the `rv_mesh.h` types (`MeshHeader`, `NodeStatus`,
|
||||
`AnomalyAlert`, `MeshRole`, `MeshMsgType`, `AuthClass`) and ships
|
||||
byte-identical `crc32_ieee()`, `decode_mesh()`, `decode_node_status()`,
|
||||
`decode_anomaly_alert()`, and `encode_health()`. Exported from
|
||||
`lib.rs`. 8 unit tests pass; `crc32_matches_firmware_vectors`
|
||||
verifies parity with the firmware-side test vectors
|
||||
(`0xCBF43926` for `"123456789"`, `0xD202EF8D` for single-byte zero),
|
||||
and `mesh_constants_match_firmware` asserts `MESH_MAGIC`,
|
||||
`MESH_VERSION`, `MESH_HEADER_SIZE`, and `MESH_MAX_PAYLOAD` match
|
||||
`rv_mesh.h` byte-for-byte. Satisfies ADR-081's portability
|
||||
acceptance test: signal/ruvector/train/mat crates are untouched.
|
||||
- **Firmware: adaptive controller** — New
|
||||
`firmware/esp32-csi-node/main/adaptive_controller.{c,h}` implements
|
||||
the three-loop closed-loop control specified by ADR-081: fast
|
||||
(~200 ms) for cadence and active probing, medium (~1 s) for channel
|
||||
selection and role transitions, slow (~30 s) for baseline
|
||||
recalibration. Pure `adaptive_controller_decide()` policy function is
|
||||
exposed in the header for offline unit testing. Default policy is
|
||||
conservative (`enable_channel_switch` and `enable_role_change` off);
|
||||
Kconfig surface added under "Adaptive Controller (ADR-081)".
|
||||
|
||||
### Fixed
|
||||
- **`provision.py` esptool v5 compat** (#391) — Stale `write_flash` (underscore) syntax in the dry-run manual-flash hint now uses `write-flash` (hyphenated) for esptool >= 5.x. The primary flash command was already correct.
|
||||
- **`provision.py` silent NVS wipe** (#391) — The script replaces the entire `csi_cfg` NVS namespace on every run, so partial invocations were silently erasing WiFi credentials and causing `Retrying WiFi connection (10/10)` in the field. Now refuses to run without `--ssid`, `--password`, and `--target-ip` unless `--force-partial` is passed. `--force-partial` prints a warning listing which keys will be wiped.
|
||||
- **Firmware: defensive `node_id` capture** (#232, #375, #385, #386, #390) — Users on multi-node deployments reported `node_id` reverting to the Kconfig default (`1`) in UDP frames and in the `csi_collector` init log, despite NVS loading the correct value. The root cause (memory corruption of `g_nvs_config`) has not been definitively isolated, but the UDP frame header is now tamper-proof: `csi_collector_init()` captures `g_nvs_config.node_id` into a module-local `s_node_id` once, and `csi_serialize_frame()` plus all other consumers (`edge_processing.c`, `wasm_runtime.c`, `display_ui.c`, `swarm_bridge_init`) read it via the new `csi_collector_get_node_id()` accessor. A canary logs `WARN` if `g_nvs_config.node_id` diverges from `s_node_id` at end-of-init, helping isolate the upstream corruption path. Validated on attached ESP32-S3 (COM8): NVS `node_id=2` propagates through boot log, capture log, init log, and byte[4] of every UDP frame.
|
||||
|
||||
### Docs
|
||||
- **CHANGELOG catch-up** (#367) — Added missing entries for v0.5.5, v0.6.0, and v0.7.0 releases.
|
||||
|
||||
## [v0.7.0] — 2026-04-06
|
||||
|
||||
Model release (no new firmware binary). Firmware remains at v0.6.0-esp32.
|
||||
|
||||
### Added
|
||||
- **Camera ground-truth training pipeline (ADR-079)** — End-to-end supervised WiFlow pose training using MediaPipe + real ESP32 CSI.
|
||||
- `scripts/collect-ground-truth.py` — MediaPipe PoseLandmarker webcam capture (17 COCO keypoints, 30fps), synchronized with CSI recording over nanosecond timestamps.
|
||||
- `scripts/align-ground-truth.js` — Time-aligns camera keypoints with 20-frame CSI windows by binary search, confidence-weighted averaging.
|
||||
- `scripts/train-wiflow-supervised.js` — 3-phase curriculum training (contrastive → supervised SmoothL1 → bone/temporal refinement) with 4 scale presets (lite/small/medium/full).
|
||||
- `scripts/eval-wiflow.js` — PCK@10/20/50, MPJPE, per-joint breakdown, baseline proxy mode.
|
||||
- `scripts/record-csi-udp.py` — Lightweight ESP32 CSI UDP recorder (no Rust build required).
|
||||
- **ruvector optimizations (O6-O10)** — Subcarrier selection (70→35, 50% reduction), attention-weighted subcarriers, Stoer-Wagner min-cut person separation, multi-SPSA gradient estimation, Mac M4 Pro training via Tailscale.
|
||||
- **Scalable WiFlow presets** — `lite` (189K params, ~19 min) through `full` (7.7M params, ~8 hrs) to match dataset size.
|
||||
- **Pre-trained WiFlow v1 model** — 92.9% PCK@20, 974 KB, 186,946 params. Published to [HuggingFace](https://huggingface.co/ruv/ruview) under `wiflow-v1/`.
|
||||
|
||||
### Validated
|
||||
- **92.9% PCK@20** pose accuracy from a 5-minute data collection session with one $9 ESP32-S3 and one laptop webcam.
|
||||
- Training pipeline validated on real paired data: 345 samples, 19 min training, eval loss 0.082, bone constraint 0.008.
|
||||
|
||||
## [v0.6.0-esp32] — 2026-04-03
|
||||
|
||||
### Added
|
||||
- **Pre-trained CSI sensing weights published** — First official pre-trained models on [HuggingFace](https://huggingface.co/ruv/ruview). `model.safetensors` (48 KB), `model-q4.bin` (8 KB 4-bit), `model-q2.bin` (4 KB), `presence-head.json`, per-node LoRA adapters.
|
||||
- **17 sensing applications** — Sleep monitor, apnea detector, stress monitor, gait analyzer, RF tomography, passive radar, material classifier, through-wall detector, device fingerprint, and more. Each as a standalone `scripts/*.js`.
|
||||
- **ADRs 069-078** — 10 new architecture decisions covering Cognitum Seed integration, self-supervised pretraining, ruvllm pipeline, WiFlow architecture, channel hopping, SNN, MinCut person separation, CNN spectrograms, novel RF applications, multi-frequency mesh.
|
||||
- **Kalman tracker** (PR #341 by @taylorjdawson) — temporal smoothing of pose keypoints.
|
||||
|
||||
### Fixed
|
||||
- Security fix merged via PR #310.
|
||||
|
||||
### Performance
|
||||
- Presence detection: 100% accuracy on 60,630 overnight samples.
|
||||
- Inference: 0.008 ms per sample, 164K embeddings/sec.
|
||||
- Contrastive self-supervised training: 51.6% improvement over baseline.
|
||||
|
||||
## [v0.5.5-esp32] — 2026-04-03
|
||||
|
||||
### Added
|
||||
- **WiFlow SOTA architecture (ADR-072)** — TCN + axial attention pose decoder, 1.8M params, 881 KB at 4-bit. 17 COCO keypoints from CSI amplitude only (no phase).
|
||||
- **Multi-frequency mesh scanning (ADR-073)** — ESP32 nodes hop across channels 1/3/5/6/9/11 at 200ms dwell. Neighbor WiFi networks used as passive radar illuminators. Null subcarriers reduced from 19% to 16%.
|
||||
- **Spiking neural network (ADR-074)** — STDP online learning, adapts to new rooms in <30s with no labels, 16-160x less compute than batch training.
|
||||
- **MinCut person counting (ADR-075)** — Stoer-Wagner min-cut on subcarrier correlation graph. Fixes #348 (was always reporting 4 people).
|
||||
- **CNN spectrogram embeddings (ADR-076)** — Treat 64×20 CSI as an image, produce 128-dim environment fingerprints (0.95+ same-room similarity).
|
||||
- **Graph transformer fusion** — Multi-node CSI fusion via GATv2 attention (replaces naive averaging).
|
||||
- **Camera-free pose training pipeline** — Trains 17-keypoint model from 10 sensor signals with no camera required.
|
||||
|
||||
### Fixed
|
||||
- **#348 person counting** — MinCut correctly counts 1-4 people (24/24 validation windows).
|
||||
|
||||
## [v0.5.4-esp32] — 2026-04-02
|
||||
|
||||
### Added
|
||||
- **ADR-069: ESP32 CSI → Cognitum Seed RVF ingest pipeline** — Live-validated pipeline connecting ESP32-S3 CSI sensing to Cognitum Seed (Pi Zero 2 W) edge intelligence appliance. 339 vectors ingested, 100% kNN validation, SHA-256 witness chain verified.
|
||||
- **Feature vector packet (magic 0xC5110003)** — New 48-byte packet with 8 normalized dimensions (presence, motion, breathing, heart rate, phase variance, person count, fall, RSSI) sent at 1 Hz alongside vitals.
|
||||
- **`scripts/seed_csi_bridge.py`** — Python bridge: UDP listener → HTTPS ingest with bearer token auth, `--validate` (kNN + PIR ground truth), `--stats`, `--compact` modes, hash-based vector IDs, NaN/inf rejection, source IP filtering, retry logic.
|
||||
- **Arena Physica research** — 26 research documents in `docs/research/` covering Maxwell's equations in WiFi sensing, Arena Physica Studio analysis, SOTA WiFi sensing 2025-2026, GOAP implementation plan for ESP32 + Pi Zero.
|
||||
- **Cognitum Seed MCP integration** — 114-tool MCP proxy enables AI assistants to query sensing state, vectors, witness chain, and device status directly.
|
||||
|
||||
### Fixed
|
||||
- **Compressed frame magic collision** — Reassigned compressed frame magic from `0xC5110003` to `0xC5110005` to free `0xC5110003` for feature vectors.
|
||||
- **Uninitialized `s_top_k[0]` read** — Guarded variance computation against `s_top_k_count == 0` in `send_feature_vector()`.
|
||||
- **Presence score normalization** — Bridge now divides by 15.0 instead of clamping, preserving dynamic range for raw values 1.41-14.92.
|
||||
- **Stale magic references** — Updated ADR-039, DDD model to reflect `0xC5110005` for compressed frames.
|
||||
|
||||
### Security
|
||||
- **Credential exposure remediation** — Removed hardcoded WiFi passwords and bearer tokens from source files. Added NVS binary/CSV patterns to `.gitignore`. Environment variable fallback for bearer token.
|
||||
- **NaN/Inf injection prevention** — Bridge validates all feature dimensions are finite before Seed ingest.
|
||||
- **UDP source filtering** — `--allowed-sources` argument restricts packet acceptance to known ESP32 IPs.
|
||||
|
||||
### Changed
|
||||
- Wire format table now includes 6 magic numbers: `0xC5110001` (raw), `0xC5110002` (vitals), `0xC5110003` (features), `0xC5110004` (WASM events), `0xC5110005` (compressed), `0xC5110006` (fused vitals).
|
||||
|
||||
## [v0.5.3-esp32] — 2026-03-30
|
||||
|
||||
### Added
|
||||
- **Cross-node RSSI-weighted feature fusion** — Multiple ESP32 nodes fuse CSI features using RSSI-based weighting. Closer node gets higher weight. Reduces variance noise by 29%, keypoint jitter by 72%.
|
||||
- **DynamicMinCut person separation** — Uses `ruvector_mincut::DynamicMinCut` on the subcarrier temporal correlation graph to detect independent motion clusters. Replaces variance-based heuristic for multi-person counting.
|
||||
- **RSSI-based position tracking** — Skeleton position driven by RSSI differential between nodes. Walk between ESP32s and the skeleton follows you.
|
||||
- **Per-node state pipeline (ADR-068)** — Each ESP32 node gets independent `HashMap<u8, NodeState>` with frame history, classification, vitals, and person count. Fixes #249 (the #1 user-reported issue).
|
||||
- **RuVector Phase 1-3 integration** — Subcarrier importance weighting, temporal keypoint smoothing (EMA), coherence gating, skeleton kinematic constraints (Jakobsen relaxation), compressed pose history.
|
||||
- **Client-side lerp smoothing** — UI keypoints interpolate between frames (alpha=0.15) for fluid skeleton movement.
|
||||
- **Multi-node mesh tests** — 8 integration tests covering 1-255 node configurations.
|
||||
- **`wifi_densepose` Python package** — `from wifi_densepose import WiFiDensePose` now works (#314).
|
||||
|
||||
### Fixed
|
||||
- **Watchdog crash on busy LANs (#321)** — Batch-limited edge_dsp to 4 frames before 20ms yield. Fixed idle-path busy-spin (`pdMS_TO_TICKS(5)==0`).
|
||||
- **No detection from edge vitals (#323)** — Server now generates `sensing_update` from Tier 2+ vitals packets.
|
||||
- **RSSI byte offset mismatch (#332)** — Server parsed RSSI from wrong byte (was reading sequence counter).
|
||||
- **Stack overflow risk** — Moved 4KB of BPM scratch buffers from stack to static storage.
|
||||
- **Stale node memory leak** — `node_states` HashMap evicts nodes inactive >60s.
|
||||
- **Unsafe raw pointer removed** — Replaced with safe `.clone()` for adaptive model borrow.
|
||||
- **Firmware CI** — Upgraded to IDF v5.4, replaced `xxd` with `od` (#327).
|
||||
- **Person count double-counting** — Multi-node aggregation changed from `sum` to `max`.
|
||||
- **Skeleton jitter** — Removed tick-based noise, dampened procedural animation, recalibrated feature scaling for real ESP32 data.
|
||||
|
||||
### Changed
|
||||
- Motion-responsive skeleton: arm swing (0-80px) driven by CSI variance, leg kick (0-50px) by motion_band_power, vertical bob when walking.
|
||||
- Person count thresholds recalibrated for real ESP32 hardware (1→2 at 0.70, EMA alpha 0.04).
|
||||
- Vital sign filtering: larger median window (31), faster EMA (0.05), looser HR jump filter (15 BPM).
|
||||
- Vendored ruvector updated to v2.1.0-40 (316 commits ahead).
|
||||
|
||||
### Benchmarks (2-node mesh, COM6 + COM9, 30s)
|
||||
| Metric | Baseline | v0.5.3 | Improvement |
|
||||
|--------|----------|--------|-------------|
|
||||
| Variance noise | 109.4 | 77.6 | **-29%** |
|
||||
| Feature stability | std=154.1 | std=105.4 | **-32%** |
|
||||
| Keypoint jitter | std=4.5px | std=1.3px | **-72%** |
|
||||
| Confidence | 0.643 | 0.686 | **+7%** |
|
||||
| Presence accuracy | 93.4% | 94.6% | **+1.3pp** |
|
||||
|
||||
### Verified
|
||||
- Real hardware: COM6 (node 1) + COM9 (node 2) on ruv.net WiFi
|
||||
- All 284 Rust tests pass, 352 signal crate tests pass
|
||||
- Firmware builds clean at 843 KB
|
||||
- QEMU CI: 11/11 jobs green
|
||||
|
||||
## [v0.5.2-esp32] — 2026-03-28
|
||||
|
||||
### Fixed
|
||||
- RSSI byte offset in frame parser (#332)
|
||||
- Per-node state pipeline for multi-node sensing (#249)
|
||||
- Firmware CI upgraded to IDF v5.4 (#327)
|
||||
|
||||
## [v0.5.1-esp32] — 2026-03-27
|
||||
|
||||
### Fixed
|
||||
- Watchdog crash on busy LANs (#321)
|
||||
- No detection from edge vitals (#323)
|
||||
- `wifi_densepose` Python package import (#314)
|
||||
- Pre-compiled firmware binaries added to release
|
||||
|
||||
## [v0.5.0-esp32] — 2026-03-15
|
||||
|
||||
### Added
|
||||
- **60 GHz mmWave sensor fusion (ADR-063)** — Auto-detects Seeed MR60BHA2 (60 GHz, HR/BR/presence) and HLK-LD2410 (24 GHz, presence/distance) on UART at boot. Probes 115200 then 256000 baud, registers device capabilities, starts background parser.
|
||||
- **48-byte fused vitals packet** (magic `0xC5110004`) — Kalman-style fusion: mmWave 80% + CSI 20% when both available. Automatic fallback to standard 32-byte CSI-only packet.
|
||||
- **Server-side fusion bridge** (`scripts/mmwave_fusion_bridge.py`) — Reads two serial ports simultaneously for dual-sensor setups where mmWave runs on a separate ESP32.
|
||||
- **Multimodal ambient intelligence roadmap (ADR-064)** — 25+ applications from fall detection to sleep monitoring to RF tomography.
|
||||
|
||||
### Verified
|
||||
- Real hardware: ESP32-S3 (COM7) WiFi CSI + ESP32-C6/MR60BHA2 (COM4) 60 GHz mmWave running concurrently. HR=75 bpm, BR=25/min at 52 cm range. All 11 QEMU CI jobs green.
|
||||
|
||||
## [v0.4.3-esp32] — 2026-03-15
|
||||
|
||||
### Fixed
|
||||
- **Fall detection false positives (#263)** — Default threshold raised from 2.0 to 15.0 rad/s²; normal walking (2-5 rad/s²) no longer triggers alerts. Added 3-consecutive-frame debounce and 5-second cooldown between alerts. Verified on real ESP32-S3 hardware: 0 false alerts in 60s / 1,300+ live WiFi CSI frames.
|
||||
- **Kconfig default mismatch** — `CONFIG_EDGE_FALL_THRESH` Kconfig default was still 2000 (=2.0) while `nvs_config.c` fallback was updated to 15.0. Fixed Kconfig to 15000. Caught by real hardware testing — mock data did not reproduce.
|
||||
- **provision.py NVS generator API change** — `esp_idf_nvs_partition_gen` package changed its `generate()` signature; switched to subprocess-first invocation for cross-version compatibility.
|
||||
- **QEMU CI pipeline (11 jobs)** — Fixed all failures: fuzz test `esp_timer` stubs, QEMU `libgcrypt` dependency, NVS matrix generator, IDF container `pip` path, flash image padding, validation WARN handling, swarm `ip`/`cargo` missing.
|
||||
|
||||
### Added
|
||||
- **4MB flash support (#265)** — `partitions_4mb.csv` and `sdkconfig.defaults.4mb` for ESP32-S3 boards with 4MB flash (e.g. SuperMini). Dual OTA slots, 1.856 MB each. Thanks to @sebbu for the community workaround that confirmed feasibility.
|
||||
- **`--strict` flag** for `validate_qemu_output.py` — WARNs now pass by default in CI (no real WiFi in QEMU); use `--strict` to fail on warnings.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **QEMU ESP32-S3 testing platform (ADR-061)** — 9-layer firmware testing without hardware
|
||||
- Mock CSI generator with 10 physics-based scenarios (empty room, walking, fall, multi-person, etc.)
|
||||
- Single-node QEMU runner with 16-check UART validation
|
||||
- Multi-node TDM mesh simulation (TAP networking, 2-6 nodes)
|
||||
- GDB remote debugging with VS Code integration
|
||||
- Code coverage via gcov/lcov + apptrace
|
||||
- Fuzz testing (3 libFuzzer targets + ASAN/UBSAN)
|
||||
- NVS provisioning matrix (14 configs)
|
||||
- Snapshot-based regression testing (sub-second VM restore)
|
||||
- Chaos testing with fault injection + health monitoring
|
||||
- **QEMU Swarm Configurator (ADR-062)** — YAML-driven multi-ESP32 test orchestration
|
||||
- 4 topologies: star, mesh, line, ring
|
||||
- 3 node roles: sensor, coordinator, gateway
|
||||
- 9 swarm-level assertions (boot, crashes, TDM, frame rate, fall detection, etc.)
|
||||
- 7 presets: smoke (2n/15s), standard (3n/60s), ci-matrix, large-mesh, line-relay, ring-fault, heterogeneous
|
||||
- Health oracle with cross-node validation
|
||||
- **QEMU installer** (`install-qemu.sh`) — auto-detects OS, installs deps, builds Espressif QEMU fork
|
||||
- **Unified QEMU CLI** (`qemu-cli.sh`) — single entry point for all 11 QEMU test commands
|
||||
- CI: `firmware-qemu.yml` workflow with QEMU test matrix, fuzz testing, NVS validation, and swarm test jobs
|
||||
- User guide: QEMU testing and swarm configurator section with plain-language walkthrough
|
||||
|
||||
### Fixed
|
||||
- Firmware now boots in QEMU: WiFi/UDP/OTA/display guards for mock CSI mode
|
||||
- 9 bugs in mock_csi.c (LFSR bias, MAC filter init, scenario loop, overflow burst timing)
|
||||
- 23 bugs from ADR-061 deep review (inject_fault.py writes, CI cache, snapshot log corruption, etc.)
|
||||
- 16 bugs from ADR-062 deep review (log filename mismatch, SLIRP port collision, heap false positives, etc.)
|
||||
- All scripts: `--help` flags, prerequisite checks with install hints, standardized exit codes
|
||||
|
||||
- **Sensing server UI API completion (ADR-043)** — 14 fully-functional REST endpoints for model management, CSI recording, and training control
|
||||
- Model CRUD: `GET /api/v1/models`, `GET /api/v1/models/active`, `POST /api/v1/models/load`, `POST /api/v1/models/unload`, `DELETE /api/v1/models/:id`, `GET /api/v1/models/lora/profiles`, `POST /api/v1/models/lora/activate`
|
||||
- CSI recording: `GET /api/v1/recording/list`, `POST /api/v1/recording/start`, `POST /api/v1/recording/stop`, `DELETE /api/v1/recording/:id`
|
||||
@@ -520,7 +188,7 @@ Major release: complete Rust sensing server, full DensePose training pipeline, R
|
||||
- `PresenceClassifier` — rule-based 3-state classification (ABSENT / PRESENT_STILL / ACTIVE)
|
||||
- Cross-receiver agreement scoring for multi-AP confidence boosting
|
||||
- WebSocket sensing server (`ws_server.py`) broadcasting JSON at 2 Hz
|
||||
- Deterministic CSI proof bundles for reproducible verification (`archive/v1/data/proof/`)
|
||||
- Deterministic CSI proof bundles for reproducible verification (`v1/data/proof/`)
|
||||
- Commodity sensing unit tests (`b391638`)
|
||||
|
||||
### Changed
|
||||
@@ -528,7 +196,7 @@ Major release: complete Rust sensing server, full DensePose training pipeline, R
|
||||
|
||||
### Fixed
|
||||
- Review fixes for end-to-end training pipeline (`45f0304`)
|
||||
- Dockerfile paths updated from `src/` to `archive/v1/src/` (`7872987`)
|
||||
- Dockerfile paths updated from `src/` to `v1/src/` (`7872987`)
|
||||
- IoT profile installer instructions updated for aggregator CLI (`f460097`)
|
||||
- `process.env` reference removed from browser ES module (`e320bc9`)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Project: wifi-densepose
|
||||
|
||||
WiFi-based human pose estimation using Channel State Information (CSI).
|
||||
Dual codebase: Python v1 (`v1/`) and Rust port (`v2/`).
|
||||
Dual codebase: Python v1 (`v1/`) and Rust port (`rust-port/wifi-densepose-rs/`).
|
||||
### Key Rust Crates
|
||||
| Crate | Description |
|
||||
|-------|-------------|
|
||||
@@ -70,63 +70,27 @@ All 5 ruvector crates integrated in workspace:
|
||||
- ADR-031: RuView sensing-first RF mode (Proposed)
|
||||
- ADR-032: Multistatic mesh security hardening (Proposed)
|
||||
|
||||
### Supported Hardware
|
||||
|
||||
| Device | Port | Chip | Role | Cost |
|
||||
|--------|------|------|------|------|
|
||||
| ESP32-S3 (8MB flash) | COM7 | Xtensa dual-core | WiFi CSI sensing node | ~$9 |
|
||||
| ESP32-S3 SuperMini (4MB) | — | Xtensa dual-core | WiFi CSI (compact) | ~$6 |
|
||||
| ESP32-C6 + Seeed MR60BHA2 | COM4 | RISC-V + 60 GHz FMCW | mmWave HR/BR/presence | ~$15 |
|
||||
| HLK-LD2410 | — | 24 GHz FMCW | Presence + distance | ~$3 |
|
||||
|
||||
**Not supported:** ESP32 (original), ESP32-C3 — single-core, can't run CSI DSP pipeline.
|
||||
|
||||
### Build & Test Commands (this repo)
|
||||
```bash
|
||||
# Rust — full workspace tests (1,031+ tests, ~2 min)
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo test --workspace --no-default-features
|
||||
|
||||
# Rust — single crate check (no GPU needed)
|
||||
cargo check -p wifi-densepose-train --no-default-features
|
||||
|
||||
# Rust — publish crates (dependency order)
|
||||
cargo publish -p wifi-densepose-core --no-default-features
|
||||
cargo publish -p wifi-densepose-signal --no-default-features
|
||||
# ... see crate publishing order below
|
||||
|
||||
# Python — deterministic proof verification (SHA-256)
|
||||
python archive/v1/data/proof/verify.py
|
||||
python v1/data/proof/verify.py
|
||||
|
||||
# Python — test suite
|
||||
cd archive/v1 && python -m pytest tests/ -x -q
|
||||
cd v1 && python -m pytest tests/ -x -q
|
||||
```
|
||||
|
||||
### ESP32 Firmware Build (Windows — Python subprocess required)
|
||||
```bash
|
||||
# Build 8MB firmware (real WiFi CSI mode, no mocks)
|
||||
# See CLAUDE.local.md for the full Python subprocess command
|
||||
# Key: must strip MSYSTEM env vars for ESP-IDF v5.4 on Git Bash
|
||||
|
||||
# Build 4MB firmware
|
||||
cp sdkconfig.defaults.4mb sdkconfig.defaults
|
||||
# then same build process
|
||||
|
||||
# Flash to COM7
|
||||
# [python, idf_py, '-p', 'COM7', 'flash']
|
||||
|
||||
# Provision WiFi
|
||||
python firmware/esp32-csi-node/provision.py --port COM7 \
|
||||
--ssid "YourWiFi" --password "secret" --target-ip 192.168.1.20
|
||||
|
||||
# Monitor serial
|
||||
python -m serial.tools.miniterm COM7 115200
|
||||
```
|
||||
|
||||
### Firmware Release Process
|
||||
1. Build 8MB from `sdkconfig.defaults.template` (no mock)
|
||||
2. Build 4MB from `sdkconfig.defaults.4mb` (no mock)
|
||||
3. Save 6 binaries: `esp32-csi-node.bin`, `bootloader.bin`, `partition-table.bin`, `ota_data_initial.bin`, `esp32-csi-node-4mb.bin`, `partition-table-4mb.bin`
|
||||
4. Tag: `git tag v0.X.Y-esp32 && git push origin v0.X.Y-esp32`
|
||||
5. Release: `gh release create v0.X.Y-esp32 <binaries> --title "..." --notes-file ...`
|
||||
6. Verify on real hardware (COM7) before publishing
|
||||
7. **CRITICAL:** Always test with real WiFi CSI, not mock mode — mock missed the Kconfig threshold bug
|
||||
|
||||
### Crate Publishing Order
|
||||
Crates must be published in dependency order:
|
||||
1. `wifi-densepose-core` (no internal deps)
|
||||
@@ -151,12 +115,12 @@ Crates must be published in dependency order:
|
||||
|
||||
```bash
|
||||
# 1. Rust tests — must be 1,031+ passed, 0 failed
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo test --workspace --no-default-features
|
||||
|
||||
# 2. Python proof — must print VERDICT: PASS
|
||||
cd ..
|
||||
python archive/v1/data/proof/verify.py
|
||||
cd ../..
|
||||
python v1/data/proof/verify.py
|
||||
|
||||
# 3. Generate witness bundle (includes both above + firmware hashes)
|
||||
bash scripts/generate-witness-bundle.sh
|
||||
@@ -169,8 +133,8 @@ bash VERIFY.sh
|
||||
**If the Python proof hash changes** (e.g., numpy/scipy version update):
|
||||
```bash
|
||||
# Regenerate the expected hash, then verify it passes
|
||||
python archive/v1/data/proof/verify.py --generate-hash
|
||||
python archive/v1/data/proof/verify.py
|
||||
python v1/data/proof/verify.py --generate-hash
|
||||
python v1/data/proof/verify.py
|
||||
```
|
||||
|
||||
**Witness bundle contents** (`dist/witness-bundle-ADR028-<sha>.tar.gz`):
|
||||
@@ -183,9 +147,9 @@ python archive/v1/data/proof/verify.py
|
||||
- `VERIFY.sh` — One-command self-verification for recipients
|
||||
|
||||
**Key proof artifacts:**
|
||||
- `archive/v1/data/proof/verify.py` — Trust Kill Switch: feeds reference signal through production pipeline, hashes output
|
||||
- `archive/v1/data/proof/expected_features.sha256` — Published expected hash
|
||||
- `archive/v1/data/proof/sample_csi_data.json` — 1,000 synthetic CSI frames (seed=42)
|
||||
- `v1/data/proof/verify.py` — Trust Kill Switch: feeds reference signal through production pipeline, hashes output
|
||||
- `v1/data/proof/expected_features.sha256` — Published expected hash
|
||||
- `v1/data/proof/sample_csi_data.json` — 1,000 synthetic CSI frames (seed=42)
|
||||
- `docs/WITNESS-LOG-028.md` — 11-step reproducible verification procedure
|
||||
- `docs/adr/ADR-028-esp32-capability-audit.md` — Complete audit record
|
||||
|
||||
@@ -211,13 +175,13 @@ Active feature branch: `ruvsense-full-implementation` (PR #77)
|
||||
- NEVER save to root folder — use the directories below
|
||||
- `docs/adr/` — Architecture Decision Records (43 ADRs)
|
||||
- `docs/ddd/` — Domain-Driven Design models
|
||||
- `v2/crates/` — Rust workspace crates (15 crates)
|
||||
- `v2/crates/wifi-densepose-signal/src/ruvsense/` — RuvSense multistatic modules (14 files)
|
||||
- `v2/crates/wifi-densepose-ruvector/src/viewpoint/` — Cross-viewpoint fusion (5 files)
|
||||
- `v2/crates/wifi-densepose-hardware/src/esp32/` — ESP32 TDM protocol
|
||||
- `rust-port/wifi-densepose-rs/crates/` — Rust workspace crates (15 crates)
|
||||
- `rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/` — RuvSense multistatic modules (14 files)
|
||||
- `rust-port/wifi-densepose-rs/crates/wifi-densepose-ruvector/src/viewpoint/` — Cross-viewpoint fusion (5 files)
|
||||
- `rust-port/wifi-densepose-rs/crates/wifi-densepose-hardware/src/esp32/` — ESP32 TDM protocol
|
||||
- `firmware/esp32-csi-node/main/` — ESP32 C firmware (channel hopping, NVS config, TDM)
|
||||
- `archive/v1/src/` — Python source (core, hardware, services, api)
|
||||
- `archive/v1/data/proof/` — Deterministic CSI proof bundles
|
||||
- `v1/src/` — Python source (core, hardware, services, api)
|
||||
- `v1/data/proof/` — Deterministic CSI proof bundles
|
||||
- `.claude-flow/` — Claude Flow coordination state (committed for team sharing)
|
||||
- `.claude/` — Claude Code settings, agents, memory (committed for team sharing)
|
||||
|
||||
@@ -243,7 +207,7 @@ Active feature branch: `ruvsense-full-implementation` (PR #77)
|
||||
Before merging any PR, verify each item applies and is addressed:
|
||||
|
||||
1. **Rust tests pass** — `cargo test --workspace --no-default-features` (1,031+ passed, 0 failed)
|
||||
2. **Python proof passes** — `python archive/v1/data/proof/verify.py` (VERDICT: PASS)
|
||||
2. **Python proof passes** — `python v1/data/proof/verify.py` (VERDICT: PASS)
|
||||
3. **README.md** — Update platform tables, crate descriptions, hardware tables, feature summaries if scope changed
|
||||
4. **CLAUDE.md** — Update crate table, ADR list, module tables, version if scope changed
|
||||
5. **CHANGELOG.md** — Add entry under `[Unreleased]` with what was added/fixed/changed
|
||||
|
||||
@@ -1,46 +1,22 @@
|
||||
# π RuView
|
||||
|
||||
<p align="center">
|
||||
<a href="https://x.com/rUv/status/2037556932802761004">
|
||||
<a href="https://ruvnet.github.io/RuView/">
|
||||
<img src="assets/ruview-small-gemini.jpg" alt="RuView - WiFi DensePose" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
> **Beta Software** — Under active development. APIs and firmware may change. Known limitations:
|
||||
> - ESP32-C3 and original ESP32 are not supported (single-core, insufficient for CSI DSP)
|
||||
> - Single ESP32 deployments have limited spatial resolution — use 2+ nodes or add a [Cognitum Seed](https://cognitum.one) for best results
|
||||
> - Camera-free pose accuracy is limited — use [camera ground-truth training](docs/adr/ADR-079-camera-ground-truth-training.md) for 92.9% PCK@20
|
||||
>
|
||||
> Contributions and bug reports welcome at [Issues](https://github.com/ruvnet/RuView/issues).
|
||||
**See through walls with WiFi.** No cameras. No wearables. No Internet. Just radio waves.
|
||||
|
||||
## **See through walls with WiFi** ##
|
||||
WiFi DensePose turns commodity WiFi signals into real-time human pose estimation, vital sign monitoring, and presence detection — all without a single pixel of video.
|
||||
|
||||
**Turn ordinary WiFi into a sensing system.** Detect people, measure breathing and heart rate, track movement, and monitor rooms — through walls, in the dark, with no cameras or wearables. Just physics.
|
||||
|
||||
### π RuView is a WiFi sensing platform that turns radio signals into spatial intelligence.
|
||||
|
||||
Every WiFi router already fills your space with radio waves. When people move, breathe, or even sit still, they disturb those waves in measurable ways. RuView captures these disturbances using Channel State Information (CSI) from low-cost ESP32 sensors and turns them into actionable data: who's there, what they're doing, and whether they're okay.
|
||||
|
||||
**What it senses:**
|
||||
- **Presence and occupancy** — detect people through walls, count them, track entries and exits
|
||||
- **Vital signs** — breathing rate and heart rate, contactless, while sleeping or sitting
|
||||
- **Activity recognition** — walking, sitting, gestures, falls — from temporal CSI patterns
|
||||
- **Environment mapping** — RF fingerprinting identifies rooms, detects moved furniture, spots new objects
|
||||
- **Sleep quality** — overnight monitoring with sleep stage classification and apnea screening
|
||||
|
||||
Built on [RuVector](https://github.com/ruvnet/ruvector/) and [Cognitum Seed](https://cognitum.one), RuView runs entirely on edge hardware — an ESP32 mesh (as low as $9 per node) paired with a Cognitum Seed for persistent memory, cryptographic attestation, and AI integration. No cloud, no cameras, no internet required.
|
||||
|
||||
The system learns each environment locally using spiking neural networks that adapt in under 30 seconds, with multi-frequency mesh scanning across 6 WiFi channels that uses your neighbors' routers as free radar illuminators. Every measurement is cryptographically attested via an Ed25519 witness chain.
|
||||
|
||||
RuView also supports pose estimation (17 COCO keypoints via the WiFlow architecture), trained entirely without cameras using 10 sensor signals — a technique pioneered from the original *DensePose From WiFi* research at Carnegie Mellon University.
|
||||
|
||||
### Built for low-power edge applications
|
||||
By analyzing Channel State Information (CSI) disturbances caused by human movement, the system reconstructs body position, breathing rate, and heartbeat using physics-based signal processing and machine learning.
|
||||
|
||||
[Edge modules](#edge-intelligence-adr-041) are small programs that run directly on the ESP32 sensor — no internet needed, no cloud fees, instant response.
|
||||
|
||||
[](https://www.rust-lang.org/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/ruvnet/RuView)
|
||||
[](https://github.com/ruvnet/RuView)
|
||||
[](https://hub.docker.com/r/ruvnet/wifi-densepose)
|
||||
[](#vital-sign-detection)
|
||||
[](#esp32-s3-hardware-pipeline)
|
||||
@@ -49,340 +25,42 @@ RuView also supports pose estimation (17 COCO keypoints via the WiFlow architect
|
||||
|
||||
> | What | How | Speed |
|
||||
> |------|-----|-------|
|
||||
> | **Pose estimation** | CSI subcarrier amplitude/phase → 17 COCO keypoints | 171K emb/s (M4 Pro) |
|
||||
> | **Breathing detection** | Bandpass 0.1-0.5 Hz → zero-crossing BPM | 6-30 BPM |
|
||||
> | **Heart rate** | Bandpass 0.8-2.0 Hz → zero-crossing BPM | 40-120 BPM |
|
||||
> | **Presence sensing** | Trained model + PIR fusion — 100% accuracy | 0.012 ms latency |
|
||||
> | **Pose estimation** | CSI subcarrier amplitude/phase → DensePose UV maps | 54K fps (Rust) |
|
||||
> | **Breathing detection** | Bandpass 0.1-0.5 Hz → FFT peak | 6-30 BPM |
|
||||
> | **Heart rate** | Bandpass 0.8-2.0 Hz → FFT peak | 40-120 BPM |
|
||||
> | **Presence sensing** | RSSI variance + motion band power | < 1ms latency |
|
||||
> | **Through-wall** | Fresnel zone geometry + multipath modeling | Up to 5m depth |
|
||||
> | **Edge intelligence** | 8-dim feature vectors + RVF store on Cognitum Seed | $140 total BOM |
|
||||
> | **Camera-free training** | 10 sensor signals, no labels needed | 84s on M4 Pro |
|
||||
> | **Camera-supervised training** | MediaPipe + ESP32 CSI → 92.9% PCK@20 | 19 min on laptop |
|
||||
> | **Multi-frequency mesh** | Channel hopping across 6 bands, neighbor APs as illuminators | 3x sensing bandwidth |
|
||||
|
||||
```bash
|
||||
# Option 1: Docker (simulated data, no hardware needed)
|
||||
# 30 seconds to live sensing — no toolchain required
|
||||
docker pull ruvnet/wifi-densepose:latest
|
||||
docker run -p 3000:3000 ruvnet/wifi-densepose:latest
|
||||
# Open http://localhost:3000
|
||||
|
||||
# Option 2: Live sensing with ESP32-S3 hardware ($9)
|
||||
# Flash firmware, provision WiFi, and start sensing:
|
||||
python -m esptool --chip esp32s3 --port COM9 --baud 460800 \
|
||||
write_flash 0x0 bootloader.bin 0x8000 partition-table.bin \
|
||||
0xf000 ota_data_initial.bin 0x20000 esp32-csi-node.bin
|
||||
python firmware/esp32-csi-node/provision.py --port COM9 \
|
||||
--ssid "YourWiFi" --password "secret" --target-ip 192.168.1.20
|
||||
|
||||
# Option 3: Full system with Cognitum Seed ($140)
|
||||
# ESP32 streams CSI → bridge forwards to Seed for persistent storage + kNN + witness chain
|
||||
node scripts/rf-scan.js --port 5006 # Live RF room scan
|
||||
node scripts/snn-csi-processor.js --port 5006 # SNN real-time learning
|
||||
node scripts/mincut-person-counter.js --port 5006 # Correct person counting
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> **CSI-capable hardware recommended.** Presence, vital signs, through-wall sensing, and all advanced capabilities require Channel State Information (CSI) from an ESP32-S3 ($9) or research NIC. The Docker image runs with simulated data for evaluation. Consumer WiFi laptops provide RSSI-only presence detection.
|
||||
> **CSI-capable hardware required.** Pose estimation, vital signs, and through-wall sensing rely on Channel State Information (CSI) — per-subcarrier amplitude and phase data that standard consumer WiFi does not expose. You need CSI-capable hardware (ESP32-S3 or a research NIC) for full functionality. Consumer WiFi laptops can only provide RSSI-based presence detection, which is significantly less capable.
|
||||
|
||||
> **Hardware options** for live CSI capture:
|
||||
>
|
||||
> | Option | Hardware | Cost | Full CSI | Capabilities |
|
||||
> |--------|----------|------|----------|-------------|
|
||||
> | **ESP32 + Cognitum Seed** (recommended) | ESP32-S3 + [Cognitum Seed](https://cognitum.one) | ~$140 | Yes | Pose, breathing, heartbeat, motion, presence + persistent vector store, kNN search, witness chain, MCP proxy |
|
||||
> | **ESP32 Mesh** | 3-6x ESP32-S3 + WiFi router | ~$54 | Yes | Pose, breathing, heartbeat, motion, presence |
|
||||
> | **ESP32 Mesh** (recommended) | 3-6x ESP32-S3 + WiFi router | ~$54 | Yes | Pose, breathing, heartbeat, motion, presence |
|
||||
> | **Research NIC** | Intel 5300 / Atheros AR9580 | ~$50-100 | Yes | Full CSI with 3x3 MIMO |
|
||||
> | **Any WiFi** | Windows, macOS, or Linux laptop | $0 | No | RSSI-only: coarse presence and motion |
|
||||
>
|
||||
> No hardware? Verify the signal processing pipeline with the deterministic reference signal: `python archive/v1/data/proof/verify.py`
|
||||
> No hardware? Verify the signal processing pipeline with the deterministic reference signal: `python v1/data/proof/verify.py`
|
||||
>
|
||||
---
|
||||
|
||||
### Real-Time Dense Point Cloud (NEW)
|
||||
|
||||
RuView now generates **real-time 3D point clouds** by fusing camera depth + WiFi CSI + mmWave radar. All sensors stream simultaneously into a unified spatial model.
|
||||
|
||||
| Sensor | Data | Integration |
|
||||
|--------|------|-------------|
|
||||
| **Camera** | MiDaS monocular depth (GPU) | 640×480 → 19,200+ depth points per frame |
|
||||
| **ESP32 CSI** | ADR-018 binary frames (UDP) | RF tomography → 8×8×4 occupancy grid |
|
||||
| **WiFlow Pose** | 17 COCO keypoints from CSI | Skeleton overlay on point cloud |
|
||||
| **Vital Signs** | Breathing rate from CSI phase | Stored in ruOS brain every 60s |
|
||||
| **Motion** | CSI amplitude variance | Adaptive capture rate (skip depth when still) |
|
||||
|
||||
**Quick start:**
|
||||
```bash
|
||||
cd v2
|
||||
cargo build --release -p wifi-densepose-pointcloud
|
||||
./target/release/ruview-pointcloud serve --bind 127.0.0.1:9880
|
||||
# Open http://localhost:9880 for live 3D viewer
|
||||
```
|
||||
|
||||
**CLI commands:**
|
||||
```bash
|
||||
ruview-pointcloud demo # synthetic demo
|
||||
ruview-pointcloud serve --bind 127.0.0.1:9880 # live server + Three.js viewer
|
||||
ruview-pointcloud capture --output room.ply # capture to PLY
|
||||
ruview-pointcloud train # depth calibration + DPO pairs
|
||||
ruview-pointcloud cameras # list available cameras
|
||||
ruview-pointcloud csi-test --count 100 # send test CSI frames
|
||||
ruview-pointcloud fingerprint office --seconds 5 # record named CSI room fingerprint
|
||||
```
|
||||
|
||||
The HTTP/viewer server defaults to **loopback (`127.0.0.1`)** — exposing live camera/CSI/vitals on `0.0.0.0` is an explicit opt-in. Brain URL defaults to `http://127.0.0.1:9876` and is overridable via `RUVIEW_BRAIN_URL` env var or the `--brain` flag on `serve`/`train`.
|
||||
|
||||
The pose overlay currently uses an **amplitude-energy heuristic** (`heuristic_pose_from_amplitude`) rather than trained WiFlow inference — real ONNX/Candle inference is tracked as a follow-up.
|
||||
|
||||
**Performance:** 22ms pipeline, 905 req/s API, 40K voxel room model from 20 frames.
|
||||
|
||||
**Brain integration:** Spatial observations (motion, vitals, skeleton, occupancy) sync to the ruOS brain every 60 seconds for agent reasoning.
|
||||
|
||||
See [PR #405](https://github.com/ruvnet/RuView/pull/405) for full details.
|
||||
|
||||
### What's New in v0.7.0
|
||||
|
||||
<details>
|
||||
<summary><strong>Camera Ground-Truth Training — 92.9% PCK@20</strong></summary>
|
||||
|
||||
**v0.7.0 adds camera-supervised pose training** using MediaPipe + real ESP32 CSI data:
|
||||
|
||||
| Capability | What it does | ADR |
|
||||
|-----------|-------------|-----|
|
||||
| **Camera ground-truth collection** | MediaPipe PoseLandmarker captures 17 COCO keypoints at 30fps, synced with ESP32 CSI | [ADR-079](docs/adr/ADR-079-camera-ground-truth-training.md) |
|
||||
| **ruvector subcarrier selection** | Variance-based top-K reduces input by 50% (70→35 subcarriers) | ADR-079 O6 |
|
||||
| **Stoer-Wagner min-cut** | Person-specific subcarrier cluster separation for multi-person training | ADR-079 O8 |
|
||||
| **Scalable WiFlow model** | 4 presets: lite (189K) → small (474K) → medium (800K) → full (7.7M params) | ADR-079 |
|
||||
|
||||
```bash
|
||||
# Collect ground truth (camera + ESP32 simultaneously)
|
||||
python scripts/collect-ground-truth.py --duration 300 --preview
|
||||
python scripts/record-csi-udp.py --duration 300
|
||||
|
||||
# Align CSI windows with camera keypoints
|
||||
node scripts/align-ground-truth.js --gt data/ground-truth/*.jsonl --csi data/recordings/*.csi.jsonl
|
||||
|
||||
# Train WiFlow model (start lite, scale up as data grows)
|
||||
node scripts/train-wiflow-supervised.js --data data/paired/*.jsonl --scale lite
|
||||
|
||||
# Evaluate
|
||||
node scripts/eval-wiflow.js --model models/wiflow-real/wiflow-v1.json --data data/paired/*.jsonl
|
||||
```
|
||||
|
||||
**Result: 92.9% PCK@20** from a 5-minute data collection session with one ESP32-S3 and one webcam.
|
||||
|
||||
| Metric | Before (proxy) | After (camera-supervised) |
|
||||
|--------|----------------|--------------------------|
|
||||
| PCK@20 | 0% | **92.9%** |
|
||||
| Eval loss | 0.700 | **0.082** |
|
||||
| Bone constraint | N/A | **0.008** |
|
||||
| Training time | N/A | **19 minutes** |
|
||||
| Model size | N/A | **974 KB** |
|
||||
|
||||
Pre-trained model: [HuggingFace ruv/ruview/wiflow-v1](https://huggingface.co/ruv/ruview)
|
||||
|
||||
</details>
|
||||
|
||||
### Pre-Trained Models (v0.6.0) — No Training Required
|
||||
|
||||
<details>
|
||||
<summary><strong>Download from HuggingFace and start sensing immediately</strong></summary>
|
||||
|
||||
Pre-trained models are available on HuggingFace:
|
||||
> **https://huggingface.co/ruv/ruview** (primary) | [mirror](https://huggingface.co/ruvnet/wifi-densepose-pretrained)
|
||||
|
||||
Trained on 60,630 real-world samples from an 8-hour overnight collection. Just download and run — no datasets, no GPU, no training needed.
|
||||
|
||||
| Model | Size | What it does |
|
||||
|-------|------|-------------|
|
||||
| `model.safetensors` | 48 KB | Contrastive encoder — 128-dim embeddings for presence, activity, environment |
|
||||
| `model-q4.bin` | 8 KB | 4-bit quantized — fits in ESP32-S3 SRAM for edge inference |
|
||||
| `model-q2.bin` | 4 KB | 2-bit ultra-compact for memory-constrained devices |
|
||||
| `presence-head.json` | 2.6 KB | 100% accurate presence detection head |
|
||||
| `node-1.json` / `node-2.json` | 21 KB | Per-room LoRA adapters (swap for new rooms) |
|
||||
|
||||
```bash
|
||||
# Download and use (Python)
|
||||
pip install huggingface_hub
|
||||
huggingface-cli download ruv/ruview --local-dir models/
|
||||
|
||||
# Or use directly with the sensing pipeline
|
||||
node scripts/train-ruvllm.js --data data/recordings/*.csi.jsonl # retrain on your own data
|
||||
node scripts/benchmark-ruvllm.js --model models/csi-ruvllm # benchmark
|
||||
```
|
||||
|
||||
**Benchmarks (Apple M4 Pro, retrained on overnight data):**
|
||||
|
||||
| What we measured | Result | Why it matters |
|
||||
|-----------------|--------|---------------|
|
||||
| **Presence detection** | **100% accuracy** | Never misses a person, never false alarms |
|
||||
| **Inference speed** | **0.008 ms** per embedding | 125,000x faster than real-time |
|
||||
| **Throughput** | **164,183 embeddings/sec** | One Mac Mini handles 1,600+ ESP32 nodes |
|
||||
| **Contrastive learning** | **51.6% improvement** | Strong pattern learning from real overnight data |
|
||||
| **Model size** | **8 KB** (4-bit quantized) | Fits in ESP32 SRAM — no server needed |
|
||||
| **Total hardware cost** | **$140** | ESP32 ($9) + [Cognitum Seed](https://cognitum.one) ($131) |
|
||||
|
||||
</details>
|
||||
|
||||
### 17 Sensing Applications (v0.6.0)
|
||||
|
||||
<details>
|
||||
<summary><strong>Health, environment, security, and multi-frequency mesh sensing</strong></summary>
|
||||
|
||||
All applications run from a single ESP32 + optional Cognitum Seed. No camera, no cloud, no internet.
|
||||
|
||||
**Health & Wellness:**
|
||||
|
||||
| Application | Script | What it detects |
|
||||
|------------|--------|----------------|
|
||||
| Sleep Monitor | `node scripts/sleep-monitor.js` | Sleep stages (deep/light/REM/awake), efficiency, hypnogram |
|
||||
| Apnea Detector | `node scripts/apnea-detector.js` | Breathing pauses >10s, AHI severity scoring |
|
||||
| Stress Monitor | `node scripts/stress-monitor.js` | Heart rate variability, LF/HF stress ratio |
|
||||
| Gait Analyzer | `node scripts/gait-analyzer.js` | Walking cadence, stride asymmetry, tremor detection |
|
||||
|
||||
**Environment & Security:**
|
||||
|
||||
| Application | Script | What it detects |
|
||||
|------------|--------|----------------|
|
||||
| Person Counter | `node scripts/mincut-person-counter.js` | Correct occupancy count (fixes #348) |
|
||||
| Room Fingerprint | `node scripts/room-fingerprint.js` | Activity state clustering, daily patterns, anomalies |
|
||||
| Material Detector | `node scripts/material-detector.js` | New/moved objects via subcarrier null changes |
|
||||
| Device Fingerprint | `node scripts/device-fingerprint.js` | Electronic device activity (printer, router, etc.) |
|
||||
|
||||
**Multi-Frequency Mesh** (requires `--hop-channels` provisioning):
|
||||
|
||||
| Application | Script | What it detects |
|
||||
|------------|--------|----------------|
|
||||
| RF Tomography | `node scripts/rf-tomography.js` | 2D room imaging via RF backprojection |
|
||||
| Passive Radar | `node scripts/passive-radar.js` | Neighbor WiFi APs as bistatic radar illuminators |
|
||||
| Material Classifier | `node scripts/material-classifier.js` | Metal/water/wood/glass from frequency response |
|
||||
| Through-Wall | `node scripts/through-wall-detector.js` | Motion behind walls using lower-frequency penetration |
|
||||
|
||||
All scripts support `--replay data/recordings/*.csi.jsonl` for offline analysis and `--json` for programmatic output.
|
||||
|
||||
</details>
|
||||
|
||||
### What's New in v0.5.5
|
||||
|
||||
<details>
|
||||
<summary><strong>Advanced Sensing: SNN + MinCut + WiFlow + Multi-Frequency Mesh</strong></summary>
|
||||
|
||||
**v0.5.5 adds four new sensing capabilities** built on the [ruvector](https://github.com/ruvnet/ruvector) ecosystem:
|
||||
|
||||
| Capability | What it does | ADR |
|
||||
|-----------|-------------|-----|
|
||||
| **Spiking Neural Network** | Adapts to your room in <30s with STDP online learning — no labels, no batches, 16-160x less compute | [ADR-074](docs/adr/ADR-074-spiking-neural-csi-sensing.md) |
|
||||
| **MinCut Person Counting** | Stoer-Wagner min-cut on subcarrier correlation graph — **fixes #348** (was always 4, now correct) | [ADR-075](docs/adr/ADR-075-mincut-person-separation.md) |
|
||||
| **CNN Spectrogram Embeddings** | Treat CSI as a 64×20 image → 128-dim embedding for environment fingerprinting (0.95+ similarity) | [ADR-076](docs/adr/ADR-076-csi-spectrogram-embeddings.md) |
|
||||
| **WiFlow SOTA Architecture** | TCN + axial attention + pose decoder → 17 COCO keypoints, 1.8M params (881 KB at 4-bit) | [ADR-072](docs/adr/ADR-072-wiflow-architecture.md) |
|
||||
| **Multi-Frequency Mesh** | Channel hopping across 6 bands, neighbor WiFi as passive radar illuminators | [ADR-073](docs/adr/ADR-073-multifrequency-mesh-scan.md) |
|
||||
|
||||
```bash
|
||||
# Live RF room scan (spectrum visualization)
|
||||
node scripts/rf-scan.js --port 5006 --duration 30
|
||||
|
||||
# Correct person counting (fixes #348)
|
||||
node scripts/mincut-person-counter.js --port 5006
|
||||
|
||||
# SNN real-time adaptation
|
||||
node scripts/snn-csi-processor.js --port 5006
|
||||
|
||||
# CNN spectrogram embeddings
|
||||
node scripts/csi-spectrogram.js --replay data/recordings/*.csi.jsonl
|
||||
|
||||
# WiFlow 17-keypoint pose training
|
||||
node scripts/train-wiflow.js --data data/recordings/*.csi.jsonl
|
||||
|
||||
# Enable channel hopping on ESP32
|
||||
python firmware/esp32-csi-node/provision.py --port COM9 --hop-channels "1,6,11"
|
||||
```
|
||||
|
||||
**Validated benchmarks:**
|
||||
|
||||
| Metric | v0.5.4 | v0.5.5 |
|
||||
|--------|--------|--------|
|
||||
| Person counting | Broken (always 4) | **Correct** (MinCut, 24/24) |
|
||||
| WiFi channels | 1 | **6** (multi-freq hopping) |
|
||||
| Null subcarriers | 19% blocked | **16%** (frequency diversity) |
|
||||
| Pose model | 16K params (FC only) | **1.8M params** (WiFlow) |
|
||||
| Online adaptation | None | **<30s** (SNN STDP) |
|
||||
| Fingerprint dims | 8 | **128** (CNN spectrogram) |
|
||||
| Multi-node fusion | Average | **GATv2 attention** |
|
||||
| New scripts | 0 | **15+** |
|
||||
| New ADRs | 3 | **8** (069-076) |
|
||||
|
||||
</details>
|
||||
|
||||
### What's New in v0.5.4
|
||||
|
||||
<details>
|
||||
<summary><strong>Cognitum Seed Integration + Camera-Free Pose Training</strong></summary>
|
||||
|
||||
**v0.5.4 transforms RuView from a real-time sensing tool into a persistent edge AI system.** Your ESP32 now remembers what it senses, learns without cameras, and proves its data cryptographically.
|
||||
|
||||
| Capability | Details | Hardware |
|
||||
|-----------|---------|----------|
|
||||
| **Persistent vector store** | Every sensing event stored as searchable 8-dim vector in RVF format | ESP32 + [Cognitum Seed](https://cognitum.one) ($140) |
|
||||
| **kNN similarity search** | "Find the 10 most similar states to right now" — anomaly detection, fingerprinting | Cognitum Seed |
|
||||
| **Witness chain** | SHA-256 tamper-evident audit trail for every measurement (1,747 entries validated) | Cognitum Seed |
|
||||
| **Camera-free pose training** | 17 COCO keypoints from 10 sensor signals — PIR, RSSI triangulation, subcarrier asymmetry, vibration, BME280 | 2x ESP32 + Seed |
|
||||
| **Pre-trained model** | 82.8 KB (8 KB at 4-bit quantization), 100% presence accuracy, 0 skeleton violations | Download from release |
|
||||
| **Sub-ms inference** | 0.012 ms latency, 171,472 embeddings/sec on M4 Pro | Any machine with Node.js |
|
||||
| **SONA adaptation** | Adapts to new rooms in <1ms without retraining | ruvllm runtime |
|
||||
| **LoRA room adapters** | Per-node fine-tuning with 2,048 parameters per adapter | Automatic |
|
||||
| **114-tool MCP proxy** | AI assistants (Claude, GPT) query sensors directly via JSON-RPC | Cognitum Seed |
|
||||
| **Multi-frequency mesh** | Channel hopping across ch 1/3/5/6/9/11 — neighbor WiFi as passive radar | 2x ESP32 ($18) |
|
||||
| **RF room scanner** | Real-time spectrum visualization: nulls, reflectors, movement, multipath | `node scripts/rf-scan.js` |
|
||||
| **Security hardened** | Bearer tokens, TLS, source IP filtering, NaN rejection, credential rotation | All components |
|
||||
|
||||
**Training pipeline (ruvllm, no PyTorch needed):**
|
||||
|
||||
```bash
|
||||
# Collect data (2 min, ESP32s must be streaming)
|
||||
python scripts/collect-training-data.py --port 5006 --duration 120
|
||||
|
||||
# Train — contrastive pretraining + task heads + LoRA + quantization + EWC
|
||||
node scripts/train-ruvllm.js --data data/recordings/pretrain-*.csi.jsonl
|
||||
|
||||
# Camera-free 17-keypoint pose (uses PIR + RSSI + vibration + subcarrier asymmetry)
|
||||
node scripts/train-camera-free.js --data data/recordings/pretrain-*.csi.jsonl
|
||||
|
||||
# Benchmark
|
||||
node scripts/benchmark-ruvllm.js --model models/csi-ruvllm
|
||||
```
|
||||
|
||||
**Benchmarks — validated on real hardware (Apple M4 Pro + ESP32-S3 + Cognitum Seed):**
|
||||
|
||||
| What we measured | Result | Why it matters |
|
||||
|-----------------|--------|---------------|
|
||||
| **Presence detection** | **100% accuracy** | Never misses a person, never false alarms |
|
||||
| **Person counting** | **24/24 correct** (MinCut) | Fixed the #1 user-reported issue |
|
||||
| **Inference speed** | **0.012 ms** per embedding | 83,000x faster than real-time |
|
||||
| **Throughput** | **171,472 embeddings/sec** | One Mac Mini handles 1,700+ ESP32 nodes |
|
||||
| **Training time** | **84 seconds** | From zero to trained model in under 2 minutes |
|
||||
| **Contrastive learning** | **33.9% improvement** | Model learns meaningful patterns from CSI |
|
||||
| **Model size** | **8 KB** (4-bit quantized) | Fits in ESP32 SRAM — no server needed |
|
||||
| **Skeleton physics** | **0 violations** in 100 frames | Every pose is anatomically valid |
|
||||
| **Pose keypoints** | **17 COCO keypoints** | Full body pose, no camera required |
|
||||
| **WiFi channels** | **6 simultaneous** | 3x more sensing data than single-channel |
|
||||
| **Online adaptation** | **<30 seconds** (SNN) | Learns a new room without retraining |
|
||||
| **Witness chain** | **2,547 entries** verified | Cryptographic proof every measurement is real |
|
||||
| **Test suite** | **1,463 tests passed** | Rock-solid foundation |
|
||||
| **Total hardware cost** | **$140** | ESP32 ($9) + [Cognitum Seed](https://cognitum.one) ($131) |
|
||||
|
||||
See [ADR-069](docs/adr/ADR-069-cognitum-seed-csi-pipeline.md), [ADR-071](docs/adr/ADR-071-ruvllm-training-pipeline.md), and the [Cognitum Seed tutorial](docs/tutorials/cognitum-seed-pretraining.md) for full details.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
| Document | Description |
|
||||
|----------|-------------|
|
||||
| [User Guide](docs/user-guide.md) | Step-by-step guide: installation, first run, API usage, hardware setup, training |
|
||||
| [Build Guide](docs/build-guide.md) | Building from source (Rust and Python) |
|
||||
| [Architecture Decisions](docs/adr/README.md) | 79 ADRs — why each technical choice was made, organized by domain (hardware, signal processing, ML, platform, infrastructure) |
|
||||
| [Architecture Decisions](docs/adr/README.md) | 48 ADRs — why each technical choice was made, organized by domain (hardware, signal processing, ML, platform, infrastructure) |
|
||||
| [Domain Models](docs/ddd/README.md) | 7 DDD models (RuvSense, Signal Processing, Training Pipeline, Hardware Platform, Sensing Server, WiFi-Mat, CHCI) — bounded contexts, aggregates, domain events, and ubiquitous language |
|
||||
| [Desktop App](v2/crates/wifi-densepose-desktop/README.md) | **WIP** — Tauri v2 desktop app for node management, OTA updates, WASM deployment, and mesh visualization |
|
||||
| [Medical Examples](examples/medical/README.md) | Contactless blood pressure, heart rate, breathing rate via 60 GHz mmWave radar — $15 hardware, no wearable |
|
||||
|
||||
---
|
||||
|
||||
@@ -392,14 +70,10 @@ See [ADR-069](docs/adr/ADR-069-cognitum-seed-csi-pipeline.md), [ADR-071](docs/ad
|
||||
</a>
|
||||
<br>
|
||||
<em>Real-time pose skeleton from WiFi CSI signals — no cameras, no wearables</em>
|
||||
<br><br>
|
||||
<br>
|
||||
<a href="https://ruvnet.github.io/RuView/"><strong>▶ Live Observatory Demo</strong></a>
|
||||
|
|
||||
<a href="https://ruvnet.github.io/RuView/pose-fusion.html"><strong>▶ Dual-Modal Pose Fusion Demo</strong></a>
|
||||
|
||||
> The [server](#-quick-start) is optional for visualization and aggregation — the ESP32 [runs independently](#esp32-s3-hardware-pipeline) for presence detection, vital signs, and fall alerts.
|
||||
>
|
||||
> **Live ESP32 pipeline**: Connect an ESP32-S3 node → run the [sensing server](#sensing-server) → open the [pose fusion demo](https://ruvnet.github.io/RuView/pose-fusion.html) for real-time dual-modal pose estimation (webcam + WiFi CSI). See [ADR-059](docs/adr/ADR-059-live-esp32-csi-pipeline.md).
|
||||
|
||||
|
||||
## 🚀 Key Features
|
||||
@@ -581,24 +255,24 @@ Small programs that run directly on the ESP32 sensor — no internet needed, no
|
||||
| ⚛️ | [**Quantum-Inspired**](docs/edge-modules/autonomous.md) | Uses quantum-inspired math to map room-wide signal coherence and search for optimal sensor configurations |
|
||||
| 🤖 | [**Autonomous & Exotic**](docs/edge-modules/autonomous.md) | Self-managing sensor mesh — auto-heals dropped nodes, plans its own actions, and explores experimental signal representations |
|
||||
|
||||
All implemented modules are `no_std` Rust, share a [common utility library](v2/crates/wifi-densepose-wasm-edge/src/vendor_common.rs), and talk to the host through a 12-function API. Full documentation: [**Edge Modules Guide**](docs/edge-modules/README.md). See the [complete implemented module list](#edge-module-list) below.
|
||||
All implemented modules are `no_std` Rust, share a [common utility library](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/vendor_common.rs), and talk to the host through a 12-function API. Full documentation: [**Edge Modules Guide**](docs/edge-modules/README.md). See the [complete implemented module list](#edge-module-list) below.
|
||||
|
||||
<details id="edge-module-list">
|
||||
<summary><strong>🧩 Edge Intelligence — <a href="docs/edge-modules/README.md">All 65 Modules Implemented</a></strong> (ADR-041 complete)</summary>
|
||||
|
||||
All 60 modules are implemented, tested (609 tests passing), and ready to deploy. They compile to `wasm32-unknown-unknown`, run on ESP32-S3 via WASM3, and share a [common utility library](v2/crates/wifi-densepose-wasm-edge/src/vendor_common.rs). Source: [`crates/wifi-densepose-wasm-edge/src/`](v2/crates/wifi-densepose-wasm-edge/src/)
|
||||
All 60 modules are implemented, tested (609 tests passing), and ready to deploy. They compile to `wasm32-unknown-unknown`, run on ESP32-S3 via WASM3, and share a [common utility library](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/vendor_common.rs). Source: [`crates/wifi-densepose-wasm-edge/src/`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/)
|
||||
|
||||
**Core modules** (ADR-040 flagship + early implementations):
|
||||
|
||||
| Module | File | What It Does |
|
||||
|--------|------|-------------|
|
||||
| Gesture Classifier | [`gesture.rs`](v2/crates/wifi-densepose-wasm-edge/src/gesture.rs) | DTW template matching for hand gestures |
|
||||
| Coherence Filter | [`coherence.rs`](v2/crates/wifi-densepose-wasm-edge/src/coherence.rs) | Phase coherence gating for signal quality |
|
||||
| Adversarial Detector | [`adversarial.rs`](v2/crates/wifi-densepose-wasm-edge/src/adversarial.rs) | Detects physically impossible signal patterns |
|
||||
| Intrusion Detector | [`intrusion.rs`](v2/crates/wifi-densepose-wasm-edge/src/intrusion.rs) | Human vs non-human motion classification |
|
||||
| Occupancy Counter | [`occupancy.rs`](v2/crates/wifi-densepose-wasm-edge/src/occupancy.rs) | Zone-level person counting |
|
||||
| Vital Trend | [`vital_trend.rs`](v2/crates/wifi-densepose-wasm-edge/src/vital_trend.rs) | Long-term breathing and heart rate trending |
|
||||
| RVF Parser | [`rvf.rs`](v2/crates/wifi-densepose-wasm-edge/src/rvf.rs) | RVF container format parsing |
|
||||
| Gesture Classifier | [`gesture.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/gesture.rs) | DTW template matching for hand gestures |
|
||||
| Coherence Filter | [`coherence.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/coherence.rs) | Phase coherence gating for signal quality |
|
||||
| Adversarial Detector | [`adversarial.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/adversarial.rs) | Detects physically impossible signal patterns |
|
||||
| Intrusion Detector | [`intrusion.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/intrusion.rs) | Human vs non-human motion classification |
|
||||
| Occupancy Counter | [`occupancy.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/occupancy.rs) | Zone-level person counting |
|
||||
| Vital Trend | [`vital_trend.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/vital_trend.rs) | Long-term breathing and heart rate trending |
|
||||
| RVF Parser | [`rvf.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/rvf.rs) | RVF container format parsing |
|
||||
|
||||
**Vendor-integrated modules** (24 modules, ADR-041 Category 7):
|
||||
|
||||
@@ -606,128 +280,128 @@ All 60 modules are implemented, tested (609 tests passing), and ready to deploy.
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Flash Attention | [`sig_flash_attention.rs`](v2/crates/wifi-densepose-wasm-edge/src/sig_flash_attention.rs) | Tiled attention over 8 subcarrier groups — finds spatial focus regions and entropy | S (<5ms) |
|
||||
| Coherence Gate | [`sig_coherence_gate.rs`](v2/crates/wifi-densepose-wasm-edge/src/sig_coherence_gate.rs) | Z-score phasor gating with hysteresis: Accept / PredictOnly / Reject / Recalibrate | L (<2ms) |
|
||||
| Temporal Compress | [`sig_temporal_compress.rs`](v2/crates/wifi-densepose-wasm-edge/src/sig_temporal_compress.rs) | 3-tier adaptive quantization (8-bit hot / 5-bit warm / 3-bit cold) | L (<2ms) |
|
||||
| Sparse Recovery | [`sig_sparse_recovery.rs`](v2/crates/wifi-densepose-wasm-edge/src/sig_sparse_recovery.rs) | ISTA L1 reconstruction for dropped subcarriers | H (<10ms) |
|
||||
| Person Match | [`sig_mincut_person_match.rs`](v2/crates/wifi-densepose-wasm-edge/src/sig_mincut_person_match.rs) | Hungarian-lite bipartite assignment for multi-person tracking | S (<5ms) |
|
||||
| Optimal Transport | [`sig_optimal_transport.rs`](v2/crates/wifi-densepose-wasm-edge/src/sig_optimal_transport.rs) | Sliced Wasserstein-1 distance with 4 projections | L (<2ms) |
|
||||
| Flash Attention | [`sig_flash_attention.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sig_flash_attention.rs) | Tiled attention over 8 subcarrier groups — finds spatial focus regions and entropy | S (<5ms) |
|
||||
| Coherence Gate | [`sig_coherence_gate.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sig_coherence_gate.rs) | Z-score phasor gating with hysteresis: Accept / PredictOnly / Reject / Recalibrate | L (<2ms) |
|
||||
| Temporal Compress | [`sig_temporal_compress.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sig_temporal_compress.rs) | 3-tier adaptive quantization (8-bit hot / 5-bit warm / 3-bit cold) | L (<2ms) |
|
||||
| Sparse Recovery | [`sig_sparse_recovery.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sig_sparse_recovery.rs) | ISTA L1 reconstruction for dropped subcarriers | H (<10ms) |
|
||||
| Person Match | [`sig_mincut_person_match.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sig_mincut_person_match.rs) | Hungarian-lite bipartite assignment for multi-person tracking | S (<5ms) |
|
||||
| Optimal Transport | [`sig_optimal_transport.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sig_optimal_transport.rs) | Sliced Wasserstein-1 distance with 4 projections | L (<2ms) |
|
||||
|
||||
**🧠 Adaptive Learning** — On-device learning without cloud connectivity
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| DTW Gesture Learn | [`lrn_dtw_gesture_learn.rs`](v2/crates/wifi-densepose-wasm-edge/src/lrn_dtw_gesture_learn.rs) | User-teachable gesture recognition — 3-rehearsal protocol, 16 templates | S (<5ms) |
|
||||
| Anomaly Attractor | [`lrn_anomaly_attractor.rs`](v2/crates/wifi-densepose-wasm-edge/src/lrn_anomaly_attractor.rs) | 4D dynamical system attractor classification with Lyapunov exponents | H (<10ms) |
|
||||
| Meta Adapt | [`lrn_meta_adapt.rs`](v2/crates/wifi-densepose-wasm-edge/src/lrn_meta_adapt.rs) | Hill-climbing self-optimization with safety rollback | L (<2ms) |
|
||||
| EWC Lifelong | [`lrn_ewc_lifelong.rs`](v2/crates/wifi-densepose-wasm-edge/src/lrn_ewc_lifelong.rs) | Elastic Weight Consolidation — remembers past tasks while learning new ones | S (<5ms) |
|
||||
| DTW Gesture Learn | [`lrn_dtw_gesture_learn.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/lrn_dtw_gesture_learn.rs) | User-teachable gesture recognition — 3-rehearsal protocol, 16 templates | S (<5ms) |
|
||||
| Anomaly Attractor | [`lrn_anomaly_attractor.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/lrn_anomaly_attractor.rs) | 4D dynamical system attractor classification with Lyapunov exponents | H (<10ms) |
|
||||
| Meta Adapt | [`lrn_meta_adapt.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/lrn_meta_adapt.rs) | Hill-climbing self-optimization with safety rollback | L (<2ms) |
|
||||
| EWC Lifelong | [`lrn_ewc_lifelong.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/lrn_ewc_lifelong.rs) | Elastic Weight Consolidation — remembers past tasks while learning new ones | S (<5ms) |
|
||||
|
||||
**🗺️ Spatial Reasoning** — Location, proximity, and influence mapping
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| PageRank Influence | [`spt_pagerank_influence.rs`](v2/crates/wifi-densepose-wasm-edge/src/spt_pagerank_influence.rs) | 4x4 cross-correlation graph with power iteration PageRank | L (<2ms) |
|
||||
| Micro HNSW | [`spt_micro_hnsw.rs`](v2/crates/wifi-densepose-wasm-edge/src/spt_micro_hnsw.rs) | 64-vector navigable small-world graph for nearest-neighbor search | S (<5ms) |
|
||||
| Spiking Tracker | [`spt_spiking_tracker.rs`](v2/crates/wifi-densepose-wasm-edge/src/spt_spiking_tracker.rs) | 32 LIF neurons + 4 output zone neurons with STDP learning | S (<5ms) |
|
||||
| PageRank Influence | [`spt_pagerank_influence.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/spt_pagerank_influence.rs) | 4x4 cross-correlation graph with power iteration PageRank | L (<2ms) |
|
||||
| Micro HNSW | [`spt_micro_hnsw.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/spt_micro_hnsw.rs) | 64-vector navigable small-world graph for nearest-neighbor search | S (<5ms) |
|
||||
| Spiking Tracker | [`spt_spiking_tracker.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/spt_spiking_tracker.rs) | 32 LIF neurons + 4 output zone neurons with STDP learning | S (<5ms) |
|
||||
|
||||
**⏱️ Temporal Analysis** — Activity patterns, logic verification, autonomous planning
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Pattern Sequence | [`tmp_pattern_sequence.rs`](v2/crates/wifi-densepose-wasm-edge/src/tmp_pattern_sequence.rs) | Activity routine detection and deviation alerts | S (<5ms) |
|
||||
| Temporal Logic Guard | [`tmp_temporal_logic_guard.rs`](v2/crates/wifi-densepose-wasm-edge/src/tmp_temporal_logic_guard.rs) | LTL formula verification on CSI event streams | S (<5ms) |
|
||||
| GOAP Autonomy | [`tmp_goap_autonomy.rs`](v2/crates/wifi-densepose-wasm-edge/src/tmp_goap_autonomy.rs) | Goal-Oriented Action Planning for autonomous module management | S (<5ms) |
|
||||
| Pattern Sequence | [`tmp_pattern_sequence.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/tmp_pattern_sequence.rs) | Activity routine detection and deviation alerts | S (<5ms) |
|
||||
| Temporal Logic Guard | [`tmp_temporal_logic_guard.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/tmp_temporal_logic_guard.rs) | LTL formula verification on CSI event streams | S (<5ms) |
|
||||
| GOAP Autonomy | [`tmp_goap_autonomy.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/tmp_goap_autonomy.rs) | Goal-Oriented Action Planning for autonomous module management | S (<5ms) |
|
||||
|
||||
**🛡️ AI Security** — Tamper detection and behavioral anomaly profiling
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Prompt Shield | [`ais_prompt_shield.rs`](v2/crates/wifi-densepose-wasm-edge/src/ais_prompt_shield.rs) | FNV-1a replay detection, injection detection (10x amplitude), jamming (SNR) | L (<2ms) |
|
||||
| Behavioral Profiler | [`ais_behavioral_profiler.rs`](v2/crates/wifi-densepose-wasm-edge/src/ais_behavioral_profiler.rs) | 6D behavioral profile with Mahalanobis anomaly scoring | S (<5ms) |
|
||||
| Prompt Shield | [`ais_prompt_shield.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ais_prompt_shield.rs) | FNV-1a replay detection, injection detection (10x amplitude), jamming (SNR) | L (<2ms) |
|
||||
| Behavioral Profiler | [`ais_behavioral_profiler.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ais_behavioral_profiler.rs) | 6D behavioral profile with Mahalanobis anomaly scoring | S (<5ms) |
|
||||
|
||||
**⚛️ Quantum-Inspired** — Quantum computing metaphors applied to CSI analysis
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Quantum Coherence | [`qnt_quantum_coherence.rs`](v2/crates/wifi-densepose-wasm-edge/src/qnt_quantum_coherence.rs) | Bloch sphere mapping, Von Neumann entropy, decoherence detection | S (<5ms) |
|
||||
| Interference Search | [`qnt_interference_search.rs`](v2/crates/wifi-densepose-wasm-edge/src/qnt_interference_search.rs) | 16 room-state hypotheses with Grover-inspired oracle + diffusion | S (<5ms) |
|
||||
| Quantum Coherence | [`qnt_quantum_coherence.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/qnt_quantum_coherence.rs) | Bloch sphere mapping, Von Neumann entropy, decoherence detection | S (<5ms) |
|
||||
| Interference Search | [`qnt_interference_search.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/qnt_interference_search.rs) | 16 room-state hypotheses with Grover-inspired oracle + diffusion | S (<5ms) |
|
||||
|
||||
**🤖 Autonomous Systems** — Self-governing and self-healing behaviors
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Psycho-Symbolic | [`aut_psycho_symbolic.rs`](v2/crates/wifi-densepose-wasm-edge/src/aut_psycho_symbolic.rs) | 16-rule forward-chaining knowledge base with contradiction detection | S (<5ms) |
|
||||
| Self-Healing Mesh | [`aut_self_healing_mesh.rs`](v2/crates/wifi-densepose-wasm-edge/src/aut_self_healing_mesh.rs) | 8-node mesh with health tracking, degradation/recovery, coverage healing | S (<5ms) |
|
||||
| Psycho-Symbolic | [`aut_psycho_symbolic.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/aut_psycho_symbolic.rs) | 16-rule forward-chaining knowledge base with contradiction detection | S (<5ms) |
|
||||
| Self-Healing Mesh | [`aut_self_healing_mesh.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/aut_self_healing_mesh.rs) | 8-node mesh with health tracking, degradation/recovery, coverage healing | S (<5ms) |
|
||||
|
||||
**🔮 Exotic (Vendor)** — Novel mathematical models for CSI interpretation
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Time Crystal | [`exo_time_crystal.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_time_crystal.rs) | Autocorrelation subharmonic detection in 256-frame history | S (<5ms) |
|
||||
| Hyperbolic Space | [`exo_hyperbolic_space.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_hyperbolic_space.rs) | Poincare ball embedding with 32 reference locations, hyperbolic distance | S (<5ms) |
|
||||
| Time Crystal | [`exo_time_crystal.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_time_crystal.rs) | Autocorrelation subharmonic detection in 256-frame history | S (<5ms) |
|
||||
| Hyperbolic Space | [`exo_hyperbolic_space.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_hyperbolic_space.rs) | Poincare ball embedding with 32 reference locations, hyperbolic distance | S (<5ms) |
|
||||
|
||||
**🏥 Medical & Health** (Category 1) — Contactless health monitoring
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Sleep Apnea | [`med_sleep_apnea.rs`](v2/crates/wifi-densepose-wasm-edge/src/med_sleep_apnea.rs) | Detects breathing pauses during sleep | S (<5ms) |
|
||||
| Cardiac Arrhythmia | [`med_cardiac_arrhythmia.rs`](v2/crates/wifi-densepose-wasm-edge/src/med_cardiac_arrhythmia.rs) | Monitors heart rate for irregular rhythms | S (<5ms) |
|
||||
| Respiratory Distress | [`med_respiratory_distress.rs`](v2/crates/wifi-densepose-wasm-edge/src/med_respiratory_distress.rs) | Alerts on abnormal breathing patterns | S (<5ms) |
|
||||
| Gait Analysis | [`med_gait_analysis.rs`](v2/crates/wifi-densepose-wasm-edge/src/med_gait_analysis.rs) | Tracks walking patterns and detects changes | S (<5ms) |
|
||||
| Seizure Detection | [`med_seizure_detect.rs`](v2/crates/wifi-densepose-wasm-edge/src/med_seizure_detect.rs) | 6-state machine for tonic-clonic seizure recognition | S (<5ms) |
|
||||
| Sleep Apnea | [`med_sleep_apnea.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/med_sleep_apnea.rs) | Detects breathing pauses during sleep | S (<5ms) |
|
||||
| Cardiac Arrhythmia | [`med_cardiac_arrhythmia.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/med_cardiac_arrhythmia.rs) | Monitors heart rate for irregular rhythms | S (<5ms) |
|
||||
| Respiratory Distress | [`med_respiratory_distress.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/med_respiratory_distress.rs) | Alerts on abnormal breathing patterns | S (<5ms) |
|
||||
| Gait Analysis | [`med_gait_analysis.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/med_gait_analysis.rs) | Tracks walking patterns and detects changes | S (<5ms) |
|
||||
| Seizure Detection | [`med_seizure_detect.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/med_seizure_detect.rs) | 6-state machine for tonic-clonic seizure recognition | S (<5ms) |
|
||||
|
||||
**🔐 Security & Safety** (Category 2) — Perimeter and threat detection
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Perimeter Breach | [`sec_perimeter_breach.rs`](v2/crates/wifi-densepose-wasm-edge/src/sec_perimeter_breach.rs) | Detects boundary crossings with approach/departure | S (<5ms) |
|
||||
| Weapon Detection | [`sec_weapon_detect.rs`](v2/crates/wifi-densepose-wasm-edge/src/sec_weapon_detect.rs) | Metal anomaly detection via CSI amplitude shifts | S (<5ms) |
|
||||
| Tailgating | [`sec_tailgating.rs`](v2/crates/wifi-densepose-wasm-edge/src/sec_tailgating.rs) | Detects unauthorized follow-through at access points | S (<5ms) |
|
||||
| Loitering | [`sec_loitering.rs`](v2/crates/wifi-densepose-wasm-edge/src/sec_loitering.rs) | Alerts when someone lingers too long in a zone | S (<5ms) |
|
||||
| Panic Motion | [`sec_panic_motion.rs`](v2/crates/wifi-densepose-wasm-edge/src/sec_panic_motion.rs) | Detects fleeing, struggling, or panic movement | S (<5ms) |
|
||||
| Perimeter Breach | [`sec_perimeter_breach.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sec_perimeter_breach.rs) | Detects boundary crossings with approach/departure | S (<5ms) |
|
||||
| Weapon Detection | [`sec_weapon_detect.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sec_weapon_detect.rs) | Metal anomaly detection via CSI amplitude shifts | S (<5ms) |
|
||||
| Tailgating | [`sec_tailgating.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sec_tailgating.rs) | Detects unauthorized follow-through at access points | S (<5ms) |
|
||||
| Loitering | [`sec_loitering.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sec_loitering.rs) | Alerts when someone lingers too long in a zone | S (<5ms) |
|
||||
| Panic Motion | [`sec_panic_motion.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sec_panic_motion.rs) | Detects fleeing, struggling, or panic movement | S (<5ms) |
|
||||
|
||||
**🏢 Smart Building** (Category 3) — Automation and energy efficiency
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| HVAC Presence | [`bld_hvac_presence.rs`](v2/crates/wifi-densepose-wasm-edge/src/bld_hvac_presence.rs) | Occupancy-driven HVAC control with departure countdown | S (<5ms) |
|
||||
| Lighting Zones | [`bld_lighting_zones.rs`](v2/crates/wifi-densepose-wasm-edge/src/bld_lighting_zones.rs) | Auto-dim/off lighting based on zone activity | S (<5ms) |
|
||||
| Elevator Count | [`bld_elevator_count.rs`](v2/crates/wifi-densepose-wasm-edge/src/bld_elevator_count.rs) | Counts people entering/leaving with overload warning | S (<5ms) |
|
||||
| Meeting Room | [`bld_meeting_room.rs`](v2/crates/wifi-densepose-wasm-edge/src/bld_meeting_room.rs) | Tracks meeting lifecycle: start, headcount, end, availability | S (<5ms) |
|
||||
| Energy Audit | [`bld_energy_audit.rs`](v2/crates/wifi-densepose-wasm-edge/src/bld_energy_audit.rs) | Tracks after-hours usage and room utilization rates | S (<5ms) |
|
||||
| HVAC Presence | [`bld_hvac_presence.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/bld_hvac_presence.rs) | Occupancy-driven HVAC control with departure countdown | S (<5ms) |
|
||||
| Lighting Zones | [`bld_lighting_zones.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/bld_lighting_zones.rs) | Auto-dim/off lighting based on zone activity | S (<5ms) |
|
||||
| Elevator Count | [`bld_elevator_count.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/bld_elevator_count.rs) | Counts people entering/leaving with overload warning | S (<5ms) |
|
||||
| Meeting Room | [`bld_meeting_room.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/bld_meeting_room.rs) | Tracks meeting lifecycle: start, headcount, end, availability | S (<5ms) |
|
||||
| Energy Audit | [`bld_energy_audit.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/bld_energy_audit.rs) | Tracks after-hours usage and room utilization rates | S (<5ms) |
|
||||
|
||||
**🛒 Retail & Hospitality** (Category 4) — Customer insights without cameras
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Queue Length | [`ret_queue_length.rs`](v2/crates/wifi-densepose-wasm-edge/src/ret_queue_length.rs) | Estimates queue size and wait times | S (<5ms) |
|
||||
| Dwell Heatmap | [`ret_dwell_heatmap.rs`](v2/crates/wifi-densepose-wasm-edge/src/ret_dwell_heatmap.rs) | Shows where people spend time (hot/cold zones) | S (<5ms) |
|
||||
| Customer Flow | [`ret_customer_flow.rs`](v2/crates/wifi-densepose-wasm-edge/src/ret_customer_flow.rs) | Counts ins/outs and tracks net occupancy | S (<5ms) |
|
||||
| Table Turnover | [`ret_table_turnover.rs`](v2/crates/wifi-densepose-wasm-edge/src/ret_table_turnover.rs) | Restaurant table lifecycle: seated, dining, vacated | S (<5ms) |
|
||||
| Shelf Engagement | [`ret_shelf_engagement.rs`](v2/crates/wifi-densepose-wasm-edge/src/ret_shelf_engagement.rs) | Detects browsing, considering, and reaching for products | S (<5ms) |
|
||||
| Queue Length | [`ret_queue_length.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ret_queue_length.rs) | Estimates queue size and wait times | S (<5ms) |
|
||||
| Dwell Heatmap | [`ret_dwell_heatmap.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ret_dwell_heatmap.rs) | Shows where people spend time (hot/cold zones) | S (<5ms) |
|
||||
| Customer Flow | [`ret_customer_flow.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ret_customer_flow.rs) | Counts ins/outs and tracks net occupancy | S (<5ms) |
|
||||
| Table Turnover | [`ret_table_turnover.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ret_table_turnover.rs) | Restaurant table lifecycle: seated, dining, vacated | S (<5ms) |
|
||||
| Shelf Engagement | [`ret_shelf_engagement.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ret_shelf_engagement.rs) | Detects browsing, considering, and reaching for products | S (<5ms) |
|
||||
|
||||
**🏭 Industrial & Specialized** (Category 5) — Safety and compliance
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Forklift Proximity | [`ind_forklift_proximity.rs`](v2/crates/wifi-densepose-wasm-edge/src/ind_forklift_proximity.rs) | Warns when people get too close to vehicles | S (<5ms) |
|
||||
| Confined Space | [`ind_confined_space.rs`](v2/crates/wifi-densepose-wasm-edge/src/ind_confined_space.rs) | OSHA-compliant worker monitoring with extraction alerts | S (<5ms) |
|
||||
| Clean Room | [`ind_clean_room.rs`](v2/crates/wifi-densepose-wasm-edge/src/ind_clean_room.rs) | Occupancy limits and turbulent motion detection | S (<5ms) |
|
||||
| Livestock Monitor | [`ind_livestock_monitor.rs`](v2/crates/wifi-densepose-wasm-edge/src/ind_livestock_monitor.rs) | Animal presence, stillness, and escape alerts | S (<5ms) |
|
||||
| Structural Vibration | [`ind_structural_vibration.rs`](v2/crates/wifi-densepose-wasm-edge/src/ind_structural_vibration.rs) | Seismic events, mechanical resonance, structural drift | S (<5ms) |
|
||||
| Forklift Proximity | [`ind_forklift_proximity.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ind_forklift_proximity.rs) | Warns when people get too close to vehicles | S (<5ms) |
|
||||
| Confined Space | [`ind_confined_space.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ind_confined_space.rs) | OSHA-compliant worker monitoring with extraction alerts | S (<5ms) |
|
||||
| Clean Room | [`ind_clean_room.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ind_clean_room.rs) | Occupancy limits and turbulent motion detection | S (<5ms) |
|
||||
| Livestock Monitor | [`ind_livestock_monitor.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ind_livestock_monitor.rs) | Animal presence, stillness, and escape alerts | S (<5ms) |
|
||||
| Structural Vibration | [`ind_structural_vibration.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ind_structural_vibration.rs) | Seismic events, mechanical resonance, structural drift | S (<5ms) |
|
||||
|
||||
**🔮 Exotic & Research** (Category 6) — Experimental sensing applications
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Dream Stage | [`exo_dream_stage.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_dream_stage.rs) | Contactless sleep stage classification (wake/light/deep/REM) | S (<5ms) |
|
||||
| Emotion Detection | [`exo_emotion_detect.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_emotion_detect.rs) | Arousal, stress, and calm detection from micro-movements | S (<5ms) |
|
||||
| Gesture Language | [`exo_gesture_language.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_gesture_language.rs) | Sign language letter recognition via WiFi | S (<5ms) |
|
||||
| Music Conductor | [`exo_music_conductor.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_music_conductor.rs) | Tempo and dynamic tracking from conducting gestures | S (<5ms) |
|
||||
| Plant Growth | [`exo_plant_growth.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_plant_growth.rs) | Monitors plant growth, circadian rhythms, wilt detection | S (<5ms) |
|
||||
| Ghost Hunter | [`exo_ghost_hunter.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_ghost_hunter.rs) | Environmental anomaly classification (draft/insect/wind/unknown) | S (<5ms) |
|
||||
| Rain Detection | [`exo_rain_detect.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_rain_detect.rs) | Detects rain onset, intensity, and cessation via signal scatter | S (<5ms) |
|
||||
| Breathing Sync | [`exo_breathing_sync.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_breathing_sync.rs) | Detects synchronized breathing between multiple people | S (<5ms) |
|
||||
| Dream Stage | [`exo_dream_stage.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_dream_stage.rs) | Contactless sleep stage classification (wake/light/deep/REM) | S (<5ms) |
|
||||
| Emotion Detection | [`exo_emotion_detect.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_emotion_detect.rs) | Arousal, stress, and calm detection from micro-movements | S (<5ms) |
|
||||
| Gesture Language | [`exo_gesture_language.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_gesture_language.rs) | Sign language letter recognition via WiFi | S (<5ms) |
|
||||
| Music Conductor | [`exo_music_conductor.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_music_conductor.rs) | Tempo and dynamic tracking from conducting gestures | S (<5ms) |
|
||||
| Plant Growth | [`exo_plant_growth.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_plant_growth.rs) | Monitors plant growth, circadian rhythms, wilt detection | S (<5ms) |
|
||||
| Ghost Hunter | [`exo_ghost_hunter.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_ghost_hunter.rs) | Environmental anomaly classification (draft/insect/wind/unknown) | S (<5ms) |
|
||||
| Rain Detection | [`exo_rain_detect.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_rain_detect.rs) | Detects rain onset, intensity, and cessation via signal scatter | S (<5ms) |
|
||||
| Breathing Sync | [`exo_breathing_sync.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_breathing_sync.rs) | Detects synchronized breathing between multiple people | S (<5ms) |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -855,7 +529,7 @@ git clone https://github.com/ruvnet/RuView.git
|
||||
cd RuView
|
||||
|
||||
# Rust (primary — 810x faster)
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo build --release
|
||||
cargo test --workspace
|
||||
|
||||
@@ -945,13 +619,9 @@ cargo add wifi-densepose-ruvector # RuVector v2.0.4 integration layer (ADR-017
|
||||
| [`wifi-densepose-api`](https://crates.io/crates/wifi-densepose-api) | REST + WebSocket API layer | -- | [](https://crates.io/crates/wifi-densepose-api) |
|
||||
| [`wifi-densepose-config`](https://crates.io/crates/wifi-densepose-config) | Configuration management | -- | [](https://crates.io/crates/wifi-densepose-config) |
|
||||
| [`wifi-densepose-db`](https://crates.io/crates/wifi-densepose-db) | Database persistence (PostgreSQL, SQLite, Redis) | -- | [](https://crates.io/crates/wifi-densepose-db) |
|
||||
| `wifi-densepose-pointcloud` | Real-time dense point cloud from camera + WiFi CSI fusion (Three.js viewer, brain bridge). Workspace-only for now. | -- | — |
|
||||
| `wifi-densepose-geo` | Geospatial context (Sentinel-2 tiles, SRTM elevation, OSM, weather, night-mode). Workspace-only for now. | -- | — |
|
||||
|
||||
All crates integrate with [RuVector v2.0.4](https://github.com/ruvnet/ruvector) — see [AI Backbone](#ai-backbone-ruvector) below.
|
||||
|
||||
**[rUv Neural](v2/crates/ruv-neural/)** — A separate 12-crate workspace for brain network topology analysis, neural decoding, and medical sensing. See [rUv Neural](#ruv-neural) in Models & Training.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
@@ -1050,7 +720,6 @@ The neural pipeline uses a graph transformer with cross-attention to map CSI fea
|
||||
| [RVF Model Container](#rvf-model-container) | Binary packaging with Ed25519 signing, progressive 3-layer loading, SIMD quantization | [ADR-023](docs/adr/ADR-023-trained-densepose-model-ruvector-pipeline.md) |
|
||||
| [Training & Fine-Tuning](#training--fine-tuning) | 8-phase pure Rust pipeline (7,832 lines), MM-Fi/Wi-Pose pre-training, 6-term composite loss, SONA LoRA | [ADR-023](docs/adr/ADR-023-trained-densepose-model-ruvector-pipeline.md) |
|
||||
| [RuVector Crates](#ruvector-crates) | 11 vendored Rust crates from [ruvector](https://github.com/ruvnet/ruvector): attention, min-cut, solver, GNN, HNSW, temporal compression, sparse inference | [GitHub](https://github.com/ruvnet/ruvector) · [Source](vendor/ruvector/) |
|
||||
| [rUv Neural](#ruv-neural) | 12-crate brain topology analysis ecosystem: neural decoding, quantum sensor integration, cognitive state classification, BCI output | [README](v2/crates/ruv-neural/README.md) |
|
||||
| [AI Backbone (RuVector)](#ai-backbone-ruvector) | 5 AI capabilities replacing hand-tuned thresholds: attention, graph min-cut, sparse solvers, tiered compression | [crates.io](https://crates.io/crates/wifi-densepose-ruvector) |
|
||||
| [Self-Learning WiFi AI (ADR-024)](#self-learning-wifi-ai-adr-024) | Contrastive self-supervised learning, room fingerprinting, anomaly detection, 55 KB model | [ADR-024](docs/adr/ADR-024-contrastive-csi-embedding-model.md) |
|
||||
| [Cross-Environment Generalization (ADR-027)](docs/adr/ADR-027-cross-environment-domain-generalization.md) | Domain-adversarial training, geometry-conditioned inference, hardware normalization, zero-shot deployment | [ADR-027](docs/adr/ADR-027-cross-environment-domain-generalization.md) |
|
||||
@@ -1168,10 +837,10 @@ Bundle verify: 7/7 checks PASS
|
||||
**Verify it yourself** (no hardware needed):
|
||||
```bash
|
||||
# Run all tests
|
||||
cd v2 && cargo test --workspace --no-default-features
|
||||
cd rust-port/wifi-densepose-rs && cargo test --workspace --no-default-features
|
||||
|
||||
# Run the deterministic proof
|
||||
python archive/v1/data/proof/verify.py
|
||||
python v1/data/proof/verify.py
|
||||
|
||||
# Generate + verify the witness bundle
|
||||
bash scripts/generate-witness-bundle.sh
|
||||
@@ -1345,7 +1014,7 @@ ESP32-S3 node UDP/5005 Host server (optional)
|
||||
| Subcarriers per frame | 64 / 128 / 192 (depends on WiFi mode) |
|
||||
| UDP latency | < 1 ms on local network |
|
||||
| Presence detection range | Reliable at 3 m through walls |
|
||||
| Binary size | 990 KB (8MB flash) / 773 KB (4MB flash) |
|
||||
| Binary size | 947 KB (fits in 1 MB flash partition) |
|
||||
| Boot to ready | ~3.9 seconds |
|
||||
|
||||
### Flash and provision
|
||||
@@ -1354,28 +1023,14 @@ Download a pre-built binary — no build toolchain needed:
|
||||
|
||||
| Release | What's included | Tag |
|
||||
|---------|-----------------|-----|
|
||||
| [v0.7.0](https://github.com/ruvnet/RuView/releases/tag/v0.7.0) | **Latest** — Camera-supervised WiFlow model (92.9% PCK@20), ground-truth training pipeline, ruvector optimizations | `v0.7.0` |
|
||||
| [v0.6.0](https://github.com/ruvnet/RuView/releases/tag/v0.6.0-esp32) | [Pre-trained models on HuggingFace](https://huggingface.co/ruv/ruview), 17 sensing apps, 51.6% contrastive improvement, 0.008ms inference | `v0.6.0-esp32` |
|
||||
| [v0.5.5](https://github.com/ruvnet/RuView/releases/tag/v0.5.5-esp32) | SNN + MinCut (#348 fix) + CNN spectrogram + WiFlow + multi-freq mesh + graph transformer | `v0.5.5-esp32` |
|
||||
| [v0.5.4](https://github.com/ruvnet/RuView/releases/tag/v0.5.4-esp32) | Cognitum Seed integration ([ADR-069](docs/adr/ADR-069-cognitum-seed-csi-pipeline.md)), 8-dim feature vectors, RVF store, witness chain, security hardening | `v0.5.4-esp32` |
|
||||
| [v0.5.0](https://github.com/ruvnet/RuView/releases/tag/v0.5.0-esp32) | mmWave sensor fusion ([ADR-063](docs/adr/ADR-063-mmwave-sensor-fusion.md)), auto-detect MR60BHA2/LD2410, 48-byte fused vitals, all v0.4.3.1 fixes | `v0.5.0-esp32` |
|
||||
| [v0.4.3.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.3.1-esp32) | Fall detection fix ([#263](https://github.com/ruvnet/RuView/issues/263)), 4MB flash ([#265](https://github.com/ruvnet/RuView/issues/265)), watchdog fix ([#266](https://github.com/ruvnet/RuView/issues/266)) | `v0.4.3.1-esp32` |
|
||||
| [v0.4.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.1-esp32) | CSI build fix, compile guard, AMOLED display, edge intelligence ([ADR-057](docs/adr/ADR-057-firmware-csi-build-guard.md)) | `v0.4.1-esp32` |
|
||||
| [v0.2.0](https://github.com/ruvnet/RuView/releases/tag/v0.2.0-esp32) | Stable — raw CSI streaming, multi-node TDM, channel hopping | `v0.2.0-esp32` |
|
||||
| [v0.3.0-alpha](https://github.com/ruvnet/RuView/releases/tag/v0.3.0-alpha-esp32) | Alpha — adds on-device edge intelligence and WASM modules ([ADR-039](docs/adr/ADR-039-esp32-edge-intelligence.md), [ADR-040](docs/adr/ADR-040-wasm-programmable-sensing.md)) | `v0.3.0-alpha-esp32` |
|
||||
| [v0.2.0](https://github.com/ruvnet/RuView/releases/tag/v0.2.0-esp32) | Raw CSI streaming, multi-node TDM, channel hopping | `v0.2.0-esp32` |
|
||||
|
||||
```bash
|
||||
# 1. Flash the firmware to your ESP32-S3 (8MB flash — most boards)
|
||||
# 1. Flash the firmware to your ESP32-S3
|
||||
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
|
||||
write_flash --flash-mode dio --flash-size 8MB --flash-freq 80m \
|
||||
0x0 bootloader.bin 0x8000 partition-table.bin \
|
||||
0xf000 ota_data_initial.bin 0x20000 esp32-csi-node.bin
|
||||
|
||||
# 1b. For 4MB flash boards (e.g. ESP32-S3 SuperMini 4MB) — use the 4MB binaries:
|
||||
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
|
||||
write_flash --flash-mode dio --flash-size 4MB --flash-freq 80m \
|
||||
0x0 bootloader.bin 0x8000 partition-table-4mb.bin \
|
||||
0xF000 ota_data_initial.bin 0x20000 esp32-csi-node-4mb.bin
|
||||
write_flash --flash_mode dio --flash_size 8MB \
|
||||
0x0 bootloader.bin 0x8000 partition-table.bin 0x10000 esp32-csi-node.bin
|
||||
|
||||
# 2. Set WiFi credentials and server address (stored in flash, survives reboots)
|
||||
python firmware/esp32-csi-node/provision.py --port COM7 \
|
||||
@@ -1404,34 +1059,6 @@ python firmware/esp32-csi-node/provision.py --port COM8 \
|
||||
|
||||
Nodes can also hop across WiFi channels (1, 6, 11) to increase sensing bandwidth — configured via [ADR-029](docs/adr/ADR-029-ruvsense-multistatic-sensing-mode.md) channel hopping.
|
||||
|
||||
### Cognitum Seed integration (ADR-069)
|
||||
|
||||
Connect an ESP32 to a [Cognitum Seed](https://cognitum.one) ($131) for persistent vector storage, kNN search, cryptographic witness chain, and AI-accessible MCP proxy:
|
||||
|
||||
```
|
||||
ESP32-S3 ($9) ──UDP──> Host bridge ──HTTPS──> Cognitum Seed ($15)
|
||||
CSI capture seed_csi_bridge.py RVF vector store
|
||||
8-dim features @ 1 Hz kNN similarity search
|
||||
Vitals + presence Ed25519 witness chain
|
||||
114-tool MCP proxy
|
||||
```
|
||||
|
||||
```bash
|
||||
# 1. Provision ESP32 to send features to your laptop
|
||||
python firmware/esp32-csi-node/provision.py --port COM9 \
|
||||
--ssid "YourWiFi" --password "secret" --target-ip 192.168.1.20 --target-port 5006
|
||||
|
||||
# 2. Run the bridge (forwards to Seed via HTTPS)
|
||||
export SEED_TOKEN="your-pairing-token"
|
||||
python scripts/seed_csi_bridge.py \
|
||||
--seed-url https://169.254.42.1:8443 --token "$SEED_TOKEN" --validate
|
||||
|
||||
# 3. Check Seed stats
|
||||
python scripts/seed_csi_bridge.py --token "$SEED_TOKEN" --stats
|
||||
```
|
||||
|
||||
The 8-dim feature vector captures: presence, motion, breathing rate, heart rate, phase variance, person count, fall detection, and RSSI — all normalized to [0.0, 1.0]. See [ADR-069](docs/adr/ADR-069-cognitum-seed-csi-pipeline.md) for the full architecture.
|
||||
|
||||
### On-device intelligence (v0.3.0-alpha)
|
||||
|
||||
The alpha firmware can analyze signals locally and send compact results instead of raw data. This means the ESP32 works standalone — no server needed for basic sensing. Disabled by default for backward compatibility.
|
||||
@@ -1451,9 +1078,9 @@ python firmware/esp32-csi-node/provision.py --port COM7 \
|
||||
--ssid "YourWiFi" --password "secret" --target-ip 192.168.1.20 \
|
||||
--edge-tier 2
|
||||
|
||||
# Fine-tune detection thresholds (fall-thresh in milli-units: 15000 = 15.0 rad/s²)
|
||||
# Fine-tune detection thresholds
|
||||
python firmware/esp32-csi-node/provision.py --port COM7 \
|
||||
--edge-tier 2 --vital-int 500 --fall-thresh 15000 --subk-count 16
|
||||
--edge-tier 2 --vital-int 500 --fall-thresh 5000 --subk-count 16
|
||||
```
|
||||
|
||||
When Tier 2 is active, the node sends a 32-byte vitals packet once per second containing: presence, motion level, breathing BPM, heart rate BPM, confidence scores, fall alert flag, and occupancy count.
|
||||
@@ -1484,7 +1111,7 @@ See [firmware/esp32-csi-node/README.md](firmware/esp32-csi-node/README.md), [ADR
|
||||
| WASM Support | No | Yes |
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo build --release
|
||||
cargo test --workspace
|
||||
cargo bench --package wifi-densepose-signal
|
||||
@@ -1778,13 +1405,6 @@ The full RuVector ecosystem includes 90+ crates. See [github.com/ruvnet/ruvector
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><a id="ruv-neural"></a><strong>🧠 rUv Neural</strong> — Brain topology analysis ecosystem for neural decoding and medical sensing</summary>
|
||||
|
||||
[**rUv Neural**](v2/crates/ruv-neural/README.md) is a 12-crate Rust ecosystem that extends RuView's signal processing into brain network topology analysis. It transforms neural magnetic field measurements from quantum sensors (NV diamond magnetometers, optically pumped magnetometers) into dynamic connectivity graphs, using minimum cut algorithms to detect cognitive state transitions in real time. The ecosystem includes crates for signal processing (`ruv-neural-signal`), graph construction (`ruv-neural-graph`), HNSW-indexed pattern memory (`ruv-neural-memory`), graph embeddings (`ruv-neural-embed`), cognitive state decoding (`ruv-neural-decoder`), and ESP32/WASM edge targets. Medical and research applications include early neurological disease detection via topology signatures, brain-computer interfaces, clinical neurofeedback, and non-invasive biomedical sensing -- bridging RuView's RF sensing architecture with the emerging field of quantum biomedical diagnostics.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
<details>
|
||||
@@ -2043,82 +1663,6 @@ WebSocket: `ws://localhost:3001/ws/sensing` (real-time sensing + vital signs)
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>QEMU Firmware Testing (ADR-061) — 9-Layer Platform</strong></summary>
|
||||
|
||||
Test ESP32-S3 firmware without physical hardware using Espressif's QEMU fork. The platform provides 9 layers of testing capability:
|
||||
|
||||
| Layer | Capability | Script / Config |
|
||||
|-------|-----------|-----------------|
|
||||
| 1 | Mock CSI generator (10 physics-based scenarios) | `firmware/esp32-csi-node/main/mock_csi.c` |
|
||||
| 2 | Single-node QEMU runner + UART validation (16 checks) | `scripts/qemu-esp32s3-test.sh`, `scripts/validate_qemu_output.py` |
|
||||
| 3 | Multi-node TDM mesh simulation (TAP networking) | `scripts/qemu-mesh-test.sh`, `scripts/validate_mesh_test.py` |
|
||||
| 4 | GDB remote debugging (VS Code integration) | `.vscode/launch.json` |
|
||||
| 5 | Code coverage (gcov/lcov via apptrace) | `firmware/esp32-csi-node/sdkconfig.coverage` |
|
||||
| 6 | Fuzz testing (libFuzzer + ASAN/UBSAN) | `firmware/esp32-csi-node/test/fuzz_*.c` |
|
||||
| 7 | NVS provisioning matrix (14 configs) | `scripts/generate_nvs_matrix.py` |
|
||||
| 8 | Snapshot regression (sub-second VM restore) | `scripts/qemu-snapshot-test.sh` |
|
||||
| 9 | Chaos testing (fault injection + health monitoring) | `scripts/qemu-chaos-test.sh`, `scripts/inject_fault.py`, `scripts/check_health.py` |
|
||||
|
||||
```bash
|
||||
# Quick start: build + run + validate
|
||||
cd firmware/esp32-csi-node
|
||||
idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.qemu" build
|
||||
|
||||
# Single-node test (builds, merges flash, runs QEMU, validates output)
|
||||
bash scripts/qemu-esp32s3-test.sh
|
||||
|
||||
# Multi-node mesh test (3 QEMU instances with TDM)
|
||||
sudo bash scripts/qemu-mesh-test.sh 3
|
||||
|
||||
# Fuzz testing (60 seconds per target)
|
||||
cd firmware/esp32-csi-node/test && make all CC=clang && make run_serialize FUZZ_DURATION=60
|
||||
|
||||
# Chaos testing (fault injection resilience)
|
||||
bash scripts/qemu-chaos-test.sh --faults all --duration 120
|
||||
```
|
||||
|
||||
**10 test scenarios**: empty room, static person, walking, fall, multi-person, channel sweep, MAC filter, ring overflow, boundary RSSI, zero-length frames.
|
||||
|
||||
**14 NVS configs**: default, WiFi-only, full ADR-060, edge tiers 0/1/2, TDM mesh, WASM signed/unsigned, 5GHz, boundary max/min, power-save, empty-strings.
|
||||
|
||||
**CI**: GitHub Actions workflow runs 7 NVS matrix configs, 3 fuzz targets, and NVS binary validation on every push to `firmware/`.
|
||||
|
||||
See [ADR-061](docs/adr/ADR-061-qemu-esp32s3-firmware-testing.md) for the full architecture.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>QEMU Swarm Configurator (ADR-062)</strong></summary>
|
||||
|
||||
Test multiple ESP32-S3 nodes simultaneously using a YAML-driven orchestrator. Define node roles, network topologies, and validation assertions in a config file.
|
||||
|
||||
```bash
|
||||
# Quick smoke test (2 nodes, 15 seconds)
|
||||
python3 scripts/qemu_swarm.py --preset smoke
|
||||
|
||||
# Standard 3-node test (coordinator + 2 sensors)
|
||||
python3 scripts/qemu_swarm.py --preset standard
|
||||
|
||||
# See all presets
|
||||
python3 scripts/qemu_swarm.py --list-presets
|
||||
|
||||
# Preview without running
|
||||
python3 scripts/qemu_swarm.py --preset standard --dry-run
|
||||
```
|
||||
|
||||
**Topologies**: star (sensors → coordinator), mesh (fully connected), line (relay chain), ring (circular).
|
||||
|
||||
**Node roles**: sensor (generates CSI), coordinator (aggregates), gateway (bridges to host).
|
||||
|
||||
**7 presets**: smoke, standard, ci-matrix, large-mesh, line-relay, ring-fault, heterogeneous.
|
||||
|
||||
**9 swarm assertions**: boot check, crash detection, TDM collision, frame production, coordinator reception, fall detection, frame rate, boot time, heap health.
|
||||
|
||||
See [ADR-062](docs/adr/ADR-062-qemu-swarm-configurator.md) and the [User Guide](docs/user-guide.md#testing-firmware-without-hardware-qemu) for step-by-step instructions.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Python Legacy CLI</strong> — v1 API server commands</summary>
|
||||
|
||||
@@ -2138,9 +1682,7 @@ wifi-densepose tasks list # List background tasks
|
||||
<details>
|
||||
<summary><strong>Documentation Links</strong></summary>
|
||||
|
||||
- [User Guide](docs/user-guide.md) — installation, first run, API, hardware setup, QEMU testing
|
||||
- [WiFi-Mat User Guide](docs/wifi-mat-user-guide.md) | [Domain Model](docs/ddd/wifi-mat-domain-model.md)
|
||||
- [ADR-061](docs/adr/ADR-061-qemu-esp32s3-firmware-testing.md) QEMU platform | [ADR-062](docs/adr/ADR-062-qemu-swarm-configurator.md) Swarm configurator
|
||||
- [ADR-021](docs/adr/ADR-021-vital-sign-detection-rvdna-pipeline.md) | [ADR-022](docs/adr/ADR-022-windows-wifi-enhanced-fidelity-ruvector.md) | [ADR-023](docs/adr/ADR-023-trained-densepose-model-ruvector-pipeline.md)
|
||||
|
||||
</details>
|
||||
@@ -2154,7 +1696,7 @@ wifi-densepose tasks list # List background tasks
|
||||
|
||||
```bash
|
||||
# Rust tests (primary — 542+ tests)
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo test --workspace
|
||||
|
||||
# Sensing server tests (229 tests)
|
||||
@@ -2164,7 +1706,7 @@ cargo test -p wifi-densepose-sensing-server
|
||||
./target/release/sensing-server --benchmark
|
||||
|
||||
# Python tests
|
||||
python -m pytest archive/v1/tests/ -v
|
||||
python -m pytest v1/tests/ -v
|
||||
|
||||
# Pipeline verification (no hardware needed)
|
||||
./verify
|
||||
@@ -2258,7 +1800,7 @@ git clone https://github.com/ruvnet/RuView.git
|
||||
cd RuView
|
||||
|
||||
# Rust development
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo build --release
|
||||
cargo test --workspace
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
# Archive
|
||||
|
||||
Frozen, no-longer-active components of RuView preserved for historical
|
||||
reference, reproducibility, and load-bearing legacy paths the active
|
||||
codebase still depends on.
|
||||
|
||||
## What lives here
|
||||
|
||||
| Path | What it is | Why it's archived | Still load-bearing? |
|
||||
|------|------------|-------------------|---------------------|
|
||||
| `v1/` | Original Python implementation of RuView (CSI processing, hardware adapters, services, FastAPI) | Superseded by the Rust workspace at `v2/`; ~810× slower in benchmarks. Kept rather than deleted because the deterministic proof bundle (`v1/data/proof/`) is part of the pre-merge witness verification process per ADR-011 / ADR-028. | **Yes — for the proof bundle only.** Active code lives in `v2/`. |
|
||||
|
||||
## What "archived" means
|
||||
|
||||
- **Do not add new features here.** New work goes in `v2/`.
|
||||
- **Do not refactor or modernize the archived code beyond what is
|
||||
strictly necessary** to keep the load-bearing paths working. The
|
||||
Python proof bundle is intentionally frozen so that its SHA-256
|
||||
reproducibility holds across releases (per ADR-028's witness
|
||||
verification requirement).
|
||||
- **Bug fixes inside archived code are allowed** when the bug affects a
|
||||
still-load-bearing path (currently: only the Python proof). All
|
||||
other "bugs" in archived code are out-of-scope — they are part of
|
||||
the historical record and any fix would unnecessarily churn the
|
||||
witness hashes.
|
||||
- **CI continues to verify the load-bearing paths.**
|
||||
`.github/workflows/verify-pipeline.yml` runs the Python proof on
|
||||
every push and PR; if you change anything inside `archive/v1/src/`
|
||||
or `archive/v1/data/proof/`, expect the determinism check to flag
|
||||
it.
|
||||
|
||||
## Quick reference for the load-bearing paths
|
||||
|
||||
```bash
|
||||
# Run the deterministic Python proof (must print VERDICT: PASS)
|
||||
python archive/v1/data/proof/verify.py
|
||||
|
||||
# Regenerate the expected hash (only if numpy/scipy version legitimately changed)
|
||||
python archive/v1/data/proof/verify.py --generate-hash
|
||||
|
||||
# Run the full Python test suite (legacy, still maintained)
|
||||
cd archive/v1&& python -m pytest tests/ -x -q
|
||||
```
|
||||
|
||||
## Why we keep `v1/` rather than delete it
|
||||
|
||||
1. **Trust kill-switch.** The proof at `v1/data/proof/verify.py` feeds
|
||||
a known reference signal through the full pipeline and hashes the
|
||||
output. If the active code's behavior drifts, the hash changes and
|
||||
CI fails. This is what stops accidental regression in the science
|
||||
layer of the codebase.
|
||||
|
||||
2. **Witness verification.** ADR-028's witness-bundle process bundles
|
||||
the proof, the rust workspace test results, and firmware hashes
|
||||
into a tarball recipients can self-verify. Removing v1 would break
|
||||
that chain.
|
||||
|
||||
3. **Historical reference.** ADR-011 documents the "no mocks in
|
||||
production code" decision; the original violations and their fixes
|
||||
live in this Python codebase. The ADRs reference these paths.
|
||||
|
||||
If the time comes to retire the proof bundle (e.g., a Rust port of
|
||||
the proof exists and the Python version is no longer canonical), the
|
||||
right move is a single follow-up that simultaneously: ports the
|
||||
witness-bundle process, updates `verify-pipeline.yml`, and either
|
||||
deletes `archive/v1/` or moves it to a separate read-only repository.
|
||||
That decision belongs in its own ADR.
|
||||
|
||||
## See also
|
||||
|
||||
- `docs/adr/ADR-011-python-proof-of-reality-mock-elimination.md`
|
||||
- `docs/adr/ADR-028-esp32-capability-audit.md`
|
||||
- `archive/v1/data/proof/README.md` (if present)
|
||||
- `docs/WITNESS-LOG-028.md`
|
||||
@@ -1,7 +0,0 @@
|
||||
"""
|
||||
API routers package
|
||||
"""
|
||||
|
||||
from . import pose, stream, health, auth
|
||||
|
||||
__all__ = ["pose", "stream", "health", "auth"]
|
||||
@@ -1,32 +0,0 @@
|
||||
"""
|
||||
Authentication router for WiFi-DensePose API.
|
||||
Provides logout (token blacklisting) endpoint.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException, status
|
||||
|
||||
from src.api.middleware.auth import token_blacklist
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(request: Request):
|
||||
"""Logout by blacklisting the current Bearer token."""
|
||||
auth_header = request.headers.get("authorization")
|
||||
if not auth_header or not auth_header.startswith("Bearer "):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Missing or invalid Authorization header",
|
||||
)
|
||||
|
||||
token = auth_header.split(" ", 1)[1]
|
||||
token_blacklist.add_token(token)
|
||||
logger.info("Token blacklisted via /auth/logout")
|
||||
|
||||
return {"success": True, "message": "Token revoked"}
|
||||
@@ -1,135 +0,0 @@
|
||||
"""Frame budget benchmark for CSI processing pipeline.
|
||||
|
||||
Verifies that per-frame CSI processing stays within the 50 ms budget
|
||||
required for real-time sensing at 20 FPS.
|
||||
"""
|
||||
|
||||
import time
|
||||
import statistics
|
||||
import pytest
|
||||
import numpy as np
|
||||
|
||||
from src.core.csi_processor import CSIProcessor
|
||||
|
||||
|
||||
def _make_config():
|
||||
return {
|
||||
"sampling_rate": 1000,
|
||||
"window_size": 256,
|
||||
"overlap": 0.5,
|
||||
"noise_threshold": -60,
|
||||
"human_detection_threshold": 0.8,
|
||||
"smoothing_factor": 0.9,
|
||||
"max_history_size": 500,
|
||||
"num_subcarriers": 256,
|
||||
"num_antennas": 3,
|
||||
"doppler_window": 64,
|
||||
}
|
||||
|
||||
|
||||
def _make_csi_data(n_subcarriers=256, n_antennas=3, seed=None):
|
||||
"""Generate a synthetic CSI frame with complex-valued subcarriers."""
|
||||
rng = np.random.default_rng(seed)
|
||||
from unittest.mock import MagicMock
|
||||
csi = MagicMock()
|
||||
csi.amplitude = rng.random((n_antennas, n_subcarriers)).astype(np.float64) * 20.0
|
||||
csi.phase = (rng.random((n_antennas, n_subcarriers)).astype(np.float64) - 0.5) * np.pi * 2
|
||||
csi.frequency = 5.0e9
|
||||
csi.bandwidth = 80e6
|
||||
csi.num_subcarriers = n_subcarriers
|
||||
csi.num_antennas = n_antennas
|
||||
csi.snr = 25.0
|
||||
csi.timestamp = time.time()
|
||||
csi.metadata = {}
|
||||
return csi
|
||||
|
||||
|
||||
class TestSingleFrameBudget:
|
||||
"""Single-frame processing must complete in < 50 ms."""
|
||||
|
||||
def test_single_frame_under_50ms(self):
|
||||
proc = CSIProcessor(config=_make_config())
|
||||
frame = _make_csi_data(seed=42)
|
||||
|
||||
# Warm up
|
||||
proc.preprocess_csi_data(frame)
|
||||
|
||||
start = time.perf_counter()
|
||||
proc.preprocess_csi_data(frame)
|
||||
features = proc.extract_features(frame)
|
||||
if features:
|
||||
proc.detect_human_presence(features)
|
||||
elapsed_ms = (time.perf_counter() - start) * 1000
|
||||
|
||||
assert elapsed_ms < 50, f"Single frame took {elapsed_ms:.1f} ms (budget: 50 ms)"
|
||||
|
||||
|
||||
class TestSustainedFrameBudget:
|
||||
"""Sustained 100-frame processing p95 must be < 50 ms per frame."""
|
||||
|
||||
def test_sustained_100_frames_p95(self):
|
||||
proc = CSIProcessor(config=_make_config())
|
||||
rng = np.random.default_rng(123)
|
||||
n_frames = 100
|
||||
latencies = []
|
||||
|
||||
for i in range(n_frames):
|
||||
frame = _make_csi_data(seed=i)
|
||||
start = time.perf_counter()
|
||||
preprocessed = proc.preprocess_csi_data(frame)
|
||||
features = proc.extract_features(preprocessed)
|
||||
if features:
|
||||
proc.detect_human_presence(features)
|
||||
proc.add_to_history(frame)
|
||||
elapsed_ms = (time.perf_counter() - start) * 1000
|
||||
latencies.append(elapsed_ms)
|
||||
|
||||
p50 = statistics.median(latencies)
|
||||
p95 = sorted(latencies)[int(0.95 * len(latencies))]
|
||||
p99 = sorted(latencies)[int(0.99 * len(latencies))]
|
||||
|
||||
print(f"\n--- Sustained {n_frames}-frame benchmark ---")
|
||||
print(f" p50: {p50:.2f} ms")
|
||||
print(f" p95: {p95:.2f} ms")
|
||||
print(f" p99: {p99:.2f} ms")
|
||||
print(f" min: {min(latencies):.2f} ms")
|
||||
print(f" max: {max(latencies):.2f} ms")
|
||||
|
||||
assert p95 < 50, f"p95 latency {p95:.1f} ms exceeds 50 ms budget"
|
||||
|
||||
|
||||
class TestPipelineWithDoppler:
|
||||
"""Full pipeline including Doppler estimation must stay within budget."""
|
||||
|
||||
def test_doppler_pipeline(self):
|
||||
proc = CSIProcessor(config=_make_config())
|
||||
n_frames = 100
|
||||
latencies = []
|
||||
|
||||
# Fill history first
|
||||
for i in range(20):
|
||||
frame = _make_csi_data(seed=i + 1000)
|
||||
proc.add_to_history(frame)
|
||||
|
||||
for i in range(n_frames):
|
||||
frame = _make_csi_data(seed=i + 2000)
|
||||
start = time.perf_counter()
|
||||
preprocessed = proc.preprocess_csi_data(frame)
|
||||
features = proc.extract_features(preprocessed)
|
||||
if features:
|
||||
proc.detect_human_presence(features)
|
||||
proc.add_to_history(frame)
|
||||
elapsed_ms = (time.perf_counter() - start) * 1000
|
||||
latencies.append(elapsed_ms)
|
||||
|
||||
p50 = statistics.median(latencies)
|
||||
p95 = sorted(latencies)[int(0.95 * len(latencies))]
|
||||
p99 = sorted(latencies)[int(0.99 * len(latencies))]
|
||||
|
||||
print(f"\n--- Doppler pipeline benchmark ({n_frames} frames, 20 warmup) ---")
|
||||
print(f" p50: {p50:.2f} ms")
|
||||
print(f" p95: {p95:.2f} ms")
|
||||
print(f" p99: {p99:.2f} ms")
|
||||
|
||||
# Doppler adds overhead but should still be within budget
|
||||
assert p95 < 50, f"Doppler pipeline p95 {p95:.1f} ms exceeds 50 ms budget"
|
||||
@@ -1,56 +0,0 @@
|
||||
"""Shared fixtures for unit tests."""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
|
||||
# Set SECRET_KEY before any settings import
|
||||
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-unit-tests-only")
|
||||
os.environ.setdefault("JWT_SECRET_KEY", "test-secret-key-for-unit-tests-only")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_settings():
|
||||
"""Create a mock Settings object."""
|
||||
settings = MagicMock()
|
||||
settings.secret_key = "test-secret-key-for-unit-tests-only"
|
||||
settings.jwt_algorithm = "HS256"
|
||||
settings.jwt_expire_hours = 24
|
||||
settings.app_name = "test-app"
|
||||
settings.version = "0.1.0"
|
||||
settings.is_production = False
|
||||
settings.enable_rate_limiting = False
|
||||
settings.enable_authentication = False
|
||||
settings.rate_limit_requests = 100
|
||||
settings.rate_limit_window = 60
|
||||
settings.rate_limit_authenticated_requests = 1000
|
||||
settings.allowed_hosts = ["*"]
|
||||
settings.csi_buffer_size = 100
|
||||
settings.stream_buffer_size = 100
|
||||
settings.mock_hardware = True
|
||||
settings.mock_pose_data = True
|
||||
settings.enable_real_time_processing = False
|
||||
settings.trusted_proxies = ["127.0.0.1"]
|
||||
return settings
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_domain_config():
|
||||
"""Create a mock DomainConfig object."""
|
||||
config = MagicMock()
|
||||
config.pose_estimation = MagicMock()
|
||||
config.streaming = MagicMock()
|
||||
config.hardware = MagicMock()
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_redis():
|
||||
"""Provide a mock Redis client."""
|
||||
with patch("redis.Redis") as mock:
|
||||
client = MagicMock()
|
||||
client.ping.return_value = True
|
||||
client.get.return_value = None
|
||||
client.set.return_value = True
|
||||
mock.return_value = client
|
||||
yield client
|
||||
@@ -1,137 +0,0 @@
|
||||
"""Tests for AuthMiddleware and TokenManager."""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
class TestTokenManager:
|
||||
def test_create_token(self, mock_settings):
|
||||
from src.middleware.auth import TokenManager
|
||||
tm = TokenManager(mock_settings)
|
||||
token = tm.create_access_token({"sub": "user1"})
|
||||
assert isinstance(token, str)
|
||||
assert len(token) > 0
|
||||
|
||||
def test_verify_valid_token(self, mock_settings):
|
||||
from src.middleware.auth import TokenManager
|
||||
tm = TokenManager(mock_settings)
|
||||
token = tm.create_access_token({"sub": "user1", "role": "admin"})
|
||||
payload = tm.verify_token(token)
|
||||
assert payload["sub"] == "user1"
|
||||
assert payload["role"] == "admin"
|
||||
|
||||
def test_verify_invalid_token(self, mock_settings):
|
||||
from src.middleware.auth import TokenManager, AuthenticationError
|
||||
tm = TokenManager(mock_settings)
|
||||
with pytest.raises(AuthenticationError):
|
||||
tm.verify_token("invalid.token.here")
|
||||
|
||||
def test_decode_claims(self, mock_settings):
|
||||
from src.middleware.auth import TokenManager
|
||||
tm = TokenManager(mock_settings)
|
||||
token = tm.create_access_token({"sub": "user1"})
|
||||
claims = tm.decode_token_claims(token)
|
||||
assert claims is not None
|
||||
assert claims["sub"] == "user1"
|
||||
|
||||
def test_decode_claims_invalid(self, mock_settings):
|
||||
from src.middleware.auth import TokenManager
|
||||
tm = TokenManager(mock_settings)
|
||||
claims = tm.decode_token_claims("bad-token")
|
||||
assert claims is None
|
||||
|
||||
def test_token_has_expiry(self, mock_settings):
|
||||
from src.middleware.auth import TokenManager
|
||||
tm = TokenManager(mock_settings)
|
||||
token = tm.create_access_token({"sub": "user1"})
|
||||
payload = tm.verify_token(token)
|
||||
assert "exp" in payload
|
||||
assert "iat" in payload
|
||||
|
||||
|
||||
class TestUserManager:
|
||||
def test_create_user(self):
|
||||
from src.middleware.auth import UserManager
|
||||
um = UserManager()
|
||||
assert um.get_user("nonexistent") is None
|
||||
|
||||
def test_hash_password(self):
|
||||
from src.middleware.auth import UserManager
|
||||
hashed = UserManager.hash_password("secret123")
|
||||
assert hashed != "secret123"
|
||||
assert len(hashed) > 20
|
||||
|
||||
def test_verify_password(self):
|
||||
from src.middleware.auth import UserManager
|
||||
hashed = UserManager.hash_password("secret123")
|
||||
assert UserManager.verify_password("secret123", hashed) is True
|
||||
assert UserManager.verify_password("wrong", hashed) is False
|
||||
|
||||
|
||||
class TestTokenBlacklist:
|
||||
def test_add_and_check(self):
|
||||
from src.api.middleware.auth import TokenBlacklist
|
||||
bl = TokenBlacklist()
|
||||
bl.add_token("tok123")
|
||||
assert bl.is_blacklisted("tok123") is True
|
||||
assert bl.is_blacklisted("tok456") is False
|
||||
|
||||
def test_blacklisted_token_rejected(self, mock_settings):
|
||||
from src.middleware.auth import TokenManager, AuthenticationError
|
||||
from src.api.middleware.auth import token_blacklist
|
||||
|
||||
tm = TokenManager(mock_settings)
|
||||
token = tm.create_access_token({"sub": "user1"})
|
||||
# Token should be valid
|
||||
tm.verify_token(token)
|
||||
# Blacklist it
|
||||
token_blacklist.add_token(token)
|
||||
with pytest.raises(AuthenticationError, match="revoked"):
|
||||
tm.verify_token(token)
|
||||
# Cleanup
|
||||
token_blacklist._blacklisted_tokens.discard(token)
|
||||
|
||||
|
||||
class TestAuthMiddleware:
|
||||
def test_public_paths(self, mock_settings):
|
||||
with patch("src.api.middleware.auth.get_settings", return_value=mock_settings):
|
||||
from src.api.middleware.auth import AuthMiddleware
|
||||
app = MagicMock()
|
||||
mw = AuthMiddleware(app)
|
||||
assert mw._is_public_path("/health") is True
|
||||
assert mw._is_public_path("/docs") is True
|
||||
assert mw._is_public_path("/api/v1/pose/analyze") is False
|
||||
|
||||
def test_protected_paths(self, mock_settings):
|
||||
with patch("src.api.middleware.auth.get_settings", return_value=mock_settings):
|
||||
from src.api.middleware.auth import AuthMiddleware
|
||||
app = MagicMock()
|
||||
mw = AuthMiddleware(app)
|
||||
assert mw._is_protected_path("/api/v1/pose/analyze") is True
|
||||
assert mw._is_protected_path("/health") is False
|
||||
|
||||
def test_extract_token_from_header(self, mock_settings):
|
||||
with patch("src.api.middleware.auth.get_settings", return_value=mock_settings):
|
||||
from src.api.middleware.auth import AuthMiddleware
|
||||
app = MagicMock()
|
||||
mw = AuthMiddleware(app)
|
||||
request = MagicMock()
|
||||
request.headers = {"authorization": "Bearer mytoken123"}
|
||||
request.query_params = {}
|
||||
request.cookies = {}
|
||||
token = mw._extract_token(request)
|
||||
assert token == "mytoken123"
|
||||
|
||||
def test_extract_token_missing(self, mock_settings):
|
||||
with patch("src.api.middleware.auth.get_settings", return_value=mock_settings):
|
||||
from src.api.middleware.auth import AuthMiddleware
|
||||
app = MagicMock()
|
||||
mw = AuthMiddleware(app)
|
||||
request = MagicMock()
|
||||
request.headers = {}
|
||||
request.query_params = {}
|
||||
request.cookies = {}
|
||||
token = mw._extract_token(request)
|
||||
assert token is None
|
||||
@@ -1,78 +0,0 @@
|
||||
"""Tests for error handling in the API layer."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestExceptionHandlers:
|
||||
"""Test the exception handlers registered on the FastAPI app."""
|
||||
|
||||
def _get_app(self):
|
||||
"""Import app lazily to avoid side effects."""
|
||||
with patch("src.api.main.get_settings") as mock_gs, \
|
||||
patch("src.api.main.get_domain_config") as mock_gdc, \
|
||||
patch("src.api.main.get_pose_service") as mock_ps, \
|
||||
patch("src.api.main.get_stream_service") as mock_ss, \
|
||||
patch("src.api.main.get_hardware_service") as mock_hs, \
|
||||
patch("src.api.main.connection_manager") as mock_cm, \
|
||||
patch("src.api.main.PoseStreamHandler") as mock_psh:
|
||||
mock_gs.return_value = MagicMock(
|
||||
app_name="test", version="0.1", environment="test",
|
||||
is_production=False, enable_rate_limiting=False,
|
||||
enable_authentication=False, docs_url="/docs",
|
||||
redoc_url="/redoc", openapi_url="/openapi.json",
|
||||
api_prefix="/api/v1",
|
||||
)
|
||||
mock_gs.return_value.get_logging_config.return_value = {
|
||||
"version": 1, "disable_existing_loggers": False,
|
||||
"handlers": {}, "loggers": {},
|
||||
}
|
||||
mock_gs.return_value.get_cors_config.return_value = {
|
||||
"allow_origins": ["*"], "allow_methods": ["*"],
|
||||
"allow_headers": ["*"],
|
||||
}
|
||||
# Re-import to pick up patches
|
||||
import importlib
|
||||
import src.api.main as m
|
||||
importlib.reload(m)
|
||||
return m.app
|
||||
|
||||
|
||||
class TestErrorResponseModel:
|
||||
def test_error_json_structure(self):
|
||||
"""Verify error JSON has code, message, type fields."""
|
||||
error = {
|
||||
"error": {
|
||||
"code": 404,
|
||||
"message": "Not found",
|
||||
"type": "http_error"
|
||||
}
|
||||
}
|
||||
assert error["error"]["code"] == 404
|
||||
assert "message" in error["error"]
|
||||
assert "type" in error["error"]
|
||||
|
||||
def test_validation_error_structure(self):
|
||||
error = {
|
||||
"error": {
|
||||
"code": 422,
|
||||
"message": "Validation error",
|
||||
"type": "validation_error",
|
||||
"details": []
|
||||
}
|
||||
}
|
||||
assert error["error"]["type"] == "validation_error"
|
||||
assert isinstance(error["error"]["details"], list)
|
||||
|
||||
def test_internal_error_masks_details(self):
|
||||
"""In production, internal errors should not leak stack traces."""
|
||||
error = {
|
||||
"error": {
|
||||
"code": 500,
|
||||
"message": "Internal server error",
|
||||
"type": "internal_error"
|
||||
}
|
||||
}
|
||||
assert "traceback" not in str(error)
|
||||
assert error["error"]["message"] == "Internal server error"
|
||||
@@ -1,65 +0,0 @@
|
||||
"""Tests for HardwareService."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
|
||||
|
||||
class TestHardwareServiceInit:
|
||||
def test_init(self, mock_settings, mock_domain_config):
|
||||
mock_settings.mock_hardware = True
|
||||
with patch("src.services.hardware_service.RouterInterface"):
|
||||
from src.services.hardware_service import HardwareService
|
||||
svc = HardwareService(mock_settings, mock_domain_config)
|
||||
assert svc.is_running is False
|
||||
assert svc.stats["total_samples"] == 0
|
||||
assert svc.stats["connected_routers"] == 0
|
||||
|
||||
def test_stats_defaults(self, mock_settings, mock_domain_config):
|
||||
mock_settings.mock_hardware = True
|
||||
with patch("src.services.hardware_service.RouterInterface"):
|
||||
from src.services.hardware_service import HardwareService
|
||||
svc = HardwareService(mock_settings, mock_domain_config)
|
||||
assert svc.stats["successful_samples"] == 0
|
||||
assert svc.stats["failed_samples"] == 0
|
||||
assert svc.stats["last_sample_time"] is None
|
||||
|
||||
|
||||
class TestHardwareServiceLifecycle:
|
||||
@pytest.mark.asyncio
|
||||
async def test_start(self, mock_settings, mock_domain_config):
|
||||
mock_settings.mock_hardware = True
|
||||
with patch("src.services.hardware_service.RouterInterface"):
|
||||
from src.services.hardware_service import HardwareService
|
||||
svc = HardwareService(mock_settings, mock_domain_config)
|
||||
svc._initialize_routers = AsyncMock()
|
||||
svc._monitoring_loop = AsyncMock()
|
||||
await svc.start()
|
||||
assert svc.is_running is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_double_start_idempotent(self, mock_settings, mock_domain_config):
|
||||
mock_settings.mock_hardware = True
|
||||
with patch("src.services.hardware_service.RouterInterface"):
|
||||
from src.services.hardware_service import HardwareService
|
||||
svc = HardwareService(mock_settings, mock_domain_config)
|
||||
svc._initialize_routers = AsyncMock()
|
||||
svc._monitoring_loop = AsyncMock()
|
||||
await svc.start()
|
||||
await svc.start() # idempotent
|
||||
assert svc.is_running is True
|
||||
|
||||
|
||||
class TestHardwareServiceRouter:
|
||||
def test_no_routers_on_init(self, mock_settings, mock_domain_config):
|
||||
mock_settings.mock_hardware = True
|
||||
with patch("src.services.hardware_service.RouterInterface"):
|
||||
from src.services.hardware_service import HardwareService
|
||||
svc = HardwareService(mock_settings, mock_domain_config)
|
||||
assert len(svc.router_interfaces) == 0
|
||||
|
||||
def test_max_recent_samples(self, mock_settings, mock_domain_config):
|
||||
mock_settings.mock_hardware = True
|
||||
with patch("src.services.hardware_service.RouterInterface"):
|
||||
from src.services.hardware_service import HardwareService
|
||||
svc = HardwareService(mock_settings, mock_domain_config)
|
||||
assert svc.max_recent_samples == 1000
|
||||
@@ -1,67 +0,0 @@
|
||||
"""Tests for HealthCheckService."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
class TestHealthCheckServiceInit:
|
||||
def test_init(self, mock_settings):
|
||||
from src.services.health_check import HealthCheckService
|
||||
svc = HealthCheckService(mock_settings)
|
||||
assert svc._initialized is False
|
||||
assert svc._running is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_initialize(self, mock_settings):
|
||||
from src.services.health_check import HealthCheckService
|
||||
svc = HealthCheckService(mock_settings)
|
||||
await svc.initialize()
|
||||
assert svc._initialized is True
|
||||
assert "api" in svc._services
|
||||
assert "database" in svc._services
|
||||
assert "hardware" in svc._services
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_double_initialize(self, mock_settings):
|
||||
from src.services.health_check import HealthCheckService
|
||||
svc = HealthCheckService(mock_settings)
|
||||
await svc.initialize()
|
||||
await svc.initialize() # idempotent
|
||||
assert svc._initialized is True
|
||||
|
||||
|
||||
class TestHealthCheckAggregation:
|
||||
@pytest.mark.asyncio
|
||||
async def test_services_registered(self, mock_settings):
|
||||
from src.services.health_check import HealthCheckService, HealthStatus
|
||||
svc = HealthCheckService(mock_settings)
|
||||
await svc.initialize()
|
||||
assert len(svc._services) == 6
|
||||
for name, sh in svc._services.items():
|
||||
assert sh.status == HealthStatus.UNKNOWN
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_service_names(self, mock_settings):
|
||||
from src.services.health_check import HealthCheckService
|
||||
svc = HealthCheckService(mock_settings)
|
||||
await svc.initialize()
|
||||
expected = {"api", "database", "redis", "hardware", "pose", "stream"}
|
||||
assert set(svc._services.keys()) == expected
|
||||
|
||||
|
||||
class TestHealthStatus:
|
||||
def test_enum_values(self):
|
||||
from src.services.health_check import HealthStatus
|
||||
assert HealthStatus.HEALTHY.value == "healthy"
|
||||
assert HealthStatus.DEGRADED.value == "degraded"
|
||||
assert HealthStatus.UNHEALTHY.value == "unhealthy"
|
||||
assert HealthStatus.UNKNOWN.value == "unknown"
|
||||
|
||||
|
||||
class TestHealthCheck:
|
||||
def test_health_check_dataclass(self):
|
||||
from src.services.health_check import HealthCheck, HealthStatus
|
||||
hc = HealthCheck(name="test", status=HealthStatus.HEALTHY, message="ok")
|
||||
assert hc.name == "test"
|
||||
assert hc.status == HealthStatus.HEALTHY
|
||||
assert hc.duration_ms == 0.0
|
||||
@@ -1,70 +0,0 @@
|
||||
"""Tests for MetricsService."""
|
||||
|
||||
import pytest
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
class TestMetricSeries:
|
||||
def test_add_point(self):
|
||||
from src.services.metrics import MetricSeries
|
||||
ms = MetricSeries(name="test", description="desc", unit="ms")
|
||||
ms.add_point(42.0)
|
||||
assert len(ms.points) == 1
|
||||
assert ms.points[0].value == 42.0
|
||||
|
||||
def test_get_latest(self):
|
||||
from src.services.metrics import MetricSeries
|
||||
ms = MetricSeries(name="test", description="desc", unit="ms")
|
||||
ms.add_point(1.0)
|
||||
ms.add_point(2.0)
|
||||
latest = ms.get_latest()
|
||||
assert latest is not None
|
||||
assert latest.value == 2.0
|
||||
|
||||
def test_get_latest_empty(self):
|
||||
from src.services.metrics import MetricSeries
|
||||
ms = MetricSeries(name="test", description="desc", unit="ms")
|
||||
assert ms.get_latest() is None
|
||||
|
||||
def test_get_average(self):
|
||||
from src.services.metrics import MetricSeries
|
||||
ms = MetricSeries(name="test", description="desc", unit="ms")
|
||||
for v in [10.0, 20.0, 30.0]:
|
||||
ms.add_point(v)
|
||||
avg = ms.get_average(timedelta(minutes=5))
|
||||
assert avg == pytest.approx(20.0)
|
||||
|
||||
def test_get_average_empty(self):
|
||||
from src.services.metrics import MetricSeries
|
||||
ms = MetricSeries(name="test", description="desc", unit="ms")
|
||||
assert ms.get_average(timedelta(minutes=5)) is None
|
||||
|
||||
def test_get_max(self):
|
||||
from src.services.metrics import MetricSeries
|
||||
ms = MetricSeries(name="test", description="desc", unit="ms")
|
||||
for v in [10.0, 50.0, 30.0]:
|
||||
ms.add_point(v)
|
||||
mx = ms.get_max(timedelta(minutes=5))
|
||||
assert mx == 50.0
|
||||
|
||||
def test_labels(self):
|
||||
from src.services.metrics import MetricSeries
|
||||
ms = MetricSeries(name="test", description="desc", unit="ms")
|
||||
ms.add_point(1.0, {"region": "us-east"})
|
||||
assert ms.points[0].labels["region"] == "us-east"
|
||||
|
||||
def test_maxlen(self):
|
||||
from src.services.metrics import MetricSeries
|
||||
ms = MetricSeries(name="test", description="desc", unit="ms")
|
||||
for i in range(1100):
|
||||
ms.add_point(float(i))
|
||||
assert len(ms.points) == 1000
|
||||
|
||||
|
||||
class TestMetricsService:
|
||||
def test_init(self, mock_settings):
|
||||
with patch("src.services.metrics.psutil"):
|
||||
from src.services.metrics import MetricsService
|
||||
svc = MetricsService(mock_settings)
|
||||
assert svc._metrics is not None
|
||||
@@ -1,73 +0,0 @@
|
||||
"""Tests for PoseService."""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class TestPoseServiceInit:
|
||||
def test_init_sets_defaults(self, mock_settings, mock_domain_config):
|
||||
with patch.dict("sys.modules", {
|
||||
"torch": MagicMock(),
|
||||
"src.models.densepose_head": MagicMock(),
|
||||
"src.models.modality_translation": MagicMock(),
|
||||
}):
|
||||
from src.services.pose_service import PoseService
|
||||
svc = PoseService(mock_settings, mock_domain_config)
|
||||
assert svc.is_initialized is False
|
||||
assert svc.is_running is False
|
||||
assert svc.stats["total_processed"] == 0
|
||||
|
||||
def test_stats_are_zero_on_init(self, mock_settings, mock_domain_config):
|
||||
with patch.dict("sys.modules", {
|
||||
"torch": MagicMock(),
|
||||
"src.models.densepose_head": MagicMock(),
|
||||
"src.models.modality_translation": MagicMock(),
|
||||
}):
|
||||
from src.services.pose_service import PoseService
|
||||
svc = PoseService(mock_settings, mock_domain_config)
|
||||
assert svc.stats["successful_detections"] == 0
|
||||
assert svc.stats["failed_detections"] == 0
|
||||
assert svc.stats["average_confidence"] == 0.0
|
||||
|
||||
|
||||
class TestPoseServiceLifecycle:
|
||||
@pytest.mark.asyncio
|
||||
async def test_initialize_sets_flag(self, mock_settings, mock_domain_config):
|
||||
with patch.dict("sys.modules", {
|
||||
"torch": MagicMock(),
|
||||
"src.models.densepose_head": MagicMock(),
|
||||
"src.models.modality_translation": MagicMock(),
|
||||
}):
|
||||
from src.services.pose_service import PoseService
|
||||
svc = PoseService(mock_settings, mock_domain_config)
|
||||
await svc.initialize()
|
||||
assert svc.is_initialized is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_stop(self, mock_settings, mock_domain_config):
|
||||
with patch.dict("sys.modules", {
|
||||
"torch": MagicMock(),
|
||||
"src.models.densepose_head": MagicMock(),
|
||||
"src.models.modality_translation": MagicMock(),
|
||||
}):
|
||||
from src.services.pose_service import PoseService
|
||||
svc = PoseService(mock_settings, mock_domain_config)
|
||||
await svc.initialize()
|
||||
await svc.start()
|
||||
assert svc.is_running is True
|
||||
await svc.stop()
|
||||
assert svc.is_running is False
|
||||
|
||||
|
||||
class TestPoseServiceStats:
|
||||
def test_initial_classification(self, mock_settings, mock_domain_config):
|
||||
with patch.dict("sys.modules", {
|
||||
"torch": MagicMock(),
|
||||
"src.models.densepose_head": MagicMock(),
|
||||
"src.models.modality_translation": MagicMock(),
|
||||
}):
|
||||
from src.services.pose_service import PoseService
|
||||
svc = PoseService(mock_settings, mock_domain_config)
|
||||
assert svc.last_error is None
|
||||
@@ -1,62 +0,0 @@
|
||||
"""Tests for rate limiting middleware."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
|
||||
|
||||
class TestRateLimitMiddleware:
|
||||
def test_init(self, mock_settings):
|
||||
with patch("src.api.middleware.rate_limit.get_settings", return_value=mock_settings):
|
||||
from src.api.middleware.rate_limit import RateLimitMiddleware
|
||||
app = MagicMock()
|
||||
mw = RateLimitMiddleware(app)
|
||||
assert "anonymous" in mw.rate_limits
|
||||
assert "authenticated" in mw.rate_limits
|
||||
assert "admin" in mw.rate_limits
|
||||
|
||||
def test_exempt_paths(self, mock_settings):
|
||||
with patch("src.api.middleware.rate_limit.get_settings", return_value=mock_settings):
|
||||
from src.api.middleware.rate_limit import RateLimitMiddleware
|
||||
app = MagicMock()
|
||||
mw = RateLimitMiddleware(app)
|
||||
assert "/health" in mw.exempt_paths
|
||||
assert "/metrics" in mw.exempt_paths
|
||||
|
||||
def test_is_exempt(self, mock_settings):
|
||||
with patch("src.api.middleware.rate_limit.get_settings", return_value=mock_settings):
|
||||
from src.api.middleware.rate_limit import RateLimitMiddleware
|
||||
app = MagicMock()
|
||||
mw = RateLimitMiddleware(app)
|
||||
assert mw._is_exempt_path("/health") is True
|
||||
assert mw._is_exempt_path("/api/v1/pose/current") is False
|
||||
|
||||
def test_path_specific_limits(self, mock_settings):
|
||||
with patch("src.api.middleware.rate_limit.get_settings", return_value=mock_settings):
|
||||
from src.api.middleware.rate_limit import RateLimitMiddleware
|
||||
app = MagicMock()
|
||||
mw = RateLimitMiddleware(app)
|
||||
assert "/api/v1/pose/current" in mw.path_limits
|
||||
assert mw.path_limits["/api/v1/pose/current"]["requests"] == 60
|
||||
|
||||
def test_trusted_proxies_not_blocked(self, mock_settings):
|
||||
with patch("src.api.middleware.rate_limit.get_settings", return_value=mock_settings):
|
||||
from src.api.middleware.rate_limit import RateLimitMiddleware
|
||||
app = MagicMock()
|
||||
mw = RateLimitMiddleware(app)
|
||||
assert not mw._is_client_blocked("new-client-id")
|
||||
|
||||
|
||||
class TestRateLimitConfig:
|
||||
def test_anonymous_limit(self, mock_settings):
|
||||
with patch("src.api.middleware.rate_limit.get_settings", return_value=mock_settings):
|
||||
from src.api.middleware.rate_limit import RateLimitMiddleware
|
||||
app = MagicMock()
|
||||
mw = RateLimitMiddleware(app)
|
||||
assert mw.rate_limits["anonymous"]["burst"] == 10
|
||||
|
||||
def test_admin_limit(self, mock_settings):
|
||||
with patch("src.api.middleware.rate_limit.get_settings", return_value=mock_settings):
|
||||
from src.api.middleware.rate_limit import RateLimitMiddleware
|
||||
app = MagicMock()
|
||||
mw = RateLimitMiddleware(app)
|
||||
assert mw.rate_limits["admin"]["requests"] == 10000
|
||||
@@ -1,68 +0,0 @@
|
||||
"""Tests for StreamService."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, AsyncMock, patch
|
||||
|
||||
|
||||
class TestStreamServiceLifecycle:
|
||||
def test_init(self, mock_settings, mock_domain_config):
|
||||
from src.services.stream_service import StreamService
|
||||
svc = StreamService(mock_settings, mock_domain_config)
|
||||
assert svc.is_running is False
|
||||
assert len(svc.connections) == 0
|
||||
assert svc.stats["active_connections"] == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_initialize(self, mock_settings, mock_domain_config):
|
||||
from src.services.stream_service import StreamService
|
||||
svc = StreamService(mock_settings, mock_domain_config)
|
||||
await svc.initialize()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start(self, mock_settings, mock_domain_config):
|
||||
mock_settings.enable_real_time_processing = False
|
||||
from src.services.stream_service import StreamService
|
||||
svc = StreamService(mock_settings, mock_domain_config)
|
||||
await svc.start()
|
||||
assert svc.is_running is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop(self, mock_settings, mock_domain_config):
|
||||
mock_settings.enable_real_time_processing = False
|
||||
from src.services.stream_service import StreamService
|
||||
svc = StreamService(mock_settings, mock_domain_config)
|
||||
await svc.start()
|
||||
await svc.stop()
|
||||
assert svc.is_running is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_double_start(self, mock_settings, mock_domain_config):
|
||||
mock_settings.enable_real_time_processing = False
|
||||
from src.services.stream_service import StreamService
|
||||
svc = StreamService(mock_settings, mock_domain_config)
|
||||
await svc.start()
|
||||
await svc.start() # should be idempotent
|
||||
assert svc.is_running is True
|
||||
|
||||
|
||||
class TestStreamServiceConnections:
|
||||
def test_no_connections_on_init(self, mock_settings, mock_domain_config):
|
||||
from src.services.stream_service import StreamService
|
||||
svc = StreamService(mock_settings, mock_domain_config)
|
||||
assert svc.stats["total_connections"] == 0
|
||||
assert svc.stats["messages_sent"] == 0
|
||||
|
||||
def test_buffer_sizes(self, mock_settings, mock_domain_config):
|
||||
mock_settings.stream_buffer_size = 50
|
||||
from src.services.stream_service import StreamService
|
||||
svc = StreamService(mock_settings, mock_domain_config)
|
||||
assert svc.pose_buffer.maxlen == 50
|
||||
assert svc.csi_buffer.maxlen == 50
|
||||
|
||||
|
||||
class TestStreamServiceBroadcast:
|
||||
def test_stats_messages_failed_init_zero(self, mock_settings, mock_domain_config):
|
||||
from src.services.stream_service import StreamService
|
||||
svc = StreamService(mock_settings, mock_domain_config)
|
||||
assert svc.stats["messages_failed"] == 0
|
||||
assert svc.stats["data_points_streamed"] == 0
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"id": "pretrain-1775182186",
|
||||
"name": "pretrain-1775182186",
|
||||
"label": "mixed-activity",
|
||||
"started_at": "2026-04-03T02:09:46Z",
|
||||
"ended_at": "2026-04-03T02:11:46Z",
|
||||
"duration_secs": 120,
|
||||
"frame_count": 5783,
|
||||
"file_size_bytes": 2580539,
|
||||
"file_path": "data/recordings\\pretrain-1775182186.csi.jsonl",
|
||||
"nodes": {
|
||||
"2": 2886,
|
||||
"1": 2897
|
||||
}
|
||||
}
|
||||
@@ -10,16 +10,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies
|
||||
COPY archive/v1/requirements-lock.txt /app/requirements.txt
|
||||
COPY v1/requirements-lock.txt /app/requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt \
|
||||
&& pip install --no-cache-dir websockets uvicorn fastapi
|
||||
|
||||
# Copy application code
|
||||
COPY archive/v1/ /app/v1/
|
||||
COPY v1/ /app/v1/
|
||||
COPY ui/ /app/ui/
|
||||
|
||||
# Copy sensing modules
|
||||
COPY archive/v1/src/sensing/ /app/v1/src/sensing/
|
||||
COPY v1/src/sensing/ /app/v1/src/sensing/
|
||||
|
||||
EXPOSE 8765
|
||||
EXPOSE 8080
|
||||
|
||||
+6
-14
@@ -8,8 +8,8 @@ FROM rust:1.85-bookworm AS builder
|
||||
WORKDIR /build
|
||||
|
||||
# Copy workspace files
|
||||
COPY v2/Cargo.toml v2/Cargo.lock ./
|
||||
COPY v2/crates/ ./crates/
|
||||
COPY rust-port/wifi-densepose-rs/Cargo.toml rust-port/wifi-densepose-rs/Cargo.lock ./
|
||||
COPY rust-port/wifi-densepose-rs/crates/ ./crates/
|
||||
|
||||
# Copy vendored RuVector crates
|
||||
COPY vendor/ruvector/ /build/vendor/ruvector/
|
||||
@@ -50,15 +50,7 @@ ENV RUST_LOG=info
|
||||
# Override at runtime: docker run -e CSI_SOURCE=esp32 ...
|
||||
ENV CSI_SOURCE=auto
|
||||
|
||||
# MODELS_DIR controls where the server scans for .rvf model files.
|
||||
# Mount a host directory here to make models visible to the API:
|
||||
# docker run -v /path/to/models:/app/models -e MODELS_DIR=/app/models ...
|
||||
ENV MODELS_DIR=data/models
|
||||
|
||||
COPY docker/docker-entrypoint.sh /app/docker-entrypoint.sh
|
||||
|
||||
# Exec-form ENTRYPOINT so Docker appends user arguments correctly.
|
||||
# Pass flags directly: docker run <image> --source esp32 --tick-ms 500
|
||||
# Or use env vars: docker run -e CSI_SOURCE=esp32 <image>
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
CMD []
|
||||
ENTRYPOINT ["/bin/sh", "-c"]
|
||||
# Shell-form CMD allows $CSI_SOURCE to be substituted at container start.
|
||||
# The ENV default above (CSI_SOURCE=auto) applies when the variable is unset.
|
||||
CMD ["/app/sensing-server --source ${CSI_SOURCE} --tick-ms 100 --ui-path /app/ui --http-port 3000 --ws-port 3001"]
|
||||
|
||||
@@ -18,13 +18,8 @@ services:
|
||||
# wifi — use host Wi-Fi RSSI/scan data (Windows netsh)
|
||||
# simulated — generate synthetic CSI data (no hardware required)
|
||||
- CSI_SOURCE=${CSI_SOURCE:-auto}
|
||||
# MODELS_DIR controls where the server scans for .rvf model files.
|
||||
# Mount a host directory and set this to make models visible:
|
||||
# volumes: ["/path/to/models:/app/models"]
|
||||
# MODELS_DIR=/app/models
|
||||
- MODELS_DIR=${MODELS_DIR:-data/models}
|
||||
# No explicit command needed — docker-entrypoint.sh uses CSI_SOURCE.
|
||||
# Override with: command: ["--source", "esp32", "--tick-ms", "500"]
|
||||
# command is passed as arguments to ENTRYPOINT (/bin/sh -c), so $CSI_SOURCE is expanded by the shell.
|
||||
command: ["/app/sensing-server --source ${CSI_SOURCE:-auto} --tick-ms 100 --ui-path /app/ui --http-port 3000 --ws-port 3001"]
|
||||
|
||||
python-sensing:
|
||||
build:
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Docker entrypoint for WiFi-DensePose sensing server.
|
||||
#
|
||||
# Supports two usage patterns:
|
||||
#
|
||||
# 1. No arguments — use defaults from environment:
|
||||
# docker run -e CSI_SOURCE=esp32 ruvnet/wifi-densepose:latest
|
||||
#
|
||||
# 2. Pass CLI flags directly:
|
||||
# docker run ruvnet/wifi-densepose:latest --source esp32 --tick-ms 500
|
||||
# docker run ruvnet/wifi-densepose:latest --model /app/models/my.rvf
|
||||
#
|
||||
# Environment variables:
|
||||
# CSI_SOURCE — data source: auto (default), esp32, wifi, simulated
|
||||
# MODELS_DIR — directory to scan for .rvf model files (default: data/models)
|
||||
set -e
|
||||
|
||||
# If the first argument looks like a flag (starts with -), prepend the
|
||||
# server binary so users can just pass flags:
|
||||
# docker run <image> --source esp32 --tick-ms 500
|
||||
if [ "${1#-}" != "$1" ] || [ -z "$1" ]; then
|
||||
set -- /app/sensing-server \
|
||||
--source "${CSI_SOURCE:-auto}" \
|
||||
--tick-ms 100 \
|
||||
--ui-path /app/ui \
|
||||
--http-port 3000 \
|
||||
--ws-port 3001 \
|
||||
--bind-addr 0.0.0.0 \
|
||||
"$@"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
@@ -1,111 +0,0 @@
|
||||
# RuView Troubleshooting Guide
|
||||
|
||||
Known issues and fixes from the rebase-to-upstream branch (upstream #301).
|
||||
|
||||
---
|
||||
|
||||
## 1. Node not appearing in /api/v1/nodes
|
||||
|
||||
**Symptom:** ESP32-S3 node associates with WiFi, LED blinks, but no CSI frames arrive at the server. Node missing from `/api/v1/spatial/nodes`.
|
||||
|
||||
**Root cause:** After USB flash, the node enters a limping state where WiFi associates but the UDP CSI sender silently fails. The SoftAP + mDNS stack initializes but the CSI callback never fires.
|
||||
|
||||
**Fix:** Power cycle the node (unplug USB, wait 2s, replug). If that doesn't work, send DTR reset via serial: `python -m serial.tools.miniterm --dtr 0 COMx 115200` then Ctrl+C.
|
||||
|
||||
**Prevention:** Firmware 0.8.0+ includes a watchdog that detects zero CSI frames for 30s and triggers a software reset automatically. Nodes 1-10 are still on old firmware and lack this recovery (OTA-vs-BLE chicken-and-egg; see issue #6).
|
||||
|
||||
---
|
||||
|
||||
## 2. Person count stuck at 1
|
||||
|
||||
**Symptom:** `estimated_persons` always returns 1 regardless of how many people are in the room.
|
||||
|
||||
**Root cause (ADR-044):** Eight converging bugs:
|
||||
1. `score_to_person_count` had a ceiling of 3
|
||||
2. `fuse_multi_node_features` used `.max()` instead of sum — N identical readings collapsed to 1
|
||||
3. Four `.max(1)` clamps forced minimum count to 1 even when absent
|
||||
4. `field_model.estimate_occupancy` capped at `.min(3)`
|
||||
5. Normalization saturated (dividing by hardcoded thresholds instead of adaptive p95)
|
||||
6. No field model auto-calibration — eigenvalue path never activated
|
||||
7. Vitals-path clamps were asymmetric
|
||||
8. Tomography produced one blob (CC=1) so dedup gave wrong count
|
||||
|
||||
**Fix applied (Waves 1-3):**
|
||||
- Wave 1 (`9cc5f604`): ceiling 3→10, `.max()` → sum/3 aggregation, softened `.max(1)` clamps
|
||||
- Wave 2 (`306f1262`): RollingP95 adaptive normalization, field_model 30s auto-calibration, vitals clamp symmetry
|
||||
- Wave 3 (`c3df375a`+`0d4bfb09`+`6ac70ddf`): CC flood-fill infrastructure, lambda 0.1→5.0, threshold 0.01→0.15, CC>1 gate
|
||||
|
||||
**Current state:** `estimated_persons` = 6-8 for 5 bodies (3 humans + 2 dogs). Overcounts because the sum/3 dedup factor is a guess. Tomography still produces one blob (CC=1), so the CC path doesn't activate. Runtime-configurable lambda would help tune without redeployment.
|
||||
|
||||
---
|
||||
|
||||
## 3. Heart rate / breathing rate jitter
|
||||
|
||||
**Symptom:** HR and BR readings jump wildly between frames. BR CV was 23.3%, HR CV was 12.9%.
|
||||
|
||||
**Root cause (ADR-045):** 11 ESP32 nodes each compute independent vitals. The server used last-write-wins — whichever node's UDP packet arrived last overwrote the global vitals. At ~20 fps per node, this meant vitals randomly interleaved from different vantage points every 50ms.
|
||||
|
||||
**Fix applied (`46fbc061`):** Best-node selection. Each node's vitals are smoothed independently via median filter + EMA. The node with the highest combined `breathing_confidence + heartbeat_confidence` is selected as authoritative. Result: BR CV 23.3% → 12.6%, HR CV 12.9% → 11.6%.
|
||||
|
||||
**Known limitation:** The `wifi-densepose-vitals` crate has a superior 4-stage pipeline (bandpass → Hilbert envelope → autocorrelation → peak detection) but is not yet wired into the sensing server. The current `VitalSignDetector` uses a simpler FFT approach with 4 BPM frequency resolution.
|
||||
|
||||
---
|
||||
|
||||
## 4. Signal quality shows 50% always
|
||||
|
||||
**Symptom:** The dashboard signal quality gauge was always stuck at ~50%.
|
||||
|
||||
**Root cause:** Signal quality was a hardcoded placeholder value, not derived from actual CSI data.
|
||||
|
||||
**Fix applied:** ADR-044 Wave 2 replaced the fake gauge with RollingP95 adaptive normalization. The UI honesty pass (`b2070ab4`) added beta tags to unvalidated metrics, replaced the fake gauge with per-node pill indicators, and surfaced the actual per-node signal data.
|
||||
|
||||
---
|
||||
|
||||
## 5. Dashboard freezes every 2-4 seconds
|
||||
|
||||
**Symptom:** The spatial view and dashboard would freeze, then reconnect, creating a visible stutter every 2-4 seconds.
|
||||
|
||||
**Root cause:** The WebSocket broadcast channel's `recv()` returned `Err(Lagged)` when a client fell behind. The server treated this as a fatal error and dropped the connection. The client immediately reconnected, creating a connect/disconnect cycle.
|
||||
|
||||
**Fix applied (`581daf4f`):**
|
||||
- Server: `Lagged` error → `continue` (skip missed frames instead of disconnecting)
|
||||
- Server: 30s ping/pong keepalive to prevent Caddy proxy idle timeouts
|
||||
- Result: 154 frames over 8 seconds sustained, zero disconnects
|
||||
|
||||
---
|
||||
|
||||
## 6. OTA update crashes at 59%
|
||||
|
||||
**Symptom:** OTA firmware update via `/api/v1/firmware/download` progresses to ~59% then the node crashes with `StoreProhibited` on Core 1.
|
||||
|
||||
**Root cause:** NimBLE BLE advertising/scanning runs on Core 1. During OTA, the HTTP client also runs on Core 1. BLE and OTA compete for stack space, and the BLE scan callback triggers a memory access violation during the OTA write.
|
||||
|
||||
**Fix:**
|
||||
1. Stop NimBLE advertising and scanning before calling `esp_https_ota_begin()`
|
||||
2. Increase httpd stack from 4KB to 8KB (`CONFIG_HTTPD_MAX_REQ_HDR_LEN` and task stack)
|
||||
3. Resume BLE after OTA completes or fails
|
||||
|
||||
**Caveat:** Nodes running old firmware (1-10) can't receive this fix via OTA because the crash happens during the OTA itself. These nodes must be USB-flashed with firmware 0.8.0+ first, then future OTA updates will work. Node 11 was USB-flashed with the watchdog firmware and can receive OTA updates.
|
||||
|
||||
---
|
||||
|
||||
## 7. Can't SSH to babycube via LAN
|
||||
|
||||
**Symptom:** `ssh thyhack@10.0.10.10` hangs at banner exchange. Ping works, TCP port 22 is open, but SSH never completes the handshake.
|
||||
|
||||
**Workaround:** Use the Tailscale IP instead:
|
||||
```
|
||||
ssh thyhack@100.90.238.87
|
||||
```
|
||||
|
||||
**Not the cause:** CrowdSec. The 10.0.0.0/8 range is whitelisted in CrowdSec (`cscli decisions list` shows no active decisions for LAN IPs). The banner hang occurs before any authentication attempt, so it's not a firewall block.
|
||||
|
||||
**Suspected cause:** Unknown. Possibly MTU/fragmentation issue on the LAN segment, or a network stack bug in the babycube's NIC driver. The Tailscale overlay network (WireGuard UDP) bypasses whatever is causing the LAN TCP issue.
|
||||
|
||||
---
|
||||
|
||||
## 8. Right USB-C port doesn't work on some ESP32-S3 boards
|
||||
|
||||
**Symptom:** Plugging into the right USB-C port (when facing the board with USB-C toward you) shows no serial device on the host.
|
||||
|
||||
**Fix:** Use the left USB-C port. On most ESP32-S3-DevKitC boards, the left port is the USB-to-UART bridge (CP2102/CH340) used for flashing and serial monitor. The right port is the native USB (USB-JTAG) which requires different drivers and isn't used by the RuView firmware.
|
||||
@@ -35,7 +35,7 @@ git checkout 96b01008
|
||||
### Step 2: Rust Workspace — Full Test Suite
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo test --workspace --no-default-features
|
||||
```
|
||||
|
||||
@@ -89,7 +89,7 @@ ls firmware/esp32-csi-node/build/*.bin 2>/dev/null || echo "App binary in build/
|
||||
### Step 6: Verify ADR-018 Binary Frame Parser
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo test -p wifi-densepose-hardware --no-default-features
|
||||
```
|
||||
|
||||
@@ -133,7 +133,7 @@ cargo test -p wifi-densepose-train --no-default-features
|
||||
### Step 9: Verify Python Proof System
|
||||
|
||||
```bash
|
||||
python archive/v1/data/proof/verify.py
|
||||
python v1/data/proof/verify.py
|
||||
```
|
||||
|
||||
**Expected:** PASS (hash `8c0680d7...` matches `expected_features.sha256`).
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
## Introduction
|
||||
|
||||
RuView is a WiFi-based human pose estimation system built on ESP32 CSI (Channel State Information). Today, managing a RuView deployment requires juggling **6+ disconnected CLI tools**: `esptool.py` for flashing, `provision.py` for NVS configuration, `curl` for OTA and WASM management, `cargo run` for the sensing server, a browser for visualization, and manual IP tracking for node discovery. There is no single tool that provides a unified view of the entire deployment — from ESP32 hardware through the sensing pipeline to pose visualization.
|
||||
|
||||
This issue tracks the implementation of **RuView Desktop** — a Tauri v2 cross-platform desktop application that replaces all of these tools with a single, cohesive interface. The application is designed as the **control plane** for the RuView platform, managing the full lifecycle: discover, flash, provision, OTA, load WASM, observe sensing.
|
||||
|
||||
### Why Tauri (Not Electron/Flutter/Web)
|
||||
|
||||
| Requirement | Why Desktop is Required |
|
||||
|-------------|------------------------|
|
||||
| Serial port access | Browser/PWA cannot touch COM/tty ports for firmware flashing |
|
||||
| Raw UDP sockets | Node discovery via broadcast probes requires raw socket access |
|
||||
| Filesystem access | Firmware binaries, WASM modules, model files live on local disk |
|
||||
| Process management | Sensing server runs as a managed child process (sidecar) |
|
||||
| Small binary | Tauri ~20 MB vs Electron ~150 MB |
|
||||
| Rust integration | Shares crates with existing workspace |
|
||||
|
||||
### UI Design Language
|
||||
|
||||
The frontend uses a **Foundation Book** design scheme with **Unity Editor-inspired** UI panels. Think: clean typographic hierarchy, structured panels with dockable regions, monospaced data displays, and a professional dark theme with accent colors for status indicators. Powered by rUv.
|
||||
|
||||
---
|
||||
|
||||
## ADR-052 Deep Overview
|
||||
|
||||
The full architecture is documented in [ADR-052](https://github.com/ruvnet/RuView/blob/feat/tauri-desktop-frontend/docs/adr/ADR-052-tauri-desktop-frontend.md) with a companion [DDD bounded contexts appendix](https://github.com/ruvnet/RuView/blob/feat/tauri-desktop-frontend/docs/adr/ADR-052-ddd-bounded-contexts.md).
|
||||
|
||||
### Workspace Integration
|
||||
|
||||
The desktop app is a new Rust crate (`wifi-densepose-desktop`) in the existing workspace, sharing types with the sensing server and hardware crate. The frontend uses React + Vite + TypeScript with a Foundation Book / Unity-inspired design system.
|
||||
|
||||
### 6 Rust Command Groups
|
||||
|
||||
| Group | Commands | Bounded Context |
|
||||
|-------|----------|-----------------|
|
||||
| **Discovery** | `discover_nodes`, `get_node_status`, `watch_nodes` | Device Discovery |
|
||||
| **Flash** | `list_serial_ports`, `flash_firmware`, `read_chip_info` | Firmware Management |
|
||||
| **OTA** | `ota_update`, `ota_status`, `ota_batch_update` | Firmware Management |
|
||||
| **WASM** | `wasm_list`, `wasm_upload`, `wasm_control` | Edge Module |
|
||||
| **Server** | `start_server`, `stop_server`, `server_status` | Sensing Pipeline |
|
||||
| **Provision** | `provision_node`, `read_nvs` | Configuration |
|
||||
|
||||
### 7 Frontend Pages
|
||||
|
||||
| Page | Purpose |
|
||||
|------|---------|
|
||||
| **Dashboard** | Node count (online/offline), server status, quick actions, activity feed |
|
||||
| **Node Detail** | Single node deep-dive: firmware, health, TDM config, WASM modules |
|
||||
| **Flash Firmware** | 3-step wizard: select port, select firmware, flash with progress bar |
|
||||
| **WASM Modules** | Drag-and-drop upload, module list with start/stop/unload |
|
||||
| **Sensing View** | Live CSI heatmap, pose skeleton overlay, vital signs |
|
||||
| **Mesh Topology** | Force-directed graph: TDM slots, sync drift, node health |
|
||||
| **Settings** | Server ports, bind address, OTA PSK, UI theme |
|
||||
|
||||
### DDD Bounded Contexts
|
||||
|
||||
6 bounded contexts with 9 aggregates, 25+ domain events, and 3 anti-corruption layers. See the [DDD appendix](https://github.com/ruvnet/RuView/blob/feat/tauri-desktop-frontend/docs/adr/ADR-052-ddd-bounded-contexts.md) for full details.
|
||||
|
||||
| Context | Aggregate Root(s) | Key Events |
|
||||
|---------|--------------------|------------|
|
||||
| Device Discovery | `NodeRegistry` | `NodeDiscovered`, `NodeWentOffline`, `ScanCompleted` |
|
||||
| Firmware Management | `FlashSession`, `OtaSession`, `BatchOtaSession` | `FlashProgress`, `OtaCompleted`, `BatchOtaCompleted` |
|
||||
| Configuration | `ProvisioningSession` | `NodeProvisioned`, `ConfigReadBack` |
|
||||
| Sensing Pipeline | `SensingServer`, `WebSocketSession` | `ServerStarted`, `FrameReceived` |
|
||||
| Edge Module (WASM) | `ModuleRegistry` | `ModuleUploaded`, `ModuleStarted` |
|
||||
| Visualization | Query model (no aggregate) | Consumes all upstream events |
|
||||
|
||||
### Persistent Node Registry
|
||||
|
||||
Stored in `~/.ruview/nodes.db` (SQLite). On startup, previously known nodes load as Offline and reconcile against fresh discovery. The app remembers the mesh across restarts.
|
||||
|
||||
### OTA Safety Gate
|
||||
|
||||
The `TdmSafe` rolling update strategy updates even-slot nodes first, then odd-slot nodes, ensuring adjacent nodes are never offline simultaneously during mesh-wide firmware updates.
|
||||
|
||||
### Platform-Specific Considerations
|
||||
|
||||
| Platform | Concern | Solution |
|
||||
|----------|---------|----------|
|
||||
| macOS | USB serial drivers need signing on Sequoia+ | Document driver requirements |
|
||||
| Windows | COM port naming, UAC | Auto-detect via registry |
|
||||
| Linux | Serial port permissions | Bundle udev rules installer |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
| Phase | Scope | Priority |
|
||||
|-------|-------|----------|
|
||||
| 1. Skeleton | Tauri scaffolding, workspace integration, React window | P0 |
|
||||
| 2. Discovery | Serial ports, node discovery, dashboard cards | P0 |
|
||||
| 3. Flash | espflash integration, flashing wizard | P0 |
|
||||
| 4. Server | Sidecar sensing server, log viewer | P1 |
|
||||
| 5. OTA | HTTP OTA with PSK auth, batch TdmSafe | P1 |
|
||||
| 6. Provisioning | NVS GUI form, read-back, mesh presets | P1 |
|
||||
| 7. WASM | Module upload/list/control | P2 |
|
||||
| 8. Sensing | WebSocket, live charts, pose overlay | P2 |
|
||||
| 9. Mesh View | Topology graph, TDM visualization | P2 |
|
||||
| 10. Polish | App signing, auto-update, onboarding wizard | P3 |
|
||||
|
||||
Total estimated effort: ~11 weeks for a single developer.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Tauri app builds on Windows, macOS, Linux
|
||||
- [ ] Can discover ESP32 nodes on local network
|
||||
- [ ] Node registry persists across restarts
|
||||
- [ ] Can flash firmware via serial port (no Python dependency)
|
||||
- [ ] Can push OTA updates with PSK authentication
|
||||
- [ ] Rolling OTA with TdmSafe strategy for mesh deployments
|
||||
- [ ] Can upload/manage WASM modules on nodes
|
||||
- [ ] Can start/stop sensing server and view live logs
|
||||
- [ ] Can view real-time sensing data via WebSocket
|
||||
- [ ] Can provision NVS config via GUI form
|
||||
- [ ] Mesh topology visualization shows TDM slots and health
|
||||
- [ ] Binary size less than 30 MB
|
||||
- [ ] Foundation Book / Unity-inspired UI design system
|
||||
- [ ] Each new Rust module has unit tests
|
||||
|
||||
## Dependencies
|
||||
|
||||
- ADR-012: ESP32 CSI Sensor Mesh
|
||||
- ADR-039: ESP32 Edge Intelligence
|
||||
- ADR-040: WASM Programmable Sensing
|
||||
- ADR-044: Provisioning Tool Enhancements
|
||||
- ADR-050: Quality Engineering Security Hardening
|
||||
- ADR-051: Sensing Server Decomposition
|
||||
- ADR-053: UI Design System (Foundation Book + Unity-inspired)
|
||||
|
||||
## Branch
|
||||
|
||||
[`feat/tauri-desktop-frontend`](https://github.com/ruvnet/RuView/tree/feat/tauri-desktop-frontend)
|
||||
|
||||
## References
|
||||
|
||||
- [ADR-052: Tauri Desktop Frontend](https://github.com/ruvnet/RuView/blob/feat/tauri-desktop-frontend/docs/adr/ADR-052-tauri-desktop-frontend.md)
|
||||
- [ADR-052 DDD Appendix](https://github.com/ruvnet/RuView/blob/feat/tauri-desktop-frontend/docs/adr/ADR-052-ddd-bounded-contexts.md)
|
||||
- [Tauri v2 Documentation](https://v2.tauri.app/)
|
||||
- [espflash crate](https://crates.io/crates/espflash)
|
||||
|
||||
Powered by **rUv**
|
||||
@@ -216,4 +216,4 @@ full = ["mincut-matching", "attn-mincut", "temporal-compress", "solver-interpola
|
||||
- [Elastic Weight Consolidation](https://arxiv.org/abs/1612.00796)
|
||||
- [Raft Consensus](https://raft.github.io/raft.pdf)
|
||||
- [ML-DSA (FIPS 204)](https://csrc.nist.gov/pubs/fips/204/final)
|
||||
- [WiFi-DensePose Rust ADR-001: Workspace Structure](../v2/docs/adr/ADR-001-workspace-structure.md)
|
||||
- [WiFi-DensePose Rust ADR-001: Workspace Structure](../rust-port/wifi-densepose-rs/docs/adr/ADR-001-workspace-structure.md)
|
||||
|
||||
@@ -20,31 +20,31 @@ The following code paths produce fake data **in the default configuration** or a
|
||||
|
||||
| File | Line | Issue | Impact |
|
||||
|------|------|-------|--------|
|
||||
| `archive/v1/src/core/csi_processor.py` | 390 | `doppler_shift = np.random.rand(10) # Placeholder` | **Real feature extractor returns random Doppler** - kills credibility of entire feature pipeline |
|
||||
| `archive/v1/src/hardware/csi_extractor.py` | 83-84 | `amplitude = np.random.rand(...)` in CSI extraction fallback | Random data silently substituted when parsing fails |
|
||||
| `archive/v1/src/hardware/csi_extractor.py` | 129-135 | `_parse_atheros()` returns `np.random.rand()` with comment "placeholder implementation" | Named as if it parses real data, actually random |
|
||||
| `archive/v1/src/hardware/router_interface.py` | 211-212 | `np.random.rand(3, 56)` in fallback path | Silent random fallback |
|
||||
| `archive/v1/src/services/pose_service.py` | 431 | `mock_csi = np.random.randn(64, 56, 3) # Mock CSI data` | Mock CSI in production code path |
|
||||
| `archive/v1/src/services/pose_service.py` | 293-356 | `_generate_mock_poses()` with `random.randint` throughout | Entire mock pose generator in service layer |
|
||||
| `archive/v1/src/services/pose_service.py` | 489-607 | Multiple `random.randint` for occupancy, historical data | Fake statistics that look real in API responses |
|
||||
| `archive/v1/src/api/dependencies.py` | 82, 408 | "return a mock user for development" | Auth bypass in default path |
|
||||
| `v1/src/core/csi_processor.py` | 390 | `doppler_shift = np.random.rand(10) # Placeholder` | **Real feature extractor returns random Doppler** - kills credibility of entire feature pipeline |
|
||||
| `v1/src/hardware/csi_extractor.py` | 83-84 | `amplitude = np.random.rand(...)` in CSI extraction fallback | Random data silently substituted when parsing fails |
|
||||
| `v1/src/hardware/csi_extractor.py` | 129-135 | `_parse_atheros()` returns `np.random.rand()` with comment "placeholder implementation" | Named as if it parses real data, actually random |
|
||||
| `v1/src/hardware/router_interface.py` | 211-212 | `np.random.rand(3, 56)` in fallback path | Silent random fallback |
|
||||
| `v1/src/services/pose_service.py` | 431 | `mock_csi = np.random.randn(64, 56, 3) # Mock CSI data` | Mock CSI in production code path |
|
||||
| `v1/src/services/pose_service.py` | 293-356 | `_generate_mock_poses()` with `random.randint` throughout | Entire mock pose generator in service layer |
|
||||
| `v1/src/services/pose_service.py` | 489-607 | Multiple `random.randint` for occupancy, historical data | Fake statistics that look real in API responses |
|
||||
| `v1/src/api/dependencies.py` | 82, 408 | "return a mock user for development" | Auth bypass in default path |
|
||||
|
||||
#### Moderate Severity (mock gated behind flags but confusing)
|
||||
|
||||
| File | Line | Issue |
|
||||
|------|------|-------|
|
||||
| `archive/v1/src/config/settings.py` | 144-145 | `mock_hardware=False`, `mock_pose_data=False` defaults - correct, but mock infrastructure exists |
|
||||
| `archive/v1/src/core/router_interface.py` | 27-300 | 270+ lines of mock data generation infrastructure in production code |
|
||||
| `archive/v1/src/services/pose_service.py` | 84-88 | Silent conditional: `if not self.settings.mock_pose_data` with no logging of real-mode |
|
||||
| `archive/v1/src/services/hardware_service.py` | 72-375 | Interleaved mock/real paths throughout |
|
||||
| `v1/src/config/settings.py` | 144-145 | `mock_hardware=False`, `mock_pose_data=False` defaults - correct, but mock infrastructure exists |
|
||||
| `v1/src/core/router_interface.py` | 27-300 | 270+ lines of mock data generation infrastructure in production code |
|
||||
| `v1/src/services/pose_service.py` | 84-88 | Silent conditional: `if not self.settings.mock_pose_data` with no logging of real-mode |
|
||||
| `v1/src/services/hardware_service.py` | 72-375 | Interleaved mock/real paths throughout |
|
||||
|
||||
#### Low Severity (placeholders/TODOs)
|
||||
|
||||
| File | Line | Issue |
|
||||
|------|------|-------|
|
||||
| `archive/v1/src/core/router_interface.py` | 198 | "Collect real CSI data from router (placeholder implementation)" |
|
||||
| `archive/v1/src/api/routers/health.py` | 170-171 | `uptime_seconds = 0.0 # TODO` |
|
||||
| `archive/v1/src/services/pose_service.py` | 739 | `"uptime_seconds": 0.0 # TODO` |
|
||||
| `v1/src/core/router_interface.py` | 198 | "Collect real CSI data from router (placeholder implementation)" |
|
||||
| `v1/src/api/routers/health.py` | 170-171 | `uptime_seconds = 0.0 # TODO` |
|
||||
| `v1/src/services/pose_service.py` | 739 | `"uptime_seconds": 0.0 # TODO` |
|
||||
|
||||
### Root Cause Analysis
|
||||
|
||||
@@ -119,7 +119,7 @@ def _parse_atheros(self, raw_data: bytes) -> CSIData:
|
||||
**All mock code moves to a dedicated module. Default execution NEVER touches mock paths.**
|
||||
|
||||
```
|
||||
archive/v1/src/
|
||||
v1/src/
|
||||
├── core/
|
||||
│ ├── csi_processor.py # Real processing only
|
||||
│ └── router_interface.py # Real hardware interface only
|
||||
@@ -157,7 +157,7 @@ if MOCK_MODE:
|
||||
A small real CSI capture file + one-command verification pipeline:
|
||||
|
||||
```
|
||||
archive/v1/data/proof/
|
||||
v1/data/proof/
|
||||
├── README.md # How to verify
|
||||
├── sample_csi_capture.bin # Real CSI data (1 second, ~50 KB)
|
||||
├── sample_csi_capture_meta.json # Capture metadata (hardware, env)
|
||||
@@ -172,7 +172,7 @@ archive/v1/data/proof/
|
||||
"""Verify WiFi-DensePose pipeline produces deterministic output from real CSI data.
|
||||
|
||||
Usage:
|
||||
python archive/v1/data/proof/verify.py
|
||||
python v1/data/proof/verify.py
|
||||
|
||||
Expected output:
|
||||
PASS: Pipeline output matches expected hash
|
||||
@@ -265,13 +265,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
WORKDIR /app
|
||||
|
||||
# Pinned requirements (not a reference to missing file)
|
||||
COPY archive/v1/requirements-lock.txt ./requirements.txt
|
||||
COPY v1/requirements-lock.txt ./requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY archive/v1/ ./v1/
|
||||
COPY v1/ ./v1/
|
||||
|
||||
# Proof of reality: verify pipeline on build
|
||||
RUN cd archive/v1 && python data/proof/verify.py
|
||||
RUN cd v1 && python data/proof/verify.py
|
||||
|
||||
EXPOSE 8000
|
||||
# Default: REAL mode (mock requires explicit opt-in)
|
||||
@@ -281,7 +281,7 @@ CMD ["uvicorn", "v1.src.api.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
**Key change**: `RUN python data/proof/verify.py` **during build** means the Docker image cannot be created unless the pipeline produces correct output from real CSI data.
|
||||
|
||||
**Requirements lockfile** (`archive/v1/requirements-lock.txt`):
|
||||
**Requirements lockfile** (`v1/requirements-lock.txt`):
|
||||
```
|
||||
# Core (required)
|
||||
fastapi==0.115.6
|
||||
@@ -307,9 +307,9 @@ name: Verify Signal Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
paths: ['archive/v1/src/**', 'archive/v1/data/proof/**']
|
||||
paths: ['v1/src/**', 'v1/data/proof/**']
|
||||
pull_request:
|
||||
paths: ['archive/v1/src/**']
|
||||
paths: ['v1/src/**']
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
@@ -322,11 +322,11 @@ jobs:
|
||||
- name: Install minimal deps
|
||||
run: pip install numpy scipy pydantic pydantic-settings
|
||||
- name: Verify pipeline determinism
|
||||
run: python archive/v1/data/proof/verify.py
|
||||
run: python v1/data/proof/verify.py
|
||||
- name: Verify no random in production paths
|
||||
run: |
|
||||
# Fail if np.random appears in production code (not in testing/)
|
||||
! grep -r "np\.random\.\(rand\|randn\|randint\)" archive/v1/src/ \
|
||||
! grep -r "np\.random\.\(rand\|randn\|randint\)" v1/src/ \
|
||||
--include="*.py" \
|
||||
--exclude-dir=testing \
|
||||
|| (echo "FAIL: np.random found in production code" && exit 1)
|
||||
@@ -336,23 +336,23 @@ jobs:
|
||||
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `archive/v1/src/core/csi_processor.py:390` | **Replace** | Real Doppler extraction from temporal CSI history |
|
||||
| `archive/v1/src/hardware/csi_extractor.py:83-84` | **Replace** | Hard error with descriptive message when parsing fails |
|
||||
| `archive/v1/src/hardware/csi_extractor.py:129-135` | **Replace** | Real Atheros CSI parser or hard error with hardware instructions |
|
||||
| `archive/v1/src/hardware/router_interface.py:198-212` | **Replace** | Hard error for unimplemented hardware, or real `iwconfig` + CSI tool integration |
|
||||
| `archive/v1/src/services/pose_service.py:293-356` | **Move** | Move `_generate_mock_poses()` to `archive/v1/src/testing/mock_pose_generator.py` |
|
||||
| `archive/v1/src/services/pose_service.py:430-431` | **Remove** | Remove mock CSI generation from production path |
|
||||
| `archive/v1/src/services/pose_service.py:489-607` | **Replace** | Real statistics from database, or explicit "no data" response |
|
||||
| `archive/v1/src/core/router_interface.py:60-300` | **Move** | Move mock generator to `archive/v1/src/testing/mock_csi_generator.py` |
|
||||
| `archive/v1/src/api/dependencies.py:82,408` | **Replace** | Real auth check or explicit dev-mode bypass with logging |
|
||||
| `archive/v1/data/proof/` | **Create** | Proof bundle (sample capture + expected hash + verify script) |
|
||||
| `archive/v1/requirements-lock.txt` | **Create** | Pinned minimal dependencies |
|
||||
| `v1/src/core/csi_processor.py:390` | **Replace** | Real Doppler extraction from temporal CSI history |
|
||||
| `v1/src/hardware/csi_extractor.py:83-84` | **Replace** | Hard error with descriptive message when parsing fails |
|
||||
| `v1/src/hardware/csi_extractor.py:129-135` | **Replace** | Real Atheros CSI parser or hard error with hardware instructions |
|
||||
| `v1/src/hardware/router_interface.py:198-212` | **Replace** | Hard error for unimplemented hardware, or real `iwconfig` + CSI tool integration |
|
||||
| `v1/src/services/pose_service.py:293-356` | **Move** | Move `_generate_mock_poses()` to `v1/src/testing/mock_pose_generator.py` |
|
||||
| `v1/src/services/pose_service.py:430-431` | **Remove** | Remove mock CSI generation from production path |
|
||||
| `v1/src/services/pose_service.py:489-607` | **Replace** | Real statistics from database, or explicit "no data" response |
|
||||
| `v1/src/core/router_interface.py:60-300` | **Move** | Move mock generator to `v1/src/testing/mock_csi_generator.py` |
|
||||
| `v1/src/api/dependencies.py:82,408` | **Replace** | Real auth check or explicit dev-mode bypass with logging |
|
||||
| `v1/data/proof/` | **Create** | Proof bundle (sample capture + expected hash + verify script) |
|
||||
| `v1/requirements-lock.txt` | **Create** | Pinned minimal dependencies |
|
||||
| `.github/workflows/verify-pipeline.yml` | **Create** | CI verification |
|
||||
|
||||
### Hardware Documentation
|
||||
|
||||
```
|
||||
archive/v1/docs/hardware-setup.md (to be created)
|
||||
v1/docs/hardware-setup.md (to be created)
|
||||
|
||||
# Supported Hardware Matrix
|
||||
|
||||
@@ -368,17 +368,17 @@ archive/v1/docs/hardware-setup.md (to be created)
|
||||
2. Capture 10 seconds of empty-room baseline
|
||||
3. Have one person walk through at normal pace
|
||||
4. Capture 10 seconds during walk-through
|
||||
5. Run calibration: `python archive/v1/scripts/calibrate.py --baseline empty.dat --activity walk.dat`
|
||||
5. Run calibration: `python v1/scripts/calibrate.py --baseline empty.dat --activity walk.dat`
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- **"Clone, build, verify" in one command**: `docker build . && docker run --rm wifi-densepose python archive/v1/data/proof/verify.py` produces a deterministic PASS
|
||||
- **"Clone, build, verify" in one command**: `docker build . && docker run --rm wifi-densepose python v1/data/proof/verify.py` produces a deterministic PASS
|
||||
- **No silent fakes**: Random data never appears in production output
|
||||
- **CI enforcement**: PRs that introduce `np.random` in production paths fail automatically
|
||||
- **Credibility anchor**: SHA-256 verified output from real CSI capture is unchallengeable proof
|
||||
- **Clear mock boundary**: Mock code exists only in `archive/v1/src/testing/`, never imported by production modules
|
||||
- **Clear mock boundary**: Mock code exists only in `v1/src/testing/`, never imported by production modules
|
||||
|
||||
### Negative
|
||||
- **Requires real CSI capture**: Someone must capture and commit a real CSI sample (one-time effort)
|
||||
@@ -390,7 +390,7 @@ archive/v1/docs/hardware-setup.md (to be created)
|
||||
|
||||
A stranger can:
|
||||
1. `git clone` the repository
|
||||
2. Run ONE command (`docker build .` or `python archive/v1/data/proof/verify.py`)
|
||||
2. Run ONE command (`docker build .` or `python v1/data/proof/verify.py`)
|
||||
3. See `PASS: Pipeline output matches expected hash` with a specific SHA-256
|
||||
4. Confirm no `np.random` in any non-test file via CI badge
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ typedef struct {
|
||||
The aggregator runs on any machine with WiFi/Ethernet to the nodes:
|
||||
|
||||
```rust
|
||||
// In v2/, new module: crates/wifi-densepose-hardware/src/esp32/
|
||||
// In wifi-densepose-rs, new module: crates/wifi-densepose-hardware/src/esp32/
|
||||
pub struct Esp32Aggregator {
|
||||
/// UDP socket listening for node streams
|
||||
socket: UdpSocket,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ADR-013: Feature-Level Sensing on Commodity Gear (Option 3)
|
||||
|
||||
## Status
|
||||
Accepted — Implemented (36/36 unit tests pass, see `archive/v1/src/sensing/` and `archive/v1/tests/unit/test_sensing.py`)
|
||||
Accepted — Implemented (36/36 unit tests pass, see `v1/src/sensing/` and `v1/tests/unit/test_sensing.py`)
|
||||
|
||||
## Date
|
||||
2026-02-28
|
||||
@@ -323,7 +323,7 @@ class PresenceClassifier:
|
||||
### Proof Bundle for Commodity Sensing
|
||||
|
||||
```
|
||||
archive/v1/data/proof/commodity/
|
||||
v1/data/proof/commodity/
|
||||
├── rssi_capture_30sec.json # 30 seconds of RSSI from 3 receivers
|
||||
├── rssi_capture_meta.json # Hardware: Intel AX200, Router: TP-Link AX1800
|
||||
├── scenario.txt # "Person walks through room at t=10s, sits at t=20s"
|
||||
@@ -375,7 +375,7 @@ class CommodityBackend(SensingBackend):
|
||||
|
||||
### Implementation Status
|
||||
|
||||
The full commodity sensing pipeline is implemented in `archive/v1/src/sensing/`:
|
||||
The full commodity sensing pipeline is implemented in `v1/src/sensing/`:
|
||||
|
||||
| Module | File | Description |
|
||||
|--------|------|-------------|
|
||||
@@ -384,7 +384,7 @@ The full commodity sensing pipeline is implemented in `archive/v1/src/sensing/`:
|
||||
| Classifier | `classifier.py` | `PresenceClassifier` with ABSENT/PRESENT_STILL/ACTIVE levels, confidence scoring |
|
||||
| Backend | `backend.py` | `CommodityBackend` wiring collector → extractor → classifier, reports PRESENCE + MOTION capabilities |
|
||||
|
||||
**Test coverage**: 36 tests in `archive/v1/tests/unit/test_sensing.py` — all passing:
|
||||
**Test coverage**: 36 tests in `v1/tests/unit/test_sensing.py` — all passing:
|
||||
- `TestRingBuffer` (4), `TestSimulatedCollector` (5), `TestFeatureExtractor` (8), `TestCusum` (4), `TestPresenceClassifier` (7), `TestCommodityBackend` (6), `TestBandPower` (2)
|
||||
|
||||
**Dependencies**: `numpy`, `scipy` (for FFT and spectral analysis)
|
||||
|
||||
@@ -510,7 +510,7 @@ impl CompressedHeartbeatSpectrogram {
|
||||
|
||||
## Dependency Changes Required
|
||||
|
||||
Add to `v2/Cargo.toml` workspace (already present from ADR-016):
|
||||
Add to `rust-port/wifi-densepose-rs/Cargo.toml` workspace (already present from ADR-016):
|
||||
```toml
|
||||
ruvector-mincut = "2.0.4" # already present
|
||||
ruvector-attn-mincut = "2.0.4" # already present
|
||||
|
||||
@@ -22,8 +22,8 @@ This ADR answers *how* to build it — the concrete development sequence, the sp
|
||||
| Frame types | `wifi-densepose-hardware/src/csi_frame.rs` | Complete — `CsiFrame`, `CsiMetadata`, `SubcarrierData`, `to_amplitude_phase()` |
|
||||
| Parse error types | `wifi-densepose-hardware/src/error.rs` | Complete — `ParseError` enum with 6 variants |
|
||||
| Signal processing pipeline | `wifi-densepose-signal` crate | Complete — Hampel, Fresnel, BVP, Doppler, spectrogram |
|
||||
| CSI extractor (Python) | `archive/v1/src/hardware/csi_extractor.py` | Stub — `_read_raw_data()` raises `NotImplementedError` |
|
||||
| Router interface (Python) | `archive/v1/src/hardware/router_interface.py` | Stub — `_parse_csi_response()` raises `RouterConnectionError` |
|
||||
| CSI extractor (Python) | `v1/src/hardware/csi_extractor.py` | Stub — `_read_raw_data()` raises `NotImplementedError` |
|
||||
| Router interface (Python) | `v1/src/hardware/router_interface.py` | Stub — `_parse_csi_response()` raises `RouterConnectionError` |
|
||||
|
||||
**Not yet implemented:**
|
||||
|
||||
@@ -211,10 +211,10 @@ The bridge test: parse a known binary frame, convert to `CsiData`, assert `ampli
|
||||
|
||||
### Layer 4 — Python `_read_raw_data()` Real Implementation
|
||||
|
||||
Replace the `NotImplementedError` stub in `archive/v1/src/hardware/csi_extractor.py` with a UDP socket reader. This allows the Python pipeline to receive real CSI from the aggregator while the Rust pipeline is being integrated.
|
||||
Replace the `NotImplementedError` stub in `v1/src/hardware/csi_extractor.py` with a UDP socket reader. This allows the Python pipeline to receive real CSI from the aggregator while the Rust pipeline is being integrated.
|
||||
|
||||
```python
|
||||
# archive/v1/src/hardware/csi_extractor.py
|
||||
# v1/src/hardware/csi_extractor.py
|
||||
# Replace _read_raw_data() stub:
|
||||
|
||||
import socket as _socket
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
The WiFi-DensePose UI was originally built to require the full FastAPI DensePose backend (`localhost:8000`) for all functionality. This backend depends on heavy Python packages (PyTorch ~2GB, torchvision, OpenCV, SQLAlchemy, Redis) making it impractical for lightweight sensing-only deployments where the user simply wants to visualize live WiFi signal data from ESP32 CSI or Windows RSSI collectors.
|
||||
|
||||
A Rust port exists (`v2`) using Axum with lighter runtime footprint (~10MB binary, ~5MB RAM), but it still requires libtorch C++ bindings and OpenBLAS for compilation—a non-trivial build.
|
||||
A Rust port exists (`rust-port/wifi-densepose-rs`) using Axum with lighter runtime footprint (~10MB binary, ~5MB RAM), but it still requires libtorch C++ bindings and OpenBLAS for compilation—a non-trivial build.
|
||||
|
||||
Users need a way to run the UI with **only the sensing pipeline** active, without installing the full DensePose backend stack.
|
||||
|
||||
@@ -34,7 +34,7 @@ Implement a **sensing-only UI mode** that:
|
||||
- Breathing ring modulation when breathing-band power detected
|
||||
- Side panel with RSSI sparkline, feature meters, and classification badge
|
||||
|
||||
4. **Python WebSocket bridge** (`archive/v1/src/sensing/ws_server.py`) that:
|
||||
4. **Python WebSocket bridge** (`v1/src/sensing/ws_server.py`) that:
|
||||
- Auto-detects ESP32 UDP CSI stream on port 5005 (ADR-018 binary frames)
|
||||
- Falls back to `WindowsWifiCollector` → `SimulatedCollector`
|
||||
- Runs `RssiFeatureExtractor` → `PresenceClassifier` pipeline
|
||||
@@ -80,7 +80,7 @@ Windows WiFi RSSI ───┘ │ │
|
||||
### Created
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `archive/v1/src/sensing/ws_server.py` | Python asyncio WebSocket server with auto-detect collectors |
|
||||
| `v1/src/sensing/ws_server.py` | Python asyncio WebSocket server with auto-detect collectors |
|
||||
| `ui/components/SensingTab.js` | Sensing tab UI with Three.js integration |
|
||||
| `ui/components/gaussian-splats.js` | Custom GLSL Gaussian splat renderer |
|
||||
| `ui/services/sensing.service.js` | WebSocket client with reconnect + simulation fallback |
|
||||
|
||||
@@ -22,7 +22,7 @@ The current Python DensePose backend requires ~2GB+ of dependencies:
|
||||
|
||||
This makes the DensePose backend impractical for edge deployments, CI pipelines, and developer laptops where users only need WiFi sensing + pose estimation.
|
||||
|
||||
Meanwhile, the Rust port at `v2/` already has:
|
||||
Meanwhile, the Rust port at `rust-port/wifi-densepose-rs/` already has:
|
||||
|
||||
- **12 workspace crates** covering core, signal, nn, api, db, config, hardware, wasm, cli, mat, train
|
||||
- **5 RuVector crates** (v2.0.4, published on crates.io) integrated into signal, mat, and train crates
|
||||
@@ -40,8 +40,8 @@ Use the `wifi-densepose-nn` crate with `default-features = ["onnx"]` only. This
|
||||
|
||||
| Component | Rust Crate | Replaces Python |
|
||||
|-----------|-----------|-----------------|
|
||||
| CSI processing | `wifi-densepose-signal::csi_processor` | `archive/v1/src/sensing/feature_extractor.py` |
|
||||
| Motion detection | `wifi-densepose-signal::motion` | `archive/v1/src/sensing/classifier.py` |
|
||||
| CSI processing | `wifi-densepose-signal::csi_processor` | `v1/src/sensing/feature_extractor.py` |
|
||||
| Motion detection | `wifi-densepose-signal::motion` | `v1/src/sensing/classifier.py` |
|
||||
| BVP extraction | `wifi-densepose-signal::bvp` | N/A (new capability) |
|
||||
| Fresnel geometry | `wifi-densepose-signal::fresnel` | N/A (new capability) |
|
||||
| Subcarrier selection | `wifi-densepose-signal::subcarrier_selection` | N/A (new capability) |
|
||||
@@ -143,7 +143,7 @@ The `wifi-densepose-nn::onnx` module loads `.onnx` files directly.
|
||||
|
||||
```bash
|
||||
# Build the Rust workspace (ONNX-only, no libtorch)
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo check --workspace 2>&1
|
||||
|
||||
# Build release binary
|
||||
|
||||
@@ -34,7 +34,7 @@ The `vendor/ruvector` codebase provides a rich set of signal processing primitiv
|
||||
|
||||
### Current Project State
|
||||
|
||||
The Rust port (`v2/`) already contains:
|
||||
The Rust port (`rust-port/wifi-densepose-rs/`) already contains:
|
||||
|
||||
- **`wifi-densepose-signal`**: CSI processing, BVP extraction, phase sanitization, Hampel filter, spectrogram generation, Fresnel geometry, motion detection, subcarrier selection
|
||||
- **`wifi-densepose-sensing-server`**: Axum server receiving ESP32 CSI frames (UDP 5005), WebSocket broadcasting sensing updates, signal field generation, with three data source modes:
|
||||
@@ -108,7 +108,7 @@ ESP32 CSI (UDP:5005) ──▶│ ┌──────────────
|
||||
### Module Structure
|
||||
|
||||
```
|
||||
v2/crates/wifi-densepose-vitals/
|
||||
rust-port/wifi-densepose-rs/crates/wifi-densepose-vitals/
|
||||
├── Cargo.toml
|
||||
└── src/
|
||||
├── lib.rs # Public API and re-exports
|
||||
|
||||
@@ -592,7 +592,7 @@ impl FrameBuilder {
|
||||
### 3.3 Module Structure
|
||||
|
||||
```
|
||||
v2/crates/wifi-densepose-wifiscan/
|
||||
rust-port/wifi-densepose-rs/crates/wifi-densepose-wifiscan/
|
||||
├── Cargo.toml
|
||||
└── src/
|
||||
├── lib.rs # Public API, re-exports
|
||||
|
||||
@@ -699,28 +699,28 @@ let dashboard = container.load_dashboard()?;
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `v2/.../wifi-densepose-train/src/dataset_mmfi.rs` | MM-Fi dataset loader with subcarrier resampling |
|
||||
| `v2/.../wifi-densepose-train/src/dataset_wipose.rs` | Wi-Pose dataset loader |
|
||||
| `v2/.../wifi-densepose-train/src/graph_transformer.rs` | Graph transformer integration |
|
||||
| `v2/.../wifi-densepose-train/src/body_gnn.rs` | GNN body graph reasoning |
|
||||
| `v2/.../wifi-densepose-train/src/adaptation.rs` | SONA LoRA + EWC++ adaptation |
|
||||
| `v2/.../wifi-densepose-train/src/trainer.rs` | Training loop with multi-term loss |
|
||||
| `rust-port/.../wifi-densepose-train/src/dataset_mmfi.rs` | MM-Fi dataset loader with subcarrier resampling |
|
||||
| `rust-port/.../wifi-densepose-train/src/dataset_wipose.rs` | Wi-Pose dataset loader |
|
||||
| `rust-port/.../wifi-densepose-train/src/graph_transformer.rs` | Graph transformer integration |
|
||||
| `rust-port/.../wifi-densepose-train/src/body_gnn.rs` | GNN body graph reasoning |
|
||||
| `rust-port/.../wifi-densepose-train/src/adaptation.rs` | SONA LoRA + EWC++ adaptation |
|
||||
| `rust-port/.../wifi-densepose-train/src/trainer.rs` | Training loop with multi-term loss |
|
||||
| `scripts/generate_densepose_labels.py` | Teacher-student UV label generation |
|
||||
| `scripts/benchmark_inference.py` | Inference latency benchmarking |
|
||||
| `v2/.../wifi-densepose-train/src/rvf_builder.rs` | RVF container build pipeline |
|
||||
| `v2/.../wifi-densepose-train/src/bin/build_rvf.rs` | CLI binary for building `.rvf` containers |
|
||||
| `v2/.../wifi-densepose-train/src/bin/verify_rvf.rs` | CLI binary for verifying `.rvf` containers |
|
||||
| `rust-port/.../wifi-densepose-train/src/rvf_builder.rs` | RVF container build pipeline |
|
||||
| `rust-port/.../wifi-densepose-train/src/bin/build_rvf.rs` | CLI binary for building `.rvf` containers |
|
||||
| `rust-port/.../wifi-densepose-train/src/bin/verify_rvf.rs` | CLI binary for verifying `.rvf` containers |
|
||||
|
||||
### Modified Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `v2/.../wifi-densepose-train/Cargo.toml` | Add ruvector-gnn, graph-transformer, sona, sparse-inference, math, rvf-types, rvf-wire, rvf-manifest, rvf-index, rvf-quant, rvf-crypto, rvf-runtime deps |
|
||||
| `v2/.../wifi-densepose-train/src/model.rs` | Integrate graph transformer + GNN layers |
|
||||
| `v2/.../wifi-densepose-train/src/losses.rs` | Add optimal transport + GNN edge consistency loss terms |
|
||||
| `v2/.../wifi-densepose-train/src/config.rs` | Add training hyperparameters for new components |
|
||||
| `v2/.../sensing-server/Cargo.toml` | Add rvf-runtime, rvf-types, rvf-index, rvf-quant deps |
|
||||
| `v2/.../sensing-server/src/main.rs` | Add `--model` flag, load `.rvf` container, progressive startup, serve embedded dashboard |
|
||||
| `rust-port/.../wifi-densepose-train/Cargo.toml` | Add ruvector-gnn, graph-transformer, sona, sparse-inference, math, rvf-types, rvf-wire, rvf-manifest, rvf-index, rvf-quant, rvf-crypto, rvf-runtime deps |
|
||||
| `rust-port/.../wifi-densepose-train/src/model.rs` | Integrate graph transformer + GNN layers |
|
||||
| `rust-port/.../wifi-densepose-train/src/losses.rs` | Add optimal transport + GNN edge consistency loss terms |
|
||||
| `rust-port/.../wifi-densepose-train/src/config.rs` | Add training hyperparameters for new components |
|
||||
| `rust-port/.../sensing-server/Cargo.toml` | Add rvf-runtime, rvf-types, rvf-index, rvf-quant deps |
|
||||
| `rust-port/.../sensing-server/src/main.rs` | Add `--model` flag, load `.rvf` container, progressive startup, serve embedded dashboard |
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
@@ -371,7 +371,7 @@ ESP32 SRAM budget: 520 KB. Model at INT8: 53-60 KB = 10-12% of SRAM. Ample margi
|
||||
|
||||
### 2.6 Concrete Module Additions
|
||||
|
||||
All new/modified files in `v2/crates/wifi-densepose-sensing-server/src/`:
|
||||
All new/modified files in `rust-port/wifi-densepose-rs/crates/wifi-densepose-sensing-server/src/`:
|
||||
|
||||
#### 2.6.1 `embedding.rs` (NEW, ~450 lines)
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ Implement a **macOS CoreWLAN sensing adapter** as a Swift helper binary + Rust a
|
||||
|
||||
### 3.2 Swift Helper Binary
|
||||
|
||||
**File:** `v2/tools/macos-wifi-scan/main.swift`
|
||||
**File:** `rust-port/wifi-densepose-rs/tools/macos-wifi-scan/main.swift`
|
||||
|
||||
```swift
|
||||
// Modes:
|
||||
|
||||
@@ -232,10 +232,10 @@ python scripts/provision.py --port COM7 \
|
||||
|
||||
| Component | File | Purpose |
|
||||
|-----------|------|---------|
|
||||
| Reference signal | `archive/v1/data/proof/sample_csi_data.json` | 1,000 synthetic CSI frames, seed=42 |
|
||||
| Generator | `archive/v1/data/proof/generate_reference_signal.py` | Deterministic multipath model |
|
||||
| Verifier | `archive/v1/data/proof/verify.py` | SHA-256 hash comparison |
|
||||
| Expected hash | `archive/v1/data/proof/expected_features.sha256` | `0b82bd45...` |
|
||||
| Reference signal | `v1/data/proof/sample_csi_data.json` | 1,000 synthetic CSI frames, seed=42 |
|
||||
| Generator | `v1/data/proof/generate_reference_signal.py` | Deterministic multipath model |
|
||||
| Verifier | `v1/data/proof/verify.py` | SHA-256 hash comparison |
|
||||
| Expected hash | `v1/data/proof/expected_features.sha256` | `0b82bd45...` |
|
||||
|
||||
**Audit-time result:** PASS. Hash regenerated with numpy 2.4.2 + scipy 1.17.1. Pipeline hash: `8c0680d7d285739ea9597715e84959d9c356c87ee3ad35b5f1e69a4ca41151c6`.
|
||||
|
||||
|
||||
@@ -198,16 +198,16 @@ When a `.rvf` model is loaded:
|
||||
### New Files
|
||||
- `ui/components/ModelPanel.js` — Model library, inspector, load/unload controls
|
||||
- `ui/components/TrainingPanel.js` — Recording controls, training progress, metric charts
|
||||
- `v2/.../sensing-server/src/recording.rs` — CSI recording API handlers
|
||||
- `v2/.../sensing-server/src/training_api.rs` — Training API handlers + WS progress stream
|
||||
- `v2/.../sensing-server/src/model_manager.rs` — Model loading, hot-swap, 32LoRA activation
|
||||
- `rust-port/.../sensing-server/src/recording.rs` — CSI recording API handlers
|
||||
- `rust-port/.../sensing-server/src/training_api.rs` — Training API handlers + WS progress stream
|
||||
- `rust-port/.../sensing-server/src/model_manager.rs` — Model loading, hot-swap, 32LoRA activation
|
||||
- `data/models/` — Default model storage directory
|
||||
|
||||
### Modified Files
|
||||
- `v2/.../sensing-server/src/main.rs` — Wire recording, training, and model APIs
|
||||
- `v2/.../train/src/trainer.rs` — Add WebSocket progress callback, LoRA training mode
|
||||
- `v2/.../train/src/dataset.rs` — MM-Fi and Wi-Pose dataset loaders
|
||||
- `v2/.../nn/src/onnx.rs` — LoRA weight injection, INT8 quantization support
|
||||
- `rust-port/.../sensing-server/src/main.rs` — Wire recording, training, and model APIs
|
||||
- `rust-port/.../train/src/trainer.rs` — Add WebSocket progress callback, LoRA training mode
|
||||
- `rust-port/.../train/src/dataset.rs` — MM-Fi and Wi-Pose dataset loaders
|
||||
- `rust-port/.../nn/src/onnx.rs` — LoRA weight injection, INT8 quantization support
|
||||
- `ui/components/LiveDemoTab.js` — Model selector, LoRA dropdown, A/B spsplit view
|
||||
- `ui/components/SettingsPanel.js` — Model and training configuration sections
|
||||
- `ui/components/PoseDetectionCanvas.js` — Pose trail rendering, confidence heatmap overlay
|
||||
|
||||
@@ -24,7 +24,7 @@ No on-device processing. CSI frames streamed as-is (magic `0xC5110001`).
|
||||
- Phase extraction and unwrapping from I/Q pairs
|
||||
- Welford running variance per subcarrier
|
||||
- Top-K subcarrier selection by variance
|
||||
- Delta compression (XOR + RLE) for 30-50% bandwidth reduction (magic `0xC5110005`, reassigned from `0xC5110003` by ADR-069)
|
||||
- Delta compression (XOR + RLE) for 30-50% bandwidth reduction (magic `0xC5110003`)
|
||||
|
||||
### Tier 2 — Full Edge Intelligence
|
||||
All of Tier 1, plus:
|
||||
@@ -50,7 +50,7 @@ Core 0 (WiFi) Core 1 (DSP)
|
||||
│ Multi-person clustering │
|
||||
│ Delta compression │
|
||||
│ ──▶ UDP vitals (0xC5110002)│
|
||||
│ ──▶ UDP compressed (0x05) │
|
||||
│ ──▶ UDP compressed (0x03) │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -73,11 +73,11 @@ Core 0 (WiFi) Core 1 (DSP)
|
||||
| 24-27 | u32 LE | Timestamp (ms since boot) |
|
||||
| 28-31 | u32 LE | Reserved |
|
||||
|
||||
**Compressed Frame (magic `0xC5110005`, reassigned from `0xC5110003` by ADR-069)**:
|
||||
**Compressed Frame (magic `0xC5110003`)**:
|
||||
|
||||
| Offset | Type | Field |
|
||||
|--------|------|-------|
|
||||
| 0-3 | u32 LE | Magic `0xC5110005` |
|
||||
| 0-3 | u32 LE | Magic `0xC5110003` |
|
||||
| 4 | u8 | Node ID |
|
||||
| 5 | u8 | WiFi channel |
|
||||
| 6-7 | u16 LE | Original I/Q length |
|
||||
@@ -128,7 +128,7 @@ All configurable via `provision.py --edge-tier 2 --pres-thresh 0.05 ...`
|
||||
- `firmware/esp32-csi-node/main/edge_processing.h` — Types and API
|
||||
- `firmware/esp32-csi-node/main/ota_update.c/h` — HTTP OTA endpoint
|
||||
- `firmware/esp32-csi-node/main/power_mgmt.c/h` — Power management
|
||||
- `v2/.../wifi-densepose-sensing-server/src/main.rs` — Vitals parser + REST endpoint
|
||||
- `rust-port/.../wifi-densepose-sensing-server/src/main.rs` — Vitals parser + REST endpoint
|
||||
- `scripts/provision.py` — Edge config CLI arguments
|
||||
- `.github/workflows/firmware-ci.yml` — CI build + size gate (updated to 950 KB for Tier 3)
|
||||
|
||||
|
||||
@@ -164,8 +164,8 @@ Core 1 (DSP Task)
|
||||
- `firmware/esp32-csi-node/main/wasm_runtime.c/h` — Runtime host with 12 API bindings + manifest
|
||||
- `firmware/esp32-csi-node/main/wasm_upload.c/h` — HTTP REST endpoints (RVF-aware)
|
||||
- `firmware/esp32-csi-node/main/rvf_parser.c/h` — RVF container parser and verifier
|
||||
- `v2/.../wifi-densepose-wasm-edge/` — Rust WASM crate (gesture, coherence, adversarial, rvf, occupancy, vital_trend, intrusion)
|
||||
- `v2/.../wifi-densepose-sensing-server/src/main.rs` — `0xC5110004` parser
|
||||
- `rust-port/.../wifi-densepose-wasm-edge/` — Rust WASM crate (gesture, coherence, adversarial, rvf, occupancy, vital_trend, intrusion)
|
||||
- `rust-port/.../wifi-densepose-sensing-server/src/main.rs` — `0xC5110004` parser
|
||||
- `docs/adr/ADR-039-esp32-edge-intelligence.md` — Updated with Tier 3 reference
|
||||
|
||||
---
|
||||
|
||||
@@ -289,7 +289,7 @@ Startup creates `data/models/` and `data/recordings/` directories and populates
|
||||
|
||||
```bash
|
||||
# 1. Start sensing server with auto source (simulated fallback)
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo run -p wifi-densepose-sensing-server -- --http-port 3000 --source auto
|
||||
|
||||
# 2. Verify model endpoints return 200
|
||||
@@ -312,11 +312,11 @@ curl -s http://localhost:3000/api/v1/models/lora/profiles | jq '.'
|
||||
# Navigate to http://localhost:3000/ui/
|
||||
|
||||
# 7. Run mobile tests
|
||||
cd ../ui/mobile
|
||||
cd ../../ui/mobile
|
||||
npx jest --no-coverage
|
||||
|
||||
# 8. Run Rust workspace tests (must pass, 1031+ tests)
|
||||
cd ../../v2
|
||||
cd ../../rust-port/wifi-densepose-rs
|
||||
cargo test --workspace --no-default-features
|
||||
```
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
# ADR-044: Geospatial Satellite Integration
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
RuView generates real-time 3D point clouds from camera + WiFi CSI, but these exist in a local coordinate frame with no geographic reference. Integrating free satellite imagery, terrain elevation, and map data provides environmental context that enables the ruOS brain to reason about the physical world beyond the room.
|
||||
|
||||
## Decision
|
||||
|
||||
### Data Sources (all free, no API keys)
|
||||
| Source | Data | Resolution | Update | Format |
|
||||
|--------|------|-----------|--------|--------|
|
||||
| EOX Sentinel-2 Cloudless | Satellite tiles | 10m | Static mosaic | XYZ/JPEG |
|
||||
| SRTM GL1 (NASA) | Elevation/DEM | 30m (1-arcsec) | Static | Binary HGT |
|
||||
| Overpass API (OSM) | Buildings, roads | Vector | Real-time | JSON |
|
||||
| ip-api.com | IP geolocation | ~1km | Per-request | JSON |
|
||||
| Sentinel-2 STAC | Temporal satellite | 10m | Every 5 days | COG/STAC |
|
||||
| Open Meteo | Weather | Point | Hourly | JSON |
|
||||
|
||||
### Architecture
|
||||
Pure Rust implementation in `wifi-densepose-geo` crate. No GDAL/PROJ/GEOS — coordinate transforms implemented directly (~250 LOC). Tile caching on disk at `~/.local/share/ruview/geo-cache/`.
|
||||
|
||||
### Coordinate System
|
||||
- WGS84 for geographic coordinates
|
||||
- ENU (East-North-Up) as the bridge between local sensor frame and world
|
||||
- Local sensor frame: camera origin, +Z forward, +Y up
|
||||
|
||||
### Temporal Awareness
|
||||
Nightly scheduled fetch of Sentinel-2 latest imagery + OSM diffs + weather.
|
||||
Changes detected via image comparison and stored as brain memories for
|
||||
contrastive learning.
|
||||
|
||||
### Brain Integration
|
||||
Geospatial context stored as brain memories:
|
||||
- `spatial-geo`: location, elevation, nearby landmarks
|
||||
- `spatial-change`: detected changes in satellite/OSM data
|
||||
- `spatial-weather`: current conditions + forecast
|
||||
- `spatial-season`: vegetation index, snow cover, seasonal patterns
|
||||
- `spatial-local`: hyperlocal web context from Common Crawl WET
|
||||
|
||||
### Extended Data Sources (via ruvector WET/Common Crawl)
|
||||
| Source | Data | Use |
|
||||
|--------|------|-----|
|
||||
| Common Crawl WET | Web text near location | Local business info, reviews, events |
|
||||
| Wikidata | Structured knowledge | Building names, POI descriptions |
|
||||
| NASA FIRMS | Active fire (3-hour) | Safety alerts |
|
||||
| USGS Earthquakes | Seismic events | Safety context |
|
||||
| OpenAQ | Air quality (PM2.5) | Environmental health |
|
||||
| Overture Maps | Building footprints (Meta/MS) | Higher quality than OSM |
|
||||
|
||||
The ruvector brain server has existing `web_ingest` + Common Crawl support.
|
||||
WET files filtered by geographic URL patterns provide hyperlocal context.
|
||||
|
||||
## Consequences
|
||||
### Positive
|
||||
- Agent gains environmental awareness beyond the room
|
||||
- Temporal data enables seasonal calibration of CSI sensing
|
||||
- Change detection finds construction, vegetation, weather effects
|
||||
- All data sources are genuinely free with no API keys
|
||||
|
||||
### Negative
|
||||
- Initial data fetch requires internet (~2MB tiles + ~25MB DEM)
|
||||
- Cached data becomes stale (mitigated by nightly refresh)
|
||||
- IP geolocation has ~1km accuracy (mitigated by manual override)
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# ADR-050: Provisioning Tool Enhancements
|
||||
# ADR-044: Provisioning Tool Enhancements
|
||||
|
||||
**Status**: Proposed
|
||||
**Date**: 2026-03-03
|
||||
@@ -108,7 +108,7 @@ Remove duplicated platform-detection logic from `ws_server.py` and `install.sh`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
1. Add `create_collector()` and `BaseCollector.is_available()` to `archive/v1/src/sensing/rssi_collector.py`
|
||||
1. Add `create_collector()` and `BaseCollector.is_available()` to `v1/src/sensing/rssi_collector.py`
|
||||
2. Refactor `ws_server.py` `_init_collector()` to call `create_collector()`
|
||||
3. Update `install.sh` `detect_wifi_hardware()` to use shared detection logic
|
||||
4. Add unit tests for each platform path (mock `/proc/net/wireless` presence/absence)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# ADR-051: Sensing Server Decomposition — main.rs God Object Breakup
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | Proposed |
|
||||
| Date | 2026-03-06 |
|
||||
| Deciders | ruv |
|
||||
| Depends on | ADR-050 (Quality Engineering — Sprint 2) |
|
||||
| Issue | [#174](https://github.com/ruvnet/RuView/issues/174) |
|
||||
|
||||
## Context
|
||||
|
||||
`sensing-server/src/main.rs` is 3,765 lines with cyclomatic complexity ~65. It contains 12 structs, 60+ functions, 10 constants, and a 37-field `AppStateInner` god object. This violates the project's 500-line file limit (CLAUDE.md) and makes unit testing individual components impossible.
|
||||
|
||||
The file mixes concerns:
|
||||
- CLI argument parsing and server bootstrap
|
||||
- HTTP route handlers (health, models, recordings, training, pose, vitals)
|
||||
- WebSocket upgrade and client management
|
||||
- UDP CSI frame receiver and parser
|
||||
- Signal processing pipeline (feature extraction, classification, smoothing)
|
||||
- Simulated data generator
|
||||
- Windows WiFi scanning integration
|
||||
- Pose estimation from WiFi signals
|
||||
- Vital sign smoothing and filtering
|
||||
- Model/recording file management
|
||||
|
||||
## Decision
|
||||
|
||||
Decompose `main.rs` into 14 focused modules. Each module owns its types, constants, and functions. `main.rs` retains only CLI parsing, state initialization, router construction, and server startup (~250 lines).
|
||||
|
||||
### Module Extraction Plan
|
||||
|
||||
| Module | Source Lines | Contents | Target Size |
|
||||
|--------|-------------|----------|-------------|
|
||||
| `cli.rs` | 59-152 | `Args` struct, CLI parsing | ~100 |
|
||||
| `state.rs` | 154-370 | `AppStateInner`, all DTOs (`Esp32Frame`, `SensingUpdate`, `NodeInfo`, etc.), `SharedState` type alias | ~220 |
|
||||
| `signal.rs` | 542-890 | `generate_signal_field()`, `estimate_breathing_rate_hz()`, `compute_subcarrier_variances()`, `extract_features_from_frame()`, `raw_classify()` | ~350 |
|
||||
| `smoothing.rs` | 886-1060 | Classification smoothing, vital sign smoothing, `trimmed_mean()`, constants | ~180 |
|
||||
| `routes_health.rs` | 1660-2005 | `/health/*`, `/api/v1/info` endpoints | ~350 |
|
||||
| `routes_model.rs` | 2058-2230 | `/api/v1/models/*`, LoRA profiles, `scan_model_files()` | ~180 |
|
||||
| `routes_recording.rs` | 2233-2440 | `/api/v1/recording/*`, `scan_recording_files()` | ~210 |
|
||||
| `routes_training.rs` | 2443-2560 | `/api/v1/train/*`, `/api/v1/adaptive/*` | ~120 |
|
||||
| `routes_sensing.rs` | 2562-2710 | Vital signs, edge vitals, WASM events, model info, SONA endpoints | ~150 |
|
||||
| `routes_pose.rs` | 1701-1930, 2007-2055 | Pose estimation, `derive_single_person_pose()`, pose/stats/zones endpoints | ~280 |
|
||||
| `websocket.rs` | 1492-1660 | WS upgrade handlers, `handle_ws_client()`, `handle_ws_pose_client()` | ~170 |
|
||||
| `udp_receiver.rs` | 2725-2890 | UDP CSI frame receiver task, frame parsing | ~170 |
|
||||
| `data_sources.rs` | 1063-1465, 2888-3020 | Windows WiFi task, simulated data task, `probe_windows_wifi()`, `parse_netsh_interfaces_output()` | ~400 |
|
||||
| `router.rs` | (new) | `build_router()` function assembling all routes | ~80 |
|
||||
|
||||
### Extraction Order (6 Phases)
|
||||
|
||||
1. **Phase 1**: `cli.rs` + `state.rs` — Zero behavioral change, just move types
|
||||
2. **Phase 2**: `signal.rs` + `smoothing.rs` — Pure functions, easy to test
|
||||
3. **Phase 3**: `routes_health.rs` + `routes_model.rs` + `routes_recording.rs` — Stateless-ish handlers
|
||||
4. **Phase 4**: `routes_training.rs` + `routes_sensing.rs` + `routes_pose.rs` — Remaining HTTP handlers
|
||||
5. **Phase 5**: `websocket.rs` + `udp_receiver.rs` + `data_sources.rs` — Async tasks
|
||||
6. **Phase 6**: `router.rs` — Assemble all routes, slim `main.rs` to ~250 lines
|
||||
|
||||
### State Refactoring
|
||||
|
||||
`AppStateInner` (37 fields) will be split into domain-specific sub-states:
|
||||
|
||||
```rust
|
||||
pub struct AppStateInner {
|
||||
pub config: ServerConfig, // CLI args, ports, paths
|
||||
pub sensing: SensingState, // CSI frames, features, classification
|
||||
pub vitals: VitalsState, // Vital sign buffers, smoothing state
|
||||
pub models: ModelState, // Active model, discovered models, LoRA
|
||||
pub recording: RecordingState, // Active recording, file handles
|
||||
pub training: TrainingState, // Training status, adaptive model
|
||||
pub pose: PoseState, // Person detections, pose history
|
||||
pub broadcast_tx: broadcast::Sender<SensingUpdate>,
|
||||
}
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Each module is independently unit-testable
|
||||
- No file exceeds 500 lines
|
||||
- Domain boundaries are explicit (state sub-structs)
|
||||
- New developers can find code by domain
|
||||
- Merge conflict surface reduced (parallel module edits)
|
||||
|
||||
### Negative
|
||||
|
||||
- Large refactor with ~3,700 lines touched — high merge conflict risk
|
||||
- `pub(crate)` visibility needed for cross-module state access
|
||||
- Some functions share mutable state, requiring careful `&mut` threading
|
||||
|
||||
### Neutral
|
||||
|
||||
- No behavioral change — all endpoints, WebSocket, UDP behavior stays identical
|
||||
- Existing integration tests (if any) continue to pass unchanged
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
1. Each phase is a separate commit for easy revert
|
||||
2. Run `cargo test` and `cargo check` after each phase
|
||||
3. Use `pub(crate)` for internal types, keep public API surface minimal
|
||||
4. Add `#[cfg(test)] mod tests` to each new module with at least smoke tests
|
||||
5. Consider adding `tower` middleware for auth (Sprint 1 remaining item) during Phase 3
|
||||
|
||||
## References
|
||||
|
||||
- ADR-050: Quality Engineering Response (Sprint 2 plan)
|
||||
- Issue #170: Quality Engineering Analysis
|
||||
- CLAUDE.md: 500-line file limit rule
|
||||
@@ -1,621 +0,0 @@
|
||||
# ADR-052 Appendix: DDD Bounded Contexts — Tauri Desktop Frontend
|
||||
|
||||
This document maps out the domain model for the RuView Tauri desktop application
|
||||
described in ADR-052. It defines bounded contexts, their aggregates, entities,
|
||||
value objects, and the domain events flowing between them.
|
||||
|
||||
## Context Map
|
||||
|
||||
```
|
||||
+-------------------+ +---------------------+ +--------------------+
|
||||
| | | | | |
|
||||
| Device Discovery |------>| Firmware Management |------>| Configuration / |
|
||||
| | | | | Provisioning |
|
||||
+-------------------+ +---------------------+ +--------------------+
|
||||
| | |
|
||||
| | |
|
||||
v v v
|
||||
+-------------------+ +---------------------+ +--------------------+
|
||||
| | | | | |
|
||||
| Sensing Pipeline |<------| Edge Module | | Visualization |
|
||||
| | | (WASM) | | |
|
||||
+-------------------+ +---------------------+ +--------------------+
|
||||
|
||||
Relationship types:
|
||||
-----> Upstream/Downstream (upstream publishes events, downstream consumes)
|
||||
<----- Conformist (downstream conforms to upstream's model)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Device Discovery Context
|
||||
|
||||
**Purpose**: Find, identify, and monitor ESP32 CSI nodes on the local network.
|
||||
|
||||
**Upstream of**: Firmware Management, Configuration, Sensing Pipeline, Visualization
|
||||
|
||||
### Aggregates
|
||||
|
||||
#### `NodeRegistry` (Aggregate Root)
|
||||
|
||||
Maintains the authoritative list of all known nodes. Merges discovery results
|
||||
from multiple strategies (mDNS, UDP probe, HTTP sweep) and deduplicates by MAC
|
||||
address.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `nodes` | `Map<MacAddress, Node>` | All discovered nodes keyed by MAC |
|
||||
| `scan_state` | `ScanState` | Idle, Scanning, Error |
|
||||
| `last_scan` | `DateTime<Utc>` | Timestamp of last completed scan |
|
||||
|
||||
**Invariant**: No two nodes may share the same MAC address. If a node is
|
||||
discovered via multiple strategies, the most recent data wins.
|
||||
|
||||
**Persistence**: The registry is persisted to `~/.ruview/nodes.db` (SQLite via
|
||||
`rusqlite`). On startup, all previously known nodes are loaded as `Offline` and
|
||||
reconciled against a fresh discovery scan. This means the app **remembers the
|
||||
mesh** across restarts — critical for field deployments where nodes may be
|
||||
temporarily powered off.
|
||||
|
||||
#### `Node` (Entity)
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `mac` | `MacAddress` (VO) | IEEE 802.11 MAC address (unique identity) |
|
||||
| `ip` | `IpAddr` | Current IP address (may change on DHCP renewal) |
|
||||
| `hostname` | `Option<String>` | mDNS hostname |
|
||||
| `node_id` | `u8` | NVS-provisioned node ID |
|
||||
| `firmware_version` | `Option<SemVer>` | Firmware version string |
|
||||
| `health` | `HealthStatus` (VO) | Online / Offline / Degraded |
|
||||
| `discovery_method` | `DiscoveryMethod` (VO) | How this node was found |
|
||||
| `last_seen` | `DateTime<Utc>` | Last successful contact |
|
||||
| `tdm_config` | `Option<TdmConfig>` (VO) | TDM slot assignment |
|
||||
| `edge_tier` | `Option<u8>` | Edge processing tier (0/1/2) |
|
||||
|
||||
### Value Objects
|
||||
|
||||
- `MacAddress` — 6-byte hardware address, formatted as `AA:BB:CC:DD:EE:FF`
|
||||
- `HealthStatus` — enum: `Online`, `Offline`, `Degraded(reason: String)`
|
||||
- `DiscoveryMethod` — enum: `Mdns`, `UdpProbe`, `HttpSweep`, `Manual`
|
||||
- `TdmConfig` — `{ slot_index: u8, total_nodes: u8 }`
|
||||
- `SemVer` — semantic version `major.minor.patch`
|
||||
|
||||
### Domain Events
|
||||
|
||||
| Event | Payload | Consumers |
|
||||
|-------|---------|-----------|
|
||||
| `NodeDiscovered` | `{ node: Node }` | Firmware Mgmt (check for updates), Visualization (add to mesh graph) |
|
||||
| `NodeWentOffline` | `{ mac: MacAddress, last_seen: DateTime }` | Visualization (gray out node), Sensing Pipeline (remove from active set) |
|
||||
| `NodeCameOnline` | `{ node: Node }` | Visualization (restore node), Sensing Pipeline (re-add) |
|
||||
| `NodeHealthChanged` | `{ mac: MacAddress, old: HealthStatus, new: HealthStatus }` | Visualization (update indicator) |
|
||||
| `ScanCompleted` | `{ found: usize, new: usize, lost: usize }` | Dashboard (update summary) |
|
||||
|
||||
### Anti-Corruption Layer
|
||||
|
||||
When receiving data from the ESP32 OTA status endpoint (`GET /ota/status`), the
|
||||
response format is owned by the firmware and may change across firmware versions.
|
||||
The ACL translates the raw JSON response into `Node` entity fields:
|
||||
|
||||
```rust
|
||||
/// ACL: Translate ESP32 OTA status response to Node fields.
|
||||
fn translate_ota_status(raw: &serde_json::Value) -> Result<NodePatch, AclError> {
|
||||
NodePatch {
|
||||
firmware_version: raw["version"].as_str().map(SemVer::parse).transpose()?,
|
||||
uptime_secs: raw["uptime_s"].as_u64(),
|
||||
free_heap: raw["free_heap"].as_u64(),
|
||||
// Firmware may add fields in future versions — unknown fields are ignored
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Firmware Management Context
|
||||
|
||||
**Purpose**: Flash, update, and verify firmware on ESP32 nodes.
|
||||
|
||||
**Upstream of**: Configuration (a fresh flash triggers provisioning)
|
||||
**Downstream of**: Device Discovery (needs node list and serial port info)
|
||||
|
||||
### Aggregates
|
||||
|
||||
#### `FlashSession` (Aggregate Root)
|
||||
|
||||
Represents a single firmware flashing operation from start to completion. Each
|
||||
session has a lifecycle: Created -> Connecting -> Erasing -> Writing -> Verifying ->
|
||||
Completed | Failed.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | `Uuid` | Session identifier |
|
||||
| `port` | `SerialPort` (VO) | Target serial port |
|
||||
| `firmware` | `FirmwareBinary` (Entity) | The binary being flashed |
|
||||
| `chip` | `ChipType` (VO) | Target chip (ESP32, ESP32-S3, ESP32-C3) |
|
||||
| `phase` | `FlashPhase` (VO) | Current phase of the flash operation |
|
||||
| `progress` | `Progress` (VO) | Bytes written / total, speed |
|
||||
| `started_at` | `DateTime<Utc>` | When the session started |
|
||||
| `error` | `Option<String>` | Error message if failed |
|
||||
|
||||
**Invariant**: Only one `FlashSession` may be active per serial port at a time.
|
||||
|
||||
#### `FirmwareBinary` (Entity)
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `path` | `PathBuf` | Filesystem path to the `.bin` file |
|
||||
| `size_bytes` | `u64` | Binary size |
|
||||
| `version` | `Option<SemVer>` | Extracted from ESP32 image header |
|
||||
| `chip_type` | `Option<ChipType>` | Detected from image magic bytes |
|
||||
| `checksum` | `Sha256Hash` (VO) | SHA-256 of the binary |
|
||||
|
||||
#### `OtaSession` (Aggregate Root)
|
||||
|
||||
Represents an over-the-air firmware update to a running node.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | `Uuid` | Session identifier |
|
||||
| `target_node` | `MacAddress` | Target node MAC |
|
||||
| `target_ip` | `IpAddr` | Target node IP |
|
||||
| `firmware` | `FirmwareBinary` | The binary being pushed |
|
||||
| `psk` | `Option<SecureString>` | PSK for authentication (ADR-050) |
|
||||
| `phase` | `OtaPhase` | Uploading / Rebooting / Verifying / Done / Failed |
|
||||
| `progress` | `Progress` | Upload progress |
|
||||
|
||||
#### `BatchOtaSession` (Aggregate Root)
|
||||
|
||||
Coordinates rolling firmware updates across multiple mesh nodes. Prevents all
|
||||
nodes from rebooting simultaneously, which would collapse the sensing network.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | `Uuid` | Batch session identifier |
|
||||
| `firmware` | `FirmwareBinary` | The binary being deployed |
|
||||
| `strategy` | `OtaStrategy` | `Sequential`, `TdmSafe`, `Parallel` |
|
||||
| `max_concurrent` | `usize` | Max nodes updating at once |
|
||||
| `batch_delay_secs` | `u64` | Delay between batches |
|
||||
| `fail_fast` | `bool` | Abort remaining on first failure |
|
||||
| `node_states` | `Map<MacAddress, BatchNodeState>` | Per-node progress |
|
||||
|
||||
**Invariant**: In `TdmSafe` mode, adjacent TDM slots are never updated
|
||||
concurrently. Even-slot nodes update first, then odd-slot nodes.
|
||||
|
||||
**Lifecycle**: `Planning → InProgress → Completed | PartialFailure | Aborted`
|
||||
|
||||
- `BatchNodeState` — enum: `Queued`, `Uploading(Progress)`, `Rebooting`, `Verifying`, `Done`, `Failed(String)`, `Skipped`
|
||||
- `OtaStrategy` — enum:
|
||||
- `Sequential` — one node at a time, wait for rejoin
|
||||
- `TdmSafe` — update non-adjacent slots to maintain sensing coverage
|
||||
- `Parallel` — all at once (development only)
|
||||
|
||||
### Value Objects
|
||||
|
||||
- `SerialPort` — `{ name: String, vid: u16, pid: u16, manufacturer: Option<String> }`
|
||||
- `ChipType` — enum: `Esp32`, `Esp32s3`, `Esp32c3`
|
||||
- `FlashPhase` — enum: `Connecting`, `Erasing`, `Writing`, `Verifying`, `Completed`, `Failed`
|
||||
- `OtaPhase` — enum: `Uploading`, `Rebooting`, `Verifying`, `Completed`, `Failed`
|
||||
- `Progress` — `{ bytes_done: u64, bytes_total: u64, speed_bps: u64 }`
|
||||
- `Sha256Hash` — 32-byte hash
|
||||
- `SecureString` — zeroized-on-drop string for PSK tokens
|
||||
|
||||
### Domain Events
|
||||
|
||||
| Event | Payload | Consumers |
|
||||
|-------|---------|-----------|
|
||||
| `FlashStarted` | `{ session_id, port, firmware_version }` | UI (show progress) |
|
||||
| `FlashProgress` | `{ session_id, phase, progress }` | UI (update progress bar) |
|
||||
| `FlashCompleted` | `{ session_id, duration_secs }` | Configuration (trigger provisioning prompt) |
|
||||
| `FlashFailed` | `{ session_id, error }` | UI (show error) |
|
||||
| `OtaStarted` | `{ session_id, target_mac, firmware_version }` | Discovery (mark node as updating) |
|
||||
| `OtaCompleted` | `{ session_id, target_mac, new_version }` | Discovery (refresh node info) |
|
||||
| `OtaFailed` | `{ session_id, target_mac, error }` | UI (show error) |
|
||||
| `BatchOtaStarted` | `{ batch_id, strategy, node_count }` | UI (show batch progress) |
|
||||
| `BatchNodeUpdated` | `{ batch_id, mac, state }` | UI (update per-node status), Discovery (refresh) |
|
||||
| `BatchOtaCompleted` | `{ batch_id, succeeded, failed, skipped }` | UI (show summary), Discovery (full rescan) |
|
||||
|
||||
### Anti-Corruption Layer
|
||||
|
||||
The `espflash` crate has its own error types and progress reporting model. The
|
||||
ACL translates these into domain events:
|
||||
|
||||
```rust
|
||||
/// ACL: Translate espflash progress callbacks to domain FlashProgress events.
|
||||
impl From<espflash::ProgressCallbackMessage> for FlashProgress {
|
||||
fn from(msg: espflash::ProgressCallbackMessage) -> Self {
|
||||
match msg {
|
||||
espflash::ProgressCallbackMessage::Connecting => FlashProgress {
|
||||
phase: FlashPhase::Connecting,
|
||||
progress: Progress::indeterminate(),
|
||||
},
|
||||
espflash::ProgressCallbackMessage::Erasing { addr, total } => FlashProgress {
|
||||
phase: FlashPhase::Erasing,
|
||||
progress: Progress::new(addr as u64, total as u64),
|
||||
},
|
||||
// ... etc
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Configuration / Provisioning Context
|
||||
|
||||
**Purpose**: Manage NVS configuration for ESP32 nodes — WiFi credentials, network
|
||||
targets, TDM mesh settings, edge intelligence parameters, WASM security keys.
|
||||
|
||||
**Downstream of**: Device Discovery (needs serial port), Firmware Management (post-flash provisioning)
|
||||
|
||||
### Aggregates
|
||||
|
||||
#### `ProvisioningSession` (Aggregate Root)
|
||||
|
||||
Represents a single NVS write or read operation on a connected ESP32.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | `Uuid` | Session identifier |
|
||||
| `port` | `SerialPort` (VO) | Target serial port |
|
||||
| `config` | `NodeConfig` (Entity) | Configuration to write |
|
||||
| `direction` | `Direction` | Read or Write |
|
||||
| `phase` | `ProvisionPhase` | Generating / Flashing / Verifying / Done |
|
||||
|
||||
#### `NodeConfig` (Entity)
|
||||
|
||||
The full set of NVS key-value pairs for a single node. Maps directly to the
|
||||
firmware's `nvs_config_t` struct (see `firmware/esp32-csi-node/main/nvs_config.h`).
|
||||
|
||||
| Field | Type | NVS Key | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `wifi_ssid` | `Option<String>` | `ssid` | WiFi SSID |
|
||||
| `wifi_password` | `Option<SecureString>` | `password` | WiFi password |
|
||||
| `target_ip` | `Option<IpAddr>` | `target_ip` | Aggregator IP |
|
||||
| `target_port` | `Option<u16>` | `target_port` | Aggregator UDP port |
|
||||
| `node_id` | `Option<u8>` | `node_id` | Node identifier |
|
||||
| `tdm_slot` | `Option<u8>` | `tdm_slot` | TDM slot index |
|
||||
| `tdm_total` | `Option<u8>` | `tdm_nodes` | Total TDM nodes |
|
||||
| `edge_tier` | `Option<u8>` | `edge_tier` | Processing tier |
|
||||
| `hop_count` | `Option<u8>` | `hop_count` | Channel hop count |
|
||||
| `channel_list` | `Option<Vec<u8>>` | `chan_list` | Channel sequence |
|
||||
| `dwell_ms` | `Option<u32>` | `dwell_ms` | Hop dwell time |
|
||||
| `power_duty` | `Option<u8>` | `power_duty` | Power duty cycle |
|
||||
| `presence_thresh` | `Option<u16>` | `pres_thresh` | Presence threshold |
|
||||
| `fall_thresh` | `Option<u16>` | `fall_thresh` | Fall detection threshold |
|
||||
| `vital_window` | `Option<u16>` | `vital_win` | Vital sign window |
|
||||
| `vital_interval_ms` | `Option<u16>` | `vital_int` | Vital sign interval |
|
||||
| `top_k_count` | `Option<u8>` | `subk_count` | Top-K subcarriers |
|
||||
| `wasm_max_modules` | `Option<u8>` | `wasm_max` | Max WASM modules |
|
||||
| `wasm_verify` | `Option<bool>` | `wasm_verify` | Require WASM signature |
|
||||
| `wasm_pubkey` | `Option<[u8; 32]>` | `wasm_pubkey` | Ed25519 public key |
|
||||
| `ota_psk` | `Option<SecureString>` | `ota_psk` | OTA pre-shared key |
|
||||
|
||||
**Invariant**: `tdm_slot < tdm_total` when both are set.
|
||||
**Invariant**: `channel_list.len() == hop_count` when both are set.
|
||||
**Invariant**: `10 <= power_duty <= 100`.
|
||||
|
||||
#### `MeshConfig` (Entity)
|
||||
|
||||
A mesh-level configuration that generates per-node `NodeConfig` instances.
|
||||
Corresponds to ADR-044 Phase 2 (config file provisioning).
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `common` | `NodeConfig` | Shared settings (WiFi, target IP, edge tier) |
|
||||
| `nodes` | `Vec<MeshNodeEntry>` | Per-node overrides (port, node_id, tdm_slot) |
|
||||
|
||||
```rust
|
||||
pub struct MeshNodeEntry {
|
||||
pub port: String,
|
||||
pub node_id: u8,
|
||||
pub tdm_slot: u8,
|
||||
// All other fields inherited from common
|
||||
}
|
||||
```
|
||||
|
||||
**Invariant**: `tdm_total` is automatically computed as `nodes.len()`.
|
||||
|
||||
### Value Objects
|
||||
|
||||
- `ProvisionPhase` — enum: `Generating`, `Flashing`, `Verifying`, `Completed`, `Failed`
|
||||
- `Direction` — enum: `Read`, `Write`
|
||||
- `Preset` — enum: `Basic`, `Vitals`, `Mesh3`, `Mesh6Vitals` (ADR-044 Phase 3)
|
||||
|
||||
### Domain Events
|
||||
|
||||
| Event | Payload | Consumers |
|
||||
|-------|---------|-----------|
|
||||
| `NodeProvisioned` | `{ port, node_id, config_summary }` | Discovery (trigger re-scan), UI (show success) |
|
||||
| `NvsReadCompleted` | `{ port, config: NodeConfig }` | UI (populate form) |
|
||||
| `ProvisionFailed` | `{ port, error }` | UI (show error) |
|
||||
| `MeshProvisionStarted` | `{ node_count }` | UI (show batch progress) |
|
||||
| `MeshProvisionCompleted` | `{ success_count, fail_count }` | UI (show summary) |
|
||||
|
||||
---
|
||||
|
||||
## 4. Sensing Pipeline Context
|
||||
|
||||
**Purpose**: Control the sensing server process, receive real-time CSI data, and
|
||||
manage the signal processing pipeline.
|
||||
|
||||
**Downstream of**: Device Discovery (needs node IPs for data attribution)
|
||||
|
||||
### Aggregates
|
||||
|
||||
#### `SensingServer` (Aggregate Root)
|
||||
|
||||
Represents the managed sensing server child process.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `state` | `ServerState` (VO) | Stopped / Starting / Running / Stopping / Crashed |
|
||||
| `config` | `ServerConfig` (VO) | Port configuration, log level, model paths |
|
||||
| `pid` | `Option<u32>` | OS process ID when running |
|
||||
| `started_at` | `Option<DateTime<Utc>>` | Start timestamp |
|
||||
| `log_buffer` | `RingBuffer<LogEntry>` | Last N log lines |
|
||||
| `ws_url` | `Option<Url>` | WebSocket URL for live data |
|
||||
|
||||
**Invariant**: Only one `SensingServer` process may be managed at a time.
|
||||
|
||||
#### `SensingSession` (Entity)
|
||||
|
||||
An active connection to the sensing server's WebSocket for receiving real-time data.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `connection_state` | `WsState` | Connecting / Connected / Disconnected |
|
||||
| `frames_received` | `u64` | Total CSI frames received this session |
|
||||
| `last_frame_at` | `Option<DateTime<Utc>>` | Timestamp of last received frame |
|
||||
| `subscriptions` | `HashSet<DataChannel>` | Which data streams are active |
|
||||
|
||||
### Value Objects
|
||||
|
||||
- `ServerState` — enum: `Stopped`, `Starting`, `Running`, `Stopping`, `Crashed(exit_code: i32)`
|
||||
- `ServerConfig` — `{ http_port: u16, ws_port: u16, udp_port: u16, model_dir: PathBuf, log_level: Level }`
|
||||
- `LogEntry` — `{ timestamp: DateTime, level: Level, target: String, message: String }`
|
||||
- `DataChannel` — enum: `CsiFrames`, `PoseUpdates`, `VitalSigns`, `ActivityClassification`
|
||||
- `WsState` — enum: `Connecting`, `Connected`, `Disconnected(reason: String)`
|
||||
|
||||
### Domain Events
|
||||
|
||||
| Event | Payload | Consumers |
|
||||
|-------|---------|-----------|
|
||||
| `ServerStarted` | `{ pid, ports: ServerConfig }` | UI (enable sensing view), Discovery (start health polling via WS) |
|
||||
| `ServerStopped` | `{ exit_code, uptime_secs }` | UI (disable sensing view) |
|
||||
| `ServerCrashed` | `{ exit_code, last_log_lines }` | UI (show crash report) |
|
||||
| `CsiFrameReceived` | `{ node_id, timestamp, subcarrier_count }` | Visualization (update charts) |
|
||||
| `PoseUpdated` | `{ persons: Vec<PersonPose> }` | Visualization (draw skeletons) |
|
||||
| `VitalSignUpdate` | `{ node_id, bpm, breath_rate }` | Visualization (update vitals chart) |
|
||||
| `ActivityDetected` | `{ label, confidence }` | Visualization (show activity) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Edge Module (WASM) Context
|
||||
|
||||
**Purpose**: Upload, manage, and monitor WASM edge processing modules running
|
||||
on ESP32 nodes.
|
||||
|
||||
**Downstream of**: Device Discovery (needs node IPs and WASM capability info)
|
||||
**Upstream of**: Sensing Pipeline (WASM modules emit edge-processed events)
|
||||
|
||||
### Aggregates
|
||||
|
||||
#### `ModuleRegistry` (Aggregate Root)
|
||||
|
||||
Tracks all WASM modules across all nodes.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `modules` | `Map<(MacAddress, ModuleId), WasmModule>` | Per-node module inventory |
|
||||
|
||||
#### `WasmModule` (Entity)
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | `ModuleId` (VO) | Node-assigned module identifier |
|
||||
| `name` | `String` | Filename of the uploaded `.wasm` |
|
||||
| `size_bytes` | `u64` | Module size |
|
||||
| `status` | `ModuleStatus` (VO) | Loaded / Running / Stopped / Error |
|
||||
| `node_mac` | `MacAddress` | Which node this module runs on |
|
||||
| `uploaded_at` | `DateTime<Utc>` | Upload timestamp |
|
||||
| `signed` | `bool` | Whether the module has an Ed25519 signature |
|
||||
|
||||
### Value Objects
|
||||
|
||||
- `ModuleId` — string identifier assigned by the node firmware
|
||||
- `ModuleStatus` — enum: `Loaded`, `Running`, `Stopped`, `Error(String)`
|
||||
|
||||
### Domain Events
|
||||
|
||||
| Event | Payload | Consumers |
|
||||
|-------|---------|-----------|
|
||||
| `ModuleUploaded` | `{ node_mac, module_id, name, size }` | UI (refresh list) |
|
||||
| `ModuleStarted` | `{ node_mac, module_id }` | UI (update status) |
|
||||
| `ModuleStopped` | `{ node_mac, module_id }` | UI (update status) |
|
||||
| `ModuleUnloaded` | `{ node_mac, module_id }` | UI (remove from list) |
|
||||
| `ModuleError` | `{ node_mac, module_id, error }` | UI (show error) |
|
||||
|
||||
### Anti-Corruption Layer
|
||||
|
||||
The ESP32 WASM management HTTP API (`/wasm/*` on port 8032) returns raw JSON
|
||||
with firmware-specific field names. The ACL normalizes these:
|
||||
|
||||
```rust
|
||||
/// ACL: Translate ESP32 WASM list response to domain WasmModule entities.
|
||||
fn translate_wasm_list(raw: &[serde_json::Value]) -> Vec<WasmModule> {
|
||||
raw.iter().filter_map(|entry| {
|
||||
Some(WasmModule {
|
||||
id: ModuleId(entry["id"].as_str()?.to_string()),
|
||||
name: entry["name"].as_str().unwrap_or("unknown").to_string(),
|
||||
size_bytes: entry["size"].as_u64().unwrap_or(0),
|
||||
status: match entry["state"].as_str() {
|
||||
Some("running") => ModuleStatus::Running,
|
||||
Some("stopped") => ModuleStatus::Stopped,
|
||||
Some("loaded") => ModuleStatus::Loaded,
|
||||
other => ModuleStatus::Error(
|
||||
format!("Unknown state: {:?}", other)
|
||||
),
|
||||
},
|
||||
// ...
|
||||
})
|
||||
}).collect()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Visualization Context
|
||||
|
||||
**Purpose**: Render real-time and historical sensing data — CSI heatmaps, pose
|
||||
skeletons, vital sign charts, mesh topology graphs.
|
||||
|
||||
**Downstream of**: Sensing Pipeline (receives data events), Device Discovery (needs
|
||||
node metadata for labeling)
|
||||
|
||||
This context is **purely presentational** and contains no domain logic. It
|
||||
transforms domain events from other contexts into visual representations.
|
||||
|
||||
### Aggregates
|
||||
|
||||
None — this context is a **Query Model** (CQRS read side). It subscribes to
|
||||
domain events and projects them into view models.
|
||||
|
||||
### View Models
|
||||
|
||||
#### `DashboardView`
|
||||
|
||||
| Field | Source Context | Description |
|
||||
|-------|---------------|-------------|
|
||||
| `nodes` | Device Discovery | Node cards with health, version, signal quality |
|
||||
| `server` | Sensing Pipeline | Server status, uptime, port info |
|
||||
| `recent_activity` | All contexts | Timeline of recent events |
|
||||
|
||||
#### `SignalView`
|
||||
|
||||
| Field | Source Context | Description |
|
||||
|-------|---------------|-------------|
|
||||
| `csi_heatmap` | Sensing Pipeline | Subcarrier amplitude x time matrix |
|
||||
| `signal_field` | Sensing Pipeline | 2D signal strength grid |
|
||||
| `activity_label` | Sensing Pipeline | Current classification |
|
||||
| `confidence` | Sensing Pipeline | Classification confidence |
|
||||
|
||||
#### `PoseView`
|
||||
|
||||
| Field | Source Context | Description |
|
||||
|-------|---------------|-------------|
|
||||
| `persons` | Sensing Pipeline | Array of detected person skeletons |
|
||||
| `zones` | Sensing Pipeline | Active zones in the sensing area |
|
||||
|
||||
#### `VitalsView`
|
||||
|
||||
| Field | Source Context | Description |
|
||||
|-------|---------------|-------------|
|
||||
| `breathing_rate_bpm` | Sensing Pipeline | Per-node breathing rate time series |
|
||||
| `heart_rate_bpm` | Sensing Pipeline | Per-node heart rate time series |
|
||||
|
||||
#### `MeshView`
|
||||
|
||||
| Field | Source Context | Description |
|
||||
|-------|---------------|-------------|
|
||||
| `nodes` | Device Discovery | Positioned nodes for graph layout |
|
||||
| `edges` | Device Discovery | Inter-node visibility/connectivity |
|
||||
| `tdm_timeline` | Device Discovery | TDM slot schedule visualization |
|
||||
| `sync_status` | Sensing Pipeline | Per-node sync status with server |
|
||||
|
||||
---
|
||||
|
||||
## Cross-Context Event Flow
|
||||
|
||||
```
|
||||
NodeDiscovered
|
||||
Device Discovery ─────────────────────────────────> Firmware Management
|
||||
│ │
|
||||
│ NodeDiscovered │ FlashCompleted
|
||||
│ NodeHealthChanged │
|
||||
├──────────────────> Visualization v
|
||||
│ Configuration
|
||||
│ NodeDiscovered │
|
||||
├──────────────────> Sensing Pipeline │ NodeProvisioned
|
||||
│ │
|
||||
│ v
|
||||
│ Device Discovery
|
||||
│ (re-scan triggered)
|
||||
│
|
||||
│ NodeDiscovered
|
||||
└──────────────────> Edge Module (WASM)
|
||||
│
|
||||
│ ModuleUploaded, ModuleStarted
|
||||
│
|
||||
v
|
||||
Sensing Pipeline
|
||||
│
|
||||
│ CsiFrameReceived, PoseUpdated, VitalSignUpdate
|
||||
│
|
||||
v
|
||||
Visualization
|
||||
```
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
1. **Event Bus**: Domain events are dispatched via Tauri's event system
|
||||
(`app_handle.emit("event-name", payload)`). The frontend subscribes using
|
||||
`listen("event-name", callback)`. This provides natural cross-context
|
||||
communication without coupling contexts directly.
|
||||
|
||||
2. **State Isolation**: Each bounded context maintains its own `State<'_, T>`
|
||||
managed by Tauri. Contexts do not share mutable state directly — they
|
||||
communicate exclusively through events.
|
||||
|
||||
3. **Module Organization**: Each bounded context maps to a Rust module under
|
||||
`src/commands/` and `src/domain/`:
|
||||
|
||||
```
|
||||
src/
|
||||
commands/ # Tauri command handlers (application layer)
|
||||
discovery.rs # Device Discovery context commands
|
||||
flash.rs # Firmware Management context commands
|
||||
ota.rs # Firmware Management context commands
|
||||
provision.rs # Configuration context commands
|
||||
server.rs # Sensing Pipeline context commands
|
||||
wasm.rs # Edge Module context commands
|
||||
domain/ # Domain models (pure Rust, no Tauri dependency)
|
||||
discovery/
|
||||
mod.rs
|
||||
node.rs # Node entity, MacAddress VO
|
||||
registry.rs # NodeRegistry aggregate
|
||||
events.rs # Discovery domain events
|
||||
firmware/
|
||||
mod.rs
|
||||
binary.rs # FirmwareBinary entity
|
||||
flash.rs # FlashSession aggregate
|
||||
ota.rs # OtaSession aggregate
|
||||
events.rs
|
||||
config/
|
||||
mod.rs
|
||||
nvs.rs # NodeConfig entity
|
||||
mesh.rs # MeshConfig entity
|
||||
provision.rs # ProvisioningSession aggregate
|
||||
events.rs
|
||||
sensing/
|
||||
mod.rs
|
||||
server.rs # SensingServer aggregate
|
||||
session.rs # SensingSession entity
|
||||
events.rs
|
||||
wasm/
|
||||
mod.rs
|
||||
module.rs # WasmModule entity
|
||||
registry.rs # ModuleRegistry aggregate
|
||||
events.rs
|
||||
acl/ # Anti-corruption layers
|
||||
ota_status.rs # ESP32 OTA status response translator
|
||||
wasm_api.rs # ESP32 WASM API response translator
|
||||
espflash.rs # espflash crate adapter
|
||||
```
|
||||
|
||||
4. **Testing Strategy**: Domain modules under `src/domain/` have no Tauri
|
||||
dependency and can be tested with standard `cargo test`. Command handlers
|
||||
under `src/commands/` require Tauri test utilities for integration testing.
|
||||
|
||||
5. **Shared Kernel**: The `MacAddress`, `SemVer`, and `SecureString` value objects
|
||||
are shared across contexts. They live in a `src/domain/shared.rs` module.
|
||||
This is acceptable because they are immutable value objects with no behavior
|
||||
beyond validation and formatting.
|
||||
@@ -1,810 +0,0 @@
|
||||
# ADR-052: Tauri Desktop Frontend — RuView Hardware Management & Visualization
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | Proposed |
|
||||
| Date | 2026-03-06 |
|
||||
| Deciders | ruv |
|
||||
| Depends on | ADR-012 (ESP32 CSI Mesh), ADR-039 (Edge Intelligence), ADR-040 (WASM Programmable Sensing), ADR-044 (Provisioning Enhancements), ADR-050 (Security Hardening), ADR-051 (Server Decomposition) |
|
||||
| Issue | [#177](https://github.com/ruvnet/RuView/issues/177) |
|
||||
|
||||
## Context
|
||||
|
||||
RuView currently requires users to interact with multiple disconnected tools to manage a WiFi DensePose deployment:
|
||||
|
||||
| Task | Current Tool | Pain Point |
|
||||
|------|-------------|------------|
|
||||
| Flash firmware | `esptool.py` CLI | Requires Python, pip, correct chip/baud flags |
|
||||
| Provision NVS | `provision.py` CLI | 13+ flags, no GUI, no read-back |
|
||||
| OTA update | `curl POST :8032/ota` | Manual HTTP, PSK header construction |
|
||||
| WASM modules | `curl` to `:8032/wasm/*` | No visibility into module state |
|
||||
| Start sensing server | `cargo run` or binary | Manual port configuration, no log viewer |
|
||||
| View sensing data | Browser at `localhost:8080` | Separate window, no hardware context |
|
||||
| Mesh topology | Mental model | No visualization of TDM slots, sync, health |
|
||||
| Node discovery | Manual IP tracking | No mDNS/UDP broadcast discovery |
|
||||
|
||||
There is no single tool that provides a unified view of the entire deployment — from ESP32 hardware through the sensing pipeline to pose visualization. Field operators deploying multi-node meshes must context-switch between terminals, browsers, and serial monitors.
|
||||
|
||||
### Why a Desktop App
|
||||
|
||||
A browser-based UI cannot access serial ports (for flashing), raw UDP sockets (for node discovery), or the local filesystem (for firmware binaries). A desktop application is required for hardware management. Tauri v2 is the natural choice because:
|
||||
|
||||
1. **Rust backend** — integrates directly with the existing Rust workspace (`v2/`). Crates like `wifi-densepose-hardware` (serial port parsing), `wifi-densepose-config`, and `wifi-densepose-sensing-server` can be linked as library dependencies.
|
||||
2. **Small binary** — Tauri bundles the system webview rather than shipping Chromium (~150 MB savings vs Electron).
|
||||
3. **Cross-platform** — Windows, macOS, Linux from the same codebase.
|
||||
4. **Security model** — Tauri's capability-based permissions system restricts frontend access to explicitly allowed Rust commands.
|
||||
|
||||
### Why Not Electron / Flutter / Native
|
||||
|
||||
| Option | Rejected Because |
|
||||
|--------|-----------------|
|
||||
| Electron | 150+ MB bundle, no Rust integration, duplicates webview |
|
||||
| Flutter | No serial port plugins, Dart FFI to Rust is awkward |
|
||||
| Native (GTK/Qt) | Platform-specific UI code, no web component reuse |
|
||||
| Web-only (PWA) | Cannot access serial ports or raw UDP |
|
||||
|
||||
## Decision
|
||||
|
||||
Build a Tauri v2 desktop application as a new crate in the Rust workspace. The frontend uses TypeScript with React and Vite. The Rust backend exposes Tauri commands that bridge the frontend to serial ports, UDP sockets, HTTP management endpoints, and the sensing server process.
|
||||
|
||||
### 1. Workspace Integration
|
||||
|
||||
Add a new crate to the workspace:
|
||||
|
||||
```
|
||||
v2/
|
||||
Cargo.toml # Add "crates/wifi-densepose-desktop" to members
|
||||
crates/
|
||||
wifi-densepose-desktop/ # NEW — Tauri app crate
|
||||
Cargo.toml
|
||||
tauri.conf.json
|
||||
capabilities/
|
||||
default.json # Tauri v2 capability permissions
|
||||
icons/ # App icons (all platforms)
|
||||
src/
|
||||
main.rs # Tauri entry point
|
||||
lib.rs # Command module re-exports
|
||||
commands/
|
||||
mod.rs
|
||||
discovery.rs # Node discovery commands
|
||||
flash.rs # Firmware flashing commands
|
||||
ota.rs # OTA update commands
|
||||
wasm.rs # WASM module management commands
|
||||
server.rs # Sensing server lifecycle commands
|
||||
provision.rs # NVS provisioning commands
|
||||
serial.rs # Serial port enumeration
|
||||
state.rs # Tauri managed state
|
||||
discovery/
|
||||
mod.rs
|
||||
mdns.rs # mDNS service discovery
|
||||
udp_broadcast.rs # UDP broadcast probe
|
||||
flash/
|
||||
mod.rs
|
||||
espflash.rs # Rust-native ESP32 flashing (via espflash crate)
|
||||
esptool.rs # Fallback: bundled esptool.py wrapper
|
||||
frontend/
|
||||
package.json
|
||||
tsconfig.json
|
||||
vite.config.ts
|
||||
index.html
|
||||
src/
|
||||
main.tsx
|
||||
App.tsx
|
||||
routes.tsx
|
||||
hooks/
|
||||
useNodes.ts # Node discovery and status polling
|
||||
useServer.ts # Sensing server state
|
||||
useWebSocket.ts # WS connection to sensing server
|
||||
stores/
|
||||
nodeStore.ts # Zustand store for discovered nodes
|
||||
serverStore.ts # Sensing server process state
|
||||
settingsStore.ts # User preferences (dark mode, ports)
|
||||
pages/
|
||||
Dashboard.tsx # Hardware management overview
|
||||
NodeDetail.tsx # Single node detail + config
|
||||
FlashFirmware.tsx # Firmware flashing wizard
|
||||
WasmModules.tsx # WASM module manager
|
||||
SensingView.tsx # Live sensing data visualization
|
||||
MeshTopology.tsx # Multi-node mesh topology view
|
||||
Settings.tsx # App settings and preferences
|
||||
components/
|
||||
NodeCard.tsx # Node status card (health, version, signal)
|
||||
NodeList.tsx # Discovered node list
|
||||
FirmwareProgress.tsx # Flash/OTA progress indicator
|
||||
LogViewer.tsx # Scrolling log output
|
||||
SignalChart.tsx # Real-time CSI signal chart
|
||||
PoseOverlay.tsx # Pose skeleton overlay
|
||||
MeshGraph.tsx # D3/force-graph mesh topology
|
||||
SerialPortSelect.tsx # Serial port dropdown
|
||||
ProvisionForm.tsx # NVS provisioning form
|
||||
lib/
|
||||
tauri.ts # Typed Tauri invoke wrappers
|
||||
types.ts # Shared TypeScript types
|
||||
```
|
||||
|
||||
### 2. Rust Backend — Tauri Commands
|
||||
|
||||
#### 2.1 Node Discovery
|
||||
|
||||
```rust
|
||||
// commands/discovery.rs
|
||||
|
||||
/// Discover ESP32 CSI nodes on the local network.
|
||||
/// Strategy 1: mDNS — nodes announce _ruview._tcp service
|
||||
/// Strategy 2: UDP broadcast probe on port 5005 (CSI aggregator port)
|
||||
/// Strategy 3: HTTP health check sweep on port 8032 (OTA server)
|
||||
#[tauri::command]
|
||||
async fn discover_nodes(timeout_ms: u64) -> Result<Vec<DiscoveredNode>, String>;
|
||||
|
||||
/// Get detailed status from a specific node via HTTP.
|
||||
/// Calls GET /ota/status on port 8032.
|
||||
#[tauri::command]
|
||||
async fn get_node_status(ip: String) -> Result<NodeStatus, String>;
|
||||
|
||||
/// Subscribe to node health updates (periodic polling).
|
||||
#[tauri::command]
|
||||
async fn watch_nodes(interval_ms: u64, state: State<'_, AppState>) -> Result<(), String>;
|
||||
```
|
||||
|
||||
The `DiscoveredNode` struct:
|
||||
|
||||
```rust
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct DiscoveredNode {
|
||||
pub ip: String,
|
||||
pub mac: Option<String>,
|
||||
pub hostname: Option<String>,
|
||||
pub node_id: u8,
|
||||
pub firmware_version: Option<String>,
|
||||
pub tdm_slot: Option<u8>,
|
||||
pub tdm_total: Option<u8>,
|
||||
pub edge_tier: Option<u8>,
|
||||
pub uptime_secs: Option<u64>,
|
||||
pub discovery_method: DiscoveryMethod, // Mdns | UdpProbe | HttpSweep
|
||||
pub last_seen: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 Firmware Flashing
|
||||
|
||||
```rust
|
||||
// commands/flash.rs
|
||||
|
||||
/// List available serial ports with chip detection.
|
||||
#[tauri::command]
|
||||
async fn list_serial_ports() -> Result<Vec<SerialPortInfo>, String>;
|
||||
|
||||
/// Flash firmware binary to an ESP32 via serial port.
|
||||
/// Uses the `espflash` crate for Rust-native flashing (no Python dependency).
|
||||
/// Falls back to bundled esptool.py if espflash fails.
|
||||
/// Emits progress events via Tauri event system.
|
||||
#[tauri::command]
|
||||
async fn flash_firmware(
|
||||
port: String,
|
||||
firmware_path: String,
|
||||
chip: Chip, // Esp32, Esp32s3, Esp32c3
|
||||
baud: Option<u32>,
|
||||
app_handle: AppHandle,
|
||||
) -> Result<FlashResult, String>;
|
||||
|
||||
/// Read firmware info from a connected ESP32 (chip type, flash size, MAC).
|
||||
#[tauri::command]
|
||||
async fn read_chip_info(port: String) -> Result<ChipInfo, String>;
|
||||
```
|
||||
|
||||
Flash progress is emitted as Tauri events:
|
||||
|
||||
```rust
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct FlashProgress {
|
||||
pub phase: FlashPhase, // Connecting | Erasing | Writing | Verifying
|
||||
pub progress_pct: f32, // 0.0 - 100.0
|
||||
pub bytes_written: u64,
|
||||
pub bytes_total: u64,
|
||||
pub speed_bps: u64,
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3 OTA Updates
|
||||
|
||||
```rust
|
||||
// commands/ota.rs
|
||||
|
||||
/// Push firmware to a node via HTTP OTA (port 8032).
|
||||
/// Includes PSK authentication per ADR-050.
|
||||
#[tauri::command]
|
||||
async fn ota_update(
|
||||
node_ip: String,
|
||||
firmware_path: String,
|
||||
psk: Option<String>,
|
||||
app_handle: AppHandle,
|
||||
) -> Result<OtaResult, String>;
|
||||
|
||||
/// Get OTA status from a node (current version, partition info).
|
||||
#[tauri::command]
|
||||
async fn ota_status(node_ip: String, psk: Option<String>) -> Result<OtaStatus, String>;
|
||||
|
||||
/// Batch OTA update — push firmware to multiple nodes sequentially.
|
||||
/// Skips nodes already running the target version.
|
||||
#[tauri::command]
|
||||
async fn ota_batch_update(
|
||||
nodes: Vec<String>, // IPs
|
||||
firmware_path: String,
|
||||
psk: Option<String>,
|
||||
app_handle: AppHandle,
|
||||
) -> Result<Vec<OtaResult>, String>;
|
||||
```
|
||||
|
||||
#### 2.4 WASM Module Management
|
||||
|
||||
```rust
|
||||
// commands/wasm.rs
|
||||
|
||||
/// List WASM modules loaded on a node.
|
||||
/// Calls GET /wasm/list on port 8032.
|
||||
#[tauri::command]
|
||||
async fn wasm_list(node_ip: String) -> Result<Vec<WasmModule>, String>;
|
||||
|
||||
/// Upload a WASM module to a node.
|
||||
/// Calls POST /wasm/upload on port 8032 with binary payload.
|
||||
#[tauri::command]
|
||||
async fn wasm_upload(
|
||||
node_ip: String,
|
||||
wasm_path: String,
|
||||
app_handle: AppHandle,
|
||||
) -> Result<WasmUploadResult, String>;
|
||||
|
||||
/// Start/stop a WASM module on a node.
|
||||
#[tauri::command]
|
||||
async fn wasm_control(
|
||||
node_ip: String,
|
||||
module_id: String,
|
||||
action: WasmAction, // Start | Stop | Unload
|
||||
) -> Result<(), String>;
|
||||
```
|
||||
|
||||
#### 2.5 Sensing Server Lifecycle
|
||||
|
||||
```rust
|
||||
// commands/server.rs
|
||||
|
||||
/// Start the sensing server as a managed child process.
|
||||
/// The server binary is either bundled with the Tauri app (sidecar)
|
||||
/// or discovered on PATH.
|
||||
#[tauri::command]
|
||||
async fn start_server(
|
||||
config: ServerConfig,
|
||||
state: State<'_, AppState>,
|
||||
app_handle: AppHandle,
|
||||
) -> Result<(), String>;
|
||||
|
||||
/// Stop the managed sensing server process.
|
||||
#[tauri::command]
|
||||
async fn stop_server(state: State<'_, AppState>) -> Result<(), String>;
|
||||
|
||||
/// Get sensing server status (running/stopped, PID, ports, uptime).
|
||||
#[tauri::command]
|
||||
async fn server_status(state: State<'_, AppState>) -> Result<ServerStatus, String>;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct ServerConfig {
|
||||
pub http_port: u16, // Default: 8080
|
||||
pub ws_port: u16, // Default: 8765
|
||||
pub udp_port: u16, // Default: 5005
|
||||
pub static_dir: Option<String>, // Path to UI static files
|
||||
pub model_dir: Option<String>, // Path to ML models
|
||||
pub log_level: String, // trace, debug, info, warn, error
|
||||
}
|
||||
```
|
||||
|
||||
The sensing server is bundled as a Tauri sidecar binary. Tauri v2 supports sidecar binaries via `externalBin` in `tauri.conf.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"bundle": {
|
||||
"externalBin": ["sensing-server"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.6 NVS Provisioning
|
||||
|
||||
```rust
|
||||
// commands/provision.rs
|
||||
|
||||
/// Provision NVS configuration to an ESP32 via serial port.
|
||||
/// Replaces the Python provision.py script with a Rust-native implementation.
|
||||
/// Generates NVS partition binary and flashes it to the NVS partition offset.
|
||||
#[tauri::command]
|
||||
async fn provision_node(
|
||||
port: String,
|
||||
config: NvsConfig,
|
||||
app_handle: AppHandle,
|
||||
) -> Result<ProvisionResult, String>;
|
||||
|
||||
/// Read current NVS configuration from a connected ESP32.
|
||||
/// Reads the NVS partition and parses key-value pairs.
|
||||
#[tauri::command]
|
||||
async fn read_nvs(port: String) -> Result<NvsConfig, String>;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct NvsConfig {
|
||||
pub wifi_ssid: Option<String>,
|
||||
pub wifi_password: Option<String>,
|
||||
pub target_ip: Option<String>,
|
||||
pub target_port: Option<u16>,
|
||||
pub node_id: Option<u8>,
|
||||
pub tdm_slot: Option<u8>,
|
||||
pub tdm_total: Option<u8>,
|
||||
pub edge_tier: Option<u8>,
|
||||
pub presence_thresh: Option<u16>,
|
||||
pub fall_thresh: Option<u16>,
|
||||
pub vital_window: Option<u16>,
|
||||
pub vital_interval_ms: Option<u16>,
|
||||
pub top_k_count: Option<u8>,
|
||||
pub hop_count: Option<u8>,
|
||||
pub channel_list: Option<Vec<u8>>,
|
||||
pub dwell_ms: Option<u32>,
|
||||
pub power_duty: Option<u8>,
|
||||
pub wasm_max_modules: Option<u8>,
|
||||
pub wasm_verify: Option<bool>,
|
||||
pub wasm_pubkey: Option<Vec<u8>>,
|
||||
pub ota_psk: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Frontend Architecture
|
||||
|
||||
#### 3.1 Tech Stack
|
||||
|
||||
| Layer | Choice | Rationale |
|
||||
|-------|--------|-----------|
|
||||
| Framework | React 19 | Component model, ecosystem, team familiarity |
|
||||
| Build | Vite 6 | Fast HMR, Tauri plugin support |
|
||||
| State | Zustand | Lightweight, no boilerplate, works with Tauri events |
|
||||
| Routing | React Router v7 | File-based routes, type-safe |
|
||||
| UI Components | shadcn/ui + Tailwind CSS | Accessible, customizable, no runtime CSS-in-JS |
|
||||
| Charts | Recharts or visx | Real-time signal visualization |
|
||||
| Topology Graph | D3 force-directed | Mesh network visualization |
|
||||
| Serial UI | Custom | Tauri command integration |
|
||||
| Icons | Lucide React | Consistent, tree-shakeable |
|
||||
|
||||
#### 3.2 Page Layout
|
||||
|
||||
```
|
||||
+------------------------------------------+
|
||||
| RuView [Settings] [?] |
|
||||
+-------+----------------------------------+
|
||||
| | |
|
||||
| Nav | Dashboard / Active Page |
|
||||
| | |
|
||||
| [D] | +--------+ +--------+ +------+ |
|
||||
| [F] | | Node 1 | | Node 2 | | +Add | |
|
||||
| [W] | +--------+ +--------+ +------+ |
|
||||
| [S] | |
|
||||
| [M] | Server Status: Running |
|
||||
| [T] | +--------------------------+ |
|
||||
| | | Live Signal / Pose View | |
|
||||
| | +--------------------------+ |
|
||||
+-------+----------------------------------+
|
||||
| Status Bar: 3 nodes | Server: :8080 |
|
||||
+------------------------------------------+
|
||||
|
||||
Nav items:
|
||||
[D] Dashboard — overview of all nodes and server
|
||||
[F] Flash — firmware flashing wizard
|
||||
[W] WASM — edge module management
|
||||
[S] Sensing — live sensing data view
|
||||
[M] Mesh — topology visualization
|
||||
[T] Settings — ports, paths, preferences
|
||||
```
|
||||
|
||||
#### 3.3 Dashboard Page
|
||||
|
||||
The dashboard is the primary landing page showing:
|
||||
|
||||
1. **Node Grid** — cards for each discovered ESP32 node showing:
|
||||
- IP address and hostname
|
||||
- Firmware version (with update indicator if newer available)
|
||||
- Node ID and TDM slot assignment
|
||||
- Edge processing tier (raw / stats / vitals)
|
||||
- Signal quality indicator (last CSI frame age)
|
||||
- Health status (online/offline/degraded)
|
||||
- Quick actions: OTA update, configure, view logs
|
||||
|
||||
2. **Sensing Server Panel** — start/stop button, port configuration, log tail
|
||||
|
||||
3. **Discovery Controls** — scan button, auto-discovery toggle, network range filter
|
||||
|
||||
#### 3.4 Flash Firmware Page
|
||||
|
||||
A wizard-style flow:
|
||||
|
||||
1. **Select Port** — dropdown of detected serial ports with chip info
|
||||
2. **Select Firmware** — file picker for `.bin` files, or select from bundled builds
|
||||
3. **Configure** — chip type, baud rate, flash mode
|
||||
4. **Flash** — progress bar with phase indicators (connecting, erasing, writing, verifying)
|
||||
5. **Provision** — optional NVS provisioning form (WiFi, target IP, TDM, edge tier)
|
||||
6. **Verify** — serial monitor showing boot log, success/fail indicator
|
||||
|
||||
#### 3.5 WASM Module Manager Page
|
||||
|
||||
| Column | Content |
|
||||
|--------|---------|
|
||||
| Module ID | Auto-assigned by node |
|
||||
| Name | Filename of uploaded `.wasm` |
|
||||
| Size | Module size in KB |
|
||||
| Status | Running / Stopped / Error |
|
||||
| Node | Which ESP32 node it runs on |
|
||||
| Actions | Start / Stop / Unload / View Logs |
|
||||
|
||||
Upload panel: drag-and-drop `.wasm` file, select target node(s), upload button.
|
||||
|
||||
#### 3.6 Sensing View Page
|
||||
|
||||
Embeds the existing web UI (`ui/`) via an iframe pointing at the sensing server's static file route, or builds native React components that connect to the same WebSocket API. The native approach is preferred because it allows:
|
||||
|
||||
- Tighter integration with the node status sidebar
|
||||
- Shared state between hardware management and visualization
|
||||
- Offline access to recorded data
|
||||
|
||||
Key visualization components:
|
||||
- **CSI Heatmap** — subcarrier amplitude over time
|
||||
- **Signal Field** — 2D signal strength visualization
|
||||
- **Pose Skeleton** — detected body keypoints and connections
|
||||
- **Vital Signs** — real-time breathing rate and heart rate charts
|
||||
- **Activity Classification** — current activity label with confidence
|
||||
|
||||
#### 3.7 Mesh Topology Page
|
||||
|
||||
A force-directed graph showing:
|
||||
- Nodes as circles (color = health status, size = edge tier)
|
||||
- Edges between nodes that can see each other
|
||||
- TDM slot labels on each node
|
||||
- Sync status indicators (in-sync / drifting / lost)
|
||||
- Click a node to navigate to its detail page
|
||||
|
||||
### 4. Platform-Specific Considerations
|
||||
|
||||
#### 4.1 macOS
|
||||
|
||||
- **Serial driver signing**: CP210x and CH340 drivers require user approval in System Preferences > Security
|
||||
- **App signing**: Tauri apps must be signed and notarized for distribution outside the App Store
|
||||
- **USB permissions**: No special permissions needed beyond driver installation
|
||||
- **CoreWLAN**: The sensing server can use CoreWLAN for WiFi scanning (ADR-025); the desktop app inherits this capability
|
||||
|
||||
#### 4.2 Windows
|
||||
|
||||
- **COM port access**: Windows assigns COM port numbers; the app lists them via the Windows Registry or `SetupDi` API
|
||||
- **Driver installation**: USB-to-serial drivers (CP210x, CH340, FTDI) must be installed; the app can detect missing drivers and link to downloads
|
||||
- **Firewall**: The sensing server's UDP listener may trigger Windows Firewall prompts; the app should pre-configure rules or guide the user
|
||||
- **Code signing**: EV certificate required for SmartScreen trust; unsigned apps trigger warnings
|
||||
|
||||
#### 4.3 Linux
|
||||
|
||||
- **udev rules**: ESP32 serial ports (`/dev/ttyUSB*`, `/dev/ttyACM*`) require udev rules for non-root access. The app bundles a `99-ruview-esp32.rules` file and offers to install it:
|
||||
```
|
||||
SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", MODE="0666" # CP210x
|
||||
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", MODE="0666" # CH340
|
||||
```
|
||||
- **AppImage/deb/rpm**: Tauri supports all three packaging formats
|
||||
- **Wayland vs X11**: Tauri uses webkit2gtk which works on both
|
||||
|
||||
### 5. Cargo.toml for the Desktop Crate
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "wifi-densepose-desktop"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description = "Tauri desktop frontend for RuView WiFi DensePose"
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[lib]
|
||||
name = "wifi_densepose_desktop"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-shell = "2" # Sidecar process management
|
||||
tauri-plugin-dialog = "2" # File picker dialogs
|
||||
tauri-plugin-fs = "2" # Filesystem access
|
||||
tauri-plugin-process = "2" # Process management
|
||||
tauri-plugin-notification = "2" # Desktop notifications
|
||||
|
||||
# Workspace crates
|
||||
wifi-densepose-hardware = { workspace = true }
|
||||
wifi-densepose-config = { workspace = true }
|
||||
wifi-densepose-core = { workspace = true }
|
||||
|
||||
# Serial port access
|
||||
serialport = { workspace = true }
|
||||
|
||||
# ESP32 flashing (Rust-native, replaces esptool.py)
|
||||
espflash = "3"
|
||||
|
||||
# Network discovery
|
||||
mdns-sd = "0.11" # mDNS/DNS-SD service discovery
|
||||
|
||||
# HTTP client for OTA and WASM management
|
||||
reqwest = { version = "0.12", features = ["json", "multipart", "stream"] }
|
||||
|
||||
# Async runtime
|
||||
tokio = { workspace = true }
|
||||
|
||||
# Serialization
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
# Logging
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
# Time
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
```
|
||||
|
||||
### 6. Tauri Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
|
||||
"productName": "RuView",
|
||||
"version": "0.3.0",
|
||||
"identifier": "net.ruv.ruview",
|
||||
"build": {
|
||||
"frontendDist": "../frontend/dist",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeDevCommand": "cd frontend && npm run dev",
|
||||
"beforeBuildCommand": "cd frontend && npm run build"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "RuView - WiFi DensePose",
|
||||
"width": 1280,
|
||||
"height": 800,
|
||||
"minWidth": 900,
|
||||
"minHeight": 600
|
||||
}
|
||||
]
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"externalBin": ["sensing-server"],
|
||||
"linux": {
|
||||
"deb": { "depends": ["libwebkit2gtk-4.1-0"] },
|
||||
"appimage": { "bundleMediaFramework": true }
|
||||
},
|
||||
"windows": {
|
||||
"wix": { "language": "en-US" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Tauri v2 Capabilities (Permissions)
|
||||
|
||||
```json
|
||||
{
|
||||
"identifier": "default",
|
||||
"description": "RuView default capability set",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"shell:allow-execute",
|
||||
"shell:allow-open",
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-save",
|
||||
"fs:allow-read",
|
||||
"fs:allow-write",
|
||||
"process:allow-exit",
|
||||
"notification:default"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Development Workflow
|
||||
|
||||
```bash
|
||||
# Prerequisites
|
||||
cargo install tauri-cli@^2
|
||||
cd v2/crates/wifi-densepose-desktop/frontend
|
||||
npm install
|
||||
|
||||
# Development (hot-reload frontend + Rust rebuild)
|
||||
cd v2/crates/wifi-densepose-desktop
|
||||
cargo tauri dev
|
||||
|
||||
# Production build
|
||||
cargo tauri build
|
||||
|
||||
# Build sensing-server sidecar (must be done before tauri build)
|
||||
cargo build --release -p wifi-densepose-sensing-server
|
||||
# Copy to sidecar location:
|
||||
# target/release/sensing-server -> crates/wifi-densepose-desktop/binaries/sensing-server-{arch}
|
||||
```
|
||||
|
||||
### 9. Persistent Node Registry
|
||||
|
||||
Discovery alone is transient — nodes appear when they broadcast, disappear when they don't. A persistent local registry transforms discovery into **reconciliation**.
|
||||
|
||||
```
|
||||
~/.ruview/nodes.db (SQLite via rusqlite)
|
||||
```
|
||||
|
||||
**Schema:**
|
||||
|
||||
```sql
|
||||
CREATE TABLE nodes (
|
||||
mac TEXT PRIMARY KEY, -- e.g. "AA:BB:CC:DD:EE:FF"
|
||||
last_ip TEXT, -- last known IP
|
||||
last_seen INTEGER NOT NULL, -- Unix timestamp
|
||||
firmware TEXT, -- e.g. "0.3.1"
|
||||
chip TEXT DEFAULT 'esp32s3', -- esp32, esp32s3, esp32c3
|
||||
mesh_role TEXT DEFAULT 'node', -- 'coordinator' | 'node' | 'aggregator'
|
||||
tdm_slot INTEGER, -- assigned TDM slot index
|
||||
capabilities TEXT, -- JSON: {"wasm": true, "ota": true, "csi": true}
|
||||
friendly_name TEXT, -- user-assigned label
|
||||
notes TEXT -- free-form notes
|
||||
);
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- On discovery broadcast, upsert into registry (update `last_ip`, `last_seen`, `firmware`)
|
||||
- Dashboard shows **all registered nodes**, dimming those not seen recently
|
||||
- User can manually add nodes by MAC/IP (for networks without mDNS)
|
||||
- Export/import registry as JSON for fleet management across machines
|
||||
- Node health history (uptime, last OTA, error count) tracked over time
|
||||
|
||||
This means the desktop app **remembers the mesh** across restarts, which is critical for field deployments where nodes may be offline temporarily.
|
||||
|
||||
### 10. OTA Safety Gate — Rolling Updates
|
||||
|
||||
Mesh deployments cannot tolerate all nodes rebooting simultaneously. The OTA subsystem includes a **rolling update mode** that preserves sensing continuity:
|
||||
|
||||
```rust
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct BatchOtaConfig {
|
||||
/// Update strategy
|
||||
pub strategy: OtaStrategy,
|
||||
/// Max nodes updating concurrently
|
||||
pub max_concurrent: usize,
|
||||
/// Delay between batches (seconds)
|
||||
pub batch_delay_secs: u64,
|
||||
/// Abort if any node fails
|
||||
pub fail_fast: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum OtaStrategy {
|
||||
/// Update one node at a time, wait for it to rejoin mesh
|
||||
Sequential,
|
||||
/// Update non-adjacent TDM slots to maintain coverage
|
||||
TdmSafe,
|
||||
/// Update all nodes simultaneously (development only)
|
||||
Parallel,
|
||||
}
|
||||
```
|
||||
|
||||
**`TdmSafe` strategy:**
|
||||
|
||||
1. Sort nodes by TDM slot index
|
||||
2. Update even-slot nodes first (slots 0, 2, 4...)
|
||||
3. Wait for each to reboot and rejoin mesh (verified via beacon)
|
||||
4. Then update odd-slot nodes (slots 1, 3, 5...)
|
||||
5. At no point are adjacent nodes offline simultaneously
|
||||
|
||||
**UI flow:**
|
||||
|
||||
- User selects target firmware + target nodes
|
||||
- App shows pre-update diff (current vs new version per node)
|
||||
- Progress bar per node with states: `queued → uploading → rebooting → verifying → done`
|
||||
- Abort button halts remaining updates without rolling back completed ones
|
||||
- Post-update health check confirms all nodes are sensing
|
||||
|
||||
### 11. Plugin Architecture (Future)
|
||||
|
||||
This desktop tool is quietly becoming the **control plane for RuView**. Once it manages discovery, firmware, OTA, WASM, sensing, and mesh topology, plugin extensibility becomes inevitable:
|
||||
|
||||
- **Firmware management** today → **swarm orchestration** tomorrow
|
||||
- **WASM upload** today → **edge module marketplace** tomorrow
|
||||
- **Sensing view** today → **activity classification dashboard** tomorrow
|
||||
|
||||
The Tauri command surface should be designed with this trajectory in mind:
|
||||
|
||||
- Commands are grouped by bounded context (already done)
|
||||
- Each context can be extended by loading additional Tauri plugins
|
||||
- The node registry becomes the source of truth for all plugins
|
||||
- Event bus (Tauri's `emit`/`listen`) provides cross-plugin communication
|
||||
|
||||
This does NOT mean building a plugin system in Phase 1. It means keeping the architecture open to it: no hardcoded views, state flows through the registry, commands are typed and versioned.
|
||||
|
||||
### 12. Security Considerations
|
||||
|
||||
1. **PSK Storage**: OTA PSK tokens are stored in the OS keychain via `tauri-plugin-stronghold` or the platform's native credential store, never in plaintext config files.
|
||||
|
||||
2. **Serial Port Access**: Tauri's capability system restricts which commands the frontend can invoke. Serial port access is only available through the typed `flash_firmware` and `provision_node` commands, not raw serial I/O.
|
||||
|
||||
3. **Network Requests**: OTA and WASM management commands only communicate with nodes on the local network. The app does not make external network requests except for update checks (opt-in).
|
||||
|
||||
4. **Firmware Validation**: Before flashing, the app validates the firmware binary header (ESP32 image magic bytes, partition table offset) to prevent bricking.
|
||||
|
||||
5. **WASM Signature Verification**: The desktop app can sign WASM modules before upload using a locally stored Ed25519 key pair, complementing the node-side verification (ADR-040).
|
||||
|
||||
### 13. Implementation Phases
|
||||
|
||||
| Phase | Scope | Effort | Priority |
|
||||
|-------|-------|--------|----------|
|
||||
| **Phase 1: Skeleton** | Tauri project scaffolding, workspace integration, basic window with React | 1 week | P0 |
|
||||
| **Phase 2: Discovery** | Serial port listing, UDP/mDNS node discovery, dashboard with node cards | 1 week | P0 |
|
||||
| **Phase 3: Flash** | espflash integration, firmware flashing wizard with progress events | 1 week | P0 |
|
||||
| **Phase 4: Server** | Sidecar sensing server start/stop, log viewer, status panel | 1 week | P1 |
|
||||
| **Phase 5: OTA** | HTTP OTA with PSK auth, batch update, version comparison | 1 week | P1 |
|
||||
| **Phase 6: Provisioning** | NVS read/write via serial, provisioning form, mesh config file | 1 week | P1 |
|
||||
| **Phase 7: WASM** | Module upload/list/start/stop, drag-and-drop, per-module logs | 1 week | P2 |
|
||||
| **Phase 8: Sensing** | WebSocket integration, live signal charts, pose overlay | 2 weeks | P2 |
|
||||
| **Phase 9: Mesh View** | Force-directed topology graph, TDM slot visualization, sync status | 1 week | P2 |
|
||||
| **Phase 10: Polish** | App signing, auto-update, udev rules installer, onboarding wizard | 1 week | P3 |
|
||||
|
||||
Total estimated effort: ~11 weeks for a single developer.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **Single pane of glass** — all hardware management, sensing, and visualization in one app
|
||||
- **No Python dependency** — Rust-native `espflash` replaces `esptool.py` for firmware flashing
|
||||
- **Replaces 6+ CLI tools** — flash, provision, OTA, WASM management, server control, visualization
|
||||
- **Accessible to non-developers** — GUI replaces CLI flags and curl commands
|
||||
- **Cross-platform** — one codebase for Windows, macOS, Linux
|
||||
- **Workspace integration** — shares types, config, and hardware crates with sensing server
|
||||
- **Small binary** — ~15-20 MB vs ~150 MB for Electron equivalent
|
||||
|
||||
### Negative
|
||||
|
||||
- **New frontend dependency** — introduces Node.js/npm build step into the Rust workspace
|
||||
- **Tauri version churn** — Tauri v2 is recent; API stability is not yet proven at scale
|
||||
- **webkit2gtk on Linux** — depends on system webview version; old distros may have stale webkit
|
||||
- **espflash limitations** — the `espflash` crate may not support all chip variants or flash modes that `esptool.py` handles; fallback to bundled Python is needed
|
||||
- **Maintenance surface** — adds ~5,000 lines of TypeScript and ~2,000 lines of Rust
|
||||
|
||||
### Risks
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|-----------|--------|------------|
|
||||
| espflash cannot flash all ESP32 variants | Medium | High | Bundle esptool.py as fallback sidecar |
|
||||
| Tauri v2 breaking changes | Low | Medium | Pin to specific Tauri version; update in dedicated PRs |
|
||||
| Serial port access fails on macOS Sequoia+ | Medium | Medium | Test on latest macOS; document driver requirements |
|
||||
| webkit2gtk version mismatch on Linux | Medium | Low | Set minimum version in deb/rpm dependencies |
|
||||
| Sidecar sensing server fails to start | Low | Medium | Detect failure and show manual start instructions |
|
||||
|
||||
## References
|
||||
|
||||
- Tauri v2 documentation: https://v2.tauri.app/
|
||||
- espflash crate: https://crates.io/crates/espflash
|
||||
- mdns-sd crate: https://crates.io/crates/mdns-sd
|
||||
- ADR-012: ESP32 CSI Sensor Mesh
|
||||
- ADR-039: ESP32 Edge Intelligence
|
||||
- ADR-040: WASM Programmable Sensing
|
||||
- ADR-044: Provisioning Tool Enhancements
|
||||
- ADR-050: Quality Engineering — Security Hardening
|
||||
- ADR-051: Sensing Server Decomposition
|
||||
- `firmware/esp32-csi-node/` — ESP32 firmware source
|
||||
- `firmware/esp32-csi-node/provision.py` — Current provisioning script
|
||||
- `v2/crates/wifi-densepose-sensing-server/` — Sensing server
|
||||
- `v2/crates/wifi-densepose-hardware/` — Hardware crate
|
||||
- `ui/` — Existing web UI
|
||||
@@ -1,274 +0,0 @@
|
||||
# ADR-053: UI Design System — Dark Professional + Unity-Inspired Interface
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | Accepted |
|
||||
| Date | 2026-03-06 |
|
||||
| Deciders | ruv |
|
||||
| Depends on | ADR-052 (Tauri Desktop Frontend) |
|
||||
|
||||
## Context
|
||||
|
||||
RuView Desktop (ADR-052) needs a UI design system that communicates precision and control — befitting a hardware management control plane for embedded sensing infrastructure. The interface must handle dense data (CSI heatmaps, node registries, log streams, mesh topologies) without feeling overwhelming, while remaining usable by both engineers and field operators.
|
||||
|
||||
Two design inspirations:
|
||||
|
||||
1. **Data-first professional tools** — Dense information displays where data speaks for itself. Clean typography, structured layouts, and deliberate use of color for status. The interface shows what matters and hides what doesn't. Think: network monitoring dashboards, embedded systems IDEs, infrastructure control panels.
|
||||
|
||||
2. **Unity Editor** — Dockable panel system, inspector/hierarchy/scene separation, property grids, dark professional theme, and dense-but-organized data display. Unity's UI is purpose-built for managing complex real-time systems — exactly what RuView needs.
|
||||
|
||||
The combination yields a professional control panel for WiFi sensing infrastructure. Data is organized into scannable panels with clear hierarchy. Status is communicated through consistent color coding. The layout adapts from high-level overview down to individual node details through progressive disclosure.
|
||||
|
||||
## Decision
|
||||
|
||||
### Design Principles
|
||||
|
||||
1. **Data is the interface** — The system reveals patterns through visualization, not through explanation. Every pixel earns its place.
|
||||
2. **Precision typography** — Typography is clean and authoritative. Technical values are displayed without ambiguity. Labels are concise.
|
||||
3. **Panel-based layout** — Dockable regions inspired by Unity's panel system. The operator can see the entire mesh at a glance, then drill into any node.
|
||||
4. **Status through color** — Deliberate color coding: green (online), amber (degraded), red (offline/failed), blue (scanning/new). No gratuitous color.
|
||||
5. **Progressive disclosure** — Dashboard shows the overview. Clicking a node reveals its details. Summary first, detail on interaction.
|
||||
6. **Dual typography** — Monospace for all technical values (MAC addresses, firmware versions, CSI amplitudes). Sans-serif for labels and descriptions. The contrast signals "data vs. context."
|
||||
7. **Powered by rUv** — Subtle branding: footer tagline, about dialog, splash screen.
|
||||
|
||||
### Color System
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* Background layers */
|
||||
--bg-base: #0d1117; /* App background */
|
||||
--bg-surface: #161b22; /* Panel backgrounds */
|
||||
--bg-elevated: #1c2333; /* Cards, modals, dropdowns */
|
||||
--bg-hover: #242d3d; /* Hover state */
|
||||
--bg-active: #2d3748; /* Active/selected state */
|
||||
|
||||
/* Text hierarchy */
|
||||
--text-primary: #e6edf3; /* Headings, primary content */
|
||||
--text-secondary: #8b949e; /* Labels, descriptions */
|
||||
--text-muted: #484f58; /* Disabled, hints, placeholders */
|
||||
|
||||
/* Status indicators */
|
||||
--status-online: #3fb950; /* Node online, healthy */
|
||||
--status-warning: #d29922; /* Degraded, needs attention */
|
||||
--status-error: #f85149; /* Offline, failed, critical */
|
||||
--status-info: #58a6ff; /* Scanning, discovering, info */
|
||||
|
||||
/* Accent */
|
||||
--accent: #7c3aed; /* rUv purple — primary actions */
|
||||
--accent-hover: #6d28d9;
|
||||
|
||||
/* Borders */
|
||||
--border: #30363d;
|
||||
--border-active: #58a6ff;
|
||||
|
||||
/* Data display */
|
||||
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
|
||||
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
### Typography Scale
|
||||
|
||||
```css
|
||||
/* Typographic hierarchy */
|
||||
.heading-xl { font: 600 28px/1.2 var(--font-sans); } /* Page titles */
|
||||
.heading-lg { font: 600 20px/1.3 var(--font-sans); } /* Section titles */
|
||||
.heading-md { font: 600 16px/1.4 var(--font-sans); } /* Card titles */
|
||||
.heading-sm { font: 600 13px/1.4 var(--font-sans); } /* Panel labels */
|
||||
.body { font: 400 14px/1.6 var(--font-sans); } /* Body text */
|
||||
.body-sm { font: 400 12px/1.5 var(--font-sans); } /* Captions */
|
||||
.data { font: 400 13px/1.4 var(--font-mono); } /* Technical values */
|
||||
.data-lg { font: 500 18px/1.2 var(--font-mono); } /* Key metrics */
|
||||
```
|
||||
|
||||
### Layout System
|
||||
|
||||
Three-region layout: navigation sidebar, node list, and detail inspector. Unity's docking system provides the mechanical framework.
|
||||
|
||||
```
|
||||
+--[ Sidebar ]--+--[ Main ]-------------------------------------+
|
||||
| | |
|
||||
| [Nav Items] | +--[ Command Bar ]---------------------------+ |
|
||||
| | | Breadcrumb | Actions | Search | |
|
||||
| Dashboard | +-------+-----------------------------------+ |
|
||||
| Nodes | | | | |
|
||||
| Flash | | Node | Detail Inspector | |
|
||||
| OTA | | List | (selected node properties) | |
|
||||
| Edge Modules | | | | |
|
||||
| Sensing | | | [Property Grid] | |
|
||||
| Mesh View | | | [Status Indicators] | |
|
||||
| Settings | | | [Action Buttons] | |
|
||||
| | | | | |
|
||||
+-[ Status Bar ]+--+-------+-----------------------------------+ |
|
||||
| rUv | 3 nodes online | Server: running | Port: 8080 |
|
||||
+---------------------------------------------------------------+
|
||||
```
|
||||
|
||||
**Panel behaviors:**
|
||||
- Sidebar collapses to icon-only on narrow windows
|
||||
- Node List / Inspector split is resizable via drag handle
|
||||
- Inspector scrolls independently — drill into any node without losing the list
|
||||
- Status Bar shows global system state at a glance (node count, server status, port)
|
||||
|
||||
### Component Library
|
||||
|
||||
#### 1. NodeCard
|
||||
|
||||
```
|
||||
+-- NodeCard -----------------------------------------------+
|
||||
| [●] ESP32-S3 Node #2 firmware: 0.3.1 |
|
||||
| MAC: AA:BB:CC:DD:EE:FF TDM Slot: 2/4 |
|
||||
| IP: 192.168.1.42 Edge Tier: 1 |
|
||||
| Last seen: 3s ago [Flash] [OTA] [···] |
|
||||
+-----------------------------------------------------------+
|
||||
```
|
||||
|
||||
Status dot uses `--status-online/warning/error`. Card background shifts on hover.
|
||||
|
||||
#### 2. FlashProgress
|
||||
|
||||
```
|
||||
+-- Flash Progress -----------------------------------------+
|
||||
| Flashing firmware to COM3 (ESP32-S3) |
|
||||
| |
|
||||
| Phase: Writing |
|
||||
| [████████████████████░░░░░░░░░░] 67.3% |
|
||||
| 412 KB / 612 KB • 38.2 KB/s • ~5s remaining |
|
||||
+-----------------------------------------------------------+
|
||||
```
|
||||
|
||||
Progress bar uses `--accent` fill with subtle pulse animation during active writes.
|
||||
|
||||
#### 3. Mesh Topology View (Three.js)
|
||||
|
||||
Interactive 3D visualization of the sensing network. Each node is a sphere. Edges are lines representing signal paths. The coordinator node is visually distinct (larger, outlined ring). Built with **Three.js**, consistent with the existing visualization stack in `ui/observatory/js/` and `ui/components/`.
|
||||
|
||||
```
|
||||
+-- Mesh Topology ------------------------------------------+
|
||||
| |
|
||||
| [Node 0]----[Node 1] |
|
||||
| | \ / | |
|
||||
| | [Coordinator] | Coordinator = TDM master |
|
||||
| | / \ | |
|
||||
| [Node 2]----[Node 3] |
|
||||
| |
|
||||
| Drift: ±0.3ms | Cycle: 50ms | 4/4 nodes online |
|
||||
+-----------------------------------------------------------+
|
||||
```
|
||||
|
||||
**Three.js implementation details:**
|
||||
- Force-directed layout computed on CPU, rendered as `THREE.Group` with `THREE.Mesh` (spheres) and `THREE.Line` (edges)
|
||||
- Node spheres use `THREE.MeshPhongMaterial` with emissive color matching `--status-online/warning/error`
|
||||
- Edge lines use `THREE.LineBasicMaterial` with opacity mapped to signal strength
|
||||
- Coordinator node rendered with `THREE.RingGeometry` outline
|
||||
- Camera: `OrbitControls` for pan/zoom/rotate, reset button returns to default view
|
||||
- Follows existing patterns: `BufferGeometry` + `BufferAttribute` for dynamic updates (see `ui/observatory/js/subcarrier-manifold.js`)
|
||||
- Raycasting for node click → opens detail in Inspector panel
|
||||
- Real-time updates as nodes join, leave, or change status — geometry attributes updated per frame
|
||||
|
||||
#### 4. PropertyGrid (Unity Inspector-style)
|
||||
|
||||
```
|
||||
+-- Node Inspector -----------------------------------------+
|
||||
| General [▼] |
|
||||
| MAC Address AA:BB:CC:DD:EE:FF |
|
||||
| IP Address 192.168.1.42 |
|
||||
| Firmware 0.3.1 |
|
||||
| Chip ESP32-S3 |
|
||||
| TDM Configuration [▼] |
|
||||
| Slot Index 2 |
|
||||
| Total Nodes 4 |
|
||||
| Cycle Period 50 ms |
|
||||
| Sync Drift +0.12 ms |
|
||||
| WASM Modules [▼] |
|
||||
| [0] activity_detect running 12.4 KB 83 us/f |
|
||||
| [1] vital_monitor stopped 8.1 KB — us/f |
|
||||
+-----------------------------------------------------------+
|
||||
```
|
||||
|
||||
Collapsible sections with alternating row backgrounds for scanability.
|
||||
|
||||
#### 5. StatusBadge
|
||||
|
||||
```
|
||||
[● Online] [◐ Degraded] [○ Offline] [↻ Updating]
|
||||
```
|
||||
|
||||
Small inline badges with status dot, label, and optional tooltip.
|
||||
|
||||
#### 6. LogViewer
|
||||
|
||||
```
|
||||
+-- Server Log (auto-scroll) -----------[ Clear ] [ ⏸ ]---+
|
||||
| 19:42:01.234 INFO sensing-server HTTP on 127.0.0.1:8080|
|
||||
| 19:42:01.235 INFO sensing-server WS on 127.0.0.1:8765 |
|
||||
| 19:42:01.890 INFO udp_receiver CSI frame from .42 |
|
||||
| 19:42:02.003 WARN vital_signs Low signal quality |
|
||||
+-----------------------------------------------------------+
|
||||
```
|
||||
|
||||
Monospace, color-coded by log level (INFO=text, WARN=amber, ERROR=red). Virtual scrolling for performance.
|
||||
|
||||
### Spacing and Grid
|
||||
|
||||
```css
|
||||
/* 4px base grid */
|
||||
--space-1: 4px; /* Tight spacing (within components) */
|
||||
--space-2: 8px; /* Component internal padding */
|
||||
--space-3: 12px; /* Between related elements */
|
||||
--space-4: 16px; /* Card padding, section gaps */
|
||||
--space-5: 24px; /* Between sections */
|
||||
--space-6: 32px; /* Page-level spacing */
|
||||
--space-8: 48px; /* Major section breaks */
|
||||
|
||||
/* Panel dimensions */
|
||||
--sidebar-width: 220px;
|
||||
--sidebar-collapsed: 52px;
|
||||
--statusbar-height: 28px;
|
||||
--toolbar-height: 44px;
|
||||
```
|
||||
|
||||
### Animations
|
||||
|
||||
Minimal and purposeful:
|
||||
- Panel collapse/expand: 200ms ease-out
|
||||
- Node card health transition: 300ms (color fade, not flash)
|
||||
- Progress bar fill: smooth 60fps CSS transition
|
||||
- Mesh graph: Three.js render loop at 60fps, force simulation on requestAnimationFrame
|
||||
- No loading spinners — use skeleton placeholders instead
|
||||
|
||||
### Branding
|
||||
|
||||
- **Splash screen**: rUv logo + "RuView Desktop" + version, 1.5s duration
|
||||
- **Status bar**: "Powered by rUv" in `--text-muted`, left-aligned
|
||||
- **About dialog**: rUv logo, version, license, links to GitHub and docs
|
||||
- **App icon**: Stylized WiFi signal + human silhouette in rUv purple (#7c3aed)
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Professional, data-dense UI suitable for hardware management
|
||||
- Consistent design language across all 7 pages
|
||||
- Dual typography (mono + sans-serif) ensures readability at all information densities
|
||||
- Unity-inspired panels feel natural to engineers familiar with IDE/editor tools
|
||||
- Dark theme reduces eye strain for extended monitoring sessions
|
||||
|
||||
### Negative
|
||||
|
||||
- Custom design system means no off-the-shelf component library (shadcn/ui partially usable)
|
||||
- Dockable panels add complexity to the layout system
|
||||
- Dark-only theme may not suit all users (could add light mode later)
|
||||
|
||||
### Neutral
|
||||
|
||||
- The design system is CSS-only with React components — no heavy UI framework dependency
|
||||
- Component library can be extracted as a separate package if other rUv projects need it
|
||||
|
||||
## References
|
||||
|
||||
- ADR-052: Tauri Desktop Frontend
|
||||
- Unity Editor UI Guidelines: https://docs.unity3d.com/Manual/UIE-USS.html
|
||||
- Three.js (existing project dependency): `ui/observatory/js/`, `ui/components/`
|
||||
- Inter font: https://rsms.me/inter/
|
||||
- JetBrains Mono: https://www.jetbrains.com/lp/mono/
|
||||
@@ -1,699 +0,0 @@
|
||||
# ADR-054: RuView Desktop Full Implementation
|
||||
|
||||
## Status
|
||||
**Accepted** — Implementation in progress
|
||||
|
||||
## Context
|
||||
|
||||
RuView Desktop v0.3.0 shipped with a complete React/TypeScript frontend but stub-only Rust backend commands. Users report:
|
||||
- Settings cannot be saved (#206) ✅ Fixed in PR #209
|
||||
- Flash firmware does nothing
|
||||
- OTA updates are non-functional
|
||||
- Node discovery returns hardcoded data
|
||||
- Server start/stop is cosmetic only
|
||||
|
||||
This ADR defines the complete implementation plan to make all desktop features production-ready with proper security, optimization, and error handling.
|
||||
|
||||
## Decision
|
||||
|
||||
Implement all 14 Tauri commands with full functionality, security hardening, and performance optimization.
|
||||
|
||||
---
|
||||
|
||||
## 1. Command Implementation Matrix
|
||||
|
||||
| Module | Command | Current | Target | Priority | Security |
|
||||
|--------|---------|---------|--------|----------|----------|
|
||||
| **Settings** | `get_settings` | ✅ Done | ✅ Done | P0 | File permissions |
|
||||
| | `save_settings` | ✅ Done | ✅ Done | P0 | Input validation |
|
||||
| **Discovery** | `discover_nodes` | Stub | Full mDNS + UDP | P1 | Network boundary |
|
||||
| | `list_serial_ports` | Stub | Real enumeration | P1 | USB device access |
|
||||
| **Flash** | `flash_firmware` | Stub | espflash integration | P1 | Binary validation |
|
||||
| | `flash_progress` | Stub | Event streaming | P1 | Progress channel |
|
||||
| **OTA** | `ota_update` | Stub | HTTP multipart + PSK | P1 | TLS + PSK auth |
|
||||
| | `batch_ota_update` | Stub | Parallel with backoff | P2 | Rate limiting |
|
||||
| **WASM** | `wasm_list` | Stub | HTTP GET /api/wasm | P2 | Response validation |
|
||||
| | `wasm_upload` | Stub | HTTP POST multipart | P2 | Size limits, signing |
|
||||
| | `wasm_control` | Stub | HTTP POST commands | P2 | Action whitelist |
|
||||
| **Server** | `start_server` | Partial | Child process spawn | P1 | Port validation |
|
||||
| | `stop_server` | Partial | Graceful shutdown | P1 | PID verification |
|
||||
| | `server_status` | Partial | Health check | P1 | Timeout handling |
|
||||
| **Provision** | `provision_node` | Stub | NVS binary write | P2 | Serial validation |
|
||||
| | `read_nvs` | Stub | NVS binary read | P2 | Parse validation |
|
||||
|
||||
---
|
||||
|
||||
## 2. Implementation Details
|
||||
|
||||
### 2.1 Discovery Module
|
||||
|
||||
**Dependencies:**
|
||||
```toml
|
||||
mdns-sd = "0.11"
|
||||
serialport = "4.6"
|
||||
tokio = { version = "1", features = ["net", "time"] }
|
||||
```
|
||||
|
||||
**discover_nodes Implementation:**
|
||||
```rust
|
||||
pub async fn discover_nodes(timeout_ms: Option<u64>) -> Result<Vec<DiscoveredNode>, String> {
|
||||
let timeout = Duration::from_millis(timeout_ms.unwrap_or(3000));
|
||||
let mut nodes = Vec::new();
|
||||
|
||||
// 1. mDNS discovery (_ruview._tcp.local)
|
||||
let mdns = ServiceDaemon::new()?;
|
||||
let receiver = mdns.browse("_ruview._tcp.local.")?;
|
||||
|
||||
// 2. UDP broadcast probe (port 5005)
|
||||
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
||||
socket.set_broadcast(true)?;
|
||||
socket.send_to(b"RUVIEW_DISCOVER", "255.255.255.255:5005").await?;
|
||||
|
||||
// 3. Collect responses with timeout
|
||||
tokio::select! {
|
||||
_ = collect_mdns(&receiver, &mut nodes) => {},
|
||||
_ = collect_udp(&socket, &mut nodes) => {},
|
||||
_ = tokio::time::sleep(timeout) => {},
|
||||
}
|
||||
|
||||
Ok(nodes)
|
||||
}
|
||||
```
|
||||
|
||||
**list_serial_ports Implementation:**
|
||||
```rust
|
||||
pub async fn list_serial_ports() -> Result<Vec<SerialPortInfo>, String> {
|
||||
let ports = serialport::available_ports()
|
||||
.map_err(|e| format!("Failed to enumerate ports: {}", e))?;
|
||||
|
||||
Ok(ports.into_iter().map(|p| SerialPortInfo {
|
||||
name: p.port_name,
|
||||
vid: extract_vid(&p.port_type),
|
||||
pid: extract_pid(&p.port_type),
|
||||
manufacturer: extract_manufacturer(&p.port_type),
|
||||
chip: detect_esp_chip(&p.port_type),
|
||||
}).collect())
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Flash Module
|
||||
|
||||
**Dependencies:**
|
||||
```toml
|
||||
espflash = "4.0"
|
||||
tokio = { version = "1", features = ["sync"] }
|
||||
```
|
||||
|
||||
**flash_firmware Implementation:**
|
||||
```rust
|
||||
pub async fn flash_firmware(
|
||||
port: String,
|
||||
firmware_path: String,
|
||||
chip: Option<String>,
|
||||
baud: Option<u32>,
|
||||
app: AppHandle,
|
||||
) -> Result<FlashResult, String> {
|
||||
// 1. Validate firmware binary
|
||||
let firmware = std::fs::read(&firmware_path)
|
||||
.map_err(|e| format!("Cannot read firmware: {}", e))?;
|
||||
validate_esp_binary(&firmware)?;
|
||||
|
||||
// 2. Open serial connection
|
||||
let serial = serialport::new(&port, baud.unwrap_or(460800))
|
||||
.timeout(Duration::from_secs(30))
|
||||
.open()
|
||||
.map_err(|e| format!("Cannot open {}: {}", port, e))?;
|
||||
|
||||
// 3. Connect to ESP bootloader
|
||||
let mut flasher = Flasher::connect(serial, None, None)?;
|
||||
|
||||
// 4. Flash with progress callback
|
||||
let start = Instant::now();
|
||||
flasher.write_bin_to_flash(
|
||||
0x0,
|
||||
&firmware,
|
||||
Some(&mut |current, total| {
|
||||
let _ = app.emit("flash_progress", FlashProgress {
|
||||
phase: "writing".into(),
|
||||
progress_pct: (current as f32 / total as f32) * 100.0,
|
||||
bytes_written: current as u64,
|
||||
bytes_total: total as u64,
|
||||
});
|
||||
}),
|
||||
)?;
|
||||
|
||||
Ok(FlashResult {
|
||||
success: true,
|
||||
message: "Flash complete".into(),
|
||||
duration_secs: start.elapsed().as_secs_f64(),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 OTA Module
|
||||
|
||||
**Dependencies:**
|
||||
```toml
|
||||
reqwest = { version = "0.12", features = ["multipart", "rustls-tls"] }
|
||||
sha2 = "0.10"
|
||||
```
|
||||
|
||||
**ota_update Implementation:**
|
||||
```rust
|
||||
pub async fn ota_update(
|
||||
node_ip: String,
|
||||
firmware_path: String,
|
||||
psk: Option<String>,
|
||||
) -> Result<OtaResult, String> {
|
||||
// 1. Validate IP format
|
||||
let ip: IpAddr = node_ip.parse()
|
||||
.map_err(|_| "Invalid IP address")?;
|
||||
|
||||
// 2. Read and hash firmware
|
||||
let firmware = tokio::fs::read(&firmware_path).await
|
||||
.map_err(|e| format!("Cannot read firmware: {}", e))?;
|
||||
let hash = Sha256::digest(&firmware);
|
||||
|
||||
// 3. Build multipart request
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(120))
|
||||
.build()?;
|
||||
|
||||
let form = multipart::Form::new()
|
||||
.part("firmware", multipart::Part::bytes(firmware)
|
||||
.file_name("firmware.bin")
|
||||
.mime_str("application/octet-stream")?);
|
||||
|
||||
// 4. Send with PSK auth header
|
||||
let mut req = client.post(format!("http://{}:8032/ota", ip))
|
||||
.multipart(form);
|
||||
|
||||
if let Some(key) = psk {
|
||||
req = req.header("X-OTA-PSK", key);
|
||||
}
|
||||
|
||||
let resp = req.send().await
|
||||
.map_err(|e| format!("OTA request failed: {}", e))?;
|
||||
|
||||
if resp.status().is_success() {
|
||||
Ok(OtaResult {
|
||||
success: true,
|
||||
node_ip: node_ip.clone(),
|
||||
message: "OTA update initiated".into(),
|
||||
})
|
||||
} else {
|
||||
Err(format!("OTA failed: {}", resp.status()))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**batch_ota_update Implementation:**
|
||||
```rust
|
||||
pub async fn batch_ota_update(
|
||||
node_ips: Vec<String>,
|
||||
firmware_path: String,
|
||||
psk: Option<String>,
|
||||
strategy: Option<String>,
|
||||
) -> Result<Vec<OtaResult>, String> {
|
||||
let firmware = Arc::new(tokio::fs::read(&firmware_path).await?);
|
||||
let psk = Arc::new(psk);
|
||||
|
||||
let strategy = strategy.unwrap_or("sequential".into());
|
||||
|
||||
match strategy.as_str() {
|
||||
"parallel" => {
|
||||
// All at once (max 4 concurrent)
|
||||
let semaphore = Arc::new(Semaphore::new(4));
|
||||
let handles: Vec<_> = node_ips.into_iter().map(|ip| {
|
||||
let fw = firmware.clone();
|
||||
let key = psk.clone();
|
||||
let sem = semaphore.clone();
|
||||
tokio::spawn(async move {
|
||||
let _permit = sem.acquire().await;
|
||||
ota_single(&ip, &fw, key.as_ref().as_ref()).await
|
||||
})
|
||||
}).collect();
|
||||
|
||||
let results = futures::future::join_all(handles).await;
|
||||
Ok(results.into_iter().filter_map(|r| r.ok()).collect())
|
||||
}
|
||||
"tdm_safe" => {
|
||||
// One per TDM slot group with delays
|
||||
let mut results = Vec::new();
|
||||
for ip in node_ips {
|
||||
results.push(ota_single(&ip, &firmware, psk.as_ref().as_ref()).await);
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
_ => {
|
||||
// Sequential (default)
|
||||
let mut results = Vec::new();
|
||||
for ip in node_ips {
|
||||
results.push(ota_single(&ip, &firmware, psk.as_ref().as_ref()).await);
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 Server Module
|
||||
|
||||
**Dependencies:**
|
||||
```toml
|
||||
tokio = { version = "1", features = ["process"] }
|
||||
sysinfo = "0.32"
|
||||
```
|
||||
|
||||
**start_server Implementation:**
|
||||
```rust
|
||||
pub async fn start_server(
|
||||
config: ServerConfig,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), String> {
|
||||
// 1. Check if already running
|
||||
{
|
||||
let srv = state.server.lock().map_err(|e| e.to_string())?;
|
||||
if srv.running {
|
||||
return Err("Server already running".into());
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Validate ports
|
||||
validate_port(config.http_port.unwrap_or(8080))?;
|
||||
validate_port(config.ws_port.unwrap_or(8765))?;
|
||||
|
||||
// 3. Spawn sensing server as child process
|
||||
let child = Command::new("wifi-densepose-sensing-server")
|
||||
.args([
|
||||
"--http-port", &config.http_port.unwrap_or(8080).to_string(),
|
||||
"--ws-port", &config.ws_port.unwrap_or(8765).to_string(),
|
||||
"--udp-port", &config.udp_port.unwrap_or(5005).to_string(),
|
||||
])
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to start server: {}", e))?;
|
||||
|
||||
// 4. Update state
|
||||
let mut srv = state.server.lock().map_err(|e| e.to_string())?;
|
||||
srv.running = true;
|
||||
srv.pid = Some(child.id());
|
||||
srv.child = Some(child);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**stop_server Implementation:**
|
||||
```rust
|
||||
pub async fn stop_server(state: State<'_, AppState>) -> Result<(), String> {
|
||||
let mut srv = state.server.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
if let Some(mut child) = srv.child.take() {
|
||||
// Graceful shutdown via SIGTERM
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use nix::sys::signal::{kill, Signal};
|
||||
use nix::unistd::Pid;
|
||||
let _ = kill(Pid::from_raw(child.id() as i32), Signal::SIGTERM);
|
||||
}
|
||||
|
||||
// Wait up to 5s, then force kill
|
||||
tokio::select! {
|
||||
_ = child.wait() => {},
|
||||
_ = tokio::time::sleep(Duration::from_secs(5)) => {
|
||||
let _ = child.kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
srv.running = false;
|
||||
srv.pid = None;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 2.5 WASM Module
|
||||
|
||||
**Dependencies:**
|
||||
```toml
|
||||
reqwest = { version = "0.12", features = ["json", "multipart"] }
|
||||
```
|
||||
|
||||
**wasm_list Implementation:**
|
||||
```rust
|
||||
pub async fn wasm_list(node_ip: String) -> Result<Vec<WasmModuleInfo>, String> {
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client.get(format!("http://{}:8080/api/wasm", node_ip))
|
||||
.timeout(Duration::from_secs(5))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Request failed: {}", e))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(format!("Node returned {}", resp.status()));
|
||||
}
|
||||
|
||||
let modules: Vec<WasmModuleInfo> = resp.json().await
|
||||
.map_err(|e| format!("Invalid response: {}", e))?;
|
||||
|
||||
Ok(modules)
|
||||
}
|
||||
```
|
||||
|
||||
**wasm_upload Implementation:**
|
||||
```rust
|
||||
pub async fn wasm_upload(
|
||||
node_ip: String,
|
||||
wasm_path: String,
|
||||
) -> Result<WasmUploadResult, String> {
|
||||
// 1. Validate WASM binary
|
||||
let wasm = tokio::fs::read(&wasm_path).await
|
||||
.map_err(|e| format!("Cannot read WASM: {}", e))?;
|
||||
|
||||
if wasm.len() > 256 * 1024 {
|
||||
return Err("WASM module exceeds 256KB limit".into());
|
||||
}
|
||||
|
||||
if &wasm[0..4] != b"\0asm" {
|
||||
return Err("Invalid WASM magic bytes".into());
|
||||
}
|
||||
|
||||
// 2. Upload to node
|
||||
let client = reqwest::Client::new();
|
||||
let form = multipart::Form::new()
|
||||
.part("module", multipart::Part::bytes(wasm)
|
||||
.file_name(Path::new(&wasm_path).file_name().unwrap().to_string_lossy())
|
||||
.mime_str("application/wasm")?);
|
||||
|
||||
let resp = client.post(format!("http://{}:8080/api/wasm", node_ip))
|
||||
.multipart(form)
|
||||
.timeout(Duration::from_secs(30))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if resp.status().is_success() {
|
||||
let result: WasmUploadResult = resp.json().await?;
|
||||
Ok(result)
|
||||
} else {
|
||||
Err(format!("Upload failed: {}", resp.status()))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2.6 Provision Module
|
||||
|
||||
**Dependencies:**
|
||||
```toml
|
||||
nvs-partition-tool = "0.1" # Or implement NVS binary format
|
||||
serialport = "4.6"
|
||||
```
|
||||
|
||||
**provision_node Implementation:**
|
||||
```rust
|
||||
pub async fn provision_node(
|
||||
port: String,
|
||||
config: ProvisioningConfig,
|
||||
) -> Result<ProvisionResult, String> {
|
||||
// 1. Validate config
|
||||
config.validate()?;
|
||||
|
||||
// 2. Build NVS binary blob
|
||||
let nvs_blob = build_nvs_blob(&config)?;
|
||||
|
||||
// 3. Open serial port
|
||||
let mut serial = serialport::new(&port, 115200)
|
||||
.timeout(Duration::from_secs(10))
|
||||
.open()
|
||||
.map_err(|e| format!("Cannot open {}: {}", port, e))?;
|
||||
|
||||
// 4. Enter bootloader mode
|
||||
enter_bootloader(&mut serial)?;
|
||||
|
||||
// 5. Write NVS partition (offset 0x9000, size 0x6000)
|
||||
write_partition(&mut serial, 0x9000, &nvs_blob)?;
|
||||
|
||||
// 6. Reset device
|
||||
reset_device(&mut serial)?;
|
||||
|
||||
Ok(ProvisionResult {
|
||||
success: true,
|
||||
message: "Provisioning complete".into(),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Security Hardening
|
||||
|
||||
### 3.1 Input Validation
|
||||
|
||||
```rust
|
||||
// All string inputs sanitized
|
||||
fn validate_ip(ip: &str) -> Result<IpAddr, String> {
|
||||
ip.parse::<IpAddr>().map_err(|_| "Invalid IP address".into())
|
||||
}
|
||||
|
||||
fn validate_port(port: u16) -> Result<(), String> {
|
||||
if port < 1024 && port != 0 {
|
||||
return Err("Privileged ports (1-1023) not allowed".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_path(path: &str) -> Result<PathBuf, String> {
|
||||
let path = PathBuf::from(path);
|
||||
if path.components().any(|c| c == std::path::Component::ParentDir) {
|
||||
return Err("Path traversal detected".into());
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Network Security
|
||||
|
||||
```rust
|
||||
// OTA PSK validation
|
||||
fn validate_psk(psk: &str) -> Result<(), String> {
|
||||
if psk.len() < 16 {
|
||||
return Err("PSK must be at least 16 characters".into());
|
||||
}
|
||||
if !psk.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
|
||||
return Err("PSK contains invalid characters".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Rate limiting for network operations
|
||||
struct RateLimiter {
|
||||
last_request: Instant,
|
||||
min_interval: Duration,
|
||||
}
|
||||
|
||||
impl RateLimiter {
|
||||
fn check(&mut self) -> Result<(), String> {
|
||||
if self.last_request.elapsed() < self.min_interval {
|
||||
return Err("Rate limit exceeded".into());
|
||||
}
|
||||
self.last_request = Instant::now();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Binary Validation
|
||||
|
||||
```rust
|
||||
fn validate_esp_binary(data: &[u8]) -> Result<(), String> {
|
||||
// Check ESP binary magic (0xE9 at offset 0)
|
||||
if data.is_empty() || data[0] != 0xE9 {
|
||||
return Err("Invalid ESP firmware magic byte".into());
|
||||
}
|
||||
|
||||
// Check minimum size (header + some code)
|
||||
if data.len() < 256 {
|
||||
return Err("Firmware too small".into());
|
||||
}
|
||||
|
||||
// Check maximum size (4MB flash)
|
||||
if data.len() > 4 * 1024 * 1024 {
|
||||
return Err("Firmware exceeds flash size".into());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Performance Optimization
|
||||
|
||||
### 4.1 Async Everything
|
||||
|
||||
All I/O operations are async with proper timeouts:
|
||||
|
||||
```rust
|
||||
// Timeout wrapper
|
||||
async fn with_timeout<T, F: Future<Output = Result<T, String>>>(
|
||||
future: F,
|
||||
duration: Duration,
|
||||
) -> Result<T, String> {
|
||||
tokio::time::timeout(duration, future)
|
||||
.await
|
||||
.map_err(|_| "Operation timed out".into())?
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 Connection Pooling
|
||||
|
||||
```rust
|
||||
// Reusable HTTP client
|
||||
lazy_static! {
|
||||
static ref HTTP_CLIENT: reqwest::Client = reqwest::Client::builder()
|
||||
.pool_max_idle_per_host(5)
|
||||
.pool_idle_timeout(Duration::from_secs(30))
|
||||
.build()
|
||||
.unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Streaming Progress
|
||||
|
||||
Flash and OTA operations stream progress via Tauri events:
|
||||
|
||||
```rust
|
||||
// Real-time progress updates
|
||||
app.emit("flash_progress", FlashProgress { ... })?;
|
||||
app.emit("ota_progress", OtaProgress { ... })?;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Testing Strategy
|
||||
|
||||
### 5.1 Unit Tests
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_validate_ip() {
|
||||
assert!(validate_ip("192.168.1.1").is_ok());
|
||||
assert!(validate_ip("invalid").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_esp_binary() {
|
||||
let valid = vec![0xE9; 1024];
|
||||
assert!(validate_esp_binary(&valid).is_ok());
|
||||
|
||||
let invalid = vec![0x00; 1024];
|
||||
assert!(validate_esp_binary(&invalid).is_err());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Integration Tests
|
||||
|
||||
```rust
|
||||
#[tokio::test]
|
||||
async fn test_discover_nodes_timeout() {
|
||||
let result = discover_nodes(Some(100)).await;
|
||||
assert!(result.is_ok());
|
||||
// Should return empty or cached results within timeout
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Mock Testing
|
||||
|
||||
```rust
|
||||
// Mock serial port for flash tests
|
||||
struct MockSerial {
|
||||
responses: VecDeque<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Read for MockSerial { ... }
|
||||
impl Write for MockSerial { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Dependencies Update
|
||||
|
||||
**Cargo.toml additions:**
|
||||
```toml
|
||||
[dependencies]
|
||||
# Discovery
|
||||
mdns-sd = "0.11"
|
||||
serialport = "4.6"
|
||||
|
||||
# HTTP client
|
||||
reqwest = { version = "0.12", features = ["json", "multipart", "rustls-tls"] }
|
||||
|
||||
# Crypto
|
||||
sha2 = "0.10"
|
||||
|
||||
# Process management
|
||||
sysinfo = "0.32"
|
||||
|
||||
# Async
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
futures = "0.3"
|
||||
|
||||
# Flash
|
||||
espflash = "4.0"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Implementation Timeline
|
||||
|
||||
| Week | Deliverable |
|
||||
|------|-------------|
|
||||
| 1 | Discovery + Serial ports (real enumeration) |
|
||||
| 1 | Server start/stop (child process management) |
|
||||
| 2 | Flash firmware (espflash integration) |
|
||||
| 2 | OTA update (HTTP multipart) |
|
||||
| 3 | Batch OTA (parallel + sequential strategies) |
|
||||
| 3 | WASM management (list/upload/control) |
|
||||
| 4 | Provision NVS (binary format) |
|
||||
| 4 | Security audit + E2E testing |
|
||||
|
||||
---
|
||||
|
||||
## 8. Rollout Plan
|
||||
|
||||
1. **v0.3.1** — Settings fix + Discovery + Server
|
||||
2. **v0.4.0** — Flash + OTA (single node)
|
||||
3. **v0.5.0** — Batch OTA + WASM + Provision
|
||||
4. **v1.0.0** — Full E2E tested, security audited
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Desktop app becomes fully functional
|
||||
- Real device management capabilities
|
||||
- Production-ready security posture
|
||||
- Async performance throughout
|
||||
|
||||
### Negative
|
||||
- Additional dependencies increase binary size
|
||||
- espflash adds ~2MB to binary
|
||||
- Hardware required for full testing
|
||||
|
||||
### Neutral
|
||||
- Feature parity with browser-based UI
|
||||
- Same API contract as sensing server
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Tauri v2 Commands](https://v2.tauri.app/develop/commands/)
|
||||
- [espflash Documentation](https://github.com/esp-rs/espflash)
|
||||
- [ESP32 OTA Protocol](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/ota.html)
|
||||
- [mDNS-SD Rust](https://docs.rs/mdns-sd/)
|
||||
@@ -1,119 +0,0 @@
|
||||
# ADR-055: Integrated Sensing Server in Desktop App
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
The RuView Desktop application (ADR-054) requires the WiFi sensing server to provide real-time CSI data, activity detection, and vital signs monitoring. Currently, the sensing server is a separate binary (`wifi-densepose-sensing-server`) that must be installed separately and found in the system PATH.
|
||||
|
||||
This creates several problems:
|
||||
1. **Distribution complexity**: Users must install two binaries
|
||||
2. **Path issues**: Binary may not be in PATH, causing "No such file or directory" errors
|
||||
3. **Version mismatch**: Server and desktop app versions may diverge
|
||||
4. **Poor UX**: Error messages about missing binaries confuse users
|
||||
|
||||
## Decision
|
||||
Bundle the sensing server binary inside the desktop application and provide intelligent binary discovery with clear fallback paths.
|
||||
|
||||
### Binary Discovery Order
|
||||
The desktop app searches for the sensing server in this order:
|
||||
1. **Custom path** from user settings (`server_path`)
|
||||
2. **Bundled resources** (`Contents/Resources/bin/` on macOS)
|
||||
3. **Next to executable** (same directory as the app binary)
|
||||
4. **System PATH** (legacy fallback)
|
||||
|
||||
### Implementation
|
||||
```rust
|
||||
fn find_server_binary(app: &AppHandle, custom_path: Option<&str>) -> Result<String, String> {
|
||||
// 1. Custom path from settings
|
||||
if let Some(path) = custom_path {
|
||||
if std::path::Path::new(path).exists() {
|
||||
return Ok(path.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Bundled in resources
|
||||
if let Ok(resource_dir) = app.path().resource_dir() {
|
||||
let bundled = resource_dir.join("bin").join(DEFAULT_SERVER_BIN);
|
||||
if bundled.exists() {
|
||||
return Ok(bundled.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Next to executable
|
||||
if let Ok(exe_path) = std::env::current_exe() {
|
||||
if let Some(exe_dir) = exe_path.parent() {
|
||||
let sibling = exe_dir.join(DEFAULT_SERVER_BIN);
|
||||
if sibling.exists() {
|
||||
return Ok(sibling.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. System PATH
|
||||
// ... which lookup ...
|
||||
|
||||
Err("Sensing server binary not found")
|
||||
}
|
||||
```
|
||||
|
||||
### Bundle Configuration
|
||||
In `tauri.conf.json`:
|
||||
```json
|
||||
{
|
||||
"bundle": {
|
||||
"resources": [
|
||||
{
|
||||
"src": "../../target/release/wifi-densepose-sensing-server",
|
||||
"target": "bin/wifi-densepose-sensing-server"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- **Single package distribution**: Users download one DMG/MSI/EXE
|
||||
- **Version alignment**: Server and UI always match
|
||||
- **Better UX**: No PATH configuration required
|
||||
- **Offline capable**: Works without network access to download server
|
||||
|
||||
### Negative
|
||||
- **Larger bundle size**: ~10-15MB additional for server binary
|
||||
- **Build complexity**: Must build server before bundling desktop
|
||||
- **Platform-specific**: Need separate server binaries per platform
|
||||
|
||||
### Neutral
|
||||
- CI/CD workflow updated to build server before desktop
|
||||
- GitHub Actions builds all platforms (macOS arm64/x64, Windows x64)
|
||||
|
||||
## WebSocket Integration
|
||||
The Sensing page connects to the bundled server's WebSocket endpoint:
|
||||
- `ws://127.0.0.1:{ws_port}/ws/sensing` - Real-time CSI data stream
|
||||
- `ws://127.0.0.1:{ws_port}/ws/pose` - Pose estimation stream
|
||||
|
||||
Message format:
|
||||
```typescript
|
||||
interface WsSensingUpdate {
|
||||
type: string;
|
||||
timestamp: number;
|
||||
source: string;
|
||||
tick: number;
|
||||
nodes: WsNodeInfo[];
|
||||
classification: { motion_level: string; presence: boolean; confidence: number };
|
||||
vital_signs?: { breathing_rate_hz?: number; heart_rate_bpm?: number };
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
- Server binary signed with same certificate as desktop app
|
||||
- Communication over localhost only (127.0.0.1)
|
||||
- No external network access by default
|
||||
- Process spawned as child of desktop app (inherits permissions)
|
||||
|
||||
## Related ADRs
|
||||
- ADR-054: Desktop Full Implementation
|
||||
- ADR-053: UI Design System
|
||||
- ADR-052: Tauri Desktop Frontend
|
||||
@@ -1,251 +0,0 @@
|
||||
# ADR-056: RuView Desktop Complete Capabilities Reference
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
RuView Desktop is a comprehensive WiFi-based sensing platform that combines hardware management, real-time signal processing, neural network inference, and intelligent monitoring. This ADR documents all integrated capabilities across the desktop application and underlying crates.
|
||||
|
||||
## Decision
|
||||
The RuView Desktop application consolidates all WiFi-DensePose functionality into a single, unified interface with the following capabilities.
|
||||
|
||||
---
|
||||
|
||||
## 1. Hardware Management
|
||||
|
||||
### 1.1 Node Discovery
|
||||
- **mDNS discovery**: Automatic detection of ESP32 nodes via Bonjour/Avahi
|
||||
- **UDP probe**: Direct UDP broadcast discovery on port 5005
|
||||
- **HTTP sweep**: Sequential IP scanning with health checks
|
||||
- **Manual registration**: User-defined node configuration
|
||||
|
||||
### 1.2 Firmware Flashing
|
||||
- **Serial flashing**: Direct USB flash via espflash integration
|
||||
- **Chip detection**: Automatic ESP32/S2/S3/C3/C6 identification
|
||||
- **Progress monitoring**: Real-time progress with speed metrics
|
||||
- **Verification**: Post-flash integrity verification
|
||||
|
||||
### 1.3 OTA Updates
|
||||
- **Single-node OTA**: HTTP-based firmware push to individual nodes
|
||||
- **Batch OTA**: Coordinated multi-node updates with strategies:
|
||||
- `sequential`: One node at a time
|
||||
- `tdm_safe`: Respects TDM slot timing
|
||||
- `parallel`: Concurrent updates with throttling
|
||||
- **Rollback support**: Automatic rollback on verification failure
|
||||
- **Version tracking**: Pre/post version comparison
|
||||
|
||||
### 1.4 Node Configuration
|
||||
- **NVS provisioning**: WiFi credentials, node ID, TDM slot assignment
|
||||
- **Mesh configuration**: Coordinator/node/aggregator role assignment
|
||||
- **TDM scheduling**: Time-division multiplexing slot allocation
|
||||
|
||||
---
|
||||
|
||||
## 2. Sensing Server
|
||||
|
||||
### 2.1 Data Sources
|
||||
- **ESP32 CSI**: Real UDP frames from ESP32 hardware (port 5005)
|
||||
- **Windows WiFi**: Native Windows RSSI monitoring via netsh
|
||||
- **Simulation**: Synthetic data generation for demo/testing
|
||||
- **Auto**: Automatic source detection based on available hardware
|
||||
|
||||
### 2.2 Real-Time Processing
|
||||
- **CSI pipeline**: 56-subcarrier amplitude/phase extraction
|
||||
- **FFT analysis**: Spectral decomposition for motion detection
|
||||
- **Vital signs**: Breathing rate (0.1-0.5 Hz), heart rate (0.8-2.0 Hz)
|
||||
- **Motion classification**: still/walking/running/exercising
|
||||
- **Presence detection**: Binary presence with confidence score
|
||||
|
||||
### 2.3 WebSocket Streaming
|
||||
- **Sensing endpoint**: `ws://localhost:8765/ws/sensing`
|
||||
- **Pose endpoint**: `ws://localhost:8765/ws/pose`
|
||||
- **Real-time broadcast**: 10-100 Hz update rate
|
||||
- **Multi-client support**: Concurrent WebSocket connections
|
||||
|
||||
### 2.4 REST API
|
||||
- **Health check**: `GET /health`
|
||||
- **Status**: `GET /api/status`
|
||||
- **Recording control**: `POST /api/recording/start|stop`
|
||||
- **Model management**: `GET/POST /api/models`
|
||||
|
||||
---
|
||||
|
||||
## 3. Neural Network Inference
|
||||
|
||||
### 3.1 Model Formats
|
||||
- **RVF (RuVector Format)**: Proprietary binary container with:
|
||||
- Model weights (quantized f32/f16/i8)
|
||||
- Vital sign configuration
|
||||
- SONA environment profiles
|
||||
- Training provenance
|
||||
- Cryptographic attestation
|
||||
|
||||
### 3.2 Inference Capabilities
|
||||
- **Pose estimation**: 17 COCO keypoints from WiFi CSI
|
||||
- **Activity recognition**: Multi-class classification
|
||||
- **Vital signs**: Breathing and heart rate extraction
|
||||
- **Multi-person detection**: Up to 3 simultaneous subjects
|
||||
|
||||
### 3.3 Self-Learning (SONA)
|
||||
- **Environment adaptation**: LoRA-based fine-tuning to room geometry
|
||||
- **Profile switching**: Multiple learned environment profiles
|
||||
- **Online learning**: Continuous adaptation during runtime
|
||||
- **Transfer learning**: Profile export/import between deployments
|
||||
|
||||
---
|
||||
|
||||
## 4. WASM Edge Modules
|
||||
|
||||
### 4.1 Module Management
|
||||
- **Upload**: Deploy WASM modules to ESP32 nodes
|
||||
- **Start/Stop**: Runtime control of edge processing
|
||||
- **Status monitoring**: CPU, memory, execution count
|
||||
- **Hot reload**: Update modules without node reboot
|
||||
|
||||
### 4.2 Supported Operations
|
||||
- **Local filtering**: On-device noise reduction
|
||||
- **Feature extraction**: Pre-compute features at edge
|
||||
- **Compression**: Reduce data before transmission
|
||||
- **Custom logic**: User-defined processing pipelines
|
||||
|
||||
---
|
||||
|
||||
## 5. Mesh Visualization
|
||||
|
||||
### 5.1 Network Topology
|
||||
- **Live mesh view**: Real-time node connectivity graph
|
||||
- **Signal quality**: RSSI/SNR visualization per link
|
||||
- **Latency monitoring**: Round-trip time measurement
|
||||
- **Packet loss**: Delivery success rate tracking
|
||||
|
||||
### 5.2 CSI Visualization
|
||||
- **Amplitude heatmap**: Per-subcarrier amplitude display
|
||||
- **Phase unwrapping**: Continuous phase visualization
|
||||
- **Spectrogram**: Time-frequency representation
|
||||
- **Signal field**: 3D voxel grid of RF perturbations
|
||||
|
||||
---
|
||||
|
||||
## 6. Training & Export
|
||||
|
||||
### 6.1 Dataset Management
|
||||
- **Recording**: Capture CSI frames with annotations
|
||||
- **Labeling**: Activity and pose ground truth
|
||||
- **Augmentation**: Synthetic data generation
|
||||
- **Export**: Standard formats (JSON, CSV, NumPy)
|
||||
|
||||
### 6.2 Training Pipeline (ADR-023)
|
||||
- **Contrastive pretraining**: Self-supervised feature learning
|
||||
- **Supervised fine-tuning**: Labeled pose estimation
|
||||
- **SONA adaptation**: Environment-specific tuning
|
||||
- **Validation**: Cross-environment testing
|
||||
|
||||
### 6.3 Export Formats
|
||||
- **RVF container**: Production deployment format
|
||||
- **ONNX**: Interoperability with external tools
|
||||
- **PyTorch**: Research and experimentation
|
||||
- **Candle**: Rust-native inference
|
||||
|
||||
---
|
||||
|
||||
## 7. Security Features
|
||||
|
||||
### 7.1 Network Security
|
||||
- **OTA PSK**: Pre-shared key for firmware updates
|
||||
- **Node authentication**: MAC-based node verification
|
||||
- **Encrypted transport**: Optional TLS for API endpoints
|
||||
|
||||
### 7.2 Code Signing
|
||||
- **Firmware verification**: Hash-based integrity checks
|
||||
- **WASM attestation**: Module signature validation
|
||||
- **Model provenance**: Training lineage tracking
|
||||
|
||||
---
|
||||
|
||||
## 8. Configuration & Settings
|
||||
|
||||
### 8.1 Server Configuration
|
||||
- **Ports**: HTTP (8080), WebSocket (8765), UDP (5005)
|
||||
- **Bind address**: Localhost or network-wide
|
||||
- **Data source**: auto/wifi/esp32/simulate
|
||||
- **Log level**: debug/info/warn/error
|
||||
|
||||
### 8.2 Application Settings
|
||||
- **Theme**: Dark/light mode
|
||||
- **Auto-discovery**: Periodic node scanning
|
||||
- **Discovery interval**: Configurable scan frequency
|
||||
- **UI customization**: Responsive layout options
|
||||
|
||||
---
|
||||
|
||||
## 9. Crate Architecture
|
||||
|
||||
| Crate | Capabilities |
|
||||
|-------|-------------|
|
||||
| `wifi-densepose-core` | CSI frame primitives, traits, error types |
|
||||
| `wifi-densepose-signal` | FFT, phase unwrapping, vital signs, RuvSense |
|
||||
| `wifi-densepose-nn` | ONNX/PyTorch/Candle inference backends |
|
||||
| `wifi-densepose-train` | Training pipeline, dataset, metrics |
|
||||
| `wifi-densepose-mat` | Mass casualty assessment tool |
|
||||
| `wifi-densepose-hardware` | ESP32 protocol, TDM, channel hopping |
|
||||
| `wifi-densepose-ruvector` | Cross-viewpoint fusion, attention |
|
||||
| `wifi-densepose-api` | REST API (Axum) |
|
||||
| `wifi-densepose-db` | Postgres/SQLite/Redis persistence |
|
||||
| `wifi-densepose-config` | Configuration management |
|
||||
| `wifi-densepose-wasm` | Browser WASM bindings |
|
||||
| `wifi-densepose-cli` | Command-line interface |
|
||||
| `wifi-densepose-sensing-server` | Real-time sensing server |
|
||||
| `wifi-densepose-wifiscan` | Multi-BSSID scanning |
|
||||
| `wifi-densepose-vitals` | Vital sign extraction |
|
||||
| `wifi-densepose-desktop` | Tauri desktop application |
|
||||
|
||||
---
|
||||
|
||||
## 10. UI Design System (ADR-053)
|
||||
|
||||
### 10.1 Pages
|
||||
- **Dashboard**: Overview, node status, quick actions
|
||||
- **Discovery**: Network scanning interface
|
||||
- **Nodes**: Node management and configuration
|
||||
- **Flash**: Serial firmware flashing
|
||||
- **OTA**: Over-the-air update management
|
||||
- **Edge Modules**: WASM deployment
|
||||
- **Sensing**: Real-time monitoring with server control
|
||||
- **Mesh View**: Network topology visualization
|
||||
- **Settings**: Application configuration
|
||||
|
||||
### 10.2 Components
|
||||
- **StatusBadge**: Health indicator
|
||||
- **NodeCard**: Node information display
|
||||
- **LogViewer**: Real-time log streaming
|
||||
- **ActivityFeed**: Sensing data visualization
|
||||
- **ProgressBar**: Operation progress
|
||||
- **ConfigForm**: Settings input
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- **Unified interface**: All capabilities in one application
|
||||
- **Bundled deployment**: Single package with server included
|
||||
- **Real-time feedback**: WebSocket-based live updates
|
||||
- **Cross-platform**: macOS, Windows, Linux support
|
||||
- **Extensible**: WASM modules, custom models, API access
|
||||
|
||||
### Negative
|
||||
- **Larger bundle**: ~6MB app + ~2.6MB server
|
||||
- **Complexity**: Many features require learning curve
|
||||
- **Hardware dependency**: Full functionality requires ESP32 nodes
|
||||
|
||||
### Neutral
|
||||
- Documentation required for all features
|
||||
- Training materials needed for advanced capabilities
|
||||
- Community contributions welcome
|
||||
|
||||
## Related ADRs
|
||||
- ADR-053: UI Design System
|
||||
- ADR-054: Desktop Full Implementation
|
||||
- ADR-055: Integrated Sensing Server
|
||||
- ADR-023: 8-Phase Training Pipeline
|
||||
- ADR-016: RuVector Integration
|
||||
@@ -1,82 +0,0 @@
|
||||
# ADR-057: Firmware CSI Build Guard and sdkconfig.defaults
|
||||
|
||||
| Field | Value |
|
||||
|-------------|---------------------------------------------|
|
||||
| **Status** | Accepted |
|
||||
| **Date** | 2026-03-12 |
|
||||
| **Authors** | ruv |
|
||||
| **Issues** | #223, #238, #234, #210, #190 |
|
||||
|
||||
## Context
|
||||
|
||||
Multiple GitHub issues (#223, #238, #234, #210, #190) report firmware problems
|
||||
that fall into two categories:
|
||||
|
||||
1. **CSI not enabled at runtime** — The committed `sdkconfig` had
|
||||
`# CONFIG_ESP_WIFI_CSI_ENABLED is not set` (line 1135), meaning users who
|
||||
built from source or used pre-built binaries got the runtime error:
|
||||
`E (6700) wifi:CSI not enabled in menuconfig!`
|
||||
|
||||
Root cause: `sdkconfig.defaults.template` existed with the correct setting
|
||||
(`CONFIG_ESP_WIFI_CSI_ENABLED=y`) but ESP-IDF only reads
|
||||
`sdkconfig.defaults` — not `.template` suffixed files. No `sdkconfig.defaults`
|
||||
file was committed.
|
||||
|
||||
2. **Unsupported ESP32 variants** — Users attempting to use original ESP32
|
||||
(D0WD) and ESP32-C3 boards. The firmware targets ESP32-S3 only
|
||||
(`CONFIG_IDF_TARGET="esp32s3"`, Xtensa architecture) and this was not
|
||||
surfaced clearly enough in documentation or build errors.
|
||||
|
||||
## Decision
|
||||
|
||||
### Fix 1: Commit `sdkconfig.defaults` (not just template)
|
||||
|
||||
Copy `sdkconfig.defaults.template` → `sdkconfig.defaults` so that ESP-IDF
|
||||
applies the correct defaults (including `CONFIG_ESP_WIFI_CSI_ENABLED=y`)
|
||||
automatically when `sdkconfig` is regenerated.
|
||||
|
||||
### Fix 2: `#error` compile-time guard in `csi_collector.c`
|
||||
|
||||
Add a preprocessor guard:
|
||||
|
||||
```c
|
||||
#ifndef CONFIG_ESP_WIFI_CSI_ENABLED
|
||||
#error "CONFIG_ESP_WIFI_CSI_ENABLED must be set in sdkconfig."
|
||||
#endif
|
||||
```
|
||||
|
||||
This turns a confusing runtime crash into a clear compile-time error with
|
||||
instructions on how to fix it.
|
||||
|
||||
### Fix 3: Fix committed `sdkconfig`
|
||||
|
||||
Change line 1135 from `# CONFIG_ESP_WIFI_CSI_ENABLED is not set` to
|
||||
`CONFIG_ESP_WIFI_CSI_ENABLED=y`.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Positive**: New builds will always have CSI enabled. Users building from
|
||||
source will get a clear compile error if CSI is somehow disabled.
|
||||
- **Positive**: Pre-built release binaries will include CSI support.
|
||||
- **Neutral**: Original ESP32 and ESP32-C3 remain unsupported. This is by
|
||||
design — only ESP32-S3 has the CSI API surface we depend on. Future ADRs
|
||||
may address multi-target support if demand warrants it.
|
||||
- **Negative**: None identified.
|
||||
|
||||
## Hardware Support Matrix
|
||||
|
||||
| Variant | CSI Support | Firmware Target | Status |
|
||||
|--------------|-------------|-----------------|---------------|
|
||||
| ESP32-S3 | Yes | Yes | Supported |
|
||||
| ESP32 (orig) | Partial | No | Unsupported |
|
||||
| ESP32-C3 | Yes (IDF 5.1+) | No | Unsupported |
|
||||
| ESP32-C6 | Yes | No | Unsupported |
|
||||
|
||||
## Notes
|
||||
|
||||
- ESP32-C3 and C6 use RISC-V architecture; a separate build target
|
||||
(`idf.py set-target esp32c3`) would be needed.
|
||||
- Original ESP32 has limited CSI (no STBC HT-LTF2, fewer subcarriers).
|
||||
- Users on unsupported hardware can still write custom firmware using the
|
||||
ADR-018 binary frame format (magic `0xC5110001`) for interop with the
|
||||
Rust aggregator.
|
||||
@@ -1,392 +0,0 @@
|
||||
# ADR-058: Dual-Modal WASM Browser Pose Estimation — Live Video + WiFi CSI Fusion
|
||||
|
||||
- **Status**: Proposed
|
||||
- **Date**: 2026-03-12
|
||||
- **Deciders**: ruv
|
||||
- **Tags**: wasm, browser, cnn, pose-estimation, ruvector, video, multimodal, fusion
|
||||
|
||||
## Context
|
||||
|
||||
WiFi-DensePose estimates human poses from WiFi CSI (Channel State Information).
|
||||
The `ruvector-cnn` crate provides a pure Rust CNN (MobileNet-V3) with WASM bindings.
|
||||
Both modalities exist independently — what's missing is **fusing live webcam video
|
||||
with WiFi CSI** in a single browser demo to achieve robust pose estimation that
|
||||
works even when one modality degrades (occlusion, signal noise, poor lighting).
|
||||
|
||||
Existing assets:
|
||||
|
||||
1. **`wifi-densepose-wasm`** — CSI signal processing compiled to WASM
|
||||
2. **`wifi-densepose-sensing-server`** — Axum server streaming live CSI via WebSocket
|
||||
3. **`ruvector-cnn`** — Pure Rust CNN with MobileNet-V3 backbones, SIMD, contrastive learning
|
||||
4. **`ruvector-cnn-wasm`** — wasm-bindgen bindings: `WasmCnnEmbedder`, `SimdOps`, `LayerOps`, contrastive losses
|
||||
5. **`vendor/ruvector/examples/wasm-vanilla/`** — Reference vanilla JS WASM example
|
||||
|
||||
Research shows multi-modal fusion (camera + WiFi) significantly outperforms either alone:
|
||||
- Camera fails under occlusion, poor lighting, privacy constraints
|
||||
- WiFi CSI fails with signal noise, multipath, low spatial resolution
|
||||
- Fusion compensates: WiFi provides through-wall coverage, camera provides fine-grained detail
|
||||
|
||||
## Decision
|
||||
|
||||
Build a **dual-modal browser demo** at `examples/wasm-browser-pose/` that:
|
||||
|
||||
1. Captures **live webcam video** via `getUserMedia` API
|
||||
2. Receives **live WiFi CSI** via WebSocket from the sensing server
|
||||
3. Processes **both streams** through separate CNN pipelines in `ruvector-cnn-wasm`
|
||||
4. **Fuses embeddings** with learned attention weights for combined pose estimation
|
||||
5. Renders **video overlay** with skeleton + WiFi confidence heatmap on Canvas
|
||||
6. Runs entirely in the browser — all inference client-side via WASM
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Browser │
|
||||
│ │
|
||||
│ ┌────────────┐ ┌────────────────┐ ┌───────────────────┐ │
|
||||
│ │ getUserMedia│───▶│ Video Frame │───▶│ CNN WASM │ │
|
||||
│ │ (Webcam) │ │ Capture │ │ (Visual Embedder) │ │
|
||||
│ └────────────┘ │ 224×224 RGB │ │ → 512-dim │ │
|
||||
│ └────────────────┘ └────────┬──────────┘ │
|
||||
│ │ │
|
||||
│ visual_embedding │
|
||||
│ │ │
|
||||
│ ┌──────▼──────┐ │
|
||||
│ ┌────────────┐ ┌────────────────┐ │ │ │
|
||||
│ │ WebSocket │───▶│ CSI WASM │ │ Attention │ │
|
||||
│ │ Client │ │ (densepose- │ │ Fusion │ │
|
||||
│ │ │ │ wasm) │ │ Module │ │
|
||||
│ └────────────┘ └───────┬────────┘ │ │ │
|
||||
│ │ └──────┬──────┘ │
|
||||
│ ┌───────▼────────┐ │ │
|
||||
│ │ CNN WASM │ fused_embedding │
|
||||
│ │ (CSI Embedder) │ │ │
|
||||
│ │ → 512-dim │ ┌──────▼──────┐ │
|
||||
│ └───────┬────────┘ │ Pose │ │
|
||||
│ │ │ Decoder │ │
|
||||
│ csi_embedding │ → 17 kpts │ │
|
||||
│ │ └──────┬──────┘ │
|
||||
│ └──────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────────┐ ┌─────▼──────┐ │
|
||||
│ │ Video Canvas │◀────────│ Overlay │ │
|
||||
│ │ + Skeleton │ │ Renderer │ │
|
||||
│ │ + Heatmap │ └────────────┘ │
|
||||
│ └──────────────┘ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
▲ ▲
|
||||
│ getUserMedia │ WebSocket
|
||||
│ (camera) │ (ws://host:3030/ws/csi)
|
||||
│ │
|
||||
┌────┴────┐ ┌───────┴─────────┐
|
||||
│ Webcam │ │ Sensing Server │
|
||||
└─────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### Dual Pipeline Design
|
||||
|
||||
Two parallel CNN pipelines run on each frame tick (~30 FPS):
|
||||
|
||||
| Pipeline | Input | Preprocessing | CNN Config | Output |
|
||||
|----------|-------|---------------|------------|--------|
|
||||
| **Visual** | Webcam frame (640×480) | Resize to 224×224 RGB, ImageNet normalize | MobileNet-V3 Small, 512-dim | Visual embedding |
|
||||
| **CSI** | CSI frame (ADR-018 binary) | Amplitude/phase/delta → 224×224 pseudo-RGB | MobileNet-V3 Small, 512-dim | CSI embedding |
|
||||
|
||||
Both use the same `WasmCnnEmbedder` but with separate instances and weight sets.
|
||||
|
||||
### Fusion Strategy
|
||||
|
||||
**Learned attention-weighted fusion** combines the two 512-dim embeddings:
|
||||
|
||||
```javascript
|
||||
// Attention fusion: learn which modality to trust per-dimension
|
||||
// α ∈ [0,1]^512 — attention weights (shipped as JSON, trained offline)
|
||||
// visual_emb, csi_emb ∈ R^512
|
||||
|
||||
function fuseEmbeddings(visual_emb, csi_emb, attention_weights) {
|
||||
const fused = new Float32Array(512);
|
||||
for (let i = 0; i < 512; i++) {
|
||||
const α = attention_weights[i];
|
||||
fused[i] = α * visual_emb[i] + (1 - α) * csi_emb[i];
|
||||
}
|
||||
return fused;
|
||||
}
|
||||
```
|
||||
|
||||
**Dynamic confidence gating** adjusts fusion based on signal quality:
|
||||
|
||||
| Condition | Behavior |
|
||||
|-----------|----------|
|
||||
| Good video + good CSI | Balanced fusion (α ≈ 0.5) |
|
||||
| Poor lighting / occlusion | CSI-dominant (α → 0, WiFi takes over) |
|
||||
| CSI noise / no ESP32 | Video-dominant (α → 1, camera only) |
|
||||
| Video-only mode (no WiFi) | α = 1.0, pure visual CNN pose estimation |
|
||||
| CSI-only mode (no camera) | α = 0.0, pure WiFi pose estimation |
|
||||
|
||||
Quality detection:
|
||||
- **Video quality**: Frame brightness variance (dark = low quality), motion blur score
|
||||
- **CSI quality**: Signal-to-noise ratio from `wifi-densepose-wasm`, coherence gate output
|
||||
|
||||
### CSI-to-Image Encoding
|
||||
|
||||
CSI data encoded as 3-channel pseudo-image for the CSI CNN pipeline:
|
||||
|
||||
| Channel | Data | Normalization |
|
||||
|---------|------|---------------|
|
||||
| R | CSI amplitude (subcarrier × time window) | Min-max to [0, 255] |
|
||||
| G | CSI phase (unwrapped, subcarrier × time window) | Min-max to [0, 255] |
|
||||
| B | Temporal difference (frame-to-frame Δ amplitude) | Abs, min-max to [0, 255] |
|
||||
|
||||
### Video Processing
|
||||
|
||||
Webcam frames processed through standard ImageNet pipeline:
|
||||
|
||||
```javascript
|
||||
// Capture frame from video element
|
||||
const frame = captureVideoFrame(videoElement, 224, 224); // Returns Uint8Array RGB
|
||||
|
||||
// ImageNet normalization happens inside WasmCnnEmbedder.extract()
|
||||
const visual_embedding = visual_embedder.extract(frame, 224, 224);
|
||||
```
|
||||
|
||||
### Pose Keypoint Mapping
|
||||
|
||||
17 COCO-format keypoints decoded from the fused 512-dim embedding:
|
||||
|
||||
```
|
||||
0: nose 1: left_eye 2: right_eye
|
||||
3: left_ear 4: right_ear 5: left_shoulder
|
||||
6: right_shoulder 7: left_elbow 8: right_elbow
|
||||
9: left_wrist 10: right_wrist 11: left_hip
|
||||
12: right_hip 13: left_knee 14: right_knee
|
||||
15: left_ankle 16: right_ankle
|
||||
```
|
||||
|
||||
Each keypoint decoded as (x, y, confidence) = 51 values from the 512-dim embedding
|
||||
via a learned linear projection.
|
||||
|
||||
### Operating Modes
|
||||
|
||||
The demo supports three modes, selectable in the UI:
|
||||
|
||||
| Mode | Video | CSI | Fusion | Use Case |
|
||||
|------|-------|-----|--------|----------|
|
||||
| **Dual (default)** | ✅ | ✅ | Attention-weighted | Best accuracy, full demo |
|
||||
| **Video Only** | ✅ | ❌ | α = 1.0 | No ESP32 available, quick demo |
|
||||
| **CSI Only** | ❌ | ✅ | α = 0.0 | Privacy mode, through-wall sensing |
|
||||
|
||||
**Video Only mode works without any hardware** — just a webcam — making the demo
|
||||
instantly accessible for anyone wanting to try it.
|
||||
|
||||
### File Layout
|
||||
|
||||
```
|
||||
examples/wasm-browser-pose/
|
||||
├── index.html # Single-page app (vanilla JS, no bundler)
|
||||
├── js/
|
||||
│ ├── app.js # Main entry, mode selection, orchestration
|
||||
│ ├── video-capture.js # getUserMedia, frame extraction, quality detection
|
||||
│ ├── csi-processor.js # WebSocket CSI client, frame parsing, pseudo-image encoding
|
||||
│ ├── fusion.js # Attention-weighted embedding fusion, confidence gating
|
||||
│ ├── pose-decoder.js # Fused embedding → 17 keypoints
|
||||
│ └── canvas-renderer.js # Video overlay, skeleton, CSI heatmap, confidence bars
|
||||
├── data/
|
||||
│ ├── visual-weights.json # Visual CNN → embedding projection (placeholder until trained)
|
||||
│ ├── csi-weights.json # CSI CNN → embedding projection (placeholder until trained)
|
||||
│ ├── fusion-weights.json # Attention fusion α weights (512 values)
|
||||
│ └── pose-weights.json # Fused embedding → keypoint projection
|
||||
├── css/
|
||||
│ └── style.css # Dark theme UI styling
|
||||
├── pkg/ # Built WASM packages (gitignored, built by script)
|
||||
│ ├── wifi_densepose_wasm/
|
||||
│ └── ruvector_cnn_wasm/
|
||||
├── build.sh # wasm-pack build script for both packages
|
||||
└── README.md # Setup and usage instructions
|
||||
```
|
||||
|
||||
### Build Pipeline
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# build.sh — builds both WASM packages into pkg/
|
||||
|
||||
set -e
|
||||
|
||||
# Build wifi-densepose-wasm (CSI processing)
|
||||
wasm-pack build ../../v2/crates/wifi-densepose-wasm \
|
||||
--target web --out-dir "$(pwd)/pkg/wifi_densepose_wasm" --no-typescript
|
||||
|
||||
# Build ruvector-cnn-wasm (CNN inference for both video and CSI)
|
||||
wasm-pack build ../../vendor/ruvector/crates/ruvector-cnn-wasm \
|
||||
--target web --out-dir "$(pwd)/pkg/ruvector_cnn_wasm" --no-typescript
|
||||
|
||||
echo "Build complete. Serve with: python3 -m http.server 8080"
|
||||
```
|
||||
|
||||
### UI Layout
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ WiFi-DensePose — Live Dual-Modal Pose Estimation │
|
||||
│ [Dual Mode ▼] [⚙ Settings] FPS: 28 ◉ Live │
|
||||
├───────────────────────────┬─────────────────────────────┤
|
||||
│ │ │
|
||||
│ ┌───────────────────┐ │ ┌───────────────────┐ │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ Video + Skeleton │ │ │ CSI Heatmap │ │
|
||||
│ │ Overlay │ │ │ (amplitude × │ │
|
||||
│ │ (main canvas) │ │ │ subcarrier) │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ └───────────────────┘ │ └───────────────────┘ │
|
||||
│ │ │
|
||||
├───────────────────────────┴─────────────────────────────┤
|
||||
│ Fusion Confidence: ████████░░ 78% │
|
||||
│ Video: ██████████ 95% │ CSI: ██████░░░░ 61% │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ Embedding Space (2D projection) │ │
|
||||
│ │ · · · │ │
|
||||
│ │ · · · · · · (color = pose cluster) │ │
|
||||
│ │ · · · · │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Latency: Video 12ms │ CSI 8ms │ Fusion 1ms │ Total 21ms│
|
||||
│ [▶ Record] [📷 Snapshot] [Confidence: ████ 0.6] │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### WASM Module Structure
|
||||
|
||||
| Package | Source Crate | Provides | Size (est.) |
|
||||
|---------|-------------|----------|-------------|
|
||||
| `wifi_densepose_wasm` | `wifi-densepose-wasm` | CSI frame parsing, signal processing, feature extraction | ~200KB |
|
||||
| `ruvector_cnn_wasm` | `ruvector-cnn-wasm` | `WasmCnnEmbedder` (×2 instances), `SimdOps`, `LayerOps`, contrastive losses | ~150KB |
|
||||
|
||||
Two `WasmCnnEmbedder` instances are created — one for video frames, one for CSI pseudo-images.
|
||||
They share the same WASM module but have independent state.
|
||||
|
||||
### Browser API Requirements
|
||||
|
||||
| API | Purpose | Required | Fallback |
|
||||
|-----|---------|----------|----------|
|
||||
| `getUserMedia` | Webcam capture | For video mode | CSI-only mode |
|
||||
| WebAssembly | CNN inference | Yes | None (hard requirement) |
|
||||
| WASM SIMD128 | Accelerated inference | No | Scalar fallback (~2× slower) |
|
||||
| WebSocket | CSI data stream | For CSI mode | Video-only mode |
|
||||
| Canvas 2D | Rendering | Yes | None |
|
||||
| `requestAnimationFrame` | Render loop | Yes | `setTimeout` fallback |
|
||||
| ES Modules | Code organization | Yes | None |
|
||||
|
||||
Target: Chrome 89+, Firefox 89+, Safari 15+, Edge 89+
|
||||
|
||||
### Performance Budget
|
||||
|
||||
| Stage | Target Latency | Notes |
|
||||
|-------|---------------|-------|
|
||||
| Video frame capture + resize | <3ms | `drawImage` to offscreen canvas |
|
||||
| Video CNN embedding | <15ms | 224×224 RGB → 512-dim |
|
||||
| CSI receive + parse | <2ms | Binary WebSocket message |
|
||||
| CSI pseudo-image encoding | <3ms | Amplitude/phase/delta channels |
|
||||
| CSI CNN embedding | <15ms | 224×224 pseudo-RGB → 512-dim |
|
||||
| Attention fusion | <1ms | Element-wise weighted sum |
|
||||
| Pose decoding | <1ms | Linear projection |
|
||||
| Canvas overlay render | <3ms | Video + skeleton + heatmap |
|
||||
| **Total (dual mode)** | **<33ms** | **30 FPS capable** |
|
||||
| **Total (video only)** | **<22ms** | **45 FPS capable** |
|
||||
|
||||
Note: Video and CSI CNN pipelines can run in parallel using Web Workers,
|
||||
reducing dual-mode latency to ~max(15, 15) + 5 = ~20ms (50 FPS).
|
||||
|
||||
### Contrastive Learning Integration
|
||||
|
||||
The demo optionally shows real-time contrastive learning in the browser:
|
||||
|
||||
- **InfoNCE loss** (`WasmInfoNCELoss`): Compare video vs CSI embeddings for the same pose — trains cross-modal alignment
|
||||
- **Triplet loss** (`WasmTripletLoss`): Push apart different poses, pull together same pose across modalities
|
||||
- **SimdOps**: Accelerated dot products for real-time similarity computation
|
||||
- **Embedding space panel**: Live 2D projection shows video and CSI embeddings converging when viewing the same person
|
||||
|
||||
### Relationship to Existing Crates
|
||||
|
||||
| Existing Crate | Role in This Demo |
|
||||
|---------------|-------------------|
|
||||
| `ruvector-cnn-wasm` | CNN inference for **both** video frames and CSI pseudo-images |
|
||||
| `wifi-densepose-wasm` | CSI frame parsing and signal processing |
|
||||
| `wifi-densepose-sensing-server` | WebSocket CSI data source |
|
||||
| `wifi-densepose-core` | ADR-018 frame format definitions |
|
||||
| `ruvector-cnn` | Underlying MobileNet-V3, layers, contrastive learning |
|
||||
|
||||
No new Rust crates are needed. The example is pure HTML/JS consuming existing WASM packages.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **Instant demo**: Video-only mode works with just a webcam — no ESP32 needed
|
||||
- **Multi-modal showcase**: Demonstrates camera + WiFi fusion, the core innovation of the project
|
||||
- **Graceful degradation**: Works with video-only, CSI-only, or both
|
||||
- **Through-wall capability**: CSI mode shows pose estimation where cameras cannot reach
|
||||
- **Zero-install**: Anyone with a browser can try it
|
||||
- **Training data collection**: Can record paired (video, CSI) data for offline model training
|
||||
- **Reusable**: JS modules embed directly in the Tauri desktop app's webview
|
||||
|
||||
### Negative
|
||||
|
||||
- **Model weights**: Requires offline-trained weights for visual CNN, CSI CNN, fusion, and pose decoder (~200KB total JSON)
|
||||
- **WASM size**: Two WASM modules total ~350KB (acceptable)
|
||||
- **No GPU**: CPU-only WASM inference; adequate at 224×224 but limits resolution scaling
|
||||
- **Camera privacy**: Video mode requires camera permission (mitigated: CSI-only mode available)
|
||||
- **Two CNN instances**: Memory footprint doubles vs single-modal (~10MB total, acceptable for desktop browsers)
|
||||
|
||||
### Risks
|
||||
|
||||
- **Cross-modal alignment**: Video and CSI embeddings must be trained jointly for fusion to work;
|
||||
without proper training, fusion may be worse than either modality alone
|
||||
- **Latency on mobile**: Dual CNN on mobile browsers may exceed 33ms; implement automatic quality reduction
|
||||
- **WebSocket drops**: Network jitter → CSI frame gaps; buffer last 3 frames, interpolate missing data
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
1. **Phase 1 — Scaffold**: File layout, build.sh, index.html shell, mode selector UI
|
||||
2. **Phase 2 — Video pipeline**: getUserMedia → frame capture → CNN embedding → basic pose display
|
||||
3. **Phase 3 — CSI pipeline**: WebSocket client → CSI parsing → pseudo-image → CNN embedding
|
||||
4. **Phase 4 — Fusion**: Attention-weighted combination, confidence gating, mode switching
|
||||
5. **Phase 5 — Pose decoder**: Linear projection with placeholder weights → 17 keypoints
|
||||
6. **Phase 6 — Overlay renderer**: Video canvas with skeleton overlay, CSI heatmap panel
|
||||
7. **Phase 7 — Training**: Use `wifi-densepose-train` to generate real weights for both CNNs + fusion + decoder
|
||||
8. **Phase 8 — Contrastive demo**: Embedding space visualization, cross-modal similarity display
|
||||
9. **Phase 9 — Web Workers**: Move CNN inference to workers for parallel video + CSI processing
|
||||
10. **Phase 10 — Polish**: Recording, snapshots, adaptive quality, mobile optimization
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### 1. CSI-Only (No Video)
|
||||
Rejected: Misses the opportunity to show multi-modal fusion and makes the demo less
|
||||
accessible (requires ESP32 hardware). Video-only mode as a fallback is strictly better.
|
||||
|
||||
### 2. Server-Side Video Inference
|
||||
Rejected: Adds latency, requires webcam stream upload (privacy concern), and defeats
|
||||
the WASM-first architecture. All inference must be client-side.
|
||||
|
||||
### 3. TensorFlow.js for Video, ruvector-cnn-wasm for CSI
|
||||
Rejected: Would require two different ML frameworks. Using `ruvector-cnn-wasm` for both
|
||||
keeps a single WASM module, unified embedding space, and simpler fusion.
|
||||
|
||||
### 4. Pre-recorded Video Demo
|
||||
Rejected: Live webcam input is far more compelling for demonstrations.
|
||||
Pre-recorded mode can be added as a secondary option.
|
||||
|
||||
### 5. React/Vue Framework
|
||||
Rejected: Adds build tooling. Vanilla JS + ES modules keeps the demo self-contained.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR-018: Binary CSI Frame Format](ADR-018-binary-csi-frame-format.md)
|
||||
- [ADR-024: Contrastive CSI Embedding / AETHER](ADR-024-contrastive-csi-embedding.md)
|
||||
- [ADR-055: Integrated Sensing Server](ADR-055-integrated-sensing-server.md)
|
||||
- `vendor/ruvector/crates/ruvector-cnn/src/lib.rs` — CNN embedder implementation
|
||||
- `vendor/ruvector/crates/ruvector-cnn-wasm/src/lib.rs` — WASM bindings
|
||||
- `vendor/ruvector/examples/wasm-vanilla/index.html` — Reference vanilla JS WASM pattern
|
||||
- Person-in-WiFi: Fine-grained Person Perception using WiFi (ICCV 2019) — camera+WiFi fusion precedent
|
||||
- WiPose: Multi-Person WiFi Pose Estimation (TMC 2022) — cross-modal embedding approach
|
||||
@@ -1,83 +0,0 @@
|
||||
# ADR-059: Live ESP32 CSI Pipeline Integration
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-03-12
|
||||
|
||||
## Context
|
||||
|
||||
ADR-058 established a dual-modal browser demo combining webcam video and WiFi CSI for pose estimation. However, it used simulated CSI data. To demonstrate real-world capability, we need an end-to-end pipeline from physical ESP32 hardware through to the browser visualization.
|
||||
|
||||
The ESP32-S3 firmware (`firmware/esp32-csi-node/`) already supports CSI collection and UDP streaming (ADR-018). The sensing server (`wifi-densepose-sensing-server`) already supports UDP ingestion and WebSocket bridging. The missing piece was connecting these components and enabling the browser demo to consume live data.
|
||||
|
||||
## Decision
|
||||
|
||||
Implement a complete live CSI pipeline:
|
||||
|
||||
```
|
||||
ESP32-S3 (CSI capture) → UDP:5005 → sensing-server (Rust/Axum) → WS:8765 → browser demo
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
1. **ESP32 Firmware** — Rebuilt with native Windows ESP-IDF v5.4.0 toolchain (no Docker). Configured for target network and PC IP via `sdkconfig`. Helper scripts added:
|
||||
- `build_firmware.ps1` — Sets up IDF environment, cleans, builds, and flashes
|
||||
- `read_serial.ps1` — Serial monitor with DTR/RTS reset capability
|
||||
|
||||
2. **Sensing Server** — `wifi-densepose-sensing-server` started with:
|
||||
- `--source esp32` — Expect real ESP32 UDP frames
|
||||
- `--bind-addr 0.0.0.0` — Accept connections from any interface
|
||||
- `--ui-path <path>` — Serve the demo UI via HTTP
|
||||
|
||||
3. **Browser Demo** — `main.js` updated to auto-connect to `ws://localhost:8765/ws/sensing` on page load. Falls back to simulated CSI if the WebSocket is unavailable (GitHub Pages).
|
||||
|
||||
### Network Configuration
|
||||
|
||||
The ESP32 sends UDP packets to a configured target IP. If the PC's IP doesn't match the firmware's compiled target, a secondary IP alias can be added:
|
||||
|
||||
```powershell
|
||||
# PowerShell (Admin)
|
||||
New-NetIPAddress -IPAddress 192.168.1.100 -PrefixLength 24 -InterfaceAlias "Wi-Fi"
|
||||
```
|
||||
|
||||
### Data Flow
|
||||
|
||||
| Stage | Protocol | Format | Rate |
|
||||
|-------|----------|--------|------|
|
||||
| ESP32 → Server | UDP | ADR-018 binary frame (magic `0xC5110001`, I/Q pairs) | ~100 Hz |
|
||||
| Server → Browser | WebSocket | ADR-018 binary frame (forwarded) | ~10 Hz (tick-ms=100) |
|
||||
| Browser decode | JavaScript | Float32 amplitude/phase arrays | Per frame |
|
||||
|
||||
### Build Environment (Windows)
|
||||
|
||||
ESP-IDF v5.4.0 on Windows requires:
|
||||
- IDF_PATH pointing to the ESP-IDF framework
|
||||
- IDF_TOOLS_PATH pointing to toolchain binaries
|
||||
- MSYS/MinGW environment variables removed (ESP-IDF rejects them)
|
||||
- Python venv from ESP-IDF tools for `idf.py` execution
|
||||
|
||||
The `build_firmware.ps1` script handles all of this automatically.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- First end-to-end demonstration of real WiFi CSI → pose estimation in a browser
|
||||
- No Docker required for firmware builds on Windows
|
||||
- Demo gracefully degrades to simulated CSI when no server is available
|
||||
- Same demo works on GitHub Pages (simulated) and locally (live ESP32)
|
||||
|
||||
### Negative
|
||||
- ESP32 target IP is compiled into firmware; changing it requires a rebuild or NVS override
|
||||
- Windows firewall may block UDP:5005; user must allow it
|
||||
- Mixed content restrictions prevent HTTPS pages from connecting to ws:// (local only)
|
||||
|
||||
## Related
|
||||
|
||||
- [ADR-018](ADR-018-esp32-dev-implementation.md) — ESP32 CSI frame format and UDP streaming
|
||||
- [ADR-058](ADR-058-ruvector-wasm-browser-pose-example.md) — Dual-modal WASM browser pose demo
|
||||
- [ADR-039](ADR-039-edge-intelligence-framework.md) — Edge intelligence on ESP32
|
||||
- Issue [#245](https://github.com/ruvnet/RuView/issues/245) — Tracking issue
|
||||
@@ -1,59 +0,0 @@
|
||||
# ADR-060: Provision Channel Override and MAC Address Filtering
|
||||
|
||||
- **Status:** Accepted
|
||||
- **Date:** 2026-03-12
|
||||
- **Issues:** [#247](https://github.com/ruvnet/RuView/issues/247), [#229](https://github.com/ruvnet/RuView/issues/229)
|
||||
|
||||
## Context
|
||||
|
||||
Two related provisioning gaps were reported by users:
|
||||
|
||||
1. **Channel mismatch (Issue #247):** The CSI collector initializes on the
|
||||
Kconfig default channel (typically 6), even when the ESP32 connects to an AP
|
||||
on a different channel (e.g. 11). On managed networks where the user cannot
|
||||
change the router channel, this makes nodes undiscoverable. The
|
||||
`provision.py` script has no `--channel` argument.
|
||||
|
||||
2. **Missing MAC filter (Issue #229):** The v0.2.0 release notes documented a
|
||||
`--filter-mac` argument for `provision.py`, but it was never implemented.
|
||||
The firmware's CSI callback accepts frames from all sources, causing signal
|
||||
mixing in multi-AP environments.
|
||||
|
||||
## Decision
|
||||
|
||||
### Channel configuration
|
||||
|
||||
- Add `--channel` argument to `provision.py` that writes a `csi_channel` key
|
||||
(u8) to NVS.
|
||||
- In `nvs_config.c`, read the `csi_channel` key and override
|
||||
`channel_list[0]` when present.
|
||||
- In `csi_collector_init()`, after WiFi connects, auto-detect the AP channel
|
||||
via `esp_wifi_sta_get_ap_info()` and use it as the default CSI channel when
|
||||
no NVS override is set. This ensures the CSI collector always matches the
|
||||
connected AP's channel without requiring manual provisioning.
|
||||
|
||||
### MAC address filtering
|
||||
|
||||
- Add `--filter-mac` argument to `provision.py` that writes a `filter_mac`
|
||||
key (6-byte blob) to NVS.
|
||||
- In `nvs_config.h`, add a `filter_mac[6]` field and `filter_mac_set` flag.
|
||||
- In `nvs_config.c`, read the `filter_mac` blob from NVS.
|
||||
- In the CSI callback (`wifi_csi_callback`), if `filter_mac_set` is true,
|
||||
compare the source MAC from the received frame against the configured MAC
|
||||
and drop non-matching frames.
|
||||
|
||||
### Provisioning flow
|
||||
|
||||
```
|
||||
python provision.py --port COM7 --channel 11
|
||||
python provision.py --port COM7 --filter-mac "AA:BB:CC:DD:EE:FF"
|
||||
python provision.py --port COM7 --channel 11 --filter-mac "AA:BB:CC:DD:EE:FF"
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
- Users on managed networks can force the CSI channel to match their AP
|
||||
- Multi-AP environments can filter CSI to a single source
|
||||
- Auto-channel detection eliminates the most common misconfiguration
|
||||
- Backward compatible: existing provisioned nodes without these keys behave
|
||||
as before (use Kconfig default channel, accept all MACs)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,199 +0,0 @@
|
||||
# ADR-062: QEMU ESP32-S3 Swarm Configurator
|
||||
|
||||
| Field | Value |
|
||||
|-------------|------------------------------------------------|
|
||||
| **Status** | Accepted |
|
||||
| **Date** | 2026-03-14 |
|
||||
| **Authors** | RuView Team |
|
||||
| **Relates** | ADR-061 (QEMU testing platform), ADR-060 (channel/MAC filter), ADR-018 (binary frame), ADR-039 (edge intel) |
|
||||
|
||||
## Glossary
|
||||
|
||||
| Term | Definition |
|
||||
|------|-----------|
|
||||
| Swarm | A group of N QEMU ESP32-S3 instances running simultaneously |
|
||||
| Topology | How nodes are connected: star, mesh, line, ring |
|
||||
| Role | Node function: `sensor` (collects CSI), `coordinator` (aggregates + forwards), `gateway` (bridges to host) |
|
||||
| Scenario matrix | Cross-product of topology × node count × NVS config × mock scenario |
|
||||
| Health oracle | Python process that monitors all node UART logs and declares swarm health |
|
||||
|
||||
## Context
|
||||
|
||||
ADR-061 Layer 3 provides a basic multi-node mesh test: N identical nodes with sequential TDM slots connected via a Linux bridge. This is useful but limited:
|
||||
|
||||
1. **All nodes are identical** — real deployments have heterogeneous roles (sensor, coordinator, gateway)
|
||||
2. **Single topology** — only fully-connected bridge; no star, line, or ring topologies
|
||||
3. **No scenario variation per node** — all nodes run the same mock CSI scenario
|
||||
4. **Manual configuration** — each test requires hand-editing env vars and arguments
|
||||
5. **No swarm-level health monitoring** — validation checks individual nodes, not collective behavior
|
||||
6. **No cross-node timing validation** — TDM slot ordering and inter-frame gaps aren't verified
|
||||
|
||||
Real WiFi-DensePose deployments use 3-8 ESP32-S3 nodes in various topologies. A single coordinator aggregates CSI from multiple sensors. The firmware must handle TDM conflicts, missing nodes, role-based behavior differences, and network partitions — none of which ADR-061 Layer 3 tests.
|
||||
|
||||
## Decision
|
||||
|
||||
Build a **QEMU Swarm Configurator** — a YAML-driven tool that defines multi-node test scenarios declaratively and orchestrates them under QEMU with swarm-level validation.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ swarm_config.yaml │
|
||||
│ nodes: [{role: sensor, scenario: 2, channel: 6}] │
|
||||
│ topology: star │
|
||||
│ duration: 60s │
|
||||
│ assertions: [all_nodes_boot, tdm_no_collision, ...] │
|
||||
└──────────────────────┬──────────────────────────────┘
|
||||
│
|
||||
┌────────────▼────────────┐
|
||||
│ qemu_swarm.py │
|
||||
│ (orchestrator) │
|
||||
└───┬────┬────┬───┬──────┘
|
||||
│ │ │ │
|
||||
┌────▼┐ ┌▼──┐ ▼ ┌▼────┐
|
||||
│Node0│ │N1 │... │N(n-1)│ QEMU instances
|
||||
│sens │ │sen│ │coord │
|
||||
└──┬──┘ └─┬─┘ └──┬───┘
|
||||
│ │ │
|
||||
┌──▼──────▼─────────▼──┐
|
||||
│ Virtual Network │ TAP bridge / SLIRP
|
||||
│ (topology-shaped) │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
┌──────────▼───────────┐
|
||||
│ Aggregator (Rust) │ Collects frames
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
┌──────────▼───────────┐
|
||||
│ Health Oracle │ Swarm-level assertions
|
||||
│ (swarm_health.py) │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
### YAML Configuration Schema
|
||||
|
||||
```yaml
|
||||
# swarm_config.yaml
|
||||
swarm:
|
||||
name: "3-sensor-star"
|
||||
duration_s: 60
|
||||
topology: star # star | mesh | line | ring
|
||||
aggregator_port: 5005
|
||||
|
||||
nodes:
|
||||
- role: coordinator
|
||||
node_id: 0
|
||||
scenario: 0 # empty room (baseline)
|
||||
channel: 6
|
||||
edge_tier: 2
|
||||
is_gateway: true # receives aggregated frames
|
||||
|
||||
- role: sensor
|
||||
node_id: 1
|
||||
scenario: 2 # walking person
|
||||
channel: 6
|
||||
tdm_slot: 1 # TDM slot index (auto-assigned from node position if omitted)
|
||||
|
||||
- role: sensor
|
||||
node_id: 2
|
||||
scenario: 3 # fall event
|
||||
channel: 6
|
||||
tdm_slot: 2
|
||||
|
||||
assertions:
|
||||
- all_nodes_boot
|
||||
- no_crashes
|
||||
- tdm_no_collision
|
||||
- all_nodes_produce_frames
|
||||
- coordinator_receives_from_all
|
||||
- fall_detected_by_node_2
|
||||
- frame_rate_above: 15 # Hz minimum per node
|
||||
- max_boot_time_s: 10
|
||||
```
|
||||
|
||||
### Topologies
|
||||
|
||||
| Topology | Network | Description |
|
||||
|----------|---------|-------------|
|
||||
| `star` | All sensors connect to coordinator; coordinator has TAP to each sensor | Hub-and-spoke, most common |
|
||||
| `mesh` | All nodes on same bridge (existing Layer 3 behavior) | Every node sees every other |
|
||||
| `line` | Node 0 ↔ Node 1 ↔ Node 2 ↔ ... | Linear chain, tests multi-hop |
|
||||
| `ring` | Like line but last connects to first | Circular, tests routing |
|
||||
|
||||
### Node Roles
|
||||
|
||||
| Role | Behavior | NVS Keys |
|
||||
|------|----------|----------|
|
||||
| `sensor` | Runs mock CSI, sends frames to coordinator | `node_id`, `tdm_slot`, `target_ip` |
|
||||
| `coordinator` | Receives frames from sensors, runs edge aggregation | `node_id`, `tdm_slot=0`, `edge_tier=2` |
|
||||
| `gateway` | Like coordinator but also bridges to host UDP | `node_id`, `target_ip=host`, `is_gateway=1` |
|
||||
|
||||
### Assertions (Swarm-Level)
|
||||
|
||||
| Assertion | What It Checks |
|
||||
|-----------|---------------|
|
||||
| `all_nodes_boot` | Every node's UART log shows boot indicators within timeout |
|
||||
| `no_crashes` | No Guru Meditation, assert, panic in any log |
|
||||
| `tdm_no_collision` | No two nodes transmit in the same TDM slot |
|
||||
| `all_nodes_produce_frames` | Every sensor node's log contains CSI frame output |
|
||||
| `coordinator_receives_from_all` | Coordinator log shows frames from each sensor's node_id |
|
||||
| `fall_detected_by_node_N` | Node N's log reports a fall detection event |
|
||||
| `frame_rate_above` | Each node produces at least N frames/second |
|
||||
| `max_boot_time_s` | All nodes boot within N seconds |
|
||||
| `no_heap_errors` | No OOM or heap corruption in any log |
|
||||
| `network_partitioned_recovery` | After deliberate partition, nodes resume communication (future) |
|
||||
|
||||
### Preset Configurations
|
||||
|
||||
| Preset | Nodes | Topology | Purpose |
|
||||
|--------|-------|----------|---------|
|
||||
| `smoke` | 2 | star | Quick CI smoke test (15s) |
|
||||
| `standard` | 3 | star | Default 3-node (sensor + sensor + coordinator) |
|
||||
| `large-mesh` | 6 | mesh | Scale test with 6 fully-connected nodes |
|
||||
| `line-relay` | 4 | line | Multi-hop relay chain |
|
||||
| `ring-fault` | 4 | ring | Ring with fault injection mid-test |
|
||||
| `heterogeneous` | 5 | star | Mixed scenarios: walk, fall, static, channel-sweep, empty |
|
||||
| `ci-matrix` | 3 | star | CI-optimized preset (30s, minimal assertions) |
|
||||
|
||||
## File Layout
|
||||
|
||||
```
|
||||
scripts/
|
||||
├── qemu_swarm.py # Main orchestrator (CLI entry point)
|
||||
├── swarm_health.py # Swarm-level health oracle
|
||||
└── swarm_presets/
|
||||
├── smoke.yaml
|
||||
├── standard.yaml
|
||||
├── large_mesh.yaml
|
||||
├── line_relay.yaml
|
||||
├── ring_fault.yaml
|
||||
├── heterogeneous.yaml
|
||||
└── ci_matrix.yaml
|
||||
|
||||
.github/workflows/
|
||||
└── firmware-qemu.yml # MODIFIED: add swarm test job
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Declarative testing** — define swarm topology in YAML, not shell scripts
|
||||
2. **Role-based nodes** — test coordinator/sensor/gateway interactions
|
||||
3. **Topology variety** — star/mesh/line/ring match real deployment patterns
|
||||
4. **Swarm-level assertions** — validate collective behavior, not just individual nodes
|
||||
5. **Preset library** — quick CI smoke tests and thorough manual validation
|
||||
6. **Reproducible** — YAML configs are version-controlled and shareable
|
||||
|
||||
### Limitations
|
||||
|
||||
1. **Still requires root** for TAP bridge topologies (star, line, ring); mesh can use SLIRP
|
||||
2. **QEMU resource usage** — 6+ QEMU instances use ~2GB RAM, may slow CI runners
|
||||
3. **No real RF** — inter-node communication is IP-based, not WiFi CSI multipath
|
||||
|
||||
## References
|
||||
|
||||
- ADR-061: QEMU ESP32-S3 firmware testing platform (Layers 1-9)
|
||||
- ADR-060: Channel override and MAC address filter provisioning
|
||||
- ADR-018: Binary CSI frame format (magic `0xC5110001`)
|
||||
- ADR-039: Edge intelligence pipeline (biquad, vitals, fall detection)
|
||||
@@ -1,261 +0,0 @@
|
||||
# ADR-063: 60 GHz mmWave Sensor Fusion with WiFi CSI
|
||||
|
||||
**Status:** Proposed
|
||||
**Date:** 2026-03-15
|
||||
**Deciders:** @ruvnet
|
||||
**Related:** ADR-014 (SOTA signal processing), ADR-021 (vital sign extraction), ADR-029 (RuvSense multistatic), ADR-039 (edge intelligence), ADR-042 (CHCI coherent sensing)
|
||||
|
||||
## Context
|
||||
|
||||
RuView currently senses the environment using WiFi CSI — a passive technique that analyzes how WiFi signals are disturbed by human presence and movement. While this works through walls and requires no line of sight, CSI-derived vital signs (breathing rate, heart rate) are inherently noisy because they rely on phase extraction from multipath-rich WiFi channels.
|
||||
|
||||
A complementary sensing modality exists: **60 GHz mmWave radar** modules (e.g., Seeed MR60BHA2) that use active FMCW radar at 60 GHz to measure breathing and heart rate with clinical-grade accuracy. These modules are inexpensive (~$15), run on ESP32-C6/C3, and output structured vital signs over UART.
|
||||
|
||||
**Live hardware capture (COM4, 2026-03-15)** from a Seeed MR60BHA2 on an ESP32-C6 running ESPHome:
|
||||
|
||||
```
|
||||
[D][sensor:093]: 'Real-time respiratory rate': Sending state 22.00000
|
||||
[D][sensor:093]: 'Real-time heart rate': Sending state 92.00000 bpm
|
||||
[D][sensor:093]: 'Distance to detection object': Sending state 0.00000 cm
|
||||
[D][sensor:093]: 'Target Number': Sending state 0.00000
|
||||
[D][binary_sensor:036]: 'Person Information': Sending state OFF
|
||||
[D][sensor:093]: 'Seeed MR60BHA2 Illuminance': Sending state 0.67913 lx
|
||||
```
|
||||
|
||||
### The Opportunity
|
||||
|
||||
Fusing WiFi CSI with mmWave radar creates a sensor system that is greater than the sum of its parts:
|
||||
|
||||
| Capability | WiFi CSI Alone | mmWave Alone | Fused |
|
||||
|-----------|---------------|-------------|-------|
|
||||
| Through-wall sensing | Yes (5m+) | No (LoS only, ~3m) | Yes — CSI for room-scale, mmWave for precision |
|
||||
| Heart rate accuracy | ±5-10 BPM | ±1-2 BPM | ±1-2 BPM (mmWave primary, CSI cross-validates) |
|
||||
| Breathing accuracy | ±2-3 BPM | ±0.5 BPM | ±0.5 BPM |
|
||||
| Presence detection | Good (adaptive threshold) | Excellent (range-gated) | Excellent + through-wall |
|
||||
| Multi-person | Via subcarrier clustering | Via range-Doppler bins | Combined spatial + RF resolution |
|
||||
| Fall detection | Phase acceleration | Range/velocity + micro-Doppler | Dual-confirm reduces false positives to near-zero |
|
||||
| Pose estimation | Via trained model | Not available | CSI provides pose; mmWave provides ground-truth vitals for training |
|
||||
| Coverage | Whole room (passive) | ~120° cone, 3m range | Full room + precision zone |
|
||||
| Cost per node | ~$9 (ESP32-S3) | ~$15 (ESP32-C6 + MR60BHA2) | ~$24 combined |
|
||||
|
||||
### RuVector Integration Points
|
||||
|
||||
The RuVector v2.0.4 stack (already integrated per ADR-016) provides the signal processing backbone:
|
||||
|
||||
| RuVector Component | Role in mmWave Fusion |
|
||||
|-------------------|----------------------|
|
||||
| `ruvector-attention` (`bvp.rs`) | Blood Volume Pulse estimation — mmWave heart rate can calibrate the WiFi CSI BVP phase extraction |
|
||||
| `ruvector-temporal-tensor` (`breathing.rs`) | Breathing rate estimation — mmWave provides ground-truth for adaptive filter tuning |
|
||||
| `ruvector-solver` (`triangulation.rs`) | Multilateration — mmWave range-gated distance + CSI amplitude = 3D position |
|
||||
| `ruvector-attn-mincut` (`spectrogram.rs`) | Time-frequency decomposition — mmWave Doppler complements CSI phase spectrogram |
|
||||
| `ruvector-mincut` (`metrics.rs`, DynamicPersonMatcher) | Multi-person association — mmWave target IDs help disambiguate CSI subcarrier clusters |
|
||||
|
||||
### RuvSense Integration Points
|
||||
|
||||
The RuvSense multistatic sensing pipeline (ADR-029) gains new capabilities:
|
||||
|
||||
| RuvSense Module | mmWave Integration |
|
||||
|----------------|-------------------|
|
||||
| `pose_tracker.rs` (AETHER re-ID) | mmWave distance + velocity as additional re-ID features for Kalman tracker |
|
||||
| `longitudinal.rs` (Welford stats) | mmWave vitals as reference signal for CSI drift detection |
|
||||
| `intention.rs` (pre-movement) | mmWave micro-Doppler detects pre-movement 100-200ms earlier than CSI |
|
||||
| `adversarial.rs` (consistency check) | mmWave provides independent signal to detect CSI spoofing/anomalies |
|
||||
| `coherence_gate.rs` | mmWave presence as additional gate input — if mmWave says "no person", CSI coherence gate rejects |
|
||||
|
||||
### Cross-Viewpoint Fusion Integration
|
||||
|
||||
The viewpoint fusion pipeline (`ruvector/src/viewpoint/`) extends naturally:
|
||||
|
||||
| Viewpoint Module | mmWave Extension |
|
||||
|-----------------|-----------------|
|
||||
| `attention.rs` (CrossViewpointAttention) | mmWave range becomes a new "viewpoint" in the attention mechanism |
|
||||
| `geometry.rs` (GeometricDiversityIndex) | mmWave cone geometry contributes to Fisher Information / Cramer-Rao bounds |
|
||||
| `coherence.rs` (phase phasor) | mmWave phase coherence as validation for WiFi phasor coherence |
|
||||
| `fusion.rs` (MultistaticArray) | mmWave node becomes a member of the multistatic array with its own domain events |
|
||||
|
||||
## Decision
|
||||
|
||||
Add 60 GHz mmWave radar sensor support to the RuView firmware and sensing pipeline with auto-detection and device-specific capabilities.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Sensing Node │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
|
||||
│ │ ESP32-S3 │ │ ESP32-C6 │ │ Combined │ │
|
||||
│ │ WiFi CSI │ │ + MR60BHA2 │ │ S3 + UART │ │
|
||||
│ │ (COM7) │ │ 60GHz mmWave │ │ mmWave │ │
|
||||
│ │ │ │ (COM4) │ │ │ │
|
||||
│ │ Passive │ │ Active radar │ │ Both modes │ │
|
||||
│ │ Through-wall │ │ LoS, precise │ │ │ │
|
||||
│ └──────┬───────┘ └──────┬───────┘ └─────┬──────┘ │
|
||||
│ │ │ │ │
|
||||
│ └────────┬───────────┘ │ │
|
||||
│ ▼ │ │
|
||||
│ ┌────────────────┐ │ │
|
||||
│ │ Fusion Engine │◄──────────────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ • Kalman fuse │ Vitals packet (extended): │
|
||||
│ │ • Cross-validate│ magic 0xC5110004 │
|
||||
│ │ • Ground-truth │ + mmwave_hr, mmwave_br │
|
||||
│ │ calibration │ + mmwave_distance │
|
||||
│ │ • Fall confirm │ + mmwave_target_count │
|
||||
│ └────────────────┘ + confidence scores │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Three Deployment Modes
|
||||
|
||||
**Mode 1: Standalone CSI (existing)** — ESP32-S3 only, WiFi CSI sensing.
|
||||
|
||||
**Mode 2: Standalone mmWave** — ESP32-C6 + MR60BHA2, precise vitals in a single room.
|
||||
|
||||
**Mode 3: Fused (recommended)** — ESP32-S3 + mmWave module on UART, or two separate nodes with server-side fusion.
|
||||
|
||||
### Auto-Detection Protocol
|
||||
|
||||
The firmware will auto-detect connected mmWave modules at boot:
|
||||
|
||||
1. **UART probe** — On configured UART pins, send the MR60BHA2 identification command (`0x01 0x01 0x00 0x01 ...`) and check for valid response header
|
||||
2. **Protocol detection** — Identify the sensor family:
|
||||
- Seeed MR60BHA2 (breathing + heart rate)
|
||||
- Seeed MR60FDA1 (fall detection)
|
||||
- Seeed MR24HPC1 (presence + light sleep/deep sleep)
|
||||
- HLK-LD2410 (presence + distance)
|
||||
- HLK-LD2450 (multi-target tracking)
|
||||
3. **Capability registration** — Register detected sensor capabilities in the edge config:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
uint8_t mmwave_detected; /** 1 if mmWave module found on UART */
|
||||
uint8_t mmwave_type; /** Sensor family (MR60BHA2, MR60FDA1, etc.) */
|
||||
uint8_t mmwave_has_hr; /** Heart rate capability */
|
||||
uint8_t mmwave_has_br; /** Breathing rate capability */
|
||||
uint8_t mmwave_has_fall; /** Fall detection capability */
|
||||
uint8_t mmwave_has_presence; /** Presence detection capability */
|
||||
uint8_t mmwave_has_distance; /** Range measurement capability */
|
||||
uint8_t mmwave_has_tracking; /** Multi-target tracking capability */
|
||||
float mmwave_hr_bpm; /** Latest heart rate from mmWave */
|
||||
float mmwave_br_bpm; /** Latest breathing rate from mmWave */
|
||||
float mmwave_distance_cm; /** Distance to nearest target */
|
||||
uint8_t mmwave_target_count; /** Number of detected targets */
|
||||
bool mmwave_person_present;/** mmWave presence state */
|
||||
} mmwave_state_t;
|
||||
```
|
||||
|
||||
### Supported Sensors
|
||||
|
||||
| Sensor | Frequency | Capabilities | UART Protocol | Cost |
|
||||
|--------|-----------|-------------|---------------|------|
|
||||
| **Seeed MR60BHA2** | 60 GHz | HR, BR, presence, illuminance | Seeed proprietary frames | ~$15 |
|
||||
| **Seeed MR60FDA1** | 60 GHz | Fall detection, presence | Seeed proprietary frames | ~$15 |
|
||||
| **Seeed MR24HPC1** | 24 GHz | Presence, sleep stage, distance | Seeed proprietary frames | ~$10 |
|
||||
| **HLK-LD2410** | 24 GHz | Presence, distance (motion + static) | HLK binary protocol | ~$3 |
|
||||
| **HLK-LD2450** | 24 GHz | Multi-target tracking (x,y,speed) | HLK binary protocol | ~$5 |
|
||||
|
||||
### Fusion Algorithms
|
||||
|
||||
**1. Vital Sign Fusion (Kalman filter)**
|
||||
```
|
||||
mmWave HR (high confidence, 1 Hz) ─┐
|
||||
├─► Kalman fuse → fused HR ± confidence
|
||||
CSI-derived HR (lower confidence) ─┘
|
||||
```
|
||||
|
||||
**2. Fall Detection (dual-confirm)**
|
||||
```
|
||||
CSI phase accel > thresh ──────┐
|
||||
├─► AND gate → confirmed fall (near-zero false positives)
|
||||
mmWave range-velocity pattern ─┘
|
||||
```
|
||||
|
||||
**3. Presence Validation**
|
||||
```
|
||||
CSI adaptive threshold ────┐
|
||||
├─► Weighted vote → robust presence
|
||||
mmWave target count > 0 ──┘
|
||||
```
|
||||
|
||||
**4. Training Calibration**
|
||||
```
|
||||
mmWave ground-truth vitals → train CSI BVP extraction model
|
||||
mmWave distance → calibrate CSI triangulation
|
||||
mmWave micro-Doppler → label CSI activity patterns
|
||||
```
|
||||
|
||||
### Vitals Packet Extension
|
||||
|
||||
Extend the existing 32-byte vitals packet (magic `0xC5110002`) with a new 48-byte fused packet:
|
||||
|
||||
```c
|
||||
typedef struct __attribute__((packed)) {
|
||||
/* Existing 32-byte vitals fields */
|
||||
uint32_t magic; /* 0xC5110004 (fused vitals) */
|
||||
uint8_t node_id;
|
||||
uint8_t flags; /* Bit0=presence, Bit1=fall, Bit2=motion, Bit3=mmwave_present */
|
||||
uint16_t breathing_rate; /* Fused BPM * 100 */
|
||||
uint32_t heartrate; /* Fused BPM * 10000 */
|
||||
int8_t rssi;
|
||||
uint8_t n_persons;
|
||||
uint8_t mmwave_type; /* Sensor type enum */
|
||||
uint8_t fusion_confidence;/* 0-100 fusion quality score */
|
||||
float motion_energy;
|
||||
float presence_score;
|
||||
uint32_t timestamp_ms;
|
||||
/* New mmWave fields (16 bytes) */
|
||||
float mmwave_hr_bpm; /* Raw mmWave heart rate */
|
||||
float mmwave_br_bpm; /* Raw mmWave breathing rate */
|
||||
float mmwave_distance; /* Distance to nearest target (cm) */
|
||||
uint8_t mmwave_targets; /* Target count */
|
||||
uint8_t mmwave_confidence;/* mmWave signal quality 0-100 */
|
||||
uint16_t reserved;
|
||||
} edge_fused_vitals_pkt_t;
|
||||
|
||||
_Static_assert(sizeof(edge_fused_vitals_pkt_t) == 48, "fused vitals must be 48 bytes");
|
||||
```
|
||||
|
||||
### NVS Configuration
|
||||
|
||||
New provisioning parameters:
|
||||
|
||||
```bash
|
||||
python provision.py --port COM7 \
|
||||
--mmwave-uart-tx 17 --mmwave-uart-rx 18 \ # UART pins for mmWave module
|
||||
--mmwave-type auto \ # auto-detect, or: mr60bha2, ld2410, etc.
|
||||
--fusion-mode kalman \ # kalman, vote, mmwave-primary, csi-primary
|
||||
--fall-dual-confirm true # require both CSI + mmWave for fall alert
|
||||
```
|
||||
|
||||
### Implementation Phases
|
||||
|
||||
| Phase | Scope | Effort |
|
||||
|-------|-------|--------|
|
||||
| **Phase 1** | UART driver + MR60BHA2 parser + auto-detection | 2 weeks |
|
||||
| **Phase 2** | Fused vitals packet + Kalman vital sign fusion | 1 week |
|
||||
| **Phase 3** | Dual-confirm fall detection + presence voting | 1 week |
|
||||
| **Phase 4** | HLK-LD2410/LD2450 support + multi-target fusion | 2 weeks |
|
||||
| **Phase 5** | RuVector calibration pipeline (mmWave as ground truth) | 3 weeks |
|
||||
| **Phase 6** | Server-side fusion for separate CSI + mmWave nodes | 2 weeks |
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Near-zero false positive fall detection (dual-confirm)
|
||||
- Clinical-grade vital signs when mmWave is present, with CSI as fallback
|
||||
- Self-calibrating CSI pipeline using mmWave ground truth
|
||||
- Backward compatible — existing CSI-only nodes work unchanged
|
||||
- Low incremental cost (~$3-15 per mmWave module)
|
||||
- Auto-detection means zero configuration for supported sensors
|
||||
- RuVector attention/solver/temporal-tensor modules gain a high-quality reference signal
|
||||
|
||||
### Negative
|
||||
- Added firmware complexity (~2-3 KB RAM for mmWave state + UART buffer)
|
||||
- mmWave modules require line-of-sight (complementary to CSI, not replacement)
|
||||
- Multiple UART protocols to maintain (Seeed, HLK families)
|
||||
- 48-byte fused packet requires server parser update
|
||||
|
||||
### Neutral
|
||||
- ESP32-C6 cannot run the full CSI pipeline (single-core RISC-V) but can serve as a dedicated mmWave bridge node
|
||||
- mmWave modules add ~15 mA power draw per node
|
||||
@@ -1,327 +0,0 @@
|
||||
# ADR-064: Multimodal Ambient Intelligence — WiFi CSI + mmWave + Environmental Sensors
|
||||
|
||||
**Status:** Proposed
|
||||
**Date:** 2026-03-15
|
||||
**Deciders:** @ruvnet
|
||||
**Related:** ADR-063 (mmWave fusion), ADR-039 (edge intelligence), ADR-042 (CHCI), ADR-029 (RuvSense multistatic), ADR-024 (AETHER contrastive embeddings)
|
||||
|
||||
## Context
|
||||
|
||||
With ADR-063 we demonstrated real-time fusion of WiFi CSI (ESP32-S3, COM7) and 60 GHz mmWave radar (Seeed MR60BHA2 on ESP32-C6, COM4). The live capture showed:
|
||||
|
||||
- **mmWave**: HR 75 bpm, BR 25/min, presence at 52 cm, 1.4 Hz update
|
||||
- **WiFi CSI**: Channel 5, RSSI -41, 20+ Hz frame rate, through-wall coverage
|
||||
- **BH1750**: Ambient light 0.0-0.7 lux (room darkness level)
|
||||
|
||||
This ADR explores the full spectrum of what becomes possible when these modalities are combined — from immediately practical applications to speculative research directions.
|
||||
|
||||
---
|
||||
|
||||
## Tier 1: Practical (Build Now)
|
||||
|
||||
### 1.1 Intelligent Fall Detection with Zero False Positives
|
||||
|
||||
**Current state:** CSI-only fall detection with 15.0 rad/s² threshold (v0.4.3.1).
|
||||
**With fusion:** mmWave confirms fall via range-velocity signature (sudden height drop + impact deceleration). CSI provides the alert; mmWave provides the confirmation.
|
||||
|
||||
```
|
||||
CSI phase acceleration > 15 rad/s² ─┐
|
||||
├─► AND gate + temporal correlation
|
||||
mmWave: height drop > 50cm in <1s ──┘ → CONFIRMED FALL (call 911)
|
||||
```
|
||||
|
||||
**Impact:** Elderly care facilities spend $34B/year on fall injuries. A $24 sensor node with zero false positives replaces $200/month medical alert wearables that residents forget to wear.
|
||||
|
||||
### 1.2 Sleep Quality Monitoring
|
||||
|
||||
**Sensors used:** mmWave (BR/HR), CSI (bed occupancy, movement), BH1750 (light)
|
||||
|
||||
| Metric | Source | Method |
|
||||
|--------|--------|--------|
|
||||
| Sleep onset | CSI motion → still transition | Phase variance drops below threshold |
|
||||
| Sleep stages | mmWave BR variability | BR 12-20 = light sleep, 6-12 = deep sleep |
|
||||
| REM detection | mmWave HR variability | HR variability increases during REM |
|
||||
| Restlessness | CSI motion energy | Counts of motion episodes per hour |
|
||||
| Room darkness | BH1750 | Correlate light exposure with sleep latency |
|
||||
| Wake events | CSI + mmWave | Motion + HR spike = awakening |
|
||||
|
||||
**Output:** Sleep score (0-100), time in each stage, disturbance log.
|
||||
**No wearable required.** Works through a mattress.
|
||||
|
||||
### 1.3 Occupancy-Aware HVAC and Lighting
|
||||
|
||||
**Sensors:** CSI (room-level presence through walls), mmWave (precise count + distance), BH1750 (ambient light)
|
||||
|
||||
- CSI detects which rooms are occupied (through walls, whole-floor sensing)
|
||||
- mmWave counts exact number of people in the sensor's room
|
||||
- BH1750 measures if lights are on/needed
|
||||
- System sends MQTT/UDP commands to smart home controllers
|
||||
|
||||
**Energy savings:** 20-40% HVAC reduction by not heating/cooling empty rooms.
|
||||
|
||||
### 1.4 Bathroom Safety for Elderly
|
||||
|
||||
**Sensor placement:** One CSI node outside bathroom (through-wall), one mmWave inside.
|
||||
|
||||
- CSI detects person entered bathroom (through-wall)
|
||||
- mmWave monitors vitals while showering (waterproof enclosure)
|
||||
- If no movement for > N minutes AND HR drops: alert
|
||||
- Fall detection in shower (slippery surface = high risk)
|
||||
|
||||
### 1.5 Baby/Infant Breathing Monitor
|
||||
|
||||
**mmWave at crib-side:** Contactless breathing monitoring at 0.5-1m range.
|
||||
- BR < 10 or BR = 0 for > 20s: alarm (apnea detection)
|
||||
- CSI provides room context (parent present? other motion?)
|
||||
- BH1750 tracks night feeding times (light on/off events)
|
||||
|
||||
---
|
||||
|
||||
## Tier 2: Advanced (Research Prototype)
|
||||
|
||||
### 2.1 Gait Analysis and Fall Risk Prediction
|
||||
|
||||
**Method:** CSI tracks walking pattern across the room; mmWave measures stride length and velocity.
|
||||
|
||||
| Feature | Source | Clinical Use |
|
||||
|---------|--------|-------------|
|
||||
| Gait velocity | mmWave Doppler | < 0.8 m/s = fall risk indicator |
|
||||
| Stride variability | CSI phase patterns | High variability = cognitive decline marker |
|
||||
| Turning stability | CSI + mmWave | Difficulty turning = Parkinson's indicator |
|
||||
| Get-up time | mmWave (sit→stand) | Timed Up and Go (TUG) test, contactless |
|
||||
|
||||
**Clinical value:** Gait velocity is called the "sixth vital sign" — it predicts hospitalization, cognitive decline, and mortality. Currently requires a $10,000 GAITRite mat. A $24 sensor node replaces it.
|
||||
|
||||
### 2.2 Emotion and Stress Detection via Micro-Vitals
|
||||
|
||||
**mmWave at desk:** Continuous HR variability (HRV) monitoring during work.
|
||||
|
||||
- **HRV time-domain:** SDNN, RMSSD from beat-to-beat intervals
|
||||
- **HRV frequency-domain:** LF/HF ratio (sympathetic/parasympathetic balance)
|
||||
- Low HF power = stress; high HF = relaxation
|
||||
- CSI detects fidgeting, posture shifts (correlated with stress)
|
||||
- BH1750 correlates lighting with mood/productivity
|
||||
|
||||
**Application:** Smart office that adjusts lighting, temperature, and notification frequency based on detected stress level.
|
||||
|
||||
### 2.3 Gesture Recognition as Room Control
|
||||
|
||||
**CSI:** Already has DTW template matching gesture classifier (`ruvsense/gesture.rs`).
|
||||
**mmWave:** Adds range-Doppler micro-gesture detection (hand wave, swipe, circle).
|
||||
|
||||
- CSI recognizes gross gestures (wave arm, walk pattern)
|
||||
- mmWave recognizes fine hand gestures (swipe left/right, push/pull)
|
||||
- Fused: spatial context (CSI knows where you are) + precise gesture (mmWave knows what your hand did)
|
||||
|
||||
**Use case:** Wave at the sensor to turn off lights. Swipe to change music. No voice assistant, no camera, no wearable.
|
||||
|
||||
### 2.4 Respiratory Disease Screening
|
||||
|
||||
**mmWave BR patterns over days/weeks:**
|
||||
|
||||
| Pattern | Indicator |
|
||||
|---------|-----------|
|
||||
| BR > 20 at rest, trending up | Possible pneumonia/COVID |
|
||||
| Periodic breathing (Cheyne-Stokes) | Heart failure |
|
||||
| Obstructive apnea pattern | Sleep apnea (> 5 events/hour) |
|
||||
| BR variability decrease | COPD exacerbation |
|
||||
|
||||
**CSI adds:** Cough detection (sudden phase disturbance pattern), movement reduction (malaise indicator).
|
||||
|
||||
**Longitudinal tracking** via `ruvsense/longitudinal.rs` (Welford stats, biomechanics drift detection) — the system learns your normal breathing pattern and alerts on deviations.
|
||||
|
||||
### 2.5 Multi-Room Activity Recognition
|
||||
|
||||
**3-6 CSI nodes (through walls) + 1-2 mmWave (key rooms):**
|
||||
|
||||
```
|
||||
Kitchen (CSI): person detected, high motion → cooking
|
||||
Living room (mmWave + CSI): 2 people, low motion, HR stable → watching TV
|
||||
Bedroom (CSI): person detected, minimal motion → sleeping
|
||||
Bathroom (CSI): person entered 3 min ago, still inside → OK
|
||||
Front door (CSI): motion pattern = leaving/arriving
|
||||
```
|
||||
|
||||
**Output:** Activity timeline, daily routine deviation alerts, loneliness detection (no visitors in N days).
|
||||
|
||||
---
|
||||
|
||||
## Tier 3: Speculative (Research Frontier)
|
||||
|
||||
### 3.1 Cardiac Arrhythmia Detection
|
||||
|
||||
**mmWave at < 1m range:** Beat-to-beat interval extraction from chest wall displacement.
|
||||
|
||||
- Atrial fibrillation: irregular R-R intervals (coefficient of variation > 0.1)
|
||||
- Bradycardia/tachycardia: sustained HR < 60 or > 100
|
||||
- Premature ventricular contractions: occasional short-long-short patterns
|
||||
|
||||
**Challenge:** Requires sub-millimeter displacement resolution. The MR60BHA2 may lack the SNR for single-beat extraction, but clinical-grade 60 GHz modules (Infineon BGT60TR13C) can achieve this.
|
||||
|
||||
**CSI role:** Validates that the person is stationary (motion corrupts beat-to-beat analysis).
|
||||
|
||||
### 3.2 Blood Pressure Estimation (Contactless)
|
||||
|
||||
**Theory:** Pulse Transit Time (PTT) between two body points correlates with blood pressure. With two mmWave sensors at different body positions, PTT can be estimated from the phase difference of reflected chest/wrist signals.
|
||||
|
||||
**Feasibility:** Academic papers demonstrate ±10 mmHg accuracy in controlled settings. Far from clinical grade but useful for trending.
|
||||
|
||||
### 3.3 RF Tomography — 3D Occupancy Imaging
|
||||
|
||||
**Method:** Multiple CSI nodes form a tomographic array. Each TX-RX pair measures signal attenuation. Inverse problem (ISTA L1 solver, already in `ruvsense/tomography.rs`) reconstructs a 3D voxel grid of where absorbers (people) are.
|
||||
|
||||
**mmWave adds:** Range-gated targets as sparse priors for the tomographic reconstruction, dramatically reducing the ill-posedness of the inverse problem.
|
||||
|
||||
```
|
||||
CSI tomography (coarse 3D grid, 50cm resolution) ─┐
|
||||
├─► Sparse fusion
|
||||
mmWave targets (precise range, cm resolution) ─────┘ → 10cm 3D occupancy map
|
||||
```
|
||||
|
||||
### 3.4 Sign Language Recognition
|
||||
|
||||
**CSI phase patterns (body/arm movement) + mmWave Doppler (hand micro-movements):**
|
||||
|
||||
- CSI captures the gross arm trajectory of each sign
|
||||
- mmWave captures the finger configuration at the pause point
|
||||
- AETHER contrastive embeddings (`ADR-024`) learn to map (CSI phase sequence, mmWave Doppler) → sign label
|
||||
- No camera required — works in the dark, preserves privacy
|
||||
|
||||
**Training data:** Record CSI + mmWave while performing signs with a camera as ground truth, then deploy camera-free.
|
||||
|
||||
### 3.5 Cognitive Load Estimation
|
||||
|
||||
**Multimodal features:**
|
||||
|
||||
| Feature | Source | Cognitive Load Indicator |
|
||||
|---------|--------|------------------------|
|
||||
| HR increase | mmWave | Sympathetic activation |
|
||||
| BR irregularity | mmWave | Cognitive interference |
|
||||
| Posture stiffness | CSI motion variance | Reduced when concentrating |
|
||||
| Fidgeting frequency | CSI high-freq motion | Increases with frustration |
|
||||
| Micro-saccade proxy | mmWave head micro-movement | Correlated with attention |
|
||||
|
||||
**Application:** Adaptive learning systems that slow down when the student is overloaded. Smart meeting rooms that detect when participants are disengaged.
|
||||
|
||||
### 3.6 Drone/Robot Navigation via RF Sensing
|
||||
|
||||
**CSI mesh as indoor GPS:** A network of CSI nodes creates a spatial RF fingerprint map. A robot or drone with an ESP32 can localize itself by matching its observed CSI to the map.
|
||||
|
||||
**mmWave on the robot:** Obstacle avoidance + human detection (don't collide with people).
|
||||
|
||||
**CSI from the environment:** Tells the robot where people are in adjacent rooms (through walls) so it can plan routes that avoid occupied spaces.
|
||||
|
||||
### 3.7 Building Structural Health Monitoring
|
||||
|
||||
**CSI multipath signature over months/years:**
|
||||
|
||||
- The CSI channel response is a fingerprint of the room's geometry
|
||||
- Subtle shifts in multipath (wall crack propagation, foundation settlement) change the CSI signature
|
||||
- `ruvsense/cross_room.rs` (environment fingerprinting) tracks these long-term drifts
|
||||
- mmWave detects surface vibrations (micro-displacement from traffic, wind, seismic)
|
||||
|
||||
**Application:** Early warning for structural degradation in bridges, tunnels, old buildings.
|
||||
|
||||
### 3.8 Swarm Sensing — Emergent Spatial Awareness
|
||||
|
||||
**50+ nodes across a building:**
|
||||
|
||||
Each node runs local edge intelligence (ADR-039). The `hive-mind` consensus system (ADR-062) aggregates across nodes. Emergent behaviors:
|
||||
|
||||
- **Flow detection:** Track how people move between rooms over time
|
||||
- **Anomaly detection:** "This hallway usually has 5 people/hour but had 0 today"
|
||||
- **Emergency routing:** During fire, track which exits are blocked (no movement) vs available
|
||||
- **Crowd density:** Concert/stadium safety — detect dangerous compression zones through walls
|
||||
|
||||
---
|
||||
|
||||
## Tier 4: Exotic / Sci-Fi Adjacent
|
||||
|
||||
### 4.1 Emotion Contagion Mapping
|
||||
|
||||
If multiple people are in a room and the system can estimate individual HR/HRV (via multi-target mmWave + CSI subcarrier clustering), you can detect:
|
||||
|
||||
- Physiological synchrony (two people's HR converging = rapport/empathy)
|
||||
- Stress propagation (one person's stress → others' HR rises)
|
||||
- "Emotional temperature" of a room
|
||||
|
||||
### 4.2 Dream State Detection and Lucid Dream Induction
|
||||
|
||||
During REM sleep (detected via mmWave HR variability + CSI minimal body movement):
|
||||
|
||||
- Detect REM onset with high confidence
|
||||
- Trigger a subtle environmental cue (gentle light via smart bulb, barely audible tone)
|
||||
- The sleeper incorporates the cue into the dream, recognizing it as a dream trigger
|
||||
- BH1750 confirms room is dark (not a natural awakening)
|
||||
|
||||
Based on published lucid dreaming induction research (e.g., LaBerge's MILD technique with external cues).
|
||||
|
||||
### 4.3 Plant Growth Monitoring
|
||||
|
||||
WiFi signals pass through plant tissue differently based on water content.
|
||||
|
||||
- CSI amplitude through a greenhouse changes as plants absorb/release water
|
||||
- mmWave reflects off leaf surfaces — micro-displacement from growth
|
||||
- Long-term CSI drift correlates with biomass increase
|
||||
|
||||
Academic proof-of-concept: "Sensing Plant Water Content Using WiFi Signals" (2023).
|
||||
|
||||
### 4.4 Pet Behavior Analysis
|
||||
|
||||
- CSI detects pet movement patterns (different phase signature than humans — lower, faster)
|
||||
- mmWave detects breathing rate (pets have higher BR than humans)
|
||||
- System learns pet's daily routine and alerts on deviations (lethargy, pacing, not eating)
|
||||
|
||||
### 4.5 Paranormal Investigation Tool
|
||||
|
||||
(For the entertainment/hobbyist market)
|
||||
|
||||
- CSI detects "unexplained" signal disturbances in empty rooms
|
||||
- mmWave confirms no physical presence
|
||||
- System logs "anomalous RF events" with timestamps
|
||||
- Export as Ghost Hunting report
|
||||
|
||||
**Actual explanation:** Temperature changes, HVAC drafts, and EMI cause CSI fluctuations. But it would sell.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Priority Matrix
|
||||
|
||||
| Application | Sensors Needed | Effort | Value | Priority |
|
||||
|------------|---------------|--------|-------|----------|
|
||||
| Fall detection (zero false positive) | CSI + mmWave | 1 week | Critical (healthcare) | **P0** |
|
||||
| Sleep monitoring | mmWave + BH1750 | 2 weeks | High (wellness) | **P1** |
|
||||
| Occupancy HVAC/lighting | CSI + mmWave | 1 week | High (energy) | **P1** |
|
||||
| Baby breathing monitor | mmWave | 1 week | Critical (safety) | **P1** |
|
||||
| Bathroom safety | CSI + mmWave | 1 week | Critical (elderly) | **P1** |
|
||||
| Gait analysis | CSI + mmWave | 3 weeks | High (clinical) | **P2** |
|
||||
| Gesture control | CSI + mmWave | 4 weeks | Medium (UX) | **P2** |
|
||||
| Multi-room activity | CSI mesh + mmWave | 4 weeks | High (elder care) | **P2** |
|
||||
| Respiratory screening | mmWave longitudinal | 6 weeks | High (health) | **P2** |
|
||||
| Stress/emotion detection | mmWave HRV + CSI | 6 weeks | Medium (wellness) | **P3** |
|
||||
| RF tomography | CSI mesh + mmWave | 8 weeks | Medium (research) | **P3** |
|
||||
| Sign language | CSI + mmWave + ML | 12 weeks | Medium (accessibility) | **P3** |
|
||||
| Cardiac arrhythmia | High-res mmWave | 12 weeks | High (clinical) | **P3** |
|
||||
| Swarm sensing | 50+ nodes | 16 weeks | High (safety) | **P3** |
|
||||
|
||||
## Decision
|
||||
|
||||
Document these possibilities as the product roadmap for the RuView multimodal ambient intelligence platform. Prioritize P0-P1 items (fall detection, sleep, occupancy, baby monitor, bathroom safety) for immediate implementation using the existing hardware (ESP32-S3 + MR60BHA2 + BH1750).
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Positions RuView as a platform, not just a WiFi sensing demo
|
||||
- Each application can ship as a WASM edge module (ADR-040), deployable to existing hardware
|
||||
- Healthcare applications have clear regulatory paths (fall detection is FDA Class I exempt)
|
||||
- Most P0-P1 applications require no additional hardware beyond what's already deployed
|
||||
|
||||
### Negative
|
||||
- Clinical applications (arrhythmia, blood pressure) require medical device validation
|
||||
- Privacy concerns scale with capability — need clear data retention policies
|
||||
- Some exotic applications may attract scrutiny (surveillance concerns)
|
||||
|
||||
### Risk Mitigation
|
||||
- All processing happens on-device (edge) — no cloud, no recordings by default
|
||||
- No cameras — signal-based sensing preserves visual privacy
|
||||
- Open source — users can audit exactly what is sensed and transmitted
|
||||
@@ -1,234 +0,0 @@
|
||||
# ADR-065: Hotel Guest Happiness Scoring -- WiFi CSI + Cognitum Seed Bridge
|
||||
|
||||
**Status:** Proposed
|
||||
**Date:** 2026-03-20
|
||||
**Deciders:** @ruvnet
|
||||
**Related:** ADR-040 (WASM edge modules), ADR-039 (edge intelligence), ADR-042 (CHCI), ADR-064 (multimodal ambient intelligence), ADR-060 (multi-node aggregation)
|
||||
|
||||
## Context
|
||||
|
||||
Hotels lack objective, privacy-preserving methods to measure guest satisfaction in real time. Current approaches (post-stay surveys, NPS scores) are delayed, biased toward extremes, and capture less than 10% of guests. Meanwhile, ambient RF sensing can infer behavioral cues that correlate with comfort and well-being -- without cameras, wearables, or any guest interaction.
|
||||
|
||||
### Hardware
|
||||
|
||||
Two ESP32-S3 variants are deployed:
|
||||
|
||||
| Device | Flash | PSRAM | MAC | Port | Notes |
|
||||
|--------|-------|-------|-----|------|-------|
|
||||
| ESP32-S3 (QFN56 rev 0.2) | 4 MB | 2 MB | 1C:DB:D4:83:D2:40 | COM5 | Budget node, uses `sdkconfig.defaults.4mb` + `partitions_4mb.csv` |
|
||||
| ESP32-S3 | 8 MB | 8 MB | -- | COM7 | Full-featured node, existing deployment |
|
||||
|
||||
Both run the Tier 2 DSP firmware with presence detection, vitals extraction, fall detection, and gait analysis.
|
||||
|
||||
### Cognitum Seed Device
|
||||
|
||||
A Cognitum Seed unit is deployed on the same network segment:
|
||||
|
||||
- **Address:** 169.254.42.1 (link-local)
|
||||
- **Hardware:** Raspberry Pi Zero 2 W
|
||||
- **Firmware:** 0.7.0
|
||||
- **Vector store:** 398 vectors, dim=8
|
||||
- **API endpoints:** 98 (REST, fully documented)
|
||||
- **Sensors:** PIR, reed switch (door), vibration, ADS1115 ADC (4-ch analog), BME280 (temp/humidity/pressure)
|
||||
- **Security:** Ed25519 custody chain with tamper-evident witness log
|
||||
|
||||
The Seed's 8-dimensional vector store and drift detection engine make it a natural aggregation point for behavioral feature vectors extracted from CSI data.
|
||||
|
||||
### Existing WASM Edge Modules
|
||||
|
||||
The following modules already run on-device and produce features relevant to happiness scoring:
|
||||
|
||||
| Module | Event IDs | Outputs |
|
||||
|--------|-----------|---------|
|
||||
| `exo_emotion_detect.rs` | 610-613 | Arousal level, stress index |
|
||||
| `med_gait_analysis.rs` | 130-134 | Cadence, stride length, regularity |
|
||||
| `ret_customer_flow.rs` | 410-413 | Entry/exit count, direction |
|
||||
| `ret_dwell_heatmap.rs` | 420-423 | Dwell time per zone |
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. New WASM Module: `exo_happiness_score.rs`
|
||||
|
||||
Create a new WASM edge module that fuses outputs from existing modules into an 8-dimensional happiness vector, matching the Seed's vector dimensionality (dim=8).
|
||||
|
||||
**Event ID registry (690-694):**
|
||||
|
||||
| Event ID | Name | Description |
|
||||
|----------|------|-------------|
|
||||
| 690 | `HAPPINESS_VECTOR` | Full 8-dim happiness vector emitted per scoring window |
|
||||
| 691 | `HAPPINESS_TREND` | Windowed trend (rising/falling/stable) over last N vectors |
|
||||
| 692 | `HAPPINESS_ALERT` | Score crossed a configured threshold (low satisfaction) |
|
||||
| 693 | `HAPPINESS_GROUP` | Aggregate score for multi-person zone |
|
||||
| 694 | `HAPPINESS_CALIBRATION` | Baseline recalibration event (new guest check-in) |
|
||||
|
||||
### 2. Happiness Vector Schema (8 Dimensions)
|
||||
|
||||
Each dimension is normalized to [0.0, 1.0] where 1.0 = maximal positive signal:
|
||||
|
||||
| Dim | Name | Source | Derivation |
|
||||
|-----|------|--------|------------|
|
||||
| 0 | `gait_speed` | `med_gait_analysis` (130) | Normalized walking velocity. Brisk = positive. |
|
||||
| 1 | `stride_regularity` | `med_gait_analysis` (131) | Low stride-to-stride variance = relaxed gait. |
|
||||
| 2 | `movement_fluidity` | CSI phase jerk (d3/dt3) | Low jerk = smooth, unhurried movement. |
|
||||
| 3 | `breathing_calm` | Vitals BR extraction | BR 12-18 at rest = calm. Deviation penalized. |
|
||||
| 4 | `posture_openness` | CSI subcarrier spread | Wide phase spread across subcarriers = open posture. |
|
||||
| 5 | `dwell_comfort` | `ret_dwell_heatmap` (420) | Moderate dwell in amenity zones = engagement. |
|
||||
| 6 | `direction_entropy` | `ret_customer_flow` (410) | Low entropy = purposeful movement. Wandering penalized. |
|
||||
| 7 | `group_energy` | Multi-target CSI clustering | Synchronized movement of 2+ people = social engagement. |
|
||||
|
||||
The composite scalar happiness score is the weighted L2 norm:
|
||||
|
||||
```
|
||||
score = sum(w[i] * v[i] for i in 0..7) / sum(w[i])
|
||||
```
|
||||
|
||||
Default weights are uniform (all 1.0), configurable via NVS or Seed API.
|
||||
|
||||
### 3. ESP32 to Seed Bridge
|
||||
|
||||
```
|
||||
ESP32-S3 (CSI) Cognitum Seed (169.254.42.1)
|
||||
+------------------+ +----------------------------+
|
||||
| Tier 2 DSP | | |
|
||||
| + WASM modules | UDP 5555 | /api/v1/store/ingest |
|
||||
| exo_happiness |──────────────| (POST, 8-dim vector) |
|
||||
| _score.rs | | |
|
||||
| | | /api/v1/drift/check |
|
||||
| |◄─────────────| (drift alerts via webhook) |
|
||||
| | | |
|
||||
| | | /api/v1/witness/append |
|
||||
| | | (Ed25519 audit trail) |
|
||||
+------------------+ +----------------------------+
|
||||
```
|
||||
|
||||
**Data flow:**
|
||||
|
||||
1. ESP32 runs CSI capture at 20+ Hz and feeds subcarrier data through existing WASM modules.
|
||||
2. `exo_happiness_score.rs` collects outputs from emotion, gait, flow, and dwell modules every scoring window (default: 30 seconds).
|
||||
3. The 8-dim happiness vector is packed as a 32-byte payload (8x float32) and sent via UDP to port 5555 on 169.254.42.1.
|
||||
4. A lightweight bridge task on the Seed receives the UDP packet and POSTs it to `/api/v1/store/ingest` with metadata (room ID, timestamp, MAC).
|
||||
5. The Seed's drift detection engine monitors the happiness vector stream and flags anomalies (sudden drops, sustained low scores).
|
||||
6. Every ingested vector is appended to the Seed's Ed25519 witness chain, providing a tamper-proof audit trail.
|
||||
|
||||
### 4. Seed Drift Detection for Happiness Trends
|
||||
|
||||
The Seed's built-in drift detection compares incoming vectors against a rolling baseline:
|
||||
|
||||
- **Check-in calibration:** When a new guest checks in, event 694 resets the baseline.
|
||||
- **Drift threshold:** Configurable (default: cosine distance > 0.3 from baseline triggers alert).
|
||||
- **Trend window:** Last 20 vectors (~10 minutes at 30s intervals).
|
||||
- **Alert routing:** Seed webhook notifies hotel management system when happiness trend is declining.
|
||||
|
||||
### 5. RuView Live Dashboard Update
|
||||
|
||||
`ruview_live.py` gains a `--seed` flag:
|
||||
|
||||
```bash
|
||||
python ruview_live.py --port COM5 --seed 169.254.42.1 --mode happiness
|
||||
```
|
||||
|
||||
This mode displays:
|
||||
- Real-time 8-dim radar chart of the happiness vector
|
||||
- Scalar happiness score (0-100) with color coding (red/yellow/green)
|
||||
- Trend sparkline over the last hour
|
||||
- Seed witness chain status (last hash, chain length)
|
||||
- Room-level aggregate when multiple ESP32 nodes report
|
||||
|
||||
### 6. Architecture
|
||||
|
||||
```
|
||||
+------------------------------------------+
|
||||
| Hotel Room |
|
||||
| |
|
||||
| [ESP32-S3] [Cognitum Seed] |
|
||||
| COM5 or COM7 169.254.42.1 |
|
||||
| 4MB or 8MB flash Pi Zero 2 W |
|
||||
| | | |
|
||||
| | WiFi CSI | PIR, reed, |
|
||||
| | 20+ Hz | BME280, |
|
||||
| v | vibration |
|
||||
| +-----------+ | |
|
||||
| | Tier 2 DSP| v |
|
||||
| | presence | +-------------+ |
|
||||
| | vitals | | Seed API | |
|
||||
| | gait | | 98 endpoints| |
|
||||
| | fall det | | 398 vectors | |
|
||||
| +-----------+ | dim=8 | |
|
||||
| | +-------------+ |
|
||||
| v ^ |
|
||||
| +-----------+ UDP 5555 | |
|
||||
| | WASM edge |─────────────┘ |
|
||||
| | happiness | |
|
||||
| | score | Drift alerts |
|
||||
| | (690-694) |◄────────────── |
|
||||
| +-----------+ /api/v1/drift/check |
|
||||
| |
|
||||
+------------------------------------------+
|
||||
|
|
||||
| MQTT / HTTP
|
||||
v
|
||||
+------------------+
|
||||
| Hotel Management |
|
||||
| System / RuView |
|
||||
| Live Dashboard |
|
||||
+------------------+
|
||||
```
|
||||
|
||||
### 7. 4MB Flash Support
|
||||
|
||||
The 4MB ESP32-S3 variant (COM5) is officially supported for happiness scoring. The existing `partitions_4mb.csv` and `sdkconfig.defaults.4mb` from ADR-265 provide dual OTA slots (1.856 MB each), sufficient for the full Tier 2 DSP firmware plus `exo_happiness_score.wasm` (estimated < 40 KB).
|
||||
|
||||
Build for 4MB variant:
|
||||
|
||||
```bash
|
||||
cp sdkconfig.defaults.4mb sdkconfig.defaults
|
||||
idf.py build
|
||||
```
|
||||
|
||||
The WASM module loader selects which modules to instantiate based on available heap. On the 4MB/2MB PSRAM variant, happiness scoring runs with a reduced scoring window (60s instead of 30s) to conserve memory.
|
||||
|
||||
### 8. Privacy Considerations
|
||||
|
||||
- **No cameras.** All sensing is RF-based (WiFi subcarrier amplitude/phase).
|
||||
- **No facial recognition.** Happiness is inferred from movement patterns, not expressions.
|
||||
- **No audio capture.** Breathing rate is extracted from chest wall displacement via RF, not microphone.
|
||||
- **No PII stored on device.** Vectors are anonymous; room-to-guest mapping lives only in the hotel PMS.
|
||||
- **Seed witness chain** provides auditable proof of what data was collected and when, satisfying GDPR Article 30 record-keeping requirements.
|
||||
- **Guest opt-out:** A physical switch on the ESP32 node (GPIO connected to a toggle) disables CSI capture entirely. The Seed's reed switch can also serve as a "privacy mode" trigger (door-mounted magnet removed = sensing paused).
|
||||
- **Data retention:** Vectors are retained on the Seed for the duration of the stay plus 24 hours, then purged. The witness chain retains hashes (not vectors) indefinitely for audit.
|
||||
|
||||
### 9. API Integration
|
||||
|
||||
Key Cognitum Seed endpoints used:
|
||||
|
||||
| Endpoint | Method | Purpose |
|
||||
|----------|--------|---------|
|
||||
| `/api/v1/store/ingest` | POST | Ingest 8-dim happiness vector |
|
||||
| `/api/v1/store/query` | POST | Retrieve vectors by room/time range |
|
||||
| `/api/v1/drift/check` | GET | Check if current vector drifts from baseline |
|
||||
| `/api/v1/drift/configure` | PUT | Set drift threshold and window size |
|
||||
| `/api/v1/witness/append` | POST | Append event to Ed25519 custody chain |
|
||||
| `/api/v1/witness/verify` | GET | Verify chain integrity |
|
||||
| `/api/v1/sensors/bme280` | GET | Room temperature/humidity (comfort correlation) |
|
||||
| `/api/v1/sensors/pir` | GET | PIR presence (cross-validate with CSI) |
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Provides real-time, objective guest satisfaction measurement without surveys or wearables.
|
||||
- Reuses four existing WASM modules -- the happiness module is a fusion layer, not a rewrite.
|
||||
- The Seed's 8-dim vector store is a natural fit; no schema changes needed.
|
||||
- Ed25519 witness chain satisfies hospitality industry audit requirements and GDPR record-keeping.
|
||||
- Both 4MB and 8MB ESP32-S3 variants are supported, enabling low-cost deployment at scale (~$8 per room for the 4MB node).
|
||||
- Seed's environmental sensors (BME280, PIR) provide complementary context (room temperature, humidity) that can be correlated with happiness scores.
|
||||
- No cloud dependency -- all processing is local (ESP32 edge + Seed link-local network).
|
||||
|
||||
### Negative
|
||||
|
||||
- Happiness inference from movement patterns is a proxy, not a direct measurement. Correlation with actual guest satisfaction must be validated empirically.
|
||||
- The 4MB variant has reduced scoring frequency (60s vs 30s) due to memory constraints.
|
||||
- UDP transport between ESP32 and Seed is unreliable; packets may be lost. Mitigation: sequence numbers and a small retry buffer on the ESP32 side.
|
||||
- Link-local addressing (169.254.x.x) limits the Seed to the same network segment as the ESP32. Multi-room deployments need one Seed per subnet or a routed bridge.
|
||||
- Drift detection thresholds require per-property tuning; a luxury resort has different movement patterns than a budget hotel.
|
||||
- The system cannot distinguish between guests in a multi-occupancy room without additional multi-target CSI clustering, which is experimental (ADR-064, Tier 3).
|
||||
@@ -1,278 +0,0 @@
|
||||
# ADR-066: ESP32 CSI Swarm with Cognitum Seed Coordinator
|
||||
|
||||
**Status:** Proposed
|
||||
**Date:** 2026-03-20
|
||||
**Deciders:** @ruvnet
|
||||
**Related:** ADR-065 (happiness scoring + Seed bridge), ADR-039 (edge intelligence), ADR-060 (provisioning), ADR-018 (CSI binary protocol), ADR-040 (WASM runtime)
|
||||
|
||||
## Context
|
||||
|
||||
ADR-065 established a single ESP32-S3 node pushing happiness vectors to a Cognitum Seed at `169.254.42.1` (Pi Zero 2 W, firmware 0.7.0). The Seed is now on the same WiFi network (`RedCloverWifi`, `10.1.10.236`) as the ESP32 node (`10.1.10.168`).
|
||||
|
||||
The Seed already exposes REST APIs for:
|
||||
- Peer discovery (`/api/v1/peers`) — 0 peers currently registered
|
||||
- Delta sync (`/api/v1/delta/pull`, `/api/v1/delta/push`) — epoch-based replication
|
||||
- Reflex rules (`/api/v1/sensor/reflex/rules`) — 3 rules (fragility alarm, drift cutoff, HD anomaly indicator)
|
||||
- Actuators (`/api/v1/sensor/actuators`) — relay + PWM outputs
|
||||
- Cognitive engine (`/api/v1/cognitive/tick`) — periodic inference loop
|
||||
- Witness chain (`/api/v1/custody/epoch`) — epoch 316, cryptographically signed
|
||||
- kNN search (`/api/v1/store/search`) — similarity queries across the full vector store
|
||||
|
||||
A hotel deployment requires multiple ESP32 nodes (lobby, hallway, restaurant, rooms) coordinated as a swarm with centralized analytics on the Seed.
|
||||
|
||||
## Decision
|
||||
|
||||
Implement a Seed-coordinated ESP32 swarm where each node operates autonomously for CSI sensing and edge processing, while the Seed serves as the swarm coordinator for registration, aggregation, drift detection, cross-zone inference, and actuator control.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
ESP32 Node A ESP32 Node B ESP32 Node C
|
||||
(Lobby) (Hallway) (Restaurant)
|
||||
node_id=1 node_id=2 node_id=3
|
||||
10.1.10.168 10.1.10.xxx 10.1.10.xxx
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ WiFi CSI │ │ WiFi CSI │ │ WiFi CSI │
|
||||
│ Tier 2 DSP │ │ Tier 2 DSP │ │ Tier 2 DSP │
|
||||
│ WASM Tier 3 │ │ WASM Tier 3 │ │ WASM Tier 3 │
|
||||
│ Swarm Bridge │ │ Swarm Bridge │ │ Swarm Bridge │
|
||||
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
|
||||
│ HTTP POST │ HTTP POST │ HTTP POST
|
||||
│ (happiness vectors, │ │
|
||||
│ heartbeat, events) │ │
|
||||
└──────────┬───────────────┴──────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────┐
|
||||
│ Cognitum Seed │
|
||||
│ (Coordinator) │
|
||||
│ 10.1.10.236 │
|
||||
├───────────────┤
|
||||
│ Vector Store │ ← 8-dim vectors tagged with node_id + zone
|
||||
│ kNN Search │ ← Cross-zone similarity ("which room matches?")
|
||||
│ Drift Detect │ ← Global mood trend across all zones
|
||||
│ Witness Chain │ ← Tamper-proof audit trail per node
|
||||
│ Reflex Rules │ ← Trigger actuators on swarm-wide patterns
|
||||
│ Cognitive Eng │ ← Periodic cross-zone inference
|
||||
│ Peer Registry │ ← Node health, last-seen, capabilities
|
||||
└───────────────┘
|
||||
```
|
||||
|
||||
### Swarm Protocol
|
||||
|
||||
#### 1. Node Registration (on boot)
|
||||
|
||||
Each ESP32 registers with the Seed via HTTP POST on startup. The Seed's peer discovery API tracks active nodes.
|
||||
|
||||
```
|
||||
POST /api/v1/store/ingest
|
||||
{
|
||||
"vectors": [{
|
||||
"id": "node-1-reg",
|
||||
"values": [0,0,0,0,0,0,0,0],
|
||||
"metadata": {
|
||||
"type": "registration",
|
||||
"node_id": 1,
|
||||
"zone": "lobby",
|
||||
"mac": "1C:DB:D4:83:D2:40",
|
||||
"ip": "10.1.10.168",
|
||||
"firmware": "0.5.0",
|
||||
"capabilities": ["csi", "tier2", "presence", "vitals", "happiness"],
|
||||
"flash_mb": 4,
|
||||
"psram_mb": 2
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Heartbeat (every 30 seconds)
|
||||
|
||||
```
|
||||
POST /api/v1/store/ingest
|
||||
{
|
||||
"vectors": [{
|
||||
"id": "node-1-hb-{epoch}",
|
||||
"values": [happiness, gait, stride, fluidity, calm, posture, dwell, social],
|
||||
"metadata": {
|
||||
"type": "heartbeat",
|
||||
"node_id": 1,
|
||||
"zone": "lobby",
|
||||
"uptime_s": 3600,
|
||||
"csi_frames": 72000,
|
||||
"free_heap": 317140,
|
||||
"presence_now": true,
|
||||
"persons": 2,
|
||||
"rssi": -60
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Happiness Vector Ingestion (every 5 seconds when presence detected)
|
||||
|
||||
```
|
||||
POST /api/v1/store/ingest
|
||||
{
|
||||
"vectors": [{
|
||||
"id": "node-1-h-{epoch}-{ts}",
|
||||
"values": [0.72, 0.65, 0.80, 0.71, 0.55, 0.60, 0.85, 0.45],
|
||||
"metadata": {
|
||||
"type": "happiness",
|
||||
"node_id": 1,
|
||||
"zone": "lobby",
|
||||
"timestamp_ms": 1742486400000,
|
||||
"persons": 2,
|
||||
"direction": "entering"
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Cross-Zone Queries (Seed-side)
|
||||
|
||||
The Seed can answer questions across the entire swarm:
|
||||
|
||||
```
|
||||
POST /api/v1/store/search
|
||||
{"vector": [0.8, 0.7, 0.9, 0.8, 0.6, 0.7, 0.9, 0.5], "k": 5}
|
||||
|
||||
Response: nearest neighbors across all zones, showing which
|
||||
rooms had the most similar mood to a "happy" reference vector.
|
||||
```
|
||||
|
||||
#### 5. Reflex Rules for Swarm Patterns
|
||||
|
||||
Configure the Seed's reflex engine to act on swarm-wide patterns:
|
||||
|
||||
| Rule | Trigger | Action | Use Case |
|
||||
|------|---------|--------|----------|
|
||||
| `low_happiness_alert` | Mean happiness < 0.3 across 3+ nodes for 5 min | Activate `alarm` relay | Staff alert: guest dissatisfaction |
|
||||
| `crowd_surge` | Presence count > 10 across lobby + hallway | PWM indicator brightness 100% | Lobby congestion warning |
|
||||
| `zone_drift` | Drift score > 0.5 on any node | Log to witness chain | Trend change documentation |
|
||||
| `ghost_anomaly` | Event 650 (anomaly) from any node | Notify + log | Security: unexpected RF disturbance |
|
||||
|
||||
### ESP32 Firmware: Swarm Bridge Module
|
||||
|
||||
New module `swarm_bridge.c` added to the CSI firmware, activated via NVS config:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
char seed_url[64]; // e.g. "http://10.1.10.236"
|
||||
char zone_name[16]; // e.g. "lobby"
|
||||
uint16_t heartbeat_sec; // Default: 30
|
||||
uint16_t ingest_sec; // Default: 5
|
||||
uint8_t enabled; // 0 = disabled, 1 = enabled
|
||||
} swarm_config_t;
|
||||
```
|
||||
|
||||
NVS keys (provisioned via `provision.py --seed-url http://10.1.10.236 --zone lobby`):
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `seed_url` | string | (empty) | Seed base URL; empty = swarm disabled |
|
||||
| `zone_name` | string | `"default"` | Zone identifier for this node |
|
||||
| `swarm_hb` | u16 | 30 | Heartbeat interval (seconds) |
|
||||
| `swarm_ingest` | u16 | 5 | Vector ingest interval (seconds) |
|
||||
|
||||
The swarm bridge runs as a FreeRTOS task on Core 0 (separate from DSP on Core 1):
|
||||
|
||||
```
|
||||
swarm_bridge_task (Core 0, priority 3, stack 4096)
|
||||
├── On boot: POST registration to Seed
|
||||
├── Every 30s: POST heartbeat with latest happiness vector
|
||||
├── Every 5s (if presence): POST happiness vector
|
||||
└── On event 650+ (anomaly): POST immediately
|
||||
```
|
||||
|
||||
HTTP client uses `esp_http_client` (already in ESP-IDF, no extra dependencies). JSON is formatted with `snprintf` (no cJSON dependency needed for the small payloads).
|
||||
|
||||
### Node Discovery and Addressing
|
||||
|
||||
Nodes find the Seed via:
|
||||
|
||||
1. **NVS provisioned URL** (primary) — `provision.py --seed-url http://10.1.10.236`
|
||||
2. **mDNS fallback** — Seed advertises `_cognitum._tcp.local`; ESP32 resolves `cognitum.local`
|
||||
3. **Link-local fallback** — `http://169.254.42.1` when connected via USB
|
||||
|
||||
### Vector ID Scheme
|
||||
|
||||
```
|
||||
{node_id}-{type}-{epoch}-{timestamp_ms}
|
||||
```
|
||||
|
||||
Examples:
|
||||
- `1-reg` — Node 1 registration
|
||||
- `1-hb-316` — Node 1 heartbeat at epoch 316
|
||||
- `1-h-316-1742486400000` — Node 1 happiness vector at epoch 316, timestamp T
|
||||
- `2-h-316-1742486401000` — Node 2 happiness vector at same epoch
|
||||
|
||||
### Witness Chain Integration
|
||||
|
||||
Every vector ingested into the Seed increments the epoch and extends the witness chain. The chain provides:
|
||||
|
||||
- **Per-node audit trail** — filter by node_id metadata to get one node's history
|
||||
- **Tamper detection** — Ed25519 signed, hash-chained; break = detectable
|
||||
- **Regulatory compliance** — prove "sensor X reported Y at time Z" for disputes
|
||||
- **Cross-node ordering** — Seed epoch gives total order across all nodes
|
||||
|
||||
### Scaling Considerations
|
||||
|
||||
| Nodes | Vectors/hour | Seed storage/day | kNN latency |
|
||||
|-------|---|---|---|
|
||||
| 1 | 720 | ~1.5 MB | < 1 ms |
|
||||
| 5 | 3,600 | ~7.5 MB | < 2 ms |
|
||||
| 10 | 7,200 | ~15 MB | < 5 ms |
|
||||
| 20 | 14,400 | ~30 MB | < 10 ms |
|
||||
|
||||
The Seed's Pi Zero 2 W has 512 MB RAM and typically an 8-32 GB SD card. At 30 MB/day for 20 nodes, storage lasts 250+ days before compaction is needed. The Seed's optimizer runs automatic compaction in the background.
|
||||
|
||||
### Provisioning for Swarm
|
||||
|
||||
```bash
|
||||
# Node 1: Lobby (COM5, existing)
|
||||
python provision.py --port COM5 \
|
||||
--ssid "RedCloverWifi" --password "redclover2.4" \
|
||||
--node-id 1 --seed-url "http://10.1.10.236" --zone "lobby"
|
||||
|
||||
# Node 2: Hallway (future device)
|
||||
python provision.py --port COM6 \
|
||||
--ssid "RedCloverWifi" --password "redclover2.4" \
|
||||
--node-id 2 --seed-url "http://10.1.10.236" --zone "hallway"
|
||||
|
||||
# Node 3: Restaurant (future device)
|
||||
python provision.py --port COM8 \
|
||||
--ssid "RedCloverWifi" --password "redclover2.4" \
|
||||
--node-id 3 --seed-url "http://10.1.10.236" --zone "restaurant"
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **Zero infrastructure** — no cloud, no server, no database. Seed + ESP32s + WiFi router is the entire stack
|
||||
- **Autonomous nodes** — each ESP32 runs full Tier 2 DSP independently; Seed loss degrades gracefully to local-only operation
|
||||
- **Cryptographic audit** — witness chain gives tamper-proof history for every observation across all nodes
|
||||
- **Real-time cross-zone analytics** — Seed kNN search answers "which zones are happy/stressed right now" in < 5 ms
|
||||
- **Physical actuators** — Seed's relay/PWM outputs can trigger real-world actions (lights, alarms, displays) based on swarm-wide patterns
|
||||
- **Horizontal scaling** — add ESP32 nodes by flashing firmware + running provision.py; no Seed reconfiguration needed
|
||||
- **Privacy-preserving** — no cameras, no audio, no PII; only 8-dimensional feature vectors stored
|
||||
|
||||
### Negative
|
||||
|
||||
- **Single point of aggregation** — Seed failure loses cross-zone analytics (nodes continue autonomously)
|
||||
- **WiFi dependency** — nodes must be on the same network as the Seed; no mesh/LoRa fallback yet
|
||||
- **HTTP overhead** — REST/JSON adds ~200 bytes overhead per vector vs raw binary UDP; acceptable at 5-second intervals
|
||||
- **Pi Zero 2 W limits** — 512 MB RAM, single-core ARM; adequate for 20 nodes but not 100+
|
||||
- **No WASM OTA via Seed** — currently WASM modules are uploaded per-node; future work could use Seed as WASM distribution hub
|
||||
|
||||
### Implementation Progress
|
||||
|
||||
**ADR-069** implements the first stage of this swarm vision with live hardware validation (2026-04-02). A single ESP32-S3 node (COM9, firmware v0.5.2) was validated sending CSI-derived feature vectors through a host-side bridge into the Cognitum Seed's RVF store (firmware v0.8.1). The pipeline confirmed: UDP streaming (211 packets/15s), 8-dim feature extraction, batched HTTPS ingest (4 batches of 5 vectors), and witness chain integrity (193 entries, SHA-256 verified). Multi-node deployment (Phase 4 of ADR-069) is the next step toward the full swarm architecture described here.
|
||||
|
||||
### Future Work
|
||||
|
||||
- **Seed-initiated WASM push** — Seed distributes WASM modules to all nodes via their OTA endpoints
|
||||
- **mDNS auto-discovery** — nodes find Seed without provisioned URL
|
||||
- **Mesh fallback** — ESP-NOW peer-to-peer when WiFi is down
|
||||
- **Multi-Seed federation** — multiple Seeds for multi-floor/multi-building deployments
|
||||
- **Seed dashboard** — web UI on the Seed showing live swarm map with per-zone happiness
|
||||
@@ -1,151 +0,0 @@
|
||||
# ADR-067: RuVector v2.0.4 to v2.0.5 Upgrade + New Crate Adoption
|
||||
|
||||
**Status:** Proposed
|
||||
**Date:** 2026-03-23
|
||||
**Deciders:** @ruvnet
|
||||
**Related:** ADR-016 (RuVector training pipeline integration), ADR-017 (RuVector signal + MAT integration), ADR-029 (RuvSense multistatic sensing)
|
||||
|
||||
## Context
|
||||
|
||||
RuView currently pins all five core RuVector crates at **v2.0.4** (from crates.io) plus a vendored `ruvector-crv` v0.1.1 and optional `ruvector-gnn` v2.0.5. The upstream RuVector workspace has moved to **v2.0.5** with meaningful improvements to the crates we depend on, and has introduced new crates that could benefit RuView's detection pipeline.
|
||||
|
||||
### Current Integration Map
|
||||
|
||||
| RuView Module | RuVector Crate | Current Version | Purpose |
|
||||
|---------------|----------------|-----------------|---------|
|
||||
| `signal/subcarrier.rs` | ruvector-mincut | 2.0.4 | Graph min-cut subcarrier partitioning |
|
||||
| `signal/spectrogram.rs` | ruvector-attn-mincut | 2.0.4 | Attention-gated spectrogram denoising |
|
||||
| `signal/bvp.rs` | ruvector-attention | 2.0.4 | Attention-weighted BVP aggregation |
|
||||
| `signal/fresnel.rs` | ruvector-solver | 2.0.4 | Fresnel geometry estimation |
|
||||
| `mat/triangulation.rs` | ruvector-solver | 2.0.4 | TDoA survivor localization |
|
||||
| `mat/breathing.rs` | ruvector-temporal-tensor | 2.0.4 | Tiered compressed breathing buffer |
|
||||
| `mat/heartbeat.rs` | ruvector-temporal-tensor | 2.0.4 | Tiered compressed heartbeat spectrogram |
|
||||
| `viewpoint/*` (4 files) | ruvector-attention | 2.0.4 | Cross-viewpoint fusion with geometric bias |
|
||||
| `crv/` (optional) | ruvector-crv | 0.1.1 (vendored) | CRV protocol integration |
|
||||
| `crv/` (optional) | ruvector-gnn | 2.0.5 | GNN graph topology |
|
||||
|
||||
### What Changed Upstream (v2.0.4 → v2.0.5 → HEAD)
|
||||
|
||||
**ruvector-mincut:**
|
||||
- Flat capacity matrix + allocation reuse — **10-30% faster** for all min-cut operations
|
||||
- Tier 2-3 Dynamic MinCut (ADR-124): Gomory-Hu tree construction for fast global min-cut, incremental edge insert/delete without full recomputation
|
||||
- Source-anchored canonical min-cut with SHA-256 witness hashing
|
||||
- Fixed: unsafe indexing removed, WASM Node.js panic from `std::time`
|
||||
|
||||
**ruvector-attention / ruvector-attn-mincut:**
|
||||
- Migrated to workspace versioning (no API changes)
|
||||
- Documentation improvements
|
||||
|
||||
**ruvector-temporal-tensor:**
|
||||
- Formatting fixes only (no API changes)
|
||||
|
||||
**ruvector-gnn:**
|
||||
- Panic replaced with `Result` in `MultiHeadAttention` and `RuvectorLayer` constructors (breaking improvement — safer)
|
||||
- Bumped to v2.0.5
|
||||
|
||||
**sona (new — Self-Optimizing Neural Architecture):**
|
||||
- v0.1.6 → v0.1.8: state persistence (`loadState`/`saveState`), trajectory counter fix
|
||||
- Micro-LoRA and Base-LoRA for instant and background learning
|
||||
- EWC++ (Elastic Weight Consolidation) to prevent catastrophic forgetting
|
||||
- ReasoningBank pattern extraction and similarity search
|
||||
- WASM support for edge devices
|
||||
|
||||
**ruvector-coherence (new):**
|
||||
- Spectral coherence scoring for graph index health
|
||||
- Fiedler eigenvalue estimation, effective resistance sampling
|
||||
- HNSW health monitoring with alerts
|
||||
- Batch evaluation of attention mechanism quality
|
||||
|
||||
**ruvector-core (new):**
|
||||
- ONNX embedding support for real semantic embeddings
|
||||
- HNSW index with SIMD-accelerated distance metrics
|
||||
- Quantization (4-32x memory reduction)
|
||||
- Arena allocator for cache-optimized operations
|
||||
|
||||
## Decision
|
||||
|
||||
### Phase 1: Version Bump (Low Risk)
|
||||
|
||||
Bump the 5 core crates from v2.0.4 to v2.0.5 in the workspace `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
ruvector-mincut = "2.0.5" # was 2.0.4 — 10-30% faster, safer
|
||||
ruvector-attn-mincut = "2.0.5" # was 2.0.4 — workspace versioning
|
||||
ruvector-temporal-tensor = "2.0.5" # was 2.0.4 — fmt only
|
||||
ruvector-solver = "2.0.5" # was 2.0.4 — workspace versioning
|
||||
ruvector-attention = "2.0.5" # was 2.0.4 — workspace versioning
|
||||
```
|
||||
|
||||
**Expected impact:** The mincut performance improvement directly benefits `signal/subcarrier.rs` which runs subcarrier graph partitioning every tick. 10-30% faster partitioning reduces per-frame CPU cost.
|
||||
|
||||
### Phase 2: Add ruvector-coherence (Medium Value)
|
||||
|
||||
Add `ruvector-coherence` with `spectral` feature to `wifi-densepose-ruvector`:
|
||||
|
||||
**Use case:** Replace or augment the custom phase coherence logic in `viewpoint/coherence.rs` with spectral graph coherence scoring. The current implementation uses phasor magnitude for phase coherence — spectral Fiedler estimation would provide a more robust measure of multi-node CSI consistency, especially for detecting when a node's signal quality degrades.
|
||||
|
||||
**Integration point:** `viewpoint/coherence.rs` — add `SpectralCoherenceScore` as a secondary coherence metric alongside existing phase phasor coherence. Use spectral gap estimation to detect structural changes in the multi-node CSI graph (e.g., a node dropping out or a new reflector appearing).
|
||||
|
||||
### Phase 3: Add SONA for Adaptive Learning (High Value)
|
||||
|
||||
Replace the logistic regression adaptive classifier in the sensing server with a SONA-backed learning engine:
|
||||
|
||||
**Current state:** The sensing server's adaptive training (`POST /api/v1/adaptive/train`) uses a hand-rolled logistic regression on 15 CSI features. It requires explicit labeled recordings and provides no cross-session persistence.
|
||||
|
||||
**Proposed improvement:** Use `sona::SonaEngine` to:
|
||||
1. **Learn from implicit feedback** — trajectory tracking on person-count decisions (was the count stable? did the user correct it?)
|
||||
2. **Persist across sessions** — `saveState()`/`loadState()` replaces the current `adaptive_model.json`
|
||||
3. **Pattern matching** — `find_patterns()` enables "this CSI signature looks like room X where we learned Y"
|
||||
4. **Prevent forgetting** — EWC++ ensures learning in a new room doesn't overwrite patterns from previous rooms
|
||||
|
||||
**Integration point:** New `adaptive_sona.rs` module in `wifi-densepose-sensing-server`, behind a `sona` feature flag. The existing logistic regression remains the default.
|
||||
|
||||
### Phase 4: Evaluate ruvector-core for CSI Embeddings (Exploratory)
|
||||
|
||||
**Current state:** The person detection pipeline uses hand-crafted features (variance, change_points, motion_band_power, spectral_power) with fixed normalization ranges.
|
||||
|
||||
**Potential:** Use `ruvector-core`'s ONNX embedding support to generate learned CSI embeddings that capture room geometry, person count, and activity patterns in a single vector. This would enable:
|
||||
- Similarity search: "is this CSI frame similar to known 2-person patterns?"
|
||||
- Transfer learning: embeddings learned in one room partially transfer to similar rooms
|
||||
- Quantized storage: 4-32x memory reduction for pattern databases
|
||||
|
||||
**Status:** Exploratory — requires training data collection and embedding model design. Not a near-term target.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- **Phase 1:** Free 10-30% performance gain in subcarrier partitioning. Security fixes (unsafe indexing, WASM panic). Zero API changes required.
|
||||
- **Phase 2:** More robust multi-node coherence detection. Helps with the "flickering persons" issue (#292) by providing a second opinion on signal quality.
|
||||
- **Phase 3:** Fundamentally improves the adaptive learning pipeline. Users no longer need to manually record labeled data — the system learns from ongoing use.
|
||||
- **Phase 4:** Path toward real ML-based detection instead of heuristic thresholds.
|
||||
|
||||
### Negative
|
||||
- **Phase 1:** Minimal risk — semver minor bump, no API breaks.
|
||||
- **Phase 2:** Adds a dependency. Spectral computation has O(n) cost per tick for Fiedler estimation (n = number of subcarriers, typically 56-128). Acceptable.
|
||||
- **Phase 3:** SONA adds ~200KB to the binary. The learning loop needs careful tuning to avoid adapting to noise.
|
||||
- **Phase 4:** Requires significant research and training data. Not guaranteed to outperform tuned heuristics for WiFi CSI.
|
||||
|
||||
### Risks
|
||||
- `ruvector-gnn` v2.0.5 changed constructors from panic to `Result` — any existing `crv` feature users need to handle the `Result`. Our vendored `ruvector-crv` may need updates.
|
||||
- SONA's WASM support is experimental — keep it behind a feature flag until validated.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
| Phase | Scope | Effort | Priority |
|
||||
|-------|-------|--------|----------|
|
||||
| 1 | Bump 5 crates to v2.0.5 | 1 hour | High — free perf + security |
|
||||
| 2 | Add ruvector-coherence | 1 day | Medium — improves multi-node stability |
|
||||
| 3 | SONA adaptive learning | 3 days | Medium — replaces manual training workflow |
|
||||
| 4 | CSI embeddings via ruvector-core | 1-2 weeks | Low — exploratory research |
|
||||
|
||||
## Vendor Submodule
|
||||
|
||||
The `vendor/ruvector` git submodule has been updated from commit `f8f2c60` (v2.0.4 era) to `51a3557` (latest `origin/main`). This provides local reference for the full upstream source when developing Phases 2-4.
|
||||
|
||||
## References
|
||||
|
||||
- Upstream repo: https://github.com/ruvnet/ruvector
|
||||
- ADR-124 (Dynamic MinCut): `vendor/ruvector/docs/adr/ADR-124*.md`
|
||||
- SONA docs: `vendor/ruvector/crates/sona/src/lib.rs`
|
||||
- ruvector-coherence spectral: `vendor/ruvector/crates/ruvector-coherence/src/spectral.rs`
|
||||
- ruvector-core embeddings: `vendor/ruvector/crates/ruvector-core/src/embeddings.rs`
|
||||
@@ -1,186 +0,0 @@
|
||||
# ADR-068: Per-Node State Pipeline for Multi-Node Sensing
|
||||
|
||||
| Field | Value |
|
||||
|------------|-------------------------------------|
|
||||
| Status | Accepted |
|
||||
| Date | 2026-03-27 |
|
||||
| Authors | rUv, claude-flow |
|
||||
| Drivers | #249, #237, #276, #282 |
|
||||
| Supersedes | — |
|
||||
|
||||
## Context
|
||||
|
||||
The sensing server (`wifi-densepose-sensing-server`) was originally designed for
|
||||
single-node operation. When multiple ESP32 nodes send CSI frames simultaneously,
|
||||
all data is mixed into a single shared pipeline:
|
||||
|
||||
- **One** `frame_history` VecDeque for all nodes
|
||||
- **One** `smoothed_person_score` / `smoothed_motion` / vital sign buffers
|
||||
- **One** baseline and debounce state
|
||||
|
||||
This means the classification, person count, and vital signs reported to the UI
|
||||
are an uncontrolled aggregate of all nodes' data. The result: the detection
|
||||
window shows identical output regardless of how many nodes are deployed, where
|
||||
people stand, or how many people are in the room (#249 — 24 comments, the most
|
||||
reported issue).
|
||||
|
||||
### Root Cause Verified
|
||||
|
||||
Investigation of `AppStateInner` (main.rs lines 279-367) confirmed:
|
||||
|
||||
| Shared field | Impact |
|
||||
|---------------------------|--------------------------------------------|
|
||||
| `frame_history` | Temporal analysis mixes all nodes' CSI data |
|
||||
| `smoothed_person_score` | Person count aggregates all nodes |
|
||||
| `smoothed_motion` | Motion classification undifferentiated |
|
||||
| `smoothed_hr` / `br` | Vital signs are global, not per-node |
|
||||
| `baseline_motion` | Adaptive baseline learned from mixed data |
|
||||
| `debounce_counter` | All nodes share debounce state |
|
||||
|
||||
## Decision
|
||||
|
||||
Introduce **per-node state tracking** via a `HashMap<u8, NodeState>` in
|
||||
`AppStateInner`. Each ESP32 node (identified by its `node_id` byte) gets an
|
||||
independent sensing pipeline with its own temporal history, smoothing buffers,
|
||||
baseline, and classification state.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
UDP frames │ AppStateInner │
|
||||
───────────► │ │
|
||||
node_id=1 ──► │ node_states: HashMap<u8, NodeState> │
|
||||
node_id=2 ──► │ ├── 1: NodeState { frame_history, │
|
||||
node_id=3 ──► │ │ smoothed_motion, vitals, ... }│
|
||||
│ ├── 2: NodeState { ... } │
|
||||
│ └── 3: NodeState { ... } │
|
||||
│ │
|
||||
│ ┌── Per-Node Pipeline ──┐ │
|
||||
│ │ extract_features() │ │
|
||||
│ │ smooth_and_classify() │ │
|
||||
│ │ smooth_vitals() │ │
|
||||
│ │ score_to_person_count()│ │
|
||||
│ └────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌── Multi-Node Fusion ──┐ │
|
||||
│ │ Aggregate person count │ │
|
||||
│ │ Per-node classification│ │
|
||||
│ │ All-nodes WebSocket msg│ │
|
||||
│ └────────────────────────┘ │
|
||||
│ │
|
||||
│ ──► WebSocket broadcast (sensing_update) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### NodeState Struct
|
||||
|
||||
```rust
|
||||
struct NodeState {
|
||||
frame_history: VecDeque<Vec<f64>>,
|
||||
smoothed_person_score: f64,
|
||||
prev_person_count: usize,
|
||||
smoothed_motion: f64,
|
||||
current_motion_level: String,
|
||||
debounce_counter: u32,
|
||||
debounce_candidate: String,
|
||||
baseline_motion: f64,
|
||||
baseline_frames: u64,
|
||||
smoothed_hr: f64,
|
||||
smoothed_br: f64,
|
||||
smoothed_hr_conf: f64,
|
||||
smoothed_br_conf: f64,
|
||||
hr_buffer: VecDeque<f64>,
|
||||
br_buffer: VecDeque<f64>,
|
||||
rssi_history: VecDeque<f64>,
|
||||
vital_detector: VitalSignDetector,
|
||||
latest_vitals: VitalSigns,
|
||||
last_frame_time: Option<std::time::Instant>,
|
||||
edge_vitals: Option<Esp32VitalsPacket>,
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-Node Aggregation
|
||||
|
||||
- **Person count**: Sum of per-node `prev_person_count` for active nodes
|
||||
(seen within last 10 seconds).
|
||||
- **Classification**: Per-node classification included in `SensingUpdate.nodes`.
|
||||
- **Vital signs**: Per-node vital signs; UI can render per-node or aggregate.
|
||||
- **Signal field**: Generated from the most-recently-updated node's features.
|
||||
- **Stale nodes**: Nodes with no frame for >10 seconds are excluded from
|
||||
aggregation and marked offline (consistent with PR #300).
|
||||
|
||||
### Backward Compatibility
|
||||
|
||||
- The simulated data path (`simulated_data_task`) continues using global state.
|
||||
- Single-node deployments behave identically (HashMap has one entry).
|
||||
- The WebSocket message format (`sensing_update`) remains the same but the
|
||||
`nodes` array now contains all active nodes, and `estimated_persons` reflects
|
||||
the cross-node aggregate.
|
||||
- The edge vitals path (#323 fix) also uses per-node state.
|
||||
|
||||
## Scaling Characteristics
|
||||
|
||||
| Nodes | Per-Node Memory | Total Overhead | Notes |
|
||||
|-------|----------------|----------------|-------|
|
||||
| 1 | ~50 KB | ~50 KB | Identical to current |
|
||||
| 3 | ~50 KB | ~150 KB | Typical home setup |
|
||||
| 10 | ~50 KB | ~500 KB | Small office |
|
||||
| 50 | ~50 KB | ~2.5 MB | Building floor |
|
||||
| 100 | ~50 KB | ~5 MB | Large deployment |
|
||||
| 256 | ~50 KB | ~12.8 MB | Max (u8 node_id) |
|
||||
|
||||
Memory is dominated by `frame_history` (100 frames x ~500 bytes each = ~50 KB
|
||||
per node). This scales linearly and fits comfortably in server memory even at
|
||||
256 nodes.
|
||||
|
||||
## QEMU Validation
|
||||
|
||||
The existing QEMU swarm infrastructure (ADR-062, `scripts/qemu_swarm.py`)
|
||||
supports multi-node simulation with configurable topologies:
|
||||
|
||||
- `star`: Central coordinator + sensor nodes
|
||||
- `mesh`: Fully connected peer network
|
||||
- `line`: Sequential chain
|
||||
- `ring`: Circular topology
|
||||
|
||||
Each QEMU instance runs with a unique `node_id` via NVS provisioning. The
|
||||
swarm health validator (`scripts/swarm_health.py`) checks per-node UART output.
|
||||
|
||||
Validation plan:
|
||||
1. QEMU swarm with 3-5 nodes in mesh topology
|
||||
2. Verify server produces distinct per-node classifications
|
||||
3. Verify aggregate person count reflects multi-node contributions
|
||||
4. Verify stale-node eviction after timeout
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Each node's CSI data is processed independently — no cross-contamination
|
||||
- Person count scales with the number of deployed nodes
|
||||
- Vital signs are per-node, enabling room-level health monitoring
|
||||
- Foundation for spatial localization (per-node positions + triangulation)
|
||||
- Scales to 256 nodes with <13 MB memory overhead
|
||||
|
||||
### Negative
|
||||
- Slightly more memory per node (~50 KB each)
|
||||
- `smooth_and_classify_node` function duplicates some logic from global version
|
||||
- Per-node `VitalSignDetector` instances add CPU cost proportional to node count
|
||||
|
||||
### Risks
|
||||
- Node ID collisions (mitigated by NVS persistence since v0.5.0)
|
||||
- HashMap growth without cleanup (mitigated by stale-node eviction)
|
||||
|
||||
## Related ADRs
|
||||
|
||||
- **ADR-069** (ESP32 CSI → Cognitum Seed RVF Ingest Pipeline) extends this ADR's per-node state architecture with Cognitum Seed integration. Live hardware validation (2026-04-02) confirmed per-node feature vectors flowing through the bridge into the Seed's RVF store with witness chain attestation.
|
||||
|
||||
## References
|
||||
|
||||
- Issue #249: Detection window same regardless (24 comments)
|
||||
- Issue #237: Same display for 0/1/2 people (12 comments)
|
||||
- Issue #276: Only one can be detected (8 comments)
|
||||
- Issue #282: Detection fail (5 comments)
|
||||
- PR #295: Hysteresis smoothing (partial mitigation)
|
||||
- PR #300: ESP32 offline detection after 5s
|
||||
- ADR-062: QEMU Swarm Configurator
|
||||
@@ -1,403 +0,0 @@
|
||||
# ADR-069: ESP32 CSI → Cognitum Seed RVF Ingest Pipeline
|
||||
|
||||
| Field | Value |
|
||||
|------------|----------------------------------------------------------|
|
||||
| Status | Accepted |
|
||||
| Date | 2026-04-02 |
|
||||
| Authors | rUv, claude-flow |
|
||||
| Drivers | #348 (multinode mesh accuracy), Research: Arena Physica |
|
||||
| Supersedes | — |
|
||||
| Related | ADR-066 (ESP32 swarm + Seed coordinator), ADR-068 (per-node state), ADR-018 (CSI binary protocol), ADR-039 (edge intelligence), ADR-065 (happiness scoring + Seed bridge) |
|
||||
|
||||
## Context
|
||||
|
||||
The wifi-densepose project has two hardware components that need to work as an integrated sensing pipeline:
|
||||
|
||||
1. **ESP32-S3** (COM9 / 192.168.1.105) — Captures WiFi CSI at 100 Hz, runs dual-core DSP pipeline (phase extraction, subcarrier selection, breathing/heart rate estimation, presence/fall detection), and sends ADR-018 binary frames via UDP.
|
||||
|
||||
2. **Cognitum Seed** (USB / 169.254.42.1 / 192.168.1.109) — A Pi Zero 2 W edge intelligence appliance running firmware v0.8.1. It provides:
|
||||
- **RVF vector store** — Append-only binary format with content-addressed IDs, kNN queries (cosine/L2/dot), and kNN graph with boundary analysis
|
||||
- **Witness chain** — SHA-256 tamper-evident audit trail for every write operation
|
||||
- **Ed25519 custody** — Device-bound keypair for cryptographic attestation
|
||||
- **Sensor pipeline** — 5 sensors (reed switch, PIR, vibration, ADS1115 4-ch ADC, BME280), 13 drift detectors, anti-spoofing
|
||||
- **Cognitive container** — Spectral graph analysis with Stoer-Wagner min-cut fragility scoring
|
||||
- **MCP proxy** — 114 tools via JSON-RPC 2.0 for AI assistant integration
|
||||
- **Thermal governor** — DVFS management with zone-based frequency scaling
|
||||
- **Temporal coherence** — Phase boundary detection across vector store evolution
|
||||
- **Swarm sync** — Epoch-based delta replication between peers
|
||||
- **Reflex rules** — 3 rules (fragility alarm, drift cutoff, HD anomaly indicator)
|
||||
- **98 HTTPS API endpoints** with per-client bearer token authentication
|
||||
|
||||
### Current State
|
||||
|
||||
| Component | Status | Details |
|
||||
|-----------|--------|---------|
|
||||
| ESP32 CSI capture | Working | 100 Hz, ADR-018 binary frames via UDP |
|
||||
| ESP32 edge DSP | Working | 10-stage pipeline on Core 1 (phase, variance, vitals, fall) |
|
||||
| ESP32 → sensing-server | Working | UDP port 5005, binary protocol |
|
||||
| Cognitum Seed | Online | v0.8.1, paired, 19 vectors, epoch 25, WiFi connected |
|
||||
| Seed vector store | Working | 8-dim RVF, kNN queries in 85ms for 20k vectors |
|
||||
| Seed MCP proxy | Working | 114 tools, default-deny policy |
|
||||
| ESP32 → Seed pipeline | **Validated** | Bridge on host laptop, UDP 5006 → HTTPS ingest (see Validation Results) |
|
||||
|
||||
### Gap Analysis (from Arena Physica research)
|
||||
|
||||
Arena Physica's approach (Heaviside-0 forward model, Marconi-0 inverse diffusion) demonstrates that neural surrogates for Maxwell's equations are production-viable. Our research identified that:
|
||||
|
||||
1. **Physics-informed intermediate supervision** — Evaluating pipeline stages independently catches failures that end-to-end metrics miss
|
||||
2. **Vector embeddings for EM fields** — Storing CSI features as vectors enables similarity search for environment fingerprinting and anomaly detection
|
||||
3. **Witness chain for sensing integrity** — Tamper-evident audit trails are critical for healthcare/safety applications (fall detection, vital signs)
|
||||
4. **Edge compute for inference** — Pi Zero 2 W can run ~2.5M parameter models at 10+ Hz with INT8 quantization
|
||||
|
||||
### Problem
|
||||
|
||||
There is no pipeline connecting ESP32 CSI sensing to the Cognitum Seed's vector store. The ESP32 sends raw CSI frames to the Rust sensing-server (typically running on a laptop/desktop), but cannot leverage the Seed's:
|
||||
- Persistent vector storage with kNN search
|
||||
- Cryptographic witness chain for data integrity
|
||||
- Cognitive container for structural analysis
|
||||
- Sensor fusion with environmental sensors (BME280 temperature/humidity, PIR motion)
|
||||
- Swarm sync for multi-Seed deployments
|
||||
|
||||
## Decision
|
||||
|
||||
Build a three-stage pipeline connecting ESP32 CSI capture to Cognitum Seed RVF storage:
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌──────────────────────────┐
|
||||
│ ESP32-S3 (COM9) │
|
||||
│ node_id=1 │
|
||||
│ 192.168.1.105 │
|
||||
│ Firmware v0.5.2 │
|
||||
│ ┌──────────────────────┐ │
|
||||
│ │ Core 0: WiFi + CSI │ │
|
||||
│ │ 100 Hz capture │ │
|
||||
│ │ ADR-018 framing │ │
|
||||
│ ├──────────────────────┤ │
|
||||
│ │ Core 1: Edge DSP │ │
|
||||
│ │ Phase extraction │ │
|
||||
│ │ Subcarrier select │ │
|
||||
│ │ Vital signs (HR/BR)│ │
|
||||
│ │ Presence/fall det. │ │
|
||||
│ │ Feature vector │ │◄── 8-dim feature extraction
|
||||
│ └──────────┬───────────┘ │
|
||||
│ │ UDP │
|
||||
└────────────┼─────────────┘
|
||||
│ Port 5005 (raw CSI, magic 0xC5110001)
|
||||
│ + Port 5006 (vitals 0xC5110002 + features 0xC5110003)
|
||||
▼
|
||||
┌────────────────────────────────────────────┐
|
||||
│ Host Laptop (192.168.1.20) │
|
||||
│ Bridge script (Python) │
|
||||
│ ┌────────────────────────────────────────┐ │
|
||||
│ │ Stage 1: CSI Receiver │ │
|
||||
│ │ UDP listener on port 5006 │ │
|
||||
│ │ Parses 0xC5110003 feature packets │ │
|
||||
│ │ (also accepts 0xC5110001/0002) │ │
|
||||
│ │ Batches 10 vectors per ingest │ │
|
||||
│ └──────────┬─────────────────────────────┘ │
|
||||
└────────────┼───────────────────────────────┘
|
||||
│ HTTPS POST (bearer token)
|
||||
▼
|
||||
┌────────────────────────────────────────────┐
|
||||
│ Cognitum Seed (Pi Zero 2 W) │
|
||||
│ 169.254.42.1 / 192.168.1.109 │
|
||||
│ Firmware v0.8.1 │
|
||||
│ ┌────────────────────────────────────────┐ │
|
||||
│ │ Stage 2: RVF Ingest │ │
|
||||
│ │ POST /api/v1/store/ingest │ │
|
||||
│ │ Content-addressed vector ID │ │
|
||||
│ │ Metadata: node_id, timestamp, type │ │
|
||||
│ │ Witness chain entry per batch │ │
|
||||
│ ├────────────────────────────────────────┤ │
|
||||
│ │ Stage 3: Cognitive Analysis │ │
|
||||
│ │ kNN graph rebuild (every 10s) │ │
|
||||
│ │ Boundary analysis (fragility) │ │
|
||||
│ │ Temporal coherence (phase detect) │ │
|
||||
│ │ Reflex rules (alarm triggers) │ │
|
||||
│ ├────────────────────────────────────────┤ │
|
||||
│ │ Existing Sensors │ │
|
||||
│ │ BME280 → temp/humidity/pressure │ │
|
||||
│ │ PIR → motion ground truth │ │
|
||||
│ │ Reed switch → door/window state │ │
|
||||
│ │ ADS1115 → analog inputs │ │
|
||||
│ └────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Outputs: │
|
||||
│ • /api/v1/store/query — kNN search │
|
||||
│ • /api/v1/boundary — fragility score │
|
||||
│ • /api/v1/coherence/profile — phases │
|
||||
│ • /api/v1/cognitive/snapshot — graph │
|
||||
│ • /api/v1/custody/attestation — signed │
|
||||
│ • MCP proxy — 114 tools for AI agents │
|
||||
└────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Stage 1: ESP32 Feature Vector Extraction
|
||||
|
||||
The ESP32 edge processing pipeline (Core 1) already computes all signals needed. We add a compact 8-dimensional feature vector extracted from the existing DSP outputs:
|
||||
|
||||
| Dimension | Feature | Source | Range |
|
||||
|-----------|---------|--------|-------|
|
||||
| 0 | Presence score | `s_presence_score / 10.0` (clamped) | 0.0–1.0 |
|
||||
| 1 | Motion energy | `s_motion_energy / 10.0` (clamped) | 0.0–1.0 |
|
||||
| 2 | Breathing rate | `s_breathing_bpm / 30.0` (clamped) | 0.0–1.0 |
|
||||
| 3 | Heart rate | `s_heartrate_bpm / 120.0` (clamped) | 0.0–1.0 |
|
||||
| 4 | Phase variance (mean) | Top-K subcarrier Welford variance mean | 0.0–1.0 |
|
||||
| 5 | Person count | `n_active_persons / 4.0` (clamped) | 0.0–1.0 |
|
||||
| 6 | Fall detected | Binary: 1.0 if `s_fall_detected`, else 0.0 | 0.0 or 1.0 |
|
||||
| 7 | RSSI (normalized) | `(s_latest_rssi + 100) / 100` (clamped) | 0.0–1.0 |
|
||||
|
||||
This maps directly to the Seed's store dimension of 8, enabling kNN queries like "find the 10 most similar sensing states to the current one."
|
||||
|
||||
**Packet format** (magic `0xC5110003`, defined as `edge_feature_pkt_t` in `edge_processing.h`):
|
||||
|
||||
```c
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t magic; // EDGE_FEATURE_MAGIC = 0xC5110003
|
||||
uint8_t node_id; // ESP32 node identifier
|
||||
uint8_t reserved; // alignment padding
|
||||
uint16_t seq; // sequence number
|
||||
int64_t timestamp_us; // microseconds since boot
|
||||
float features[8]; // 8-dim normalized feature vector (32 bytes)
|
||||
} edge_feature_pkt_t; // Total: 48 bytes (static_assert enforced)
|
||||
```
|
||||
|
||||
**Transmission rate:** 1 Hz (one feature vector per second, aggregated from 100 Hz CSI). This keeps UDP bandwidth under 50 bytes/s per node and avoids overwhelming the Seed's vector store.
|
||||
|
||||
### Stage 2: Seed-Side RVF Ingest
|
||||
|
||||
A lightweight Rust service on the Seed (or a Python bridge script) listens for feature packets on UDP port 5006 and ingests them via the Seed's REST API:
|
||||
|
||||
```bash
|
||||
# Ingest a feature vector with metadata
|
||||
curl -sk -X POST https://169.254.42.1:8443/api/v1/store/ingest \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"vectors": [[0, [0.85, 0.3, 0.52, 0.65, 0.4, 0.78, 0.1, -0.45]]],
|
||||
"metadata": {
|
||||
"node_id": 1,
|
||||
"type": "csi_feature",
|
||||
"timestamp": 1775166970
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**Batching:** Accumulate 10 vectors (10 seconds) per ingest call to reduce HTTP overhead (`--batch-size 10` default in `seed_csi_bridge.py`; also supports time-based flushing via `--flush-interval`). At 1 vector/second per node, a 4-node mesh generates 14,400 vectors/hour (345,600/day). Daily compaction is required to stay within the Seed's 100K vector working set (see Storage Budget).
|
||||
|
||||
**Witness chain:** Each ingest automatically appends a witness entry, providing a tamper-evident record of all sensing data. The epoch increments monotonically, and the SHA-256 chain can be verified at any time via `POST /api/v1/witness/verify`.
|
||||
|
||||
### Stage 3: Cognitive Analysis & Sensor Fusion
|
||||
|
||||
Once CSI feature vectors are in the RVF store, the Seed's existing subsystems activate:
|
||||
|
||||
1. **kNN Graph** — Rebuilt every 10 seconds. Similar sensing states cluster together. Anomalous states (intruder, fall, unusual breathing) appear as outliers.
|
||||
|
||||
2. **Boundary Analysis** — Stoer-Wagner min-cut computes a fragility score (0.0–1.0). High fragility indicates the vector space is splitting — a regime change in the environment (door opened, person entered/left, HVAC state change).
|
||||
|
||||
3. **Temporal Coherence** — Phase boundary detection across the vector store timeline identifies when the environment transitions between states (occupied → empty, day → night, normal → abnormal).
|
||||
|
||||
4. **Reflex Rules** — Three pre-configured rules fire automatically:
|
||||
- `fragility_alarm` (threshold 0.3) → relay actuator for presence alert
|
||||
- `drift_cutoff` (threshold 1.0) → cutoff when sensor drift detected
|
||||
- `hd_anomaly_indicator` (threshold 200) → PWM brightness for anomaly severity
|
||||
|
||||
5. **Sensor Fusion** — The Seed's BME280 (temperature/humidity/pressure) and PIR sensor provide environmental ground truth that correlates with CSI features:
|
||||
- PIR motion validates CSI presence detection
|
||||
- Temperature changes correlate with occupancy
|
||||
- Humidity changes correlate with breathing detection fidelity
|
||||
|
||||
6. **MCP Integration** — AI assistants can query the full pipeline via the 114-tool MCP proxy:
|
||||
```json
|
||||
{"method": "tools/call", "params": {"name": "seed.memory.query", "arguments": {"vector": [0.8, 0.5, 0.4, 0.6, 0.3, 0.7, 0.1, -0.3], "k": 5}}}
|
||||
```
|
||||
|
||||
### ESP32 Provisioning
|
||||
|
||||
The ESP32's existing NVS provisioning system supports configuring the Seed as the target:
|
||||
|
||||
```bash
|
||||
python firmware/esp32-csi-node/provision.py \
|
||||
--port COM9 \
|
||||
--target-ip 192.168.1.20 \
|
||||
--target-port 5006 \
|
||||
--node-id 1
|
||||
```
|
||||
|
||||
Note: `--target-ip` is the host laptop (192.168.1.20), not the Seed IP, because the bridge runs on the host and forwards to the Seed via HTTPS (see Known Issue 4).
|
||||
|
||||
No firmware recompilation needed — the `stream_sender` module reads target IP/port from NVS at boot.
|
||||
|
||||
### Data Flow Rates
|
||||
|
||||
| Path | Rate | Size | Bandwidth |
|
||||
|------|------|------|-----------|
|
||||
| CSI capture → ring buffer | 100 Hz | ~400 B | 40 KB/s (internal) |
|
||||
| Edge DSP → sensing-server | 100 Hz | ~200 B | 20 KB/s (existing) |
|
||||
| Edge DSP → Seed features | 1 Hz | 48 B | 48 B/s (new) |
|
||||
| Seed ingest (batched) | 0.1 Hz | ~500 B | 50 B/s (HTTP) |
|
||||
| Seed kNN graph rebuild | 0.1 Hz | internal | — |
|
||||
| Seed witness chain | per batch | 32 B hash | — |
|
||||
|
||||
### Storage Budget
|
||||
|
||||
| Timeframe | Vectors/node | 4 nodes | RVF size | RAM |
|
||||
|-----------|-------------|---------|----------|-----|
|
||||
| 1 hour | 3,600 | 14,400 | ~580 KB | ~6 MB |
|
||||
| 24 hours | 86,400 | 345,600 | ~14 MB | ~140 MB |
|
||||
| 7 days | 604,800 | 2,419,200 | ~97 MB | exceeds |
|
||||
|
||||
**Compaction policy:** Run `POST /api/v1/store/compact` daily at 03:00, retaining only the last 24 hours of vectors. Archive older vectors to USB drive via `POST /api/v1/store/export` before compaction.
|
||||
|
||||
**Dimension reduction:** For deployments exceeding 100K vectors, reduce feature extraction rate to 0.1 Hz (one vector per 10 seconds) or increase compaction frequency.
|
||||
|
||||
## Validation Results
|
||||
|
||||
**Live hardware test performed 2026-04-02.**
|
||||
|
||||
### Hardware Under Test
|
||||
|
||||
| Component | Port | IP | Firmware | WiFi | RSSI |
|
||||
|-----------|------|----|----------|------|------|
|
||||
| ESP32-S3 (8MB) | COM9 | 192.168.1.105 | v0.5.2 | ruv.net (ch 5) | -34 dBm |
|
||||
| Cognitum Seed | USB | 169.254.42.1 / 192.168.1.109 | v0.8.1 | ruv.net | — |
|
||||
| Host laptop | — | 192.168.1.20 | — | ruv.net | — |
|
||||
|
||||
Seed device_id: `ecaf97dd-fc90-4b0e-b0e7-e9f896b9fbb6`. Pairing token issued to `wifi-densepose-claude`.
|
||||
|
||||
### Pipeline Validated
|
||||
|
||||
1. **UDP streaming** -- 211 packets captured in 15 seconds:
|
||||
- 196 raw CSI frames (magic `0xC5110001`)
|
||||
- 15 vitals frames (magic `0xC5110002`)
|
||||
|
||||
2. **Bridge pipeline** -- 20 vitals packets (`0xC5110002`) parsed, converted to 8-dim feature vectors via the bridge's `parse_vitals_packet()` fallback path, ingested in 4 batches of 5 vectors each (`--batch-size 5`). The native `0xC5110003` feature packet path is implemented in firmware but was not exercised in this validation run (firmware was v0.5.2; the `send_feature_vector()` addition requires a reflash).
|
||||
|
||||
3. **RVF ingest** -- All 20 vectors accepted by Seed. Epochs advanced 88 to 91. Witness chain verified valid (193 entries, SHA-256 chain intact).
|
||||
|
||||
4. **Seed sensors** -- BME280, PIR, reed switch, ADS1115, vibration sensor all present and healthy.
|
||||
|
||||
### Live Vital Signs Captured
|
||||
|
||||
| Metric | Observed Range | Expected | Notes |
|
||||
|--------|---------------|----------|-------|
|
||||
| Presence score | 1.41 -- 14.92 | 0.0 -- 1.0 | **Needs normalization** (see Known Issues) |
|
||||
| Motion energy | 1.41 -- 14.92 | 0.0 -- 1.0 | Same raw value as presence score |
|
||||
| Breathing rate | 19.8 -- 33.5 BPM | 12 -- 25 BPM | Plausible but slightly high |
|
||||
| Heart rate | 75.3 -- 99.1 BPM | 60 -- 100 BPM | Plausible range |
|
||||
| RSSI | -43 to -72 dBm | -30 to -80 dBm | Normal |
|
||||
| Fall detected | No | — | Correct (no falls occurred) |
|
||||
| n_persons | 4 | 1 | **Miscalibrated** (see Known Issues) |
|
||||
|
||||
### Known Issues Found
|
||||
|
||||
1. **`presence_score` exceeds 1.0 in vitals packets** -- Raw values range 1.41 to 14.92 in the vitals packet (`0xC5110002`). The bridge's vitals-to-feature conversion clamps to 1.0 for dim 0 and divides by 10.0 for dim 1 (`motion_energy / 10.0`), but dim 0 clamps without scaling. **Note:** The firmware's native feature vector (`0xC5110003`) already normalizes correctly by dividing `s_presence_score` by 10.0 (see `edge_processing.c` line 657). This issue only affects the vitals-packet fallback path in the bridge.
|
||||
|
||||
2. **`n_persons = 4` with 1 person present** -- The multi-person counting algorithm is miscalibrated for single-occupancy scenarios. The per-node state pipeline (ADR-068) may mitigate this when the baseline is properly trained, but the raw edge count is unreliable.
|
||||
|
||||
3. **Content-addressed vector IDs cause deduplication** -- Similar feature vectors hash to the same ID, causing the Seed to silently drop duplicates. **Fixed in bridge:** `seed_csi_bridge.py` now uses `_make_vector_id()` which generates a SHA-256 hash of `node_id:timestamp_us:seq_counter`, producing unique 32-bit IDs. This was observed during validation and fixed before the final test run.
|
||||
|
||||
4. **Bridge runs on host, not Seed** -- The ESP32 target IP must be the host laptop (192.168.1.20), not the Seed IP. The bridge script on the host forwards to the Seed via HTTPS. This adds a hop but avoids running a UDP listener on the Pi Zero 2 W.
|
||||
|
||||
5. **PIR GPIO read returned 404** -- `GET /api/v1/sensor/gpio/read?pin=6` returned 404. The PIR endpoint may require a different pin number or endpoint format. Ground-truth validation against PIR is deferred to Phase 3.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: ESP32 Feature Extraction (firmware change) -- DONE
|
||||
|
||||
Implemented as `send_feature_vector()` in `edge_processing.c` (lines 644-699) and `edge_feature_pkt_t` in `edge_processing.h` (lines 112-124). The function reads from static globals (`s_presence_score`, `s_motion_energy`, `s_breathing_bpm`, `s_heartrate_bpm`, subcarrier Welford variance, person tracker, fall flag, RSSI) and normalizes each dimension to 0.0-1.0 with clamping.
|
||||
|
||||
Called at the same 1 Hz cadence as `send_vitals_packet()` in Step 13 of the edge processing pipeline (line 855). The compressed frame magic was reassigned from `0xC5110003` to `0xC5110005` to free up `0xC5110003` for feature vectors (`EDGE_COMPRESSED_MAGIC` in `edge_processing.h` line 29).
|
||||
|
||||
### Phase 2: Seed Ingest Bridge (Python script on host) -- DONE
|
||||
|
||||
Implemented as `scripts/seed_csi_bridge.py`. The bridge:
|
||||
1. Listens on UDP port 5006 (configurable via `--udp-port`)
|
||||
2. Accepts all three packet formats: `0xC5110003` (ADR-069 features), `0xC5110002` (vitals, converted to 8-dim), and `0xC5110001` (raw CSI, minimal features)
|
||||
3. Generates unique vector IDs via SHA-256 hash of `node_id:timestamp:seq` (avoids content-addressed deduplication -- see Known Issue 3)
|
||||
4. Batches vectors (default 10, configurable via `--batch-size`) with time-based flush fallback (`--flush-interval`)
|
||||
5. POSTs to Seed's `/api/v1/store/ingest` with bearer token
|
||||
6. Supports `--validate` mode (kNN query + PIR comparison after each batch)
|
||||
7. Supports `--stats` mode (print Seed status, boundary, coherence, graph)
|
||||
8. Supports `--compact` mode (trigger store compaction)
|
||||
|
||||
### Phase 3: Validation & Ground Truth -- BLOCKED
|
||||
|
||||
Use the Seed's PIR sensor as ground truth for presence detection:
|
||||
1. Query PIR state: `GET /api/v1/sensor/gpio/read?pin=6`
|
||||
2. Compare with CSI presence score (feature dim 0)
|
||||
3. Log agreement/disagreement rate
|
||||
4. Use kNN to find historical vectors matching current PIR state → validate CSI accuracy
|
||||
|
||||
**Status:** The bridge implements `--validate` mode with PIR comparison (see `_run_validation()` in `seed_csi_bridge.py`). However, the PIR endpoint returned 404 during validation (Known Issue 5). This phase is blocked until the correct PIR API endpoint is identified.
|
||||
|
||||
### Phase 4: Multi-Node Mesh (addresses #348)
|
||||
|
||||
Deploy 3 ESP32 nodes, each sending feature vectors to the bridge host (which forwards to the Seed):
|
||||
- Node 1 (lobby): `--node-id 1 --target-ip 192.168.1.20 --target-port 5006`
|
||||
- Node 2 (hallway): `--node-id 2 --target-ip 192.168.1.20 --target-port 5006`
|
||||
- Node 3 (room): `--node-id 3 --target-ip 192.168.1.20 --target-port 5006`
|
||||
|
||||
All nodes target the host laptop (192.168.1.20) where the bridge script runs. The bridge batches and forwards all nodes' vectors to the Seed via HTTPS. The Seed's kNN graph naturally clusters vectors by node and by sensing state. Cross-node analysis via boundary fragility detects when a person moves between zones.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Bearer token** — All write operations require the pairing token. Token stored as SHA-256 hash on device.
|
||||
2. **TLS** — All API calls over HTTPS (port 8443) with device-provisioned CA certificate.
|
||||
3. **Witness chain** — Every ingest is cryptographically chained. Tampering detection via `POST /api/v1/witness/verify`.
|
||||
4. **Ed25519 attestation** — Device identity bound to hardware keypair. Attestation includes epoch, vector count, and witness head.
|
||||
5. **Anti-spoofing** — Sensor pipeline has entropy-based spoofing detection (min 0.5 bits entropy, streak threshold 3).
|
||||
6. **USB-only pairing** — Pairing window can only be opened from USB interface (169.254.42.1), not from WiFi.
|
||||
|
||||
## Hardware Bill of Materials
|
||||
|
||||
| Component | Port | IP | Cost |
|
||||
|-----------|------|----|------|
|
||||
| ESP32-S3 (8MB) | COM9 | 192.168.1.105 (DHCP) | ~$9 |
|
||||
| Cognitum Seed (Pi Zero 2W) | USB | 169.254.42.1 / 192.168.1.109 | ~$15 |
|
||||
| USB-C cable (data) | — | — | ~$3 |
|
||||
| **Total** | | | **~$27** |
|
||||
|
||||
### Seed Sensors (included)
|
||||
|
||||
| Sensor | Interface | Channels | Purpose |
|
||||
|--------|-----------|----------|---------|
|
||||
| Reed switch | GPIO 5 | 1 | Door/window state |
|
||||
| PIR motion | GPIO 6 | 1 | Motion ground truth |
|
||||
| Vibration | GPIO 13 | 1 | Structural vibration |
|
||||
| ADS1115 | I2C 0x48 | 4 | Analog inputs (extensible) |
|
||||
| BME280 | I2C 0x76 | 3 | Temperature, humidity, pressure |
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|-----------|--------|------------|
|
||||
| Pi Zero thermal throttling at sustained ingest | Medium | Performance degrades | Thermal governor already manages DVFS; 1 Hz ingest is minimal load |
|
||||
| WiFi congestion with ESP32 CSI + UDP | Low | Lost packets | Feature vectors are 48 bytes at 1 Hz; negligible vs CSI traffic |
|
||||
| RVF store exceeds RAM at high vector count | Medium | OOM | Compaction policy + dimension reduction + daily export |
|
||||
| Bearer token exposure | Low | Unauthorized writes | TLS encryption + USB-only pairing + token hashing |
|
||||
| ESP32 NVS corruption | Low | Config lost | NVS is wear-leveled flash with CRC; re-provision via USB |
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- ESP32 CSI features become persistent, searchable, and cryptographically attested
|
||||
- kNN similarity search enables environment fingerprinting and anomaly detection
|
||||
- PIR + BME280 provide ground truth for CSI validation
|
||||
- MCP proxy enables AI assistants to query sensing state directly
|
||||
- Witness chain provides audit trail for healthcare/safety applications
|
||||
- Architecture aligns with Arena Physica's insight: store embeddings, not raw signals
|
||||
|
||||
### Negative
|
||||
- Additional firmware packet type (48 bytes, trivial)
|
||||
- Bridge script needed on Seed or host machine
|
||||
- Daily compaction required for long-running deployments
|
||||
- Bearer token must be managed (stored securely, rotated if compromised)
|
||||
|
||||
### Neutral
|
||||
- Existing sensing-server pipeline unchanged (ESP32 still sends to port 5005)
|
||||
- Seed's existing sensors continue operating independently
|
||||
- Target IP/port configurable via NVS provisioning (no recompilation for deployment changes)
|
||||
- Firmware recompilation needed once to add `send_feature_vector()` (Phase 1), but subsequent node deployments only need provisioning
|
||||
@@ -1,203 +0,0 @@
|
||||
# ADR-070: Self-Supervised Pretraining from Live ESP32 CSI + Cognitum Seed
|
||||
|
||||
| Field | Value |
|
||||
|------------|----------------------------------------------------------|
|
||||
| Status | Accepted |
|
||||
| Date | 2026-04-02 |
|
||||
| Authors | rUv, claude-flow |
|
||||
| Drivers | README limitation "No pre-trained model weights provided"|
|
||||
| Related | ADR-069 (Cognitum Seed pipeline), ADR-027 (MERIDIAN), ADR-024 (AETHER contrastive), ADR-015 (MM-Fi dataset) |
|
||||
|
||||
## Context
|
||||
|
||||
The README lists "No pre-trained model weights are provided; training from scratch is required" as a known limitation. Users must collect their own CSI dataset and train from scratch, which is a significant barrier to adoption.
|
||||
|
||||
We now have the infrastructure to generate pre-trained weights directly from live hardware:
|
||||
|
||||
- **2 ESP32-S3 nodes** (COM8 node_id=2 at 192.168.1.104, COM9 node_id=1 at 192.168.1.105) streaming CSI + vitals + 8-dim feature vectors at 1 Hz each
|
||||
- **Cognitum Seed** (Pi Zero 2 W) with RVF vector store, kNN search, witness chain, and environmental sensors (BME280, PIR, vibration)
|
||||
- **Recording API** in sensing-server (`POST /api/v1/recording/start`) that saves CSI frames to `.csi.jsonl`
|
||||
- **Self-supervised training** via `rapid_adapt.rs` (contrastive TTT + entropy minimization)
|
||||
- **AETHER contrastive embeddings** (ADR-024) for environment-independent representations
|
||||
|
||||
### Why Self-Supervised?
|
||||
|
||||
No cameras or labels are needed. The system learns from:
|
||||
|
||||
1. **Temporal coherence** — Frames close in time should have similar embeddings (positive pairs), frames far apart should differ (negative pairs)
|
||||
2. **Multi-node consistency** — The same person seen from 2 nodes should produce correlated features, different people should produce decorrelated features
|
||||
3. **Cognitum Seed ground truth** — PIR sensor, BME280 environment changes, and kNN cluster transitions provide weak supervision without human labeling
|
||||
4. **Physical constraints** — Breathing 6-30 BPM, heart rate 40-150 BPM, person count 0-4, RSSI physics
|
||||
|
||||
## Decision
|
||||
|
||||
Implement a 4-phase pretraining pipeline that collects CSI from 2 ESP32 nodes, stores feature vectors in the Cognitum Seed, and produces distributable pre-trained weights.
|
||||
|
||||
### Phase 1: Data Collection (30 min)
|
||||
|
||||
Capture labeled scenarios using the sensing-server recording API and Cognitum Seed:
|
||||
|
||||
| Scenario | Duration | Label | Activity |
|
||||
|----------|----------|-------|----------|
|
||||
| Empty room | 5 min | `empty` | No one present, establish baseline |
|
||||
| 1 person stationary | 5 min | `1p-still` | Sit at desk, normal breathing |
|
||||
| 1 person walking | 5 min | `1p-walk` | Walk around room, varied paths |
|
||||
| 1 person varied | 5 min | `1p-varied` | Stand, sit, wave arms, turn |
|
||||
| 2 people | 5 min | `2p` | Both moving in room |
|
||||
| Transitions | 5 min | `transitions` | Enter/exit room, appear/disappear |
|
||||
|
||||
**Data rate per scenario:**
|
||||
- 2 nodes × 100 Hz CSI = 200 frames/sec = 60,000 frames per 5 min
|
||||
- 2 nodes × 1 Hz features = 2 vectors/sec = 600 vectors per 5 min
|
||||
- Total: 360,000 CSI frames + 3,600 feature vectors per collection run
|
||||
|
||||
**Cognitum Seed role:**
|
||||
- Stores all feature vectors with witness chain attestation
|
||||
- PIR sensor provides binary presence ground truth
|
||||
- BME280 tracks environmental conditions during collection
|
||||
- kNN graph clusters naturally emerge from the vector distribution
|
||||
|
||||
### Phase 2: Contrastive Pretraining
|
||||
|
||||
Train a contrastive encoder on the collected CSI data:
|
||||
|
||||
```
|
||||
Input: Raw CSI frame (128 subcarriers × 2 I/Q = 256 features)
|
||||
↓
|
||||
TCN temporal encoder (3 layers, kernel=7)
|
||||
↓
|
||||
Projection head → 128-dim embedding
|
||||
↓
|
||||
Contrastive loss (InfoNCE):
|
||||
positive: frames within 0.5s window from same node
|
||||
negative: frames >5s apart or from different scenario
|
||||
cross-node positive: same timestamp, different node
|
||||
```
|
||||
|
||||
**Self-supervised signals:**
|
||||
- Temporal adjacency (frames within 500ms = positive pair)
|
||||
- Cross-node agreement (same person seen from 2 viewpoints)
|
||||
- PIR consistency (embedding should cluster by PIR state)
|
||||
- Scenario boundary (embeddings should shift at label transitions)
|
||||
|
||||
### Phase 3: Downstream Head Training
|
||||
|
||||
Attach lightweight heads for each task:
|
||||
|
||||
| Head | Architecture | Output | Supervision |
|
||||
|------|-------------|--------|-------------|
|
||||
| Presence | Linear(128→1) + sigmoid | 0.0-1.0 | PIR sensor (free) |
|
||||
| Person count | Linear(128→4) + softmax | 0-3 people | Scenario labels |
|
||||
| Activity | Linear(128→4) + softmax | still/walk/varied/empty | Scenario labels |
|
||||
| Vital signs | Linear(128→2) | BR, HR (BPM) | ESP32 edge vitals |
|
||||
|
||||
### Phase 4: Package & Distribute
|
||||
|
||||
Produce distributable artifacts:
|
||||
|
||||
| Artifact | Format | Size | Description |
|
||||
|----------|--------|------|-------------|
|
||||
| `pretrained-encoder.onnx` | ONNX | ~2 MB | Contrastive encoder (TCN backbone) |
|
||||
| `pretrained-heads.onnx` | ONNX | ~100 KB | Task-specific heads |
|
||||
| `pretrained.rvf` | RVF | ~500 KB | RuVector format with metadata |
|
||||
| `room-profiles.json` | JSON | ~10 KB | Environment calibration profiles |
|
||||
| `collection-witness.json` | JSON | ~5 KB | Seed witness chain attestation proving data provenance |
|
||||
|
||||
Include in GitHub release alongside firmware binaries. Users download and run:
|
||||
|
||||
```bash
|
||||
# Use pre-trained model (no training needed)
|
||||
cargo run -p wifi-densepose-sensing-server -- --model pretrained.rvf --http-port 3000
|
||||
```
|
||||
|
||||
## Hardware Setup
|
||||
|
||||
```
|
||||
192.168.1.20 (Host laptop)
|
||||
┌──────────────────────────┐
|
||||
│ sensing-server │
|
||||
│ Recording API │
|
||||
│ Training pipeline │
|
||||
│ │
|
||||
│ seed_csi_bridge.py │
|
||||
│ Feature → Seed ingest │
|
||||
└────┬──────────┬───────────┘
|
||||
│ │
|
||||
UDP:5006 │ │ HTTPS:8443
|
||||
┌───────────────────┤ ├───────────────┐
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ │
|
||||
┌──────────┐ ┌──────────┐ ┌──────────────┐ │
|
||||
│ ESP32 #1 │ │ ESP32 #2 │ │Cognitum Seed │◄───┘
|
||||
│ COM9 │ │ COM8 │ │ Pi Zero 2W │
|
||||
│ node=1 │ │ node=2 │ │ USB │
|
||||
│ .1.105 │ │ .1.104 │ │ .42.1/8443 │
|
||||
│ v0.5.4 │ │ v0.5.4 │ │ v0.8.1 │
|
||||
└──────────┘ └──────────┘ │ PIR, BME280 │
|
||||
│ RVF store │
|
||||
│ Witness chain│
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
## Data Collection Protocol
|
||||
|
||||
### Step 1: Start Seed ingest (background)
|
||||
|
||||
```bash
|
||||
export SEED_TOKEN="your-token"
|
||||
python scripts/seed_csi_bridge.py \
|
||||
--seed-url https://169.254.42.1:8443 --token "$SEED_TOKEN" \
|
||||
--udp-port 5006 --batch-size 10 --validate &
|
||||
```
|
||||
|
||||
### Step 2: Start sensing-server with recording
|
||||
|
||||
```bash
|
||||
cargo run -p wifi-densepose-sensing-server -- \
|
||||
--source esp32 --udp-port 5006 --http-port 3000
|
||||
```
|
||||
|
||||
### Step 3: Record each scenario
|
||||
|
||||
```bash
|
||||
# Empty room (leave room for 5 min)
|
||||
curl -X POST http://localhost:3000/api/v1/recording/start \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"session_name":"pretrain-empty","label":"empty","duration_secs":300}'
|
||||
|
||||
# 1 person stationary (sit at desk for 5 min)
|
||||
curl -X POST http://localhost:3000/api/v1/recording/start \
|
||||
-d '{"session_name":"pretrain-1p-still","label":"1p-still","duration_secs":300}'
|
||||
|
||||
# ... repeat for each scenario
|
||||
```
|
||||
|
||||
### Step 4: Verify with Seed
|
||||
|
||||
```bash
|
||||
python scripts/seed_csi_bridge.py --token "$SEED_TOKEN" --stats
|
||||
# Should show 3,600+ vectors from the collection run
|
||||
```
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|-----------|--------|------------|
|
||||
| 2 nodes insufficient for spatial diversity | Medium | Lower pretraining quality | Place nodes 3-5m apart at different heights |
|
||||
| PIR sensor has limited range | Low | Weak presence labels | BME280 temp changes + kNN clusters as backup |
|
||||
| Contrastive pretraining collapses | Low | Useless embeddings | Temperature scheduling, hard negative mining |
|
||||
| Model too large for ESP32 inference | N/A | N/A | Inference on host/Seed, not on ESP32 |
|
||||
| Room-specific overfitting | Medium | Poor generalization | MERIDIAN domain randomization (ADR-027), LoRA adaptation |
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Users get working model out of the box — no training needed
|
||||
- Witness chain proves data provenance (when/where/which hardware)
|
||||
- Pre-trained encoder transfers to new environments via LoRA fine-tuning
|
||||
- Removes the #1 adoption barrier from the README
|
||||
|
||||
### Negative
|
||||
- 30 min of manual data collection per pretraining run
|
||||
- Pre-trained weights are room-specific without adaptation
|
||||
- ONNX runtime dependency for inference
|
||||
@@ -1,408 +0,0 @@
|
||||
# ADR-071: ruvllm Training Pipeline for CSI Sensing Models
|
||||
|
||||
- **Status**: Proposed
|
||||
- **Date**: 2026-04-02
|
||||
- **Deciders**: ruv
|
||||
- **Relates to**: ADR-069 (Cognitum Seed CSI Pipeline), ADR-070 (Self-Supervised Pretraining), ADR-024 (Contrastive CSI Embedding / AETHER), ADR-016 (RuVector Training Pipeline)
|
||||
|
||||
## Context
|
||||
|
||||
The WiFi-DensePose project needs a training pipeline to convert collected CSI data
|
||||
(`.csi.jsonl` frames from ESP32 nodes) into deployable models for presence detection,
|
||||
activity classification, and vital sign estimation.
|
||||
|
||||
Previous ADRs established the data collection protocol (ADR-070) and Cognitum Seed
|
||||
inference target (ADR-069). What was missing was the actual training, refinement,
|
||||
quantization, and export pipeline connecting raw CSI recordings to deployable models.
|
||||
|
||||
### Why ruvllm instead of PyTorch
|
||||
|
||||
| Criterion | ruvllm | PyTorch | ONNX Runtime |
|
||||
|-----------|--------|---------|--------------|
|
||||
| Runtime dependency | Node.js only | Python + CUDA + pip | C++ runtime |
|
||||
| Install size | ~5 MB (npm) | ~2 GB (torch+cuda) | ~50 MB |
|
||||
| SONA adaptation | <1ms native | N/A | N/A |
|
||||
| Quantization | 2/4/8-bit TurboQuant | INT8/FP16 (separate tool) | INT8 only |
|
||||
| LoRA fine-tuning | Built-in LoraAdapter | Requires PEFT library | N/A |
|
||||
| EWC protection | Built-in EwcManager | Manual implementation | N/A |
|
||||
| SafeTensors export | Native SafeTensorsWriter | Via safetensors library | N/A |
|
||||
| Contrastive training | Built-in ContrastiveTrainer | Manual triplet loss | N/A |
|
||||
| Edge deployment | ESP32, Pi Zero, browser | GPU servers only | ARM (limited) |
|
||||
| M4 Pro performance | 88-135 tok/s native | ~30 tok/s (MPS) | ~50 tok/s |
|
||||
| Ecosystem integration | RuVector, Cognitum Seed | Standalone | Standalone |
|
||||
|
||||
The ruvllm package (`@ruvector/ruvllm` v2.5.4) provides the complete training
|
||||
lifecycle in a single dependency: contrastive pretraining, task head training,
|
||||
LoRA refinement, EWC consolidation, quantization, and SafeTensors/RVF export.
|
||||
No Python dependency means the entire pipeline runs on the same Node.js runtime
|
||||
as the Cognitum Seed inference engine.
|
||||
|
||||
## Decision
|
||||
|
||||
Use ruvllm's `ContrastiveTrainer`, `TrainingPipeline`, `LoraAdapter`, `EwcManager`,
|
||||
`SafeTensorsWriter`, and `ModelExporter` for the complete CSI model training lifecycle.
|
||||
|
||||
### Training Phases
|
||||
|
||||
The pipeline executes five sequential phases:
|
||||
|
||||
#### Phase 1: Contrastive Pretraining
|
||||
|
||||
Learns an embedding space where temporally and spatially similar CSI states are close
|
||||
and dissimilar states are far apart.
|
||||
|
||||
- **Encoder architecture**: 8-dim CSI feature vector -> 64-dim hidden (ReLU) -> 128-dim embedding (L2-normalized)
|
||||
- **Loss functions**: Triplet loss (margin=0.3) + InfoNCE (temperature=0.07)
|
||||
- **Triplet strategies**:
|
||||
- Temporal positive: frames within 1 second (same environment state)
|
||||
- Temporal negative: frames >30 seconds apart (different state)
|
||||
- Cross-node positive: same timestamp from different ESP32 nodes (same person, different viewpoint)
|
||||
- Cross-node negative: different timestamp + different node
|
||||
- Hard negatives: frames near motion energy transition boundaries
|
||||
- **Hyperparameters**: 20 epochs, batch size 32, hard negative ratio 0.7
|
||||
- **Implementation**: `ContrastiveTrainer.addTriplet()` + `.train()`
|
||||
|
||||
#### Phase 2: Task Head Training
|
||||
|
||||
Trains supervised heads on top of the frozen embedding for specific sensing tasks.
|
||||
|
||||
- **Presence head**: 128 -> 1 (sigmoid), threshold at presence_score > 0.3
|
||||
- **Activity head**: 128 -> 3 (softmax: still/moving/empty), derived from motion_energy thresholds
|
||||
- **Vitals head**: 128 -> 2 (linear: breathing BPM, heart rate BPM), normalized targets
|
||||
- **Implementation**: `TrainingPipeline.addData()` + `.train()` with cosine LR scheduler,
|
||||
early stopping (patience=5), and quality-weighted MSE loss
|
||||
|
||||
#### Phase 3: LoRA Refinement
|
||||
|
||||
Per-node LoRA adapters for room-specific adaptation without forgetting the base model.
|
||||
|
||||
- **Configuration**: rank=4, alpha=8, dropout=0.1
|
||||
- **Per-node training**: Each ESP32 node gets its own LoRA adapter trained on
|
||||
node-specific data with reduced learning rate (0.5x base)
|
||||
- **Implementation**: `LoraManager.create()` for each node, `TrainingPipeline` with
|
||||
`LoraAdapter` passed to constructor
|
||||
|
||||
#### Phase 4: Quantization (TurboQuant)
|
||||
|
||||
Reduces model size for edge deployment with minimal quality loss.
|
||||
|
||||
| Bit Width | Compression | Typical RMSE | Target Device |
|
||||
|-----------|-------------|-------------|---------------|
|
||||
| 8-bit | 4x | <0.001 | Cognitum Seed (Pi Zero) |
|
||||
| 4-bit | 8x | <0.01 | Standard edge inference |
|
||||
| 2-bit | 16x | <0.05 | ESP32-S3 feature extraction |
|
||||
|
||||
- **Method**: Uniform affine quantization with scale/zero-point per tensor
|
||||
- **Quality validation**: RMSE between original fp32 and dequantized weights
|
||||
|
||||
#### Phase 5: EWC Consolidation
|
||||
|
||||
Elastic Weight Consolidation prevents catastrophic forgetting when the model
|
||||
is later fine-tuned on new room data or updated CSI conditions.
|
||||
|
||||
- **Fisher information**: Computed from training data gradients
|
||||
- **Lambda**: 2000 (base), 3000 (per-node)
|
||||
- **Tasks registered**: Base pretraining + one per ESP32 node
|
||||
- **Implementation**: `EwcManager.registerTask()` for each training phase
|
||||
|
||||
### Data Pipeline
|
||||
|
||||
```
|
||||
.csi.jsonl files
|
||||
|
|
||||
v
|
||||
Parse frames: feature (8-dim), vitals, raw CSI
|
||||
|
|
||||
v
|
||||
Generate contrastive triplets (temporal, cross-node, hard negatives)
|
||||
|
|
||||
v
|
||||
Encode through CsiEncoder (8 -> 64 -> 128)
|
||||
|
|
||||
v
|
||||
Phase 1: ContrastiveTrainer (triplet + InfoNCE loss)
|
||||
|
|
||||
v
|
||||
Phase 2: TrainingPipeline (presence + activity + vitals heads)
|
||||
|
|
||||
v
|
||||
Phase 3: LoRA per-node refinement
|
||||
|
|
||||
v
|
||||
Phase 4: TurboQuant (2/4/8-bit quantization)
|
||||
|
|
||||
v
|
||||
Phase 5: EWC consolidation
|
||||
|
|
||||
v
|
||||
Export: SafeTensors, JSON config, RVF manifest, per-node LoRA adapters
|
||||
```
|
||||
|
||||
### Export Formats
|
||||
|
||||
| Format | File | Consumer |
|
||||
|--------|------|----------|
|
||||
| SafeTensors | `model.safetensors` | HuggingFace ecosystem, general inference |
|
||||
| JSON config | `config.json` | Model loading metadata |
|
||||
| JSON model | `model.json` | Full model state for Node.js loading |
|
||||
| Quantized binaries | `quantized/model-q{2,4,8}.bin` | Edge deployment |
|
||||
| Per-node LoRA | `lora/node-{id}.json` | Room-specific adaptation |
|
||||
| RVF manifest | `model.rvf.jsonl` | Cognitum Seed ingest (ADR-069) |
|
||||
| Training metrics | `training-metrics.json` | Dashboards, CI validation |
|
||||
|
||||
### Hardware Targets
|
||||
|
||||
| Device | Role | Quantization | Expected Latency |
|
||||
|--------|------|-------------|-----------------|
|
||||
| Mac Mini M4 Pro | Training (primary) | fp32 | <5 min total |
|
||||
| Cognitum Seed Pi Zero | Inference | 4-bit / 8-bit | <10 ms per frame |
|
||||
| ESP32-S3 | Feature extraction only | 2-bit (encoder weights) | <5 ms per frame |
|
||||
| Browser (WASM) | Visualization | 4-bit | <20 ms per frame |
|
||||
|
||||
### Performance Targets
|
||||
|
||||
| Metric | Target | Measured |
|
||||
|--------|--------|----------|
|
||||
| Training time (5,783 frames, M4 Pro) | <5 min | TBD |
|
||||
| Inference latency (M4 Pro) | <1 ms | TBD |
|
||||
| Inference latency (Pi Zero) | <10 ms | TBD |
|
||||
| SONA adaptation | <1 ms | <0.05 ms (ruvllm spec) |
|
||||
| Presence detection accuracy | >85% | TBD |
|
||||
| 4-bit quality loss (RMSE) | <0.01 | TBD |
|
||||
| 2-bit quality loss (RMSE) | <0.05 | TBD |
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **Zero Python dependency**: The entire training and inference pipeline runs on
|
||||
Node.js, eliminating Python/CUDA/pip dependency management on training and
|
||||
deployment targets.
|
||||
- **Integrated lifecycle**: Contrastive pretraining, task heads, LoRA refinement,
|
||||
EWC consolidation, and quantization in a single script using one library.
|
||||
- **Edge-first**: 2-bit quantization enables running the encoder on ESP32-S3.
|
||||
4-bit quantization fits comfortably on Cognitum Seed Pi Zero.
|
||||
- **Continual learning**: EWC protection means the model can be updated with new
|
||||
room data without losing previously learned patterns.
|
||||
- **Per-node adaptation**: LoRA adapters allow room-specific fine-tuning with
|
||||
minimal storage overhead (rank-4 adapter ~2KB per node).
|
||||
- **HuggingFace compatibility**: SafeTensors export enables sharing models on the
|
||||
HuggingFace Hub and loading in other frameworks.
|
||||
- **Reproducibility**: Seeded encoder initialization and deterministic data pipeline
|
||||
ensure reproducible training runs.
|
||||
|
||||
### Negative
|
||||
|
||||
- **No GPU acceleration**: ruvllm's JS training loop does not use GPU compute.
|
||||
For the small model sizes in CSI sensing (8->64->128), this is acceptable
|
||||
(~seconds on M4 Pro), but would not scale to large vision models.
|
||||
- **Simplified backpropagation**: The LoRA backward pass and contrastive training
|
||||
use approximate gradient updates rather than full automatic differentiation.
|
||||
Sufficient for the target model sizes but not equivalent to PyTorch autograd.
|
||||
- **Quantization is post-training only**: No quantization-aware training (QAT).
|
||||
For 4-bit and 8-bit this produces acceptable quality loss; 2-bit may need
|
||||
QAT in future if quality degrades.
|
||||
|
||||
### Risks
|
||||
|
||||
- **Quality ceiling**: The simplified training may produce lower accuracy than a
|
||||
PyTorch-trained equivalent. Mitigated by: (a) the model is small enough that
|
||||
the training loop converges quickly, (b) SONA adaptation can compensate at
|
||||
inference time, (c) we can switch to PyTorch for training only if needed
|
||||
while keeping ruvllm for inference.
|
||||
- **ruvllm API stability**: The library is at v2.5.4 with active development.
|
||||
Mitigated by vendoring the package in `vendor/ruvector/npm/packages/ruvllm/`.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `scripts/train-ruvllm.js` | Full 5-phase training pipeline |
|
||||
| `scripts/benchmark-ruvllm.js` | Model benchmarking (latency, quality, accuracy) |
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# Train on collected CSI data
|
||||
node scripts/train-ruvllm.js \
|
||||
--data data/recordings/pretrain-1775182186.csi.jsonl \
|
||||
--output models/csi-v1 \
|
||||
--epochs 20
|
||||
|
||||
# Train with benchmark
|
||||
node scripts/train-ruvllm.js \
|
||||
--data data/recordings/pretrain-*.csi.jsonl \
|
||||
--output models/csi-v1 \
|
||||
--benchmark
|
||||
|
||||
# Standalone benchmark
|
||||
node scripts/benchmark-ruvllm.js \
|
||||
--model models/csi-v1 \
|
||||
--data data/recordings/pretrain-*.csi.jsonl \
|
||||
--samples 5000 \
|
||||
--json
|
||||
```
|
||||
|
||||
### Output Structure
|
||||
|
||||
```
|
||||
models/csi-v1/
|
||||
model.safetensors # SafeTensors (HuggingFace compatible)
|
||||
config.json # Model configuration
|
||||
model.json # Full JSON model state
|
||||
model.rvf.jsonl # RVF manifest for Cognitum Seed
|
||||
training-metrics.json # Training loss curves, timing, config
|
||||
contrastive/
|
||||
triplets.jsonl # Contrastive training pairs
|
||||
triplets.csv # CSV format for analysis
|
||||
embeddings.json # Embedding matrices
|
||||
quantized/
|
||||
model-q2.bin # 2-bit quantized (ESP32 edge)
|
||||
model-q4.bin # 4-bit quantized (Pi Zero default)
|
||||
model-q8.bin # 8-bit quantized (high quality)
|
||||
lora/
|
||||
node-1.json # LoRA adapter for ESP32 node 1
|
||||
node-2.json # LoRA adapter for ESP32 node 2
|
||||
```
|
||||
|
||||
## Camera-Free Supervision
|
||||
|
||||
### Motivation
|
||||
|
||||
Traditional WiFi-based pose estimation (WiFlow, Person-in-WiFi) requires camera-supervised
|
||||
training: a camera captures ground-truth poses during CSI collection, and the model learns
|
||||
to map CSI to those poses. This creates a deployment paradox — the camera is needed for
|
||||
training but the whole point of WiFi sensing is to avoid cameras.
|
||||
|
||||
The camera-free pipeline (`scripts/train-camera-free.js`) replaces camera supervision with
|
||||
10 sensor signals from the Cognitum Seed and 2 ESP32 nodes, generating weak labels through
|
||||
sensor fusion.
|
||||
|
||||
### 10 Supervision Signals (No Camera)
|
||||
|
||||
| # | Signal | Source | Provides |
|
||||
|---|--------|--------|----------|
|
||||
| 1 | PIR sensor | Seed GPIO 6 | Binary presence ground truth |
|
||||
| 2 | BME280 temperature | Seed I2C 0x76 | Occupancy proxy (temp rises with people) |
|
||||
| 3 | BME280 humidity | Seed I2C 0x76 | Breathing confirmation / zone |
|
||||
| 4 | Cross-node RSSI | 2 ESP32 nodes | Rough XY position (differential triangulation) |
|
||||
| 5 | Vitals stability | ESP32 CSI | HR/BR variance indicates activity level |
|
||||
| 6 | Temporal CSI patterns | ESP32 CSI | Periodic=walking, stable=sitting, flat=empty |
|
||||
| 7 | kNN cluster labels | Seed vector store | Natural groupings in embedding space |
|
||||
| 8 | Boundary fragility | Seed Stoer-Wagner | Regime change detection (entry/exit/activity) |
|
||||
| 9 | Reed switch | Seed GPIO 5 | Door open/close events |
|
||||
| 10 | Vibration sensor | Seed GPIO 13 | Footstep detection |
|
||||
|
||||
### Camera-Free Training Phases
|
||||
|
||||
The pipeline extends the base 5 phases with camera-free-specific phases:
|
||||
|
||||
```
|
||||
Phase 0: Multi-Modal Data Collection
|
||||
├── UDP port 5006 → ESP32 CSI features + vitals
|
||||
├── HTTPS → Seed sensor embeddings (45-dim, every 100ms)
|
||||
├── HTTPS → Seed boundary/coherence (every 10s)
|
||||
└── Build synchronized MultiModalFrame timeline
|
||||
|
||||
Phase 1: Weak Label Generation
|
||||
├── Presence: PIR || CSI_presence > 0.3 || temp_rising > 0.1°C/min
|
||||
├── Position: RSSI differential → 5×5 grid (25 zones)
|
||||
├── Activity: CSI variance + FFT periodicity → stationary/walking/gesture/empty
|
||||
├── Occupancy: max(node1_persons, node2_persons) validated by temp
|
||||
├── Body region: upper/lower subcarrier groups → which body part moves
|
||||
├── Entry/exit: reed_switch + PIR transition + boundary fragility spike
|
||||
├── Breathing zone: humidity change rate → person location
|
||||
└── Pose proxy: 5-keypoint coarse pose from RSSI + subcarrier asymmetry + vibration
|
||||
|
||||
Phase 2: Enhanced Contrastive Pretraining
|
||||
├── Base triplets (temporal, cross-node, transition, scenario boundary)
|
||||
├── Sensor-verified negatives: PIR=0 vs PIR=1 must differ
|
||||
├── Activity boundary: before/after fragility spike must differ
|
||||
└── Cross-modal: CSI embedding ≈ Seed embedding for same state
|
||||
|
||||
Phase 3: Pose Proxy Training (5-keypoint)
|
||||
├── Head: RSSI centroid between 2 nodes
|
||||
├── Hands: per-subcarrier variance asymmetry (left/right from 2 nodes)
|
||||
├── Feet: vibration sensor + RSSI ground reflection
|
||||
└── Skeleton physics constraints (anthropometric bone length limits)
|
||||
|
||||
Phase 4: 17-Keypoint Interpolation
|
||||
├── Shoulders = 0.3 × head + 0.7 × hands
|
||||
├── Elbows = midpoint(shoulder, hand)
|
||||
├── Hips = midpoint(head, feet)
|
||||
├── Knees = midpoint(hip, foot)
|
||||
├── Face = derived from head position
|
||||
└── Iterative bone length constraint projection (3 iterations)
|
||||
|
||||
Phase 5: Self-Refinement Loop (3 rounds)
|
||||
├── Run inference on all collected data
|
||||
├── Keep predictions where temporal consistency confidence > 0.8
|
||||
├── Use as pseudo-labels for next training round
|
||||
└── Decaying learning rate per round (diminishing returns)
|
||||
```
|
||||
|
||||
### Seed API Endpoints Used
|
||||
|
||||
| Endpoint | Data | Collection Rate |
|
||||
|----------|------|----------------|
|
||||
| `GET /api/v1/sensor/stream` | SSE sensor readings | Continuous (100ms) |
|
||||
| `GET /api/v1/sensor/embedding/latest` | 45-dim sensor embedding | Per-frame |
|
||||
| `GET /api/v1/boundary` | Fragility score | Every 10s |
|
||||
| `GET /api/v1/coherence/profile` | Temporal phase boundaries | Every 10s |
|
||||
| `GET /api/v1/store/query` | kNN similarity search | On demand |
|
||||
| `POST /api/v1/boundary/recompute` | Trigger analysis | On regime change |
|
||||
|
||||
### Graceful Degradation
|
||||
|
||||
The pipeline works with or without the Cognitum Seed:
|
||||
|
||||
| Mode | Signals | Pose Quality |
|
||||
|------|---------|-------------|
|
||||
| Full (Seed + 2 ESP32) | 10 signals | 5-keypoint trained, 17-keypoint interpolated |
|
||||
| CSI-only (2 ESP32) | 3 signals (RSSI, vitals, temporal) | Coarser position/activity only |
|
||||
| Single node | 2 signals (vitals, temporal) | Presence + activity only |
|
||||
|
||||
When the Seed API is unreachable, the pipeline automatically falls back to
|
||||
CSI-only training, producing the same output format (SafeTensors, HuggingFace,
|
||||
quantized) with reduced label quality.
|
||||
|
||||
### Output Format
|
||||
|
||||
Same as the base pipeline (SafeTensors + HuggingFace compatible), plus:
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `pose-decoder.json` | 5-keypoint pose decoder weights |
|
||||
| `model.rvf.jsonl` | Extended with `camera_free_supervision` record |
|
||||
| `training-metrics.json` | Includes weak label stats and multi-modal triplet counts |
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# Full pipeline with Seed
|
||||
node scripts/train-camera-free.js \
|
||||
--data data/recordings/pretrain-*.csi.jsonl \
|
||||
--seed-url https://169.254.42.1:8443 \
|
||||
--output models/csi-camerafree-v1
|
||||
|
||||
# CSI-only (no Seed)
|
||||
node scripts/train-camera-free.js \
|
||||
--data data/recordings/pretrain-*.csi.jsonl \
|
||||
--no-seed \
|
||||
--output models/csi-camerafree-v1
|
||||
|
||||
# With benchmark
|
||||
node scripts/train-camera-free.js \
|
||||
--data data/recordings/*.csi.jsonl \
|
||||
--benchmark
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [ruvllm source](vendor/ruvector/npm/packages/ruvllm/) — v2.5.4
|
||||
- [ADR-069](ADR-069-cognitum-seed-csi-pipeline.md) — Cognitum Seed CSI Pipeline
|
||||
- [ADR-070](ADR-070-self-supervised-pretraining.md) — Self-Supervised Pretraining Protocol
|
||||
- [ADR-024](ADR-024-contrastive-csi-embedding.md) — Contrastive CSI Embedding / AETHER
|
||||
- [ADR-016](ADR-016-ruvector-training-pipeline.md) — RuVector Training Pipeline Integration
|
||||
@@ -1,238 +0,0 @@
|
||||
# ADR-072: WiFlow Pose Estimation Architecture
|
||||
|
||||
- **Status**: Proposed
|
||||
- **Date**: 2026-04-02
|
||||
- **Deciders**: ruv
|
||||
- **Relates to**: ADR-071 (ruvllm Training Pipeline), ADR-070 (Self-Supervised Pretraining), ADR-024 (Contrastive CSI Embedding / AETHER), ADR-069 (Cognitum Seed CSI Pipeline)
|
||||
|
||||
## Context
|
||||
|
||||
The WiFi-DensePose project needs a neural architecture that can convert raw CSI amplitude
|
||||
data into 17-keypoint COCO pose estimates. The existing `train-ruvllm.js` pipeline uses a
|
||||
simple 2-layer FC encoder (8 -> 64 -> 128) that produces contrastive embeddings for
|
||||
presence detection but cannot output spatial keypoint coordinates.
|
||||
|
||||
We evaluated published WiFi-based pose estimation architectures:
|
||||
|
||||
| Architecture | Params | Input | Key Innovation | Publication |
|
||||
|-------------|--------|-------|---------------|-------------|
|
||||
| **WiFlow** | 4.82M | 540x20 | TCN + AsymConv + Axial Attention | arXiv:2602.08661 |
|
||||
| WiPose | 11.2M | 3x3x30x20 | 3D CNN + heatmap regression | CVPR 2021 |
|
||||
| MetaFi++ | 8.6M | 114x30x20 | Transformer + meta-learning | NeurIPS 2023 |
|
||||
| Person-in-WiFi 3D | 15.3M | Multi-antenna | Deformable attention + 3D | CVPR 2024 |
|
||||
|
||||
WiFlow is the lightest published SOTA architecture, designed specifically for commercial
|
||||
WiFi hardware. Its key advantage is operating on CSI amplitude only (no phase), which
|
||||
is critical for ESP32-S3 where phase calibration is unreliable.
|
||||
|
||||
### Why WiFlow
|
||||
|
||||
1. **Lightest SOTA**: 4.82M parameters at original scale; our adaptation targets ~2.5M
|
||||
2. **Amplitude-only**: Discards phase, which is noisy on consumer hardware
|
||||
3. **Published architecture**: Fully specified in arXiv:2602.08661, reproducible
|
||||
4. **Temporal modeling**: TCN with dilated causal convolutions captures motion dynamics
|
||||
5. **Efficient attention**: Axial attention reduces O(H^2W^2) to O(H^2W + HW^2)
|
||||
6. **Proven on commercial WiFi**: Validated on commodity Intel 5300 and Atheros hardware
|
||||
|
||||
## Decision
|
||||
|
||||
Implement the WiFlow architecture in pure JavaScript (ruvllm native) with the following
|
||||
adaptations for our ESP32 single TX/RX deployment.
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
```
|
||||
CSI Amplitude [128, 20]
|
||||
|
|
||||
Stage 1: TCN (Dilated Causal Conv)
|
||||
dilation = (1, 2, 4, 8), kernel = 7
|
||||
128 -> 256 -> 192 -> 128 channels
|
||||
|
|
||||
Stage 2: Asymmetric Conv Encoder
|
||||
1xk conv (k=3), stride (1,2)
|
||||
[1, 128, 20] -> [256, 8, 20]
|
||||
|
|
||||
Stage 3: Axial Self-Attention
|
||||
Width (temporal): 8 heads
|
||||
Height (feature): 8 heads
|
||||
|
|
||||
Decoder: Adaptive Avg Pool + Linear
|
||||
[256, 8, 20] -> pool -> [2048] -> [17, 2]
|
||||
|
|
||||
17 COCO Keypoints [x, y] in [0, 1]
|
||||
```
|
||||
|
||||
### Our Adaptation vs Original WiFlow
|
||||
|
||||
| Aspect | WiFlow Original | Our Adaptation | Reason |
|
||||
|--------|----------------|----------------|--------|
|
||||
| Input channels | 540 (18 links x 30 SC) | 128 (1 TX x 1 RX x 128 SC) | Single ESP32 link |
|
||||
| Time steps | 20 | 20 | Same |
|
||||
| TCN channels | 540 -> 256 -> 128 -> 64 | 128 -> 256 -> 192 -> 128 | Proportional reduction |
|
||||
| Spatial blocks | 4 (stride 2) | 4 (stride 2) | Same |
|
||||
| Attention heads | 8 | 8 | Same |
|
||||
| Parameters | 4.82M | ~1.8M | Fewer input channels |
|
||||
| Input type | Amplitude only | Amplitude only | Same |
|
||||
| Output | 17 x 2 | 17 x 2 | Same |
|
||||
|
||||
### Parameter Budget Breakdown
|
||||
|
||||
| Stage | Parameters | % of Total |
|
||||
|-------|-----------|------------|
|
||||
| TCN (4 blocks, k=7, d=1,2,4,8) | ~969K | 54% |
|
||||
| Asymmetric Conv (4 blocks, 1x3, stride 2) | ~174K | 10% |
|
||||
| Axial Attention (width + height, 8 heads) | ~592K | 33% |
|
||||
| Pose Decoder (pool + linear -> 17x2) | ~70K | 4% |
|
||||
| **Total** | **~1.8M** | **100%** |
|
||||
|
||||
### Loss Function
|
||||
|
||||
```
|
||||
L = L_H + 0.2 * L_B
|
||||
|
||||
L_H = SmoothL1(predicted, target, beta=0.1)
|
||||
L_B = (1/14) * sum_b (bone_length_b - prior_b)^2
|
||||
```
|
||||
|
||||
14 bone connections enforce anatomical constraints:
|
||||
- Nose-eye (x2): 0.06
|
||||
- Eye-ear (x2): 0.06
|
||||
- Shoulder-elbow (x2): 0.15
|
||||
- Elbow-wrist (x2): 0.13
|
||||
- Shoulder-hip (x2): 0.26
|
||||
- Hip-knee (x2): 0.25
|
||||
- Knee-ankle (x2): 0.25
|
||||
- Shoulder width: 0.20
|
||||
|
||||
All lengths normalized to person height.
|
||||
|
||||
### Training Strategy (Camera-Free Pipeline)
|
||||
|
||||
Since we have no ground-truth pose labels from cameras, training proceeds in three phases:
|
||||
|
||||
#### Phase 1: Contrastive Pretraining
|
||||
- Temporal triplets: adjacent windows are positive pairs, distant windows are negative
|
||||
- Cross-node triplets: same-time windows from different ESP32 nodes are positive
|
||||
- Uses ruvllm `ContrastiveTrainer` with triplet + InfoNCE loss
|
||||
- Learns a representation where similar CSI states cluster together
|
||||
|
||||
#### Phase 2: Pose Proxy Training
|
||||
- Generate coarse pose proxies from vitals data:
|
||||
- Person detected (presence > 0.3): place standing skeleton at center
|
||||
- High motion: perturb limb positions proportional to motion energy
|
||||
- Breathing: add micro-oscillation to torso keypoints
|
||||
- Train with SmoothL1 + bone constraint loss
|
||||
- Confidence-weighted updates (higher presence = stronger gradient)
|
||||
|
||||
#### Phase 3: Self-Refinement (Future)
|
||||
- Multi-node consistency: same person seen from different nodes should produce
|
||||
consistent pose after geometric transform
|
||||
- Temporal smoothness: adjacent frames should produce similar poses
|
||||
- Bone constraint tightening: gradually reduce tolerance
|
||||
|
||||
### Integration with Existing Pipeline
|
||||
|
||||
```
|
||||
train-ruvllm.js (ADR-071) train-wiflow.js (ADR-072)
|
||||
| |
|
||||
| 8-dim features | 128-dim raw CSI amplitude
|
||||
| -> 128-dim embedding | -> 17x2 keypoint coordinates
|
||||
| -> presence/activity/vitals | -> bone-constrained pose
|
||||
| |
|
||||
+-- ContrastiveTrainer -----+------+
|
||||
+-- TrainingPipeline -------+------+
|
||||
+-- LoRA per-node ----------+------+
|
||||
+-- TurboQuant quantize ----+------+
|
||||
+-- SafeTensors export -----+------+
|
||||
```
|
||||
|
||||
Both pipelines share the ruvllm infrastructure; WiFlow adds the deeper architecture
|
||||
for direct pose regression while the simple encoder handles embedding tasks.
|
||||
|
||||
### Performance Targets
|
||||
|
||||
| Metric | Target | Notes |
|
||||
|--------|--------|-------|
|
||||
| PCK@20 | > 80% | On lab data with 2+ nodes |
|
||||
| Forward latency | < 50ms | Pi Zero 2W at INT8 |
|
||||
| Model size (INT8) | < 2 MB | TurboQuant |
|
||||
| Bone violation rate | < 10% | 50% tolerance |
|
||||
| Temporal jitter | < 3cm | Exponential smoothing |
|
||||
|
||||
### Risk Assessment
|
||||
|
||||
| Risk | Severity | Mitigation |
|
||||
|------|----------|------------|
|
||||
| Single TX/RX has less spatial info than 18 links | High | 2-node multi-static compensates; cross-node fusion from ADR-029 |
|
||||
| Camera-free labels are coarse | Medium | Bone constraints enforce anatomy; contrastive pretrain provides structure |
|
||||
| Pure JS too slow for real-time | Medium | INT8 quantization; axial attention is O(H^2W+HW^2) not O(H^2W^2) |
|
||||
| Overfitting with ~5K frames | Medium | Temporal augmentation + noise + cross-node interpolation |
|
||||
| Phase not available (amplitude-only) | Low | WiFlow was designed amplitude-only; not a limitation |
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Proven SOTA architecture adapted to our hardware constraints
|
||||
- Pure JavaScript implementation runs everywhere ruvllm runs (Node.js, browser WASM)
|
||||
- Bone constraints enforce physically plausible outputs even with noisy inputs
|
||||
- Shares training infrastructure with existing ruvllm pipeline
|
||||
- Modular: each stage (TCN, AsymConv, Axial, Decoder) is independently testable
|
||||
|
||||
### Negative
|
||||
- ~1.8M parameters is 193x larger than simple CsiEncoder (9,344 params)
|
||||
- Forward pass is slower (~50ms vs <1ms for simple encoder)
|
||||
- Camera-free training will produce lower accuracy than supervised WiFlow
|
||||
- No ground-truth PCK evaluation possible without camera labels
|
||||
- Axial attention is O(N^2) within each axis, limiting scalability
|
||||
|
||||
### Neutral
|
||||
- FLOPs dominated by TCN (~48%) due to dilated convolutions
|
||||
- INT8 quantization brings model to ~1.7MB, viable for edge deployment
|
||||
- Architecture is fixed (no NAS); future work could explore lighter variants
|
||||
|
||||
## Implementation
|
||||
|
||||
### Files Created
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `scripts/wiflow-model.js` | WiFlow architecture (all stages, loss, metrics) |
|
||||
| `scripts/train-wiflow.js` | Training pipeline (contrastive + pose proxy + LoRA + quant) |
|
||||
| `scripts/benchmark-wiflow.js` | Benchmarking (latency, params, FLOPs, memory, quality) |
|
||||
| `docs/adr/ADR-072-wiflow-architecture.md` | This document |
|
||||
|
||||
### Usage
|
||||
|
||||
```bash
|
||||
# Train on collected data
|
||||
node scripts/train-wiflow.js --data data/recordings/pretrain-*.csi.jsonl
|
||||
|
||||
# Train with more epochs and custom output
|
||||
node scripts/train-wiflow.js --data data/recordings/*.csi.jsonl --epochs 50 --output models/wiflow-v2
|
||||
|
||||
# Contrastive pretraining only (no labels needed)
|
||||
node scripts/train-wiflow.js --data data/recordings/*.csi.jsonl --contrastive-only
|
||||
|
||||
# Benchmark
|
||||
node scripts/benchmark-wiflow.js
|
||||
|
||||
# Benchmark with trained model
|
||||
node scripts/benchmark-wiflow.js --model models/wiflow-v1
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
- ruvllm (vendored at `vendor/ruvector/npm/packages/ruvllm/src/`)
|
||||
- `ContrastiveTrainer`, `tripletLoss`, `infoNCELoss`, `computeGradient`
|
||||
- `TrainingPipeline`
|
||||
- `LoraAdapter`, `LoraManager`
|
||||
- `EwcManager`
|
||||
- `ModelExporter`, `SafeTensorsWriter`
|
||||
- No external ML frameworks (no PyTorch, no TensorFlow, no ONNX Runtime)
|
||||
|
||||
## References
|
||||
|
||||
- WiFlow: arXiv:2602.08661
|
||||
- COCO Keypoints: https://cocodataset.org/#keypoints-2020
|
||||
- Axial Attention: Wang et al., "Axial-DeepLab", ECCV 2020
|
||||
- TCN: Bai et al., "An Empirical Evaluation of Generic Convolutional and Recurrent Networks for Sequence Modeling", 2018
|
||||
@@ -1,202 +0,0 @@
|
||||
# ADR-073: Multi-Frequency Mesh Scanning
|
||||
|
||||
| Field | Value |
|
||||
|-------------|--------------------------------------------|
|
||||
| **Status** | Proposed |
|
||||
| **Date** | 2026-04-02 |
|
||||
| **Authors** | ruv |
|
||||
| **Depends** | ADR-018 (binary frame), ADR-029 (channel hopping), ADR-039 (edge processing), ADR-060 (channel override) |
|
||||
|
||||
## Context
|
||||
|
||||
The current WiFi-DensePose deployment uses 2 ESP32-S3 nodes operating on a single WiFi channel (channel 5, 2432 MHz). A scan of the office environment reveals 9 WiFi networks across 6 distinct channels (1, 3, 5, 6, 9, 11), each broadcasting continuously. These neighbor networks are free RF illuminators whose signals pass through the room and interact with objects, people, and walls.
|
||||
|
||||
**Current single-channel limitations:**
|
||||
|
||||
1. **19% null subcarriers** — metal objects (desk, monitor frame, filing cabinet) create frequency-selective fading that blocks specific subcarriers on channel 5. These nulls are permanent blind spots in the RF map.
|
||||
|
||||
2. **No frequency diversity** — objects that are transparent at 2432 MHz may be opaque at 2412 MHz or 2462 MHz, and vice versa. A metal mesh that blocks one wavelength (122.5 mm at 2432 MHz) may pass another (124.0 mm at 2412 MHz) due to the mesh aperture-to-wavelength ratio.
|
||||
|
||||
3. **Single-perspective CSI** — both nodes see the same 52-64 subcarriers on the same channel. The subcarrier indices map to the same frequency bins, providing no spectral diversity.
|
||||
|
||||
4. **Neighbor illuminator waste** — 6 other APs broadcast continuously in the room. Their signals pass through walls, furniture, and people, creating CSI-measurable reflections that we currently ignore because we only listen on channel 5.
|
||||
|
||||
## Decision
|
||||
|
||||
Implement interleaved multi-frequency channel hopping across the 2 ESP32-S3 nodes, scanning 6 WiFi channels to build a wideband RF map of the room.
|
||||
|
||||
### Channel Allocation Strategy
|
||||
|
||||
The 2.4 GHz ISM band has 3 non-overlapping 20 MHz channels (1, 6, 11) and several partially-overlapping channels between them. We allocate channels to maximize both spectral coverage and illuminator exploitation:
|
||||
|
||||
```
|
||||
Node 1: ch 1, 6, 11 (non-overlapping, full band coverage)
|
||||
Node 2: ch 3, 5, 9 (interleaved, near neighbor APs)
|
||||
```
|
||||
|
||||
**Rationale for this split:**
|
||||
|
||||
| Channel | Freq (MHz) | Node | Neighbor Illuminators | Purpose |
|
||||
|---------|------------|------|----------------------------------------------|-----------------------------------|
|
||||
| 1 | 2412 | 1 | (none visible, but lower freq = better penetration) | Low-frequency penetration |
|
||||
| 3 | 2422 | 2 | conclusion mesh (signal 44) | Exploit neighbor AP as illuminator |
|
||||
| 5 | 2432 | 2 | ruv.net (100), Cohen-Guest (100), HP LaserJet (94) | Primary channel, strongest illuminators |
|
||||
| 6 | 2437 | 1 | Innanen (signal 19) | Center band, non-overlapping |
|
||||
| 9 | 2452 | 2 | NETGEAR72 (42), NETGEAR72-Guest (42) | Exploit dual NETGEAR illuminators |
|
||||
| 11 | 2462 | 1 | COGECO-21B20 (100), COGECO-4321 (30) | High-frequency, strong illuminators |
|
||||
|
||||
Each node dwells on a channel for 250 ms (configurable), collects 3-4 CSI frames, then hops to the next. The 3-channel rotation completes in 750 ms, giving ~1.3 full rotations per second.
|
||||
|
||||
### Physics Basis
|
||||
|
||||
At 2.4 GHz, WiFi wavelength ranges from 122.0 mm (ch 14, 2484 MHz) to 124.0 mm (ch 1, 2412 MHz). While this is a narrow range (~2%), the effect on multipath is significant:
|
||||
|
||||
1. **Frequency-selective fading**: multipath reflections create constructive/destructive interference patterns that vary with frequency. A 2 cm path length difference produces a null at 2432 MHz but constructive interference at 2412 MHz.
|
||||
|
||||
2. **Diffraction around objects**: Huygens-Fresnel diffraction depends on wavelength. Objects smaller than ~lambda/2 (61 mm) scatter differently across the band. Common office objects (monitor bezels, chair legs, cable bundles) are in this range.
|
||||
|
||||
3. **Material transparency**: some materials (wire mesh, perforated metal, PCB ground planes) have frequency-dependent transmission. A monitor's EMI shielding mesh with 5 mm apertures blocks 2.4 GHz signals but the exact attenuation varies with frequency due to slot antenna effects.
|
||||
|
||||
4. **Subcarrier orthogonality**: OFDM subcarriers on different channels are in different frequency bins. A null on subcarrier 15 of channel 5 does not imply a null on subcarrier 15 of channel 1, because they map to different absolute frequencies.
|
||||
|
||||
### Null Diversity Mechanism
|
||||
|
||||
```
|
||||
Channel 5 subcarriers: ▅▆█▇▅▃▁_▁▃▅▆█▇▅▃▁_▁▃▅▆█▇▅▃
|
||||
^ null (metal desk)
|
||||
Channel 1 subcarriers: ▃▅▆█▇▅▃▅▆█▇▅▃▅▆█▇▅▃▅▆█▇▅▃▅▃
|
||||
^ resolved! Different freq = different null pattern
|
||||
|
||||
Channel 11 subcarriers: ▅▃▁_▁▃▅▆█▇▅▃▅▆▅▃▁_▁▃▅▆█▇▅▃▅
|
||||
^ null here instead (shifted by frequency offset)
|
||||
```
|
||||
|
||||
By fusing subcarrier data across channels, nulls that exist on one channel are filled by non-null data from other channels. The remaining nulls (present on ALL channels) represent truly opaque objects — large metal surfaces that block all 2.4 GHz frequencies.
|
||||
|
||||
### Wideband View
|
||||
|
||||
Single channel: ~52-64 subcarriers (20 MHz bandwidth)
|
||||
Multi-channel (6 channels): ~312-384 effective subcarrier observations (120 MHz coverage)
|
||||
|
||||
This is not simply 6x the resolution (the subcarrier spacing within each channel is the same), but it provides:
|
||||
- 6x the spectral diversity for null mitigation
|
||||
- 6x the illuminator variety (different APs = different signal paths)
|
||||
- Frequency-dependent scattering signatures for material classification
|
||||
|
||||
## Integration
|
||||
|
||||
### Firmware (already supported)
|
||||
|
||||
The channel hopping infrastructure is already implemented in the ESP32 firmware (ADR-029):
|
||||
|
||||
```c
|
||||
// csi_collector.h — already exists
|
||||
void csi_collector_set_hop_table(const uint8_t *channels, uint8_t hop_count, uint32_t dwell_ms);
|
||||
void csi_collector_start_hop_timer(void);
|
||||
```
|
||||
|
||||
The ADR-018 binary frame header already includes the channel/frequency field at bytes [8..11], so the server-side parser can distinguish frames from different channels without any firmware changes.
|
||||
|
||||
### Provisioning Commands
|
||||
|
||||
```bash
|
||||
# Node 1 (COM7): non-overlapping channels 1, 6, 11
|
||||
python firmware/esp32-csi-node/provision.py --port COM7 \
|
||||
--ssid "ruv.net" --password "..." --target-ip 192.168.1.20 \
|
||||
--hop-channels 1,6,11 --hop-dwell-ms 250
|
||||
|
||||
# Node 2 (COM_): interleaved channels 3, 5, 9
|
||||
python firmware/esp32-csi-node/provision.py --port COM_ \
|
||||
--ssid "ruv.net" --password "..." --target-ip 192.168.1.20 \
|
||||
--hop-channels 3,5,9 --hop-dwell-ms 250
|
||||
```
|
||||
|
||||
Note: `--hop-channels` and `--hop-dwell-ms` require provision.py support for writing these values to NVS. If not yet implemented, the firmware's `csi_collector_set_hop_table()` can be called directly from the main init code with compile-time constants.
|
||||
|
||||
### Server-Side Processing
|
||||
|
||||
Three new Node.js scripts consume the multi-channel CSI data:
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `scripts/rf-scan.js` | Single-channel live RF room scanner with ASCII spectrum |
|
||||
| `scripts/rf-scan-multifreq.js` | Multi-channel scanner with null diversity analysis |
|
||||
| `scripts/benchmark-rf-scan.js` | Quantitative benchmark of multi-channel performance |
|
||||
|
||||
All scripts parse the ADR-018 binary UDP format and use the frequency field to separate frames by channel.
|
||||
|
||||
### Cognitum Seed Integration
|
||||
|
||||
The Cognitum Seed vector store (ADR-069) currently stores 1,605 vectors from single-channel CSI. With multi-frequency scanning:
|
||||
|
||||
1. **Per-channel feature vectors**: store separate 8-dim feature vectors for each channel, tagged with channel number. This increases the vector count to ~9,630 (6 channels x 1,605).
|
||||
|
||||
2. **Wideband feature vector**: concatenate or average per-channel features into a 48-dim wideband vector for richer kNN search. Objects that are ambiguous on one channel may be clearly distinguishable in the wideband representation.
|
||||
|
||||
3. **Null-aware embeddings**: encode null subcarrier patterns as part of the feature vector. The null pattern itself is informative — a consistent null at subcarrier 15 across all channels indicates a large metal object, while a null only on channel 5 indicates a frequency-dependent scatterer.
|
||||
|
||||
## Performance Targets
|
||||
|
||||
| Metric | Single-Channel Baseline | Multi-Channel Target | Method |
|
||||
|--------|------------------------|---------------------|--------|
|
||||
| Subcarrier count | ~52-64 | ~312-384 (6x) | 6 channels x 52-64 subcarriers |
|
||||
| Null gap | 19% | <5% | Null diversity across channels |
|
||||
| Position resolution | ~30 cm | ~15 cm | sqrt(6) improvement from independent observations |
|
||||
| Per-channel FPS | 12 fps | ~4 fps | 250 ms dwell x 3 channels = 750 ms rotation |
|
||||
| Total FPS (all channels) | 12 fps | ~12 fps per node (4 fps x 3 channels) |
|
||||
| Wideband rotation | N/A | ~1.3 Hz | Full 3-channel rotation in 750 ms |
|
||||
|
||||
## Risks
|
||||
|
||||
### Per-Channel Sample Rate Reduction
|
||||
|
||||
Channel hopping reduces the per-channel sample rate from 12 fps (single channel) to approximately 4 fps per channel (250 ms dwell, 3 channels). This affects:
|
||||
|
||||
- **Vitals extraction**: breathing rate (0.1-0.5 Hz) requires at least 2 fps (Nyquist). At 4 fps per channel, this is met. Heart rate (0.8-2.0 Hz) requires at least 4 fps, which is marginal. Mitigation: keep one channel as "primary" with longer dwell for vitals, or fuse phase data across channels.
|
||||
|
||||
- **Motion tracking**: 4 fps is sufficient for walking speed (<2 m/s) but insufficient for fast gestures. If gesture recognition is needed, reduce to 2-channel hopping or increase dwell rate.
|
||||
|
||||
### Channel Hopping Latency
|
||||
|
||||
`esp_wifi_set_channel()` takes ~1-5 ms on ESP32-S3. During the transition, no CSI frames are captured. At 250 ms dwell, this is <2% overhead.
|
||||
|
||||
### AP Disconnection
|
||||
|
||||
Channel hopping may cause the ESP32 to lose connection to the home AP (ruv.net on channel 5) when dwelling on other channels. The STA reconnects automatically, but there may be brief UDP packet loss. Mitigation: the firmware already handles this gracefully — CSI collection works in promiscuous mode regardless of STA connection state.
|
||||
|
||||
### Increased Server Load
|
||||
|
||||
2 nodes x 3 channels x 4 fps = 24 frames/second total UDP traffic. Each frame is ~150-200 bytes (20-byte header + 64 subcarriers x 2 bytes I/Q). Total: ~4.8 KB/s — negligible.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. **5 GHz channels**: ESP32-S3 supports 5 GHz CSI, and the shorter wavelength (60 mm) provides better spatial resolution. Rejected because: (a) no 5 GHz APs visible in the current environment, so no free illuminators; (b) 5 GHz has worse wall penetration, reducing the effective sensing volume.
|
||||
|
||||
2. **More nodes**: adding a 3rd or 4th ESP32 node would increase spatial diversity without channel hopping. Rejected for now due to cost, but this is complementary — more nodes + channel hopping would give both spatial and spectral diversity.
|
||||
|
||||
3. **Wider bandwidth (HT40)**: using 40 MHz channels doubles subcarrier count per channel. Rejected because: (a) HT40 requires a secondary channel, reducing available channels for hopping; (b) many neighbor APs use HT20, so their illumination only covers 20 MHz.
|
||||
|
||||
## SNN Integration (ADR-074)
|
||||
|
||||
Multi-frequency scanning produces subcarrier data across 6 channels, creating temporal patterns that are well-suited for spiking neural network processing. ADR-074 introduces an SNN with STDP learning that consumes the multi-channel CSI stream.
|
||||
|
||||
**Key interactions with multi-frequency data:**
|
||||
|
||||
1. **Null diversity as SNN input**: subcarriers that are null on one channel but active on another produce a distinctive spike pattern (spikes only during certain channel dwells). STDP learns to associate these cross-channel patterns with specific objects or zones — something a single-channel SNN cannot do.
|
||||
|
||||
2. **Channel-interleaved temporal coding**: because each node dwells on 3 channels in a 750ms rotation, the SNN receives subcarrier data in a repeating temporal pattern (ch1 → ch2 → ch3 → ch1 ...). The SNN's LIF membrane dynamics integrate spikes across the rotation, naturally performing cross-channel fusion through temporal summation. A hidden neuron that receives spikes from subcarrier 15 on channel 1 AND subcarrier 15 on channel 6 will fire more strongly than one receiving either alone.
|
||||
|
||||
3. **Expanded input mode**: on the server (not constrained by ESP32 memory), the SNN can use 384 input neurons (6 channels x 64 subcarriers) instead of 128. This provides maximum spectral diversity per frame but requires ~150 KB of weight storage. The `snn-csi-processor.js` script supports this via the `--hidden` flag to scale the network.
|
||||
|
||||
4. **Illuminator fingerprinting**: different neighbor APs have different beamforming patterns and power levels. The SNN learns which subcarrier patterns belong to which illuminator, enabling it to distinguish AP-specific signatures from human-caused perturbations. This is especially useful for the NETGEAR dual-AP setup on channel 9, where two illuminators from different positions create stereo-like RF coverage.
|
||||
|
||||
## References
|
||||
|
||||
- ADR-018: CSI binary frame format
|
||||
- ADR-029: Channel hopping infrastructure
|
||||
- ADR-039: Edge processing pipeline
|
||||
- ADR-060: Channel override provisioning
|
||||
- ADR-069: Cognitum Seed CSI pipeline
|
||||
- ADR-074: Spiking neural network for CSI sensing
|
||||
- IEEE 802.11-2020, Section 21 (OFDM PHY)
|
||||
- ESP-IDF CSI Guide: https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32s3/api-guides/wifi.html#wi-fi-channel-state-information
|
||||
@@ -1,208 +0,0 @@
|
||||
# ADR-074: Spiking Neural Network for CSI Sensing
|
||||
|
||||
| Field | Value |
|
||||
|-------------|--------------------------------------------|
|
||||
| **Status** | Proposed |
|
||||
| **Date** | 2026-04-02 |
|
||||
| **Authors** | ruv |
|
||||
| **Depends** | ADR-018 (binary frame), ADR-029 (channel hopping), ADR-069 (Cognitum Seed), ADR-073 (multi-frequency mesh) |
|
||||
|
||||
## Context
|
||||
|
||||
The current WiFi-DensePose CSI sensing pipeline uses two approaches for interpreting subcarrier data:
|
||||
|
||||
1. **Static thresholds** — presence detection fires when subcarrier variance exceeds a fixed value. This works in calibrated environments but fails when the RF landscape changes (furniture moved, new objects, temperature drift). Recalibration requires manual intervention or batch retraining.
|
||||
|
||||
2. **Batch-trained FC encoder** — the neural network in `wifi-densepose-nn` maps CSI frames to 8-dimensional feature vectors. It requires labeled training data, offline training epochs, and model deployment. The encoder cannot adapt to a new environment without collecting new data and retraining.
|
||||
|
||||
Neither approach handles online adaptation. When an ESP32 node is deployed in a new room, the first hours produce noisy, unreliable output until the thresholds are tuned or a model is trained. In disaster scenarios (ADR MAT), there is no time for calibration.
|
||||
|
||||
**Spiking Neural Networks (SNNs)** offer an alternative. Unlike traditional ANNs that process continuous values in batch mode, SNNs communicate through discrete spike events and learn online via Spike-Timing-Dependent Plasticity (STDP). This is a natural fit for CSI data:
|
||||
|
||||
- CSI subcarrier amplitudes are temporal signals sampled at 12-22 fps
|
||||
- Amplitude changes (not absolute values) carry the information about motion, breathing, and presence
|
||||
- STDP learns temporal correlations between subcarriers without labels
|
||||
- Event-driven processing means idle rooms (no motion) consume near-zero compute
|
||||
|
||||
The `@ruvector/spiking-neural` package (vendored at `vendor/ruvector/npm/packages/spiking-neural/`) provides production-ready LIF neurons, STDP learning, lateral inhibition, and SIMD-optimized vector math in pure JavaScript with zero dependencies.
|
||||
|
||||
## Decision
|
||||
|
||||
Integrate `@ruvector/spiking-neural` into the CSI sensing pipeline as an online unsupervised pattern learner that runs alongside the existing FC encoder. The SNN provides real-time adaptation while the FC encoder provides stable baseline predictions.
|
||||
|
||||
### Network Architecture
|
||||
|
||||
```
|
||||
CSI Frame (128 subcarriers)
|
||||
|
|
||||
v
|
||||
[ Rate Encoding ] -----> 128 input neurons (one per subcarrier)
|
||||
| amplitude delta -> spike rate
|
||||
v
|
||||
[ LIF Hidden Layer ] ---> 64 hidden neurons (tau=20ms)
|
||||
| STDP learns subcarrier correlations
|
||||
| lateral inhibition -> sparse codes
|
||||
v
|
||||
[ LIF Output Layer ] ---> 8 output neurons
|
||||
|
|
||||
v
|
||||
presence | motion | breathing | heart_rate | phase_var | persons | fall | rssi
|
||||
```
|
||||
|
||||
**Layer parameters:**
|
||||
|
||||
| Layer | Neurons | tau (ms) | v_thresh (mV) | Function |
|
||||
|-------|---------|----------|---------------|----------|
|
||||
| Input | 128 | N/A | N/A | Rate-coded spike generation from subcarrier deltas |
|
||||
| Hidden | 64 | 20.0 | -50.0 | STDP learns correlated subcarrier groups |
|
||||
| Output | 8 | 25.0 | -50.0 | Each neuron specializes in one sensing modality |
|
||||
|
||||
**Synapse parameters:**
|
||||
|
||||
| Connection | Count | a_plus | a_minus | w_init | Lateral Inhibition |
|
||||
|------------|-------|--------|---------|--------|-------------------|
|
||||
| Input -> Hidden | 8,192 | 0.005 | 0.005 | 0.3 | No |
|
||||
| Hidden -> Output | 512 | 0.003 | 0.003 | 0.2 | Yes (strength=15.0) |
|
||||
|
||||
Total synapses: 8,704. At 4 bytes per weight, this is 34 KB — fits in ESP32 SRAM.
|
||||
|
||||
### Input Encoding
|
||||
|
||||
CSI amplitudes are converted to spike rates using rate coding:
|
||||
|
||||
1. Compute per-subcarrier amplitude: `amp[i] = sqrt(I[i]^2 + Q[i]^2)` from the ADR-018 binary frame
|
||||
2. Compute amplitude delta from previous frame: `delta[i] = |amp[i] - prev_amp[i]|`
|
||||
3. Normalize deltas to [0, 1] range: `norm[i] = min(delta[i] / max_delta, 1.0)`
|
||||
4. Feed `norm` to `rateEncoding(norm, dt, max_rate)` which produces Poisson spikes
|
||||
|
||||
Higher amplitude changes produce more spikes. Static subcarriers (no motion) produce few or no spikes. This is the key energy advantage: an empty room generates almost no spikes, so the SNN does almost no work.
|
||||
|
||||
### STDP Learning Rule
|
||||
|
||||
STDP strengthens connections between neurons that fire together (within a time window) and weakens connections between neurons that fire out of sync:
|
||||
|
||||
- **LTP (Long-Term Potentiation)**: if a presynaptic neuron fires before a postsynaptic neuron within 20ms, the weight increases by `a_plus * exp(-dt/tau_stdp)`
|
||||
- **LTD (Long-Term Depression)**: if a postsynaptic neuron fires before a presynaptic neuron, the weight decreases by `a_minus * exp(-dt/tau_stdp)`
|
||||
|
||||
Over time, this causes the hidden layer neurons to specialize. Subcarriers that consistently change together (e.g., subcarriers 10-20 affected by a person walking through zone A) become strongly connected to the same hidden neuron. Different motion patterns activate different hidden neuron clusters.
|
||||
|
||||
### Lateral Inhibition (Winner-Take-All)
|
||||
|
||||
The output layer uses lateral inhibition with strength 15.0. When one output neuron fires, it suppresses all others. This forces each output neuron to specialize in a distinct pattern:
|
||||
|
||||
- Output 0: presence (any subcarrier activity above baseline)
|
||||
- Output 1: motion (widespread subcarrier changes, high spike rate)
|
||||
- Output 2: breathing (periodic 0.1-0.5 Hz modulation on chest-area subcarriers)
|
||||
- Output 3: heart rate (periodic 0.8-2.0 Hz modulation, lower amplitude than breathing)
|
||||
- Output 4: phase variance (phase instability across subcarriers)
|
||||
- Output 5: person count (number of distinct active subcarrier clusters)
|
||||
- Output 6: fall (sudden high-amplitude burst followed by silence)
|
||||
- Output 7: RSSI trend (overall signal strength change)
|
||||
|
||||
The neuron-to-label mapping is not fixed by training. Instead, the mapping is discovered by observing which output neuron fires most for each known condition during an optional calibration phase. If no calibration is available, the output is reported as raw spike counts per output neuron, and downstream consumers (Cognitum Seed, SONA) interpret the patterns.
|
||||
|
||||
### Integration with Existing Pipeline
|
||||
|
||||
The SNN does not replace the FC encoder. It runs in parallel:
|
||||
|
||||
```
|
||||
CSI Frame ----+----> FC Encoder --------> 8-dim feature vector (stable, trained)
|
||||
|
|
||||
+----> SNN (STDP) --------> 8-dim spike rate vector (adaptive, online)
|
||||
|
|
||||
+----> SONA Adapter -------> Weighted fusion of both signals
|
||||
```
|
||||
|
||||
SONA (Self-Optimizing Neural Architecture) receives both signals and learns which source is more reliable for each output dimension. In a new environment where the FC encoder has not been retrained, SONA automatically weights the SNN output higher because it adapts faster. As the FC encoder is retrained on local data, SONA shifts weight back toward it.
|
||||
|
||||
### Energy and Compute Budget
|
||||
|
||||
| Metric | FC Encoder | SNN (STDP) | Ratio |
|
||||
|--------|-----------|------------|-------|
|
||||
| Compute per frame (idle room) | 8,192 MACs | ~50 spike events | ~160x less |
|
||||
| Compute per frame (active room) | 8,192 MACs | ~500 spike events | ~16x less |
|
||||
| Memory | 34 KB weights | 34 KB weights | Equal |
|
||||
| Adaptation | Offline retraining | Online, continuous | SNN wins |
|
||||
| Stability | High (frozen weights) | Lower (weights drift) | FC wins |
|
||||
| Latency to first useful output | Hours (needs training data) | ~30 seconds | SNN wins |
|
||||
|
||||
The SNN's event-driven nature means it processes only spikes, not every subcarrier on every frame. In an idle room with no motion, subcarrier deltas are near zero, spike rates drop to near zero, and the SNN consumes negligible compute. This is ideal for battery-powered or thermally constrained deployments (ESP32, Cognitum Seed Pi Zero).
|
||||
|
||||
### Deployment Targets
|
||||
|
||||
| Platform | Runtime | Notes |
|
||||
|----------|---------|-------|
|
||||
| Node.js server | `require('@ruvector/spiking-neural')` | Primary. Receives UDP frames, runs SNN. |
|
||||
| Cognitum Seed (Pi Zero) | Node.js ARM | 34 KB model fits. ~0.06ms per step at 100 neurons. |
|
||||
| ESP32-S3 (WASM) | wasm3 interpreter | Optional. SNN weights exported as flat Float32Array. |
|
||||
| Browser | WebAssembly or JS | Via `wifi-densepose-wasm` crate's JS bindings. |
|
||||
|
||||
### Multi-Channel SNN (ADR-073 Integration)
|
||||
|
||||
With multi-frequency mesh scanning (ADR-073), the SNN input expands:
|
||||
|
||||
- **Single-channel mode**: 128 input neurons (64 subcarriers x 2 for I/Q or amplitude/phase)
|
||||
- **Multi-channel mode**: 128 input neurons, but the subcarrier index rotates across channels. Each channel's subcarriers map to the same neuron indices, but at different time slots. The SNN's temporal dynamics naturally integrate cross-channel information because STDP operates across time.
|
||||
|
||||
Alternatively, for maximum spectral diversity, a wider SNN (384 input neurons for 6 channels x 64 subcarriers) can be used on the server where memory is not constrained.
|
||||
|
||||
## Performance Targets
|
||||
|
||||
| Metric | Target | Method |
|
||||
|--------|--------|--------|
|
||||
| SNN step latency | <0.1ms | 128-64-8 network, ~8,700 synapses |
|
||||
| STDP convergence | <30 seconds | ~360 frames at 12 fps, patterns stabilize |
|
||||
| Output accuracy (after adaptation) | >80% | Compared to manually labeled ground truth |
|
||||
| Memory footprint | <50 KB | Weights + neuron state |
|
||||
| Idle room spike rate | <10 spikes/frame | Event-driven: near-zero compute when nothing moves |
|
||||
| Adaptation to new environment | <2 minutes | STDP relearns subcarrier correlations |
|
||||
|
||||
## Risks
|
||||
|
||||
### Weight Drift
|
||||
|
||||
STDP learning never stops. In a stable environment, weights can slowly drift as the network over-fits to the current RF landscape. Mitigation: implement weight decay (multiply all weights by 0.999 per second) and clamp weights to [w_min, w_max].
|
||||
|
||||
### Output Neuron Reassignment
|
||||
|
||||
If the RF environment changes significantly (new furniture, different room), output neurons may reassign their specialization. The mapping from output neuron index to label (presence, motion, etc.) may change. Mitigation: periodically log the output neuron activity and detect reassignment events. Downstream consumers should use the spike pattern, not the neuron index, for classification.
|
||||
|
||||
### Interference with FC Encoder
|
||||
|
||||
If SONA naively averages the SNN and FC encoder outputs, a poorly adapted SNN could degrade overall accuracy. Mitigation: SONA uses confidence-weighted fusion. The SNN output includes a confidence signal (total spike count / expected spike count). Low confidence = low weight.
|
||||
|
||||
### STDP Learning Rate Sensitivity
|
||||
|
||||
If `a_plus` and `a_minus` are too high, the SNN oscillates and never converges. If too low, adaptation takes too long. The default values (0.005 and 0.003) are conservative. The script includes a `--learning-rate` flag for tuning.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. **Online gradient descent on FC encoder** — backprop through the FC network with each new frame. Rejected because: (a) requires a loss function, which requires labels; (b) continuous gradient updates on a small model lead to catastrophic forgetting of the pretrained representations.
|
||||
|
||||
2. **Adaptive thresholds only** — replace fixed thresholds with exponentially-weighted moving averages. Rejected because: (a) single-variable thresholds cannot capture multi-subcarrier correlations; (b) no representation learning — each subcarrier is still processed independently.
|
||||
|
||||
3. **Reservoir computing (Echo State Network)** — use a fixed random recurrent network as a temporal feature extractor. Partially viable, but: (a) requires a linear readout layer trained with labels; (b) the random reservoir does not adapt to the specific RF environment.
|
||||
|
||||
4. **Train SNN with supervision** — use surrogate gradient methods to train the SNN on labeled data. Rejected because: (a) defeats the purpose of online unsupervised learning; (b) the `@ruvector/spiking-neural` package does not implement surrogate gradients.
|
||||
|
||||
## Implementation
|
||||
|
||||
The integration is implemented in `scripts/snn-csi-processor.js`, a standalone Node.js script that:
|
||||
|
||||
1. Receives live CSI frames via UDP (port 5006, ADR-018 binary format)
|
||||
2. Decodes subcarrier I/Q data and computes amplitude deltas
|
||||
3. Feeds deltas through rate encoding into the SNN
|
||||
4. Applies STDP learning on every frame (online, unsupervised)
|
||||
5. Maps output neuron spike counts to sensing labels
|
||||
6. Prints real-time ASCII visualization of SNN activity
|
||||
7. Optionally forwards learned patterns to Cognitum Seed
|
||||
|
||||
## References
|
||||
|
||||
- ADR-018: CSI binary frame format
|
||||
- ADR-029: Channel hopping infrastructure
|
||||
- ADR-069: Cognitum Seed CSI pipeline
|
||||
- ADR-073: Multi-frequency mesh scanning
|
||||
- Maass, W. (1997). "Networks of spiking neurons: The third generation of neural network models." Neural Networks, 10(9), 1659-1671.
|
||||
- Bi, G. & Poo, M. (1998). "Synaptic modifications in cultured hippocampal neurons: Dependence on spike timing." Journal of Neuroscience, 18(24), 10464-10472.
|
||||
- `@ruvector/spiking-neural` v1.0.1 — LIF, STDP, lateral inhibition, SIMD
|
||||
@@ -1,195 +0,0 @@
|
||||
# ADR-075: Min-Cut Based Person Separation from Subcarrier Correlation
|
||||
|
||||
- **Status:** Proposed
|
||||
- **Date:** 2026-04-02
|
||||
- **Issue:** #348 — `n_persons` always reports 4 regardless of actual occupancy
|
||||
- **Depends on:** ADR-016 (RuVector integration), ADR-041 (person tracking), ADR-073 (multifrequency mesh scan)
|
||||
|
||||
## Context
|
||||
|
||||
### The Bug
|
||||
|
||||
Issue #348 reports that the ESP32 firmware's multi-person counting always reports
|
||||
`n_persons = 4`. The root cause is in the WASM edge module
|
||||
`sig_mincut_person_match.rs`, which uses a fixed `MAX_PERSONS = 4` constant and a
|
||||
threshold-based variance classifier to populate person slots. The classifier bins
|
||||
subcarriers into "dynamic" vs "static" using a single fixed variance threshold
|
||||
(`DYNAMIC_VAR_THRESH = 0.15`). In practice:
|
||||
|
||||
1. The threshold is miscalibrated for real-world CSI data — almost any room with
|
||||
multipath reflections pushes a majority of subcarriers above 0.15 variance.
|
||||
2. The subcarrier-to-person assignment uses a greedy Hungarian-lite matcher that
|
||||
fills all 4 slots once there are >= 4 dynamic subcarriers (which is nearly
|
||||
always the case).
|
||||
3. There is no mechanism to determine how many independent movers exist — the
|
||||
algorithm assumes all 4 slots should be filled.
|
||||
|
||||
### Prior Art
|
||||
|
||||
The Rust crate `ruvector-mincut` (vendored at `vendor/ruvector/crates/ruvector-mincut/`)
|
||||
implements a full dynamic min-cut algorithm with O(n^{o(1)}) amortized update time,
|
||||
Stoer-Wagner exact min-cut, and online edge insert/delete. It is already integrated
|
||||
in the training pipeline (`wifi-densepose-train/src/metrics.rs`) via
|
||||
`DynamicPersonMatcher`.
|
||||
|
||||
### WiFi Sensing Insight
|
||||
|
||||
When a person moves through a room, they perturb the Fresnel zones of specific
|
||||
subcarrier frequencies. Subcarriers whose Fresnel zones overlap the person's body
|
||||
change **together** — their amplitudes are temporally correlated. When two people
|
||||
move independently, they create two **separate** groups of correlated subcarriers.
|
||||
This correlation structure forms a natural graph partitioning problem.
|
||||
|
||||
## Decision
|
||||
|
||||
Replace the fixed-threshold person counter with a spectral min-cut algorithm
|
||||
operating on the subcarrier temporal correlation graph. This runs in the bridge
|
||||
script (`scripts/mincut-person-counter.js`) or on Cognitum Seed, and feeds the
|
||||
corrected person count back to the feature vector before ingest.
|
||||
|
||||
### Algorithm
|
||||
|
||||
1. **Sliding window accumulation**: Maintain the last 2 seconds of subcarrier
|
||||
amplitude data (~40 frames at 20 fps). Each frame provides a 64-element
|
||||
amplitude vector (one per subcarrier).
|
||||
|
||||
2. **Pairwise Pearson correlation**: For all subcarrier pairs (i, j), compute
|
||||
the Pearson correlation coefficient over the sliding window:
|
||||
|
||||
```
|
||||
r(i,j) = cov(amp_i, amp_j) / (std(amp_i) * std(amp_j))
|
||||
```
|
||||
|
||||
This produces a 64x64 correlation matrix.
|
||||
|
||||
3. **Graph construction**: Build a weighted undirected graph:
|
||||
- **Nodes** = subcarriers (64 for single-antenna ESP32-S3, up to 128 for dual)
|
||||
- **Edges** = pairs with |r(i,j)| > 0.3 (correlation threshold)
|
||||
- **Weight** = |r(i,j)| (correlation strength)
|
||||
- Discard null subcarriers (amplitude consistently near zero)
|
||||
- Expected: ~1500-2500 edges for 64 active subcarriers
|
||||
|
||||
4. **Iterative Stoer-Wagner min-cut**: Apply the Stoer-Wagner algorithm to find
|
||||
the global minimum cut. If the min-cut weight is below a separation threshold
|
||||
(empirically 2.0), the cut represents a real boundary between independent
|
||||
movers. Split the graph at the cut and recurse on each partition.
|
||||
|
||||
5. **Person count**: The number of partitions after all valid cuts = number of
|
||||
independent movers = person count. A single connected component with high
|
||||
internal correlation and no low-weight cut = 1 person (or 0 if variance is
|
||||
also low).
|
||||
|
||||
6. **Empty room detection**: If the total variance across all subcarriers is
|
||||
below a noise floor threshold, report 0 persons regardless of graph structure.
|
||||
|
||||
### Stoer-Wagner Algorithm
|
||||
|
||||
Stoer-Wagner finds the exact global minimum cut of an undirected weighted graph
|
||||
in O(V * E) time using a sequence of "minimum cut phases":
|
||||
|
||||
```
|
||||
function stoerWagner(G):
|
||||
best_cut = infinity
|
||||
while |V(G)| > 1:
|
||||
(s, t, cut_of_phase) = minimumCutPhase(G)
|
||||
if cut_of_phase < best_cut:
|
||||
best_cut = cut_of_phase
|
||||
best_partition = partition induced by t
|
||||
merge(s, t) // contract vertices s and t
|
||||
return best_cut, best_partition
|
||||
|
||||
function minimumCutPhase(G):
|
||||
A = {arbitrary start vertex}
|
||||
while A != V(G):
|
||||
z = vertex most tightly connected to A
|
||||
// "most tightly connected" = max sum of edge weights to A
|
||||
add z to A
|
||||
s = second-to-last vertex added
|
||||
t = last vertex added (most tightly connected)
|
||||
cut_of_phase = sum of weights of edges incident to t
|
||||
return (s, t, cut_of_phase)
|
||||
```
|
||||
|
||||
For V=64 subcarriers and E~2000 edges, this runs in ~8 million operations,
|
||||
well under 1ms on modern hardware and under 10ms even on ESP32-S3.
|
||||
|
||||
### Integration Points
|
||||
|
||||
```
|
||||
ESP32 Node 1 ──UDP 5006──┐
|
||||
├──> mincut-person-counter.js ──> corrected n_persons
|
||||
ESP32 Node 2 ──UDP 5006──┘ │
|
||||
├──> seed_csi_bridge.py (feature dim 5 override)
|
||||
└──> csi-graph-visualizer.js (debug view)
|
||||
```
|
||||
|
||||
The person counter runs as a standalone Node.js process alongside the existing
|
||||
`rf-scan.js` and `seed_csi_bridge.py` bridge scripts. It can also replay
|
||||
recorded `.csi.jsonl` files for offline analysis.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### 1. Threshold-based peak counting (current, broken)
|
||||
|
||||
Count subcarriers with variance above a threshold, then cluster by proximity.
|
||||
**Problem:** threshold is environment-dependent, miscalibrates easily, and
|
||||
cannot distinguish correlated from independent motion.
|
||||
|
||||
### 2. PCA / spectral clustering on correlation matrix
|
||||
|
||||
Compute eigenvectors of the correlation matrix; the number of large eigenvalues
|
||||
indicates the number of independent sources. **Problem:** requires choosing an
|
||||
eigenvalue gap threshold, which is as fragile as the current variance threshold.
|
||||
Also does not give per-person subcarrier assignments.
|
||||
|
||||
### 3. Min-cut on correlation graph (this ADR)
|
||||
|
||||
**Advantages:**
|
||||
- Directly models the physical structure (Fresnel zone groupings)
|
||||
- Threshold-free person counting (cut weight is a natural separation metric)
|
||||
- Produces per-person subcarrier groups as a side effect
|
||||
- Stoer-Wagner is simple to implement (~100 lines) and runs in polynomial time
|
||||
- Already validated in Rust via `ruvector-mincut` integration
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Graph size | V=64, E~2000 |
|
||||
| Stoer-Wagner complexity | O(V * E) = O(128,000) per cut |
|
||||
| Iterative cuts (max 4) | O(512,000) total |
|
||||
| Wall time (Node.js) | < 5 ms per 2-second window |
|
||||
| Wall time (Rust/WASM) | < 0.5 ms |
|
||||
| Memory | ~32 KB for correlation matrix + graph |
|
||||
| Sliding window | 2 seconds = ~40 frames * 64 subcarriers * 8 bytes = 20 KB |
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Fixes #348: person count now reflects actual independent movers
|
||||
- Robust across environments (no per-room threshold calibration)
|
||||
- Per-person subcarrier groups enable per-person feature extraction
|
||||
- Graph visualization aids debugging and room mapping
|
||||
- Algorithm is well-understood (Stoer-Wagner, 1997)
|
||||
|
||||
### Negative
|
||||
|
||||
- Adds a new process to the sensing pipeline
|
||||
- 2-second latency for person count changes (sliding window)
|
||||
- Correlation-based: cannot detect stationary persons (no motion = no signal)
|
||||
- Assumes independent motion — two people walking in sync may be counted as one
|
||||
|
||||
### Migration
|
||||
|
||||
1. Deploy `scripts/mincut-person-counter.js` alongside existing bridge
|
||||
2. Override feature vector dimension 5 (`n_persons`) with corrected count
|
||||
3. Once validated, port Stoer-Wagner to C for direct ESP32-S3 firmware integration
|
||||
4. Deprecate the fixed-threshold `PersonMatcher` in `sig_mincut_person_match.rs`
|
||||
|
||||
## References
|
||||
|
||||
- Stoer, M. & Wagner, F. (1997). "A Simple Min-Cut Algorithm." JACM 44(4).
|
||||
- `vendor/ruvector/crates/ruvector-mincut/src/algorithm/mod.rs` — DynamicMinCut API
|
||||
- `v2/.../sig_mincut_person_match.rs` — current (broken) WASM edge matcher
|
||||
- `scripts/rf-scan.js` — CSI packet parsing and subcarrier classification
|
||||
@@ -1,259 +0,0 @@
|
||||
# ADR-076: CSI Spectrogram Embeddings via CNN + Graph Transformer
|
||||
|
||||
| Field | Value |
|
||||
|-------------|--------------------------------------------|
|
||||
| **Status** | Proposed |
|
||||
| **Date** | 2026-04-02 |
|
||||
| **Authors** | ruv |
|
||||
| **Depends** | ADR-018 (binary frame), ADR-024 (AETHER contrastive embeddings), ADR-029 (RuvSense), ADR-069 (Cognitum Seed bridge), ADR-073 (multi-frequency mesh scan) |
|
||||
|
||||
## Context
|
||||
|
||||
The current CSI processing pipeline extracts an 8-dimensional hand-crafted feature vector per frame: mean amplitude, amplitude variance, max amplitude, mean phase, phase variance, bandwidth, spectral centroid, and RSSI. These features are effective for basic presence detection and room fingerprinting but discard the rich spatial-frequency structure present in the raw subcarrier data.
|
||||
|
||||
A single CSI frame from an ESP32-S3 contains 64 subcarriers (or 128 in HT40 mode), each with I/Q components. When stacked over time, 20 consecutive frames form a **64x20 subcarrier-by-time matrix** — effectively a grayscale spectrogram image. This matrix encodes:
|
||||
|
||||
1. **Frequency-selective fading** — metal objects create persistent null zones at specific subcarrier indices (visible as dark vertical stripes)
|
||||
2. **Doppler signatures** — human motion produces time-varying amplitude patterns across subcarriers (visible as horizontal wave patterns)
|
||||
3. **Multipath structure** — room geometry creates characteristic interference patterns unique to each environment
|
||||
4. **Activity fingerprints** — walking, sitting, breathing, and falling produce distinct 2D texture patterns in the subcarrier-time matrix
|
||||
|
||||
These 2D structural patterns are invisible to the 8-dim feature vector, which collapses all subcarrier information into scalar statistics. A CNN embedding can preserve this spatial structure.
|
||||
|
||||
### Existing Vendor Libraries
|
||||
|
||||
**@ruvector/cnn** (v0.1.0) provides:
|
||||
- WASM-based CNN feature extraction (~5ms per 224x224 image, ~900KB model)
|
||||
- Configurable embedding dimension (default 512, we use 128 for compact storage)
|
||||
- L2-normalized embeddings with cosine similarity search
|
||||
- Contrastive training via InfoNCE and triplet loss
|
||||
- SIMD-optimized layer operations (batch norm, global average pooling, ReLU)
|
||||
- Works in both Node.js and browser environments
|
||||
|
||||
**ruvector-graph-transformer** provides:
|
||||
- Sublinear O(n log n) graph attention via LSH bucketing and PPR sampling
|
||||
- Proof-gated mutation substrate for verified computations
|
||||
- Temporal causal attention with Granger causality (relevant for CSI time series)
|
||||
- Manifold attention on product spaces S^n x H^m x R^k
|
||||
|
||||
**@ruvector/graph-wasm** (v2.0.2) provides:
|
||||
- Neo4j-compatible property graph database in WASM
|
||||
- Node/edge creation with arbitrary properties and embeddings
|
||||
- Hyperedge support for multi-node relationships
|
||||
- Cypher query language
|
||||
|
||||
### Current Limitations of 8-dim Features
|
||||
|
||||
| Limitation | Impact |
|
||||
|------------|--------|
|
||||
| No subcarrier-level information | Cannot distinguish frequency-selective vs broadband fading |
|
||||
| No temporal pattern encoding | Walking gait (periodic) looks identical to random motion (aperiodic) |
|
||||
| No 2D structure | Room fingerprint reduced to 8 numbers; two rooms with similar statistics are indistinguishable |
|
||||
| No cross-subcarrier correlation | Cannot detect standing waves, node patterns, or multipath clusters |
|
||||
| Poor kNN discrimination | 8 dimensions provides limited hypersphere surface area for separating environments |
|
||||
|
||||
## Decision
|
||||
|
||||
Treat the CSI subcarrier-by-time matrix as a grayscale spectrogram image and apply CNN embedding to produce a 128-dimensional representation that preserves 2D spatial-frequency structure. Use a graph transformer to fuse embeddings across multiple ESP32 nodes.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
ESP32 Node 1 ESP32 Node 2
|
||||
| |
|
||||
v v
|
||||
UDP 5006 UDP 5006
|
||||
| |
|
||||
v v
|
||||
[64 subcarriers] [64 subcarriers]
|
||||
[20-frame window] [20-frame window]
|
||||
| |
|
||||
v v
|
||||
64x20 amplitude 64x20 amplitude
|
||||
matrix (grayscale) matrix (grayscale)
|
||||
| |
|
||||
v v
|
||||
@ruvector/cnn @ruvector/cnn
|
||||
CnnEmbedder CnnEmbedder
|
||||
| |
|
||||
v v
|
||||
128-dim vector 128-dim vector
|
||||
| |
|
||||
+-------+ +----------+
|
||||
| |
|
||||
v v
|
||||
Graph Transformer (2-node graph)
|
||||
Edge weight = cross-node correlation
|
||||
|
|
||||
v
|
||||
Fused 128-dim vector
|
||||
|
|
||||
+-------+-------+
|
||||
| |
|
||||
v v
|
||||
Cognitum Seed kNN Search
|
||||
(128-dim store) (similar rooms)
|
||||
```
|
||||
|
||||
### Step 1: CSI-to-Spectrogram Conversion
|
||||
|
||||
Each ESP32 transmits CSI frames via UDP in ADR-018 binary format. The `iq_hex` field contains I/Q pairs for each subcarrier (2 bytes per subcarrier: I + Q as unsigned 8-bit values).
|
||||
|
||||
```
|
||||
Amplitude[sc] = sqrt(I[sc]^2 + Q[sc]^2)
|
||||
```
|
||||
|
||||
A sliding window of 20 frames produces a 64x20 matrix. Normalization to 0-255 grayscale:
|
||||
|
||||
```
|
||||
pixel[sc][t] = clamp(255 * (amplitude[sc][t] - min) / (max - min), 0, 255)
|
||||
```
|
||||
|
||||
Where `min` and `max` are computed over the entire 64x20 window for per-window contrast normalization. This ensures the CNN sees the relative structure regardless of absolute signal strength (which varies with distance, TX power, and environmental absorption).
|
||||
|
||||
### Step 2: CNN Embedding
|
||||
|
||||
The 64x20 grayscale matrix is resized to the CNN's expected input size (224x224 via nearest-neighbor upsampling, since we want to preserve the discrete subcarrier structure rather than blur it with bilinear interpolation). The input is replicated across 3 channels (RGB) since @ruvector/cnn expects RGB input.
|
||||
|
||||
Configuration:
|
||||
- **Input**: 224x224x3 (upsampled from 64x20, grayscale replicated to RGB)
|
||||
- **Embedding dimension**: 128 (reduced from default 512 for compact storage and faster kNN)
|
||||
- **Normalization**: L2-enabled (cosine similarity = dot product on unit sphere)
|
||||
- **Latency**: ~5ms per window on modern hardware
|
||||
|
||||
The 128-dim embedding encodes the 2D structure of the spectrogram: null zones, Doppler patterns, multipath signatures, and activity textures.
|
||||
|
||||
### Step 3: Graph Transformer for Multi-Node Fusion
|
||||
|
||||
With 2 ESP32 nodes (generalizable to N), we construct a graph:
|
||||
|
||||
```
|
||||
Nodes: {Node_1, Node_2}
|
||||
Edges: {(Node_1, Node_2, weight=cross_correlation)}
|
||||
Node features: 128-dim CNN embedding per node
|
||||
```
|
||||
|
||||
The graph attention mechanism learns which node is more informative for each prediction:
|
||||
|
||||
1. **Query/Key/Value** from each node's 128-dim embedding
|
||||
2. **Edge weight** = Pearson cross-correlation between the two nodes' raw amplitude vectors (captures how much their CSI observations agree)
|
||||
3. **Attention score** = softmax(Q_i * K_j / sqrt(d) + edge_weight_bias)
|
||||
4. **Output** = weighted sum of value vectors
|
||||
|
||||
This produces a fused 128-dim vector that combines both nodes' perspectives, automatically weighting the node with cleaner signal (higher SNR, less fading) more heavily.
|
||||
|
||||
**Generalization to 3+ nodes**: Adding a third ESP32 adds one node and 2 edges to the graph. The attention mechanism handles variable-size graphs without architecture changes.
|
||||
|
||||
### Step 4: Storage and Search
|
||||
|
||||
The fused 128-dim embedding is stored in Cognitum Seed (ADR-069) alongside the existing 8-dim features:
|
||||
|
||||
| Store | Dimension | Content | Use Case |
|
||||
|-------|-----------|---------|----------|
|
||||
| `csi-features` | 8-dim | Hand-crafted statistics | Fast presence detection |
|
||||
| `csi-spectrograms` | 128-dim | CNN spectrogram embedding | Environment fingerprinting, anomaly detection |
|
||||
| `csi-spectrograms-fused` | 128-dim | Graph-fused multi-node embedding | Cross-viewpoint room signature |
|
||||
|
||||
kNN search on the 128-dim store finds past spectrograms that "look like" the current one:
|
||||
- **Environment fingerprinting**: "What room does this RF pattern match?"
|
||||
- **Cross-room transfer**: "Which training room is most similar to this deployment room?"
|
||||
- **Anomaly detection**: Low similarity to all known patterns = unknown environment or novel activity
|
||||
- **Temporal segmentation**: Similarity drops = activity transition boundaries
|
||||
|
||||
### Comparison: 8-dim vs 128-dim vs Combined
|
||||
|
||||
| Property | 8-dim hand-crafted | 128-dim CNN | Combined |
|
||||
|----------|-------------------|-------------|----------|
|
||||
| Subcarrier structure | Lost | Preserved | Both available |
|
||||
| Temporal patterns | Lost | Preserved (20-frame window) | Both |
|
||||
| Computation | ~0.1ms | ~5ms | ~5ms |
|
||||
| Storage per vector | 32 bytes | 512 bytes | 544 bytes |
|
||||
| kNN discrimination | Low (8-dim curse) | High (128-dim surface) | Highest |
|
||||
| Interpretability | High (named features) | Low (learned) | Mixed |
|
||||
| Training required | No | Optional (pre-trained works) | Optional |
|
||||
| Multi-node fusion | Average/max | Graph attention | Graph attention |
|
||||
|
||||
### Contrastive Training (Optional Enhancement)
|
||||
|
||||
The CNN embedding works out-of-the-box with the pre-trained weights. For domain-specific improvements, contrastive training with CSI data:
|
||||
|
||||
1. **Positive pairs**: Same room, different time windows (should embed similarly)
|
||||
2. **Negative pairs**: Different rooms or different activities (should embed differently)
|
||||
3. **Loss**: InfoNCE with temperature 0.07 (standard SimCLR)
|
||||
4. **Augmentation**: Time-shift (slide window by 1-5 frames), subcarrier dropout (zero 10% of rows), amplitude jitter (multiply by uniform [0.8, 1.2])
|
||||
|
||||
This teaches the CNN that "same room at different times" should produce similar embeddings, while "different rooms" should produce different embeddings.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
1. **Richer representation**: 128 dimensions capture 2D structure that 8 dimensions cannot
|
||||
2. **Environment fingerprinting**: kNN on spectrograms can distinguish rooms that look identical in 8-dim feature space
|
||||
3. **Activity detection**: Temporal patterns (gait periodicity, breathing frequency) are encoded in the spectrogram texture
|
||||
4. **Multi-node fusion**: Graph attention automatically weights the most informative node, improving robustness to single-node occlusion or interference
|
||||
5. **Incremental adoption**: 128-dim store operates alongside 8-dim store; no migration needed
|
||||
6. **Browser-compatible**: WASM-based CNN runs in the sensing-server UI for live visualization
|
||||
|
||||
### Negative
|
||||
|
||||
1. **5ms latency per window**: Acceptable for 1.3 Hz update rate (750ms rotation from ADR-073), but constrains real-time applications
|
||||
2. **900KB model download**: One-time cost, cached after first load
|
||||
3. **128-dim storage**: 16x more bytes per vector than 8-dim; mitigated by the fact that we store one embedding per 20-frame window (not per frame)
|
||||
4. **Opaque embeddings**: Unlike named 8-dim features, CNN embeddings are not human-interpretable
|
||||
5. **Input size mismatch**: 64x20 matrix must be upsampled to 224x224; nearest-neighbor preserves structure but wastes computation on padded regions
|
||||
|
||||
### Risks and Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| CNN embeddings not discriminative enough for CSI | Contrastive fine-tuning on CSI spectrograms; fall back to 8-dim if 128-dim kNN recall is worse |
|
||||
| Graph transformer overhead for 2-node graph | Lightweight attention (single head, no MLP); O(1) for 2 nodes |
|
||||
| Upsampling artifacts from 64x20 to 224x224 | Nearest-neighbor preserves discrete structure; consider training a smaller CNN on native 64x20 input |
|
||||
| WASM initialization delay | Call `init()` at server startup, not per-request |
|
||||
|
||||
## Implementation
|
||||
|
||||
### Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `scripts/csi-spectrogram.js` | CSI-to-spectrogram pipeline with CNN embedding, ASCII visualization, Cognitum Seed ingest |
|
||||
| `scripts/mesh-graph-transformer.js` | Multi-node graph attention fusion using @ruvector/graph-wasm |
|
||||
| `docs/adr/ADR-076-csi-spectrogram-embeddings.md` | This ADR |
|
||||
|
||||
### Dependencies
|
||||
|
||||
| Package | Version | Source |
|
||||
|---------|---------|--------|
|
||||
| `@ruvector/cnn` | 0.1.0 | `vendor/ruvector/npm/packages/ruvector-cnn/` |
|
||||
| `@ruvector/graph-wasm` | 2.0.2 | `vendor/ruvector/npm/packages/graph-wasm/` |
|
||||
|
||||
### Data Format
|
||||
|
||||
CSI JSONL frames from `data/recordings/pretrain-1775182186.csi.jsonl`:
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": 1775182186.123,
|
||||
"node_id": 1,
|
||||
"magic": 3289481217,
|
||||
"size": 148,
|
||||
"rssi": -45,
|
||||
"type": "CSI",
|
||||
"iq_hex": "00000f030d030e040d030d030d030c020d020d01...",
|
||||
"subcarriers": 64
|
||||
}
|
||||
```
|
||||
|
||||
`iq_hex` encoding: 2 hex characters per byte, 4 hex characters per subcarrier (I byte + Q byte). Total length = `subcarriers * 4` hex characters.
|
||||
|
||||
## References
|
||||
|
||||
- ADR-018: Binary CSI frame format
|
||||
- ADR-024: AETHER contrastive CSI embeddings (Rust-side)
|
||||
- ADR-029: RuvSense multistatic sensing mode
|
||||
- ADR-069: Cognitum Seed RVF ingest bridge
|
||||
- ADR-073: Multi-frequency mesh scanning
|
||||
- SimCLR: Chen et al., "A Simple Framework for Contrastive Learning of Visual Representations" (2020)
|
||||
- GATv2: Brody et al., "How Attentive are Graph Attention Networks?" (2021)
|
||||
@@ -1,284 +0,0 @@
|
||||
# ADR-077: Novel RF Sensing Applications
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-04-02
|
||||
**Authors:** ruv
|
||||
**Depends on:** ADR-018 (CSI binary protocol), ADR-073 (multifrequency mesh scan), ADR-075 (MinCut person separation), ADR-076 (CSI spectrogram embeddings)
|
||||
|
||||
## Context
|
||||
|
||||
The existing ESP32 CSI + Cognitum Seed infrastructure collects rich multi-modal data:
|
||||
- 2 ESP32-S3 nodes streaming CSI at ~22 fps each (64-128 subcarriers, channel hopping ch 1/3/5/6/9/11)
|
||||
- Vitals extraction: breathing rate, heart rate, motion energy, presence score (1 Hz per node)
|
||||
- 8-dimensional feature vectors per frame
|
||||
- Cognitum Seed with BME280 (temp/humidity/pressure), PIR, reed switch, vibration sensor
|
||||
|
||||
No new hardware is required. All 6 applications below derive novel insights from data already being collected via the ADR-018 binary protocol over UDP port 5006.
|
||||
|
||||
## Decision
|
||||
|
||||
Implement 6 novel RF sensing applications as standalone Node.js scripts that process live UDP or replayed `.csi.jsonl` recordings.
|
||||
|
||||
---
|
||||
|
||||
## Application 1: Sleep Quality Monitoring
|
||||
|
||||
### Input
|
||||
Breathing rate (BR) and heart rate (HR) time series from vitals packets (0xC5110002), sampled at ~1 Hz per node over 6-8 hours.
|
||||
|
||||
### Algorithm
|
||||
Sliding window analysis (5-minute windows, 1-minute stride) classifying sleep stages:
|
||||
|
||||
| Stage | BR (BPM) | BR Variance | HR Pattern | Motion |
|
||||
|-------|----------|-------------|------------|--------|
|
||||
| **Deep (N3)** | 6-12 | Very low (<2.0) | Slow, regular | None |
|
||||
| **Light (N1/N2)** | 12-18 | Moderate (2.0-8.0) | Normal | Minimal |
|
||||
| **REM** | 15-25 | High (>8.0), irregular | Elevated | Eyes only (low CSI motion) |
|
||||
| **Awake** | >18 or <6 | Any | Variable | Moderate-high |
|
||||
|
||||
Each 5-minute window is scored by:
|
||||
1. Compute BR mean and variance within the window
|
||||
2. Compute HR mean and coefficient of variation (CV)
|
||||
3. Compute motion energy mean (from vitals `motion_energy` field)
|
||||
4. Classify stage using threshold hierarchy: Awake > REM > Light > Deep
|
||||
|
||||
### Output
|
||||
- Real-time sleep stage classification
|
||||
- ASCII hypnogram (time vs. stage)
|
||||
- Summary: total sleep time, sleep efficiency (TST / time in bed), time per stage
|
||||
- Optional JSON for health app integration
|
||||
|
||||
### Validation
|
||||
Overnight recording (`overnight-1775217646.csi.jsonl`, 113k frames, ~40 min) should show:
|
||||
- Transition from active (awake) to resting states
|
||||
- Decreased motion energy over time
|
||||
- BR stabilization in sleeping segments
|
||||
|
||||
### Clinical Relevance
|
||||
Consumer-grade sleep tracking without wearables. RF-based sensing avoids compliance issues (forgotten wristbands, dead batteries). Not diagnostic; informational only.
|
||||
|
||||
---
|
||||
|
||||
## Application 2: Breathing Disorder Screening (Apnea Detection)
|
||||
|
||||
### Input
|
||||
Breathing rate time series from vitals packets at ~1 Hz.
|
||||
|
||||
### Algorithm
|
||||
Detect respiratory events in the BR time series:
|
||||
|
||||
| Event | Definition | Duration |
|
||||
|-------|-----------|----------|
|
||||
| **Apnea** | BR drops below 3 BPM (effective cessation) | >= 10 seconds |
|
||||
| **Hypopnea** | BR drops > 50% from 5-min rolling baseline | >= 10 seconds |
|
||||
|
||||
Scoring:
|
||||
1. Maintain 5-minute rolling baseline BR (exponential moving average)
|
||||
2. Flag apnea when BR < 3 BPM for >= 10 consecutive seconds
|
||||
3. Flag hypopnea when BR < 50% of baseline for >= 10 consecutive seconds
|
||||
4. Compute AHI (Apnea-Hypopnea Index) = total events / hours monitored
|
||||
|
||||
| AHI | Severity |
|
||||
|-----|----------|
|
||||
| < 5 | Normal |
|
||||
| 5-15 | Mild |
|
||||
| 15-30 | Moderate |
|
||||
| > 30 | Severe |
|
||||
|
||||
### Output
|
||||
- Per-event log: type (apnea/hypopnea), start time, duration, BR during event
|
||||
- Hourly AHI and overall AHI
|
||||
- Severity classification
|
||||
- Alert on severe events (consecutive apneas > 30s)
|
||||
|
||||
### Clinical Relevance
|
||||
Pre-screening tool for obstructive sleep apnea (OSA). Provides motivation for clinical polysomnography referral. Not a diagnostic device; informational pre-screen only.
|
||||
|
||||
---
|
||||
|
||||
## Application 3: Emotional State / Stress Detection
|
||||
|
||||
### Input
|
||||
Heart rate time series from vitals packets at ~1 Hz.
|
||||
|
||||
### Algorithm
|
||||
Heart Rate Variability (HRV) analysis:
|
||||
|
||||
1. **RMSSD** (Root Mean Square of Successive Differences):
|
||||
- Compute successive HR differences within 5-minute windows
|
||||
- RMSSD = sqrt(mean(diff^2))
|
||||
- High RMSSD = high vagal tone = relaxed
|
||||
- Low RMSSD = sympathetic dominance = stressed
|
||||
|
||||
2. **LF/HF Ratio** (via FFT on 5-minute HR windows):
|
||||
- LF band: 0.04-0.15 Hz (sympathetic + parasympathetic)
|
||||
- HF band: 0.15-0.40 Hz (parasympathetic)
|
||||
- High LF/HF (> 2.0) = stressed
|
||||
- Low LF/HF (< 1.0) = relaxed
|
||||
|
||||
3. **Stress Score** (0-100):
|
||||
- `score = 50 * (1 - RMSSD_norm) + 50 * LF_HF_norm`
|
||||
- Where `RMSSD_norm` = RMSSD / max_expected_RMSSD (capped at 1.0)
|
||||
- And `LF_HF_norm` = min(LF_HF / 4.0, 1.0)
|
||||
|
||||
### Output
|
||||
- Real-time stress score (0-100)
|
||||
- RMSSD and LF/HF ratio per window
|
||||
- ASCII trend chart over hours
|
||||
- Activity context correlation (motion level vs. stress)
|
||||
|
||||
### Validation
|
||||
- Periods of activity (walking, working) should correlate with higher stress scores
|
||||
- Quiet rest should show lower scores
|
||||
- Sleeping should show lowest scores (high HRV, low LF/HF)
|
||||
|
||||
---
|
||||
|
||||
## Application 4: Gait Analysis / Movement Disorder Detection
|
||||
|
||||
### Input
|
||||
- Motion energy time series from vitals packets
|
||||
- CSI phase variance from raw CSI frames (0xC5110001)
|
||||
- Cross-node RSSI from vitals packets
|
||||
|
||||
### Algorithm
|
||||
|
||||
1. **Cadence Extraction**: FFT on motion_energy within 5-second sliding windows
|
||||
- Walking cadence: dominant frequency 0.8-2.0 Hz (normal: ~1.0 Hz = 120 steps/min)
|
||||
- Running: > 2.0 Hz
|
||||
- Stationary: no dominant peak
|
||||
|
||||
2. **Stride Regularity**: Autocorrelation of motion_energy
|
||||
- Regular walking: strong autocorrelation peak at step period
|
||||
- Irregularity score = 1 - (peak_height / baseline)
|
||||
|
||||
3. **Asymmetry Detection**: Compare motion energy oscillation between two ESP32 nodes
|
||||
- Symmetric gait: both nodes see similar oscillation period and amplitude
|
||||
- Asymmetry index = |period_node1 - period_node2| / mean_period
|
||||
|
||||
4. **Tremor Detection**: High-frequency phase variance analysis
|
||||
- Compute phase variance per subcarrier in 2-second windows
|
||||
- Tremor band: 3-8 Hz component in phase variance time series
|
||||
- Parkinsonian tremor: 4-6 Hz, resting
|
||||
- Essential tremor: 5-8 Hz, action
|
||||
|
||||
### Output
|
||||
- Cadence (steps/min)
|
||||
- Stride regularity score (0-1)
|
||||
- Asymmetry index (0 = symmetric, 1 = highly asymmetric)
|
||||
- Tremor score and dominant frequency
|
||||
- Walking vs. stationary classification
|
||||
|
||||
### Validation
|
||||
Overnight data should show clear stationary periods with no cadence detected. Any walking segments should show cadence in the 0.8-2.0 Hz range.
|
||||
|
||||
---
|
||||
|
||||
## Application 5: Material/Object Change Detection
|
||||
|
||||
### Input
|
||||
Per-subcarrier amplitude from raw CSI frames (0xC5110001).
|
||||
|
||||
### Algorithm
|
||||
|
||||
1. **Baseline Establishment** (first 10 minutes or configurable):
|
||||
- Record mean amplitude per subcarrier (Welford online mean)
|
||||
- Record null pattern: which subcarriers are below null threshold (amplitude < 2.0)
|
||||
|
||||
2. **Change Detection** (sliding 30-second windows):
|
||||
- Compare current null pattern to baseline
|
||||
- New nulls appearing = new metal object blocking RF path
|
||||
- Existing nulls disappearing = metal object removed
|
||||
- Null position shifted = object moved
|
||||
- Amplitude change without null change = non-metal material (wood, water, glass)
|
||||
|
||||
3. **Material Classification** heuristic:
|
||||
- Metal: sharp null (amplitude drops to near 0 on specific subcarriers)
|
||||
- Water/human: broad amplitude reduction across many subcarriers
|
||||
- Wood/plastic: minimal amplitude change, mostly phase shift
|
||||
- Glass: frequency-selective (affects higher subcarriers more)
|
||||
|
||||
### Output
|
||||
- Change events with timestamp, type (add/remove/move), affected subcarrier range
|
||||
- Estimated material category
|
||||
- Null pattern delta visualization (ASCII)
|
||||
- Event timeline for monitoring
|
||||
|
||||
### Validation
|
||||
Overnight data has 19% null baseline. Changes in null pattern over the recording period indicate environment changes (doors opening/closing, person entering/leaving).
|
||||
|
||||
---
|
||||
|
||||
## Application 6: Room Environment Fingerprinting
|
||||
|
||||
### Input
|
||||
- 8-dimensional feature vectors from feature packets (0xC5110003)
|
||||
- Motion energy and presence score from vitals packets
|
||||
|
||||
### Algorithm
|
||||
|
||||
1. **Online Clustering** using running k-means (k=5, updateable centroids):
|
||||
- Each incoming 8-dim feature vector is assigned to nearest centroid
|
||||
- Centroid updated via exponential moving average (alpha=0.01)
|
||||
- New cluster created if distance to all centroids exceeds threshold
|
||||
|
||||
2. **State Labeling** (heuristic from vitals correlation):
|
||||
- Cluster with lowest motion_energy = "empty/sleeping"
|
||||
- Cluster with highest motion_energy = "active/walking"
|
||||
- Intermediate clusters = "resting", "working", "transitional"
|
||||
|
||||
3. **Transition Tracking**:
|
||||
- Build state transition matrix (from_state -> to_state counts)
|
||||
- Detect anomalous transitions (rare in historical data)
|
||||
|
||||
4. **Daily Profile**:
|
||||
- Aggregate state durations per hour
|
||||
- Compare across days for routine detection
|
||||
|
||||
### Output
|
||||
- Current room state and confidence
|
||||
- State timeline (ASCII)
|
||||
- Transition matrix
|
||||
- Daily pattern profile
|
||||
- Anomaly score (deviation from established daily pattern)
|
||||
|
||||
### Validation
|
||||
Overnight recording should show 2-3 stable clusters corresponding to activity periods at different times. Transitions should be infrequent and correspond to real behavioral changes.
|
||||
|
||||
---
|
||||
|
||||
## Implementation
|
||||
|
||||
All scripts share common infrastructure:
|
||||
- ADR-018 binary packet parsing (same as rf-scan.js, mincut-person-counter.js)
|
||||
- JSONL replay via readline interface
|
||||
- Live UDP via dgram
|
||||
- Pure Node.js, no external dependencies
|
||||
- CLI: `--replay <file>` for offline, `--port <N>` for live, `--json` for programmatic output
|
||||
|
||||
| Script | Primary Packets | Key Algorithm |
|
||||
|--------|----------------|---------------|
|
||||
| `sleep-monitor.js` | vitals (0xC5110002) | BR/HR window classification |
|
||||
| `apnea-detector.js` | vitals (0xC5110002) | BR pause detection, AHI scoring |
|
||||
| `stress-monitor.js` | vitals (0xC5110002) | HRV RMSSD + FFT LF/HF |
|
||||
| `gait-analyzer.js` | vitals + raw CSI | FFT cadence + phase tremor |
|
||||
| `material-detector.js` | raw CSI (0xC5110001) | Null pattern baseline + delta |
|
||||
| `room-fingerprint.js` | feature (0xC5110003) + vitals | Online k-means clustering |
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- 6 new sensing applications from existing hardware (zero additional cost)
|
||||
- All offline-capable via JSONL replay (no live hardware needed for development)
|
||||
- Pure JS, no native dependencies, runs on any platform with Node.js
|
||||
- Each script is standalone and composable
|
||||
|
||||
### Negative
|
||||
- Vitals accuracy depends on ESP32 CSI quality (RSSI, multipath)
|
||||
- HRV analysis at 1 Hz HR sampling is coarse compared to ECG
|
||||
- Material classification is heuristic, not definitive
|
||||
- Sleep staging without EEG is approximate (consumer-grade accuracy)
|
||||
|
||||
### Risks
|
||||
- Users may misinterpret health-related outputs as clinical diagnoses
|
||||
- Mitigation: all scripts include disclaimers in output headers
|
||||
@@ -1,354 +0,0 @@
|
||||
# ADR-078: Multi-Frequency Mesh Sensing Applications
|
||||
|
||||
| Field | Value |
|
||||
|-------------|--------------------------------------------|
|
||||
| **Status** | Proposed |
|
||||
| **Date** | 2026-04-02 |
|
||||
| **Authors** | ruv |
|
||||
| **Depends** | ADR-018 (binary frame), ADR-029 (channel hopping), ADR-073 (multi-frequency mesh scan) |
|
||||
|
||||
## Context
|
||||
|
||||
ADR-073 established multi-frequency mesh scanning: 2 ESP32-S3 nodes hopping across 6 WiFi channels (1, 3, 5, 6, 9, 11) with 9 neighbor WiFi networks as passive illuminators. This ADR defines 5 sensing applications that are **unique to multi-frequency mesh scanning** and impossible with single-channel WiFi sensing.
|
||||
|
||||
### Why Multi-Frequency is Required
|
||||
|
||||
Single-channel WiFi sensing captures CSI on one frequency (e.g., channel 5 at 2432 MHz). This provides amplitude and phase across ~52-64 OFDM subcarriers within a 20 MHz bandwidth. Multi-frequency mesh scanning extends this to 6 channels spanning 2412-2462 MHz (50 MHz total), with each channel providing independent multipath observations. The applications below exploit the frequency dimension that single-channel sensing cannot access.
|
||||
|
||||
### Available Infrastructure
|
||||
|
||||
| Resource | Detail |
|
||||
|----------|--------|
|
||||
| Node 1 (COM7) | ESP32-S3, channels 1, 6, 11 (non-overlapping), 200ms dwell |
|
||||
| Node 2 | ESP32-S3, channels 3, 5, 9 (interleaved, near neighbor APs), 200ms dwell |
|
||||
| Neighbor APs | 9 networks across channels 3, 5, 6, 9, 11 |
|
||||
| Data transport | UDP port 5006, ADR-018 binary format |
|
||||
| Recorded data | `data/recordings/overnight-*.csi.jsonl` |
|
||||
|
||||
### Neighbor AP Illuminator Table
|
||||
|
||||
| SSID | Channel | Freq (MHz) | Signal (%) | Role |
|
||||
|------|---------|------------|------------|------|
|
||||
| ruv.net | 5 | 2432 | 100 | Primary illuminator |
|
||||
| Cohen-Guest | 5 | 2432 | 100 | Co-channel illuminator |
|
||||
| COGECO-21B20 | 11 | 2462 | 100 | High-freq illuminator |
|
||||
| HP M255 LaserJet | 5 | 2432 | 94 | Device fingerprinting target |
|
||||
| conclusion mesh | 3 | 2422 | 44 | Low-freq illuminator |
|
||||
| NETGEAR72 | 9 | 2452 | 42 | Mid-high illuminator |
|
||||
| NETGEAR72-Guest | 9 | 2452 | 42 | Co-channel illuminator |
|
||||
| COGECO-4321 | 11 | 2462 | 30 | Weak high-freq illuminator |
|
||||
| Innanen | 6 | 2437 | 19 | Weak center-band illuminator |
|
||||
|
||||
## Decision
|
||||
|
||||
Implement 5 multi-frequency-specific sensing applications, each as a standalone Node.js script in `scripts/`.
|
||||
|
||||
---
|
||||
|
||||
## Application 1: RF Tomographic Imaging
|
||||
|
||||
### Principle
|
||||
|
||||
Each WiFi channel "sees" through the room differently because multipath interference patterns are frequency-dependent. A 2 cm path length difference produces a null at 2432 MHz but constructive interference at 2412 MHz. With 6 channels x 2 nodes, we have 12 independent RF path observations through the room.
|
||||
|
||||
RF tomography back-projects attenuation along each transmitter-receiver path. Where paths overlap with high attenuation, there is an absorbing object (person, furniture, wall). Where paths show low attenuation, the space is clear.
|
||||
|
||||
### Algorithm
|
||||
|
||||
```
|
||||
For each CSI frame:
|
||||
1. Compute path attenuation = RSSI_free_space - RSSI_measured
|
||||
2. For each cell in a 10x10 room grid:
|
||||
a. Compute the cell's distance to the TX->RX line (perpendicular distance)
|
||||
b. Weight contribution by 1/distance (cells near the path contribute more)
|
||||
3. Accumulate weighted attenuation across all frames, channels, and node pairs
|
||||
4. Normalize: cells with high accumulated attenuation = absorbers (people/objects)
|
||||
```
|
||||
|
||||
Uses the Algebraic Reconstruction Technique (ART) for iterative refinement, or simple backprojection for real-time display.
|
||||
|
||||
### Resolution
|
||||
|
||||
- Theoretical: ~lambda/2 = 6 cm (at 2.4 GHz)
|
||||
- Practical with 2 nodes: ~20 cm (limited by node geometry)
|
||||
- Frequency diversity gain: sqrt(6) improvement over single-channel = ~2.4x
|
||||
|
||||
### Why Single-Channel Cannot Do This
|
||||
|
||||
Single-channel provides only 1 frequency observation per path. Frequency-selective fading means a single channel may show zero attenuation through a person (if the path happens to be at a constructive interference point). Multiple channels provide independent attenuation measurements through the same spatial path, enabling reliable detection.
|
||||
|
||||
### Script
|
||||
|
||||
`scripts/rf-tomography.js`
|
||||
|
||||
---
|
||||
|
||||
## Application 2: Passive Bistatic Radar
|
||||
|
||||
### Principle
|
||||
|
||||
Neighbor WiFi APs transmit continuously and uncontrollably. The ESP32 nodes capture CSI from these transmissions, which includes phase and amplitude modulated by objects in the room. Each neighbor AP acts as a free "illuminator of opportunity" at a known position and frequency.
|
||||
|
||||
This is the same principle used by military passive radar systems (e.g., the Ukrainian Kolchuga, Czech VERA-NG) that use FM radio and TV transmitters to detect aircraft without emitting any signals themselves. Here we use WiFi APs instead of broadcast towers, and detect people instead of aircraft.
|
||||
|
||||
### Algorithm
|
||||
|
||||
```
|
||||
For each neighbor AP (identified by BSSID/channel):
|
||||
1. Track CSI phase progression across consecutive frames
|
||||
2. Compute Doppler shift: fd = d(phase)/dt / (2*pi)
|
||||
- Positive Doppler = target moving toward the AP
|
||||
- Negative Doppler = target moving away
|
||||
3. Compute range from subcarrier phase slope:
|
||||
- tau = d(phase)/d(subcarrier_freq) / (2*pi)
|
||||
- range = c * tau (where c = speed of light)
|
||||
4. Build range-Doppler map per AP
|
||||
5. Fuse multi-static detections:
|
||||
- Each AP provides a range ellipse (locus of constant TX->target->RX delay)
|
||||
- Intersection of 3+ ellipses = target position
|
||||
```
|
||||
|
||||
### Multi-Static Geometry
|
||||
|
||||
With 3+ neighbor APs as transmitters and 2 ESP32 receivers, we have 6+ bistatic pairs. Each pair constrains the target to an ellipse. The intersection provides 2D position.
|
||||
|
||||
```
|
||||
AP1 (ch5) AP2 (ch11)
|
||||
\ /
|
||||
\ TARGET /
|
||||
\ /|\ /
|
||||
\ / | \ /
|
||||
ESP32_1 ---*--+--*--- ESP32_2
|
||||
/ \ | / \
|
||||
/ \|/ \
|
||||
/ TARGET \
|
||||
/ \
|
||||
AP3 (ch3) AP4 (ch9)
|
||||
```
|
||||
|
||||
### Why Single-Channel Cannot Do This
|
||||
|
||||
Single-channel only captures CSI from APs on that one channel. With channel 5, you see ruv.net and Cohen-Guest, but miss COGECO-21B20 (ch11), conclusion mesh (ch3), NETGEAR72 (ch9). Multi-frequency scanning captures illumination from all 9 APs across 6 channels, providing the geometric diversity needed for position triangulation.
|
||||
|
||||
### Script
|
||||
|
||||
`scripts/passive-radar.js`
|
||||
|
||||
---
|
||||
|
||||
## Application 3: Frequency-Selective Material Classification
|
||||
|
||||
### Principle
|
||||
|
||||
Different materials interact with 2.4 GHz WiFi signals differently, and critically, their absorption/reflection varies with frequency:
|
||||
|
||||
| Material | Attenuation Pattern | Frequency Dependence |
|
||||
|----------|--------------------|--------------------|
|
||||
| Metal | Total reflection, deep null | Frequency-flat (blocks all equally) |
|
||||
| Water/Human body | Strong absorption | Increases with frequency (dielectric loss ~ f^2) |
|
||||
| Wood | Mild attenuation | Increases with frequency (moisture content) |
|
||||
| Glass | Low attenuation | Nearly frequency-flat |
|
||||
| Drywall | Low-moderate attenuation | Slight frequency dependence |
|
||||
| Concrete | Moderate-high attenuation | Increases with frequency |
|
||||
|
||||
### Algorithm
|
||||
|
||||
```
|
||||
For each subcarrier index i across all channels:
|
||||
1. Measure attenuation A(i, ch) on each channel
|
||||
2. Compute frequency selectivity:
|
||||
- Flat ratio = std(A across channels) / mean(A across channels)
|
||||
- Slope = linear regression of A vs frequency
|
||||
3. Classify:
|
||||
- Flat ratio < 0.1 AND high attenuation -> Metal
|
||||
- Flat ratio < 0.1 AND low attenuation -> Glass/Air
|
||||
- Positive slope (A increases with freq) AND high A -> Water/Human
|
||||
- Positive slope AND moderate A -> Wood
|
||||
- High variance across channels -> Complex scatterer
|
||||
```
|
||||
|
||||
### Physics Basis
|
||||
|
||||
At 2.4 GHz, water's complex permittivity is epsilon_r = 77 - j10. The imaginary component (loss) increases with frequency within the WiFi band. Metal is a perfect conductor regardless of frequency. Glass (epsilon_r ~ 6 - j0.1) has negligible loss at all WiFi frequencies.
|
||||
|
||||
The 50 MHz span (2412-2462 MHz) is only ~2% of the carrier frequency, but this is sufficient to detect the frequency-dependent absorption signature of water-bearing materials (human body, wet wood, potted plants) versus frequency-flat materials (metal, glass).
|
||||
|
||||
### Why Single-Channel Cannot Do This
|
||||
|
||||
Material classification requires measuring how attenuation varies with frequency. A single channel provides only one frequency point -- there is no frequency axis to measure against. Multi-frequency scanning provides 6 frequency points spanning 50 MHz, enabling slope and variance computation.
|
||||
|
||||
### Script
|
||||
|
||||
`scripts/material-classifier.js`
|
||||
|
||||
---
|
||||
|
||||
## Application 4: Through-Wall Motion Detection
|
||||
|
||||
### Principle
|
||||
|
||||
Lower WiFi frequencies penetrate walls better than higher frequencies. At 2.4 GHz, wall attenuation for a standard drywall+stud partition is approximately:
|
||||
|
||||
| Channel | Freq (MHz) | Drywall Loss (dB) | Concrete Loss (dB) |
|
||||
|---------|------------|-------------------|-------------------|
|
||||
| 1 | 2412 | 2.5 | 8.0 |
|
||||
| 6 | 2437 | 2.6 | 8.3 |
|
||||
| 11 | 2462 | 2.7 | 8.6 |
|
||||
|
||||
The absolute differences are small (~0.2 dB), but with 6 channels we can:
|
||||
|
||||
1. **Baseline the wall's frequency-dependent attenuation profile** during a calibration period (no one behind the wall)
|
||||
2. **Detect changes above baseline** that indicate motion behind the wall
|
||||
3. **Weight lower channels more heavily** since they have better through-wall SNR
|
||||
4. **Cross-validate** across channels: real through-wall motion appears on all channels (with frequency-dependent amplitude), while interference/noise typically appears on only one channel
|
||||
|
||||
### Algorithm
|
||||
|
||||
```
|
||||
Calibration phase (60 seconds, no motion behind wall):
|
||||
For each channel ch:
|
||||
baseline_mean[ch] = mean(CSI amplitude over calibration)
|
||||
baseline_std[ch] = std(CSI amplitude over calibration)
|
||||
|
||||
Detection phase:
|
||||
For each frame on channel ch:
|
||||
1. Compute deviation = |current_amplitude - baseline_mean[ch]| / baseline_std[ch]
|
||||
2. Channel weight = f(penetration_quality[ch])
|
||||
3. Per-channel score = deviation * weight
|
||||
|
||||
Fused score = weighted sum across channels
|
||||
Alert if fused_score > threshold for N consecutive frames
|
||||
```
|
||||
|
||||
### Why Single-Channel Cannot Do This
|
||||
|
||||
Single-channel through-wall detection suffers from high false-positive rates because it cannot distinguish wall effects from motion. With multi-frequency, we can:
|
||||
|
||||
1. Characterize the wall's frequency response during calibration
|
||||
2. Subtract the wall effect per channel
|
||||
3. Cross-validate detections across channels (real motion is coherent across frequencies; noise is not)
|
||||
|
||||
The frequency diversity provides a ~2.4x improvement in detection SNR (sqrt(6) independent observations).
|
||||
|
||||
### Script
|
||||
|
||||
`scripts/through-wall-detector.js`
|
||||
|
||||
---
|
||||
|
||||
## Application 5: Device Fingerprinting via RF Emissions
|
||||
|
||||
### Principle
|
||||
|
||||
Every electronic device has unique RF characteristics visible in the WiFi spectrum. When a device transmits (or even when its internal oscillators radiate EMI), it modulates nearby WiFi signals in device-specific ways:
|
||||
|
||||
- **WiFi APs**: each AP has unique transmit power, phase noise, and clock drift characteristics
|
||||
- **Printers**: the HP M255 LaserJet creates specific subcarrier patterns when printing (motor EMI)
|
||||
- **Microwave ovens**: 2.45 GHz magnetron radiates across channels 8-11, creating distinctive wideband interference
|
||||
- **Bluetooth devices**: 2.4 GHz frequency-hopping creates transient spikes across channels
|
||||
|
||||
### Algorithm
|
||||
|
||||
```
|
||||
Learning phase:
|
||||
For each known device (from WiFi scan SSID/BSSID correlation):
|
||||
1. Record CSI patterns when device is active vs inactive
|
||||
2. Compute per-channel signature:
|
||||
- Mean amplitude profile across subcarriers
|
||||
- Variance profile (active devices increase variance on specific subcarriers)
|
||||
- Phase noise characteristics
|
||||
3. Store signature as device fingerprint
|
||||
|
||||
Detection phase:
|
||||
For each analysis window:
|
||||
1. Compute current CSI profile per channel
|
||||
2. Correlate against stored fingerprints
|
||||
3. Report device activity: "HP printer active (confidence 0.87)"
|
||||
```
|
||||
|
||||
### Multi-Frequency Advantage
|
||||
|
||||
Different devices affect different channels:
|
||||
|
||||
- HP printer (ch5): affects subcarriers 20-40 on channel 5 during print jobs
|
||||
- NETGEAR72 router (ch9): creates clock-drift correlated phase patterns on channel 9
|
||||
- Microwave: broadband interference strongest on channels 9-11
|
||||
|
||||
Single-channel sensing only sees devices that affect that one channel. Multi-frequency scanning observes the full 2412-2462 MHz band, detecting device activity regardless of which channel the device operates on.
|
||||
|
||||
### Script
|
||||
|
||||
`scripts/device-fingerprint.js`
|
||||
|
||||
---
|
||||
|
||||
## Implementation
|
||||
|
||||
### Shared Infrastructure
|
||||
|
||||
All 5 scripts share common infrastructure:
|
||||
|
||||
| Component | Detail |
|
||||
|-----------|--------|
|
||||
| Packet format | ADR-018 binary (UDP) or .csi.jsonl (replay) |
|
||||
| IQ parsing | `parseIqHex()` for JSONL, `parseCSIFrame()` for binary UDP |
|
||||
| Channel assignment | From binary freq field, or simulated round-robin for legacy JSONL |
|
||||
| Node positions | Configurable, default: Node 1 at (0,0), Node 2 at (3,0) meters |
|
||||
| Visualization | ASCII Unicode block characters and box drawing |
|
||||
|
||||
### Scripts
|
||||
|
||||
| Script | Application | Lines | Key Algorithm |
|
||||
|--------|------------|-------|---------------|
|
||||
| `scripts/rf-tomography.js` | RF Tomographic Imaging | ~500 | ART backprojection |
|
||||
| `scripts/passive-radar.js` | Passive Bistatic Radar | ~500 | Range-Doppler + multi-static fusion |
|
||||
| `scripts/material-classifier.js` | Material Classification | ~450 | Frequency-selective attenuation analysis |
|
||||
| `scripts/through-wall-detector.js` | Through-Wall Detection | ~400 | Baselined multi-channel anomaly detection |
|
||||
| `scripts/device-fingerprint.js` | Device Fingerprinting | ~450 | Per-channel signature correlation |
|
||||
|
||||
### Data Requirements
|
||||
|
||||
- **Live mode**: UDP port 5006, 2 ESP32 nodes channel-hopping per ADR-073
|
||||
- **Replay mode**: `--replay <file.csi.jsonl>` with overnight recordings
|
||||
- **Calibration**: through-wall detector requires 60s calibration with `--calibrate`
|
||||
|
||||
## Performance Targets
|
||||
|
||||
| Application | Latency | Update Rate | Accuracy Target |
|
||||
|-------------|---------|-------------|-----------------|
|
||||
| RF Tomography | <100ms per frame | 1 Hz image update | 20 cm spatial resolution |
|
||||
| Passive Radar | <200ms per frame | 2 Hz range-Doppler | 1 m range, 0.1 m/s velocity |
|
||||
| Material Classification | <500ms per window | 0.5 Hz classification | 70% correct material ID |
|
||||
| Through-Wall Detection | <100ms per frame | 2 Hz detection | 90% true positive, <10% false positive |
|
||||
| Device Fingerprinting | <1s per window | 0.2 Hz activity update | 80% correct device ID |
|
||||
|
||||
## Risks
|
||||
|
||||
### Limited Frequency Span
|
||||
|
||||
The 50 MHz span (2412-2462 MHz) is only 2% of the carrier frequency. Material classification accuracy depends on the attenuation slope being measurable within this narrow range. Mitigation: use long averaging windows (5-10 seconds) to improve SNR of frequency-dependent measurements.
|
||||
|
||||
### Node Geometry
|
||||
|
||||
2 nodes provide limited spatial diversity for tomographic imaging. The backprojection is essentially 1D along the node-to-node axis, with poor resolution perpendicular to it. Mitigation: neighbor APs provide additional geometric diversity for passive radar mode.
|
||||
|
||||
### Legacy Data Compatibility
|
||||
|
||||
Overnight recordings (`data/recordings/overnight-*.csi.jsonl`) were captured before multi-frequency scanning was deployed and lack channel/frequency fields. Scripts simulate channel assignment for replay. Full multi-frequency data requires re-recording with channel hopping enabled.
|
||||
|
||||
### Phase Calibration
|
||||
|
||||
Passive radar requires accurate phase tracking across consecutive frames. ESP32 CSI phase includes a random offset per channel hop that must be removed. Mitigation: use phase-difference between consecutive frames rather than absolute phase.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
1. **5 GHz multi-frequency**: rejected -- no 5 GHz APs visible in environment, no free illuminators.
|
||||
2. **UWB (ultra-wideband)**: rejected -- ESP32-S3 does not support UWB. Would require additional hardware (DW1000/DW3000 modules).
|
||||
3. **Dedicated radar hardware**: rejected -- multi-frequency WiFi sensing achieves similar capabilities using existing infrastructure at zero additional cost.
|
||||
|
||||
## References
|
||||
|
||||
- Wilson, J. & Patwari, N. (2010). "Radio Tomographic Imaging with Wireless Networks." IEEE Trans. Mobile Computing.
|
||||
- Colone, F. et al. (2012). "WiFi-Based Passive Bistatic Radar: Data Processing Schemes and Experimental Results." IEEE Trans. Aerospace and Electronic Systems.
|
||||
- Adib, F. & Katabi, D. (2013). "See Through Walls with WiFi!" ACM SIGCOMM.
|
||||
- Banerjee, A. et al. (2014). "RF-based material identification using WiFi signals." ACM MobiCom.
|
||||
@@ -1,512 +0,0 @@
|
||||
# ADR-079: Camera Ground-Truth Training Pipeline
|
||||
|
||||
- **Status**: Accepted
|
||||
- **Date**: 2026-04-06
|
||||
- **Deciders**: ruv
|
||||
- **Relates to**: ADR-072 (WiFlow Architecture), ADR-070 (Self-Supervised Pretraining), ADR-071 (ruvllm Training Pipeline), ADR-024 (AETHER Contrastive), ADR-064 (Multimodal Ambient Intelligence), ADR-075 (MinCut Person Separation)
|
||||
|
||||
## Context
|
||||
|
||||
WiFlow (ADR-072) currently trains without ground-truth pose labels, using proxy poses
|
||||
generated from presence/motion heuristics. This produces a PCK@20 of only 2.5% — far
|
||||
below the 30-50% achievable with supervised training. The fundamental bottleneck is the
|
||||
absence of spatial keypoint labels.
|
||||
|
||||
Academic WiFi pose estimation systems (Wi-Pose, Person-in-WiFi 3D, MetaFi++) all train
|
||||
with synchronized camera ground truth and achieve PCK@20 of 40-85%. They discard the
|
||||
camera at deployment — the camera is a training-time teacher, not a runtime dependency.
|
||||
|
||||
ADR-064 already identified this: *"Record CSI + mmWave while performing signs with a
|
||||
camera as ground truth, then deploy camera-free."* This ADR specifies the implementation.
|
||||
|
||||
### Current Training Pipeline Gap
|
||||
|
||||
```
|
||||
Current: CSI amplitude → WiFlow → 17 keypoints (proxy-supervised, PCK@20 = 2.5%)
|
||||
↑
|
||||
Heuristic proxies:
|
||||
- Standing skeleton when presence > 0.3
|
||||
- Limb perturbation from motion energy
|
||||
- No spatial accuracy
|
||||
```
|
||||
|
||||
### Target Pipeline
|
||||
|
||||
```
|
||||
Training: CSI amplitude ──→ WiFlow ──→ 17 keypoints (camera-supervised, PCK@20 target: 35%+)
|
||||
↑
|
||||
Laptop camera ──→ MediaPipe ──→ 17 COCO keypoints (ground truth)
|
||||
(time-synchronized, 30 fps)
|
||||
|
||||
Deploy: CSI amplitude ──→ WiFlow ──→ 17 keypoints (camera-free, trained model only)
|
||||
```
|
||||
|
||||
## Decision
|
||||
|
||||
Build a camera ground-truth collection and training pipeline using the laptop webcam
|
||||
as a teacher signal. The camera is used **only during training data collection** and is
|
||||
not required at deployment.
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Data Collection Phase │
|
||||
│ │
|
||||
│ ESP32-S3 nodes ──UDP──→ Sensing Server ──→ CSI frames (.jsonl) │
|
||||
│ ↑ time sync │
|
||||
│ Laptop Camera ──→ MediaPipe Pose ──→ Keypoints (.jsonl) │
|
||||
│ ↑ │
|
||||
│ collect-ground-truth.py │
|
||||
│ (single orchestrator) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Training Phase │
|
||||
│ │
|
||||
│ Paired dataset: { csi_window[128,20], keypoints[17,2], conf } │
|
||||
│ ↓ │
|
||||
│ train-wiflow-supervised.js │
|
||||
│ Phase 1: Contrastive pretrain (ADR-072, reuse) │
|
||||
│ Phase 2: Supervised keypoint regression (NEW) │
|
||||
│ Phase 3: Fine-tune with bone constraints + confidence │
|
||||
│ ↓ │
|
||||
│ WiFlow model (1.8M params) → SafeTensors export │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Deployment (camera-free) │
|
||||
│ │
|
||||
│ ESP32-S3 CSI → Sensing Server → WiFlow inference → 17 keypoints│
|
||||
│ (No camera. Trained model runs on CSI input only.) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Component 1: `scripts/collect-ground-truth.py`
|
||||
|
||||
Single Python script that orchestrates synchronized capture from the laptop camera
|
||||
and the ESP32 CSI stream.
|
||||
|
||||
**Dependencies:** `mediapipe`, `opencv-python`, `requests` (all pip-installable, no GPU)
|
||||
|
||||
**Capture flow:**
|
||||
|
||||
```python
|
||||
# Pseudocode
|
||||
camera = cv2.VideoCapture(0) # Laptop webcam
|
||||
sensing_api = "http://localhost:3000" # Sensing server
|
||||
|
||||
# Start CSI recording via existing API
|
||||
requests.post(f"{sensing_api}/api/v1/recording/start")
|
||||
|
||||
while recording:
|
||||
frame = camera.read()
|
||||
t = time.time_ns() # Nanosecond timestamp
|
||||
|
||||
# MediaPipe Pose: 33 landmarks → map to 17 COCO keypoints
|
||||
result = mp_pose.process(frame)
|
||||
keypoints_17 = map_mediapipe_to_coco(result.pose_landmarks)
|
||||
confidence = mean(landmark.visibility for relevant landmarks)
|
||||
|
||||
# Write to ground-truth JSONL (one line per frame)
|
||||
write_jsonl({
|
||||
"ts_ns": t,
|
||||
"keypoints": keypoints_17, # [[x,y], ...] normalized [0,1]
|
||||
"confidence": confidence, # 0-1, used for loss weighting
|
||||
"n_visible": count(visibility > 0.5),
|
||||
})
|
||||
|
||||
# Optional: show live preview with skeleton overlay
|
||||
if preview:
|
||||
draw_skeleton(frame, keypoints_17)
|
||||
cv2.imshow("Ground Truth", frame)
|
||||
|
||||
# Stop CSI recording
|
||||
requests.post(f"{sensing_api}/api/v1/recording/stop")
|
||||
```
|
||||
|
||||
**MediaPipe → COCO keypoint mapping:**
|
||||
|
||||
| COCO Index | Joint | MediaPipe Index |
|
||||
|------------|-------|-----------------|
|
||||
| 0 | Nose | 0 |
|
||||
| 1 | Left Eye | 2 |
|
||||
| 2 | Right Eye | 5 |
|
||||
| 3 | Left Ear | 7 |
|
||||
| 4 | Right Ear | 8 |
|
||||
| 5 | Left Shoulder | 11 |
|
||||
| 6 | Right Shoulder | 12 |
|
||||
| 7 | Left Elbow | 13 |
|
||||
| 8 | Right Elbow | 14 |
|
||||
| 9 | Left Wrist | 15 |
|
||||
| 10 | Right Wrist | 16 |
|
||||
| 11 | Left Hip | 23 |
|
||||
| 12 | Right Hip | 24 |
|
||||
| 13 | Left Knee | 25 |
|
||||
| 14 | Right Knee | 26 |
|
||||
| 15 | Left Ankle | 27 |
|
||||
| 16 | Right Ankle | 28 |
|
||||
|
||||
### Component 2: Time Alignment (`scripts/align-ground-truth.js`)
|
||||
|
||||
CSI frames arrive at ~100 Hz with server-side timestamps. Camera keypoints arrive at
|
||||
~30 fps with client-side timestamps. Alignment is needed because:
|
||||
|
||||
1. Camera and sensing server clocks differ (typically < 50ms on LAN)
|
||||
2. CSI is aggregated into 20-frame windows for WiFlow input
|
||||
3. Ground-truth keypoints must be averaged over the same window
|
||||
|
||||
**Alignment algorithm:**
|
||||
|
||||
```
|
||||
For each CSI window W_i (20 frames, ~200ms at 100Hz):
|
||||
t_start = W_i.first_frame.timestamp
|
||||
t_end = W_i.last_frame.timestamp
|
||||
|
||||
# Find all camera keypoints within this time window
|
||||
matching_keypoints = [k for k in camera_data if t_start <= k.ts <= t_end]
|
||||
|
||||
if len(matching_keypoints) >= 3: # At least 3 camera frames per window
|
||||
# Average keypoints, weighted by confidence
|
||||
avg_keypoints = weighted_mean(matching_keypoints, weights=confidences)
|
||||
avg_confidence = mean(confidences)
|
||||
|
||||
paired_dataset.append({
|
||||
csi_window: W_i.amplitudes, # [128, 20] float32
|
||||
keypoints: avg_keypoints, # [17, 2] float32
|
||||
confidence: avg_confidence, # scalar
|
||||
n_camera_frames: len(matching_keypoints),
|
||||
})
|
||||
```
|
||||
|
||||
**Clock sync strategy:**
|
||||
|
||||
- NTP is sufficient (< 20ms error on LAN)
|
||||
- The 200ms CSI window is 10x larger than typical clock drift
|
||||
- For tighter sync: use a handclap/jump as a sync marker — visible spike in both
|
||||
CSI motion energy and camera skeleton velocity. Auto-detect and align.
|
||||
|
||||
**Output:** `data/recordings/paired-{timestamp}.jsonl` — one line per paired sample:
|
||||
```json
|
||||
{"csi": [128x20 flat], "kp": [[0.45,0.12], ...], "conf": 0.92, "ts": 1775300000000}
|
||||
```
|
||||
|
||||
### Component 3: Supervised Training (`scripts/train-wiflow-supervised.js`)
|
||||
|
||||
Extends the existing `train-ruvllm.js` pipeline with a supervised phase.
|
||||
|
||||
**Phase 1: Contrastive Pretrain (reuse ADR-072)**
|
||||
- Same as existing: temporal + cross-node triplets
|
||||
- Learns CSI representation without labels
|
||||
- 50 epochs, ~5 min on laptop
|
||||
|
||||
**Phase 2: Supervised Keypoint Regression (NEW)**
|
||||
- Load paired dataset from Component 2
|
||||
- Loss: confidence-weighted SmoothL1 on keypoints
|
||||
|
||||
```
|
||||
L_supervised = (1/N) * sum_i [ conf_i * SmoothL1(pred_i, gt_i, beta=0.05) ]
|
||||
```
|
||||
|
||||
- Only train on samples where `conf > 0.5` (discard frames where MediaPipe lost tracking)
|
||||
- Learning rate: 1e-4 with cosine decay
|
||||
- 200 epochs, ~15 min on laptop CPU (1.8M params, no GPU needed)
|
||||
|
||||
**Phase 3: Refinement with Bone Constraints**
|
||||
- Fine-tune with combined loss:
|
||||
|
||||
```
|
||||
L = L_supervised + 0.3 * L_bone + 0.1 * L_temporal
|
||||
|
||||
L_bone = (1/14) * sum_b (bone_len_b - prior_b)^2 # ADR-072 bone priors
|
||||
L_temporal = SmoothL1(kp_t, kp_{t-1}) # Temporal smoothness
|
||||
```
|
||||
|
||||
- 50 epochs at lower LR (1e-5)
|
||||
- Tighten bone constraint weight from 0.3 → 0.5 over epochs
|
||||
|
||||
**Phase 4: Quantization + Export**
|
||||
- Reuse ruvllm TurboQuant: float32 → int8 (4x smaller, ~881 KB)
|
||||
- Export via SafeTensors for cross-platform deployment
|
||||
- Validate quantized model PCK@20 within 2% of full-precision
|
||||
|
||||
### Component 4: Evaluation Script (`scripts/eval-wiflow.js`)
|
||||
|
||||
Measure actual PCK@20 using held-out paired data (20% split).
|
||||
|
||||
```
|
||||
PCK@k = (1/N) * sum_i [ (||pred_i - gt_i|| < k * torso_length) ? 1 : 0 ]
|
||||
```
|
||||
|
||||
**Metrics reported:**
|
||||
|
||||
| Metric | Description | Target |
|
||||
|--------|-------------|--------|
|
||||
| PCK@20 | % of keypoints within 20% torso length | > 35% |
|
||||
| PCK@50 | % within 50% torso length | > 60% |
|
||||
| MPJPE | Mean per-joint position error (pixels) | < 40px |
|
||||
| Per-joint PCK | Breakdown by joint (wrists are hardest) | Report all 17 |
|
||||
| Inference latency | Single window prediction time | < 50ms |
|
||||
|
||||
### Optimization Strategy
|
||||
|
||||
#### O1: Curriculum Learning
|
||||
|
||||
Train easy poses first, hard poses later:
|
||||
|
||||
| Stage | Epochs | Data Filter | Rationale |
|
||||
|-------|--------|-------------|-----------|
|
||||
| 1 | 50 | `conf > 0.9`, standing only | Establish stable skeleton baseline |
|
||||
| 2 | 50 | `conf > 0.7`, low motion | Add sitting, subtle movements |
|
||||
| 3 | 50 | `conf > 0.5`, all poses | Full dataset including occlusions |
|
||||
| 4 | 50 | All data, with augmentation | Robustness via noise injection |
|
||||
|
||||
#### O2: Data Augmentation (CSI domain)
|
||||
|
||||
Augment CSI windows to increase effective dataset size without collecting more data:
|
||||
|
||||
| Augmentation | Implementation | Expected Gain |
|
||||
|-------------|----------------|---------------|
|
||||
| Time shift | Roll CSI window by ±2 frames | +30% data |
|
||||
| Amplitude noise | Gaussian noise, sigma=0.02 | Robustness |
|
||||
| Subcarrier dropout | Zero 10% of subcarriers randomly | Robustness |
|
||||
| Temporal flip | Reverse window + reverse keypoint velocity | +100% data |
|
||||
| Multi-node mix | Swap node CSI, keep same-time keypoints | Cross-node generalization |
|
||||
|
||||
#### O3: Knowledge Distillation from MediaPipe
|
||||
|
||||
Instead of raw keypoint regression, distill MediaPipe's confidence and heatmap
|
||||
information:
|
||||
|
||||
```
|
||||
L_distill = KL_div(softmax(wifi_heatmap / T), softmax(camera_heatmap / T))
|
||||
```
|
||||
|
||||
- Temperature T=4 for soft targets (transfers inter-joint relationships)
|
||||
- WiFlow predicts a 17-channel heatmap [17, H, W] instead of direct [17, 2]
|
||||
- Argmax for final keypoint extraction
|
||||
- **Trade-off:** Adds ~200K params for heatmap decoder, but improves spatial precision
|
||||
|
||||
#### O4: Active Learning Loop
|
||||
|
||||
Identify which poses the model is worst at and collect more data for those:
|
||||
|
||||
```
|
||||
1. Train initial model on first collection session
|
||||
2. Run inference on new CSI data, compute prediction entropy
|
||||
3. Flag high-entropy windows (model is uncertain)
|
||||
4. During next collection, the preview overlay highlights these moments:
|
||||
"Hold this pose — model needs more examples"
|
||||
5. Re-train with augmented dataset
|
||||
```
|
||||
|
||||
Expected: 2-3 active learning iterations reach saturation.
|
||||
|
||||
#### O6: Subcarrier Selection (ruvector-solver)
|
||||
|
||||
Variance-based top-K subcarrier selection, equivalent to ruvector-solver's sparse
|
||||
interpolation (114→56). Removes noise/static subcarriers before training:
|
||||
|
||||
```
|
||||
For each subcarrier d in [0, dim):
|
||||
variance[d] = mean over samples of temporal_variance(csi[d, :])
|
||||
Select top-K by variance (K = dim * 0.5)
|
||||
```
|
||||
|
||||
**Validated:** 128 → 56 subcarriers (56% input reduction), proportional model size reduction.
|
||||
|
||||
#### O7: Attention-Weighted Subcarriers (ruvector-attention)
|
||||
|
||||
Compute per-subcarrier attention weights based on temporal energy correlation with
|
||||
ground-truth keypoint motion. High-energy subcarriers that covary with skeleton
|
||||
movement get amplified:
|
||||
|
||||
```
|
||||
For each subcarrier d:
|
||||
energy[d] = sum of squared first-differences over time
|
||||
weight[d] = softmax(energy, temperature=0.1)
|
||||
Apply: csi[d, :] *= weight[d] * dim (mean weight = 1)
|
||||
```
|
||||
|
||||
**Validated:** Top-5 attention subcarriers identified automatically per dataset.
|
||||
|
||||
#### O8: Stoer-Wagner MinCut Person Separation (ruvector-mincut / ADR-075)
|
||||
|
||||
JS implementation of the Stoer-Wagner algorithm for person separation in CSI, equivalent
|
||||
to `DynamicPersonMatcher` in `wifi-densepose-train/src/metrics.rs`. Builds a subcarrier
|
||||
correlation graph and finds the minimum cut to identify person-specific subcarrier clusters:
|
||||
|
||||
```
|
||||
1. Build dim×dim Pearson correlation matrix across subcarriers
|
||||
2. Run Stoer-Wagner min-cut on correlation graph
|
||||
3. Partition subcarriers into person-specific groups
|
||||
4. Train per-partition models for multi-person scenarios
|
||||
```
|
||||
|
||||
**Validated:** Stoer-Wagner executes on 56-dim graph, identifies partition boundaries.
|
||||
|
||||
#### O9: Multi-SPSA Gradient Estimation
|
||||
|
||||
Average over K=3 random perturbation directions per gradient step. Reduces variance
|
||||
by sqrt(K) = 1.73x compared to single SPSA, at 3x forward pass cost (net win for
|
||||
convergence quality):
|
||||
|
||||
```
|
||||
For k in 1..K:
|
||||
delta_k = random ±1 per parameter
|
||||
grad_k = (loss(w + eps*delta_k) - loss(w - eps*delta_k)) / (2*eps*delta_k)
|
||||
grad = mean(grad_1, ..., grad_K)
|
||||
```
|
||||
|
||||
#### O10: Mac M4 Pro Training via Tailscale
|
||||
|
||||
Training runs on Mac Mini M4 Pro (16-core GPU, ARM NEON SIMD) via Tailscale SSH,
|
||||
using ruvllm's native Node.js SIMD ops:
|
||||
|
||||
| | Windows (CPU) | Mac M4 Pro |
|
||||
|---|---|---|
|
||||
| Node.js | v24.12.0 (x86) | v25.9.0 (ARM) |
|
||||
| SIMD | SSE4/AVX2 | NEON |
|
||||
| Cores | Consumer laptop | 12P + 4E cores |
|
||||
| Training | Slow (minutes/epoch) | Fast (seconds/epoch) |
|
||||
|
||||
#### O5: Cross-Environment Transfer
|
||||
|
||||
Train on one room, deploy in another:
|
||||
|
||||
| Strategy | Implementation |
|
||||
|----------|---------------|
|
||||
| Room-invariant features | Normalize CSI by running mean/variance |
|
||||
| LoRA adapters | Train a 4-rank LoRA per room (ADR-071) — 7.3 KB each |
|
||||
| Few-shot calibration | 2 min of camera data in new room → fine-tune LoRA only |
|
||||
| AETHER embeddings | Use contrastive room-independent features (ADR-024) as input |
|
||||
|
||||
The LoRA approach is most practical: ship a base model + collect 2 min of calibration
|
||||
data per new room using the laptop camera.
|
||||
|
||||
### Data Collection Protocol
|
||||
|
||||
Recommended collection sessions per room:
|
||||
|
||||
| Session | Duration | Activity | People | Total CSI Frames |
|
||||
|---------|----------|----------|--------|-----------------|
|
||||
| 1. Baseline | 5 min | Empty + 1 person entry/exit | 0-1 | 30,000 |
|
||||
| 2. Standing poses | 5 min | Stand, arms up/down/sides, turn | 1 | 30,000 |
|
||||
| 3. Sitting | 5 min | Sit, type, lean, stand up/sit down | 1 | 30,000 |
|
||||
| 4. Walking | 5 min | Walk paths across room | 1 | 30,000 |
|
||||
| 5. Mixed | 5 min | Varied activities, transitions | 1 | 30,000 |
|
||||
| 6. Multi-person | 5 min | 2 people, varied activities | 2 | 30,000 |
|
||||
| **Total** | **30 min** | | | **180,000** |
|
||||
|
||||
At 20-frame windows: **9,000 paired training samples** per 30-min session.
|
||||
With augmentation (O2): **~27,000 effective samples**.
|
||||
|
||||
Camera placement: position laptop so the camera has a clear view of the sensing area.
|
||||
The camera FOV should cover the same space the ESP32 nodes cover.
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
scripts/
|
||||
collect-ground-truth.py # Camera capture + MediaPipe + CSI sync
|
||||
align-ground-truth.js # Time-align CSI windows with camera keypoints
|
||||
train-wiflow-supervised.js # Supervised training pipeline
|
||||
eval-wiflow.js # PCK evaluation on held-out data
|
||||
|
||||
data/
|
||||
ground-truth/ # Raw camera keypoint captures
|
||||
gt-{timestamp}.jsonl
|
||||
paired/ # Aligned CSI + keypoint pairs
|
||||
paired-{timestamp}.jsonl
|
||||
|
||||
models/
|
||||
wiflow-supervised/ # Trained model outputs
|
||||
wiflow-v1.safetensors
|
||||
wiflow-v1-int8.safetensors
|
||||
training-log.json
|
||||
eval-report.json
|
||||
```
|
||||
|
||||
### Privacy Considerations
|
||||
|
||||
- Camera frames are processed **locally** by MediaPipe — no cloud upload
|
||||
- Raw video is **never saved** — only extracted keypoint coordinates are stored
|
||||
- The `.jsonl` ground-truth files contain only `[x,y]` joint coordinates, not images
|
||||
- The trained model runs on CSI only — no camera data leaves the laptop
|
||||
- Users can delete `data/ground-truth/` after training; the model is self-contained
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **10-20x accuracy improvement**: PCK@20 from 2.5% → 35%+ with real supervision
|
||||
- **Reuses existing infrastructure**: sensing server recording API, ruvllm training, SafeTensors
|
||||
- **No new hardware**: laptop webcam + existing ESP32 nodes
|
||||
- **Privacy preserved at deployment**: camera only needed during 30-min training session
|
||||
- **Incremental**: can improve with more collection sessions + active learning
|
||||
- **Distributable**: trained model weights can be shared on HuggingFace (ADR-070)
|
||||
|
||||
### Negative
|
||||
|
||||
- **Camera placement matters**: must see the same area ESP32 nodes sense
|
||||
- **Single-room models**: need LoRA calibration per room (2 min + camera)
|
||||
- **MediaPipe limitations**: occlusion, side views, multiple people reduce keypoint quality
|
||||
- **Time sync**: NTP drift can misalign frames (mitigated by 200ms windows)
|
||||
|
||||
### Risks
|
||||
|
||||
| Risk | Probability | Impact | Mitigation |
|
||||
|------|-------------|--------|------------|
|
||||
| MediaPipe keypoints too noisy | Low | Medium | Filter by confidence; MediaPipe is robust indoors |
|
||||
| Clock drift > 100ms | Low | High | Add handclap sync marker detection |
|
||||
| Single camera can't see all poses | Medium | Medium | Position camera centrally; collect from 2 angles |
|
||||
| Model overfits to one room | High | Medium | LoRA adapters + AETHER normalization (O5) |
|
||||
| Insufficient data (< 5K pairs) | Low | High | Augmentation (O2) + active learning (O4) |
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
| Phase | Task | Effort | Status |
|
||||
|-------|------|--------|--------|
|
||||
| P1 | `collect-ground-truth.py` — camera + MediaPipe capture | 2 hrs | **Done** |
|
||||
| P2 | `align-ground-truth.js` — time alignment + pairing | 1 hr | **Done** |
|
||||
| P3 | `train-wiflow-supervised.js` — supervised training | 3 hrs | **Done** |
|
||||
| P4 | `eval-wiflow.js` — PCK evaluation | 1 hr | **Done** |
|
||||
| P5 | ruvector optimizations (O6-O9) | 2 hrs | **Done** |
|
||||
| P6 | Mac M4 Pro training via Tailscale (O10) | 1 hr | **Done** |
|
||||
| P7 | Data collection session (30 min recording) | 1 hr | Pending |
|
||||
| P8 | Training + evaluation on real paired data | 30 min | Pending |
|
||||
| P9 | LoRA cross-room calibration (O5) | 2 hrs | Pending |
|
||||
|
||||
## Validated Hardware
|
||||
|
||||
| Component | Spec | Validated |
|
||||
|-----------|------|-----------|
|
||||
| Mac Mini camera | 1920x1080, 30fps | Yes — 14/17 keypoints, conf 0.94-1.0 |
|
||||
| MediaPipe PoseLandmarker | v0.10.33 Tasks API, lite model | Yes — via Tailscale SSH |
|
||||
| Mac M4 Pro GPU | 16-core, Metal 4, NEON SIMD | Yes — Node.js v25.9.0 |
|
||||
| Tailscale SSH | LAN-accessible Mac, passwordless | Yes |
|
||||
| ESP32-S3 CSI | 128 subcarriers, 100Hz | Yes — existing recordings |
|
||||
| Sensing server recording API | `/api/v1/recording/start\|stop` | Yes — existing |
|
||||
|
||||
## Baseline Benchmark
|
||||
|
||||
Proxy-pose baseline (no camera supervision, standing skeleton heuristic):
|
||||
|
||||
```
|
||||
PCK@10: 11.8%
|
||||
PCK@20: 35.3%
|
||||
PCK@50: 94.1%
|
||||
MPJPE: 0.067
|
||||
Latency: 0.03ms/sample
|
||||
```
|
||||
|
||||
Per-joint PCK@20: upper body (nose, shoulders, wrists) at 0% — proxy has no spatial
|
||||
accuracy for these. Camera supervision targets these joints specifically.
|
||||
|
||||
## References
|
||||
|
||||
- WiFlow: arXiv:2602.08661 — WiFi-based pose estimation with TCN + axial attention
|
||||
- Wi-Pose (CVPR 2021) — 3D CNN WiFi pose with camera supervision
|
||||
- Person-in-WiFi 3D (CVPR 2024) — Deformable attention with camera labels
|
||||
- MediaPipe Pose — Google's real-time 33-landmark body pose estimator
|
||||
- MetaFi++ (NeurIPS 2023) — Meta-learning cross-modal WiFi sensing
|
||||
@@ -1,99 +0,0 @@
|
||||
# ADR-080: QE Analysis Remediation Plan
|
||||
|
||||
- **Status:** Proposed
|
||||
- **Date:** 2026-04-06
|
||||
- **Source:** [QE Analysis Gist (2026-04-05)](https://gist.github.com/proffesor-for-testing/a6b84d7a4e26b7bbef0cf12f932925b7)
|
||||
- **Full Reports:** [proffesor-for-testing/RuView `qe-reports` branch](https://github.com/proffesor-for-testing/RuView/tree/qe-reports/docs/qe-reports)
|
||||
|
||||
## Context
|
||||
|
||||
An 8-agent QE swarm analyzed ~305K lines across Rust, Python, C firmware, and TypeScript on 2026-04-05. The overall score was **55/100 (C+) — Quality Gate FAILED**. This ADR captures the findings and establishes a remediation plan.
|
||||
|
||||
## Decision
|
||||
|
||||
Address the 15 prioritized issues from the QE analysis in three waves: P0 (immediate), P1 (this sprint), P2 (this quarter).
|
||||
|
||||
## P0 — Fix Immediately
|
||||
|
||||
### 1. Rate Limiter Bypass (Security HIGH)
|
||||
|
||||
- **Location:** `archive/v1/src/middleware/rate_limit.py:200-206`
|
||||
- **Problem:** Trusts `X-Forwarded-For` without validation. Any client bypasses rate limits via header spoofing.
|
||||
- **Fix:** Validate forwarded headers against trusted proxy list, or use connection IP directly.
|
||||
|
||||
### 2. Exception Details Leaked in Responses (Security HIGH)
|
||||
|
||||
- **Location:** `archive/v1/src/api/routers/pose.py:140`, `stream.py:297`, +5 endpoints
|
||||
- **Problem:** Stack traces visible regardless of environment.
|
||||
- **Fix:** Wrap with generic error responses in production; log details server-side only.
|
||||
|
||||
### 3. WebSocket JWT in URL (Security HIGH, CWE-598)
|
||||
|
||||
- **Location:** `archive/v1/src/api/routers/stream.py:74`, `archive/v1/src/middleware/auth.py:243`
|
||||
- **Problem:** Tokens in query strings visible in logs/proxies/browser history.
|
||||
- **Fix:** Use WebSocket subprotocol or first-message auth pattern.
|
||||
|
||||
### 4. Rust Tests Not in CI
|
||||
|
||||
- **Problem:** 2,618 tests across 153K lines of Rust — zero run in any GitHub Actions workflow. Regressions ship undetected.
|
||||
- **Fix:** Add `cargo test --workspace --no-default-features` to CI. 1-2 hour task.
|
||||
|
||||
### 5. WebSocket Path Mismatch (Bug)
|
||||
|
||||
- **Location:** `ui/mobile/src/services/ws.service.ts:104` constructs `/ws/sensing`, but `constants/websocket.ts:1` defines `WS_PATH = '/api/v1/stream/pose'`.
|
||||
- **Problem:** Mobile WebSocket silently fails.
|
||||
- **Fix:** Align paths. Verify which endpoint the server actually serves.
|
||||
|
||||
## P1 — Fix This Sprint
|
||||
|
||||
| # | Issue | Location | Impact |
|
||||
|---|-------|----------|--------|
|
||||
| 6 | God file: 4,846 lines, CC=121 | `sensing-server/src/main.rs` | Untestable monolith |
|
||||
| 7 | O(L×V) voxel scan per frame | `ruvsense/tomography.rs:345-383` | ~10ms wasted; use DDA ray march |
|
||||
| 8 | Sequential neural inference | `wifi-densepose-nn inference.rs:334-336` | 2-4× GPU latency penalty |
|
||||
| 9 | 720 `.unwrap()` in Rust | Workspace-wide | Each = potential panic in RT paths |
|
||||
| 10 | 112KB alloc/frame in Python | `csi_processor.py:412-414` | Deque→list→numpy every frame |
|
||||
|
||||
## P2 — Fix This Quarter
|
||||
|
||||
| # | Issue | Impact |
|
||||
|---|-------|--------|
|
||||
| 11 | 11/12 Python modules have zero unit tests (12,280 LOC) | Services, middleware, DB untested |
|
||||
| 12 | Firmware at 19% coverage (WASM runtime, OTA, swarm) | Security-critical code untested |
|
||||
| 13 | MAT screen auto-falls back to simulated data | Disaster responders could monitor fake data |
|
||||
| 14 | Token blacklist never consulted during auth | Revoked tokens remain valid |
|
||||
| 15 | 50ms frame budget never benchmarked | Real-time requirement unverified |
|
||||
|
||||
## Bright Spots
|
||||
|
||||
- 79 ADRs (exceptional governance)
|
||||
- Witness bundle system (ADR-028) with SHA-256 proof
|
||||
- 2,618 Rust tests with mathematical rigor
|
||||
- Daily security scanning (Bandit, Semgrep, Safety)
|
||||
- Ed25519 WASM signature verification on firmware
|
||||
- Clean mobile state management with good test coverage
|
||||
|
||||
## Full QE Reports (9 files, 4,914 lines)
|
||||
|
||||
| Report | What it covers |
|
||||
|--------|---------------|
|
||||
| `EXECUTIVE-SUMMARY.md` | Top-level synthesis with all scores and priority matrix |
|
||||
| `00-qe-queen-summary.md` | Master coordination, quality posture, test pyramid |
|
||||
| `01-code-quality-complexity.md` | Cyclomatic complexity, code smells, top 20 hotspots |
|
||||
| `02-security-review.md` | 15 security findings (3 HIGH, 7 MEDIUM), OWASP coverage |
|
||||
| `03-performance-analysis.md` | 23 perf findings (4 CRITICAL), frame budget analysis |
|
||||
| `04-test-analysis.md` | 3,353 tests inventoried, duplication, quality grading |
|
||||
| `05-quality-experience.md` | API/CLI/Mobile/DX UX assessment |
|
||||
| `06-product-assessment-sfdipot.md` | SFDIPOT analysis, 57 test ideas, 14 session charters |
|
||||
| `07-coverage-gaps.md` | Coverage matrix, top 20 risk gaps, 8-week roadmap |
|
||||
|
||||
## Consequences
|
||||
|
||||
- **P0 fixes** eliminate 3 security vulnerabilities and 2 functional bugs
|
||||
- **P1 fixes** improve performance, reliability, and maintainability
|
||||
- **P2 fixes** close coverage gaps and harden the system for production
|
||||
- Target score improvement: 55 → 75+ after P0+P1 completion
|
||||
|
||||
---
|
||||
|
||||
*Generated from QE swarm analysis (fleet-02558e91) on 2026-04-05*
|
||||
@@ -1,503 +0,0 @@
|
||||
# ADR-081: Adaptive CSI Mesh Firmware Kernel
|
||||
|
||||
| Field | Value |
|
||||
|-------------|-----------------------------------------------------------------------|
|
||||
| **Status** | Accepted — Layers 1/2/3/4/5 implemented and host-tested; mesh RX path and Ed25519 signing tracked as Phase 3.5 polish |
|
||||
| **Date** | 2026-04-19 |
|
||||
| **Authors** | ruv |
|
||||
| **Depends** | ADR-018, ADR-028, ADR-029, ADR-031, ADR-032, ADR-039, ADR-066, ADR-073 |
|
||||
|
||||
## Context
|
||||
|
||||
RuView's firmware grew bottom-up. ADR-018 defined a binary CSI frame, ADR-029
|
||||
added channel hopping and TDM, ADR-039 added a tiered edge-intelligence
|
||||
pipeline, ADR-040 added programmable WASM modules, ADR-060 added per-node
|
||||
channel and MAC overrides, ADR-066 added a swarm bridge to a coordinator, and
|
||||
ADR-073 added multifrequency mesh scanning. Each one was a sound local
|
||||
decision. Together they produced a firmware that works on ESP32-S3 but is
|
||||
**implicitly coupled** to that chipset through `csi_collector.c` calling
|
||||
`esp_wifi_*` directly and through hard-coded assumptions about the WiFi driver
|
||||
callback shape.
|
||||
|
||||
This is a problem for three reasons:
|
||||
|
||||
1. **Portability.** Espressif exposes CSI through an official driver API. On
|
||||
locked Broadcom and Cypress chips, projects like Nexmon achieve the same
|
||||
thing by patching the firmware blob — but only for specific chip and
|
||||
firmware build combinations. Future RuView nodes will likely span both
|
||||
models plus eventually a custom silicon path. Today, none of the modules
|
||||
above can be reused unchanged on any non-ESP32 chip.
|
||||
|
||||
2. **Adaptivity.** The current firmware reacts to configuration, not to
|
||||
conditions. Channel hop intervals, edge tier, vitals cadence, top-K
|
||||
subcarriers, fall threshold, and power duty are all read from NVS at boot
|
||||
and never revisited. There is no closed-loop control: if a channel becomes
|
||||
congested, if motion spikes, if inter-node coherence drops, or if the
|
||||
environment is stable enough to coast at lower cadence, nothing changes
|
||||
onboard. The adaptive classifier in `wifi-densepose-sensing-server` does
|
||||
adapt — but only on the host side, after the data has already traversed the
|
||||
network at fixed rate.
|
||||
|
||||
3. **Mesh as an afterthought.** ADR-029 wired in a `TdmCoordinator` and ADR-066
|
||||
added a swarm bridge to a Cognitum Seed, but there is no first-class node
|
||||
role enumeration (anchor / observer / fusion-relay / coordinator), no
|
||||
role-assignment protocol, no `FEATURE_DELTA` message type, no
|
||||
coordinator-driven channel plan, and no automatic role re-election when a
|
||||
node drops. Multi-node deployments today are stitched together by manual
|
||||
per-node NVS provisioning.
|
||||
|
||||
The hard truth is that the firmware hack — getting raw CSI off a radio — is
|
||||
not the moat. The moat is **adaptive control, multi-node fusion, compact
|
||||
state encoding, persistent memory, and contrastive reasoning on top of the
|
||||
radio layer**. The current architecture does not name those layers, so they
|
||||
get reinvented inline by every new ADR.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt a **5-layer adaptive RF sensing kernel** as the canonical RuView
|
||||
firmware architecture, and refactor the existing modules to fit underneath
|
||||
it. The five layers, top to bottom:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Layer 5 — Rust handoff │
|
||||
│ Two streams only: feature_state (default) and debug_csi_frame (gated) │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Layer 4 — On-device feature extraction │
|
||||
│ 100 ms motion, 1 s respiration, 5 s baseline windows │
|
||||
│ Emits compact rv_feature_state_t (magic 0xC5110006) │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Layer 3 — Mesh sensing plane │
|
||||
│ Roles: Anchor / Observer / Fusion relay / Coordinator │
|
||||
│ Messages: TIME_SYNC, ROLE_ASSIGN, CHANNEL_PLAN, CALIBRATION_START, │
|
||||
│ FEATURE_DELTA, HEALTH, ANOMALY_ALERT │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Layer 2 — Adaptive controller │
|
||||
│ Fast loop ~200 ms — packet rate, active probing │
|
||||
│ Medium loop ~1 s — channel selection, role changes │
|
||||
│ Slow loop ~30 s — baseline recalibration │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Layer 1 — Radio Abstraction Layer (rv_radio_ops_t vtable) │
|
||||
│ ESP32 binding, future Nexmon binding, future custom silicon binding │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Layer 1 — Radio Abstraction Layer
|
||||
|
||||
A single function-pointer vtable, `rv_radio_ops_t`, defined in
|
||||
`firmware/esp32-csi-node/main/rv_radio_ops.h`:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
int (*init)(void);
|
||||
int (*set_channel)(uint8_t ch, uint8_t bw);
|
||||
int (*set_mode)(uint8_t mode); /* RV_RADIO_MODE_* */
|
||||
int (*set_csi_enabled)(bool en);
|
||||
int (*set_capture_profile)(uint8_t profile_id);
|
||||
int (*get_health)(rv_radio_health_t *out);
|
||||
} rv_radio_ops_t;
|
||||
```
|
||||
|
||||
Capture profiles, named not numbered:
|
||||
|
||||
| Profile | Intent |
|
||||
|--------------------------------|-------------------------------------------------------|
|
||||
| `RV_PROFILE_PASSIVE_LOW_RATE` | Default idle: minimum cadence, presence only |
|
||||
| `RV_PROFILE_ACTIVE_PROBE` | Inject NDP frames at high rate |
|
||||
| `RV_PROFILE_RESP_HIGH_SENS` | Quietest channel, longest window, vitals-only |
|
||||
| `RV_PROFILE_FAST_MOTION` | Short window, high cadence |
|
||||
| `RV_PROFILE_CALIBRATION` | Synchronized burst across nodes |
|
||||
|
||||
Two bindings ship in this ADR:
|
||||
|
||||
- **ESP32 binding** (`rv_radio_ops_esp32.c`) wraps `csi_collector.c`,
|
||||
`esp_wifi_set_channel()`, `esp_wifi_set_csi()`, and
|
||||
`csi_inject_ndp_frame()`.
|
||||
- **Mock binding** (`rv_radio_ops_mock.c`) wraps `mock_csi.c` so QEMU
|
||||
scenarios can exercise the controller and mesh plane without a radio.
|
||||
|
||||
A third binding (Nexmon-patched Broadcom) is reserved but not implemented
|
||||
here.
|
||||
|
||||
### Layer 2 — Adaptive controller
|
||||
|
||||
`firmware/esp32-csi-node/main/adaptive_controller.{c,h}`. A single FreeRTOS
|
||||
task with three cooperating timers:
|
||||
|
||||
| Loop | Period | Inputs | Outputs |
|
||||
|--------|---------|------------------------------------------------------------------------|------------------------------------------------------|
|
||||
| Fast | ~200 ms | packet yield, retry/drop rate, motion score | cadence (vital_interval_ms), active vs passive probe |
|
||||
| Medium | ~1 s | CSI variance, RSSI median, channel occupancy, inter-node agreement | channel selection (via radio ops), role transitions |
|
||||
| Slow | ~30 s | drift profile (Stable/Linear/StepChange), respiration confidence | baseline recalibration, switch to delta-only mode |
|
||||
|
||||
The controller publishes its decisions through the radio ops vtable
|
||||
(`set_capture_profile`, `set_channel`) and through the mesh plane
|
||||
(`CHANNEL_PLAN`, `ROLE_ASSIGN`). Default policy is conservative and matches
|
||||
today's behavior; aggressive adaptation is opt-in via Kconfig.
|
||||
|
||||
### Layer 3 — Mesh sensing plane
|
||||
|
||||
Extends `swarm_bridge.c` with explicit node roles (Anchor / Observer /
|
||||
Fusion relay / Coordinator) and a 7-message type protocol:
|
||||
|
||||
| Message | Cadence | Sender(s) | Purpose |
|
||||
|----------------------|--------------------|------------------|-----------------------------------------------|
|
||||
| `TIME_SYNC` | 100 ms | Anchor | Reuse ADR-032 `SyncBeacon` (28 bytes, HMAC) |
|
||||
| `ROLE_ASSIGN` | event-driven | Coordinator | Node ID → role mapping |
|
||||
| `CHANNEL_PLAN` | event-driven | Coordinator | Per-node channel + dwell schedule |
|
||||
| `CALIBRATION_START` | event-driven | Coordinator | Synchronized calibration burst |
|
||||
| `FEATURE_DELTA` | 1–10 Hz | Observer / Relay | Compact feature delta (see Layer 4) |
|
||||
| `HEALTH` | 1 Hz | All | `rv_node_status_t` (see below) |
|
||||
| `ANOMALY_ALERT` | event-driven | Observer | Phase-physics violation, multi-link mismatch |
|
||||
|
||||
Node status payload:
|
||||
|
||||
```c
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint8_t node_id[8];
|
||||
uint64_t local_time_us;
|
||||
uint8_t role;
|
||||
uint8_t current_channel;
|
||||
uint8_t current_bw;
|
||||
int8_t noise_floor_dbm;
|
||||
uint16_t pkt_yield;
|
||||
uint16_t sync_error_us;
|
||||
uint16_t health_flags;
|
||||
} rv_node_status_t;
|
||||
```
|
||||
|
||||
Time-sync target is an engineering goal, not a guaranteed constant — it
|
||||
depends on the clock quality of the chosen radio family. The first
|
||||
acceptance test (Phase 2) measures it on real hardware.
|
||||
|
||||
### Layer 4 — On-device feature extraction
|
||||
|
||||
Defined in `firmware/esp32-csi-node/main/rv_feature_state.h`. Single
|
||||
on-the-wire packet, **60 bytes packed** (verified by `_Static_assert` and
|
||||
host unit test), magic `0xC5110006` (next free after ADR-039's
|
||||
`0xC5110002`, ADR-069's `0xC5110003`, ADR-063's `0xC5110004`, and ADR-039's
|
||||
compressed `0xC5110005`):
|
||||
|
||||
```c
|
||||
#define RV_FEATURE_STATE_MAGIC 0xC5110006u
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t magic; /* RV_FEATURE_STATE_MAGIC */
|
||||
uint8_t node_id;
|
||||
uint8_t mode; /* RV_PROFILE_* identifier */
|
||||
uint16_t seq; /* monotonic per-node sequence */
|
||||
uint64_t ts_us; /* node-local microseconds */
|
||||
float motion_score;
|
||||
float presence_score;
|
||||
float respiration_bpm;
|
||||
float respiration_conf;
|
||||
float heartbeat_bpm;
|
||||
float heartbeat_conf;
|
||||
float anomaly_score;
|
||||
float env_shift_score;
|
||||
float node_coherence;
|
||||
uint16_t quality_flags;
|
||||
uint16_t reserved;
|
||||
uint32_t crc32; /* IEEE polynomial over bytes [0..end-4] */
|
||||
} rv_feature_state_t;
|
||||
|
||||
_Static_assert(sizeof(rv_feature_state_t) == 60,
|
||||
"rv_feature_state_t must be 60 bytes on the wire");
|
||||
```
|
||||
|
||||
Three windows feed it: 100 ms (motion), 1 s (respiration), 5 s (baseline /
|
||||
env shift). Each `rv_feature_state_t` represents the most recent state of
|
||||
all three; mode field tells the receiver which window dominates this
|
||||
update.
|
||||
|
||||
`rv_feature_state_t` does not replace ADR-039's `edge_vitals_pkt_t`
|
||||
(0xC5110002) or ADR-063's `edge_fused_vitals_pkt_t` (0xC5110004). Those
|
||||
remain the wire format for vitals-specific consumers. `rv_feature_state_t`
|
||||
is the **default upstream payload** for the sensing pipeline; vitals
|
||||
packets are now an alternate emission mode for backward compatibility.
|
||||
|
||||
### Layer 5 — Rust handoff
|
||||
|
||||
The Rust side sees only two streams from a node:
|
||||
|
||||
1. **`feature_state` stream** — `rv_feature_state_t`, default-on, 1–10 Hz.
|
||||
2. **`debug_csi_frame` stream** — ADR-018 raw frames (magic 0xC5110001),
|
||||
default-off, opt-in via NVS or `CHANNEL_PLAN`. Used for calibration,
|
||||
debugging, training-set capture.
|
||||
|
||||
The Rust handoff is mirrored as a trait in
|
||||
`crates/wifi-densepose-hardware/src/radio_ops.rs` so test harnesses (and
|
||||
eventually the Rust-side controller for centralized coordinator nodes) can
|
||||
swap radio backends without touching `wifi-densepose-signal`,
|
||||
`wifi-densepose-ruvector`, `wifi-densepose-train`, or
|
||||
`wifi-densepose-mat`. Rust-side mirror trait is **out of scope for the
|
||||
firmware-only PR** that ships this ADR; tracked as Phase 4 follow-up.
|
||||
|
||||
## State Machine
|
||||
|
||||
```
|
||||
BOOT → SELF_TEST → RADIO_INIT → TIME_SYNC → CALIBRATION → SENSE_IDLE
|
||||
↓ ↑
|
||||
SENSE_ACTIVE
|
||||
↓
|
||||
ALERT
|
||||
↓
|
||||
DEGRADED
|
||||
```
|
||||
|
||||
Transitions:
|
||||
|
||||
- **CALIBRATION** on boot, on role change, on sustained inter-node
|
||||
disagreement.
|
||||
- **SENSE_ACTIVE** when motion or anomaly score crosses threshold.
|
||||
- **DEGRADED** when packet yield, sync quality, or memory pressure drops
|
||||
below threshold; falls back to ADR-039 Tier-0 raw passthrough as the
|
||||
last-resort survivable mode.
|
||||
|
||||
## Data budgets
|
||||
|
||||
| Stream | Default rate | Notes |
|
||||
|-------------------------|-----------------------------|----------------------------------------------|
|
||||
| Raw capture (internal) | 50–200 pps per observer | Stays on-device unless debug stream enabled |
|
||||
| `rv_feature_state_t` | 1–10 Hz per node | Default upstream |
|
||||
| `ANOMALY_ALERT` | event-driven | Burst-bounded |
|
||||
| Debug ADR-018 raw CSI | 0 (off by default) | Burst-only via `CHANNEL_PLAN` debug flag |
|
||||
|
||||
ADR-039 measured raw CSI at ~5 KB/frame and ~100 KB/s per node. The default
|
||||
upstream with ADR-081's 60-byte `rv_feature_state_t` at 5 Hz is **300 B/s
|
||||
per node — a 99.7% reduction**. A 50-node deployment at 5 Hz fits in
|
||||
15 KB/s total, easily carried by a single-AP backhaul.
|
||||
|
||||
## Channel planning policy
|
||||
|
||||
Codified rules — these are constraints on the controller, not just defaults:
|
||||
|
||||
- Keep one anchor on a stable channel; observers distributed across the
|
||||
least-congested channels.
|
||||
- Rotate **one** observer at a time. Never change all nodes simultaneously.
|
||||
- Pin `RV_PROFILE_RESP_HIGH_SENS` to the quietest stable channel for the
|
||||
duration of a respiration window.
|
||||
- Use a short active burst on a quiet channel for calibration, then return
|
||||
to passive capture.
|
||||
|
||||
This generalizes the per-deployment policy in ADR-073 ("node 1: ch 1/6/11,
|
||||
node 2: ch 3/5/9") into a controller-driven plan that the coordinator can
|
||||
publish via `CHANNEL_PLAN`. IEEE 802.11bf is the standards direction this
|
||||
points toward.
|
||||
|
||||
## Security & integrity
|
||||
|
||||
- Every `FEATURE_DELTA` carries node id, monotonic seq, ts_us, and CRC32
|
||||
(IEEE polynomial), per the struct above.
|
||||
- Every control message (`ROLE_ASSIGN`, `CHANNEL_PLAN`, `CALIBRATION_START`)
|
||||
carries sender role, epoch, replay window index, and authorization class,
|
||||
reusing the HMAC-SHA256 + 16-frame replay window from ADR-032
|
||||
(`secure_tdm.rs`).
|
||||
- Optional Ed25519 signature at session/batch granularity for signed
|
||||
`CHANNEL_PLAN` and `CALIBRATION_START` messages, reusing the
|
||||
ADR-040/RVF Ed25519 path already shipping in firmware.
|
||||
|
||||
## Reuse map (do not rewrite)
|
||||
|
||||
| Concern | Existing component |
|
||||
|-----------------------------|----------------------------------------------------------------------------------------------------------|
|
||||
| ADR-018 binary frame | `firmware/esp32-csi-node/main/csi_collector.c` (magic `0xC5110001`) |
|
||||
| ESP32 CSI driver glue | `firmware/esp32-csi-node/main/csi_collector.c:225-303` |
|
||||
| Channel hopping | `csi_collector_set_hop_table()` and `csi_collector_start_hop_timer()` |
|
||||
| NDP injection | `csi_inject_ndp_frame()` (placeholder, sufficient for L1 binding) |
|
||||
| TDM scheduling | `crates/wifi-densepose-hardware/src/esp32/tdm.rs` |
|
||||
| Secure beacons | `crates/wifi-densepose-hardware/src/esp32/secure_tdm.rs` (HMAC + replay) |
|
||||
| Edge intelligence (Tier 1/2)| `firmware/esp32-csi-node/main/edge_processing.c` (magic `0xC5110002`/`0xC5110005`) |
|
||||
| Fused vitals | ADR-063 `edge_fused_vitals_pkt_t` (magic `0xC5110004`) |
|
||||
| Swarm bridge | `firmware/esp32-csi-node/main/swarm_bridge.c` |
|
||||
| WASM Tier 3 modules | `firmware/esp32-csi-node/main/wasm_runtime.c` (ADR-040) |
|
||||
| Multistatic fusion | `crates/wifi-densepose-ruvector/src/viewpoint/fusion.rs` |
|
||||
| Adaptive classifier | `crates/wifi-densepose-sensing-server/src/adaptive_classifier.rs:61-75` |
|
||||
| Feature primitives (Rust) | `crates/wifi-densepose-signal/src/{motion.rs,features.rs,ruvsense/coherence.rs}` |
|
||||
|
||||
## Implementation status (2026-04-19)
|
||||
|
||||
This ADR ships **with** the initial implementation, not ahead of it.
|
||||
Artifacts delivered alongside the ADR:
|
||||
|
||||
| Component | File | State |
|
||||
|-----------------------------------------|-------------------------------------------------------------------------|-------------|
|
||||
| L1 vtable + profile/mode/health enums | `firmware/esp32-csi-node/main/rv_radio_ops.h` | Implemented |
|
||||
| L1 ESP32 binding | `firmware/esp32-csi-node/main/rv_radio_ops_esp32.c` | Implemented |
|
||||
| L1 Mock (QEMU) binding | `firmware/esp32-csi-node/main/rv_radio_ops_mock.c` | Implemented |
|
||||
| L2 Controller FreeRTOS plumbing | `firmware/esp32-csi-node/main/adaptive_controller.c` | Implemented |
|
||||
| L2 Pure decision policy (testable) | `firmware/esp32-csi-node/main/adaptive_controller_decide.c` | Implemented |
|
||||
| L3 Mesh-plane types + encoder/decoder | `firmware/esp32-csi-node/main/rv_mesh.{h,c}` | Implemented |
|
||||
| L3 HEALTH emit (slow loop, 30 s) | `adaptive_controller.c:slow_loop_cb()` | Implemented |
|
||||
| L3 ANOMALY_ALERT on state transition | `adaptive_controller.c:apply_decision()` | Implemented |
|
||||
| L3 Role tracking + epoch monotonicity | `adaptive_controller.c` (`s_role`, `s_mesh_epoch`) | Implemented |
|
||||
| L4 Feature state packet + helpers | `firmware/esp32-csi-node/main/rv_feature_state.{h,c}` | Implemented |
|
||||
| L4 Emitter from fast loop (5 Hz) | `adaptive_controller.c:emit_feature_state()` | Implemented |
|
||||
| L1 Packet yield + send-fail accessors | `csi_collector.c:csi_collector_get_pkt_yield_per_sec()` + send fail | Implemented |
|
||||
| L5 Rust mirror trait + mesh decoder | `crates/wifi-densepose-hardware/src/radio_ops.rs` | Implemented |
|
||||
| Host C unit tests (60 assertions) | `firmware/esp32-csi-node/tests/host/` | **60/60 ✓** |
|
||||
| Rust unit tests (8 assertions) | `crates/wifi-densepose-hardware` (`radio_ops::tests`) | **8/8 ✓** |
|
||||
| QEMU validator hooks (3 new checks) | `scripts/validate_qemu_output.py` (check 17/18/19) | Passing |
|
||||
| L3 mesh RX path (receive + dispatch) | — | Phase 3.5 |
|
||||
| Ed25519 signing for CHANNEL_PLAN etc. | — | Phase 3.5 |
|
||||
| Hardware validation on COM7 | — | Pending |
|
||||
|
||||
## Measured performance
|
||||
|
||||
Host-side benchmarks (`firmware/esp32-csi-node/tests/host/`), x86-64,
|
||||
gcc `-O2`, 2026-04-19. Numbers are illustrative of algorithmic cost on
|
||||
a modern CPU; on-target ESP32-S3 Xtensa LX7 at 240 MHz is ~5–10×
|
||||
slower for bit-by-bit CRC and broadly comparable for the decide
|
||||
function after inlining.
|
||||
|
||||
| Operation | Cost per call | Notes |
|
||||
|---------------------------------------------|---------------------|-------------------------------------|
|
||||
| `adaptive_controller_decide()` | **3.2 ns** (host) | O(1) policy, 9 branches evaluated |
|
||||
| `rv_feature_state_crc32()` (56 B hashed) | **612 ns** (host) | 87 MB/s — bit-by-bit IEEE CRC32 |
|
||||
| `rv_feature_state_finalize()` (full) | **592 ns** (host) | CRC-dominated |
|
||||
| `rv_mesh_encode_health()` + `_decode()` | **1010 ns** (host) | Full roundtrip, hdr+payload+CRC |
|
||||
|
||||
Projected on-target cost at 5 Hz cadence:
|
||||
|
||||
| Budget | Value |
|
||||
|--------------------------------------------|---------------------|
|
||||
| Controller fast-loop tick work (ESP32-S3) | < 10 μs (est.) |
|
||||
| CRC32 per feature packet (ESP32-S3) | ~3–6 μs (est.) |
|
||||
| Feature-state emit cost @ 5 Hz | ~30 μs/sec (0.003%) |
|
||||
| UDP send cost (existing stream_sender) | — unchanged — |
|
||||
|
||||
**Bandwidth:**
|
||||
|
||||
| Mode | Rate |
|
||||
|---------------------------------------------|-------------|
|
||||
| Raw ADR-018 CSI (pre-ADR-081) | ~100 KB/s |
|
||||
| ADR-039 compressed CSI (Tier 1) | ~50–70 KB/s |
|
||||
| ADR-039 vitals packet (32 B @ 1 Hz) | 32 B/s |
|
||||
| **ADR-081 feature state (60 B @ 5 Hz)** | **300 B/s** |
|
||||
|
||||
**Memory:**
|
||||
|
||||
| Component | Static RAM |
|
||||
|---------------------------------------------|---------------------|
|
||||
| Controller state (s_cfg + s_last_obs + …) | ~80 bytes |
|
||||
| Feature-state emit packet (stack, per tick) | 60 bytes |
|
||||
| CRC lookup table | 0 (bit-by-bit) |
|
||||
| Three FreeRTOS software timers | ~3 × 56 B overhead |
|
||||
|
||||
**Tests:**
|
||||
|
||||
| Suite | Assertions | Result |
|
||||
|---------------------------------------------|-----------:|------------|
|
||||
| `test_adaptive_controller` (host C) | 18 | **PASS** |
|
||||
| `test_rv_feature_state` (host C) | 15 | **PASS** |
|
||||
| `test_rv_mesh` (host C) | 27 | **PASS** |
|
||||
| `radio_ops::tests` (Rust) | 8 | **PASS** |
|
||||
| **Total** | **68** | **68/68** |
|
||||
| QEMU validator (`ADR-061` pipeline) | +3 checks | hooked |
|
||||
|
||||
Cross-language parity: the Rust `crc32_ieee()` is verified against the
|
||||
same known vectors used by the C test (`0xCBF43926` for `"123456789"`,
|
||||
`0xD202EF8D` for a single zero byte), and the `mesh_constants_match_firmware`
|
||||
test asserts `MESH_MAGIC`, `MESH_VERSION`, `MESH_HEADER_SIZE`, and
|
||||
`MESH_MAX_PAYLOAD` match the C header byte-for-byte. Any drift between
|
||||
the two implementations fails CI.
|
||||
|
||||
## New components this ADR authorizes
|
||||
|
||||
| New file | Purpose |
|
||||
|-------------------------------------------------------------------------------------------|--------------------------------------------------------|
|
||||
| `firmware/esp32-csi-node/main/rv_radio_ops.h` | `rv_radio_ops_t` vtable + profile/mode/health enums |
|
||||
| `firmware/esp32-csi-node/main/rv_radio_ops_esp32.c` | ESP32 binding wrapping `csi_collector` + `esp_wifi_*` |
|
||||
| `firmware/esp32-csi-node/main/rv_feature_state.h` | `rv_feature_state_t` packet + `RV_FEATURE_STATE_MAGIC` |
|
||||
| `firmware/esp32-csi-node/main/adaptive_controller.h` | Controller API + observation/decision structs |
|
||||
| `firmware/esp32-csi-node/main/adaptive_controller.c` | 200 ms / 1 s / 30 s loops, FreeRTOS task |
|
||||
| `crates/wifi-densepose-hardware/src/radio_ops.rs` *(Phase 4 follow-up)* | Rust mirror trait for backend swapping |
|
||||
|
||||
## Roadmap
|
||||
|
||||
| Phase | Scope | Status |
|
||||
|-------|--------------------------------------------|--------------------------------------------------|
|
||||
| 1 | Single supported-CSI node + features → Rust | Largely done via ADR-018, ADR-039 |
|
||||
| 2 | 3-node Seed v2 mesh + time-sync + plan | Partially done (ADR-029, ADR-066, ADR-073) |
|
||||
| 3 | Adaptive controller, delta reporting, DEGRADED | **This ADR** authorizes the firmware skeleton |
|
||||
| 4 | Cross-chipset bindings (Nexmon, custom) | Reserved; gated by Phase 3 stability |
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. **Portability gate.** A second `rv_radio_ops_t` binding (mock or
|
||||
alternate chipset) compiles and runs the controller + mesh plane code
|
||||
unchanged. The signal/ruvector/train/mat crates compile against a Rust
|
||||
mirror trait without modification.
|
||||
2. **Mesh resilience benchmark.** A 3-node prototype maintains stable
|
||||
`presence_score` and `motion_score` when one observer changes channel
|
||||
or drops out for 5 seconds.
|
||||
3. **Default upstream is compact.** Raw ADR-018 CSI is off by default; the
|
||||
default upstream is `rv_feature_state_t` at 1–10 Hz.
|
||||
4. **Integrity.** Every `FEATURE_DELTA` carries node id, seq, ts_us, CRC32.
|
||||
Every control message carries epoch + replay-window + authorization
|
||||
class, verified against ADR-032's existing HMAC machinery.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- The firmware hack is no longer the moat. The 5 layers are explicit and
|
||||
separately testable.
|
||||
- Default upstream bandwidth drops ~99% vs. raw ADR-018, making 50+ node
|
||||
deployments practical.
|
||||
- A documented vtable + Kconfig surface gates new features ("which layer
|
||||
does this belong in?") instead of letting them accrete inline.
|
||||
- Adaptive control of cadence, channel, and role becomes a first-class
|
||||
firmware concern — the user-facing knob ("be smarter when busy, save
|
||||
power when idle") finally has a home.
|
||||
|
||||
### Negative
|
||||
|
||||
- An abstraction tax on the single-chipset case: `rv_radio_ops_t` is a
|
||||
vtable for a family currently of size 1.
|
||||
- Adds ~5–8 KB SRAM for controller state and the new feature-state ring.
|
||||
- Requires re-routing existing `swarm_bridge` traffic through the mesh
|
||||
plane message types over time (incremental, not breaking).
|
||||
|
||||
### Neutral
|
||||
|
||||
- This ADR introduces no new dependencies, no new networking stacks, and
|
||||
no new hardware requirements.
|
||||
- ADR-039, ADR-063, ADR-066, ADR-069, ADR-073 are **not superseded**; they
|
||||
are reframed as components of Layer 3 / Layer 4.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# Host-side C unit tests (no ESP-IDF, no QEMU required)
|
||||
cd firmware/esp32-csi-node/tests/host
|
||||
make check
|
||||
# → test_adaptive_controller: 18/18 pass, decide() = 3.2 ns/call
|
||||
# → test_rv_feature_state: 15/15 pass, CRC32(56 B) = 612 ns/pkt
|
||||
# → test_rv_mesh: 27/27 pass, HEALTH roundtrip = 1.0 µs
|
||||
|
||||
# Rust-side radio_ops trait + mesh decoder tests
|
||||
cd v2
|
||||
cargo test -p wifi-densepose-hardware --no-default-features --lib radio_ops
|
||||
# → 8 passed; verifies MockRadio, CRC32 parity with firmware vectors,
|
||||
# HEALTH encode/decode roundtrip, bad-magic/short/CRC rejection,
|
||||
# and that MESH_MAGIC/VERSION/HEADER_SIZE match rv_mesh.h
|
||||
|
||||
# QEMU end-to-end (requires ESP-IDF + qemu-system-xtensa, see ADR-061)
|
||||
bash scripts/qemu-esp32s3-test.sh
|
||||
# → Validator now runs 19 checks; new ADR-081 checks 17/18/19 verify
|
||||
# adaptive_ctrl boot line, rv_radio_mock binding registration, and
|
||||
# slow-loop heartbeat.
|
||||
|
||||
# Full workspace
|
||||
cargo test --workspace --no-default-features
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
ADR-018, ADR-028, ADR-029, ADR-030, ADR-031, ADR-032, ADR-039, ADR-040,
|
||||
ADR-060, ADR-061, ADR-063, ADR-066, ADR-069, ADR-073, ADR-078.
|
||||
@@ -1,185 +0,0 @@
|
||||
# ADR-082: Pose Tracker Confirmed-Track Output Filter
|
||||
|
||||
| Field | Value |
|
||||
|-------------|-----------------------------------------------------------------------|
|
||||
| **Status** | Accepted — implemented in commit landing this ADR |
|
||||
| **Date** | 2026-04-25 |
|
||||
| **Authors** | ruv |
|
||||
| **Issue** | [#420 — "24 ghost people in the UI with 3× ESP32-S3 nodes"](https://github.com/ruvnet/RuView/issues/420) |
|
||||
| **Depends** | ADR-026 (track lifecycle), ADR-024 (AETHER re-ID embeddings) |
|
||||
|
||||
## Context
|
||||
|
||||
Multiple users running the Rust sensing server with 3 ESP32-S3 nodes have
|
||||
reported the same symptom: the live UI renders 22–24 phantom skeletons that
|
||||
flicker at high rate, while `GET /api/v1/sensing/latest` correctly reports
|
||||
`estimated_persons: 1`. The problem is reproducible across both Docker and
|
||||
native deployments and is independent of the firmware MGMT-only mitigation
|
||||
shipped for #396.
|
||||
|
||||
The two-number contradiction (1 in the snapshot, ~24 in the WebSocket stream)
|
||||
narrows the bug to the path that produces `update.persons`. That path is
|
||||
`tracker_bridge::tracker_update` → `tracker_bridge::tracker_to_person_detections`
|
||||
→ WebSocket frame.
|
||||
|
||||
### Pose tracker lifecycle (per ADR-026)
|
||||
|
||||
`signal::ruvsense::pose_tracker::TrackLifecycleState` has four states:
|
||||
|
||||
```
|
||||
Tentative -> Active -> Lost -> Terminated
|
||||
```
|
||||
|
||||
The state machine and its predicates:
|
||||
|
||||
| State | `is_alive()` | `accepts_updates()` | Meaning |
|
||||
|--------------|--------------|---------------------|---------|
|
||||
| `Tentative` | true | true | New detection, < 2 confirmed hits |
|
||||
| `Active` | true | true | Confirmed track, currently observed |
|
||||
| `Lost` | **true** | false | Confirmed track, missed `loss_misses` updates, still inside `reid_window` |
|
||||
| `Terminated` | false | false | Removed on next `prune_terminated()` |
|
||||
|
||||
`PoseTracker::active_tracks()` filters by `is_alive()`, which means it returns
|
||||
`Tentative ∪ Active ∪ Lost` — every track that has not yet been Terminated.
|
||||
|
||||
### Root cause
|
||||
|
||||
`crates/wifi-densepose-sensing-server/src/tracker_bridge.rs` exposes the
|
||||
tracker output to the WebSocket stream via:
|
||||
|
||||
```rust
|
||||
/// Convert active PoseTracker tracks back into server-side PersonDetection values.
|
||||
///
|
||||
/// Only tracks whose lifecycle `is_alive()` are included.
|
||||
pub fn tracker_to_person_detections(tracker: &PoseTracker) -> Vec<PersonDetection> {
|
||||
tracker
|
||||
.active_tracks()
|
||||
.into_iter()
|
||||
.map(|track| { /* ... */ })
|
||||
.collect()
|
||||
}
|
||||
```
|
||||
|
||||
The doc comment is correct as a description of `is_alive()`, but `is_alive()`
|
||||
is the wrong gate for *rendering*. `Lost` tracks have not received a
|
||||
measurement in `loss_misses` ticks; they are kept around only so the
|
||||
re-identification machinery can attempt to match them when a similar
|
||||
detection reappears within `reid_window`. They are not currently observed and
|
||||
must not appear as live skeletons in the UI.
|
||||
|
||||
With 3 ESP32-S3 nodes streaming CSI at ~10 Hz each, `derive_pose_from_sensing`
|
||||
emits a per-node detection every tick. Detections that fall outside the
|
||||
Mahalanobis gate (cost ≥ 9.0) cannot match an existing track, so a new
|
||||
`Tentative` track is created and the previous one ages into `Lost`. With
|
||||
`reid_window ≈ 30` ticks (~3 s at 10 Hz), up to 30 ticks × 3 nodes ≈ 90
|
||||
phantom Lost tracks can co-exist before any of them reach `Terminated`.
|
||||
The actually-observed-now person is one of them; the other ~22–89 are ghosts.
|
||||
|
||||
The snapshot endpoint `/api/v1/sensing/latest` reads `estimated_persons` from
|
||||
the multistatic eigenvalue counter (`signal::ruvsense::field_model`), which
|
||||
operates on the CSI data directly and reports 1. The WebSocket stream reads
|
||||
`update.persons`, which is the unfiltered `is_alive()` set — hence the
|
||||
22-vs-1 mismatch.
|
||||
|
||||
This is a documentation/implementation discrepancy in `tracker_bridge`, not a
|
||||
flaw in the lifecycle state machine itself.
|
||||
|
||||
## Decision
|
||||
|
||||
Introduce a **confirmed-track filter** at the bridge boundary that returns
|
||||
only tracks the UI is meant to render:
|
||||
|
||||
* `Active` — confirmed and currently observed; always render.
|
||||
* `Tentative` — confirmed for the *current* tick (created or matched this
|
||||
cycle); render so first-frame visibility latency stays at one tick.
|
||||
* `Lost` — **never** render. They exist only to support re-ID over the
|
||||
`reid_window` and have, by definition, not been observed for at least
|
||||
`loss_misses` ticks.
|
||||
* `Terminated` — never render (already excluded by `is_alive()`).
|
||||
|
||||
### Naming
|
||||
|
||||
Add `PoseTracker::confirmed_tracks()` — the name reflects "tracks the system
|
||||
is currently confirming a person is present at this position." Keep
|
||||
`active_tracks()` unchanged so callers that legitimately need the re-ID set
|
||||
(re-identification, soft-confidence overlays, debug UIs) still have it.
|
||||
|
||||
The bridge’s public surface stays the same; only the internal accessor
|
||||
swaps. WebSocket consumers see the corrected `update.persons` automatically.
|
||||
|
||||
### Why include `Tentative`
|
||||
|
||||
A walking person’s first detection lands in `Tentative` until two consecutive
|
||||
hits arrive (~0.1 s at 10 Hz). Excluding `Tentative` makes the UI
|
||||
under-render by one tick on every entry; the gain (filtering out spurious
|
||||
single-detection ghosts) is real but small relative to the much larger Lost
|
||||
problem and isn’t worth the visible latency. If single-tick ghosts become
|
||||
the dominant complaint after this ADR ships, escalate to `Active`-only and
|
||||
revisit `birth_hits` calibration.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
* `update.persons.length` matches `estimated_persons` within ±1 (Tentative
|
||||
vs. Active hand-off frame) under steady state. #420 closed.
|
||||
* No change to the lifecycle state machine, no change to `reid_window` or
|
||||
`loss_misses`, no change to the WebSocket schema. Pure filter at egress.
|
||||
* `PoseTracker::active_tracks()` keeps its semantics for re-ID consumers;
|
||||
this avoids breaking ADR-024 (AETHER) call sites.
|
||||
|
||||
### Negative / risks
|
||||
|
||||
* Existing test `test_tracker_update_stable_ids` exercises three sequential
|
||||
identical-person updates and asserts the ID is stable across all three.
|
||||
Filtering Lost out doesn’t affect it (the track stays in `Tentative` →
|
||||
`Active`, never Lost during the test). Confirmed by reading the test;
|
||||
no regression expected.
|
||||
* Single-tick `Tentative` exposure means very-spurious one-frame detections
|
||||
*can* still flicker briefly. Acceptable trade-off as discussed above.
|
||||
|
||||
### Neutral
|
||||
|
||||
* `prune_terminated()` and the existing transition logic
|
||||
(`predict_all` → `mark_lost` → `terminate`) are unchanged.
|
||||
|
||||
## Implementation
|
||||
|
||||
1. **`signal::ruvsense::pose_tracker`** — add:
|
||||
```rust
|
||||
/// Tracks the UI is meant to render: Tentative + Active.
|
||||
/// Excludes Lost (re-ID candidates) and Terminated.
|
||||
pub fn confirmed_tracks(&self) -> Vec<&PoseTrack> {
|
||||
self.tracks
|
||||
.iter()
|
||||
.filter(|t| matches!(
|
||||
t.lifecycle,
|
||||
TrackLifecycleState::Tentative | TrackLifecycleState::Active
|
||||
))
|
||||
.collect()
|
||||
}
|
||||
```
|
||||
2. **`sensing-server::tracker_bridge`** — change
|
||||
`tracker_to_person_detections` to call `tracker.confirmed_tracks()` and
|
||||
update the doc comment to describe the new contract.
|
||||
3. **Regression test** in `tracker_bridge.rs::tests`:
|
||||
* Drive a track to `Active` over two updates.
|
||||
* Submit empty detections for `loss_misses + 1` predict cycles to push
|
||||
the track to `Lost`.
|
||||
* Assert `tracker_update(... empty ...)` returns an empty `Vec`.
|
||||
4. **Validation**: workspace tests + ESP32-S3 on COM7 streaming round-trip.
|
||||
|
||||
## Validation
|
||||
|
||||
* `cargo test --workspace --no-default-features` — must stay green
|
||||
(≥ 1,538 passed, 0 failed; new regression test adds one).
|
||||
* Live verification on ESP32 setup: WebSocket `update.persons.length`
|
||||
must equal `estimated_persons` ± 1 in steady state.
|
||||
|
||||
## Related
|
||||
|
||||
* ADR-026 — Track lifecycle state machine (this ADR doesn’t change it)
|
||||
* ADR-024 — AETHER re-ID embeddings (uses `active_tracks()`, unchanged)
|
||||
* PR #425 — Workspace `--no-default-features` build fix (unrelated, just
|
||||
the prior PR on this branch line)
|
||||
* Issue #420 — original report
|
||||
@@ -1,245 +0,0 @@
|
||||
# ADR-083: Per-Cluster Pi Compute Hop
|
||||
|
||||
| Field | Value |
|
||||
|----------------|--------------------------------------------------------------------------------------|
|
||||
| **Status** | Proposed — pending field evidence on three-tier proposal scope |
|
||||
| **Date** | 2026-04-26 |
|
||||
| **Authors** | ruv |
|
||||
| **Supersedes** | — |
|
||||
| **Refines** | ADR-028 (capability audit), ADR-081 (5-layer kernel), ADR-066 (swarm bridge) |
|
||||
| **Companion** | `docs/research/architecture/three-tier-rust-node.md`, `docs/research/architecture/decision-tree.md`, `docs/research/sota/2026-Q2-rf-sensing-and-edge-rust.md` |
|
||||
|
||||
## Context
|
||||
|
||||
ADR-028 established the per-node BOM at ~$9 (ESP32-S3 8MB) — ~$15 with a
|
||||
mmWave sensor — and ADR-081 framed the firmware as a 5-layer adaptive
|
||||
kernel running entirely on a single ESP32-S3 die. Both decisions are
|
||||
correct for the **per-node** dimension; deployments that fit the
|
||||
"sensor talks UDP to a server somewhere" shape work fine on this stack.
|
||||
|
||||
The three-tier-node research exploration
|
||||
(`docs/research/architecture/three-tier-rust-node.md`) raised a separate
|
||||
question: **what changes when a deployment scales past one or two rooms,
|
||||
and where should the heavy compute live?** The exploration's answer
|
||||
("dual ESP32-S3 + Pi Zero 2W per node") is one shape, but the
|
||||
companion decision-tree (`decision-tree.md` §1, §3 L3, §5) identifies a
|
||||
materially cheaper path: keep today's single-S3 sensor node unchanged
|
||||
and add **one Pi per cluster of 3–6 sensor nodes**. The 2026-Q2 SOTA
|
||||
survey (`sota/2026-Q2-rf-sensing-and-edge-rust.md`) confirms that the
|
||||
load this path needs to carry — model inference, QUIC backhaul, and a
|
||||
real secure-boot story — fits comfortably on a Pi-class SoC, while the
|
||||
load it doesn't need to carry — CSI capture, ISR-precise wake control —
|
||||
is exactly what the ESP32-S3 already does well.
|
||||
|
||||
The three things this ADR is about, all of which the current single-S3
|
||||
deployment shape pushes onto the cloud or onto every individual node:
|
||||
|
||||
1. **Per-deployment ML inference.** WiFlow / DT-Pose / GraphPose-Fi
|
||||
class models (4–10M params, 0.5–1.5 GFLOPs) want a Cortex-A53-class
|
||||
target. The ESP32-S3 cannot host these; the cloud can but only at
|
||||
the cost of round-trip latency. A per-cluster Pi inference hop is
|
||||
the natural home.
|
||||
2. **QUIC backhaul.** `quinn` + `rustls` is mature on Linux but does
|
||||
not run on ESP32-class hardware in any production-grade form
|
||||
(SOTA §5). A Pi terminating QUIC for a cluster gives every sensor
|
||||
node QUIC's loss/handoff/multiplex properties without porting QUIC
|
||||
to the MCU.
|
||||
3. **Secure-boot anchor for OTA.** ESP-IDF Secure Boot V2 covers each
|
||||
sensor node, but cluster-wide policy (which model is current, which
|
||||
sensor MCU image is canary, what is the rollout ring) needs a
|
||||
higher-trust local store. A Pi running buildroot + dm-verity +
|
||||
signed FIT is a defensible anchor without the BOM hit of CM4 / Pi 5
|
||||
(the latter is its own decision; see ADR-085 sketch below and
|
||||
decision-tree.md L6).
|
||||
|
||||
The cluster-Pi shape does **not** require any change to ADR-028 or
|
||||
ADR-081. The sensor node continues to be a single-MCU ESP32-S3 running
|
||||
the 5-layer kernel. Everything new lives at the cluster boundary.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt **a per-cluster Pi hop** as the canonical RuView mid-scale
|
||||
deployment shape. A "cluster" is **3–6 ESP32-S3 sensor nodes within
|
||||
WiFi mesh range of one Pi**.
|
||||
|
||||
Specifically:
|
||||
|
||||
1. **Sensor nodes are unchanged.** They continue to run the ADR-081
|
||||
5-layer kernel on a single ESP32-S3, emit `rv_feature_state_t`
|
||||
packets (60 byte, ~5 Hz, ~300 B/s) over UDP, and connect via
|
||||
ESP-WIFI-MESH or direct WiFi to the cluster Pi.
|
||||
2. **Each cluster has exactly one Pi** acting as:
|
||||
- **Sensor aggregator**: ingests UDP from all cluster sensor
|
||||
nodes, runs feature-level fusion (multistatic + viewpoint
|
||||
attention from the existing `wifi-densepose-ruvector` crate).
|
||||
- **ML inference target**: hosts the WiFi-pose model and runs
|
||||
inference at the cluster boundary, not on each sensor MCU.
|
||||
- **QUIC client to the cloud / gateway**: terminates QUIC mTLS,
|
||||
batches cluster-level events.
|
||||
- **OTA + secure-boot anchor for its sensor nodes**: holds signed
|
||||
manifests, stages canary rollouts, owns provisioning state.
|
||||
3. **Cluster Pi SoC choice is deferred** to a future ADR (sketched
|
||||
below as ADR-085). The acceptable candidates are Pi Zero 2W, Pi 4,
|
||||
Pi 5, and CM4. The decision tree's L6 distinguishes these by
|
||||
secure-boot threat model; this ADR does not pre-commit.
|
||||
4. **The single-node deployment shape is not deprecated.** A
|
||||
home-lab / single-room / development deployment can still run a
|
||||
single ESP32-S3 talking UDP directly to the existing
|
||||
`wifi-densepose-sensing-server`, no Pi required. The cluster Pi
|
||||
becomes the recommended shape for fleets ≥ 3 sensor nodes.
|
||||
|
||||
### Boundary contract
|
||||
|
||||
The cluster Pi exposes two interfaces:
|
||||
|
||||
| Interface | Direction | Schema |
|
||||
|------------------------|-------------------|-----------------------------------------------------------------------|
|
||||
| **UDP `rv_feature_state_t` ingest** | sensor → Pi | Existing 60-byte packed struct from ADR-081 (magic `0xC5110006`) |
|
||||
| **QUIC mTLS uplink** | Pi → gateway/cloud | New: cluster-level event envelope (CBOR), batched, ~10 KB/min upper bound |
|
||||
|
||||
Sensor → Pi is **the same wire as today's sensor → server**. Cluster Pi
|
||||
uplink is **new** and is what the existing `wifi-densepose-sensing-server`
|
||||
becomes — relocated from the user's laptop / container to the cluster
|
||||
node. Concretely: the sensing server already exists in
|
||||
`crates/wifi-densepose-sensing-server`; it cross-compiles to ARMv7 /
|
||||
AArch64 today via `cargo build --target aarch64-unknown-linux-gnu`. The
|
||||
relocation is a deployment change, not a re-implementation.
|
||||
|
||||
### Three-tier vs cluster hop
|
||||
|
||||
This ADR's cluster-Pi shape is the L3-hybrid path in
|
||||
`decision-tree.md` §2 — **not** the full three-tier (dual-MCU + per-node
|
||||
Pi) shape. It captures most of the value (ML, QUIC, secure-boot anchor)
|
||||
at minimal BOM impact. The full three-tier shape remains the long-term
|
||||
exploration target, blocked behind L4 (no_std CSI maturity) and L2
|
||||
(per-node ISR-jitter evidence).
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **Pose-grade ML on edge becomes deployable**, not just possible. A
|
||||
Pi (any of the eligible SoCs) hosts WiFlow-class models with
|
||||
≤ 100 ms latency per cluster, vs ≥ 1 s round-trip if pose runs in the
|
||||
cloud (SOTA §1, §3).
|
||||
- **QUIC arrives without an MCU port.** `quinn` + `rustls` runs on the
|
||||
Pi as it does on a server (SOTA §5). The sensor MCU keeps UDP — the
|
||||
cheapest, highest-tested wire it already speaks.
|
||||
- **Cluster-level secure boot becomes coherent.** Per-sensor Secure
|
||||
Boot V2 + flash encryption (ADR-028 baseline) is unchanged. The Pi
|
||||
buildroot + dm-verity image is the cluster trust anchor and signs
|
||||
the OTA manifests for its sensors. The cluster-level threat model is
|
||||
expressible without per-sensor BOM regression.
|
||||
- **No PCB respin.** Sensor nodes are bit-for-bit identical to today's
|
||||
ADR-028 baseline. The cluster Pi is a separate device on the cluster
|
||||
WiFi (and / or Ethernet, if available).
|
||||
- **Deployment cost scales sub-linearly with sensor count.** One
|
||||
$25–$60 Pi per 3–6 sensor nodes adds ~$5–$20 per sensor amortized,
|
||||
vs ~$25–$50 per sensor for the per-node-Pi shape.
|
||||
|
||||
### Negative
|
||||
|
||||
- **The cluster Pi is a new piece of infrastructure to provision,
|
||||
monitor, and update.** It is the right place for cluster-level
|
||||
responsibilities, but it is not free; it adds a Linux box to every
|
||||
multi-room deployment. Mitigated by buildroot images and the
|
||||
existing OTA tooling story (see Implementation §4).
|
||||
- **Cluster Pi failure takes the cluster offline** (sensor nodes
|
||||
cannot uplink without a working aggregator on the WiFi LAN). For
|
||||
high-availability deployments, this ADR is the floor; an HA-pair
|
||||
cluster Pi would be a follow-up.
|
||||
- **One more network hop on the sensing path.** Sensor → Pi → cloud
|
||||
adds ~5–20 ms over Sensor → cloud (depending on link quality).
|
||||
Pose latency budgets are 100s of ms, so this is well inside spec.
|
||||
|
||||
### Neutral
|
||||
|
||||
- ADR-028 (capability audit), ADR-081 (5-layer kernel), and ADR-066
|
||||
(swarm bridge) are unchanged. This ADR adds a new device class above
|
||||
the sensor; it does not modify the sensor itself.
|
||||
- The home-lab single-node shape continues to work; this ADR adds a
|
||||
recommended path for fleets, it does not deprecate the existing one.
|
||||
|
||||
## Implementation
|
||||
|
||||
The implementation is intentionally light because most of the pieces
|
||||
already exist; the ADR is largely about formalizing where they live.
|
||||
|
||||
1. **Cluster-Pi cross-compile target.** Add to
|
||||
`rust-port/wifi-densepose-rs/.cargo/config.toml` (or the equivalent
|
||||
per-crate target spec) an `aarch64-unknown-linux-gnu` target so
|
||||
`wifi-densepose-sensing-server` builds for Pi 4 / 5 / CM4 by
|
||||
default. Also retain `armv7-unknown-linux-gnueabihf` for Pi Zero 2W
|
||||
compatibility while the Pi-SoC decision (ADR-085 sketch) is open.
|
||||
2. **Cluster-Pi service unit.** Add a systemd unit file under
|
||||
`firmware/cluster-pi/` (new directory) that runs
|
||||
`wifi-densepose-sensing-server` with the cluster's UDP/QUIC ports
|
||||
and drops privileges. Buildroot integration is a separate ADR if
|
||||
the SoC choice goes to Pi Zero 2W (where there's no RPi-OS path).
|
||||
3. **QUIC uplink module.** Add `wifi-densepose-sensing-server` a
|
||||
feature-gated `quic-uplink` module using `quinn` + `rustls`. The
|
||||
feature is **off by default** in the home-lab shape and on for the
|
||||
cluster Pi.
|
||||
4. **OTA + signed-manifest flow.** Out of scope for this ADR; tracked
|
||||
as I4 in `decision-tree.md` §4. The cluster Pi's role is to *hold*
|
||||
the manifest store, not to define the manifest format. Use the
|
||||
existing ADR-066 swarm bridge channel for OTA staging.
|
||||
5. **Documentation update.** README's hardware-table gains a
|
||||
"Cluster compute" row. CLAUDE.md gets a one-paragraph cluster-Pi
|
||||
section under Architecture. User-guide gets a cluster-deployment
|
||||
section.
|
||||
6. **Validation.** A 3-sensor cluster + 1 Pi fixture in the lab.
|
||||
Pass criteria: end-to-end CSI → cluster fusion → cloud ingest;
|
||||
measured latency under 100 ms per cluster; cluster Pi reboot
|
||||
without sensor data loss > 5 s; OTA staging round-trip across all
|
||||
sensors in the cluster.
|
||||
|
||||
## Validation
|
||||
|
||||
This ADR is **proposed**, not accepted. Acceptance requires:
|
||||
|
||||
1. The cluster-Pi `wifi-densepose-sensing-server` cross-compiles
|
||||
cleanly on `aarch64-unknown-linux-gnu` and `armv7-unknown-linux-gnueabihf`
|
||||
targets with the existing workspace tests passing.
|
||||
2. A 3-sensor + 1-Pi field test demonstrates ≥ 4 hours stable
|
||||
end-to-end CSI → fusion → cloud round-trip with latency
|
||||
≤ 100 ms per cluster and zero phantom-skeleton regressions
|
||||
(ADR-082 holds across the new uplink).
|
||||
3. The cluster-Pi ↔ sensor secure-boot story is approved alongside
|
||||
ADR-085's SoC choice.
|
||||
|
||||
When the above pass, this ADR moves from **Proposed** → **Accepted**
|
||||
and the README + CLAUDE.md are updated to reflect cluster-Pi as the
|
||||
recommended fleet-shape.
|
||||
|
||||
## Related ADRs (current and proposed)
|
||||
|
||||
- **ADR-028** (Accepted) — ESP32 capability audit. Single-node BOM
|
||||
baseline. Unchanged by this ADR.
|
||||
- **ADR-029** (Proposed) — RuvSense multistatic sensing mode. Pairs
|
||||
naturally with cluster-Pi: cluster Pi is the natural home for
|
||||
multi-sensor fusion.
|
||||
- **ADR-066** — Swarm bridge to coordinator. The cluster-Pi is the
|
||||
per-cluster swarm coordinator endpoint.
|
||||
- **ADR-081** (Accepted) — 5-layer adaptive CSI mesh firmware kernel.
|
||||
Unchanged by this ADR.
|
||||
- **ADR-082** (Accepted) — Pose tracker confirmed-track output filter.
|
||||
Holds across UDP and QUIC uplinks identically.
|
||||
- **Future ADR (sketched in `decision-tree.md` L4)** — `no_std` CSI
|
||||
capture maturity benchmark. Gates the dual-MCU shape; not required
|
||||
for the cluster-Pi shape proposed here.
|
||||
- **Future ADR (sketched in `decision-tree.md` L6)** — Cluster-Pi SoC
|
||||
choice (Pi Zero 2W vs CM4 vs Pi 5). Pure secure-boot decision.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Cluster size sweet spot.** "3–6 nodes" is a planning estimate. The
|
||||
3-sensor lab fixture in §Implementation will inform whether the
|
||||
upper bound is closer to 4, 6, or 8 in practice.
|
||||
- **Cluster-Pi failure semantics.** Default behavior: sensor MCUs hold
|
||||
the last 60 s of feature packets in RAM and replay on reconnect.
|
||||
HA-pair cluster Pi is a separate ADR if needed.
|
||||
- **Mesh control-plane interaction.** If the deployment moves to
|
||||
Thread (decision-tree.md L5), the cluster Pi may need a Thread
|
||||
Border Router role. This ADR doesn't pre-commit; it's compatible
|
||||
with both ESP-WIFI-MESH and Thread futures.
|
||||
@@ -1,276 +0,0 @@
|
||||
# ADR-084: RaBitQ Similarity Sensor for CSI / Pose / Memory Routing
|
||||
|
||||
| Field | Value |
|
||||
|----------------|-----------------------------------------------------------------------------------------|
|
||||
| **Status** | Proposed |
|
||||
| **Date** | 2026-04-26 |
|
||||
| **Authors** | ruv |
|
||||
| **Refines** | ADR-024 (AETHER re-ID embeddings), ADR-027 (cross-environment domain generalization), ADR-076 (CSI spectrogram embeddings), ADR-081 (5-layer firmware kernel) |
|
||||
| **Companion** | ADR-083 (per-cluster Pi compute hop) |
|
||||
| **Implements** | `vendor/ruvector/crates/ruvector-core/src/quantization.rs::BinaryQuantized` |
|
||||
|
||||
## Context
|
||||
|
||||
RuView's signal pipeline already produces several **dense float
|
||||
embeddings** at different layers:
|
||||
|
||||
- AETHER 128-d re-ID embeddings on each `PoseTrack` (ADR-024)
|
||||
- 64–256-d CSI spectrogram embeddings (ADR-076)
|
||||
- per-room field-model eigenmode vectors (ADR-030)
|
||||
- per-frame multistatic fused vectors (ADR-029)
|
||||
|
||||
Every one of these eventually answers the same shape of question:
|
||||
**"have I seen something like this before?"** Today the answer is
|
||||
computed by full float dot-product / Mahalanobis comparisons against a
|
||||
candidate set. That cost grows linearly with stored vectors and
|
||||
quadratically when used inside dynamic-mincut graph maintenance,
|
||||
re-identification re-scoring, and cross-environment domain detection.
|
||||
|
||||
The vendored `ruvector-core` crate already ships a 1-bit quantization
|
||||
(`BinaryQuantized`, 32× compression, SIMD popcnt + hamming distance)
|
||||
that is functionally equivalent to the **RaBitQ** family of binary
|
||||
sketches: a vector is reduced to one bit per dimension, compared via
|
||||
hamming distance, and used as a coarse pre-filter before full
|
||||
precision refinement. The same module also exposes `ScalarQuantized`
|
||||
(int8, 4×) and `ProductQuantized` (PQ, 8–16×), so the tiered
|
||||
quantization story is already implemented; the *deployment pattern* is
|
||||
not.
|
||||
|
||||
The user observation that motivates this ADR: **RaBitQ-style sketches
|
||||
are not just a vector compression trick — they are a cheap similarity
|
||||
sensor.** Used as a sensor, they unlock:
|
||||
|
||||
- always-on novelty / anomaly gating that wakes heavy CNNs only on
|
||||
meaningful change
|
||||
- cluster-Pi memory routing (which shard / room / model to query first)
|
||||
- cross-node mesh exchange of compressed sketches instead of raw vectors
|
||||
- privacy-preserving event logs (sketches, not reconstructable signals)
|
||||
|
||||
This ADR formalizes the deployment pattern across the RuView stack and
|
||||
commits to `ruvector::quantization::BinaryQuantized` as the canonical
|
||||
implementation.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt **RaBitQ-style binary sketches as a first-class, cheap
|
||||
similarity sensor** at four points in the RuView pipeline:
|
||||
|
||||
1. **CSI / pose embedding hot-cache filter** at the cluster Pi.
|
||||
2. **Drift / novelty sensor** between live observation and a
|
||||
per-room normal-state bank.
|
||||
3. **Mesh-exchange compression** between sensor nodes when reporting
|
||||
cross-cluster events.
|
||||
4. **Privacy-preserving event log** at the cluster Pi and gateway.
|
||||
|
||||
The canonical pattern at every point is:
|
||||
|
||||
```text
|
||||
dense embedding ──► RaBitQ sketch ──► hamming/popcnt compare
|
||||
├──► candidate set (top-K)
|
||||
└──► novelty score (0..1)
|
||||
│
|
||||
▼
|
||||
┌── below threshold ──► emit summary, no escalation
|
||||
│
|
||||
└── above threshold ──► full-precision refinement
|
||||
├──► ruvector mincut / HNSW
|
||||
├──► AETHER re-ID rescoring
|
||||
└──► pose model / CNN wake
|
||||
```
|
||||
|
||||
### Implementation home
|
||||
|
||||
- **Sketch type and SIMD primitives**:
|
||||
`vendor/ruvector/crates/ruvector-core/src/quantization.rs::BinaryQuantized`
|
||||
— already implemented, already SIMD-accelerated (NEON on aarch64,
|
||||
POPCNT on x86_64). Re-export through a new
|
||||
`crates/wifi-densepose-ruvector/src/sketch.rs` module so consumers in
|
||||
`signal`, `train`, `mat`, and `sensing-server` see a stable
|
||||
RuView-flavored API and don't bind directly to the vendor crate.
|
||||
|
||||
- **Per-room normal-state bank**: lives at the cluster Pi (ADR-083),
|
||||
not on the sensor MCU. Sensor MCUs continue to emit dense embeddings
|
||||
in the existing `rv_feature_state_t` packet shape; sketching happens
|
||||
on the Pi where the candidate bank is.
|
||||
|
||||
- **Sketch versioning**: each sketch carries a 16-bit `sketch_version`
|
||||
field so the Pi can tell incompatible sketches apart when an
|
||||
embedding model upgrades. Bumped on every embedding-model change.
|
||||
|
||||
### Where the sensor sits in the pipeline
|
||||
|
||||
| Pipeline stage | Today (full float) | With RaBitQ similarity sensor |
|
||||
|---|---|---|
|
||||
| AETHER re-ID match | full 128-d cosine on every active track × candidate | hamming pre-filter to top-K, then full cosine on K |
|
||||
| Mincut subcarrier selection | full graph re-evaluation | sketch-flagged "likely-changed" boundary edges, full mincut on those |
|
||||
| CSI room fingerprint | trained classifier on full embedding | sketch hamming to per-room sketch, classifier on miss |
|
||||
| Field-model novelty (ADR-030) | residual-energy threshold | sketch novelty as second gate before SVD redo |
|
||||
| Mesh / inter-cluster sync | dense embedding broadcast | sketch broadcast; full vector only on miss |
|
||||
| Event log retention | full embedding stored | sketch + witness hash stored; raw embedding ephemeral |
|
||||
|
||||
In every row, the **decision boundary is unchanged** — full precision
|
||||
still owns the final answer. The sketch is a sensor that only gates
|
||||
which comparisons run, not what they decide.
|
||||
|
||||
### Acceptance criterion (per the source proposal)
|
||||
|
||||
The system-level acceptance test is:
|
||||
|
||||
> RaBitQ should reduce compare cost by **8× to 30×** while preserving
|
||||
> top-k decisions well enough that full refinement changes **fewer
|
||||
> than 10%** of final results.
|
||||
|
||||
Concretely, this means:
|
||||
|
||||
- Sketch compare must be measurably **8× cheaper** than the float
|
||||
comparison it replaces (criterion-bench in `signal/`).
|
||||
- Top-K candidate set chosen by sketch must contain ≥ 90% of the
|
||||
candidates the full-float pass would have picked (offline replay
|
||||
against recorded CSI).
|
||||
- End-to-end pose / re-ID accuracy must regress by **less than 1
|
||||
percentage point** vs the full-float baseline on the existing
|
||||
evaluation set.
|
||||
|
||||
If any of these three fail, the sensor is rolled back at that point in
|
||||
the pipeline and the failing site reverts to full float; the rest of
|
||||
the pipeline keeps using sketches. This is point-by-point, not
|
||||
all-or-nothing.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **Cheaper hot path everywhere a "have I seen this" question lives.**
|
||||
AETHER re-ID, mincut maintenance, room fingerprinting, novelty
|
||||
detection, mesh sync, and event-log retention all run a 32×-smaller,
|
||||
popcnt-friendly comparison first.
|
||||
- **Always-on anomaly gating becomes affordable.** The CNN / pose
|
||||
model only wakes when sketch novelty crosses a threshold. Energy
|
||||
budget per node drops materially in steady-state quiet rooms.
|
||||
- **Privacy story improves.** Event logs and inter-cluster mesh
|
||||
traffic carry sketches and witness hashes, not reconstructable
|
||||
embeddings. The 1-bit quantization is *not* invertible to the
|
||||
original CSI.
|
||||
- **Composes cleanly with ADR-083.** The cluster Pi is the natural
|
||||
home for the sketch bank; sensor MCUs remain unchanged.
|
||||
- **No new dependency.** `BinaryQuantized` is already in the vendored
|
||||
`ruvector-core` and already SIMD-accelerated.
|
||||
|
||||
### Negative / risks
|
||||
|
||||
- **Sketch quality depends on embedding distribution.** Pure 1-bit
|
||||
sign quantization (which `BinaryQuantized` implements) works best
|
||||
when the embedding space is roughly zero-centered and isotropic.
|
||||
AETHER and CSI spectrogram embeddings need to be benchmarked for
|
||||
this assumption; if either fails, a randomized rotation
|
||||
(Johnson-Lindenstrauss / RaBitQ-paper-style) must be added before
|
||||
sketching. Out-of-scope for this ADR; tracked as a follow-up if
|
||||
the acceptance test fails.
|
||||
- **Top-K coverage degrades for small candidate sets.** With < 16
|
||||
candidates, the sketch compare can pick the wrong K. Site-by-site
|
||||
fallback to full float is part of the rollout plan.
|
||||
- **Sketch-version skew during model upgrades.** A model change
|
||||
invalidates all stored sketches; the cluster Pi must re-sketch the
|
||||
candidate bank when `sketch_version` bumps. Cost is bounded but
|
||||
non-zero.
|
||||
|
||||
### Neutral
|
||||
|
||||
- ADR-024, ADR-027, ADR-029, ADR-030, ADR-076 are unchanged in
|
||||
*what* they compute. They gain a sketch pre-filter at the comparison
|
||||
step.
|
||||
- ADR-082's confirmed-track output filter is upstream of the sketch
|
||||
layer; it stays correct.
|
||||
|
||||
## Implementation
|
||||
|
||||
The implementation lands in five passes, each independently testable.
|
||||
Every pass is gated by the acceptance criterion above; if any fail,
|
||||
that site rolls back and the rest continue.
|
||||
|
||||
1. **`wifi-densepose-ruvector::sketch` module.** Re-export
|
||||
`BinaryQuantized` plus a thin RuView-flavored API
|
||||
(`Sketch::from_embedding`, `Sketch::distance`, `SketchBank::topk`).
|
||||
Add `sketch_version: u16` and `embedding_dim: u16` fields to the
|
||||
public type. Criterion benches: sketch ↔ float compare-cost ratio.
|
||||
|
||||
2. **AETHER re-ID pre-filter.** In
|
||||
`wifi-densepose-signal/src/ruvsense/pose_tracker.rs`, before
|
||||
computing the full 128-d cosine across active tracks × candidates,
|
||||
sketch both sides and reduce to top-K via hamming. Bench: re-ID
|
||||
pass time per frame, ID-stability under cross-room transitions.
|
||||
|
||||
3. **Cluster-Pi novelty sensor.** In
|
||||
`wifi-densepose-sensing-server`, maintain a per-room
|
||||
`SketchBank` of "normal-state" sketches; on each incoming
|
||||
`rv_feature_state_t`, compute embedding sketch, score novelty
|
||||
against the bank, and emit `novelty_score` as a new field on the
|
||||
WebSocket update envelope. Heavy CNN wake gate uses this score.
|
||||
|
||||
4. **Mesh-exchange compression.** Inter-cluster broadcasts (the
|
||||
ADR-066 swarm-bridge channel) carry sketch + witness instead of
|
||||
the full embedding when novelty is low. Full embedding only
|
||||
exchanged when novelty crosses threshold.
|
||||
|
||||
5. **Privacy-preserving event log.** Event log table on the cluster
|
||||
Pi stores `(sketch_bytes, sketch_version, novelty_score,
|
||||
witness_sha256)` instead of raw embeddings. Existing log readers
|
||||
are unchanged in API; only the storage layer rewrites.
|
||||
|
||||
Each pass adds tests: a property test (sketch ↔ float top-K agreement
|
||||
≥ 90%), a criterion bench (≥ 8× compare cost reduction), and an
|
||||
end-to-end accuracy regression test (< 1 pp drop).
|
||||
|
||||
## Validation
|
||||
|
||||
This ADR is **proposed**, not accepted. Acceptance requires the three
|
||||
acceptance numbers above to hold on **at least three of the five
|
||||
implementation passes** (the sites where the bulk of the load sits:
|
||||
AETHER re-ID, cluster-Pi novelty, and event log). The mesh-exchange
|
||||
and mincut prefilter passes are nice-to-haves; they can ship
|
||||
afterward if their per-site numbers hold.
|
||||
|
||||
Validation runs against:
|
||||
|
||||
- the existing 1,539-test workspace suite (must stay green)
|
||||
- a new `tests/integration/rabitq_sketch_pipeline.rs` integration test
|
||||
driving recorded CSI through the full pipeline with and without
|
||||
sketches, comparing top-K decisions and end-to-end pose accuracy
|
||||
- ESP32-S3 on COM7 — sensor MCU unchanged; sketch happens at the
|
||||
cluster Pi, so this validation is a smoke test that the
|
||||
sensor → Pi UDP path still works after the cluster Pi gains the
|
||||
sketch bank
|
||||
|
||||
## Related
|
||||
|
||||
- **ADR-024** (Accepted) — AETHER re-ID embeddings. Primary consumer
|
||||
of the sketch pre-filter.
|
||||
- **ADR-027** (Accepted) — Cross-environment domain generalization
|
||||
(MERIDIAN). Per-room sketch bank is the natural data structure for
|
||||
domain detection.
|
||||
- **ADR-030** (Proposed) — RuvSense persistent field model. Sketch
|
||||
novelty is the cheap second gate before SVD recompute.
|
||||
- **ADR-066** — Swarm bridge to coordinator. Inter-cluster sketch
|
||||
exchange.
|
||||
- **ADR-076** (Accepted) — CSI spectrogram embeddings. Sketch
|
||||
consumer; embedding source.
|
||||
- **ADR-081** (Accepted) — 5-layer adaptive CSI mesh firmware kernel.
|
||||
Sensor MCU unchanged by this ADR; sketches happen at the cluster Pi.
|
||||
- **ADR-083** (Proposed) — Per-cluster Pi compute hop. Defines the
|
||||
device class that hosts the sketch bank.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Does `BinaryQuantized` need a randomized rotation pre-pass for
|
||||
RuView's embedding distributions?** Pure sign quantization assumes
|
||||
zero-centered, isotropic embeddings. If AETHER / spectrogram
|
||||
distributions are skewed (likely for spectrogram), add a
|
||||
`randomized_rotation` pre-pass following the original RaBitQ paper
|
||||
(Gao & Long, SIGMOD 2024). Decided after pass-1 benchmark.
|
||||
- **Sketch dimension target.** Default to the embedding's native
|
||||
dimension (128 for AETHER, 256 for spectrogram). Higher-dimensional
|
||||
sketches (Johnson-Lindenstrauss-projected to 512) trade compute for
|
||||
recall; benchmark before committing.
|
||||
- **Per-room vs per-deployment sketch banks.** Defaulting to per-room
|
||||
for novelty detection. Cross-room re-ID may want a shared bank;
|
||||
decide once cross-room AETHER traces are available.
|
||||
+20
-20
@@ -29,7 +29,7 @@ This runs three phases:
|
||||
|
||||
1. **Environment checks** -- confirms Python, numpy, scipy, and proof files are present.
|
||||
2. **Proof pipeline replay** -- feeds a published reference signal through the full signal processing chain (noise filtering, Hamming windowing, amplitude normalization, FFT-based Doppler extraction, power spectral density via scipy.fft) and computes a SHA-256 hash of the output.
|
||||
3. **Production code integrity scan** -- scans `archive/v1/src/` for `np.random.rand` / `np.random.randn` calls in production code (test helpers are excluded).
|
||||
3. **Production code integrity scan** -- scans `v1/src/` for `np.random.rand` / `np.random.randn` calls in production code (test helpers are excluded).
|
||||
|
||||
Exit codes:
|
||||
- `0` PASS -- pipeline hash matches the published expected hash
|
||||
@@ -51,7 +51,7 @@ make verify-audit
|
||||
If the expected hash file is missing, regenerate it:
|
||||
|
||||
```bash
|
||||
python3 archive/v1/data/proof/verify.py --generate-hash
|
||||
python3 v1/data/proof/verify.py --generate-hash
|
||||
```
|
||||
|
||||
### Minimal dependencies for verification only
|
||||
@@ -63,7 +63,7 @@ pip install numpy==1.26.4 scipy==1.14.1
|
||||
Or install the pinned set that guarantees hash reproducibility:
|
||||
|
||||
```bash
|
||||
pip install -r archive/v1/requirements-lock.txt
|
||||
pip install -r v1/requirements-lock.txt
|
||||
```
|
||||
|
||||
The lock file pins: `numpy==1.26.4`, `scipy==1.14.1`, `pydantic==2.10.4`, `pydantic-settings==2.7.1`.
|
||||
@@ -82,7 +82,7 @@ The Python pipeline lives under `v1/` and provides the full API server, signal p
|
||||
### Install (verification-only -- lightweight)
|
||||
|
||||
```bash
|
||||
pip install -r archive/v1/requirements-lock.txt
|
||||
pip install -r v1/requirements-lock.txt
|
||||
```
|
||||
|
||||
This installs only the four packages needed for deterministic pipeline verification.
|
||||
@@ -98,7 +98,7 @@ This pulls in FastAPI, uvicorn, torch, OpenCV, SQLAlchemy, Redis client, and all
|
||||
### Verify the pipeline
|
||||
|
||||
```bash
|
||||
python3 archive/v1/data/proof/verify.py
|
||||
python3 v1/data/proof/verify.py
|
||||
```
|
||||
|
||||
Same as `./verify` but calls the Python script directly, skipping the bash wrapper's codebase scan phase.
|
||||
@@ -124,7 +124,7 @@ uvicorn v1.src.api.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
|
||||
### Run with commodity WiFi (RSSI sensing -- no custom hardware)
|
||||
|
||||
The commodity sensing module (`archive/v1/src/sensing/`) extracts presence and motion features from standard Linux WiFi metrics (RSSI, noise floor, link quality) without any hardware modification. See [ADR-013](adr/ADR-013-feature-level-sensing-commodity-gear.md) for full design details.
|
||||
The commodity sensing module (`v1/src/sensing/`) extracts presence and motion features from standard Linux WiFi metrics (RSSI, noise floor, link quality) without any hardware modification. See [ADR-013](adr/ADR-013-feature-level-sensing-commodity-gear.md) for full design details.
|
||||
|
||||
Requirements:
|
||||
- Any Linux machine with a WiFi interface (laptop, Raspberry Pi, etc.)
|
||||
@@ -191,7 +191,7 @@ A high-performance Rust port with ~810x speedup over the Python pipeline for the
|
||||
### Build
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
@@ -200,7 +200,7 @@ Release profile is configured with LTO, single codegen unit, and `-O3` for maxim
|
||||
### Test
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
@@ -209,7 +209,7 @@ Runs 107 tests across all workspace crates.
|
||||
### Benchmark
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo bench --package wifi-densepose-signal
|
||||
```
|
||||
|
||||
@@ -468,7 +468,7 @@ The aggregator collects UDP streams from all ESP32 nodes, performs feature-level
|
||||
docker compose -f docker-compose.esp32.yml up
|
||||
|
||||
# Or run the Rust aggregator directly
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo run --release --package wifi-densepose-hardware -- --mode esp32-aggregator --port 5000
|
||||
```
|
||||
|
||||
@@ -516,7 +516,7 @@ rustup target add wasm32-unknown-unknown
|
||||
Build:
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
|
||||
# Build WASM package (outputs to pkg/)
|
||||
wasm-pack build crates/wifi-densepose-wasm --target web --release
|
||||
@@ -601,7 +601,7 @@ uvicorn v1.src.api.main:app \
|
||||
--workers 4
|
||||
|
||||
# Or run the Rust API server
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo run --release --package wifi-densepose-api
|
||||
```
|
||||
|
||||
@@ -631,7 +631,7 @@ pytest --cov=wifi_densepose --cov-report=html
|
||||
Rust:
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
|
||||
# Build in debug mode (faster compilation)
|
||||
cargo build
|
||||
@@ -667,14 +667,14 @@ python3 -m http.server 3000 --directory ui
|
||||
|------|---------|
|
||||
| `./verify` | Trust kill switch -- one-command pipeline proof |
|
||||
| `Makefile` | `make verify`, `make verify-verbose`, `make verify-audit` |
|
||||
| `archive/v1/requirements-lock.txt` | Pinned Python deps for hash reproducibility |
|
||||
| `v1/requirements-lock.txt` | Pinned Python deps for hash reproducibility |
|
||||
| `requirements.txt` | Full Python deps (API server, torch, etc.) |
|
||||
| `archive/v1/data/proof/verify.py` | Python verification script |
|
||||
| `archive/v1/data/proof/sample_csi_data.json` | Deterministic reference signal |
|
||||
| `archive/v1/data/proof/expected_features.sha256` | Published expected hash |
|
||||
| `archive/v1/src/api/main.py` | FastAPI application entry point |
|
||||
| `archive/v1/src/sensing/` | Commodity WiFi sensing module (RSSI) |
|
||||
| `v2/Cargo.toml` | Rust workspace root |
|
||||
| `v1/data/proof/verify.py` | Python verification script |
|
||||
| `v1/data/proof/sample_csi_data.json` | Deterministic reference signal |
|
||||
| `v1/data/proof/expected_features.sha256` | Published expected hash |
|
||||
| `v1/src/api/main.py` | FastAPI application entry point |
|
||||
| `v1/src/sensing/` | Commodity WiFi sensing module (RSSI) |
|
||||
| `rust-port/wifi-densepose-rs/Cargo.toml` | Rust workspace root |
|
||||
| `ui/viz.html` | Three.js 3D visualization |
|
||||
| `Dockerfile` | Multi-stage Docker build (dev/prod/test/security) |
|
||||
| `docker-compose.yml` | Development stack (Postgres, Redis, Prometheus, Grafana) |
|
||||
|
||||
@@ -14,7 +14,7 @@ This document defines the system using [Domain-Driven Design](https://martinfowl
|
||||
| 4 | [Aggregation](#4-aggregation-context) | Server-side CSI frame reception, timestamp alignment, multi-node feature fusion | [ADR-012](../adr/ADR-012-esp32-csi-sensor-mesh.md) | `crates/wifi-densepose-hardware/src/esp32/` |
|
||||
| 5 | [Provisioning](#5-provisioning-context) | NVS configuration, firmware lifecycle, fleet management, deployment presets | [ADR-044](../adr/ADR-044-provisioning-tool-enhancements.md) | `firmware/esp32-csi-node/provision.py` |
|
||||
|
||||
All firmware paths are relative to the repository root. Rust crate paths are relative to `v2/`.
|
||||
All firmware paths are relative to the repository root. Rust crate paths are relative to `rust-port/wifi-densepose-rs/`.
|
||||
|
||||
---
|
||||
|
||||
@@ -31,7 +31,7 @@ All firmware paths are relative to the repository root. Rust crate paths are rel
|
||||
| **Core 0 / Core 1** | The two Xtensa LX7 cores on ESP32-S3; Core 0 runs WiFi + CSI callback, Core 1 runs the DSP pipeline |
|
||||
| **SPSC Ring Buffer** | Single-producer single-consumer lock-free queue between Core 0 (CSI callback) and Core 1 (DSP task) |
|
||||
| **Vitals Packet** | 32-byte UDP packet (magic `0xC5110002`) containing presence, breathing BPM, heart rate BPM, fall flag |
|
||||
| **Compressed Frame** | Delta-compressed CSI frame (magic `0xC5110005`, reassigned from `0xC5110003` by ADR-069) using XOR + RLE for 30-50% bandwidth reduction |
|
||||
| **Compressed Frame** | Delta-compressed CSI frame (magic `0xC5110003`) using XOR + RLE for 30-50% bandwidth reduction |
|
||||
| **WASM Module** | A `no_std` Rust program compiled to `wasm32-unknown-unknown`, executed on-device via WASM3 interpreter |
|
||||
| **Module Slot** | One of 4 pre-allocated PSRAM arenas (160 KB each) that host a WASM module instance |
|
||||
| **Host API** | 12 functions in the `csi` namespace that WASM modules call to read sensor data and emit events |
|
||||
@@ -158,7 +158,7 @@ All firmware paths are relative to the repository root. Rust crate paths are rel
|
||||
| +------------------+--------+ |
|
||||
| | Multi-Person Clustering | |
|
||||
| | (subcarrier groups, <=4) |----> VitalsPacket (0xC5110002) |
|
||||
| +---------------------------+----> CompressedFrame (0xC5110005)|
|
||||
| +---------------------------+----> CompressedFrame (0xC5110003)|
|
||||
| |
|
||||
+--------------------------------------------------------------+
|
||||
```
|
||||
@@ -1197,7 +1197,7 @@ pub trait ProvisioningService {
|
||||
| Sensor Node | Edge Processing | **Partnership** | Tightly coupled via SPSC ring buffer on the same chip |
|
||||
| Edge Processing | WASM Runtime | **Customer/Supplier** | Edge pipeline feeds CSI data to WASM modules via Host API |
|
||||
| Sensor Node | Aggregation | **Published Language** | ADR-018 binary wire format (magic bytes, fixed offsets) |
|
||||
| Edge Processing | Aggregation | **Published Language** | Vitals (0xC5110002), compressed (0xC5110005), and feature vectors (0xC5110003) wire formats |
|
||||
| Edge Processing | Aggregation | **Published Language** | Vitals (0xC5110002) and compressed (0xC5110003) wire formats |
|
||||
| WASM Runtime | Aggregation | **Published Language** | WASM events (0xC5110004) wire format |
|
||||
| Aggregation | Downstream crates | **Customer/Supplier** | Aggregator produces `FusedFrame` consumed by signal/nn/mat |
|
||||
|
||||
@@ -1223,8 +1223,7 @@ impl Esp32ToPipelineAdapter {
|
||||
/// Handles magic byte demuxing:
|
||||
/// 0xC5110001 -> raw CSI frame
|
||||
/// 0xC5110002 -> vitals packet
|
||||
/// 0xC5110003 -> feature vector (ADR-069, 48-byte 8-dim)
|
||||
/// 0xC5110005 -> compressed frame (decompress first)
|
||||
/// 0xC5110003 -> compressed frame (decompress first)
|
||||
/// 0xC5110004 -> WASM event packet
|
||||
pub fn parse_datagram(
|
||||
&self,
|
||||
@@ -1307,9 +1306,8 @@ All ESP32 UDP packets share a 4-byte magic prefix for demuxing at the aggregator
|
||||
|-------|------|--------|------|------|-------------|
|
||||
| `0xC5110001` | Raw CSI | Tier 0+ | ~128-404 B | 20-28.5 Hz | Full I/Q per subcarrier |
|
||||
| `0xC5110002` | Vitals | Tier 2+ | 32 B | 1 Hz (configurable) | Presence, BPM, fall flag |
|
||||
| `0xC5110003` | Feature Vector | Tier 2+ | 48 B | 1 Hz | ADR-069 8-dim normalized features for Cognitum Seed RVF ingest |
|
||||
| `0xC5110003` | Compressed | Tier 1+ | variable | 20-28.5 Hz | XOR+RLE delta-compressed CSI |
|
||||
| `0xC5110004` | WASM Events | Tier 3 | variable | event-driven | Module event_type + value tuples |
|
||||
| `0xC5110005` | Compressed | Tier 1+ | variable | 20-28.5 Hz | XOR+RLE delta-compressed CSI (reassigned from 0xC5110003) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user