mirror of
https://github.com/ruvnet/RuView
synced 2026-06-10 10:23:19 +00:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0223ef6d2e | |||
| 2f5e7ffb41 | |||
| 4ce8ffc465 | |||
| 3be63a7589 | |||
| c4e640c812 | |||
| 6e03a47867 | |||
| 9d1140de2d | |||
| 952f27a1ce | |||
| f7d043d727 | |||
| ff91d4e8cf | |||
| fc92436f52 | |||
| 285bb0ad37 | |||
| b5ec4ef043 | |||
| 21aba2df8d | |||
| a28a875594 | |||
| e12749bf68 | |||
| 3b37aaf460 | |||
| d3c683cc7e | |||
| 56de77c0ad | |||
| 0b98917dff | |||
| da4255a54c | |||
| 26a7d6775a | |||
| 341d9e05a8 | |||
| bc5408bd80 | |||
| c82c4fc4ac | |||
| 0c85d9c86f | |||
| 65c6fa7a34 | |||
| 7659b0bbe2 | |||
| 75d4685d25 | |||
| 45c15b77a5 | |||
| 47223a98be | |||
| c45690ed4e | |||
| fb782e0d71 | |||
| 944076733e | |||
| a8f48a7897 | |||
| 7df316f13e | |||
| da54ea07d2 | |||
| bf4d64ad4b | |||
| 8b57a6f64c | |||
| 5fa61ba7ea | |||
| f771cf8461 | |||
| c257e9a215 | |||
| 6e76578dcf |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"running": true,
|
||||
"startedAt": "2026-02-28T15:54:19.353Z",
|
||||
"startedAt": "2026-03-09T15:26:00.921Z",
|
||||
"workers": {
|
||||
"map": {
|
||||
"runCount": 49,
|
||||
@@ -8,16 +8,16 @@
|
||||
"failureCount": 0,
|
||||
"averageDurationMs": 1.2857142857142858,
|
||||
"lastRun": "2026-02-28T16:13:19.194Z",
|
||||
"nextRun": "2026-02-28T16:28:19.195Z",
|
||||
"nextRun": "2026-03-09T15:56:00.928Z",
|
||||
"isRunning": false
|
||||
},
|
||||
"audit": {
|
||||
"runCount": 44,
|
||||
"runCount": 45,
|
||||
"successCount": 0,
|
||||
"failureCount": 44,
|
||||
"failureCount": 45,
|
||||
"averageDurationMs": 0,
|
||||
"lastRun": "2026-02-28T16:20:19.184Z",
|
||||
"nextRun": "2026-02-28T16:30:19.185Z",
|
||||
"lastRun": "2026-03-09T15:43:00.933Z",
|
||||
"nextRun": "2026-03-09T15:38:00.914Z",
|
||||
"isRunning": false
|
||||
},
|
||||
"optimize": {
|
||||
@@ -26,7 +26,7 @@
|
||||
"failureCount": 34,
|
||||
"averageDurationMs": 0,
|
||||
"lastRun": "2026-02-28T16:23:19.387Z",
|
||||
"nextRun": "2026-02-28T16:18:19.361Z",
|
||||
"nextRun": "2026-03-09T15:45:00.915Z",
|
||||
"isRunning": false
|
||||
},
|
||||
"consolidate": {
|
||||
@@ -35,7 +35,7 @@
|
||||
"failureCount": 0,
|
||||
"averageDurationMs": 0.6521739130434783,
|
||||
"lastRun": "2026-02-28T16:05:19.091Z",
|
||||
"nextRun": "2026-02-28T16:35:19.054Z",
|
||||
"nextRun": "2026-03-09T16:02:00.918Z",
|
||||
"isRunning": false
|
||||
},
|
||||
"testgaps": {
|
||||
@@ -44,8 +44,8 @@
|
||||
"failureCount": 27,
|
||||
"averageDurationMs": 0,
|
||||
"lastRun": "2026-02-28T16:08:19.369Z",
|
||||
"nextRun": "2026-02-28T16:22:19.355Z",
|
||||
"isRunning": true
|
||||
"nextRun": "2026-03-09T15:54:00.920Z",
|
||||
"isRunning": false
|
||||
},
|
||||
"predict": {
|
||||
"runCount": 0,
|
||||
@@ -64,8 +64,8 @@
|
||||
},
|
||||
"config": {
|
||||
"autoStart": false,
|
||||
"logDir": "/home/user/wifi-densepose/.claude-flow/logs",
|
||||
"stateFile": "/home/user/wifi-densepose/.claude-flow/daemon-state.json",
|
||||
"logDir": "/Users/cohen/GitHub/ruvnet/RuView/.claude-flow/logs",
|
||||
"stateFile": "/Users/cohen/GitHub/ruvnet/RuView/.claude-flow/daemon-state.json",
|
||||
"maxConcurrent": 2,
|
||||
"workerTimeoutMs": 300000,
|
||||
"resourceThresholds": {
|
||||
@@ -131,5 +131,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"savedAt": "2026-02-28T16:23:19.387Z"
|
||||
"savedAt": "2026-03-09T15:43:00.933Z"
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
166
|
||||
31273
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"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/helpers/hook-handler.cjs pre-bash",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" pre-bash",
|
||||
"timeout": 5000
|
||||
}
|
||||
]
|
||||
@@ -18,7 +18,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node .claude/helpers/hook-handler.cjs post-edit",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" post-edit",
|
||||
"timeout": 10000
|
||||
}
|
||||
]
|
||||
@@ -29,7 +29,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node .claude/helpers/hook-handler.cjs route",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" route",
|
||||
"timeout": 10000
|
||||
}
|
||||
]
|
||||
@@ -40,12 +40,12 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node .claude/helpers/hook-handler.cjs session-restore",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" session-restore",
|
||||
"timeout": 15000
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node .claude/helpers/auto-memory-hook.mjs import",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/auto-memory-hook.mjs\" import",
|
||||
"timeout": 8000
|
||||
}
|
||||
]
|
||||
@@ -56,7 +56,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node .claude/helpers/hook-handler.cjs session-end",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" session-end",
|
||||
"timeout": 10000
|
||||
}
|
||||
]
|
||||
@@ -67,7 +67,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node .claude/helpers/auto-memory-hook.mjs sync",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/auto-memory-hook.mjs\" sync",
|
||||
"timeout": 10000
|
||||
}
|
||||
]
|
||||
@@ -79,11 +79,11 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node .claude/helpers/hook-handler.cjs compact-manual"
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" compact-manual"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node .claude/helpers/hook-handler.cjs session-end",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" session-end",
|
||||
"timeout": 5000
|
||||
}
|
||||
]
|
||||
@@ -93,11 +93,11 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node .claude/helpers/hook-handler.cjs compact-auto"
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" compact-auto"
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node .claude/helpers/hook-handler.cjs session-end",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" session-end",
|
||||
"timeout": 6000
|
||||
}
|
||||
]
|
||||
@@ -108,7 +108,7 @@
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node .claude/helpers/hook-handler.cjs status",
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs\" status",
|
||||
"timeout": 3000
|
||||
}
|
||||
]
|
||||
@@ -117,7 +117,7 @@
|
||||
},
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "node .claude/helpers/statusline.cjs"
|
||||
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/helpers/statusline.cjs\""
|
||||
},
|
||||
"permissions": {
|
||||
"allow": [
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"enabledMcpjsonServers": [
|
||||
"claude-flow"
|
||||
],
|
||||
"enableAllProjectMcpServers": true
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
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: rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui
|
||||
run: npm ci
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: rust-port/wifi-densepose-rs/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: rust-port/wifi-densepose-rs/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 rust-port/wifi-densepose-rs/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: rust-port/wifi-densepose-rs/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: rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui
|
||||
run: npm ci
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: rust-port/wifi-densepose-rs/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: rust-port/wifi-densepose-rs/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: rust-port/wifi-densepose-rs/target/release/bundle/msi/*.msi
|
||||
|
||||
- name: Upload Windows NSIS artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ruview-windows-nsis
|
||||
path: rust-port/wifi-densepose-rs/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)
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Update vendor submodules
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 */6 * * *' # Every 6 hours
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Update submodules to latest main
|
||||
run: git submodule update --remote --merge
|
||||
|
||||
- name: Check for changes
|
||||
id: check
|
||||
run: |
|
||||
if git diff --quiet; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Create PR with updates
|
||||
if: steps.check.outputs.changed == 'true'
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
BRANCH="chore/update-submodules-$(date +%Y%m%d-%H%M%S)"
|
||||
git checkout -b "$BRANCH"
|
||||
git add vendor/
|
||||
git commit -m "chore: update vendor submodules to latest main"
|
||||
git push origin "$BRANCH"
|
||||
gh pr create \
|
||||
--title "chore: update vendor submodules" \
|
||||
--body "Automated submodule update to latest upstream main." \
|
||||
--base main \
|
||||
--head "$BRANCH"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
+10
@@ -8,6 +8,16 @@ firmware/esp32-csi-node/sdkconfig.defaults
|
||||
firmware/esp32-csi-node/sdkconfig.old
|
||||
# Downloaded WASM3 source (fetched at configure time)
|
||||
firmware/esp32-csi-node/components/wasm3/wasm3-src/
|
||||
# ESP-IDF managed components (downloaded at build time)
|
||||
firmware/esp32-csi-node/managed_components/
|
||||
firmware/esp32-csi-node/dependencies.lock
|
||||
firmware/esp32-csi-node/sdkconfig.defaults.bak
|
||||
|
||||
# Claude Flow swarm runtime state
|
||||
.swarm/
|
||||
|
||||
# CSI recordings (local training data, machine-specific)
|
||||
rust-port/wifi-densepose-rs/data/recordings/
|
||||
|
||||
# NVS partition images and CSVs (contain WiFi credentials)
|
||||
nvs.bin
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
[submodule "vendor/midstream"]
|
||||
path = vendor/midstream
|
||||
url = https://github.com/ruvnet/midstream
|
||||
branch = main
|
||||
[submodule "vendor/ruvector"]
|
||||
path = vendor/ruvector
|
||||
url = https://github.com/ruvnet/ruvector
|
||||
branch = main
|
||||
[submodule "vendor/sublinear-time-solver"]
|
||||
path = vendor/sublinear-time-solver
|
||||
url = https://github.com/ruvnet/sublinear-time-solver
|
||||
branch = main
|
||||
|
||||
@@ -1,14 +1,32 @@
|
||||
# π RuView
|
||||
|
||||
<p align="center">
|
||||
<img src="assets/ruview-small-gemini.jpg" alt="RuView - WiFi DensePose" width="100%">
|
||||
<a href="https://ruvnet.github.io/RuView/">
|
||||
<img src="assets/ruview-small-gemini.jpg" alt="RuView - WiFi DensePose" width="100%">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
**See through walls with WiFi.** No cameras. No wearables. No Internet. Just radio waves.
|
||||
## **See through walls with WiFi + Ai** ##
|
||||
|
||||
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.
|
||||
**Perceive the world through signals.** No cameras. No wearables. No Internet. Just physics.
|
||||
|
||||
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.
|
||||
### π RuView is an edge AI perception system that learns directly from the environment around it.
|
||||
|
||||
Instead of relying on cameras or cloud models, it observes whatever signals exist in a space such as WiFi, radio waves across the spectrum, motion patterns, vibration, sound, or other sensory inputs and builds an understanding of what is happening locally.
|
||||
|
||||
Built on top of [RuVector](https://github.com/ruvnet/ruvector/), the project became widely known for its implementation of WiFi DensePose — a sensing technique first explored in academic research such as Carnegie Mellon University's *DensePose From WiFi* work. That research demonstrated that WiFi signals can be used to reconstruct human pose.
|
||||
|
||||
RuView extends that concept into a practical edge system. By analyzing Channel State Information (CSI) disturbances caused by human movement, RuView reconstructs body position, breathing rate, heart rate, and presence in real time using physics-based signal processing and machine learning.
|
||||
|
||||
Unlike research systems that rely on synchronized cameras for training, RuView is designed to operate entirely from radio signals and self-learned embeddings at the edge.
|
||||
|
||||
The system runs entirely on inexpensive hardware such as an ESP32 sensor mesh (as low as ~$1 per node). Small programmable edge modules analyze signals locally and learn the RF signature of a room over time, allowing the system to separate the environment from the activity happening inside it.
|
||||
|
||||
Because RuView learns in proximity to the signals it observes, it improves as it operates. Each deployment develops a local model of its surroundings and continuously adapts without requiring cameras, labeled data, or cloud infrastructure.
|
||||
|
||||
In practice this means ordinary environments gain a new kind of spatial awareness. Rooms, buildings, and devices begin to sense presence, movement, and vital activity using the signals that already fill the space.
|
||||
|
||||
### Built for low-power edge applications
|
||||
|
||||
[Edge modules](#edge-intelligence-adr-041) are small programs that run directly on the ESP32 sensor — no internet needed, no cloud fees, instant response.
|
||||
|
||||
@@ -57,17 +75,26 @@ docker run -p 3000:3000 ruvnet/wifi-densepose:latest
|
||||
|----------|-------------|
|
||||
| [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) | 44 ADRs — why each technical choice was made, organized by domain (hardware, signal processing, ML, platform, infrastructure) |
|
||||
| [Architecture Decisions](docs/adr/README.md) | 49 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](rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/README.md) | **WIP** — Tauri v2 desktop app for node management, OTA updates, WASM deployment, and mesh visualization |
|
||||
|
||||
---
|
||||
|
||||
|
||||
<img src="assets/screen.png" alt="WiFi DensePose — Live pose detection with setup guide" width="800">
|
||||
<a href="https://ruvnet.github.io/RuView/">
|
||||
<img src="assets/v2-screen.png" alt="WiFi DensePose — Live pose detection with setup guide" width="800">
|
||||
</a>
|
||||
<br>
|
||||
<em>Real-time pose skeleton from WiFi CSI signals — no cameras, no wearables</em>
|
||||
<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
|
||||
@@ -98,6 +125,7 @@ The system learns on its own and gets smarter over time — no hand-tuning, no l
|
||||
| 👁️ | **Cross-Viewpoint Fusion** | AI combines what each sensor sees from its own angle — fills in blind spots and depth ambiguity that no single viewpoint can resolve on its own ([ADR-031](docs/adr/ADR-031-ruview-sensing-first-rf-mode.md)) |
|
||||
| 🔮 | **Signal-Line Protocol** | A 6-stage processing pipeline transforms raw WiFi signals into structured body representations — from signal cleanup through graph-based spatial reasoning to final pose output ([ADR-033](docs/adr/ADR-033-crv-signal-line-sensing-integration.md)) |
|
||||
| 🔒 | **QUIC Mesh Security** | All sensor-to-sensor communication is encrypted end-to-end with tamper detection, replay protection, and seamless reconnection if a node moves or drops offline ([ADR-032](docs/adr/ADR-032-multistatic-mesh-security-hardening.md)) |
|
||||
| 🎯 | **Adaptive Classifier** | Records labeled CSI sessions, trains a 15-feature logistic regression model in pure Rust, and learns your room's unique signal characteristics — replaces hand-tuned thresholds with data-driven classification ([ADR-048](docs/adr/ADR-048-adaptive-csi-classifier.md)) |
|
||||
|
||||
### Performance & Deployment
|
||||
|
||||
@@ -110,6 +138,8 @@ Fast enough for real-time use, small enough for edge devices, simple enough for
|
||||
| 🐳 | **One-Command Setup** | `docker pull ruvnet/wifi-densepose:latest` — live sensing in 30 seconds, no toolchain needed (amd64 + arm64 / Apple Silicon) |
|
||||
| 📡 | **Fully Local** | Runs completely on a $9 ESP32 — no internet connection, no cloud account, no recurring fees. Detects presence, vital signs, and falls on-device with instant response |
|
||||
| 📦 | **Portable Models** | Trained models package into a single `.rvf` file — runs on edge, cloud, or browser (WASM) |
|
||||
| 🔭 | **Observatory Visualization** | Cinematic Three.js dashboard with 5 holographic panels — subcarrier manifold, vital signs oracle, presence heatmap, phase constellation, convergence engine — all driven by live or demo CSI data ([ADR-047](docs/adr/ADR-047-psychohistory-observatory-visualization.md)) |
|
||||
| 📟 | **AMOLED Display** | ESP32-S3 boards with built-in AMOLED screens show real-time presence, vital signs, and room status directly on the sensor — no phone or PC needed ([ADR-045](docs/adr/ADR-045-amoled-display-support.md)) |
|
||||
|
||||
---
|
||||
|
||||
@@ -613,6 +643,8 @@ cargo add wifi-densepose-ruvector # RuVector v2.0.4 integration layer (ADR-017
|
||||
|
||||
All crates integrate with [RuVector v2.0.4](https://github.com/ruvnet/ruvector) — see [AI Backbone](#ai-backbone-ruvector) below.
|
||||
|
||||
**[rUv Neural](rust-port/wifi-densepose-rs/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>
|
||||
|
||||
---
|
||||
@@ -711,6 +743,7 @@ 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](rust-port/wifi-densepose-rs/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) |
|
||||
@@ -1014,14 +1047,16 @@ Download a pre-built binary — no build toolchain needed:
|
||||
|
||||
| Release | What's included | Tag |
|
||||
|---------|-----------------|-----|
|
||||
| [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.4.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.1-esp32) | **Stable** — 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.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
|
||||
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
|
||||
write_flash --flash_mode dio --flash_size 8MB \
|
||||
0x0 bootloader.bin 0x8000 partition-table.bin 0x10000 esp32-csi-node.bin
|
||||
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
|
||||
|
||||
# 2. Set WiFi credentials and server address (stored in flash, survives reboots)
|
||||
python firmware/esp32-csi-node/provision.py --port COM7 \
|
||||
@@ -1396,6 +1431,13 @@ 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**](rust-port/wifi-densepose-rs/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>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.0 MiB |
@@ -0,0 +1,141 @@
|
||||
## 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**
|
||||
@@ -0,0 +1,110 @@
|
||||
# ADR-045: AMOLED Display Support for ESP32-S3 CSI Node
|
||||
|
||||
## Status
|
||||
|
||||
Proposed
|
||||
|
||||
## Context
|
||||
|
||||
The ESP32-S3 board (LilyGO T-Display-S3 AMOLED) has an integrated RM67162 QSPI AMOLED display (536x240) and 8MB octal PSRAM that were unused by the CSI firmware. Users want real-time on-device visualization of CSI statistics, vital signs, and system health without relying on an external server.
|
||||
|
||||
### Constraints
|
||||
|
||||
- Binary was 947 KB in a 1 MB partition — needed 8MB flash + custom partition table
|
||||
- SPIRAM was disabled in sdkconfig despite hardware having 8MB PSRAM
|
||||
- Core 1 is pinned to DSP (edge processing) — display must use Core 0
|
||||
- Existing CSI pipeline must not be affected
|
||||
|
||||
### Available APIs
|
||||
|
||||
Thread-safe edge APIs already exist (`edge_get_vitals()`, `edge_get_multi_person()`) — the display task only reads from these, no new synchronization needed.
|
||||
|
||||
## Decision
|
||||
|
||||
Add optional AMOLED display support with the following architecture:
|
||||
|
||||
### Hardware Abstraction Layer
|
||||
|
||||
- `display_hal.c/h`: RM67162 QSPI panel driver + CST816S capacitive touch via I2C
|
||||
- Auto-detect at boot: probe RM67162 and check SPIRAM; log warning and skip if absent
|
||||
|
||||
### UI Layer
|
||||
|
||||
- `display_ui.c/h`: LVGL 8.3 with 4 swipeable views via tileview widget
|
||||
- Dark theme (#0a0a0f) with cyan (#00d4ff) accent for three.js-like aesthetic
|
||||
- Views: Dashboard (CSI amplitude chart + stats), Vitals (breathing + HR line graphs), Presence (4x4 occupancy grid), System (CPU, heap, PSRAM, WiFi, uptime, FPS)
|
||||
|
||||
### Task Layer
|
||||
|
||||
- `display_task.c/h`: FreeRTOS task on Core 0, priority 1 (lowest)
|
||||
- LVGL pump loop at configurable FPS (default 30)
|
||||
- Double-buffered draw buffers allocated in SPIRAM
|
||||
|
||||
### Compile-Time Control
|
||||
|
||||
- `CONFIG_DISPLAY_ENABLE=y` (default): compiles display code, auto-detects hardware at boot
|
||||
- `CONFIG_DISPLAY_ENABLE=n`: zero-cost — no display code compiled
|
||||
- `CONFIG_SPIRAM_IGNORE_NOTFOUND=y`: boots fine on boards without PSRAM
|
||||
|
||||
### Flash Layout
|
||||
|
||||
8MB partition table (`partitions_display.csv`):
|
||||
- Dual OTA partitions: 2 x 2MB (supports larger binaries with LVGL)
|
||||
- SPIFFS: 1.9MB (for future font/asset storage)
|
||||
- NVS + otadata + phy: standard sizes
|
||||
|
||||
### Core/Task Layout
|
||||
|
||||
| Task | Core | Priority | Impact |
|
||||
|------|------|----------|--------|
|
||||
| WiFi/LwIP | 0 | 18-23 | unchanged |
|
||||
| OTA httpd | 0 | 5 | unchanged |
|
||||
| **display_task** | **0** | **1** | **NEW — lowest priority** |
|
||||
| edge_task (DSP) | 1 | 5 | unchanged |
|
||||
|
||||
### Dependencies
|
||||
|
||||
- LVGL ~8.3 (via ESP-IDF managed components)
|
||||
- espressif/esp_lcd_touch_cst816s ^1.0
|
||||
- espressif/esp_lcd_touch ^1.0
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Real-time on-device stats without network dependency
|
||||
- Zero impact on CSI pipeline (display reads thread-safe APIs, runs at lowest priority)
|
||||
- Graceful degradation: works on boards without display or PSRAM
|
||||
- SPIRAM enabled for all boards (benefits WASM runtime too)
|
||||
- 8MB flash + dual OTA 2MB partitions give headroom for future features
|
||||
|
||||
### Negative
|
||||
|
||||
- Binary size increase (~200-300 KB with LVGL)
|
||||
- SPIRAM + 8MB flash config is specific to T-Display-S3 AMOLED boards
|
||||
- Boards with only 4MB flash need `CONFIG_DISPLAY_ENABLE=n` and the old partition table
|
||||
|
||||
### Risks
|
||||
|
||||
- RM67162 init sequence is board-specific; other AMOLED panels may need different commands
|
||||
- QSPI bus conflicts if other peripherals use SPI2_HOST (currently unused)
|
||||
|
||||
## New Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `main/display_hal.c/h` | RM67162 QSPI + CST816S touch HAL |
|
||||
| `main/display_ui.c/h` | LVGL 4-view UI |
|
||||
| `main/display_task.c/h` | FreeRTOS task, LVGL pump |
|
||||
| `main/lv_conf.h` | LVGL compile config |
|
||||
| `partitions_display.csv` | 8MB partition table |
|
||||
| `idf_component.yml` | Managed component deps |
|
||||
|
||||
## Modified Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `sdkconfig.defaults` | 8MB flash, SPIRAM, custom partitions |
|
||||
| `main/CMakeLists.txt` | Conditional display sources + deps |
|
||||
| `main/main.c` | +1 include, +5 lines guarded init |
|
||||
| `main/Kconfig.projbuild` | "AMOLED Display" menu |
|
||||
@@ -0,0 +1,263 @@
|
||||
# ADR-046: Android TV Box / Armbian Deployment Target
|
||||
|
||||
## Status
|
||||
|
||||
Proposed
|
||||
|
||||
## Context
|
||||
|
||||
Issue [#138](https://github.com/ruvnet/wifi-densepose/issues/138) requests ESP8266 and mobile device support. The ESP8266 lacks CSI capability and sufficient resources, but the discussion revealed a compelling deployment target: **Android TV boxes** (Amlogic/Allwinner/Rockchip SoCs) running **Armbian** (Debian for ARM).
|
||||
|
||||
These devices cost $15–35, are always-on mains-powered, include 802.11ac WiFi, 2–4 GB RAM, quad-core ARM Cortex-A53/A55 CPUs, and HDMI output. They are widely available as consumer "IPTV boxes" (T95, H96 Max, X96, MXQ Pro, etc.) and can boot Armbian from SD card without modifying the factory Android installation.
|
||||
|
||||
### Current deployment model
|
||||
|
||||
```
|
||||
[ESP32-S3 nodes] --UDP CSI--> [Laptop/PC running sensing-server] --browser--> [UI]
|
||||
```
|
||||
|
||||
This requires a general-purpose computer ($300+) to run the Rust sensing server, NN inference, and web dashboard. For permanent installations (elder care, smart home, security), dedicating a laptop is impractical.
|
||||
|
||||
### Proposed deployment model
|
||||
|
||||
```
|
||||
[ESP32-S3 nodes] --UDP CSI--> [TV Box running Armbian + sensing-server] --HDMI--> [Display]
|
||||
$25, always-on, fanless
|
||||
```
|
||||
|
||||
### Future: custom WiFi firmware for standalone operation
|
||||
|
||||
Many TV box WiFi chipsets (Realtek RTL8822CS, MediaTek MT7661, Broadcom BCM43455) can potentially be patched for CSI extraction when running under Linux with custom drivers. This would eliminate the ESP32 dependency entirely for basic sensing:
|
||||
|
||||
```
|
||||
[TV Box with patched WiFi driver] --CSI extraction--> [sensing-server on same box] --HDMI--> [Display]
|
||||
$25 total, single device
|
||||
```
|
||||
|
||||
This ADR covers Phase 1 (TV box as aggregator) and Phase 2 (custom WiFi firmware for CSI). Phase 2 is speculative and requires per-chipset R&D.
|
||||
|
||||
## Decision
|
||||
|
||||
### Phase 1: TV Box as Aggregator (Armbian)
|
||||
|
||||
1. **Cross-compile the sensing server** for `aarch64-unknown-linux-gnu` using `cross` or Docker-based cross-compilation.
|
||||
|
||||
2. **Create an Armbian deployment package** containing:
|
||||
- Pre-built `wifi-densepose-sensing-server` binary (aarch64)
|
||||
- systemd service file for auto-start on boot
|
||||
- Kiosk-mode Chromium configuration for HDMI dashboard display
|
||||
- Network configuration for ESP32 UDP reception (port 5005)
|
||||
- Optional: `hostapd` config to create a dedicated WiFi AP for the ESP32 mesh
|
||||
|
||||
3. **Define minimum hardware requirements:**
|
||||
|
||||
| Component | Minimum | Recommended |
|
||||
|-----------|---------|-------------|
|
||||
| SoC | Amlogic S905W (A53 quad) | Amlogic S905X3 (A55 quad) |
|
||||
| RAM | 2 GB | 4 GB |
|
||||
| Storage | 8 GB eMMC + 8 GB SD | 16 GB eMMC + 16 GB SD |
|
||||
| WiFi | 802.11n 2.4 GHz | 802.11ac dual-band |
|
||||
| Ethernet | 100 Mbps | Gigabit |
|
||||
| USB | 1x USB 2.0 | 2x USB 3.0 |
|
||||
| HDMI | 1.4 | 2.0 |
|
||||
|
||||
4. **Tested reference devices** (initial target list):
|
||||
|
||||
| Device | SoC | WiFi Chip | Price | Armbian Support |
|
||||
|--------|-----|-----------|-------|-----------------|
|
||||
| T95 Max+ | S905X3 | RTL8822CS | ~$30 | Good (meson-sm1) |
|
||||
| H96 Max X3 | S905X3 | RTL8822CS | ~$35 | Good (meson-sm1) |
|
||||
| X96 Max+ | S905X3 | RTL8822CS | ~$28 | Good (meson-sm1) |
|
||||
| Tanix TX6S | H616 | MT7668 | ~$25 | Moderate (sun50i-h616) |
|
||||
|
||||
5. **New Rust compilation target** in workspace CI:
|
||||
- Add `aarch64-unknown-linux-gnu` to cross-compilation matrix
|
||||
- Binary size target: <15 MB stripped (fits easily in SD card)
|
||||
- No GPU dependency — CPU-only inference using `candle` or ONNX Runtime for ARM
|
||||
|
||||
### Phase 2: Custom WiFi Firmware for CSI Extraction (Future)
|
||||
|
||||
1. **CSI extraction feasibility by chipset:**
|
||||
|
||||
| Chipset | Driver | CSI Support | Monitor Mode | Effort |
|
||||
|---------|--------|-------------|--------------|--------|
|
||||
| Broadcom BCM43455 | brcmfmac | **Proven** (Nexmon CSI) | Yes | Low — patches exist |
|
||||
| Realtek RTL8822CS | rtw88 | **Moderate** — driver is open-source, CSI hooks need adding | Yes (patched) | Medium |
|
||||
| MediaTek MT7661 | mt76 | **Unknown** — MediaTek has released CSI tools for some chips | Yes | Medium-High |
|
||||
|
||||
2. **CSI extraction architecture** (Linux kernel driver modification):
|
||||
|
||||
```
|
||||
[WiFi chipset firmware] → [Modified kernel driver] → [Netlink/procfs CSI export]
|
||||
↓
|
||||
[userspace CSI reader]
|
||||
↓
|
||||
[sensing-server UDP input]
|
||||
```
|
||||
|
||||
The CSI data would be reformatted into the existing ESP32 binary protocol (ADR-018 header, magic `0xC5100001`) so the sensing server treats it identically to ESP32 frames. This means zero changes to the ingestion context.
|
||||
|
||||
3. **Hybrid mode**: When the TV box has both patched WiFi CSI and ESP32 UDP input, the sensing server's multi-node architecture (already supporting multiple `node_id` values) handles both sources transparently. The TV box's own WiFi becomes an additional viewpoint in the multistatic array.
|
||||
|
||||
### Phase 3: Android Companion App (Optional)
|
||||
|
||||
For users who want mobile monitoring without Armbian:
|
||||
|
||||
1. **PWA (Progressive Web App)**: The sensing server already serves a web UI. Adding a PWA manifest with offline caching makes it installable on any Android device. No native app needed.
|
||||
|
||||
2. **Native Android app** (future): Only if PWA proves insufficient. Would use Kotlin + Jetpack Compose, consuming the existing REST API and WebSocket endpoints.
|
||||
|
||||
## Deployment Architecture
|
||||
|
||||
### Single-Room Deployment (Phase 1)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Room │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ ESP32-S3 │ │ ESP32-S3 │ │ ESP32-S3 │ CSI sensor mesh │
|
||||
│ │ Node 1 │ │ Node 2 │ │ Node 3 │ ($10 each) │
|
||||
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
||||
│ │ │ │ │
|
||||
│ └──────────────┼──────────────┘ │
|
||||
│ │ UDP port 5005 │
|
||||
│ ▼ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ Android TV Box (Armbian) │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────────────────────┐ │ │
|
||||
│ │ │ wifi-densepose-sensing- │ │ │
|
||||
│ │ │ server (aarch64 binary) │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ • CSI ingestion (UDP) │ │ │
|
||||
│ │ │ • Feature extraction │ │ │
|
||||
│ │ │ • NN inference (CPU) │ │ │
|
||||
│ │ │ • WebSocket streaming │ │ │
|
||||
│ │ │ • REST API │ │ │
|
||||
│ │ │ • Web UI (:3000) │ │ │
|
||||
│ │ └──────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────────────────────┐ │ │
|
||||
│ │ │ Chromium Kiosk Mode │───│──→ HDMI out │
|
||||
│ │ │ (localhost:3000) │ │ to display │
|
||||
│ │ └──────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ Cost: $25-35 │ │
|
||||
│ │ Power: 5-10W (USB-C or barrel) │ │
|
||||
│ │ Form: fits behind TV/monitor │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
|
||||
Total system cost: $55-65 (3 ESP32 nodes + 1 TV box)
|
||||
```
|
||||
|
||||
### Multi-Room Deployment
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ Router │
|
||||
│ (WiFi AP) │
|
||||
└──────┬───────┘
|
||||
│ LAN
|
||||
┌──────────────┼──────────────┐
|
||||
│ │ │
|
||||
┌───────▼───────┐ ┌───▼────────┐ ┌──▼──────────┐
|
||||
│ Room A │ │ Room B │ │ Room C │
|
||||
│ TV Box + │ │ TV Box + │ │ TV Box + │
|
||||
│ 3x ESP32 │ │ 3x ESP32 │ │ 3x ESP32 │
|
||||
│ HDMI display │ │ HDMI │ │ HDMI │
|
||||
└───────────────┘ └────────────┘ └─────────────┘
|
||||
|
||||
Each room: self-contained sensing + display
|
||||
Central dashboard: aggregate all rooms via REST API
|
||||
```
|
||||
|
||||
### Standalone Mode (Phase 2 — Custom WiFi FW)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────┐
|
||||
│ Android TV Box (Armbian) │
|
||||
│ │
|
||||
│ ┌────────────────────┐ │
|
||||
│ │ Patched WiFi │ │
|
||||
│ │ Driver │ │
|
||||
│ │ (CSI extraction) │ │
|
||||
│ └─────────┬──────────┘ │
|
||||
│ │ CSI frames │
|
||||
│ ▼ │
|
||||
│ ┌────────────────────┐ │
|
||||
│ │ sensing-server │──→ HDMI out │
|
||||
│ │ (inference + │ │
|
||||
│ │ dashboard) │ │
|
||||
│ └────────────────────┘ │
|
||||
│ │
|
||||
│ Single device: $25 │
|
||||
│ No ESP32 nodes needed │
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **10x cost reduction** for aggregator: $25 TV box vs $300+ laptop/PC
|
||||
- **Always-on deployment**: Mains-powered, fanless, designed for 24/7 operation
|
||||
- **HDMI output**: Direct connection to TV/monitor for wall-mounted dashboards
|
||||
- **Familiar hardware**: Available globally, no specialized ordering required
|
||||
- **Armbian ecosystem**: Mature Debian-based distro with package management, systemd, SSH
|
||||
- **Path to standalone**: Custom WiFi firmware could eliminate ESP32 dependency entirely
|
||||
- **PWA for mobile**: No native app development needed for mobile monitoring
|
||||
- **Multi-room scaling**: One TV box per room, each self-contained
|
||||
|
||||
### Negative
|
||||
|
||||
- **ARM cross-compilation**: Adds CI complexity; `candle`/ONNX Runtime ARM builds need testing
|
||||
- **Armbian compatibility**: Not all TV boxes are well-supported; need a tested device list
|
||||
- **Performance uncertainty**: ARM A53 cores are ~3-5x slower than x86 for NN inference; may need model quantization (INT8) for real-time operation
|
||||
- **Phase 2 risk**: Custom WiFi firmware is chipset-specific, may require kernel patches per driver version, and CSI quality varies by chipset
|
||||
- **Support burden**: Different hardware = more configurations to support
|
||||
- **No GPU**: TV boxes lack discrete GPU; inference is CPU-only (but our models are small enough)
|
||||
|
||||
### Neutral
|
||||
|
||||
- **No changes to existing ESP32 firmware** — TV box receives the same UDP frames
|
||||
- **No changes to sensing server protocol** — Phase 2 CSI output uses same binary format
|
||||
- **Existing web UI works as-is** — Chromium kiosk mode or any browser on the LAN
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1 (2-3 weeks)
|
||||
|
||||
1. Add `aarch64-unknown-linux-gnu` cross-compilation target using `cross`
|
||||
2. Build and test sensing-server binary on reference TV box (T95 Max+ / S905X3)
|
||||
3. Create systemd service + Armbian deployment script
|
||||
4. Benchmark: measure inference latency, memory usage, thermal throttling
|
||||
5. Create `docs/deployment/armbian-tv-box.md` setup guide
|
||||
6. Add HDMI kiosk mode configuration (Chromium autostart)
|
||||
|
||||
### Phase 2 (4-8 weeks, R&D)
|
||||
|
||||
1. Acquire TV box with BCM43455 (proven Nexmon CSI support)
|
||||
2. Build Armbian with Nexmon CSI patches for BCM43455
|
||||
3. Write userspace CSI reader → ESP32 binary protocol converter
|
||||
4. Test CSI quality comparison: ESP32 vs BCM43455
|
||||
5. If viable: add RTL8822CS CSI extraction via rtw88 driver modification
|
||||
|
||||
### Phase 3 (1 week)
|
||||
|
||||
1. Add PWA manifest to sensing server web UI
|
||||
2. Test on Android Chrome, iOS Safari
|
||||
3. Add service worker for offline dashboard caching
|
||||
|
||||
## References
|
||||
|
||||
- [Nexmon CSI](https://github.com/seemoo-lab/nexmon_csi) — Broadcom WiFi CSI extraction (BCM43455, BCM4339, BCM4358)
|
||||
- [Armbian](https://www.armbian.com/) — Debian/Ubuntu for ARM SBCs and TV boxes
|
||||
- [rtw88 driver](https://github.com/torvalds/linux/tree/master/drivers/net/wireless/realtek/rtw88) — Mainline Linux driver for Realtek 802.11ac chips
|
||||
- [mt76 driver](https://github.com/torvalds/linux/tree/master/drivers/net/wireless/mediatek/mt76) — Mainline Linux driver for MediaTek WiFi chips
|
||||
- [cross](https://github.com/cross-rs/cross) — Zero-setup Rust cross-compilation
|
||||
- [ADR-018: ESP32 CSI Binary Protocol](ADR-018-dev-implementation.md) — Binary frame format reused for Phase 2 CSI extraction
|
||||
- [ADR-039: Edge Intelligence](ADR-039-esp32-edge-intelligence.md) — On-device processing tiers
|
||||
- [ADR-043: Sensing Server](ADR-043-sensing-server-ui-api-completion.md) — Single-binary deployment target
|
||||
@@ -0,0 +1,152 @@
|
||||
# ADR-047: RuView Observatory — Immersive Three.js WiFi Sensing Visualization
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (Implemented)
|
||||
|
||||
## Date
|
||||
|
||||
2026-03-04
|
||||
|
||||
## Context
|
||||
|
||||
The project has a functional tabbed dashboard UI (`ui/index.html`) with existing Three.js components (body model, gaussian splats, signal visualization, environment). While effective for monitoring, it lacks a cinematic, immersive visualization suitable for demonstrations and stakeholder presentations.
|
||||
|
||||
We need an immersive Three.js room-based visualization with practical WiFi sensing data overlays — human wireframe pose, dot-matrix body mass, vital signs HUD, signal field heatmap — powered by ESP32 CSI data (demo mode with live WebSocket path).
|
||||
|
||||
## Decision
|
||||
|
||||
### Standalone Page Architecture
|
||||
|
||||
`ui/observatory.html` is a standalone full-screen entry point, separate from the tabbed dashboard. Linked via "Observatory" nav tab in `ui/index.html`. No build step — vanilla JS modules with Three.js r160 via CDN importmap.
|
||||
|
||||
### Room-Based Visualization
|
||||
|
||||
Instead of abstract holographic panels, the observatory renders a practical room scene with:
|
||||
|
||||
| Element | Implementation | Data Source |
|
||||
|---------|---------------|-------------|
|
||||
| Human wireframe | COCO 17-keypoint skeleton, CylinderGeometry tube bones, SphereGeometry joints with glow halos | `persons[].position`, `vital_signs.breathing_rate_bpm` |
|
||||
| Dot-matrix mist | 800 Points with per-particle alpha ShaderMaterial, body-shaped distribution | `persons[].position`, `persons[].motion_score` |
|
||||
| Particle trail | 200 Points with age-based fade, emitted from moving person | `persons[].position`, `persons[].motion_score` |
|
||||
| Signal field | 400 floor-level Points with green→amber color ramp | `signal_field.values` (20×20 grid) |
|
||||
| WiFi waves | 5 wireframe SphereGeometry shells, AdditiveBlending, pulsing outward | Always-on animation from router position |
|
||||
| Router | BoxGeometry body, 3 CylinderGeometry antennas, pulsing LED, PointLight | Static scene element |
|
||||
| Room | GridHelper floor, BoxGeometry wireframe boundary, reflective MeshStandardMaterial floor, furniture (table, bed) | Static scene element |
|
||||
|
||||
### HUD Overlay
|
||||
|
||||
Glass-morphism HTML panels overlaid on the 3D canvas:
|
||||
|
||||
- **Left panel (Vital Signs):** Heart rate (BPM), respiration (RPM), confidence (%) with animated bars
|
||||
- **Right panel (WiFi Signal):** RSSI, variance, motion power, person count, 2D RSSI sparkline, presence state badge, fall alert
|
||||
- **Top-right:** Data source badge (DEMO/LIVE), scenario badge, FPS counter, settings gear
|
||||
- **Bottom:** Capability bar (Pose Estimation, Vital Monitoring, Presence Detection)
|
||||
- **Bottom-right:** Keyboard shortcut hints
|
||||
|
||||
### Settings Dialog (4 Tabs)
|
||||
|
||||
Full customization with localStorage persistence and JSON export:
|
||||
|
||||
| Tab | Controls |
|
||||
|-----|----------|
|
||||
| **Rendering** | Bloom strength/radius/threshold, exposure, vignette, film grain, chromatic aberration |
|
||||
| **Wireframe** | Bone thickness, joint size, glow intensity, particle trail, wireframe color, joint color, aura opacity |
|
||||
| **Scene** | Signal field opacity, WiFi wave intensity, room brightness, floor reflection, FOV, orbit speed, grid toggle, room boundary toggle |
|
||||
| **Data** | Scenario selector (auto-cycle or fixed), cycle speed, data source (demo/WebSocket), WS URL, reset camera, export settings |
|
||||
|
||||
### Demo-First with Live Data Path
|
||||
|
||||
Four auto-cycling scenarios (30s default, configurable) with 2s cosine crossfade:
|
||||
|
||||
| Scenario | Description |
|
||||
|----------|-------------|
|
||||
| `empty_room` | Low variance, no presence, flat amplitude, stable RSSI -45dBm |
|
||||
| `single_breathing` | 1 person, breathing 16 BPM, HR 72 BPM, sinusoidal subcarrier modulation |
|
||||
| `two_walking` | 2 persons, high motion, Doppler-like shifts, moving signal field peaks |
|
||||
| `fall_event` | 2s variance spike at t=5s, then stillness, fall flag, confidence drop |
|
||||
|
||||
Data contract matches `SensingUpdate` struct from the Rust sensing server. Live WebSocket connection configurable in settings dialog.
|
||||
|
||||
### Post-Processing Pipeline
|
||||
|
||||
EffectComposer chain: RenderPass → UnrealBloomPass → custom VignetteShader
|
||||
|
||||
- **UnrealBloom:** strength 1.0, radius 0.5, threshold 0.25 (configurable)
|
||||
- **VignetteShader:** warm shadow shift, edge chromatic aberration, film grain
|
||||
- **Adaptive quality:** Auto-degrades when FPS < 25, restores when FPS > 55
|
||||
|
||||
### RuView Foundation Color Palette
|
||||
|
||||
| Role | Color | Hex |
|
||||
|------|-------|-----|
|
||||
| Background | Deep dark | `#080c14` |
|
||||
| Primary wireframe | Green glow | `#00d878` |
|
||||
| Warm accent | Amber | `#ffb020` |
|
||||
| Signal | Blue | `#2090ff` |
|
||||
| Heart / joints | Red | `#ff4060` |
|
||||
| Alert | Crimson | `#ff3040` |
|
||||
|
||||
### Technology Choices
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| Standalone page vs tab | Full-screen immersion, independent loading |
|
||||
| Room-based vs abstract panels | Practical spatial context for WiFi sensing data |
|
||||
| Vanilla JS + CDN, no build step | Matches existing `ui/` pattern, served as static files by Axum |
|
||||
| Custom ShaderMaterial for mist | Per-particle alpha, body-shaped distribution, AdditiveBlending |
|
||||
| CylinderGeometry tube bones | Visible at any zoom vs thin Line geometry |
|
||||
| COCO 17-keypoint skeleton | Standard pose format, 16 bone connections |
|
||||
| localStorage settings | Persistent customization without server round-trip |
|
||||
| Adaptive quality | 3 levels, auto-switches based on FPS measurement |
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `A` | Toggle autopilot orbit |
|
||||
| `D` | Cycle demo scenario |
|
||||
| `F` | Toggle FPS counter |
|
||||
| `S` | Open/close settings |
|
||||
| `Space` | Pause/resume data |
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `ui/observatory.html` | Full-screen entry point with HUD overlay + settings dialog |
|
||||
| `ui/observatory/js/main.js` | Scene orchestrator (~1,100 lines): room, wireframe, mist, trails, settings, HUD, animation loop |
|
||||
| `ui/observatory/js/demo-data.js` | 4 scenarios with cosine crossfade, setScenario/setCycleDuration API |
|
||||
| `ui/observatory/js/nebula-background.js` | Procedural fBM nebula + star field background sphere |
|
||||
| `ui/observatory/js/post-processing.js` | EffectComposer: UnrealBloom + VignetteShader (chromatic, grain, warmth) |
|
||||
| `ui/observatory/css/observatory.css` | Foundation color scheme, glass-morphism panels, settings dialog, responsive |
|
||||
| `ui/index.html` | Modified: added Observatory nav link |
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Standalone page does not affect existing dashboard stability
|
||||
- Demo-first allows offline presentations without hardware
|
||||
- Same `SensingUpdate` contract enables seamless live WebSocket switch
|
||||
- Room-based visualization provides intuitive spatial context for WiFi sensing
|
||||
- Dot-matrix mist gives visual body mass without occluding wireframe
|
||||
- Full settings customization without code changes (localStorage + JSON export)
|
||||
- Adaptive quality ensures usability on weaker hardware
|
||||
- ~20 draw calls keeps performance well within budget
|
||||
|
||||
### Negative
|
||||
- Additional static files served by Axum (minimal overhead)
|
||||
- Three.js r160 loaded from CDN (no build step, matches existing pattern)
|
||||
- Settings persistence is per-browser (localStorage, not synced)
|
||||
|
||||
### Risks
|
||||
- CDN dependency for Three.js (mitigated: can vendor locally if needed)
|
||||
- Post-processing may not work on very old GPUs (mitigated: adaptive quality disables bloom)
|
||||
|
||||
## References
|
||||
|
||||
- ADR-045: AMOLED display support
|
||||
- ADR-046: Android TV / Armbian deployment
|
||||
- Existing `ui/components/scene.js` — Three.js scene pattern
|
||||
- Existing `ui/components/gaussian-splats.js` — ShaderMaterial pattern
|
||||
- Existing `ui/services/sensing.service.js` — WebSocket data contract
|
||||
@@ -0,0 +1,140 @@
|
||||
# ADR-048: Adaptive CSI Activity Classifier
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | Accepted |
|
||||
| Date | 2026-03-05 |
|
||||
| Deciders | ruv |
|
||||
| Depends on | ADR-024 (AETHER Embeddings), ADR-039 (Edge Processing), ADR-045 (AMOLED Display) |
|
||||
|
||||
## Context
|
||||
|
||||
WiFi-based activity classification using ESP32 Channel State Information (CSI) relies on hand-tuned thresholds to distinguish between activity states (absent, present_still, present_moving, active). These static thresholds are brittle — they don't account for:
|
||||
|
||||
- **Environment-specific signal patterns**: Room geometry, furniture, wall materials, and ESP32 placement all affect how CSI signals respond to human activity.
|
||||
- **Temporal noise characteristics**: Real ESP32 CSI data at ~10 FPS has significant frame-to-frame jitter that causes classification to jump between states.
|
||||
- **Vital signs estimation noise**: Heart rate and breathing rate estimates from Goertzel filter banks produce large swings (50+ BPM frame-to-frame) at low confidence levels.
|
||||
|
||||
The existing threshold-based approach produces noisy, unstable classifications that degrade the user experience in the Observatory visualization and the main dashboard.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Three-Stage Signal Smoothing Pipeline
|
||||
|
||||
All CSI-derived metrics pass through a three-stage pipeline before reaching the UI:
|
||||
|
||||
#### Stage 1: Adaptive Baseline Subtraction
|
||||
- EMA with α=0.003 (~30s time constant) tracks the "quiet room" noise floor
|
||||
- Only updates during low-motion periods to avoid inflating baseline during activity
|
||||
- 50-frame warm-up period for initial baseline learning
|
||||
- Subtracts 70% of baseline from raw motion score to remove environmental drift
|
||||
|
||||
#### Stage 2: EMA + Median Filtering
|
||||
- **Motion score**: Blended from 4 signals (temporal diff 40%, variance 20%, motion band power 25%, change points 15%), then EMA-smoothed with α=0.15
|
||||
- **Vital signs**: 21-frame sliding window → trimmed mean (drop top/bottom 25%) → EMA with α=0.02 (~5s time constant)
|
||||
- **Dead-band**: HR won't update unless trimmed mean differs by >2 BPM; BR needs >0.5 BPM
|
||||
- **Outlier rejection**: HR jumps >8 BPM/frame and BR jumps >2 BPM/frame are discarded
|
||||
|
||||
#### Stage 3: Hysteresis Debounce
|
||||
- Activity state transitions require 4 consecutive frames (~0.4s) of agreement before committing
|
||||
- Prevents rapid flickering between states
|
||||
- Independent candidate tracking resets on new direction changes
|
||||
|
||||
### 2. Adaptive Classifier Module (`adaptive_classifier.rs`)
|
||||
|
||||
A Rust-native environment-tuned classifier that learns from labeled JSONL recordings:
|
||||
|
||||
#### Feature Extraction (15 features)
|
||||
| # | Feature | Source | Discriminative Power |
|
||||
|---|---------|--------|---------------------|
|
||||
| 0 | variance | Server | Medium — temporal CSI spread |
|
||||
| 1 | motion_band_power | Server | Medium — high-frequency subcarrier energy |
|
||||
| 2 | breathing_band_power | Server | Low — respiratory band energy |
|
||||
| 3 | spectral_power | Server | Low — mean squared amplitude |
|
||||
| 4 | dominant_freq_hz | Server | Low — peak subcarrier index |
|
||||
| 5 | change_points | Server | Medium — threshold crossing count |
|
||||
| 6 | mean_rssi | Server | Low — received signal strength |
|
||||
| 7 | amp_mean | Subcarrier | Medium — mean amplitude across 56 subcarriers |
|
||||
| 8 | amp_std | Subcarrier | **High** — amplitude spread (motion increases spread) |
|
||||
| 9 | amp_skew | Subcarrier | Medium — asymmetry of amplitude distribution |
|
||||
| 10 | amp_kurt | Subcarrier | **High** — peakedness (presence creates peaks) |
|
||||
| 11 | amp_iqr | Subcarrier | Medium — inter-quartile range |
|
||||
| 12 | amp_entropy | Subcarrier | **High** — spectral entropy (motion increases disorder) |
|
||||
| 13 | amp_max | Subcarrier | Medium — peak amplitude value |
|
||||
| 14 | amp_range | Subcarrier | Medium — amplitude dynamic range |
|
||||
|
||||
#### Training Algorithm
|
||||
- **Multiclass logistic regression** with softmax output
|
||||
- **Mini-batch SGD** (batch size 32, 200 epochs, linear learning rate decay)
|
||||
- **Z-score normalisation** using global mean/stddev computed from all training data
|
||||
- Per-class statistics (mean, stddev) stored for Mahalanobis distance fallback
|
||||
- Deterministic shuffling (LCG PRNG, seed 42) for reproducible results
|
||||
|
||||
#### Training Data Pipeline
|
||||
1. Record labeled CSI sessions via `POST /api/v1/recording/start {"id":"train_<label>"}`
|
||||
2. Filename-based label assignment: `*empty*`→absent, `*still*`→present_still, `*walking*`→present_moving, `*active*`→active
|
||||
3. Train via `POST /api/v1/adaptive/train`
|
||||
4. Model saved to `data/adaptive_model.json`, auto-loaded on server restart
|
||||
|
||||
#### Inference Pipeline
|
||||
1. Extract 15-feature vector from current CSI frame
|
||||
2. Z-score normalise using stored global mean/stddev
|
||||
3. Compute softmax probabilities across 4 classes
|
||||
4. Blend adaptive model confidence (70%) with smoothed threshold confidence (30%)
|
||||
5. Override classification only when adaptive model is loaded
|
||||
|
||||
### 3. API Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| POST | `/api/v1/adaptive/train` | Train classifier from `train_*` recordings |
|
||||
| GET | `/api/v1/adaptive/status` | Check model status, accuracy, class stats |
|
||||
| POST | `/api/v1/adaptive/unload` | Revert to threshold-based classification |
|
||||
| POST | `/api/v1/recording/start` | Start recording CSI frames (JSONL) |
|
||||
| POST | `/api/v1/recording/stop` | Stop recording |
|
||||
| GET | `/api/v1/recording/list` | List available recordings |
|
||||
|
||||
### 4. Vital Signs Smoothing
|
||||
|
||||
| Parameter | Value | Rationale |
|
||||
|-----------|-------|-----------|
|
||||
| Median window | 21 frames | ~2s of history, robust to transients |
|
||||
| Aggregation | Trimmed mean (middle 50%) | More stable than pure median, less noisy than raw mean |
|
||||
| EMA alpha | 0.02 | ~5s time constant — readings change very slowly |
|
||||
| HR dead-band | ±2 BPM | Prevents display creep from micro-fluctuations |
|
||||
| BR dead-band | ±0.5 BPM | Same for breathing rate |
|
||||
| HR max jump | 8 BPM/frame | Outlier rejection threshold |
|
||||
| BR max jump | 2 BPM/frame | Outlier rejection threshold |
|
||||
|
||||
## Consequences
|
||||
|
||||
### Benefits
|
||||
- **Stable UI**: Vital signs readings hold steady for 5-10+ seconds instead of jumping every frame
|
||||
- **Environment adaptation**: Classifier learns the specific room's signal characteristics
|
||||
- **Graceful fallback**: If no adaptive model is loaded, threshold-based classification with smoothing still works
|
||||
- **No external dependencies**: Pure Rust implementation, no Python/ML frameworks needed
|
||||
- **Fast training**: 3,000+ frames train in <1 second on commodity hardware
|
||||
- **Portable model**: JSON serialisation, loadable on any platform
|
||||
|
||||
### Limitations
|
||||
- **Single-link**: With one ESP32, the feature space is limited. Multi-AP setups (ADR-029) would dramatically improve separability.
|
||||
- **No temporal features**: Current frame-level classification doesn't use sequence models (LSTM/Transformer). Could be added later.
|
||||
- **Label quality**: Training accuracy depends heavily on recording quality (distinct activities, actual room vacancy for "empty").
|
||||
- **Linear classifier**: Logistic regression may underfit non-linear decision boundaries. Could upgrade to 2-layer MLP if needed.
|
||||
|
||||
### Future Work
|
||||
- **Online learning**: Continuously update model weights from user corrections
|
||||
- **Sequence models**: Use sliding window of N frames as input for temporal pattern recognition
|
||||
- **Contrastive pretraining**: Leverage ADR-024 AETHER embeddings for self-supervised feature learning
|
||||
- **Multi-AP fusion**: Use ADR-029 multistatic sensing for richer feature space
|
||||
- **Edge deployment**: Export learned thresholds to ESP32 firmware (ADR-039 Tier 2) for on-device classification
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `crates/wifi-densepose-sensing-server/src/adaptive_classifier.rs` | Adaptive classifier module (feature extraction, training, inference) |
|
||||
| `crates/wifi-densepose-sensing-server/src/main.rs` | Smoothing pipeline, API endpoints, integration |
|
||||
| `ui/observatory/js/hud-controller.js` | UI-side lerp smoothing (4% per frame) |
|
||||
| `data/adaptive_model.json` | Trained model (auto-created by training endpoint) |
|
||||
| `data/recordings/train_*.jsonl` | Labeled training recordings |
|
||||
@@ -0,0 +1,122 @@
|
||||
# ADR-049: Cross-Platform WiFi Interface Detection and Graceful Degradation
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | Proposed |
|
||||
| Date | 2026-03-06 |
|
||||
| Deciders | ruv |
|
||||
| Depends on | ADR-013 (Feature-Level Sensing), ADR-025 (macOS CoreWLAN) |
|
||||
| Issue | [#148](https://github.com/ruvnet/wifi-densepose/issues/148) |
|
||||
|
||||
## Context
|
||||
|
||||
Users report `RuntimeError: Cannot read /proc/net/wireless` when running WiFi DensePose in environments where the Linux wireless proc filesystem is unavailable:
|
||||
|
||||
- **Docker containers** on macOS/Windows (Linux kernel detected, but no wireless subsystem)
|
||||
- **WSL2** without USB WiFi passthrough
|
||||
- **Headless Linux servers** without WiFi hardware
|
||||
- **Embedded Linux** boards without wireless-extensions support
|
||||
|
||||
The current architecture has two layers of defense:
|
||||
|
||||
1. **`ws_server.py`** (line 345-355) checks `os.path.exists("/proc/net/wireless")` before instantiating `LinuxWifiCollector` and falls back to `SimulatedCollector` if missing.
|
||||
2. **`rssi_collector.py`** `LinuxWifiCollector._validate_interface()` (line 178-196) raises a hard `RuntimeError` if `/proc/net/wireless` is missing or the interface isn't listed.
|
||||
|
||||
However, there are gaps:
|
||||
|
||||
- **Direct usage**: Any code that instantiates `LinuxWifiCollector` directly (outside `ws_server.py`) hits the unguarded `RuntimeError` with no fallback.
|
||||
- **Error message**: The RuntimeError message tells users to "use SimulatedCollector instead" but doesn't explain how.
|
||||
- **No auto-detection**: The collector selection logic is duplicated between `ws_server.py` and `install.sh` with no shared platform-detection utility.
|
||||
- **Partial `/proc/net/wireless`**: The file may exist (e.g., kernel module loaded) but contain no interfaces, producing a confusing "interface not found" error instead of a clean fallback.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Platform-Aware Collector Factory
|
||||
|
||||
Introduce a `create_collector()` factory function in `rssi_collector.py` that encapsulates the platform detection and fallback chain:
|
||||
|
||||
```python
|
||||
def create_collector(
|
||||
preferred: str = "auto",
|
||||
interface: str = "wlan0",
|
||||
sample_rate_hz: float = 10.0,
|
||||
) -> BaseCollector:
|
||||
"""
|
||||
Create the best available WiFi collector for the current platform.
|
||||
|
||||
Resolution order (when preferred="auto"):
|
||||
1. ESP32 CSI (if UDP port 5005 is receiving frames)
|
||||
2. Platform-native WiFi:
|
||||
- Linux: LinuxWifiCollector (requires /proc/net/wireless + active interface)
|
||||
- Windows: WindowsWifiCollector (netsh wlan)
|
||||
- macOS: MacosWifiCollector (CoreWLAN)
|
||||
3. SimulatedCollector (always available)
|
||||
|
||||
Raises nothing — always returns a usable collector.
|
||||
"""
|
||||
```
|
||||
|
||||
### 2. Soft Validation in LinuxWifiCollector
|
||||
|
||||
Replace the hard `RuntimeError` in `_validate_interface()` with a class method that returns availability status without raising:
|
||||
|
||||
```python
|
||||
@classmethod
|
||||
def is_available(cls, interface: str = "wlan0") -> tuple[bool, str]:
|
||||
"""Check if Linux WiFi collection is possible. Returns (available, reason)."""
|
||||
if not os.path.exists("/proc/net/wireless"):
|
||||
return False, "/proc/net/wireless not found (Docker, WSL, or no wireless subsystem)"
|
||||
with open("/proc/net/wireless") as f:
|
||||
content = f.read()
|
||||
if interface not in content:
|
||||
names = cls._parse_interface_names(content)
|
||||
return False, f"Interface '{interface}' not in /proc/net/wireless. Available: {names}"
|
||||
return True, "ok"
|
||||
```
|
||||
|
||||
The existing `_validate_interface()` continues to raise `RuntimeError` for direct callers who need fail-fast behavior, but `create_collector()` uses `is_available()` to probe without exceptions.
|
||||
|
||||
### 3. Structured Fallback Logging
|
||||
|
||||
When auto-detection skips a collector, log at `WARNING` level with actionable context:
|
||||
|
||||
```
|
||||
WiFi collector: LinuxWifiCollector unavailable (/proc/net/wireless not found — likely Docker/WSL).
|
||||
WiFi collector: Falling back to SimulatedCollector. For real sensing, connect ESP32 nodes via UDP:5005.
|
||||
```
|
||||
|
||||
### 4. Consolidate Platform Detection
|
||||
|
||||
Remove duplicated platform-detection logic from `ws_server.py` and `install.sh`. Both should use `create_collector()` (Python) or a shared `detect_wifi_platform()` shell function.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **Zero-crash startup**: `create_collector("auto")` never raises — Docker, WSL, and headless users get `SimulatedCollector` automatically with a clear log message.
|
||||
- **Single detection path**: Platform logic lives in one place (`rssi_collector.py`), reducing drift between `ws_server.py`, `install.sh`, and future entry points.
|
||||
- **Better DX**: Error messages explain *why* a collector is unavailable and *what to do* (connect ESP32, install WiFi driver, etc.).
|
||||
|
||||
### Negative
|
||||
|
||||
- **SimulatedCollector may mask hardware issues**: Users with real WiFi hardware that fails detection might unknowingly run on simulated data. Mitigated by the `WARNING`-level log.
|
||||
- **Breaking change for direct `LinuxWifiCollector` callers**: Code that catches `RuntimeError` from `_validate_interface()` as a signal needs to migrate to `is_available()` or `create_collector()`. This is a minor change — there are no known external consumers.
|
||||
|
||||
### Neutral
|
||||
|
||||
- `_validate_interface()` behavior is unchanged for existing direct callers — this is additive.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
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)
|
||||
5. Comment on issue #148 with the fix
|
||||
|
||||
## References
|
||||
|
||||
- Issue #148: RuntimeError: Cannot read /proc/net/wireless
|
||||
- ADR-013: Feature-Level Sensing on Commodity Gear
|
||||
- ADR-025: macOS CoreWLAN WiFi Sensing
|
||||
- [Linux /proc/net/wireless documentation](https://www.kernel.org/doc/html/latest/networking/statistics.html)
|
||||
@@ -0,0 +1,100 @@
|
||||
# ADR-050: Quality Engineering Response — Security Hardening & Code Quality
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Status | Accepted |
|
||||
| Date | 2026-03-06 |
|
||||
| Deciders | ruv |
|
||||
| Depends on | ADR-032 (Multistatic Mesh Security) |
|
||||
| Issue | [#170](https://github.com/ruvnet/wifi-densepose/issues/170) |
|
||||
|
||||
## Context
|
||||
|
||||
An independent quality engineering analysis ([issue #170](https://github.com/ruvnet/wifi-densepose/issues/170)) identified 7 critical findings across the Rust codebase. After verification against the source code, the following findings are confirmed and require action:
|
||||
|
||||
### Confirmed Critical Findings
|
||||
|
||||
| # | Finding | Location | Verified |
|
||||
|---|---------|----------|----------|
|
||||
| 1 | Fake HMAC in `secure_tdm.rs` — XOR fold with hardcoded key | `hardware/src/esp32/secure_tdm.rs:253` | YES — comments say "sufficient for testing" |
|
||||
| 2 | `sensing-server/main.rs` is 3,741 lines — CC=65, god object | `sensing-server/src/main.rs` | YES — confirmed 3,741 lines |
|
||||
| 3 | WebSocket server has zero authentication | Rust WS codebase | YES — no auth/token checks found |
|
||||
| 4 | Zero security tests in Rust codebase | Entire workspace | YES — no auth/injection/tampering tests |
|
||||
| 5 | 54K fps claim has no supporting benchmark | No criterion benchmarks | YES — no benchmarks exist |
|
||||
|
||||
### Findings Requiring Further Investigation
|
||||
|
||||
| # | Finding | Status |
|
||||
|---|---------|--------|
|
||||
| 6 | Unauthenticated OTA firmware endpoint | Not found in Rust code — may be ESP32 C firmware level |
|
||||
| 7 | WASM upload without mandatory signatures | Needs review of WASM loader |
|
||||
| 8 | O(n^2) autocorrelation in heart rate detection | Needs profiling to confirm impact |
|
||||
|
||||
## Decision
|
||||
|
||||
Address findings in 3 priority sprints as recommended by the report.
|
||||
|
||||
### Sprint 1: Security (Blocks Deployment)
|
||||
|
||||
1. **Replace fake HMAC with real HMAC-SHA256** in `secure_tdm.rs`
|
||||
- Use the `hmac` + `sha2` crates (already in `Cargo.lock`)
|
||||
- Remove XOR fold implementation
|
||||
- Add key derivation (no more hardcoded keys)
|
||||
|
||||
2. **Add WebSocket authentication**
|
||||
- Token-based auth on WS upgrade handshake
|
||||
- Optional API key for local-network deployments
|
||||
- Configurable via environment variable
|
||||
|
||||
3. **Add security test suite**
|
||||
- Auth bypass attempts
|
||||
- Malformed CSI frame injection
|
||||
- Protocol tampering (TDM beacon replay, nonce reuse)
|
||||
|
||||
### Sprint 2: Code Quality & Testability
|
||||
|
||||
4. **Decompose `main.rs`** (3,741 lines -> ~14 focused modules)
|
||||
- Extract HTTP routes, WebSocket handler, CSI pipeline, config, state
|
||||
- Target: no file over 500 lines
|
||||
|
||||
5. **Add criterion benchmarks**
|
||||
- CSI frame parsing throughput
|
||||
- Signal processing pipeline latency
|
||||
- WebSocket broadcast fanout
|
||||
|
||||
### Sprint 3: Functional Verification
|
||||
|
||||
6. **Vital sign accuracy verification**
|
||||
- Reference signal tests with known BPM
|
||||
- False-negative rate measurement
|
||||
|
||||
7. **Fix O(n^2) autocorrelation** (if confirmed by profiling)
|
||||
- Replace brute-force lag with FFT-based autocorrelation
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Addresses all critical security findings before any production deployment
|
||||
- `main.rs` decomposition enables unit testing of server components
|
||||
- Criterion benchmarks provide verifiable performance claims
|
||||
- Security test suite prevents regression
|
||||
|
||||
### Negative
|
||||
|
||||
- Sprint 1 security changes are breaking for any existing TDM mesh deployments (fake HMAC -> real HMAC requires firmware update)
|
||||
- `main.rs` decomposition is a large refactor with merge conflict risk
|
||||
|
||||
### Neutral
|
||||
|
||||
- The report correctly identifies that life-safety claims (disaster detection, vital signs) require rigorous verification — this is an ongoing process, not a single sprint
|
||||
|
||||
## Acknowledgment
|
||||
|
||||
Thanks to [@proffesor-for-testing](https://github.com/proffesor-for-testing) for the thorough 10-report analysis. The full report is archived at the [original gist](https://gist.github.com/proffesor-for-testing/02321e3f272720aa94484fffec6ab19b).
|
||||
|
||||
## References
|
||||
|
||||
- Issue #170: Quality Engineering Analysis
|
||||
- ADR-032: Multistatic Mesh Security Hardening
|
||||
- ADR-028: ESP32 Capability Audit
|
||||
@@ -0,0 +1,621 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,810 @@
|
||||
# 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 (`wifi-densepose-rs`). 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:
|
||||
|
||||
```
|
||||
rust-port/wifi-densepose-rs/
|
||||
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 rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/frontend
|
||||
npm install
|
||||
|
||||
# Development (hot-reload frontend + Rust rebuild)
|
||||
cd rust-port/wifi-densepose-rs/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
|
||||
- `rust-port/wifi-densepose-rs/crates/wifi-densepose-sensing-server/` — Sensing server
|
||||
- `rust-port/wifi-densepose-rs/crates/wifi-densepose-hardware/` — Hardware crate
|
||||
- `ui/` — Existing web UI
|
||||
@@ -0,0 +1,274 @@
|
||||
# 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/
|
||||
@@ -0,0 +1,699 @@
|
||||
# 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/)
|
||||
@@ -0,0 +1,119 @@
|
||||
# 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
|
||||
@@ -0,0 +1,251 @@
|
||||
# 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
|
||||
@@ -0,0 +1,82 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,392 @@
|
||||
# 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 ../../rust-port/wifi-densepose-rs/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
|
||||
@@ -0,0 +1,83 @@
|
||||
# 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
|
||||
@@ -0,0 +1,648 @@
|
||||
# Deployment Platform Domain Model
|
||||
|
||||
The Deployment Platform domain covers everything from cross-compiling the sensing server for ARM targets to managing TV box appliances running Armbian: provisioning devices, deploying binaries, configuring kiosk displays, and coordinating multi-room installations. It bridges the gap between the Sensing Server domain (which produces the binary) and the physical hardware it runs on.
|
||||
|
||||
This document defines the system using [Domain-Driven Design](https://martinfowler.com/bliki/DomainDrivenDesign.html) (DDD): bounded contexts that own their data and rules, aggregate roots that enforce invariants, value objects that carry meaning, and domain events that connect everything.
|
||||
|
||||
**Bounded Contexts:**
|
||||
|
||||
| # | Context | Responsibility | Key ADRs | Code |
|
||||
|---|---------|----------------|----------|------|
|
||||
| 1 | [Appliance Management](#1-appliance-management-context) | Device inventory, provisioning, health monitoring, OTA updates for TV box deployments | [ADR-046](../adr/ADR-046-android-tv-box-armbian-deployment.md) | `scripts/deploy/`, `config/armbian/` |
|
||||
| 2 | [Cross-Compilation](#2-cross-compilation-context) | Build pipeline for aarch64, binary packaging, CI/CD release artifacts | [ADR-046](../adr/ADR-046-android-tv-box-armbian-deployment.md) | `.github/workflows/`, `Cross.toml` |
|
||||
| 3 | [Display Kiosk](#3-display-kiosk-context) | HDMI output management, Chromium kiosk mode, screen rotation, auto-start | [ADR-046](../adr/ADR-046-android-tv-box-armbian-deployment.md) | `config/armbian/kiosk/` |
|
||||
| 4 | [WiFi CSI Bridge](#4-wifi-csi-bridge-context) | Custom WiFi driver CSI extraction, protocol translation to ESP32 binary format | [ADR-046](../adr/ADR-046-android-tv-box-armbian-deployment.md) | `tools/csi-bridge/` |
|
||||
| 5 | [Network Topology](#5-network-topology-context) | ESP32 mesh ↔ TV box connectivity, dedicated AP mode, multi-room routing | [ADR-046](../adr/ADR-046-android-tv-box-armbian-deployment.md), [ADR-012](../adr/ADR-012-esp32-csi-sensor-mesh.md) | `config/armbian/network/` |
|
||||
|
||||
---
|
||||
|
||||
## Domain-Driven Design Specification
|
||||
|
||||
### Ubiquitous Language
|
||||
|
||||
| Term | Definition |
|
||||
|------|------------|
|
||||
| **Appliance** | A TV box running Armbian with the sensing server deployed, treated as a managed device in the fleet |
|
||||
| **Fleet** | The set of all appliances across a multi-room or multi-site installation |
|
||||
| **Deployment Package** | A self-contained archive containing the sensing-server binary, systemd unit, configuration, and setup script for a target architecture |
|
||||
| **Kiosk Mode** | Chromium running in full-screen, no-UI mode pointing at `localhost:3000`, auto-started by systemd on HDMI-connected appliances |
|
||||
| **CSI Bridge** | A userspace daemon that reads CSI data from a patched WiFi driver and re-encodes it as ESP32-compatible UDP frames for the sensing server |
|
||||
| **Dedicated AP** | An optional `hostapd`-managed WiFi access point on the TV box that creates an isolated network for ESP32 nodes |
|
||||
| **OTA Update** | Over-the-air binary replacement: download new sensing-server binary, validate checksum, swap via atomic rename, restart service |
|
||||
| **Reference Device** | A TV box model that has been tested and validated for Armbian + sensing-server deployment (e.g., T95 Max+ / S905X3) |
|
||||
| **Provisioning** | First-time setup of an appliance: flash Armbian to SD, deploy package, configure WiFi, start services |
|
||||
| **Health Beacon** | Periodic JSON payload sent by each appliance to a central coordinator (if multi-room) containing uptime, CPU temp, memory usage, inference latency, connected ESP32 count |
|
||||
|
||||
---
|
||||
|
||||
## Bounded Contexts
|
||||
|
||||
### 1. Appliance Management Context
|
||||
|
||||
**Responsibility:** Track deployed TV box appliances, provision new devices, monitor health, and coordinate OTA updates across the fleet.
|
||||
|
||||
```
|
||||
+------------------------------------------------------------+
|
||||
| Appliance Management Context |
|
||||
+------------------------------------------------------------+
|
||||
| |
|
||||
| +----------------+ +----------------+ |
|
||||
| | Device | | Provisioning | |
|
||||
| | Registry | | Service | |
|
||||
| | (fleet state) | | (first-time | |
|
||||
| | | | setup) | |
|
||||
| +-------+--------+ +-------+--------+ |
|
||||
| | | |
|
||||
| +----------+----------+ |
|
||||
| v |
|
||||
| +-------------------+ |
|
||||
| | Health Monitor | |
|
||||
| | (beacon receiver,| |
|
||||
| | thermal alerts, | |
|
||||
| | connectivity) | |
|
||||
| +--------+----------+ |
|
||||
| v |
|
||||
| +-------------------+ |
|
||||
| | OTA Updater | |
|
||||
| | (binary swap, | |
|
||||
| | rollback, | |
|
||||
| | checksum verify)| |
|
||||
| +-------------------+ |
|
||||
| |
|
||||
+------------------------------------------------------------+
|
||||
```
|
||||
|
||||
**Aggregates:**
|
||||
|
||||
```rust
|
||||
/// Aggregate Root: A managed TV box appliance in the fleet.
|
||||
/// Identified by MAC address of the primary Ethernet interface.
|
||||
pub struct Appliance {
|
||||
/// Unique device identifier (Ethernet MAC address).
|
||||
pub device_id: DeviceId,
|
||||
/// Human-readable name (e.g., "living-room", "bedroom-1").
|
||||
pub name: String,
|
||||
/// Hardware model (e.g., "T95 Max+ S905X3").
|
||||
pub hardware_model: HardwareModel,
|
||||
/// Current deployment state.
|
||||
pub state: ApplianceState,
|
||||
/// Installed sensing-server version.
|
||||
pub server_version: SemanticVersion,
|
||||
/// Network configuration.
|
||||
pub network: NetworkConfig,
|
||||
/// Last received health beacon.
|
||||
pub last_health: Option<HealthBeacon>,
|
||||
/// Provisioning timestamp.
|
||||
pub provisioned_at: DateTime<Utc>,
|
||||
/// Connected ESP32 node IDs (from last beacon).
|
||||
pub connected_nodes: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Lifecycle states for an appliance.
|
||||
pub enum ApplianceState {
|
||||
/// SD card prepared, not yet booted.
|
||||
Provisioned,
|
||||
/// Booted and running, health beacons received.
|
||||
Online,
|
||||
/// No health beacon for >5 minutes.
|
||||
Unreachable,
|
||||
/// OTA update in progress.
|
||||
Updating,
|
||||
/// Manual maintenance / stopped.
|
||||
Offline,
|
||||
/// Thermal throttling or hardware issue detected.
|
||||
Degraded,
|
||||
}
|
||||
```
|
||||
|
||||
**Value Objects:**
|
||||
|
||||
```rust
|
||||
/// Hardware model specification for a TV box.
|
||||
pub struct HardwareModel {
|
||||
/// Marketing name (e.g., "T95 Max+").
|
||||
pub name: String,
|
||||
/// SoC identifier (e.g., "Amlogic S905X3").
|
||||
pub soc: String,
|
||||
/// WiFi chipset (e.g., "RTL8822CS").
|
||||
pub wifi_chipset: String,
|
||||
/// Total RAM in MB.
|
||||
pub ram_mb: u32,
|
||||
/// eMMC storage in GB.
|
||||
pub emmc_gb: u32,
|
||||
/// Whether CSI bridge is supported for this WiFi chipset.
|
||||
pub csi_bridge_supported: bool,
|
||||
/// Armbian device tree name (e.g., "meson-sm1-sei610").
|
||||
pub armbian_dtb: String,
|
||||
}
|
||||
|
||||
/// Periodic health report from an appliance.
|
||||
pub struct HealthBeacon {
|
||||
pub device_id: DeviceId,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub uptime_secs: u64,
|
||||
pub cpu_temp_celsius: f32,
|
||||
pub cpu_usage_percent: f32,
|
||||
pub memory_used_mb: u32,
|
||||
pub memory_total_mb: u32,
|
||||
pub disk_used_percent: f32,
|
||||
pub inference_latency_ms: f32,
|
||||
pub connected_esp32_nodes: Vec<u8>,
|
||||
pub server_version: SemanticVersion,
|
||||
pub csi_frames_per_sec: f32,
|
||||
pub websocket_clients: u32,
|
||||
}
|
||||
|
||||
/// Network configuration for an appliance.
|
||||
pub struct NetworkConfig {
|
||||
/// Primary IP address (Ethernet or WiFi client).
|
||||
pub ip_address: IpAddr,
|
||||
/// Whether the appliance runs a dedicated AP for ESP32 nodes.
|
||||
pub dedicated_ap: Option<DedicatedApConfig>,
|
||||
/// UDP port for ESP32 CSI reception.
|
||||
pub csi_udp_port: u16, // default: 5005
|
||||
/// HTTP port for sensing server.
|
||||
pub http_port: u16, // default: 3000
|
||||
}
|
||||
|
||||
/// Configuration for a dedicated WiFi AP hosted by the appliance.
|
||||
pub struct DedicatedApConfig {
|
||||
/// SSID for the ESP32 mesh network.
|
||||
pub ssid: String,
|
||||
/// WPA2 passphrase.
|
||||
pub passphrase: String,
|
||||
/// Channel (1-11 for 2.4 GHz).
|
||||
pub channel: u8,
|
||||
/// DHCP range for connected ESP32 nodes.
|
||||
pub dhcp_range: (IpAddr, IpAddr),
|
||||
}
|
||||
|
||||
/// Unique device identifier (Ethernet MAC).
|
||||
pub struct DeviceId(pub [u8; 6]);
|
||||
|
||||
/// Semantic version for tracking installed software.
|
||||
pub struct SemanticVersion {
|
||||
pub major: u16,
|
||||
pub minor: u16,
|
||||
pub patch: u16,
|
||||
pub pre: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
**Domain Services:**
|
||||
- `ProvisioningService` — Generates Armbian SD card image with pre-configured deployment package, WiFi credentials, and systemd units
|
||||
- `HealthMonitorService` — Listens for UDP health beacons from fleet appliances, triggers alerts on thermal throttling (>80°C), unreachable (>5 min), or high memory usage (>90%)
|
||||
- `OtaUpdateService` — Downloads new binary from release URL, verifies SHA-256 checksum, performs atomic swap (`rename(new, current)`), restarts systemd service, rolls back if health beacon fails within 60s
|
||||
|
||||
**Invariants:**
|
||||
- Device ID (MAC address) is immutable after provisioning
|
||||
- OTA update refuses to proceed if current CPU temperature >75°C (thermal headroom)
|
||||
- Rollback is automatic if no healthy beacon is received within 60 seconds of restart
|
||||
- Dedicated AP SSID must not match the upstream WiFi SSID
|
||||
|
||||
---
|
||||
|
||||
### 2. Cross-Compilation Context
|
||||
|
||||
**Responsibility:** Build the sensing-server binary for ARM64 targets, package deployment archives, and manage CI/CD release artifacts.
|
||||
|
||||
```
|
||||
+------------------------------------------------------------+
|
||||
| Cross-Compilation Context |
|
||||
+------------------------------------------------------------+
|
||||
| |
|
||||
| +----------------+ +----------------+ |
|
||||
| | Cross.toml | | GitHub Actions| |
|
||||
| | (target cfg) | | CI Matrix | |
|
||||
| +-------+--------+ +-------+--------+ |
|
||||
| | | |
|
||||
| +----------+----------+ |
|
||||
| v |
|
||||
| +-------------------+ |
|
||||
| | Build Pipeline | |
|
||||
| | (cross build | |
|
||||
| | --target | |
|
||||
| | aarch64-unknown-| |
|
||||
| | linux-gnu) | |
|
||||
| +--------+----------+ |
|
||||
| v |
|
||||
| +-------------------+ |
|
||||
| | Binary Packager | |
|
||||
| | (strip, compress,|---> .tar.gz artifact |
|
||||
| | bundle assets, | |
|
||||
| | systemd units) | |
|
||||
| +-------------------+ |
|
||||
| |
|
||||
+------------------------------------------------------------+
|
||||
```
|
||||
|
||||
**Value Objects:**
|
||||
|
||||
```rust
|
||||
/// A packaged deployment archive for a target platform.
|
||||
pub struct DeploymentPackage {
|
||||
/// Target triple (e.g., "aarch64-unknown-linux-gnu").
|
||||
pub target: String,
|
||||
/// Sensing server binary (stripped).
|
||||
pub binary: PathBuf,
|
||||
/// Binary size in bytes.
|
||||
pub binary_size: u64,
|
||||
/// SHA-256 checksum of the binary.
|
||||
pub checksum: String,
|
||||
/// Systemd service unit file.
|
||||
pub service_unit: String,
|
||||
/// Static web UI assets directory.
|
||||
pub ui_assets: PathBuf,
|
||||
/// Armbian configuration files (kiosk, network, etc.).
|
||||
pub config_files: Vec<PathBuf>,
|
||||
/// Setup script (runs on first boot).
|
||||
pub setup_script: PathBuf,
|
||||
/// Version being packaged.
|
||||
pub version: SemanticVersion,
|
||||
}
|
||||
|
||||
/// Build target specification.
|
||||
pub struct BuildTarget {
|
||||
/// Rust target triple.
|
||||
pub triple: String,
|
||||
/// CPU architecture description.
|
||||
pub arch: String,
|
||||
/// Whether NEON SIMD is available.
|
||||
pub has_neon: bool,
|
||||
/// Cross-compilation Docker image.
|
||||
pub cross_image: String,
|
||||
/// Binary size limit in bytes.
|
||||
pub size_limit: u64,
|
||||
}
|
||||
```
|
||||
|
||||
**Supported Targets:**
|
||||
|
||||
| Target Triple | Architecture | Use Case | Size Limit |
|
||||
|---------------|-------------|----------|------------|
|
||||
| `x86_64-unknown-linux-gnu` | x86-64 | PC/laptop (existing) | 30 MB |
|
||||
| `aarch64-unknown-linux-gnu` | ARM64 | TV box (Armbian) | 15 MB |
|
||||
| `armv7-unknown-linux-gnueabihf` | ARMv7 | Older TV boxes (32-bit) | 12 MB |
|
||||
| `x86_64-pc-windows-msvc` | x86-64 | Windows (existing) | 30 MB |
|
||||
|
||||
**Invariants:**
|
||||
- Stripped binary must be under size limit for target
|
||||
- SHA-256 checksum is computed and included in every deployment package
|
||||
- UI assets are embedded in binary via `include_dir!` or bundled alongside
|
||||
- No native GPU dependencies — CPU-only inference (candle or ONNX Runtime)
|
||||
|
||||
---
|
||||
|
||||
### 3. Display Kiosk Context
|
||||
|
||||
**Responsibility:** Manage HDMI output on TV box appliances, running Chromium in kiosk mode to display the sensing dashboard full-screen on boot.
|
||||
|
||||
```
|
||||
+------------------------------------------------------------+
|
||||
| Display Kiosk Context |
|
||||
+------------------------------------------------------------+
|
||||
| |
|
||||
| +----------------+ +----------------+ |
|
||||
| | systemd | | Chromium | |
|
||||
| | autologin + | | Kiosk Launch | |
|
||||
| | X11/Wayland | | (full-screen, | |
|
||||
| | session | | no-UI bars) | |
|
||||
| +-------+--------+ +-------+--------+ |
|
||||
| | | |
|
||||
| +----------+----------+ |
|
||||
| v |
|
||||
| +-------------------+ |
|
||||
| | Display Manager | |
|
||||
| | (resolution, | |
|
||||
| | rotation, | |
|
||||
| | overscan, | |
|
||||
| | sleep/wake) | |
|
||||
| +-------------------+ |
|
||||
| |
|
||||
+------------------------------------------------------------+
|
||||
```
|
||||
|
||||
**Value Objects:**
|
||||
|
||||
```rust
|
||||
/// Display configuration for kiosk mode.
|
||||
pub struct KioskConfig {
|
||||
/// URL to display (default: "http://localhost:3000").
|
||||
pub url: String,
|
||||
/// Screen rotation in degrees (0, 90, 180, 270).
|
||||
pub rotation: u16,
|
||||
/// Whether to hide the mouse cursor.
|
||||
pub hide_cursor: bool,
|
||||
/// Auto-refresh interval in seconds (0 = disabled).
|
||||
pub auto_refresh_secs: u32,
|
||||
/// Display sleep schedule (e.g., off 23:00-06:00).
|
||||
pub sleep_schedule: Option<SleepSchedule>,
|
||||
/// Overscan compensation percentage (0-10).
|
||||
pub overscan_percent: u8,
|
||||
}
|
||||
|
||||
/// Sleep schedule for display power management.
|
||||
pub struct SleepSchedule {
|
||||
/// Time to turn display off (HH:MM local time).
|
||||
pub sleep_time: String,
|
||||
/// Time to turn display on (HH:MM local time).
|
||||
pub wake_time: String,
|
||||
}
|
||||
```
|
||||
|
||||
**Invariants:**
|
||||
- Chromium kiosk starts only after sensing-server systemd unit is `active`
|
||||
- If Chromium crashes, systemd restarts it within 5 seconds (`Restart=always`)
|
||||
- Display sleep/wake uses CEC commands (HDMI-CEC) to control TV power when available
|
||||
- No browser UI elements are visible (address bar, scrollbars, etc.)
|
||||
|
||||
---
|
||||
|
||||
### 4. WiFi CSI Bridge Context
|
||||
|
||||
**Responsibility:** Extract CSI data from patched WiFi drivers on the TV box and translate it into ESP32-compatible binary frames for the sensing server. This is the Phase 2 custom firmware path.
|
||||
|
||||
```
|
||||
+------------------------------------------------------------+
|
||||
| WiFi CSI Bridge Context |
|
||||
+------------------------------------------------------------+
|
||||
| |
|
||||
| +----------------+ +----------------+ |
|
||||
| | Patched WiFi | | CSI Reader | |
|
||||
| | Driver | | (Netlink / | |
|
||||
| | (kernel space)| | procfs / | |
|
||||
| | CSI hooks | | UDP socket) | |
|
||||
| +-------+--------+ +-------+--------+ |
|
||||
| | | |
|
||||
| +----------+----------+ |
|
||||
| v |
|
||||
| +-------------------+ |
|
||||
| | Protocol | |
|
||||
| | Translator | |
|
||||
| | (chipset CSI → | |
|
||||
| | ESP32 binary | |
|
||||
| | 0xC5100001) | |
|
||||
| +--------+----------+ |
|
||||
| v |
|
||||
| +-------------------+ |
|
||||
| | UDP Sender | |
|
||||
| | (localhost:5005) |---> sensing-server |
|
||||
| +-------------------+ |
|
||||
| |
|
||||
+------------------------------------------------------------+
|
||||
```
|
||||
|
||||
**Value Objects:**
|
||||
|
||||
```rust
|
||||
/// Raw CSI extraction from a WiFi chipset.
|
||||
pub struct ChipsetCsiFrame {
|
||||
/// Source chipset type.
|
||||
pub chipset: WifiChipset,
|
||||
/// Timestamp of extraction (kernel monotonic clock).
|
||||
pub timestamp_us: u64,
|
||||
/// Number of subcarriers (varies by chipset and bandwidth).
|
||||
pub n_subcarriers: u16,
|
||||
/// Number of spatial streams / antennas.
|
||||
pub n_streams: u8,
|
||||
/// Channel frequency in MHz.
|
||||
pub freq_mhz: u16,
|
||||
/// Bandwidth (20/40/80/160 MHz).
|
||||
pub bandwidth_mhz: u16,
|
||||
/// RSSI in dBm.
|
||||
pub rssi_dbm: i8,
|
||||
/// Noise floor estimate in dBm.
|
||||
pub noise_floor_dbm: i8,
|
||||
/// Complex CSI values (I/Q pairs) per subcarrier per stream.
|
||||
pub csi_matrix: Vec<Complex<f32>>,
|
||||
/// Source MAC address (BSSID of the AP being measured).
|
||||
pub source_mac: [u8; 6],
|
||||
}
|
||||
|
||||
/// Supported WiFi chipsets for CSI extraction.
|
||||
pub enum WifiChipset {
|
||||
/// Broadcom BCM43455 via Nexmon CSI patches.
|
||||
BroadcomBcm43455,
|
||||
/// Realtek RTL8822CS via modified rtw88 driver.
|
||||
RealtekRtl8822cs,
|
||||
/// MediaTek MT7661 via mt76 driver modification.
|
||||
MediatekMt7661,
|
||||
}
|
||||
|
||||
/// Translated frame in ESP32 binary protocol (ADR-018).
|
||||
pub struct Esp32CompatFrame {
|
||||
/// Magic: 0xC5100001
|
||||
pub magic: u32,
|
||||
/// Virtual node ID assigned to this WiFi interface.
|
||||
pub node_id: u8,
|
||||
/// Number of antennas / spatial streams.
|
||||
pub n_antennas: u8,
|
||||
/// Number of subcarriers (resampled to match ESP32 format).
|
||||
pub n_subcarriers: u8,
|
||||
/// Frequency in MHz.
|
||||
pub freq_mhz: u16,
|
||||
/// Sequence number (monotonic counter).
|
||||
pub sequence: u32,
|
||||
/// RSSI in dBm.
|
||||
pub rssi: i8,
|
||||
/// Noise floor in dBm.
|
||||
pub noise_floor: i8,
|
||||
/// Amplitude values (extracted from complex CSI).
|
||||
pub amplitudes: Vec<f32>,
|
||||
/// Phase values (extracted from complex CSI).
|
||||
pub phases: Vec<f32>,
|
||||
}
|
||||
```
|
||||
|
||||
**Domain Services:**
|
||||
- `CsiExtractionService` — Reads raw CSI from patched driver via Netlink socket (BCM43455), procfs (RTL8822CS), or UDP (MT7661)
|
||||
- `SubcarrierResamplerService` — Resamples chipset-specific subcarrier counts to match ESP32 format (e.g., 256 → 128 via decimation or interpolation)
|
||||
- `ProtocolTranslatorService` — Converts `ChipsetCsiFrame` to `Esp32CompatFrame` with ADR-018 binary encoding
|
||||
- `CalibrationService` — Compensates for chipset-specific phase offsets, antenna spacing, and gain differences relative to ESP32 CSI
|
||||
|
||||
**Invariants:**
|
||||
- Bridge assigns virtual `node_id` in range 200-254 (reserved for non-ESP32 sources) to avoid collision with physical ESP32 node IDs (1-199)
|
||||
- Subcarrier resampling preserves frequency ordering (lowest to highest)
|
||||
- Phase values are unwrapped before encoding (continuous, not wrapped to ±π)
|
||||
- Bridge daemon starts only if a compatible patched driver is detected at boot
|
||||
|
||||
---
|
||||
|
||||
### 5. Network Topology Context
|
||||
|
||||
**Responsibility:** Manage network connectivity between ESP32 sensor nodes and TV box appliances, including optional dedicated AP mode and multi-room routing.
|
||||
|
||||
```
|
||||
+------------------------------------------------------------+
|
||||
| Network Topology Context |
|
||||
+------------------------------------------------------------+
|
||||
| |
|
||||
| +----------------+ +----------------+ |
|
||||
| | hostapd | | DHCP Server | |
|
||||
| | (dedicated AP | | (dnsmasq for | |
|
||||
| | for ESP32 | | ESP32 nodes) | |
|
||||
| | mesh) | | | |
|
||||
| +-------+--------+ +-------+--------+ |
|
||||
| | | |
|
||||
| +----------+----------+ |
|
||||
| v |
|
||||
| +-------------------+ |
|
||||
| | Topology Manager | |
|
||||
| | (node discovery, | |
|
||||
| | IP assignment, | |
|
||||
| | route config) | |
|
||||
| +--------+----------+ |
|
||||
| v |
|
||||
| +-------------------+ |
|
||||
| | Firewall Rules | |
|
||||
| | (iptables/nft: | |
|
||||
| | allow UDP 5005, | |
|
||||
| | block external | |
|
||||
| | access to ESP32 | |
|
||||
| | subnet) | |
|
||||
| +-------------------+ |
|
||||
| |
|
||||
+------------------------------------------------------------+
|
||||
```
|
||||
|
||||
**Value Objects:**
|
||||
|
||||
```rust
|
||||
/// Network topology for a single-room deployment.
|
||||
pub struct RoomTopology {
|
||||
/// Appliance acting as the aggregator.
|
||||
pub appliance: DeviceId,
|
||||
/// Whether the appliance runs a dedicated AP.
|
||||
pub dedicated_ap: bool,
|
||||
/// Connected ESP32 nodes with their assigned IPs.
|
||||
pub nodes: Vec<EspNodeConnection>,
|
||||
/// Upstream network interface (Ethernet or WiFi client).
|
||||
pub uplink_interface: String,
|
||||
/// Sensing network interface (dedicated AP or same as uplink).
|
||||
pub sensing_interface: String,
|
||||
}
|
||||
|
||||
/// An ESP32 node's network connection to the appliance.
|
||||
pub struct EspNodeConnection {
|
||||
/// ESP32 node ID (from firmware NVS).
|
||||
pub node_id: u8,
|
||||
/// MAC address of the ESP32.
|
||||
pub mac: [u8; 6],
|
||||
/// Assigned IP address (via DHCP or static).
|
||||
pub ip: IpAddr,
|
||||
/// Last CSI frame received timestamp.
|
||||
pub last_seen: DateTime<Utc>,
|
||||
/// Average CSI frames per second from this node.
|
||||
pub fps: f32,
|
||||
}
|
||||
```
|
||||
|
||||
**Domain Services:**
|
||||
- `DedicatedApService` — Configures `hostapd` to create a WPA2 AP on the TV box's WiFi interface, assigns DHCP range via `dnsmasq`, sets up IP forwarding
|
||||
- `NodeDiscoveryService` — Monitors UDP port 5005 for new ESP32 node IDs, registers them in the topology, alerts on node departure (no frames for >30s)
|
||||
- `FirewallService` — Configures `nftables`/`iptables` to isolate the ESP32 subnet from the upstream LAN, allowing only UDP 5005 inbound and HTTP 3000 outbound
|
||||
|
||||
**Invariants:**
|
||||
- Dedicated AP uses a separate WiFi interface or virtual interface (not the uplink)
|
||||
- ESP32 subnet is isolated from upstream LAN by default (firewall rules)
|
||||
- If dedicated AP is disabled, ESP32 nodes must be on the same LAN subnet as the appliance
|
||||
- Node discovery does not require mDNS or any discovery protocol — ESP32 nodes are configured with the appliance's IP via NVS provisioning (ADR-044)
|
||||
|
||||
---
|
||||
|
||||
## Domain Events
|
||||
|
||||
| Event | Published By | Consumed By | Payload |
|
||||
|-------|-------------|-------------|---------|
|
||||
| `ApplianceProvisioned` | Appliance Mgmt | Fleet Dashboard | `{ device_id, name, hardware_model, ip }` |
|
||||
| `ApplianceOnline` | Appliance Mgmt | Fleet Dashboard | `{ device_id, server_version, uptime }` |
|
||||
| `ApplianceUnreachable` | Appliance Mgmt | Fleet Dashboard, Alerting | `{ device_id, last_seen, reason }` |
|
||||
| `ApplianceDegraded` | Appliance Mgmt | Fleet Dashboard, Alerting | `{ device_id, cpu_temp, reason }` |
|
||||
| `OtaUpdateStarted` | Appliance Mgmt | Fleet Dashboard | `{ device_id, from_version, to_version }` |
|
||||
| `OtaUpdateCompleted` | Appliance Mgmt | Fleet Dashboard | `{ device_id, new_version, duration_secs }` |
|
||||
| `OtaUpdateRolledBack` | Appliance Mgmt | Fleet Dashboard, Alerting | `{ device_id, attempted_version, rollback_version, reason }` |
|
||||
| `BinaryBuilt` | Cross-Compilation | Release Pipeline | `{ target, version, binary_size, checksum }` |
|
||||
| `DeploymentPackageCreated` | Cross-Compilation | Appliance Mgmt | `{ target, version, package_url }` |
|
||||
| `KioskStarted` | Display Kiosk | Appliance Mgmt | `{ device_id, url, resolution }` |
|
||||
| `KioskCrashed` | Display Kiosk | Appliance Mgmt | `{ device_id, exit_code, restart_count }` |
|
||||
| `CsiBridgeStarted` | WiFi CSI Bridge | Appliance Mgmt, Sensing Server | `{ device_id, chipset, virtual_node_id }` |
|
||||
| `CsiBridgeFailed` | WiFi CSI Bridge | Appliance Mgmt | `{ device_id, chipset, error }` |
|
||||
| `EspNodeDiscovered` | Network Topology | Appliance Mgmt | `{ appliance_id, node_id, mac, ip }` |
|
||||
| `EspNodeLost` | Network Topology | Appliance Mgmt, Alerting | `{ appliance_id, node_id, last_seen }` |
|
||||
| `DedicatedApStarted` | Network Topology | Appliance Mgmt | `{ appliance_id, ssid, channel }` |
|
||||
|
||||
---
|
||||
|
||||
## Context Map
|
||||
|
||||
```
|
||||
+-------------------+ +---------------------+
|
||||
| Appliance |--------->| Fleet Dashboard |
|
||||
| Management | events | (external UI for |
|
||||
| (fleet state) | -------> | multi-room mgmt) |
|
||||
+--------+----------+ +---------------------+
|
||||
|
|
||||
| provisions, monitors
|
||||
v
|
||||
+-------------------+ +---------------------+
|
||||
| Cross-Compilation |--------->| GitHub Releases |
|
||||
| (build pipeline) | uploads | (binary artifacts) |
|
||||
+-------------------+ +---------------------+
|
||||
|
|
||||
| provides binary
|
||||
v
|
||||
+-------------------+ +---------------------+
|
||||
| Display Kiosk |--------->| Sensing Server |
|
||||
| (Chromium on | loads | (upstream domain, |
|
||||
| HDMI output) | UI from | produces web UI) |
|
||||
+-------------------+ +----------+----------+
|
||||
^
|
||||
+-------------------+ |
|
||||
| WiFi CSI Bridge |-----UDP 5005------>|
|
||||
| (patched driver) | ESP32 compat |
|
||||
+-------------------+ frames |
|
||||
|
|
||||
+-------------------+ |
|
||||
| Network Topology |-----UDP 5005------>|
|
||||
| (ESP32 mesh | ESP32 frames |
|
||||
| connectivity) | |
|
||||
+-------------------+ |
|
||||
```
|
||||
|
||||
**Relationships:**
|
||||
|
||||
| Upstream | Downstream | Relationship | Mechanism |
|
||||
|----------|-----------|--------------|-----------|
|
||||
| Cross-Compilation | Appliance Mgmt | Supplier-Consumer | Build produces binary; Appliance Mgmt deploys it |
|
||||
| Appliance Mgmt | Display Kiosk | Customer-Supplier | Appliance Mgmt starts kiosk after server is healthy |
|
||||
| WiFi CSI Bridge | Sensing Server (external) | Conformist | Bridge adapts its output to match ESP32 binary protocol (ADR-018) |
|
||||
| Network Topology | Sensing Server (external) | Shared Kernel | Both depend on UDP port 5005 and ESP32 node ID scheme |
|
||||
| Appliance Mgmt | Network Topology | Customer-Supplier | Appliance config determines whether dedicated AP is enabled |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Corruption Layers
|
||||
|
||||
### ESP32 Protocol ACL (CSI Bridge)
|
||||
|
||||
The WiFi CSI Bridge translates chipset-specific CSI formats (Nexmon, rtw88, mt76) into the ESP32 binary protocol (ADR-018). The sensing server never knows whether frames came from a real ESP32 or a TV box WiFi chipset. Virtual node IDs (200-254) prevent collision with physical ESP32 IDs but are otherwise treated identically by the ingestion context.
|
||||
|
||||
### Armbian Platform ACL
|
||||
|
||||
Appliance Management abstracts over Armbian specifics (device tree names, boot configuration, dtb overlays) through the `HardwareModel` value object. Higher-level contexts (Cross-Compilation, Display Kiosk) depend only on the target triple (`aarch64-unknown-linux-gnu`) and systemd service interface, not on Amlogic/Allwinner/Rockchip kernel specifics.
|
||||
|
||||
### Fleet Coordination ACL
|
||||
|
||||
For multi-room deployments, each appliance is self-contained (runs its own sensing server, display, and network). The fleet dashboard reads health beacons but never controls individual appliances directly. OTA updates are pulled by each appliance (not pushed), maintaining the appliance as the authority over its own state.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [ADR-046: Android TV Box / Armbian Deployment](../adr/ADR-046-android-tv-box-armbian-deployment.md) — Primary architectural decision
|
||||
- [ADR-012: ESP32 CSI Sensor Mesh](../adr/ADR-012-esp32-csi-sensor-mesh.md) — ESP32 mesh network design
|
||||
- [ADR-018: Dev Implementation](../adr/ADR-018-dev-implementation.md) — ESP32 binary CSI protocol
|
||||
- [ADR-039: Edge Intelligence](../adr/ADR-039-esp32-edge-intelligence.md) — On-device processing tiers
|
||||
- [ADR-044: Provisioning Tool](../adr/ADR-044-provisioning-tool-enhancements.md) — NVS provisioning for ESP32 nodes
|
||||
- [Hardware Platform Domain Model](hardware-platform-domain-model.md) — Upstream domain (ESP32 hardware)
|
||||
- [Sensing Server Domain Model](sensing-server-domain-model.md) — Upstream domain (server software)
|
||||
@@ -0,0 +1,106 @@
|
||||
# RF Topological Sensing — Research Index
|
||||
|
||||
## SOTA Research Compendium
|
||||
|
||||
**Generated**: 2026-03-08
|
||||
**Total Documents**: 12
|
||||
**Total Lines**: 14,322
|
||||
**Branch**: `claude/rf-mincut-sensing-uHnQX`
|
||||
|
||||
---
|
||||
|
||||
## Core Concept
|
||||
|
||||
RF Topological Sensing treats a room as a dynamic signal graph where ESP32 nodes
|
||||
are vertices and TX-RX links are edges weighted by CSI coherence. Instead of
|
||||
estimating position, minimum cut detects where the RF field topology changes —
|
||||
revealing physical boundaries corresponding to objects, people, and environmental
|
||||
shifts. This creates a "radio nervous system" that is structurally aware of space.
|
||||
|
||||
---
|
||||
|
||||
## Document Index
|
||||
|
||||
### Foundations (Documents 1-2)
|
||||
|
||||
| # | Document | Lines | Key Topics |
|
||||
|---|----------|-------|------------|
|
||||
| 01 | [RF Graph Theory & Mincut Foundations](01-rf-graph-theory-foundations.md) | 1,112 | Max-flow/min-cut theorem, Stoer-Wagner/Karger algorithms, Fiedler vector, Cheeger inequality, spectral graph theory, comparison to classical RF sensing |
|
||||
| 02 | [CSI Edge Weight Computation](02-csi-edge-weight-computation.md) | 1,059 | CSI feature extraction, coherence metrics, MUSIC/ESPRIT multipath decomposition, Kalman filtering of edges, noise robustness, normalization |
|
||||
|
||||
### Machine Learning (Documents 3-4)
|
||||
|
||||
| # | Document | Lines | Key Topics |
|
||||
|---|----------|-------|------------|
|
||||
| 03 | [Attention Mechanisms for RF Sensing](03-attention-mechanisms-rf-sensing.md) | 1,110 | GAT for RF graphs, self-attention for CSI, cross-attention fusion, differentiable mincut, antenna-level attention, efficient attention variants |
|
||||
| 04 | [Transformer Architectures for Graph Sensing](04-transformer-architectures-graph-sensing.md) | 896 | Graphormer/SAN/GPS, temporal graph transformers, ViT for spectrograms, transformer-based mincut prediction, foundation models for RF, edge deployment |
|
||||
|
||||
### Algorithms (Document 5)
|
||||
|
||||
| # | Document | Lines | Key Topics |
|
||||
|---|----------|-------|------------|
|
||||
| 05 | [Sublinear Mincut Algorithms](05-sublinear-mincut-algorithms.md) | 1,170 | Sublinear approximation, dynamic mincut, streaming algorithms, Benczúr-Karger sparsification, local partitioning, Rust implementation |
|
||||
|
||||
### Hardware & Systems (Documents 6, 10)
|
||||
|
||||
| # | Document | Lines | Key Topics |
|
||||
|---|----------|-------|------------|
|
||||
| 06 | [ESP32 Mesh Hardware Constraints](06-esp32-mesh-hardware-constraints.md) | 1,122 | ESP32 CSI capabilities, 16-node topology, TDM synchronization, computational budget, channel hopping, power analysis, firmware architecture |
|
||||
| 10 | [System Architecture & Prototype Design](10-system-architecture-prototype.md) | 1,625 | End-to-end pipeline, crate integration, DDD module design, 100ms latency budget, 3-phase prototype, benchmark design, ADR-044, Rust traits |
|
||||
|
||||
### Learning & Temporal (Documents 7-8)
|
||||
|
||||
| # | Document | Lines | Key Topics |
|
||||
|---|----------|-------|------------|
|
||||
| 07 | [Contrastive Learning for RF Coherence](07-contrastive-learning-rf-coherence.md) | 1,226 | SimCLR/MoCo for CSI, AETHER-Topo extension, delta-driven updates, self-supervised pre-training, triplet edge classification, MERIDIAN transfer |
|
||||
| 08 | [Temporal Graph Evolution & RuVector](08-temporal-graph-evolution-ruvector.md) | 1,528 | TGN/TGAT/DyRep, RuVector graph memory, cut trajectory tracking, event detection, compressed storage, cross-room transitions, drift detection |
|
||||
|
||||
### Analysis (Document 9)
|
||||
|
||||
| # | Document | Lines | Key Topics |
|
||||
|---|----------|-------|------------|
|
||||
| 09 | [Resolution & Spatial Granularity](09-resolution-spatial-granularity.md) | 1,383 | Fresnel zone analysis, node density vs resolution, Cramér-Rao bounds, graph cut resolution theory, multi-frequency enhancement, scaling laws |
|
||||
|
||||
### Quantum Sensing (Documents 11-12)
|
||||
|
||||
| # | Document | Lines | Key Topics |
|
||||
|---|----------|-------|------------|
|
||||
| 11 | [Quantum-Level Sensors](11-quantum-level-sensors.md) | 934 | NV centers, Rydberg atoms, SQUIDs, quantum illumination, quantum graph algorithms, hybrid architecture, quantum ML, NISQ applications |
|
||||
| 12 | [Quantum Biomedical Sensing](12-quantum-biomedical-sensing.md) | 1,157 | Biomagnetic mapping, neural field imaging, circulation sensing, coherence diagnostics, non-contact vitals, ambient health monitoring, BCI |
|
||||
|
||||
---
|
||||
|
||||
## Key Findings
|
||||
|
||||
### Resolution
|
||||
- 16 ESP32 nodes at 1m spacing → **30-60 cm** spatial granularity
|
||||
- Dual-band (2.4 + 5 GHz) → **6 cm** theoretical coherent limit
|
||||
- Information-theoretic limit: **8.8 cm** for dense deployment
|
||||
|
||||
### Computational Feasibility
|
||||
- Stoer-Wagner on 16-node graph: **~2,000 operations** per sweep
|
||||
- At 20 Hz: **0.07%** of one ESP32 core
|
||||
- Full pipeline CSI → mincut: **< 100 ms** latency budget
|
||||
|
||||
### Quantum Enhancement
|
||||
- NV diamond: 100-1000× sensitivity improvement at room temperature
|
||||
- Rydberg atoms: self-calibrated, SI-traceable RF field measurement
|
||||
- D-Wave quantum annealing: native QUBO solver for graph cuts
|
||||
|
||||
### Biomedical Extension
|
||||
- Non-contact cardiac monitoring at 1-3m with quantum sensors
|
||||
- Coherence-based diagnostics: disease as topological change in body's EM graph
|
||||
- Same graph algorithms (mincut, spectral) apply to both room sensing and medical
|
||||
|
||||
---
|
||||
|
||||
## Proposed ADRs
|
||||
- **ADR-044**: RF Topological Sensing (Document 10)
|
||||
- **ADR-045**: Quantum Biomedical Sensing Extension (Document 12)
|
||||
|
||||
## Implementation Phases
|
||||
1. **Phase 1** (4 weeks): 4-node POC — detect person in room
|
||||
2. **Phase 2** (8 weeks): 16-node room — track movement boundaries < 50 cm
|
||||
3. **Phase 3** (16 weeks): Multi-room mesh — cross-room transition detection
|
||||
4. **Phase 4** (2027-2028): Quantum-enhanced — NV diamond + ESP32 hybrid
|
||||
5. **Phase 5** (2029+): Biomedical — coherence diagnostics, ambient health
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,896 @@
|
||||
# Transformer Architectures for RF Topological Graph Sensing
|
||||
|
||||
**Research Document 04** | March 2026
|
||||
**Context**: RuView / wifi-densepose — 16-node ESP32 mesh, CSI coherence-weighted graphs, mincut-based boundary detection, real-time inference requirements.
|
||||
|
||||
---
|
||||
|
||||
## Abstract
|
||||
|
||||
This document surveys transformer architectures applicable to RF topological graph sensing, where a mesh of 16 ESP32 nodes forms a dynamic graph with edges weighted by Channel State Information (CSI) coherence. The primary inference task is mincut prediction — identifying physical boundaries (walls, doors, human bodies) that partition the radio field. We examine graph transformers, temporal graph networks, vision transformers applied to RF spectrograms, transformer-based mincut prediction, positional encoding strategies for RF graphs, foundation model pre-training, and efficient edge deployment. The goal is to identify architectures that can replace or augment combinatorial mincut solvers with learned models capable of real-time inference on resource-constrained hardware.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Graph Transformers](#1-graph-transformers)
|
||||
2. [Temporal Graph Transformers](#2-temporal-graph-transformers)
|
||||
3. [ViT for RF Spectrograms](#3-vit-for-rf-spectrograms)
|
||||
4. [Transformer-Based Mincut Prediction](#4-transformer-based-mincut-prediction)
|
||||
5. [Positional Encoding for RF Graphs](#5-positional-encoding-for-rf-graphs)
|
||||
6. [Foundation Models for RF](#6-foundation-models-for-rf)
|
||||
7. [Efficient Edge Deployment](#7-efficient-edge-deployment)
|
||||
8. [Synthesis and Recommendations](#8-synthesis-and-recommendations)
|
||||
|
||||
---
|
||||
|
||||
## 1. Graph Transformers
|
||||
|
||||
### 1.1 The Structural Gap Between Sequences and Graphs
|
||||
|
||||
Standard transformers operate on sequences where positional encoding captures order. Graphs have no canonical ordering — nodes are permutation-invariant, and structure is encoded in adjacency rather than position. This creates a fundamental tension: the self-attention mechanism in vanilla transformers treats all token pairs equally, ignoring the graph topology that carries critical information in RF sensing.
|
||||
|
||||
For RF topological sensing, graph structure IS the signal. An edge between ESP32 nodes 3 and 7 weighted by CSI coherence of 0.92 means the radio path between them is unobstructed. A weight of 0.31 suggests an intervening boundary. The transformer must respect this structure, not flatten it away.
|
||||
|
||||
### 1.2 Graphormer
|
||||
|
||||
Graphormer (Ying et al., NeurIPS 2021) introduced three structural encodings that inject graph topology into the transformer:
|
||||
|
||||
**Centrality Encoding.** Each node receives a learnable embedding based on its in-degree and out-degree. For an RF mesh, this captures how many strong coherence links a node maintains. Corner nodes in a room typically have lower effective degree (fewer high-coherence links) than central nodes.
|
||||
|
||||
```
|
||||
h_i^(0) = x_i + z_deg+(v_i) + z_deg-(v_i)
|
||||
```
|
||||
|
||||
Where `z_deg+` and `z_deg-` are learnable vectors indexed by degree. In our 16-node mesh, degree ranges from 0 to 15, requiring at most 16 embedding vectors per direction.
|
||||
|
||||
**Spatial Encoding.** The attention bias between nodes i and j depends on their shortest-path distance in the graph. This is added directly to the attention logits:
|
||||
|
||||
```
|
||||
A_ij = (Q_i * K_j) / sqrt(d) + b_SPD(i,j)
|
||||
```
|
||||
|
||||
Where `b_SPD(i,j)` is a learnable scalar indexed by the shortest-path distance. For a 16-node graph, the maximum shortest-path distance is 15 (in a chain), though typical RF meshes have diameter 3-5. This encoding forces the transformer to distinguish between directly connected nodes (1-hop neighbors sharing a line-of-sight path) and distant nodes.
|
||||
|
||||
**Edge Encoding.** Edge features along the shortest path between two nodes are aggregated into the attention bias. For RF graphs, edge features include CSI amplitude, phase coherence, signal-to-noise ratio, and temporal stability. This is particularly powerful because the shortest path between two nodes often traverses intermediate links whose coherence values reveal intervening geometry.
|
||||
|
||||
**Applicability to RF sensing.** Graphormer's all-pairs attention with structural bias is well-suited to our 16-node mesh because N=16 makes O(N^2) attention tractable (256 pairs). The spatial encoding naturally captures the radio topology — nodes separated by many low-coherence hops are likely in different rooms.
|
||||
|
||||
**Limitation.** Graphormer was designed for molecular property prediction with static graphs. RF graphs evolve at 10-100 Hz as people move, doors open, and multipath conditions change. The model needs temporal extension.
|
||||
|
||||
### 1.3 Spectral Attention Network (SAN)
|
||||
|
||||
SAN (Kreuzer et al., NeurIPS 2021) uses the graph Laplacian eigenvectors as positional encodings, then applies full transformer attention. The key insight is that Laplacian eigenvectors provide a canonical coordinate system for graphs analogous to Fourier modes.
|
||||
|
||||
For an RF mesh with adjacency matrix W (CSI coherence weights), the normalized Laplacian is:
|
||||
|
||||
```
|
||||
L = I - D^(-1/2) W D^(-1/2)
|
||||
```
|
||||
|
||||
The eigenvectors of L with the smallest non-zero eigenvalues capture the low-frequency structure of the graph — precisely the large-scale partitions that correspond to room boundaries. The Fiedler vector (eigenvector of the second-smallest eigenvalue) directly encodes the mincut partition.
|
||||
|
||||
SAN computes attention separately over the original graph edges ("sparse attention") and all node pairs ("full attention"), then combines them. This dual mechanism lets the model simultaneously exploit local CSI patterns and global graph structure.
|
||||
|
||||
**RF relevance.** The spectral decomposition of the CSI coherence graph is physically meaningful. Low-frequency eigenvectors correspond to room-level partitions. Mid-frequency eigenvectors capture furniture and body positions. High-frequency eigenvectors encode multipath scattering details. SAN's spectral positional encoding gives the transformer direct access to these physically grounded features.
|
||||
|
||||
### 1.4 General, Powerful, Scalable (GPS) Framework
|
||||
|
||||
GPS (Rampasek et al., NeurIPS 2022) unifies message-passing GNNs and transformers into a single framework. Each layer combines:
|
||||
|
||||
1. A local message-passing step (MPNN) operating on graph neighbors
|
||||
2. A global self-attention step operating on all node pairs
|
||||
3. A positional/structural encoding module
|
||||
|
||||
```
|
||||
h_i^(l+1) = MLP( h_i^(l) + MPNN(h_i^(l), {h_j : j in N(i)}) + Attn(h_i^(l), {h_j : j in V}) )
|
||||
```
|
||||
|
||||
This is particularly relevant for RF sensing because:
|
||||
|
||||
- **Local MPNN** captures immediate CSI relationships (direct link coherence, adjacent-link patterns)
|
||||
- **Global attention** captures long-range dependencies (a person blocking one link affects coherence patterns across the entire mesh)
|
||||
- **Positional encoding** can be chosen from multiple options (Laplacian, random walk, learned)
|
||||
|
||||
For a 16-node mesh, GPS is efficient because both the MPNN (sparse, up to 120 edges for a complete graph) and attention (256 pairs) components are small. The framework's modularity allows systematic ablation of each component's contribution to mincut prediction accuracy.
|
||||
|
||||
### 1.5 TokenGT
|
||||
|
||||
TokenGT (Kim et al., NeurIPS 2022) takes a radical approach: it represents graphs as pure sequences of tokens (node tokens + edge tokens) and applies a standard transformer without any graph-specific attention modifications.
|
||||
|
||||
For each node, TokenGT creates a token from the node features concatenated with a type identifier and orthonormal positional encoding. For each edge, it creates a token from the edge features and the identifiers of its endpoints.
|
||||
|
||||
**Token sequence for a 16-node RF mesh:**
|
||||
- 16 node tokens (each carrying node features: device ID, antenna configuration, noise floor)
|
||||
- Up to 120 edge tokens for a complete graph (each carrying CSI coherence, amplitude, phase, SNR)
|
||||
- Total: up to 136 tokens — well within standard transformer capacity
|
||||
|
||||
The advantage is simplicity: no custom attention mechanisms, no graph-specific modules. The disadvantage is that all structural information must be learned from the positional encodings and edge tokens rather than being architecturally enforced.
|
||||
|
||||
**RF applicability.** TokenGT's approach is attractive for deployment because it uses a vanilla transformer, enabling direct use of optimized inference runtimes (ONNX, TensorRT, CoreML). However, the loss of architectural inductive bias may require more training data to achieve equivalent accuracy.
|
||||
|
||||
### 1.6 Comparative Assessment for RF Topological Sensing
|
||||
|
||||
| Architecture | Structural Bias | Temporal Support | N=16 Complexity | Deployment Simplicity |
|
||||
|-------------|----------------|-----------------|-----------------|----------------------|
|
||||
| Graphormer | Strong (3 encodings) | None (static) | Low (256 pairs) | Moderate |
|
||||
| SAN | Spectral (Laplacian PE) | None (static) | Low | Moderate |
|
||||
| GPS | Hybrid (MPNN + attention) | Extensible | Low | Moderate |
|
||||
| TokenGT | Minimal (learned) | Extensible | Low (136 tokens) | High (vanilla transformer) |
|
||||
|
||||
For the RuView 16-node mesh, all four architectures are computationally feasible. The choice depends on whether we prioritize structural inductive bias (Graphormer, SAN) or deployment simplicity (TokenGT).
|
||||
|
||||
---
|
||||
|
||||
## 2. Temporal Graph Transformers
|
||||
|
||||
### 2.1 The Temporal Dimension of RF Graphs
|
||||
|
||||
RF topological graphs are inherently dynamic. A person walking through a room changes CSI coherence on multiple links simultaneously. A door opening creates a sudden topology change. Breathing modulates coherence at 0.1-0.5 Hz. The temporal evolution of the graph IS the sensing signal.
|
||||
|
||||
Static graph transformers process one snapshot at a time, discarding temporal correlations. Temporal graph transformers explicitly model how graph structure evolves, enabling:
|
||||
|
||||
- Detection of transient events (person crossing a link) vs. persistent changes (furniture rearrangement)
|
||||
- Velocity estimation from the rate of coherence change across sequential links
|
||||
- Prediction of future graph states for proactive sensing
|
||||
|
||||
### 2.2 Temporal Graph Networks (TGN)
|
||||
|
||||
TGN (Rossi et al., ICML 2020 Workshop) maintains a memory state for each node that is updated upon each interaction (edge event). The architecture has four components:
|
||||
|
||||
**Message Function.** When an edge event occurs between nodes i and j at time t (e.g., a CSI coherence measurement), a message is computed:
|
||||
|
||||
```
|
||||
m_i(t) = msg(s_i(t-), s_j(t-), delta_t, e_ij(t))
|
||||
```
|
||||
|
||||
Where `s_i(t-)` is node i's memory before the event, `delta_t` is the time since the last event, and `e_ij(t)` is the edge feature (CSI coherence vector).
|
||||
|
||||
**Memory Updater.** Node memory is updated via a GRU or LSTM:
|
||||
|
||||
```
|
||||
s_i(t) = GRU(s_i(t-), m_i(t))
|
||||
```
|
||||
|
||||
This persistent memory captures the temporal context of each ESP32 node — its recent coherence history, drift patterns, and interaction frequency.
|
||||
|
||||
**Embedding Module.** To compute the embedding for node i at time t, TGN aggregates information from temporal neighbors using attention:
|
||||
|
||||
```
|
||||
z_i(t) = sum_j alpha(s_i, s_j, e_ij, delta_t_ij) * W * s_j(t_j)
|
||||
```
|
||||
|
||||
The attention weights depend on both node memories and the time elapsed since each neighbor's last update.
|
||||
|
||||
**Link Predictor / Graph Classifier.** The embeddings are used for downstream tasks — in our case, predicting which edges will be cut (mincut prediction) or classifying graph topology (room occupancy).
|
||||
|
||||
**RF sensing adaptation.** TGN's event-driven architecture maps naturally to CSI measurements, which arrive as discrete edge events (node i measures coherence to node j). The persistent memory per node captures slow-changing context (room geometry, device calibration drift) while the embedding module captures fast dynamics (person movement).
|
||||
|
||||
For 16 nodes with measurements at 100 Hz across all 120 links, TGN processes approximately 12,000 edge events per second — feasible for the architecture but requiring careful batching.
|
||||
|
||||
### 2.3 Temporal Graph Attention (TGAT)
|
||||
|
||||
TGAT (Xu et al., ICLR 2020) introduces time-aware attention using a functional time encoding inspired by Bochner's theorem:
|
||||
|
||||
```
|
||||
Phi(t) = sqrt(1/d) * [cos(omega_1 * t), sin(omega_1 * t), ..., cos(omega_d * t), sin(omega_d * t)]
|
||||
```
|
||||
|
||||
This continuous-time encoding allows TGAT to handle irregular sampling — critical for RF sensing where different links may be measured at different rates due to the TDM (Time-Division Multiplexing) protocol on the ESP32 mesh.
|
||||
|
||||
The attention mechanism incorporates time explicitly:
|
||||
|
||||
```
|
||||
alpha_ij(t) = softmax( (W_Q * [h_i || Phi(0)]) * (W_K * [h_j || Phi(t - t_j)])^T )
|
||||
```
|
||||
|
||||
Where `t - t_j` is the time elapsed since node j's last measurement. Links measured more recently receive higher attention weight, naturally handling the staleness problem in TDM scheduling.
|
||||
|
||||
**RF sensing advantage.** The ESP32 TDM protocol means each node pair is measured at different times within the measurement cycle. TGAT's continuous time encoding elegantly handles this non-uniform sampling without requiring interpolation or resampling.
|
||||
|
||||
### 2.4 DyRep: Learning Representations over Dynamic Graphs
|
||||
|
||||
DyRep (Trivedi et al., ICLR 2019) models graph dynamics as a temporal point process, learning when edges will change (not just how). The intensity function for an edge event between nodes i and j is:
|
||||
|
||||
```
|
||||
lambda_ij(t) = f(z_i(t), z_j(t), t - t_last)
|
||||
```
|
||||
|
||||
Where `z_i(t)` is node i's representation at time t and `t_last` is the time of the last event on this edge.
|
||||
|
||||
For RF sensing, DyRep's point process formulation captures the physics:
|
||||
- A person walking toward a link increases the event intensity (coherence will change)
|
||||
- A static environment has low event intensity (coherence is stable)
|
||||
- The rate of change carries information about movement speed and direction
|
||||
|
||||
DyRep maintains two propagation mechanisms:
|
||||
1. **Localized** (association): immediate neighbor updates when a link changes
|
||||
2. **Global** (communication): attention-based updates across the entire graph
|
||||
|
||||
This dual propagation mirrors the RF sensing reality: a person blocking one link directly affects adjacent links (localized) while also changing the global multipath environment (communication).
|
||||
|
||||
### 2.5 Adapting Temporal Graph Transformers for RF Sensing
|
||||
|
||||
The key adaptation required for RF topological sensing is bridging the gap between the edge-event paradigm of TGN/TGAT/DyRep and the periodic measurement paradigm of the ESP32 mesh.
|
||||
|
||||
**Measurement-as-event mapping.** Each CSI measurement on link (i,j) at time t generates an edge event with features:
|
||||
- CSI amplitude vector (56 subcarriers after sparse interpolation)
|
||||
- Phase coherence score
|
||||
- Signal-to-noise ratio
|
||||
- Doppler shift estimate
|
||||
- Coherence change magnitude from previous measurement
|
||||
|
||||
**Temporal batching.** Rather than processing events one at a time, batch all measurements from a single TDM cycle (approximately 10ms for 16 nodes) and process them as a temporal graph snapshot. This trades strict event ordering for computational efficiency.
|
||||
|
||||
**Hybrid architecture recommendation.** Combine TGN's persistent per-node memory with TGAT's continuous time encoding:
|
||||
- Node memory captures slow context (room geometry, calibration)
|
||||
- Time encoding handles irregular TDM sampling
|
||||
- Graph attention operates on the current snapshot with temporal features
|
||||
- Mincut prediction head outputs partition probabilities
|
||||
|
||||
---
|
||||
|
||||
## 3. ViT for RF Spectrograms
|
||||
|
||||
### 3.1 CSI-to-Spectrogram Conversion
|
||||
|
||||
Channel State Information from a single link is a time series of complex-valued vectors (one complex value per OFDM subcarrier). This naturally maps to a 2D representation:
|
||||
|
||||
**Time-Frequency Spectrogram.** For each link (i,j):
|
||||
- X-axis: time (measurement index)
|
||||
- Y-axis: subcarrier index (frequency)
|
||||
- Value: CSI amplitude or phase
|
||||
- Dimensions: T timesteps x 56 subcarriers (after sparse interpolation from 114)
|
||||
|
||||
**Doppler Spectrogram.** Apply short-time Fourier transform along the time axis for each subcarrier:
|
||||
- X-axis: time window center
|
||||
- Y-axis: Doppler frequency
|
||||
- Value: spectral power
|
||||
- This reveals movement velocities — human walking produces 2-6 Hz Doppler, breathing 0.1-0.5 Hz
|
||||
|
||||
**Cross-Link Spectrogram.** Stack spectrograms from multiple links:
|
||||
- For all 120 links in a 16-node complete graph: a 120 x 56 x T tensor
|
||||
- Or reshape to a 2D image: (120*56) x T = 6720 x T
|
||||
|
||||
### 3.2 Vision Transformer Architecture for RF
|
||||
|
||||
ViT (Dosovitskiy et al., ICLR 2021) divides an image into fixed-size patches and processes them as a sequence of tokens. For RF spectrograms:
|
||||
|
||||
**Patch extraction.** A spectrogram of dimensions H x W (e.g., 56 subcarriers x 128 timesteps) is divided into patches of size P x P:
|
||||
- P = 8: yields (56/8) x (128/8) = 7 x 16 = 112 patches
|
||||
- Each patch captures a local time-frequency region
|
||||
|
||||
**Patch embedding.** Each P x P patch is flattened and linearly projected to the transformer dimension d:
|
||||
|
||||
```
|
||||
z_patch = W_embed * flatten(patch) + b_embed
|
||||
```
|
||||
|
||||
**Positional encoding.** Learned 2D positional embeddings encode both the frequency position (which subcarriers) and temporal position (which time window) of each patch.
|
||||
|
||||
**Transformer encoder.** Standard multi-head self-attention and feed-forward layers process the sequence of patch tokens.
|
||||
|
||||
**Classification head.** For mincut prediction, the [CLS] token output is projected to predict which edges belong to the cut set.
|
||||
|
||||
### 3.3 Multi-Link ViT
|
||||
|
||||
A single link's spectrogram provides limited spatial information. To capture the full RF topology, we need to process spectrograms from all links jointly.
|
||||
|
||||
**Approach 1: Channel stacking.** Treat each link's spectrogram as a separate channel of a multi-channel image. With 120 links and 56 subcarriers over 128 timesteps, this creates a 120-channel 56x128 image. Patch extraction operates across all channels simultaneously.
|
||||
|
||||
**Approach 2: Token concatenation.** Process each link's spectrogram independently through shared patch extraction and embedding, then concatenate all link tokens into a single sequence. With 112 patches per link and 120 links, this yields 13,440 tokens — too many for standard attention.
|
||||
|
||||
**Approach 3: Hierarchical ViT.** Two-stage processing:
|
||||
1. **Link-level ViT**: Process each link's spectrogram independently (shared weights), producing one embedding per link (120 embeddings)
|
||||
2. **Graph-level transformer**: Process the 120 link embeddings with graph-aware attention (using the RF topology as structural bias)
|
||||
|
||||
This hierarchical approach is the most promising because:
|
||||
- The link-level ViT captures local time-frequency patterns (Doppler signatures, phase variations)
|
||||
- The graph-level transformer captures spatial relationships between links
|
||||
- Total token count stays manageable (112 for link-level, 120 for graph-level)
|
||||
|
||||
### 3.4 ViT Variants for RF
|
||||
|
||||
**DeiT (Data-efficient Image Transformers).** Uses knowledge distillation from a CNN teacher, relevant when training data is limited — a common constraint in RF sensing where labeled datasets require manual annotation of room layouts and occupancy.
|
||||
|
||||
**Swin Transformer.** Hierarchical ViT with shifted windows, reducing attention complexity from O(N^2) to O(N). For large spectrograms, Swin's local attention windows align with the locality of time-frequency patterns.
|
||||
|
||||
**CvT (Convolutional Vision Transformer).** Replaces linear patch embedding with convolutional tokenization, providing translation equivariance. This is beneficial for Doppler spectrograms where the same movement pattern can appear at different time offsets.
|
||||
|
||||
### 3.5 Limitations and Trade-offs
|
||||
|
||||
The spectrogram/ViT approach has significant limitations for RF topological sensing:
|
||||
|
||||
1. **Loss of graph structure.** Converting CSI to spectrograms discards the explicit graph topology. The spatial relationship between links must be re-learned from data.
|
||||
|
||||
2. **Fixed temporal window.** ViT processes a fixed-size spectrogram, requiring a choice of temporal window. Too short misses slow events; too long blurs fast events.
|
||||
|
||||
3. **Redundant computation.** In a 16-node mesh, many link spectrograms share similar information due to spatial correlation. A graph-native approach avoids this redundancy.
|
||||
|
||||
4. **Complementary value.** Despite these limitations, ViT excels at extracting micro-Doppler signatures and time-frequency patterns that graph transformers may miss. The recommended approach uses ViT as a feature extractor feeding into a graph transformer, combining the strengths of both paradigms.
|
||||
|
||||
---
|
||||
|
||||
## 4. Transformer-Based Mincut Prediction
|
||||
|
||||
### 4.1 Problem Formulation
|
||||
|
||||
Given a weighted graph G = (V, E, w) where V is 16 ESP32 nodes, E is up to 120 edges, and w: E -> R+ is CSI coherence, the mincut problem is to find a partition (S, V\S) minimizing:
|
||||
|
||||
```
|
||||
cut(S, V\S) = sum_{(i,j) in E: i in S, j in V\S} w(i,j)
|
||||
```
|
||||
|
||||
The exact solution requires O(V^3) max-flow computation (e.g., push-relabel) or O(V * E) augmenting paths. For N=16 and E=120, exact computation takes microseconds — so why use a learned model?
|
||||
|
||||
**Reasons for learned mincut prediction:**
|
||||
1. **Temporal smoothing.** Exact mincut on noisy CSI measurements is unstable. A learned model can produce temporally smooth partitions.
|
||||
2. **Multi-scale partitioning.** The 2nd, 3rd, ..., kth eigenvectors of the Laplacian encode hierarchical partitions. A transformer can learn to output multi-scale partitions jointly.
|
||||
3. **Semantic enrichment.** Beyond minimum cut value, a learned model can predict what caused the partition (person, wall, furniture) based on CSI signatures.
|
||||
4. **Amortized inference.** For real-time deployment at 100 Hz, a single forward pass through a small transformer may be faster than repeated exact computation, especially when targeting k-way partitions.
|
||||
5. **Differentiable pipeline.** A learned mincut module can be trained end-to-end with downstream tasks (pose estimation, occupancy detection) through gradient flow.
|
||||
|
||||
### 4.2 MinCutPool as a Foundation
|
||||
|
||||
MinCutPool (Bianchi et al., ICML 2020) formulates graph pooling as a continuous relaxation of the mincut problem. The assignment matrix S is learned:
|
||||
|
||||
```
|
||||
S = softmax(GNN(X, A))
|
||||
```
|
||||
|
||||
Where S[i,k] is the probability that node i belongs to cluster k. The loss function is:
|
||||
|
||||
```
|
||||
L_mincut = -Tr(S^T A S) / Tr(S^T D S) + ||S^T S / ||S^T S||_F - I/sqrt(K)||_F
|
||||
```
|
||||
|
||||
The first term minimizes normalized cut. The second term encourages balanced partitions (orthogonality regularization).
|
||||
|
||||
**Transformer adaptation.** Replace the GNN in MinCutPool with a graph transformer:
|
||||
|
||||
```
|
||||
S = softmax(GraphTransformer(X, A))
|
||||
```
|
||||
|
||||
This leverages the transformer's global attention to capture long-range dependencies in the RF topology that local GNN message passing may miss.
|
||||
|
||||
### 4.3 Architecture: MinCut Transformer
|
||||
|
||||
We propose a MinCut Transformer architecture for RF topological sensing:
|
||||
|
||||
**Input representation.** For each node i:
|
||||
- Node features: device configuration, noise floor, antenna pattern (d_node = 32)
|
||||
- For each edge (i,j): CSI coherence vector, amplitude statistics, temporal gradient (d_edge = 64)
|
||||
|
||||
**Encoder.** GPS-style graph transformer with L=4 layers:
|
||||
- Local MPNN: 2-layer GCN on the CSI coherence graph
|
||||
- Global attention: multi-head attention with Graphormer-style spatial encoding
|
||||
- Hidden dimension: d = 128
|
||||
- Heads: 8
|
||||
|
||||
**Mincut prediction head.** Two output branches:
|
||||
|
||||
Branch 1 — **Partition assignment**:
|
||||
```
|
||||
S = softmax(MLP(h_nodes)) [16 x K matrix for K-way partition]
|
||||
```
|
||||
|
||||
Branch 2 — **Cut edge prediction**:
|
||||
```
|
||||
p_cut(i,j) = sigmoid(MLP([h_i || h_j || e_ij])) [probability that edge (i,j) is cut]
|
||||
```
|
||||
|
||||
**Training objective.** Multi-task loss combining:
|
||||
1. MinCutPool loss (continuous relaxation of normalized cut)
|
||||
2. Binary cross-entropy on cut edge prediction (supervised, from exact mincut labels)
|
||||
3. Temporal consistency loss (penalize rapid partition changes between adjacent frames)
|
||||
4. Spectral loss (predicted partition should align with Fiedler vector)
|
||||
|
||||
### 4.4 Spectral Supervision
|
||||
|
||||
A key insight is that the Fiedler vector of the CSI coherence Laplacian provides a strong supervisory signal:
|
||||
|
||||
```
|
||||
L = D - W
|
||||
Lv_2 = lambda_2 * v_2
|
||||
```
|
||||
|
||||
The sign of v_2 directly encodes the optimal 2-way partition. Training the transformer to predict v_2 (and higher eigenvectors for k-way partitions) provides:
|
||||
- Dense supervision (every node gets a continuous target, not just a binary label)
|
||||
- Multi-scale targets (each eigenvector encodes a different partition granularity)
|
||||
- Physically grounded learning (eigenvectors correspond to room modes of the RF field)
|
||||
|
||||
### 4.5 Comparison: Exact vs. Learned Mincut
|
||||
|
||||
| Property | Exact (Push-Relabel) | Learned (MinCut Transformer) |
|
||||
|----------|---------------------|------------------------------|
|
||||
| Accuracy | Optimal | Near-optimal (after training) |
|
||||
| Latency (N=16) | ~5 us | ~50 us (forward pass) |
|
||||
| Temporal smoothness | None (per-frame) | Built-in (temporal loss) |
|
||||
| Multi-scale output | Requires k runs | Single forward pass |
|
||||
| Semantic labels | None | Learnable |
|
||||
| Differentiable | No | Yes |
|
||||
| Noise robustness | Sensitive | Robust (learned denoising) |
|
||||
|
||||
For N=16, exact computation is fast enough for real-time use. The value of the learned approach lies in temporal smoothness, multi-scale output, and end-to-end differentiability rather than raw speed.
|
||||
|
||||
---
|
||||
|
||||
## 5. Positional Encoding for RF Graphs
|
||||
|
||||
### 5.1 Why Positional Encoding Matters
|
||||
|
||||
Graph transformers without positional encoding treat graphs as sets of nodes, ignoring topology. For RF sensing, topology IS the primary information carrier. Positional encoding injects structural information that enables the transformer to reason about spatial relationships, path connectivity, and partition structure.
|
||||
|
||||
### 5.2 Laplacian Eigenvector Positional Encoding (LapPE)
|
||||
|
||||
The eigenvectors of the graph Laplacian L provide a spectral coordinate system:
|
||||
|
||||
```
|
||||
L = U * Lambda * U^T
|
||||
PE_i = [u_1(i), u_2(i), ..., u_k(i)]
|
||||
```
|
||||
|
||||
Where u_j(i) is the i-th component of the j-th eigenvector.
|
||||
|
||||
**Sign ambiguity.** Eigenvectors are defined up to sign flip: if v is an eigenvector, so is -v. This creates a 2^k ambiguity for k eigenvectors. Solutions:
|
||||
- **SignNet** (Lim et al., ICML 2022): learn a sign-invariant function phi(|v|) + phi(-|v|)
|
||||
- **BasisNet**: learn in the span of eigenvectors rather than individual vectors
|
||||
- **Random sign augmentation**: flip signs randomly during training
|
||||
|
||||
**RF-specific considerations.** For the CSI coherence graph:
|
||||
- The first eigenvector (constant) is uninformative
|
||||
- The Fiedler vector (2nd eigenvector) directly encodes the primary room partition
|
||||
- Eigenvectors 3-5 encode secondary partitions (sub-rooms, corridors)
|
||||
- Higher eigenvectors encode local structure (furniture, body positions)
|
||||
- Using k=8 eigenvectors captures the practically relevant structural scales for a 16-node mesh
|
||||
|
||||
**Computational cost.** Eigendecomposition of a 16x16 matrix is negligible (microseconds). For larger meshes, only the bottom-k eigenvectors are needed, computable via Lanczos iteration in O(k * |E|) time.
|
||||
|
||||
### 5.3 Random Walk Positional Encoding (RWPE)
|
||||
|
||||
RWPE (Dwivedi et al., JMLR 2023) uses the diagonal of random walk powers as node features:
|
||||
|
||||
```
|
||||
PE_i = [RW_ii^1, RW_ii^2, ..., RW_ii^k]
|
||||
```
|
||||
|
||||
Where RW = D^(-1)A is the random walk matrix and RW_ii^t is the probability of returning to node i after t random walk steps.
|
||||
|
||||
**Physical interpretation for RF.** In the CSI coherence graph:
|
||||
- RW_ii^1 = 0 always (no self-loops in measurement graph)
|
||||
- RW_ii^2 captures local connectivity density (high return probability means node i is in a tightly connected cluster, i.e., a single room)
|
||||
- RW_ii^t for large t captures global graph structure (convergence rate relates to spectral gap, which relates to how well-separated the rooms are)
|
||||
|
||||
**Advantages over LapPE:**
|
||||
- No sign ambiguity (diagonal elements are always positive)
|
||||
- Computationally cheaper (matrix powers vs. eigendecomposition)
|
||||
- Naturally multi-scale (different powers capture different structural scales)
|
||||
|
||||
**For 16-node RF mesh:** Use k=16 random walk steps (powers 1 through 16). The return probabilities form a characteristic "fingerprint" for each node's position in the radio topology.
|
||||
|
||||
### 5.4 Spatial Encoding (Physical Coordinates)
|
||||
|
||||
Unlike many graph learning problems, RF mesh nodes have known physical positions (or positions estimable from CSI). This enables spatial positional encoding:
|
||||
|
||||
**Direct coordinate encoding.** If ESP32 nodes have known (x, y, z) coordinates:
|
||||
```
|
||||
PE_i = MLP([x_i, y_i, z_i])
|
||||
```
|
||||
|
||||
**Pairwise distance encoding.** For attention between nodes i and j:
|
||||
```
|
||||
bias_ij = MLP(||pos_i - pos_j||_2)
|
||||
```
|
||||
|
||||
This injects physical distance into the attention mechanism. Two nodes 1 meter apart with low CSI coherence (suggesting an intervening wall) produce a different attention pattern than two nodes 10 meters apart with the same low coherence (expected signal attenuation).
|
||||
|
||||
**Combined encoding.** The most powerful approach combines spectral (LapPE) and spatial (coordinate) encodings:
|
||||
```
|
||||
PE_i = concat(LapPE_i, RWPE_i, MLP([x_i, y_i, z_i]))
|
||||
```
|
||||
|
||||
This gives the transformer access to both the topological structure (from spectral encoding) and the physical layout (from spatial encoding).
|
||||
|
||||
### 5.5 Relative Positional Encoding
|
||||
|
||||
Rather than absolute node positions, relative encodings capture pairwise relationships:
|
||||
|
||||
**Graphormer's edge encoding along shortest paths:**
|
||||
```
|
||||
b_ij = mean(w_e : e in shortest_path(i, j))
|
||||
```
|
||||
|
||||
For RF graphs, the shortest path in the coherence graph between two distant nodes reveals the "radio corridor" connecting them — the sequence of high-coherence links that radio signals can traverse.
|
||||
|
||||
**Rotary Position Embedding (RoPE) for graphs.** Adapt RoPE from language models by using spectral coordinates:
|
||||
```
|
||||
RoPE(q, k, theta) where theta is derived from Laplacian eigenvector differences
|
||||
```
|
||||
|
||||
This injects relative spectral position into the attention mechanism without modifying the attention computation, maintaining compatibility with efficient attention implementations.
|
||||
|
||||
### 5.6 Encoding Comparison for RF Sensing
|
||||
|
||||
| Encoding | Sign Invariant | Multi-scale | Physical Grounding | Computational Cost |
|
||||
|----------|---------------|-------------|-------------------|-------------------|
|
||||
| LapPE | No (needs SignNet) | Yes (eigenvector index) | Strong (spectral = partition) | O(N^3) eigendecomp |
|
||||
| RWPE | Yes | Yes (walk length) | Moderate | O(k * N^2) mat-mul |
|
||||
| Spatial | N/A | No | Direct (coordinates) | O(N) lookup |
|
||||
| Combined | Configurable | Yes | Strong | Sum of components |
|
||||
|
||||
**Recommendation for RuView:** Use combined encoding (LapPE with SignNet + RWPE + spatial coordinates). The 16-node mesh makes computational cost irrelevant, and the combined encoding provides the richest structural information for mincut prediction.
|
||||
|
||||
---
|
||||
|
||||
## 6. Foundation Models for RF
|
||||
|
||||
### 6.1 The Case for RF Foundation Models
|
||||
|
||||
Current RF sensing models are trained from scratch for each environment, task, and hardware configuration. A foundation model pre-trained on diverse RF environments could:
|
||||
|
||||
1. **Transfer across environments.** A model pre-trained on 1000 rooms transfers to a new room with minimal fine-tuning.
|
||||
2. **Transfer across tasks.** Pre-train on self-supervised RF features, fine-tune for specific tasks (mincut, pose estimation, occupancy counting).
|
||||
3. **Transfer across hardware.** Pre-train on diverse antenna configurations, adapt to specific ESP32 deployments.
|
||||
4. **Reduce labeling requirements.** Self-supervised pre-training uses unlabeled CSI data (abundant), with only task-specific fine-tuning requiring labels (scarce).
|
||||
|
||||
### 6.2 Pre-training Objectives
|
||||
|
||||
**Masked CSI Modeling (MCM).** Analogous to masked language modeling in BERT:
|
||||
- Randomly mask 15% of CSI subcarrier values across links
|
||||
- Train the transformer to predict masked values from unmasked context
|
||||
- This forces the model to learn CSI correlation structure across links, subcarriers, and time
|
||||
|
||||
**Contrastive Link Prediction.** For each pair of links:
|
||||
- Positive pairs: links that share a node or are in the same room
|
||||
- Negative pairs: links in different rooms or with low coherence correlation
|
||||
- Contrastive loss pushes similar links together in embedding space
|
||||
- This is related to the AETHER contrastive embedding framework (ADR-024)
|
||||
|
||||
**Graph-Level Contrastive Learning.** Augment graphs by:
|
||||
- Dropping edges below a coherence threshold
|
||||
- Adding Gaussian noise to edge weights
|
||||
- Subgraph sampling
|
||||
- Temporal shifting (comparing t and t+delta)
|
||||
- Train the model to produce similar embeddings for augmented versions of the same graph
|
||||
|
||||
**Temporal Prediction.** Given CSI graphs at times t-k, ..., t-1, t, predict the graph at time t+1:
|
||||
- Edge weight prediction (CSI coherence at next timestep)
|
||||
- Topology prediction (which edges will appear/disappear)
|
||||
- This forces the model to learn physical dynamics of RF propagation
|
||||
|
||||
**Spectral Prediction.** Predict Laplacian eigenvalues from node/edge features:
|
||||
- The eigenvalue spectrum encodes global graph properties (connectivity, partition quality)
|
||||
- This objective directly trains the model for partition-related downstream tasks
|
||||
|
||||
### 6.3 Architecture for RF Foundation Model
|
||||
|
||||
**Input tokenization.** Each CSI measurement frame consists of:
|
||||
- 16 nodes with device features
|
||||
- Up to 120 edges with CSI feature vectors
|
||||
- Temporal context window of W frames
|
||||
|
||||
**Encoder.** GPS-style graph transformer:
|
||||
- 12 layers, 512 hidden dimensions, 8 attention heads
|
||||
- LapPE + RWPE + spatial positional encoding
|
||||
- Per-node memory (TGN-style) for temporal context
|
||||
- Estimated parameters: approximately 25M
|
||||
|
||||
**Pre-training data requirements.** For effective pre-training:
|
||||
- Minimum 100 diverse environments (rooms, corridors, open spaces, multi-room apartments)
|
||||
- Minimum 1000 hours of CSI data per environment
|
||||
- Diverse conditions: empty rooms, 1-5 occupants, various furniture configurations
|
||||
- Multiple hardware configurations (antenna counts, node densities, frequencies)
|
||||
|
||||
**Data sources.** Combination of:
|
||||
- Real CSI data from deployed ESP32 meshes (highest quality, limited quantity)
|
||||
- Simulated CSI using ray-tracing (unlimited quantity, limited fidelity)
|
||||
- Hybrid: real data augmented with simulated variations
|
||||
|
||||
### 6.4 Fine-tuning Strategies
|
||||
|
||||
**Linear probing.** Freeze the pre-trained encoder, train only a linear classification head. Tests whether pre-trained representations already encode task-relevant information. For mincut prediction, linear probing on the Fiedler vector prediction provides a diagnostic.
|
||||
|
||||
**Low-rank adaptation (LoRA).** Add low-rank update matrices to attention weights:
|
||||
```
|
||||
W' = W + alpha * BA
|
||||
```
|
||||
Where B is d x r and A is r x d with r << d. This enables task-specific adaptation with minimal additional parameters (typically r=4-16).
|
||||
|
||||
**Full fine-tuning.** Update all parameters on task-specific data. Most expressive but requires more labeled data and risks catastrophic forgetting.
|
||||
|
||||
**Prompt tuning.** Prepend learnable "prompt" tokens to the input sequence that steer the pre-trained model toward the desired task. For RF sensing, prompts could encode the environment type (residential, commercial, industrial) or task specification (2-way cut, k-way cut, occupancy count).
|
||||
|
||||
### 6.5 Cross-Environment Generalization
|
||||
|
||||
A critical challenge for RF foundation models is domain shift between environments. The MERIDIAN framework (ADR-027) addresses this through:
|
||||
|
||||
1. **Environment fingerprinting.** Learn a compact representation of each environment's RF characteristics (room dimensions, material properties, multipath richness).
|
||||
2. **Domain-invariant features.** Train the encoder to produce representations that are invariant to environment-specific characteristics while preserving task-relevant information.
|
||||
3. **Few-shot adaptation.** Given 5-10 minutes of data in a new environment, adapt the model to the new domain using meta-learning techniques.
|
||||
|
||||
The foundation model's pre-training across diverse environments naturally supports MERIDIAN-style generalization by exposing the model to the full distribution of RF environments during pre-training.
|
||||
|
||||
### 6.6 Scaling Laws
|
||||
|
||||
Based on analogies to language and vision foundation models, expected scaling behavior for RF foundation models:
|
||||
|
||||
| Model Size | Parameters | Pre-training Data | Expected Mincut F1 (zero-shot) |
|
||||
|-----------|-----------|-------------------|-------------------------------|
|
||||
| Tiny | 1M | 100 hours | 0.60 |
|
||||
| Small | 10M | 1K hours | 0.72 |
|
||||
| Base | 25M | 10K hours | 0.80 |
|
||||
| Large | 100M | 100K hours | 0.86 |
|
||||
|
||||
These are rough estimates. The key question is whether RF sensing exhibits the same favorable scaling behavior as language and vision. The lower dimensionality of RF data (16 nodes, 120 edges, 56 subcarriers) compared to images (millions of pixels) or text (50K+ vocabulary) suggests that smaller models may suffice.
|
||||
|
||||
---
|
||||
|
||||
## 7. Efficient Edge Deployment
|
||||
|
||||
### 7.1 Deployment Constraints
|
||||
|
||||
The ESP32 mesh operates under severe resource constraints:
|
||||
|
||||
| Resource | ESP32 | ESP32-S3 | Target Budget |
|
||||
|----------|-------|----------|--------------|
|
||||
| RAM | 520 KB | 512 KB + 8MB PSRAM | <2 MB model |
|
||||
| Flash | 4 MB | 16 MB | <4 MB model |
|
||||
| Clock | 240 MHz | 240 MHz | <10ms inference |
|
||||
| FPU | Single-precision | Single-precision | FP32 or INT8 |
|
||||
| SIMD | None | PIE (128-bit) | Use where available |
|
||||
|
||||
Real-time inference at 100 Hz requires completing a forward pass in under 10ms. For on-device inference, this is extremely challenging. The practical deployment model is:
|
||||
|
||||
1. **Edge aggregator** (ESP32-S3 with PSRAM): runs the inference model
|
||||
2. **Sensor nodes** (ESP32): collect CSI and transmit to aggregator
|
||||
3. **Optional cloud fallback**: for complex models exceeding edge capacity
|
||||
|
||||
### 7.2 Knowledge Distillation
|
||||
|
||||
Train a small "student" model to mimic a large "teacher" model:
|
||||
|
||||
**Teacher.** Full-size graph transformer (GPS, 4 layers, d=128, approximately 2M parameters):
|
||||
- Trained on labeled CSI data with exact mincut targets
|
||||
- Achieves best accuracy but too large for edge deployment
|
||||
|
||||
**Student.** Tiny graph network (2 layers, d=32, approximately 50K parameters):
|
||||
- Trained to minimize KL divergence between its output distribution and the teacher's:
|
||||
```
|
||||
L_distill = alpha * KL(p_student || p_teacher) + (1-alpha) * L_task
|
||||
```
|
||||
- Temperature scaling softens the teacher's predictions, exposing inter-class relationships
|
||||
|
||||
**Distillation strategies for RF sensing:**
|
||||
|
||||
1. **Output distillation.** Student mimics teacher's mincut partition probabilities.
|
||||
2. **Feature distillation.** Student's intermediate representations match teacher's (after projection):
|
||||
```
|
||||
L_feature = ||proj(h_student^l) - h_teacher^l||_2
|
||||
```
|
||||
3. **Attention distillation.** Student's attention patterns match teacher's:
|
||||
```
|
||||
L_attention = KL(A_student || A_teacher)
|
||||
```
|
||||
This is particularly valuable because the teacher's attention patterns encode which node pairs are most informative for the partition decision.
|
||||
|
||||
4. **Spectral distillation.** Student matches teacher's predicted Laplacian eigenvalues. This is a compact, information-dense target that encodes the entire partition structure.
|
||||
|
||||
### 7.3 Quantization
|
||||
|
||||
**Post-Training Quantization (PTQ).** Convert FP32 weights and activations to INT8 after training:
|
||||
- Weight quantization: symmetric per-channel quantization for linear layers
|
||||
- Activation quantization: asymmetric per-tensor with calibration data
|
||||
- Expected accuracy loss: 1-3% on mincut F1
|
||||
- Model size reduction: 4x (FP32 to INT8)
|
||||
- Inference speedup: 2-4x on INT8-capable hardware
|
||||
|
||||
**Quantization-Aware Training (QAT).** Simulate quantization during training using straight-through estimators:
|
||||
- Fake-quantize weights and activations during forward pass
|
||||
- Backpropagate through the quantization operation using straight-through gradient
|
||||
- Expected accuracy loss: <1% on mincut F1
|
||||
- Same size/speed benefits as PTQ
|
||||
|
||||
**Mixed-Precision Quantization.** Different layers tolerate different quantization levels:
|
||||
- Attention QK computation: sensitive, keep FP16
|
||||
- Attention values and FFN: tolerant, use INT8
|
||||
- Positional encodings: very sensitive, keep FP32
|
||||
- Output projection: tolerant, use INT8
|
||||
|
||||
For the ESP32-S3, the optimal strategy is INT8 quantization with FP32 positional encodings, yielding approximately 100KB model size for a 2-layer, d=32 student network.
|
||||
|
||||
### 7.4 Pruning
|
||||
|
||||
**Structured Pruning.** Remove entire attention heads or FFN neurons:
|
||||
- Score each head by its average attention entropy (low entropy = specialized = important)
|
||||
- Remove heads with highest entropy (most diffuse attention)
|
||||
- For a 2-layer, 4-head model: pruning to 2 heads per layer halves attention computation
|
||||
|
||||
**Unstructured Pruning.** Zero out individual weights:
|
||||
- Magnitude pruning: remove weights with smallest absolute value
|
||||
- 80% sparsity achievable with minimal accuracy loss for graph transformers
|
||||
- Requires sparse matrix support for inference speedup (not available on ESP32)
|
||||
|
||||
**Token Pruning.** For ViT-based approaches, remove uninformative patches:
|
||||
- Score each patch token by its attention received from the [CLS] token
|
||||
- Remove bottom 50% of patches after the first transformer layer
|
||||
- Reduces computation by approximately 2x in subsequent layers
|
||||
|
||||
**Structured pruning is recommended** for ESP32 deployment because it reduces model size and computation without requiring sparse matrix hardware support.
|
||||
|
||||
### 7.5 Architecture-Level Efficiency
|
||||
|
||||
Beyond compression, architectural choices dramatically affect edge efficiency:
|
||||
|
||||
**Efficient attention variants:**
|
||||
- **Linear attention** (Katharopoulos et al., ICML 2020): replaces softmax attention with kernel-based approximation, reducing O(N^2) to O(N). For N=16, the savings are minimal, but it eliminates the softmax computation.
|
||||
- **Performer** (Choromanski et al., ICLR 2021): random feature approximation of softmax attention. Similar linear complexity.
|
||||
- For N=16 nodes, standard quadratic attention (256 operations) is already fast enough. Efficient variants matter only for the ViT spectrogram path with many patches.
|
||||
|
||||
**Lightweight feed-forward networks:**
|
||||
- Replace standard 4d FFN with depthwise separable convolutions
|
||||
- Use GLU (Gated Linear Unit) activation instead of GELU to reduce hidden dimension
|
||||
|
||||
**Weight sharing:**
|
||||
- Share weights across transformer layers (ALBERT-style)
|
||||
- For a 2-layer model, this halves the parameter count
|
||||
- Accuracy loss is minimal when combined with distillation
|
||||
|
||||
### 7.6 Deployment Pipeline
|
||||
|
||||
The recommended deployment pipeline for RuView:
|
||||
|
||||
```
|
||||
1. Train large teacher model (GPU server)
|
||||
- GPS graph transformer, 4 layers, d=128
|
||||
- Full precision, all data augmentation
|
||||
- Target: best possible accuracy
|
||||
|
||||
2. Distill to student model (GPU server)
|
||||
- 2-layer graph network, d=32
|
||||
- Output + attention distillation
|
||||
- QAT with INT8 simulation
|
||||
|
||||
3. Export to ONNX
|
||||
- Fixed input shape (16 nodes, 120 edges)
|
||||
- INT8 weights, FP32 positional encodings
|
||||
|
||||
4. Convert to TFLite Micro or custom C inference
|
||||
- Flatten attention to static matrix operations
|
||||
- Pre-compute positional encodings
|
||||
- Inline all operations (no dynamic dispatch)
|
||||
|
||||
5. Deploy to ESP32-S3 aggregator
|
||||
- Model in flash, activations in PSRAM
|
||||
- Inference budget: 8ms per frame at 100 Hz
|
||||
- Fallback: reduce to 50 Hz if budget exceeded
|
||||
```
|
||||
|
||||
### 7.7 Model Size Estimates
|
||||
|
||||
| Configuration | Parameters | INT8 Size | FP32 Size | Estimated Latency (ESP32-S3) |
|
||||
|--------------|-----------|-----------|-----------|------------------------------|
|
||||
| 2L, d=16, 2H | 8K | 8 KB | 32 KB | <1 ms |
|
||||
| 2L, d=32, 4H | 50K | 50 KB | 200 KB | 2-3 ms |
|
||||
| 2L, d=64, 4H | 180K | 180 KB | 720 KB | 5-8 ms |
|
||||
| 4L, d=32, 4H | 100K | 100 KB | 400 KB | 4-6 ms |
|
||||
| 4L, d=64, 8H | 400K | 400 KB | 1.6 MB | 10-15 ms |
|
||||
|
||||
The sweet spot for ESP32-S3 deployment is the 2-layer, d=32, 4-head configuration: 50K parameters, 50 KB INT8 model, 2-3 ms inference latency. This fits comfortably within the hardware constraints while providing sufficient model capacity for mincut prediction on a 16-node graph.
|
||||
|
||||
---
|
||||
|
||||
## 8. Synthesis and Recommendations
|
||||
|
||||
### 8.1 Recommended Architecture Stack
|
||||
|
||||
Based on the analysis across all seven dimensions, we recommend a layered architecture:
|
||||
|
||||
**Layer 1: Feature Extraction (Per-Link)**
|
||||
- Lightweight 1D CNN or linear projection on raw CSI vectors
|
||||
- Extracts link-level features: coherence, Doppler, phase gradient
|
||||
- Runs on each ESP32 sensor node or on the aggregator
|
||||
- Output: 32-dimensional feature vector per link
|
||||
|
||||
**Layer 2: Graph Transformer (Graph-Level)**
|
||||
- GPS-style architecture with MPNN + global attention
|
||||
- Combined positional encoding (LapPE + RWPE + spatial)
|
||||
- 2 layers, d=32, 4 attention heads
|
||||
- Processes the 16-node graph with link features as edge attributes
|
||||
- Output: 32-dimensional embedding per node
|
||||
|
||||
**Layer 3: MinCut Prediction Head**
|
||||
- Continuous relaxation (MinCutPool-style) for partition assignment
|
||||
- Edge-level binary prediction for cut edges
|
||||
- Spectral supervision from Fiedler vector
|
||||
- Temporal consistency regularization
|
||||
|
||||
**Layer 4: Temporal Integration**
|
||||
- TGN-style persistent per-node memory (GRU, d=16)
|
||||
- TGAT-style continuous time encoding for irregular TDM sampling
|
||||
- Sliding window of 10 frames for temporal context
|
||||
|
||||
### 8.2 Training Strategy
|
||||
|
||||
**Phase 1: Self-supervised pre-training.**
|
||||
- Masked CSI modeling on unlabeled data from diverse environments
|
||||
- Graph contrastive learning with topology augmentation
|
||||
- Duration: until convergence on held-out environments
|
||||
|
||||
**Phase 2: Supervised fine-tuning.**
|
||||
- Exact mincut labels computed offline
|
||||
- Fiedler vector regression for spectral supervision
|
||||
- Multi-task: mincut + occupancy count + room classification
|
||||
- Duration: until validation plateau
|
||||
|
||||
**Phase 3: Distillation and compression.**
|
||||
- Distill to edge-deployable student model
|
||||
- Quantization-aware training with INT8
|
||||
- Structured pruning of attention heads
|
||||
- Validate accuracy within 3% of teacher model
|
||||
|
||||
**Phase 4: Deployment and adaptation.**
|
||||
- Deploy INT8 model to ESP32-S3 aggregator
|
||||
- Online few-shot adaptation using LoRA weights stored in PSRAM
|
||||
- Continuous monitoring of prediction quality vs. exact mincut
|
||||
|
||||
### 8.3 Open Research Questions
|
||||
|
||||
1. **Spectral vs. spatial positional encoding.** For RF graphs where both the topology and physical coordinates are known, what is the optimal combination? Does one subsume the other?
|
||||
|
||||
2. **Scaling laws for RF transformers.** Do RF foundation models follow the same scaling laws as language models, or does the lower intrinsic dimensionality of RF data plateau earlier?
|
||||
|
||||
3. **Temporal attention span.** How many past frames should the transformer attend to? Too few misses slow dynamics (breathing); too many wastes computation on stale information.
|
||||
|
||||
4. **Adversarial robustness.** Can an attacker manipulate CSI measurements on a few links to fool the mincut predictor? How do we harden the model against adversarial RF injection? This connects to the adversarial detection module in RuvSense.
|
||||
|
||||
5. **Graph size generalization.** A model trained on 16-node graphs should ideally generalize to 8-node or 32-node deployments. Graph transformers with relative positional encoding (rather than absolute) are better positioned for this.
|
||||
|
||||
6. **Real-time continual learning.** Can the model update itself online as the environment changes (furniture moved, walls added/removed) without catastrophic forgetting of general RF knowledge?
|
||||
|
||||
### 8.4 Expected Performance Targets
|
||||
|
||||
| Metric | Target | Baseline (Exact Mincut) |
|
||||
|--------|--------|------------------------|
|
||||
| Mincut F1 (2-way) | >0.92 | 1.00 (by definition) |
|
||||
| Mincut F1 (k-way, k=4) | >0.85 | 1.00 |
|
||||
| Temporal smoothness (jitter) | <0.05 | 0.15 (noisy) |
|
||||
| Inference latency (ESP32-S3) | <5 ms | <0.1 ms |
|
||||
| Model size (INT8) | <100 KB | N/A (algorithm) |
|
||||
| Adaptation to new room | <5 min data | N/A |
|
||||
| Zero-shot transfer (new room) | >0.75 F1 | 1.00 |
|
||||
|
||||
### 8.5 Integration with RuView Pipeline
|
||||
|
||||
The transformer-based mincut predictor integrates into the existing RuView architecture at the following points:
|
||||
|
||||
- **Input**: CSI frames from `wifi-densepose-signal` (after phase alignment and coherence scoring via RuvSense modules)
|
||||
- **Graph construction**: `ruvector-mincut` provides the coherence-weighted graph
|
||||
- **Inference**: New `wifi-densepose-nn` backend for the graph transformer model
|
||||
- **Output**: Partition assignments consumed by `wifi-densepose-mat` for mass casualty assessment and `pose_tracker` for multi-person tracking
|
||||
- **Training**: `wifi-densepose-train` with ruvector integration for dataset management
|
||||
|
||||
The differentiable mincut predictor enables end-to-end gradient flow from downstream pose estimation loss through the partition decision back to the CSI feature extractor, potentially improving the entire pipeline's accuracy.
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
1. Ying et al. "Do Transformers Really Perform Bad for Graph Representation?" NeurIPS 2021. (Graphormer)
|
||||
2. Kreuzer et al. "Rethinking Graph Transformers with Spectral Attention." NeurIPS 2021. (SAN)
|
||||
3. Rampasek et al. "Recipe for a General, Powerful, Scalable Graph Transformer." NeurIPS 2022. (GPS)
|
||||
4. Kim et al. "Pure Transformers are Powerful Graph Learners." NeurIPS 2022. (TokenGT)
|
||||
5. Rossi et al. "Temporal Graph Networks for Deep Learning on Dynamic Graphs." ICML Workshop 2020. (TGN)
|
||||
6. Xu et al. "Inductive Representation Learning on Temporal Graphs." ICLR 2020. (TGAT)
|
||||
7. Trivedi et al. "DyRep: Learning Representations over Dynamic Graphs." ICLR 2019.
|
||||
8. Dosovitskiy et al. "An Image is Worth 16x16 Words." ICLR 2021. (ViT)
|
||||
9. Bianchi et al. "Spectral Clustering with Graph Neural Networks for Graph Pooling." ICML 2020. (MinCutPool)
|
||||
10. Dwivedi et al. "Benchmarking Graph Neural Networks." JMLR 2023.
|
||||
11. Lim et al. "Sign and Basis Invariant Networks for Spectral Graph Representation Learning." ICML 2022. (SignNet)
|
||||
12. Katharopoulos et al. "Transformers are RNNs." ICML 2020. (Linear Attention)
|
||||
13. Choromanski et al. "Rethinking Attention with Performers." ICLR 2021.
|
||||
14. Hu et al. "LoRA: Low-Rank Adaptation of Large Language Models." ICLR 2022.
|
||||
|
||||
---
|
||||
|
||||
*This document supports ADR-029 (RuvSense multistatic sensing mode) and ADR-031 (RuView sensing-first RF mode) by providing the theoretical foundation for transformer-based inference on RF topological graphs.*
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,934 @@
|
||||
# Quantum-Level Sensors for RF Topological Sensing
|
||||
|
||||
## SOTA Research Document — RF Topological Sensing Series (11/12)
|
||||
|
||||
**Date**: 2026-03-08
|
||||
**Domain**: Quantum Sensing × RF Topology × Graph-Based Detection
|
||||
**Status**: Research Survey
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
Classical RF sensing using ESP32 WiFi mesh nodes operates at milliwatt power levels with
|
||||
sensitivity limited by thermal noise floors (~-90 dBm). Quantum sensors offer fundamentally
|
||||
different detection mechanisms that can surpass classical limits by orders of magnitude,
|
||||
potentially transforming RF topological sensing from room-scale detection to single-photon
|
||||
field measurement.
|
||||
|
||||
This document surveys quantum sensing technologies relevant to RF topological sensing,
|
||||
evaluates their integration potential with the existing RuVector/mincut architecture, and
|
||||
identifies near-term and long-term opportunities.
|
||||
|
||||
---
|
||||
|
||||
## 2. Quantum Sensing Fundamentals
|
||||
|
||||
### 2.1 Nitrogen-Vacancy (NV) Centers in Diamond
|
||||
|
||||
NV centers are point defects in diamond crystal lattice where a nitrogen atom replaces a
|
||||
carbon atom adjacent to a vacancy. Key properties:
|
||||
|
||||
- **Sensitivity**: ~1 pT/√Hz at room temperature for magnetic fields
|
||||
- **Operating temperature**: Room temperature (unique advantage)
|
||||
- **Frequency range**: DC to ~10 GHz (microwave)
|
||||
- **Spatial resolution**: Nanometer-scale (single NV) to micrometer (ensemble)
|
||||
- **Detection mechanism**: Optically detected magnetic resonance (ODMR)
|
||||
|
||||
```
|
||||
Diamond Crystal with NV Center:
|
||||
|
||||
C---C---C---C
|
||||
| | | |
|
||||
C---N V---C N = Nitrogen atom
|
||||
| | | V = Vacancy
|
||||
C---C---C---C C = Carbon atoms
|
||||
| | | |
|
||||
C---C---C---C
|
||||
|
||||
ODMR Protocol:
|
||||
Green Laser → NV → Red Fluorescence
|
||||
↕
|
||||
Microwave Drive
|
||||
|
||||
Resonance frequency shifts with local B-field
|
||||
ΔfNV = γNV × B_local
|
||||
γNV = 28 GHz/T
|
||||
```
|
||||
|
||||
### 2.2 Superconducting Quantum Interference Devices (SQUIDs)
|
||||
|
||||
- **Sensitivity**: ~1 fT/√Hz (femtotesla — 1000× better than NV)
|
||||
- **Operating temperature**: 4 K (liquid helium) or 77 K (high-Tc)
|
||||
- **Frequency range**: DC to ~1 GHz
|
||||
- **Detection mechanism**: Josephson junction flux quantization
|
||||
- **Limitation**: Requires cryogenic cooling
|
||||
|
||||
```
|
||||
SQUID Loop:
|
||||
|
||||
┌──────[JJ1]──────┐
|
||||
│ │ JJ = Josephson Junction
|
||||
│ Φ_ext → │ Φ = Magnetic flux
|
||||
│ (flux) │
|
||||
│ │ V = Φ₀/(2π) × dφ/dt
|
||||
└──────[JJ2]──────┘ Φ₀ = 2.07 × 10⁻¹⁵ Wb
|
||||
|
||||
Critical current: Ic = 2I₀|cos(πΦ_ext/Φ₀)|
|
||||
Voltage oscillates with period Φ₀
|
||||
```
|
||||
|
||||
### 2.3 Rydberg Atom Sensors
|
||||
|
||||
Atoms excited to high principal quantum number (n > 30) become extraordinarily sensitive
|
||||
to electric fields:
|
||||
|
||||
- **Sensitivity**: ~1 µV/m/√Hz (electric field)
|
||||
- **Operating temperature**: Room temperature (vapor cell)
|
||||
- **Frequency range**: DC to THz (broadband, tunable)
|
||||
- **Detection mechanism**: Electromagnetically Induced Transparency (EIT)
|
||||
- **Key advantage**: Self-calibrated, SI-traceable (no calibration needed)
|
||||
|
||||
```
|
||||
Rydberg EIT Level Scheme:
|
||||
|
||||
|r⟩ -------- Rydberg state (n~50) ← RF field couples |r⟩↔|r'⟩
|
||||
↕ Ωc (coupling laser)
|
||||
|e⟩ -------- Excited state
|
||||
↕ Ωp (probe laser)
|
||||
|g⟩ -------- Ground state
|
||||
|
||||
Without RF: EIT window → transparent to probe
|
||||
With RF: Autler-Townes splitting → absorption changes
|
||||
|
||||
Splitting: Ω_RF = μ_rr' × E_RF / ℏ
|
||||
where μ_rr' = n² × e × a₀ (scales as n²!)
|
||||
```
|
||||
|
||||
### 2.4 Atomic Magnetometers
|
||||
|
||||
Spin-exchange relaxation-free (SERF) magnetometers using alkali vapor:
|
||||
|
||||
- **Sensitivity**: ~0.16 fT/√Hz (best demonstrated)
|
||||
- **Operating temperature**: ~150°C (heated vapor cell)
|
||||
- **Frequency range**: DC to ~1 kHz
|
||||
- **Size**: Can be miniaturized to chip-scale (CSAM)
|
||||
- **Limitation**: Low bandwidth, requires magnetic shielding
|
||||
|
||||
### 2.5 Comparison Table
|
||||
|
||||
| Sensor Type | Sensitivity | Temp | Bandwidth | Size | Cost Est. |
|
||||
|------------|-------------|------|-----------|------|-----------|
|
||||
| NV Diamond | ~1 pT/√Hz | 300K | DC-10 GHz | cm | $1K-10K |
|
||||
| SQUID | ~1 fT/√Hz | 4-77K | DC-1 GHz | cm | $10K-100K |
|
||||
| Rydberg | ~1 µV/m/√Hz | 300K | DC-THz | 10 cm | $5K-50K |
|
||||
| SERF | ~0.16 fT/√Hz | 420K | DC-1 kHz | cm | $5K-50K |
|
||||
| ESP32 (classical) | ~-90 dBm | 300K | 2.4/5 GHz | cm | $5 |
|
||||
|
||||
---
|
||||
|
||||
## 3. Quantum-Enhanced RF Detection
|
||||
|
||||
### 3.1 Classical vs Quantum Noise Limits
|
||||
|
||||
Classical RF detection is limited by thermal (Johnson-Nyquist) noise:
|
||||
|
||||
```
|
||||
Classical thermal noise floor:
|
||||
P_noise = k_B × T × B
|
||||
|
||||
At T = 300K, B = 20 MHz (WiFi channel):
|
||||
P_noise = 1.38e-23 × 300 × 20e6 = 8.3 × 10⁻¹⁴ W
|
||||
P_noise = -101 dBm
|
||||
|
||||
Shot noise limit (coherent state):
|
||||
ΔE = √(ℏω/(2ε₀V)) per photon
|
||||
SNR_shot ∝ √N_photons
|
||||
|
||||
Heisenberg limit (entangled state):
|
||||
SNR_Heisenberg ∝ N_photons
|
||||
|
||||
Quantum advantage: √N improvement over shot noise
|
||||
For N = 10⁶ photons → 1000× SNR improvement
|
||||
```
|
||||
|
||||
### 3.2 Quantum Advantage Regimes
|
||||
|
||||
The quantum advantage for RF sensing depends on the signal regime:
|
||||
|
||||
| Regime | Classical | Quantum | Advantage |
|
||||
|--------|-----------|---------|-----------|
|
||||
| Strong signal (>-60 dBm) | Adequate | Unnecessary | None |
|
||||
| Medium (-60 to -90 dBm) | Noisy | Cleaner | 10-100× SNR |
|
||||
| Weak (<-90 dBm) | Undetectable | Detectable | Enabling |
|
||||
| Single-photon | Impossible | Feasible | Infinite |
|
||||
|
||||
For RF topological sensing, the quantum advantage is most relevant for:
|
||||
- Detecting very subtle field perturbations (breathing, heartbeat)
|
||||
- Sensing through walls or at extended range
|
||||
- Distinguishing multiple overlapping perturbations
|
||||
|
||||
### 3.3 Quantum Noise Reduction Techniques
|
||||
|
||||
**Squeezed States**: Reduce noise in one quadrature at expense of other:
|
||||
```
|
||||
ΔX₁ × ΔX₂ ≥ ℏ/2
|
||||
Squeeze X₁: ΔX₁ = e⁻ʳ × √(ℏ/2) (reduced)
|
||||
ΔX₂ = e⁺ʳ × √(ℏ/2) (increased)
|
||||
|
||||
For r = 2 (17.4 dB squeezing):
|
||||
Noise reduction in amplitude: 7.4×
|
||||
Demonstrated: 15 dB squeezing (LIGO)
|
||||
```
|
||||
|
||||
**Quantum Error Correction**: Protect quantum states from decoherence:
|
||||
- Repetition codes for phase noise
|
||||
- Surface codes for general errors
|
||||
- Overhead: ~1000 physical qubits per logical qubit (current)
|
||||
|
||||
---
|
||||
|
||||
## 4. Rydberg Atom RF Sensors — Deep Dive
|
||||
|
||||
### 4.1 Broadband RF Detection via EIT
|
||||
|
||||
Rydberg atoms provide the most promising near-term quantum RF sensor for topological
|
||||
sensing because:
|
||||
|
||||
1. **Room temperature operation** — no cryogenics
|
||||
2. **Broadband** — single vapor cell covers MHz to THz by tuning laser wavelength
|
||||
3. **Self-calibrated** — response depends only on atomic constants
|
||||
4. **Compact** — vapor cell can be cm-scale
|
||||
|
||||
```
|
||||
Rydberg Sensor Architecture:
|
||||
|
||||
┌─────────────────────────────┐
|
||||
│ Cesium Vapor Cell │
|
||||
│ │
|
||||
│ Probe (852nm) ───────→ │──→ Photodetector
|
||||
│ Coupling (509nm) ───→ │
|
||||
│ │
|
||||
│ ↕ RF field enters │
|
||||
└─────────────────────────────┘
|
||||
|
||||
Frequency tuning:
|
||||
n=30: ~300 GHz transitions
|
||||
n=50: ~50 GHz transitions
|
||||
n=70: ~10 GHz transitions (WiFi band!)
|
||||
n=100: ~1 GHz transitions
|
||||
```
|
||||
|
||||
### 4.2 Sensitivity at WiFi Frequencies
|
||||
|
||||
For 2.4 GHz detection using Rydberg states near n=70:
|
||||
|
||||
```
|
||||
Transition dipole moment:
|
||||
μ = n² × e × a₀ ≈ 70² × 1.6e-19 × 5.3e-11
|
||||
μ ≈ 4.1 × 10⁻²⁶ C·m
|
||||
|
||||
Minimum detectable field:
|
||||
E_min = ℏ × Γ / (2μ)
|
||||
where Γ = EIT linewidth ≈ 1 MHz
|
||||
|
||||
E_min ≈ 1.05e-34 × 2π × 1e6 / (2 × 4.1e-26)
|
||||
E_min ≈ 8 µV/m
|
||||
|
||||
Compare to ESP32 sensitivity: ~1 mV/m
|
||||
Quantum advantage: ~125× in field sensitivity
|
||||
```
|
||||
|
||||
### 4.3 NIST and Army Research Lab Advances
|
||||
|
||||
Key milestones in Rydberg RF sensing:
|
||||
- **2012**: First demonstration of Rydberg EIT for RF measurement (Sedlacek et al.)
|
||||
- **2018**: Broadband electric field sensing 1-500 GHz (Holloway et al., NIST)
|
||||
- **2020**: Rydberg atom receiver for AM/FM radio signals
|
||||
- **2022**: Multi-band simultaneous detection using multiple Rydberg transitions
|
||||
- **2024**: Chip-scale vapor cells with integrated photonics
|
||||
- **2025**: Field demonstrations of Rydberg receivers for communications
|
||||
|
||||
### 4.4 Integration with ESP32 Mesh
|
||||
|
||||
```
|
||||
Hybrid Rydberg-ESP32 Architecture:
|
||||
|
||||
Classical Layer (ESP32 mesh):
|
||||
┌────┐ ┌────┐ ┌────┐
|
||||
│ESP1│────│ESP2│────│ESP3│ 120 classical edges
|
||||
└────┘ └────┘ └────┘ CSI coherence weights
|
||||
│ │ │
|
||||
│ ┌────┴────┐ │
|
||||
└────│Rydberg │────┘ Quantum sensor node
|
||||
│ Sensor │ High-sensitivity edges
|
||||
└─────────┘
|
||||
|
||||
The Rydberg sensor provides:
|
||||
1. Ultra-sensitive reference measurements
|
||||
2. Ground truth calibration for classical edges
|
||||
3. Detection of sub-threshold perturbations
|
||||
4. Phase reference for coherence estimation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Quantum Illumination for Object Detection
|
||||
|
||||
### 5.1 Lloyd's Quantum Illumination Protocol
|
||||
|
||||
Quantum illumination uses entangled photon pairs to detect objects in noisy environments:
|
||||
|
||||
```
|
||||
Protocol:
|
||||
1. Generate entangled signal-idler pair: |Ψ⟩ = Σ cₙ|n⟩_S|n⟩_I
|
||||
2. Send signal photon toward target, keep idler
|
||||
3. Collect reflected signal (buried in thermal noise)
|
||||
4. Joint measurement on returned signal + stored idler
|
||||
|
||||
Classical detection: SNR = N_S / N_B
|
||||
Quantum detection: SNR = N_S × (N_B + 1) / N_B
|
||||
|
||||
Advantage: 6 dB in error exponent (factor of 4)
|
||||
|
||||
Critical: Advantage persists even when entanglement is destroyed
|
||||
by the noisy channel (unlike most quantum protocols)
|
||||
```
|
||||
|
||||
### 5.2 Microwave Quantum Illumination
|
||||
|
||||
For RF topological sensing at 2.4 GHz:
|
||||
|
||||
```
|
||||
Microwave entangled source:
|
||||
Josephson Parametric Amplifier (JPA)
|
||||
→ Generates entangled microwave-microwave pairs
|
||||
→ Or microwave-optical pairs (for optical idler storage)
|
||||
|
||||
Challenge: thermal photon number at 2.4 GHz, 300K:
|
||||
n_th = 1/(exp(hf/kT) - 1) = 1/(exp(4.8e-5) - 1) ≈ 2600
|
||||
|
||||
Background: ~2600 thermal photons per mode
|
||||
→ Classical detection hopeless for single-photon signals
|
||||
→ Quantum illumination still provides 6 dB advantage
|
||||
```
|
||||
|
||||
### 5.3 Application to RF Topology
|
||||
|
||||
Quantum illumination could enhance RF topological sensing by:
|
||||
- Detecting very weak reflections from small objects
|
||||
- Operating in high-noise environments (industrial, urban)
|
||||
- Distinguishing target-reflected signals from multipath clutter
|
||||
- Providing phase-coherent measurements for graph edge weights
|
||||
|
||||
---
|
||||
|
||||
## 6. Quantum Graph Theory
|
||||
|
||||
### 6.1 Quantum Walks on Graphs
|
||||
|
||||
Quantum walks are the quantum analog of random walks, with superposition and interference:
|
||||
|
||||
```
|
||||
Continuous-time quantum walk on graph G:
|
||||
|ψ(t)⟩ = e^{-iHt} |ψ(0)⟩
|
||||
where H = adjacency matrix A or Laplacian L
|
||||
|
||||
Key property: Quantum walk spreads quadratically faster
|
||||
Classical: ⟨x²⟩ ~ t (diffusive)
|
||||
Quantum: ⟨x²⟩ ~ t² (ballistic)
|
||||
|
||||
For graph topology detection:
|
||||
- Walk dynamics encode graph structure
|
||||
- Interference patterns reveal symmetries
|
||||
- Hitting times indicate connectivity
|
||||
```
|
||||
|
||||
### 6.2 Quantum Minimum Cut
|
||||
|
||||
**Grover-accelerated graph search**:
|
||||
```
|
||||
Classical min-cut (Stoer-Wagner): O(VE + V² log V)
|
||||
For V=16, E=120: ~4,000 operations
|
||||
|
||||
Quantum search for min-cut:
|
||||
Use Grover's algorithm to search over cuts
|
||||
Number of possible cuts: 2^V = 2^16 = 65,536
|
||||
|
||||
Classical brute force: O(2^V) = 65,536 evaluations
|
||||
Quantum (Grover): O(√(2^V)) = 256 evaluations
|
||||
|
||||
Quadratic speedup for brute-force approach
|
||||
|
||||
However: For V=16, Stoer-Wagner (4,000 ops) beats Grover (256 oracle calls)
|
||||
because each oracle call has overhead
|
||||
|
||||
Quantum advantage threshold: V > ~100 nodes
|
||||
```
|
||||
|
||||
**Quantum spectral analysis**:
|
||||
```
|
||||
Quantum Phase Estimation (QPE) for graph Laplacian:
|
||||
Input: L = D - A (graph Laplacian)
|
||||
Output: eigenvalues λ₁ ≤ λ₂ ≤ ... ≤ λ_V
|
||||
|
||||
Fiedler value λ₂ → algebraic connectivity
|
||||
Cheeger inequality: λ₂/2 ≤ h(G) ≤ √(2λ₂)
|
||||
where h(G) = min-cut / min-volume (Cheeger constant)
|
||||
|
||||
QPE complexity: O(poly(log V)) per eigenvalue
|
||||
Classical: O(V³) for full eigendecomposition
|
||||
|
||||
Quantum advantage for spectral analysis: exponential
|
||||
for V >> 100
|
||||
```
|
||||
|
||||
### 6.3 Quantum Graph Partitioning
|
||||
|
||||
```
|
||||
Variational Quantum Eigensolver (VQE) for normalized cut:
|
||||
|
||||
Minimize: NCut = cut(A,B) × (1/vol(A) + 1/vol(B))
|
||||
|
||||
Encode as QUBO:
|
||||
min x^T Q x where x ∈ {0,1}^V
|
||||
Q_ij = -w_ij + d_i × δ_ij × balance_penalty
|
||||
|
||||
Map to Ising Hamiltonian:
|
||||
H = Σ_ij J_ij σ_i^z σ_j^z + Σ_i h_i σ_i^z
|
||||
|
||||
Solve with:
|
||||
- VQE (gate-based): variational ansatz circuit
|
||||
- QAOA: alternating cost/mixer unitaries
|
||||
- Quantum annealing (D-Wave): native QUBO solver
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Hybrid Classical-Quantum RF Sensing Architecture
|
||||
|
||||
### 7.1 Where Quantum Advantage Matters
|
||||
|
||||
Not every edge in the RF sensing graph benefits from quantum sensing. The advantage
|
||||
is concentrated in specific scenarios:
|
||||
|
||||
| Scenario | Classical | Quantum | Benefit |
|
||||
|----------|-----------|---------|---------|
|
||||
| Strong LOS links | Adequate | Overkill | None |
|
||||
| Weak NLOS links | Noisy/lost | Detectable | Enables new edges |
|
||||
| Sub-threshold perturbations | Invisible | Detectable | Breathing, heartbeat |
|
||||
| Phase coherence measurement | Clock-limited | Fundamental | Better edge weights |
|
||||
| Multi-target disambiguation | Ambiguous | Resolvable | More accurate cuts |
|
||||
|
||||
### 7.2 Hybrid Architecture
|
||||
|
||||
```
|
||||
Three-Tier Hybrid Sensing:
|
||||
|
||||
Tier 1: ESP32 Classical Mesh (16 nodes, $80 total)
|
||||
┌─────────────────────────────────────┐
|
||||
│ Standard CSI extraction │
|
||||
│ 120 TX-RX edges │
|
||||
│ ~30-60 cm resolution │
|
||||
│ Person-scale detection │
|
||||
└──────────────┬──────────────────────┘
|
||||
│
|
||||
Tier 2: NV Diamond Enhancement (4 nodes, ~$20K)
|
||||
┌──────────────┴──────────────────────┐
|
||||
│ pT-level magnetic field sensing │
|
||||
│ Room-temperature operation │
|
||||
│ Complements RF with B-field edges │
|
||||
│ Breathing/heartbeat detection │
|
||||
└──────────────┬──────────────────────┘
|
||||
│
|
||||
Tier 3: Rydberg Reference (1 node, ~$50K)
|
||||
┌──────────────┴──────────────────────┐
|
||||
│ µV/m electric field sensitivity │
|
||||
│ Self-calibrated SI-traceable │
|
||||
│ Ground truth for classical edges │
|
||||
│ Sub-threshold perturbation detect │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
Graph construction:
|
||||
G_hybrid = G_classical ∪ G_magnetic ∪ G_quantum
|
||||
|
||||
Edge weight fusion:
|
||||
w_ij = α × w_classical + β × w_magnetic + γ × w_quantum
|
||||
where α + β + γ = 1, learned per-edge
|
||||
```
|
||||
|
||||
### 7.3 Quantum-Enhanced Edge Weight Computation
|
||||
|
||||
```
|
||||
Classical edge weight (ESP32):
|
||||
w_ij = coherence(CSI_i→j)
|
||||
Noise floor: ~-90 dBm
|
||||
Phase noise: ~5° RMS (clock drift limited)
|
||||
|
||||
Quantum-enhanced edge weight:
|
||||
w_ij = f(CSI_ij, B_field_ij, E_field_ij)
|
||||
|
||||
NV contribution:
|
||||
- Local magnetic field map at pT resolution
|
||||
- Detects metallic object perturbations
|
||||
- Measures eddy current signatures
|
||||
|
||||
Rydberg contribution:
|
||||
- Electric field at µV/m resolution
|
||||
- Phase-accurate reference measurement
|
||||
- Calibrates classical CSI phase errors
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Quantum Coherence for RF Field Mapping
|
||||
|
||||
### 8.1 Decoherence as Environmental Sensor
|
||||
|
||||
Quantum sensors naturally measure their environment through decoherence:
|
||||
|
||||
```
|
||||
NV Center Decoherence:
|
||||
T₁ (spin-lattice relaxation): ~6 ms at 300K
|
||||
T₂ (spin-spin dephasing): ~1 ms at 300K
|
||||
T₂* (inhomogeneous): ~1 µs
|
||||
|
||||
Environmental perturbation → T₂* change
|
||||
|
||||
Sensitivity:
|
||||
ΔB_min = (1/γ) × 1/(T₂* × √(η × T_meas))
|
||||
|
||||
where η = photon collection efficiency
|
||||
T_meas = measurement time
|
||||
|
||||
At η=0.1, T_meas=1s:
|
||||
ΔB_min ≈ 1 pT
|
||||
```
|
||||
|
||||
The key insight: **decoherence signatures encode environmental structure**. Different
|
||||
objects and materials produce different decoherence profiles:
|
||||
|
||||
| Object | Decoherence Mechanism | Signature |
|
||||
|--------|----------------------|-----------|
|
||||
| Metal | Eddy currents, Johnson noise | T₂* reduction, broadband |
|
||||
| Human body | Ionic currents, diamagnetism | T₁ modulation, low-freq |
|
||||
| Water | Diamagnetic susceptibility | Subtle T₂ shift |
|
||||
| Electronics | EM emission | Discrete frequency peaks |
|
||||
|
||||
### 8.2 Quantum Fisher Information for Optimal Placement
|
||||
|
||||
```
|
||||
Quantum Fisher Information (QFI):
|
||||
F_Q(θ) = 4(⟨∂_θψ|∂_θψ⟩ - |⟨ψ|∂_θψ⟩|²)
|
||||
|
||||
Quantum Cramér-Rao Bound:
|
||||
Var(θ̂) ≥ 1/(N × F_Q(θ))
|
||||
|
||||
For sensor placement optimization:
|
||||
- Compute F_Q at each candidate position
|
||||
- Place quantum sensors where F_Q is maximized
|
||||
- Typically: room center, doorways, narrow passages
|
||||
|
||||
Optimal placement for V=16 classical + 4 quantum:
|
||||
┌─────────────────────────┐
|
||||
│ E E E E E E │ E = ESP32 (perimeter)
|
||||
│ │
|
||||
│ E Q Q E │ Q = Quantum sensor
|
||||
│ │ (high-FI positions)
|
||||
│ E Q Q E │
|
||||
│ │
|
||||
│ E E E E E E │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Quantum Machine Learning for RF
|
||||
|
||||
### 9.1 Variational Quantum Circuits for Graph Classification
|
||||
|
||||
```
|
||||
Quantum Graph Neural Network:
|
||||
|
||||
Input: Edge weights w_ij from RF sensing graph
|
||||
|
||||
Encoding: Amplitude encoding of adjacency matrix
|
||||
|ψ_G⟩ = Σ_ij w_ij |i⟩|j⟩ / ||w||
|
||||
|
||||
Variational circuit:
|
||||
U(θ) = Π_l [U_entangle × U_rotation(θ_l)]
|
||||
|
||||
U_rotation: R_y(θ₁) ⊗ R_y(θ₂) ⊗ ... ⊗ R_y(θ_V)
|
||||
U_entangle: CNOT cascade matching graph topology
|
||||
|
||||
Measurement: ⟨Z₁⟩ → occupancy classification
|
||||
|
||||
Training: Minimize L = Σ (y - ⟨Z₁⟩)² via parameter-shift rule
|
||||
|
||||
For V=16: Requires 16 qubits + ~100 variational parameters
|
||||
→ Within reach of current NISQ devices (IBM Eagle: 127 qubits)
|
||||
```
|
||||
|
||||
### 9.2 Quantum Kernel Methods
|
||||
|
||||
```
|
||||
Quantum kernel for CSI feature space:
|
||||
|
||||
Encode CSI vector x into quantum state: |φ(x)⟩ = U(x)|0⟩
|
||||
|
||||
Kernel: K(x, x') = |⟨φ(x)|φ(x')⟩|²
|
||||
|
||||
Properties:
|
||||
- Maps to exponentially large Hilbert space
|
||||
- Can capture correlations classical kernels miss
|
||||
- Computed on quantum hardware, used in classical SVM/GP
|
||||
|
||||
For edge classification (stable/unstable/transitioning):
|
||||
- Encode temporal CSI window as quantum state
|
||||
- Quantum kernel captures phase correlations
|
||||
- Classical SVM classifies using quantum kernel values
|
||||
```
|
||||
|
||||
### 9.3 Quantum Reservoir Computing
|
||||
|
||||
```
|
||||
Quantum Reservoir for Temporal RF Patterns:
|
||||
|
||||
RF Signal → Quantum System → Measurement → Classical Readout
|
||||
|
||||
Reservoir: N coupled qubits with natural dynamics
|
||||
H_res = Σ_i h_i σ_i^z + Σ_ij J_ij σ_i^z σ_j^z + Σ_i Ω_i σ_i^x
|
||||
|
||||
Input: CSI values modulate h_i (local fields)
|
||||
Dynamics: ρ(t+1) = U × ρ(t) × U† + noise
|
||||
Output: Measure ⟨σ_i^z⟩ for all qubits → feature vector
|
||||
|
||||
Advantages for temporal RF sensing:
|
||||
- Natural temporal memory (quantum coherence)
|
||||
- No training of reservoir (only readout layer)
|
||||
- Captures non-linear temporal correlations
|
||||
- Matches temporal graph evolution naturally
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Near-Term NISQ Applications
|
||||
|
||||
### 10.1 Quantum Annealing for Graph Cuts (D-Wave)
|
||||
|
||||
```
|
||||
Min-cut as QUBO on D-Wave:
|
||||
|
||||
Variables: x_i ∈ {0,1} (node partition assignment)
|
||||
|
||||
Objective: minimize Σ_ij w_ij × x_i × (1-x_j)
|
||||
|
||||
QUBO matrix:
|
||||
Q_ij = -w_ij (off-diagonal)
|
||||
Q_ii = Σ_j w_ij (diagonal)
|
||||
|
||||
D-Wave Advantage2: 7,000+ qubits
|
||||
→ Can handle graphs up to ~3,500 nodes
|
||||
→ Our V=16 graph trivially fits
|
||||
|
||||
Practical consideration:
|
||||
- Cloud API access: ~$2K/month
|
||||
- Annealing time: ~20 µs per sample
|
||||
- 1000 samples for statistics: ~20 ms
|
||||
- Compatible with 20 Hz update rate
|
||||
|
||||
Multi-cut extension (k-way):
|
||||
Use k binary variables per node
|
||||
→ 16 × k = 48 qubits for 3-person detection
|
||||
```
|
||||
|
||||
### 10.2 VQE for Spectral Graph Analysis
|
||||
|
||||
```
|
||||
Variational Quantum Eigensolver for Laplacian spectrum:
|
||||
|
||||
Goal: Find smallest eigenvalues of L = D - A
|
||||
|
||||
Ansatz: |ψ(θ)⟩ = U(θ)|0⟩^⊗n
|
||||
|
||||
Cost: E(θ) = ⟨ψ(θ)|L|ψ(θ)⟩
|
||||
|
||||
Optimization: θ* = argmin E(θ) via classical optimizer
|
||||
|
||||
For Fiedler value (λ₂):
|
||||
1. Find ground state |v₁⟩ (constant vector, known)
|
||||
2. Constrain ⟨v₁|ψ⟩ = 0
|
||||
3. Minimize in orthogonal subspace → λ₂
|
||||
|
||||
Application: Track λ₂ over time
|
||||
- λ₂ large → graph well-connected → no obstruction
|
||||
- λ₂ drops → graph nearly disconnected → boundary detected
|
||||
- Rate of λ₂ change → speed of perturbation
|
||||
```
|
||||
|
||||
### 10.3 QAOA for Balanced Partitioning
|
||||
|
||||
```
|
||||
Quantum Approximate Optimization Algorithm:
|
||||
|
||||
Cost Hamiltonian: H_C = Σ_ij w_ij (1 - Z_i Z_j) / 2
|
||||
Mixer Hamiltonian: H_M = Σ_i X_i
|
||||
|
||||
p-layer circuit:
|
||||
|ψ(γ,β)⟩ = Π_l [e^{-iβ_l H_M} × e^{-iγ_l H_C}] |+⟩^⊗n
|
||||
|
||||
For p=1: Guaranteed approximation ratio r ≥ 0.6924 for MaxCut
|
||||
For p=3-5: Near-optimal for small graphs
|
||||
|
||||
Our V=16 graph: 16 qubits, p=3 → 96 parameters
|
||||
→ Trainable on current hardware
|
||||
→ Could provide better-than-classical cuts in some cases
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Integration with RuVector and Mincut
|
||||
|
||||
### 11.1 Quantum-Classical Data Flow
|
||||
|
||||
```
|
||||
Integration Pipeline:
|
||||
|
||||
ESP32 Mesh Quantum Sensors
|
||||
┌──────────┐ ┌──────────┐
|
||||
│ CSI Data │ │ QSensor │
|
||||
│ 120 edges│ │ 4 nodes │
|
||||
│ 20 Hz │ │ 100 Hz │
|
||||
└────┬─────┘ └────┬─────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────────────────────┐
|
||||
│ Edge Weight Fusion │
|
||||
│ │
|
||||
│ w_ij = fuse( │
|
||||
│ classical_coherence, │
|
||||
│ magnetic_perturbation, │
|
||||
│ quantum_phase_ref │
|
||||
│ ) │
|
||||
└──────────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ RfGraph Construction │
|
||||
│ G = (V_classical ∪ V_quantum, E_fused)
|
||||
└──────────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ Hybrid Mincut │
|
||||
│ - Classical: Stoer-Wagner │
|
||||
│ - Or quantum: D-Wave QUBO │
|
||||
│ - Select based on graph size│
|
||||
└──────────────┬───────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ RuVector Temporal Store │
|
||||
│ - Graph evolution history │
|
||||
│ - Quantum measurement log │
|
||||
│ - Attention-weighted fusion │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
### 11.2 Rust Module Design
|
||||
|
||||
```rust
|
||||
/// Quantum sensor integration for RF topological sensing
|
||||
pub trait QuantumSensor: Send + Sync {
|
||||
/// Get current measurement with uncertainty
|
||||
fn measure(&self) -> QuantumMeasurement;
|
||||
|
||||
/// Sensor sensitivity in appropriate units
|
||||
fn sensitivity(&self) -> f64;
|
||||
|
||||
/// Decoherence time (characterizes environment)
|
||||
fn coherence_time(&self) -> Duration;
|
||||
}
|
||||
|
||||
pub struct QuantumMeasurement {
|
||||
pub value: f64,
|
||||
pub uncertainty: f64, // Quantum uncertainty
|
||||
pub fisher_information: f64, // QFI for this measurement
|
||||
pub timestamp: Instant,
|
||||
pub sensor_type: QuantumSensorType,
|
||||
}
|
||||
|
||||
pub enum QuantumSensorType {
|
||||
NVDiamond { t2_star: Duration },
|
||||
Rydberg { principal_n: u32, transition_freq: f64 },
|
||||
SQUID { flux_quantum: f64 },
|
||||
SERF { vapor_temp: f64 },
|
||||
}
|
||||
|
||||
/// Fuse classical and quantum edge weights
|
||||
pub trait HybridEdgeWeightFusion {
|
||||
fn fuse(
|
||||
&self,
|
||||
classical: &ClassicalEdgeWeight,
|
||||
quantum: Option<&QuantumMeasurement>,
|
||||
) -> FusedEdgeWeight;
|
||||
}
|
||||
|
||||
pub struct FusedEdgeWeight {
|
||||
pub weight: f64,
|
||||
pub confidence: f64, // Higher with quantum data
|
||||
pub classical_contribution: f64,
|
||||
pub quantum_contribution: f64,
|
||||
pub fisher_bound: f64, // QCRB on precision
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Hardware Roadmap
|
||||
|
||||
### 12.1 Technology Readiness Levels
|
||||
|
||||
| Technology | Current TRL | Field-Ready | Clinical | Notes |
|
||||
|-----------|-------------|-------------|----------|-------|
|
||||
| NV Diamond magnetometer | TRL 5-6 | 2026-2028 | 2030+ | Room temp, most practical |
|
||||
| Chip-scale NV | TRL 3-4 | 2028-2030 | 2032+ | Integration with CMOS |
|
||||
| Rydberg RF receiver | TRL 4-5 | 2027-2029 | N/A | Military interest high |
|
||||
| Miniature SQUID | TRL 7-8 | Available | Available | Requires cryogenics |
|
||||
| SERF magnetometer | TRL 5-6 | 2026-2028 | 2029+ | Needs shielding |
|
||||
| Quantum annealer (D-Wave) | TRL 8-9 | Available | N/A | Cloud access now |
|
||||
| NISQ processor (IBM/Google) | TRL 6-7 | 2026+ | N/A | 1000+ qubits by 2026 |
|
||||
|
||||
### 12.2 Size, Weight, Power (SWaP) Analysis
|
||||
|
||||
```
|
||||
Current vs Projected SWaP:
|
||||
|
||||
NV Diamond Sensor (2025):
|
||||
Size: 15 × 10 × 10 cm
|
||||
Weight: 2 kg
|
||||
Power: 5 W (laser + electronics)
|
||||
|
||||
NV Diamond Sensor (2028 projected):
|
||||
Size: 5 × 3 × 3 cm
|
||||
Weight: 200 g
|
||||
Power: 1 W
|
||||
|
||||
Rydberg Vapor Cell (2025):
|
||||
Size: 20 × 15 × 15 cm
|
||||
Weight: 3 kg
|
||||
Power: 10 W (two lasers + control)
|
||||
|
||||
Chip-Scale Rydberg (2030 projected):
|
||||
Size: 3 × 3 × 1 cm
|
||||
Weight: 50 g
|
||||
Power: 0.5 W
|
||||
|
||||
Compare ESP32:
|
||||
Size: 5 × 3 × 0.5 cm
|
||||
Weight: 10 g
|
||||
Power: 0.44 W
|
||||
```
|
||||
|
||||
### 12.3 Deployment Timeline
|
||||
|
||||
```
|
||||
Phase 1 (2026): Classical-only RF topology
|
||||
- 16 ESP32 nodes
|
||||
- Stoer-Wagner mincut
|
||||
- Proof of concept
|
||||
|
||||
Phase 2 (2027-2028): Quantum-enhanced
|
||||
- 16 ESP32 + 2-4 NV diamond nodes
|
||||
- Hybrid edge weights
|
||||
- Sub-threshold detection (breathing)
|
||||
|
||||
Phase 3 (2029-2030): Full quantum integration
|
||||
- 16 ESP32 + 4 NV + 1 Rydberg
|
||||
- Quantum-classical graph fusion
|
||||
- D-Wave cloud for multi-cut optimization
|
||||
|
||||
Phase 4 (2031+): Quantum-native
|
||||
- Chip-scale quantum sensors at every node
|
||||
- On-device quantum processing
|
||||
- Room-scale coherence imaging
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. Open Questions and Future Directions
|
||||
|
||||
### 13.1 Fundamental Questions
|
||||
|
||||
1. **Quantum advantage threshold**: At what graph size does quantum mincut outperform
|
||||
classical? Preliminary analysis suggests V > 100, but constant factors matter.
|
||||
|
||||
2. **Decoherence as feature**: Can quantum decoherence rates serve as edge weights
|
||||
directly, bypassing classical CSI entirely?
|
||||
|
||||
3. **Entanglement distribution**: Can entangled sensor pairs provide correlated
|
||||
edge weights with fundamentally lower uncertainty?
|
||||
|
||||
4. **Quantum memory for temporal graphs**: Can quantum memory store graph evolution
|
||||
states more efficiently than classical RuVector?
|
||||
|
||||
### 13.2 Engineering Questions
|
||||
|
||||
5. **Noise budget**: In a real room with WiFi, Bluetooth, and power line interference,
|
||||
what is the practical quantum advantage?
|
||||
|
||||
6. **Calibration**: How often do quantum sensors need recalibration in field deployment?
|
||||
|
||||
7. **Cost trajectory**: When will quantum sensor nodes reach $100/unit for mass deployment?
|
||||
|
||||
8. **Hybrid optimization**: What is the optimal ratio of classical to quantum nodes
|
||||
for a given room size and detection requirement?
|
||||
|
||||
### 13.3 Application Questions
|
||||
|
||||
9. **Resolution limits**: Does quantum sensing fundamentally change the 30-60 cm
|
||||
resolution bound, or only improve SNR within the same Fresnel-limited resolution?
|
||||
|
||||
10. **Multi-room scaling**: Can quantum entanglement between rooms provide correlated
|
||||
sensing that classical links cannot?
|
||||
|
||||
11. **Adversarial robustness**: Are quantum-enhanced edge weights more robust against
|
||||
deliberate spoofing or jamming?
|
||||
|
||||
---
|
||||
|
||||
## 14. References
|
||||
|
||||
1. Degen, C.L., Reinhard, F., Cappellaro, P. (2017). "Quantum sensing." Rev. Mod. Phys. 89, 035002.
|
||||
2. Sedlacek, J.A., et al. (2012). "Microwave electrometry with Rydberg atoms in a vapour cell." Nature Physics 8, 819.
|
||||
3. Holloway, C.L., et al. (2014). "Broadband Rydberg atom-based electric-field probe." IEEE Trans. Antentic. Propag. 62, 6169.
|
||||
4. Lloyd, S. (2008). "Enhanced sensitivity of photodetection via quantum illumination." Science 321, 1463.
|
||||
5. Tan, S.H., et al. (2008). "Quantum illumination with Gaussian states." Phys. Rev. Lett. 101, 253601.
|
||||
6. Childs, A.M. (2010). "On the relationship between continuous- and discrete-time quantum walk." Commun. Math. Phys. 294, 581.
|
||||
7. Farhi, E., Goldstone, J., Gutmann, S. (2014). "A quantum approximate optimization algorithm." arXiv:1411.4028.
|
||||
8. Peruzzo, A., et al. (2014). "A variational eigenvalue solver on a photonic quantum processor." Nature Communications 5, 4213.
|
||||
9. Taylor, J.M., et al. (2008). "High-sensitivity diamond magnetometer with nanoscale resolution." Nature Physics 4, 810.
|
||||
10. Boto, E., et al. (2018). "Moving magnetoencephalography towards real-world applications with a wearable system." Nature 555, 657.
|
||||
11. Schuld, M., Killoran, N. (2019). "Quantum machine learning in feature Hilbert spaces." Phys. Rev. Lett. 122, 040504.
|
||||
|
||||
---
|
||||
|
||||
## 15. Summary
|
||||
|
||||
Quantum sensing represents a paradigm shift for RF topological sensing. While the classical
|
||||
ESP32 mesh provides adequate sensitivity for person-scale detection, quantum sensors enable:
|
||||
|
||||
1. **100-1000× sensitivity improvement** for subtle perturbations
|
||||
2. **New sensing modalities** (magnetic fields, electric fields) complementing RF
|
||||
3. **Self-calibrated measurements** via Rydberg atom standards
|
||||
4. **Quantum-accelerated graph algorithms** for larger meshes
|
||||
5. **Decoherence-based environmental sensing** as a fundamentally new edge weight source
|
||||
|
||||
The most practical near-term integration path uses NV diamond sensors (room temperature,
|
||||
pT sensitivity) as enhancement nodes within the classical ESP32 mesh, with Rydberg sensors
|
||||
providing calibration references. Quantum computing (D-Wave, NISQ) offers immediate
|
||||
value for graph cut optimization at scale.
|
||||
|
||||
The long-term vision is a quantum-native sensing mesh where every node performs quantum
|
||||
measurements, edge weights encode quantum coherence between nodes, and graph algorithms
|
||||
run on quantum hardware — a true quantum radio nervous system.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,790 @@
|
||||
# NV Diamond Magnetometers for Neural Current Detection
|
||||
|
||||
## SOTA Research Document — RF Topological Sensing Series (13/22)
|
||||
|
||||
**Date**: 2026-03-09
|
||||
**Domain**: Nitrogen-Vacancy Quantum Sensing × Neural Magnetometry × Graph Topology
|
||||
**Status**: Research Survey
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
Neurons communicate through ionic currents. Those currents generate magnetic fields — tiny
|
||||
ones, measured in femtotesla (10⁻¹⁵ T). For context, Earth's magnetic field is approximately
|
||||
50 μT, roughly 10¹⁰ times stronger than the magnetic signature of a single cortical column.
|
||||
|
||||
Detecting these fields has historically required SQUID magnetometers operating at 4 Kelvin
|
||||
inside massive liquid helium dewars. This technology, while sensitive (3–5 fT/√Hz), is
|
||||
expensive ($2–5M per system), immobile, and impractical for wearable or portable applications.
|
||||
|
||||
Nitrogen-vacancy (NV) centers in diamond offer a fundamentally different approach. These
|
||||
atomic-scale defects in diamond crystal lattice can detect magnetic fields at femtotesla
|
||||
sensitivity while operating at room temperature. They can be miniaturized to chip scale,
|
||||
fabricated in dense arrays, and integrated with standard electronics.
|
||||
|
||||
For the RuVector + dynamic mincut brain analysis architecture, NV diamond magnetometers
|
||||
represent the medium-term sensor technology that could enable portable, affordable,
|
||||
high-spatial-resolution neural topology measurement.
|
||||
|
||||
---
|
||||
|
||||
## 2. NV Center Physics
|
||||
|
||||
### 2.1 Crystal Structure and Defect Properties
|
||||
|
||||
Diamond has a face-centered cubic crystal lattice of carbon atoms. An NV center forms when:
|
||||
1. A nitrogen atom substitutes for one carbon atom
|
||||
2. An adjacent lattice site is vacant (missing carbon)
|
||||
|
||||
The resulting NV⁻ (negatively charged) defect has remarkable quantum properties:
|
||||
- Electronic spin triplet ground state (³A₂) with S = 1
|
||||
- Spin sublevels: mₛ = 0 and mₛ = ±1, split by 2.87 GHz at zero field
|
||||
- Optically addressable: 532 nm green laser excites, red fluorescence (637–800 nm) reads out
|
||||
- Spin-dependent fluorescence: mₛ = 0 is brighter than mₛ = ±1
|
||||
|
||||
This spin-dependent fluorescence is the key to magnetometry: magnetic fields shift the
|
||||
energy of the mₛ = ±1 states (Zeeman effect), which is detected as a change in
|
||||
fluorescence intensity when microwaves are swept through resonance.
|
||||
|
||||
### 2.2 Optically Detected Magnetic Resonance (ODMR)
|
||||
|
||||
The measurement protocol:
|
||||
|
||||
1. **Optical initialization**: Green laser (532 nm) pumps NV into mₛ = 0 ground state
|
||||
2. **Microwave interrogation**: Sweep microwave frequency around 2.87 GHz
|
||||
3. **Optical readout**: Monitor red fluorescence intensity
|
||||
4. **Resonance detection**: Fluorescence dips at frequencies corresponding to mₛ = ±1
|
||||
|
||||
The resonance frequency shifts with external magnetic field B:
|
||||
|
||||
```
|
||||
f± = D ± γₑB
|
||||
```
|
||||
|
||||
Where:
|
||||
- D = 2.87 GHz (zero-field splitting)
|
||||
- γₑ = 28 GHz/T (electron gyromagnetic ratio)
|
||||
- B = external magnetic field component along NV axis
|
||||
|
||||
For a 1 fT field: Δf = 28 × 10⁻¹⁵ GHz = 28 μHz — extraordinarily small, requiring
|
||||
long integration times or ensemble measurements.
|
||||
|
||||
### 2.3 Sensitivity Fundamentals
|
||||
|
||||
**Single NV center**: Limited by photon shot noise
|
||||
```
|
||||
η_single ≈ (ℏ/gₑμ_B) × (1/√(C² × R × T₂*))
|
||||
```
|
||||
Where C is ODMR contrast (~0.03), R is photon count rate (~10⁵/s), T₂* is inhomogeneous
|
||||
dephasing time (~1 μs in bulk diamond).
|
||||
|
||||
Typical single NV sensitivity: ~1 μT/√Hz — insufficient for neural signals.
|
||||
|
||||
**NV ensemble**: N centers improve sensitivity by √N
|
||||
```
|
||||
η_ensemble = η_single / √N
|
||||
```
|
||||
|
||||
For N = 10¹² NV centers in a 100 μm × 100 μm × 10 μm sensing volume:
|
||||
η_ensemble ≈ 1 pT/√Hz
|
||||
|
||||
**State of the art (2025–2026)**: Laboratory demonstrations have achieved:
|
||||
- 1–10 fT/√Hz using large diamond chips with optimized NV density
|
||||
- Sub-pT/√Hz using advanced dynamical decoupling sequences
|
||||
- ~100 aT/√Hz projected with quantum-enhanced protocols (squeezed states)
|
||||
|
||||
### 2.4 Dynamical Decoupling for Neural Frequency Bands
|
||||
|
||||
Neural signals occupy specific frequency bands. Pulsed measurement protocols can be tuned
|
||||
to these bands:
|
||||
|
||||
| Protocol | Sensitivity Band | Application |
|
||||
|----------|-----------------|-------------|
|
||||
| Ramsey interferometry | DC–10 Hz | Infraslow oscillations |
|
||||
| Hahn echo | 10–100 Hz | Alpha, beta rhythms |
|
||||
| CPMG (N pulses) | f = N/(2τ) | Tunable narrowband |
|
||||
| XY-8 sequence | Narrowband, robust | Specific frequency targeting |
|
||||
| KDD (Knill DD) | Broadband | General neural activity |
|
||||
|
||||
**CPMG for alpha rhythm detection (10 Hz)**:
|
||||
- Set interpulse spacing τ = 1/(2 × 10 Hz) = 50 ms
|
||||
- N = 100 pulses → total sensing time = 5 s
|
||||
- Achieved sensitivity: ~10 fT/√Hz in laboratory conditions
|
||||
|
||||
### 2.5 T₁ and T₂ Relaxation Times
|
||||
|
||||
| Parameter | Bulk Diamond | Thin Film | Nanodiamonds |
|
||||
|-----------|-------------|-----------|--------------|
|
||||
| T₁ (spin-lattice) | ~6 ms | ~1 ms | ~10 μs |
|
||||
| T₂ (spin-spin) | ~1.8 ms | ~100 μs | ~1 μs |
|
||||
| T₂* (inhomogeneous) | ~10 μs | ~1 μs | ~100 ns |
|
||||
|
||||
Longer T₂ enables better sensitivity. Electronic-grade CVD diamond with low nitrogen
|
||||
concentration ([N] < 1 ppb) achieves the best T₂ values.
|
||||
|
||||
---
|
||||
|
||||
## 3. Neural Magnetic Field Sources
|
||||
|
||||
### 3.1 Origins of Neural Magnetic Fields
|
||||
|
||||
Neurons generate magnetic fields through two mechanisms:
|
||||
|
||||
1. **Intracellular currents**: Ionic flow (Na⁺, K⁺, Ca²⁺) along axons and dendrites during
|
||||
action potentials and synaptic activity. These are the primary sources measured by MEG.
|
||||
|
||||
2. **Transmembrane currents**: Ionic currents crossing the cell membrane during depolarization
|
||||
and repolarization. Generate weaker, more localized fields.
|
||||
|
||||
The magnetic field from a current dipole at distance r:
|
||||
|
||||
```
|
||||
B(r) = (μ₀/4π) × (Q × r̂)/(r²)
|
||||
```
|
||||
|
||||
Where Q is the current dipole moment (A·m) and μ₀ = 4π × 10⁻⁷ T·m/A.
|
||||
|
||||
### 3.2 Signal Magnitudes
|
||||
|
||||
| Source | Current Dipole | Field at Scalp | Field at 6mm |
|
||||
|--------|---------------|----------------|--------------|
|
||||
| Single neuron | ~0.02 pA·m | ~0.01 fT | ~0.1 fT |
|
||||
| Cortical column (~10⁴ neurons) | ~10 nA·m | ~10–100 fT | ~50–500 fT |
|
||||
| Evoked response (~10⁶ neurons) | ~10 μA·m | ~50–200 fT | ~200–1000 fT |
|
||||
| Epileptic spike | ~100 μA·m | ~500–5000 fT | ~2000–20000 fT |
|
||||
| Alpha rhythm | ~20 μA·m | ~50–200 fT | ~200–800 fT |
|
||||
|
||||
**Key insight for NV sensors**: At 6mm standoff (close proximity, like OPM), signals are
|
||||
3–5× stronger than at scalp surface measurements typical of SQUID MEG (20–30mm gap).
|
||||
NV arrays mounted directly on the scalp benefit from this proximity gain.
|
||||
|
||||
### 3.3 Frequency Bands
|
||||
|
||||
| Band | Frequency | Typical Amplitude (scalp) | Neural Correlate |
|
||||
|------|-----------|--------------------------|------------------|
|
||||
| Delta | 1–4 Hz | 50–200 fT | Deep sleep, pathology |
|
||||
| Theta | 4–8 Hz | 30–100 fT | Memory, navigation |
|
||||
| Alpha | 8–13 Hz | 50–200 fT | Inhibition, idling |
|
||||
| Beta | 13–30 Hz | 20–80 fT | Motor planning, attention |
|
||||
| Gamma | 30–100 Hz | 10–50 fT | Perception, binding |
|
||||
| High-gamma | >100 Hz | 5–20 fT | Local cortical processing |
|
||||
|
||||
**Sensitivity requirement**: To detect all bands, the sensor needs ~5–10 fT/√Hz sensitivity
|
||||
in the 1–200 Hz range. Current NV ensembles are approaching this in laboratory conditions.
|
||||
|
||||
### 3.4 Why Magnetic Fields Are Better Than Electric Fields for Topology
|
||||
|
||||
EEG measures electric potentials at the scalp. The skull acts as a volume conductor that
|
||||
severely smears the spatial distribution, limiting source localization to ~10–20 mm.
|
||||
|
||||
Magnetic fields pass through the skull nearly unattenuated (skull has permeability μ ≈ μ₀).
|
||||
This preserves spatial information, enabling source localization to ~2–5 mm with dense
|
||||
sensor arrays.
|
||||
|
||||
For brain network topology analysis, this spatial resolution difference is critical:
|
||||
- At 20 mm resolution (EEG): can distinguish ~20 brain regions
|
||||
- At 3–5 mm resolution (NV/OPM): can distinguish ~100–400 brain regions
|
||||
- More regions = more detailed connectivity graph = more precise mincut analysis
|
||||
|
||||
---
|
||||
|
||||
## 4. Sensor Architecture for Neural Imaging
|
||||
|
||||
### 4.1 Single NV vs Ensemble NV
|
||||
|
||||
| Configuration | Sensitivity | Spatial Resolution | Use Case |
|
||||
|--------------|-------------|-------------------|----------|
|
||||
| Single NV | ~1 μT/√Hz | ~10 nm | Nanoscale imaging (not neural) |
|
||||
| Small ensemble (10⁶) | ~1 nT/√Hz | ~1 μm | Cellular-scale |
|
||||
| Large ensemble (10¹²) | ~1 pT/√Hz | ~100 μm | Neural macroscale |
|
||||
| Optimized ensemble | ~1–10 fT/√Hz | ~1 mm | Neural imaging (target) |
|
||||
|
||||
For brain topology analysis, large ensemble sensors with ~1 mm spatial resolution are the
|
||||
correct target. Single-NV experiments are scientifically interesting but irrelevant for
|
||||
whole-brain network monitoring.
|
||||
|
||||
### 4.2 Diamond Chip Fabrication
|
||||
|
||||
**CVD (Chemical Vapor Deposition) Growth**:
|
||||
1. Start with high-purity diamond substrate (Element Six, Applied Diamond)
|
||||
2. Grow epitaxial diamond layer with controlled nitrogen incorporation
|
||||
3. Target NV density: 10¹⁶–10¹⁷ cm⁻³ (balance sensitivity vs T₂)
|
||||
4. Irradiate with electrons or protons to create vacancies
|
||||
5. Anneal at 800–1200°C to mobilize vacancies to nitrogen sites
|
||||
6. Surface treatment to stabilize NV⁻ charge state
|
||||
|
||||
**Chip dimensions**: Typical sensing element: 2×2×0.5 mm diamond chip
|
||||
**Array fabrication**: Multiple chips mounted on flexible PCB for conformal sensor arrays
|
||||
|
||||
### 4.3 Optical Readout System
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Green Laser (532 nm, 100 mW) │
|
||||
│ │ │
|
||||
│ ┌────────▼────────┐ │
|
||||
│ │ Diamond Chip │ │
|
||||
│ │ (NV ensemble) │──── Microwave│
|
||||
│ └────────┬────────┘ Drive │
|
||||
│ │ │
|
||||
│ ┌────────▼────────┐ │
|
||||
│ │ Dichroic Filter │ │
|
||||
│ │ (pass >637 nm) │ │
|
||||
│ └────────┬────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────▼────────┐ │
|
||||
│ │ Photodetector │ │
|
||||
│ │ (Si APD/PIN) │ │
|
||||
│ └────────┬────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────▼────────┐ │
|
||||
│ │ Lock-in / ADC │ │
|
||||
│ └─────────────────┘ │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Power budget per sensor**: Laser ~100 mW, microwave ~10 mW, electronics ~50 mW
|
||||
**Total**: ~160 mW per sensing element
|
||||
|
||||
### 4.4 Gradiometer Configurations
|
||||
|
||||
Environmental magnetic noise (urban: ~100 nT fluctuations) is 10⁸× larger than neural
|
||||
signals. Noise rejection is essential.
|
||||
|
||||
**First-order gradiometer**: Two NV sensors separated by ~5 cm
|
||||
```
|
||||
Signal = Sensor_near - Sensor_far
|
||||
```
|
||||
Rejects uniform background fields. Retains neural signals (which have steep spatial gradient).
|
||||
|
||||
**Second-order gradiometer**: Three sensors in line
|
||||
```
|
||||
Signal = Sensor_near - 2×Sensor_mid + Sensor_far
|
||||
```
|
||||
Rejects uniform fields AND linear gradients.
|
||||
|
||||
**Synthetic gradiometry**: Software-based, using reference sensors away from the head.
|
||||
More flexible than hardware gradiometers.
|
||||
|
||||
### 4.5 Array Configurations
|
||||
|
||||
**Linear array**: 8–16 sensors along a line. Good for slice imaging.
|
||||
**2D planar array**: 8×8 = 64 sensors on flat surface. Good for one brain region.
|
||||
**Helmet conformal**: 64–256 sensors on 3D-printed helmet. Full-head coverage.
|
||||
|
||||
For topology analysis, helmet conformal arrays are required to simultaneously measure
|
||||
all brain regions.
|
||||
|
||||
---
|
||||
|
||||
## 5. Comparison with Traditional SQUID MEG
|
||||
|
||||
### 5.1 Head-to-Head Comparison
|
||||
|
||||
| Parameter | SQUID MEG | NV Diamond (Current) | NV Diamond (Projected 2028) |
|
||||
|-----------|-----------|---------------------|---------------------------|
|
||||
| Sensitivity | 3–5 fT/√Hz | 10–100 fT/√Hz | 1–10 fT/√Hz |
|
||||
| Bandwidth | DC–1000 Hz | DC–1000 Hz | DC–1000 Hz |
|
||||
| Operating temp | 4 K (liquid He) | 300 K (room temp) | 300 K |
|
||||
| Cryogenics | Required ($50K/year He) | None | None |
|
||||
| Sensor-scalp gap | 20–30 mm | ~3–6 mm | ~3–6 mm |
|
||||
| Spatial resolution | 3–5 mm | 1–3 mm (projected) | 1–3 mm |
|
||||
| Channels | 275–306 | 4–64 (current) | 128–256 |
|
||||
| System cost | $2–5M | $50–200K (projected) | $20–100K |
|
||||
| Portability | Fixed installation | Potentially wearable | Wearable |
|
||||
| Maintenance | High (cryogen refills) | Low | Low |
|
||||
| Setup time | 30–60 min | <5 min (projected) | <5 min |
|
||||
|
||||
### 5.2 Proximity Advantage
|
||||
|
||||
The most significant practical advantage of NV sensors: they can be placed directly on the
|
||||
scalp. SQUID sensors sit inside a dewar with a ~20–30 mm gap between sensor and scalp.
|
||||
|
||||
Magnetic field from a dipole falls as 1/r³. Moving from 25 mm to 6 mm standoff:
|
||||
```
|
||||
Signal gain = (25/6)³ ≈ 72×
|
||||
```
|
||||
|
||||
This 72× proximity gain partially compensates for NV's lower intrinsic sensitivity.
|
||||
Effective comparison:
|
||||
- SQUID at 25 mm: 5 fT/√Hz sensitivity, signal attenuated by distance
|
||||
- NV at 6 mm: 50 fT/√Hz sensitivity, but 72× stronger signal
|
||||
|
||||
Net SNR comparison: roughly comparable for cortical sources.
|
||||
|
||||
### 5.3 Cost Trajectory
|
||||
|
||||
| Year | SQUID MEG System | NV Array System (est.) |
|
||||
|------|-----------------|----------------------|
|
||||
| 2020 | $3M | N/A (lab only) |
|
||||
| 2024 | $3.5M | $500K (research prototype) |
|
||||
| 2026 | $4M | $200K (multi-channel) |
|
||||
| 2028 | $4M+ | $50–100K (clinical prototype) |
|
||||
| 2030 | $4M+ | $20–50K (production) |
|
||||
|
||||
The cost crossover point is approaching. NV systems will likely be 10–100× cheaper than
|
||||
SQUID MEG within 5 years.
|
||||
|
||||
---
|
||||
|
||||
## 6. Signal Processing Pipeline
|
||||
|
||||
### 6.1 Raw ODMR Signal to Magnetic Field
|
||||
|
||||
1. **Continuous-wave ODMR**: Sweep microwave frequency, measure fluorescence
|
||||
- Simple but limited bandwidth (~100 Hz)
|
||||
- Sensitivity: ~100 pT/√Hz
|
||||
|
||||
2. **Pulsed ODMR (Ramsey)**: Initialize → free precession → readout
|
||||
- Better sensitivity, tunable bandwidth
|
||||
- Sensitivity: ~1 pT/√Hz
|
||||
|
||||
3. **Dynamical decoupling (CPMG/XY-8)**: Multiple π-pulses during precession
|
||||
- Narrowband, highest sensitivity
|
||||
- Sensitivity: ~10 fT/√Hz (demonstrated)
|
||||
- Tunable to specific neural frequency bands
|
||||
|
||||
### 6.2 Multi-Channel Processing
|
||||
|
||||
For a 128-channel NV array:
|
||||
- Each channel: continuous magnetic field time series at 1–10 kHz sampling
|
||||
- Data rate: 128 × 10 kHz × 32 bit = ~5 MB/s
|
||||
- Real-time processing: band-pass filtering, artifact rejection, source localization
|
||||
|
||||
### 6.3 Beamforming with NV Arrays
|
||||
|
||||
Dense NV arrays enable beamforming (spatial filtering):
|
||||
|
||||
```
|
||||
Virtual sensor output = Σᵢ wᵢ × sensorᵢ(t)
|
||||
```
|
||||
|
||||
Where weights wᵢ are computed to maximize sensitivity to a specific brain location while
|
||||
suppressing signals from other locations.
|
||||
|
||||
**LCMV (Linearly Constrained Minimum Variance) beamformer**:
|
||||
```
|
||||
w = (C⁻¹ × L) / (L^T × C⁻¹ × L)
|
||||
```
|
||||
Where C is the data covariance matrix and L is the lead field vector for the target location.
|
||||
|
||||
NV's high spatial density enables better beamformer performance than sparse SQUID arrays.
|
||||
|
||||
### 6.4 Source Localization
|
||||
|
||||
From sensor-space measurements to brain-space current estimates:
|
||||
|
||||
1. **Forward model**: Given brain anatomy (from MRI), compute expected sensor measurements
|
||||
for a unit current at each brain location. Stored as lead field matrix L.
|
||||
|
||||
2. **Inverse solution**: Given sensor measurements B, estimate brain currents J:
|
||||
```
|
||||
J = L^T(LL^T + λI)⁻¹B (minimum-norm estimate)
|
||||
```
|
||||
|
||||
3. **Parcellation**: Map continuous source space to discrete brain regions (68–400 parcels)
|
||||
|
||||
4. **Connectivity**: Compute coupling between parcels → graph edges → mincut analysis
|
||||
|
||||
---
|
||||
|
||||
## 7. Integration with RuVector Architecture
|
||||
|
||||
### 7.1 Data Flow: NV Sensor → Brain Topology Graph
|
||||
|
||||
```
|
||||
NV Array (128 ch, 1 kHz)
|
||||
│
|
||||
▼
|
||||
Preprocessing (filter, artifact rejection)
|
||||
│
|
||||
▼
|
||||
Source Localization (128 sensors → 86 parcels)
|
||||
│
|
||||
▼
|
||||
Connectivity Estimation (PLV, coherence per parcel pair)
|
||||
│
|
||||
▼
|
||||
Brain Graph G(t) = (V=86 parcels, E=weighted connections)
|
||||
│
|
||||
▼
|
||||
RuVector Embedding (graph → 256-d vector)
|
||||
│
|
||||
▼
|
||||
Dynamic Mincut Analysis (partition detection)
|
||||
│
|
||||
▼
|
||||
State Classification / Anomaly Detection
|
||||
```
|
||||
|
||||
### 7.2 Mapping to Existing RuVector Modules
|
||||
|
||||
| RuVector Module | Neural Application |
|
||||
|----------------|-------------------|
|
||||
| `ruvector-temporal-tensor` | Store sequential brain graph snapshots |
|
||||
| `ruvector-mincut` | Compute brain network minimum cut |
|
||||
| `ruvector-attn-mincut` | Attention-weighted brain region importance |
|
||||
| `ruvector-attention` | Spatial attention across sensor array |
|
||||
| `ruvector-solver` | Sparse interpolation for source reconstruction |
|
||||
|
||||
### 7.3 Real-Time Processing Budget
|
||||
|
||||
| Stage | Latency | Computation |
|
||||
|-------|---------|-------------|
|
||||
| Sensor readout | 1 ms | Hardware |
|
||||
| Preprocessing | 2 ms | FIR filtering (SIMD) |
|
||||
| Source localization | 5 ms | Matrix multiply (86×128) |
|
||||
| Connectivity (1 band) | 10 ms | Pairwise coherence (86²/2 pairs) |
|
||||
| Graph embedding | 3 ms | GNN forward pass |
|
||||
| Mincut | 2 ms | Stoer-Wagner on 86 nodes |
|
||||
| **Total** | **~23 ms** | **Real-time capable** |
|
||||
|
||||
### 7.4 Hybrid WiFi CSI + NV Magnetic Sensing
|
||||
|
||||
WiFi CSI provides macro-level body pose and room-scale activity detection.
|
||||
NV magnetometers provide neural state information.
|
||||
|
||||
**Temporal alignment**: Neural signals (mincut topology changes) precede motor output
|
||||
by 200–500 ms. WiFi CSI detects the actual movement. Combining both:
|
||||
|
||||
```
|
||||
t = -300 ms: NV detects motor cortex network reorganization (mincut change)
|
||||
t = -100 ms: NV detects motor command formation (further topology shift)
|
||||
t = 0 ms: WiFi CSI detects actual body movement
|
||||
```
|
||||
|
||||
This enables **predictive** body tracking: RuView knows the person will move before
|
||||
the movement physically occurs.
|
||||
|
||||
---
|
||||
|
||||
## 8. Real-Time Neural Current Flow Mapping
|
||||
|
||||
### 8.1 Current Density Imaging
|
||||
|
||||
From magnetic field measurements, reconstruct current density in the brain:
|
||||
|
||||
```
|
||||
J(r) = -σ∇V(r) + J_p(r)
|
||||
```
|
||||
|
||||
Where J_p is the primary (neural) current and σ∇V is the volume current.
|
||||
|
||||
Minimum-norm current estimation provides a smooth current density map that can be
|
||||
updated at each time point, creating a movie of current flow.
|
||||
|
||||
### 8.2 Connectivity Graph Construction from Current Flow
|
||||
|
||||
For each pair of brain parcels (i, j), compute:
|
||||
|
||||
1. **Phase Locking Value**: PLV(i,j) = |⟨exp(jΔφᵢⱼ(t))⟩|
|
||||
2. **Coherence**: Coh(i,j,f) = |Sᵢⱼ(f)|² / (Sᵢᵢ(f) × Sⱼⱼ(f))
|
||||
3. **Granger causality**: GC(i→j) = ln(var(jₜ|j_past) / var(jₜ|j_past, i_past))
|
||||
|
||||
Each metric produces edge weights for the brain connectivity graph.
|
||||
|
||||
### 8.3 Temporal Resolution Advantage
|
||||
|
||||
| Technology | Time Resolution | Network Changes Visible |
|
||||
|-----------|----------------|------------------------|
|
||||
| fMRI | 2 seconds | Slow state transitions |
|
||||
| EEG | 1 ms | Fast dynamics (poor spatial) |
|
||||
| SQUID MEG | 1 ms | Fast dynamics (fixed position) |
|
||||
| OPM | 5 ms | Fast dynamics (wearable) |
|
||||
| NV Diamond | 1 ms | Fast dynamics (dense array, wearable) |
|
||||
|
||||
NV's combination of high temporal resolution AND dense spatial sampling is unique.
|
||||
|
||||
---
|
||||
|
||||
## 9. State of the Art (2024–2026)
|
||||
|
||||
### 9.1 Leading Research Groups
|
||||
|
||||
**MIT/Harvard**: Walsworth group — pioneered NV magnetometry, demonstrated cellular-scale
|
||||
magnetic imaging, working on macroscale neural sensing arrays.
|
||||
|
||||
**University of Stuttgart**: Wrachtrup group — single NV defect spectroscopy, advanced
|
||||
dynamical decoupling protocols for NV magnetometry.
|
||||
|
||||
**University of Melbourne**: Hollenberg group — NV-based quantum sensing for biological
|
||||
applications, diamond fabrication optimization.
|
||||
|
||||
**NIST Boulder**: NV ensemble magnetometry with optimized readout, approaching fT sensitivity.
|
||||
|
||||
**UC Berkeley**: Budker group — NV magnetometry for fundamental physics and biomedical
|
||||
applications.
|
||||
|
||||
### 9.2 Commercial NV Sensor Companies
|
||||
|
||||
| Company | Product | Sensitivity | Price Range |
|
||||
|---------|---------|-------------|-------------|
|
||||
| Qnami | ProteusQ (scanning) | ~1 μT/√Hz | $200K+ |
|
||||
| QZabre | NV microscope | ~100 nT/√Hz | $150K+ |
|
||||
| Element Six | Electronic-grade diamond | Material supplier | $1K–10K/chip |
|
||||
| QDTI | Quantum diamond devices | ~10 nT/√Hz | Custom |
|
||||
| NVision | NV-enhanced NMR | ~1 nT/√Hz | Custom |
|
||||
|
||||
**Note**: No company currently sells a neural-grade NV magnetometer (fT sensitivity).
|
||||
This is a gap in the market and an opportunity.
|
||||
|
||||
### 9.3 Recent Key Publications
|
||||
|
||||
- Demonstration of NV ensemble sensitivity reaching 10 fT/√Hz in laboratory conditions
|
||||
(multiple groups, 2024–2025)
|
||||
- NV diamond arrays for magnetic microscopy of biological samples
|
||||
- Theoretical proposals for NV-based MEG replacement systems
|
||||
- Integration of NV sensors with CMOS readout electronics
|
||||
|
||||
### 9.4 Remaining Challenges
|
||||
|
||||
| Challenge | Current Status | Required | Timeline |
|
||||
|-----------|---------------|----------|----------|
|
||||
| Sensitivity | 10–100 fT/√Hz | 1–10 fT/√Hz | 2–3 years |
|
||||
| Channel count | 1–4 | 64–256 | 3–5 years |
|
||||
| Laser power near head | ~100 mW/sensor | Thermal safety validated | 1–2 years |
|
||||
| Diamond quality at scale | Research-grade | Reproducible production | 2–3 years |
|
||||
| Real-time processing | Offline analysis | <50 ms end-to-end | 1–2 years |
|
||||
|
||||
---
|
||||
|
||||
## 10. Portable MEG-Style Brain Imaging
|
||||
|
||||
### 10.1 Form Factor Target
|
||||
|
||||
**Helmet design**: 3D-printed shell conforming to head shape
|
||||
- NV diamond chips mounted in helmet surface
|
||||
- Optical fibers deliver green laser light to each chip
|
||||
- Red fluorescence collected via fibers to centralized photodetectors
|
||||
- Microwave drive via printed striplines in helmet
|
||||
|
||||
**Weight budget**:
|
||||
| Component | Weight |
|
||||
|-----------|--------|
|
||||
| Diamond chips (128) | ~10 g |
|
||||
| Optical fibers | ~100 g |
|
||||
| Helmet shell | ~300 g |
|
||||
| Electronics PCBs | ~200 g |
|
||||
| **Total helmet** | **~610 g** |
|
||||
| Processing unit (backpack) | ~2 kg |
|
||||
|
||||
### 10.2 Power Requirements
|
||||
|
||||
| Component | Power |
|
||||
|-----------|-------|
|
||||
| Laser source (shared, split to 128 channels) | 5 W |
|
||||
| Microwave generation (shared) | 2 W |
|
||||
| Photodetectors + amplifiers | 3 W |
|
||||
| FPGA/processor | 5 W |
|
||||
| **Total** | **~15 W** |
|
||||
|
||||
Battery operation: 15 W × 2 hours = 30 Wh → ~200g lithium battery. Feasible for
|
||||
portable operation.
|
||||
|
||||
### 10.3 Projected Timeline
|
||||
|
||||
| Year | Milestone |
|
||||
|------|-----------|
|
||||
| 2026 | 8-channel NV bench prototype, fT sensitivity demonstrated |
|
||||
| 2027 | 32-channel NV array in shielded room |
|
||||
| 2028 | 64-channel NV helmet prototype |
|
||||
| 2029 | First wearable NV-MEG with active shielding |
|
||||
| 2030 | Clinical-grade NV-MEG system |
|
||||
|
||||
---
|
||||
|
||||
## 11. Detection of Subtle Connectivity Changes
|
||||
|
||||
### 11.1 Neuroplasticity Tracking
|
||||
|
||||
Learning physically changes brain connectivity. NV arrays with sufficient sensitivity
|
||||
could track these changes:
|
||||
|
||||
- **Motor learning**: Strengthening of motor-cerebellar connections over practice sessions
|
||||
- **Language learning**: Reorganization of language network topology
|
||||
- **Skill acquisition**: Transition from effortful (distributed) to automated (focal) processing
|
||||
|
||||
Mincut signature: as a skill is learned, the task-relevant network becomes more tightly
|
||||
integrated (lower internal mincut) and more separated from task-irrelevant networks
|
||||
(higher cross-network mincut).
|
||||
|
||||
### 11.2 Pathological Connectivity Changes
|
||||
|
||||
Early connectivity disruption before clinical symptoms:
|
||||
|
||||
| Disease | Connectivity Change | Mincut Signature | Detection Window |
|
||||
|---------|-------------------|------------------|-----------------|
|
||||
| Alzheimer's | DMN fragmentation | Increasing mc(DMN) | 5–10 years before symptoms |
|
||||
| Parkinson's | Motor loop disruption | mc(motor) asymmetry | 3–5 years before symptoms |
|
||||
| Epilepsy | Local hypersynchrony | Decreasing mc(focus) | Minutes to hours before seizure |
|
||||
| Depression | DMN over-integration | Decreasing mc(DMN) | During episode |
|
||||
| Schizophrenia | Global disorganization | Abnormal mc variance | During active phase |
|
||||
|
||||
### 11.3 Sensitivity Requirements for Clinical Detection
|
||||
|
||||
To detect a 10% change in connectivity (clinically meaningful threshold):
|
||||
- Need to resolve edge weight changes of ~10% of baseline
|
||||
- Baseline PLV typically 0.2–0.8 between connected regions
|
||||
- 10% change: ΔPLV ≈ 0.02–0.08
|
||||
- Required sensor SNR: >10 dB in the relevant frequency band
|
||||
- Translates to: ~5–10 fT/√Hz sensor sensitivity for cortical sources
|
||||
|
||||
This is achievable with projected NV technology within 2–3 years.
|
||||
|
||||
---
|
||||
|
||||
## 12. Technical Challenges
|
||||
|
||||
### 12.1 Standoff Distance
|
||||
|
||||
Diamond chips sit on the scalp surface, ~10–15 mm from cortex (scalp tissue + skull).
|
||||
Deep brain structures (hippocampus, thalamus, basal ganglia) are 50–80 mm away.
|
||||
|
||||
Signal at these distances:
|
||||
- Cortex (10 mm): ~50–200 fT → detectable
|
||||
- Hippocampus (60 mm): ~0.1–1 fT → at noise floor
|
||||
- Brainstem (80 mm): ~0.01–0.1 fT → below detection
|
||||
|
||||
**Implication**: NV sensors are primarily cortical topology monitors. Deep structure
|
||||
topology requires either invasive sensing or indirect inference from cortical measurements.
|
||||
|
||||
### 12.2 Diamond Quality and Reproducibility
|
||||
|
||||
NV magnetometry performance depends critically on diamond quality:
|
||||
- Nitrogen concentration: needs [N] < 1 ppb for long T₂
|
||||
- NV density: balance between signal strength and T₂ degradation
|
||||
- Crystal strain: inhomogeneous strain broadens ODMR linewidth
|
||||
- Surface termination: affects NV⁻ charge stability
|
||||
|
||||
Current production variability: ~2× variation in T₂ between nominally identical chips.
|
||||
This needs to improve for standardized multi-channel systems.
|
||||
|
||||
### 12.3 Laser Heating
|
||||
|
||||
100 mW of green laser per sensor × 128 sensors = 12.8 W total optical power near the head.
|
||||
Even with fiber delivery, some heating occurs:
|
||||
|
||||
- Fiber-coupled: minimal heating at head (<1°C)
|
||||
- Free-space illumination: potentially dangerous without thermal management
|
||||
- Safety standard: IEC 62471 limits for skin exposure
|
||||
|
||||
**Solution**: Fiber-coupled laser delivery with reflective diamond chip mounting to direct
|
||||
waste heat away from scalp.
|
||||
|
||||
### 12.4 Bandwidth vs Sensitivity Tradeoff
|
||||
|
||||
Dynamical decoupling achieves best sensitivity in narrow frequency bands. Neural signals
|
||||
span 1–200 Hz. Options:
|
||||
|
||||
1. **Multiplexed measurement**: Rapidly switch between DD sequences tuned to different bands.
|
||||
Reduces effective sensitivity per band by √N_bands.
|
||||
|
||||
2. **Broadband measurement**: Use less aggressive DD (shorter sequences). Lower peak
|
||||
sensitivity but covers all bands simultaneously.
|
||||
|
||||
3. **Parallel sensors**: Dedicate different sensor subsets to different frequency bands.
|
||||
Requires more sensors but maintains sensitivity in each band.
|
||||
|
||||
Option 3 is most compatible with dense NV arrays and neural topology analysis (which
|
||||
benefits from simultaneous multi-band measurement).
|
||||
|
||||
---
|
||||
|
||||
## 13. Roadmap for NV Neural Magnetometry
|
||||
|
||||
### Phase 1: Characterization (2026–2027)
|
||||
- Build 8-channel NV array
|
||||
- Demonstrate fT-level sensitivity on bench
|
||||
- Validate with known magnetic phantom sources
|
||||
- Characterize noise sources and rejection methods
|
||||
- Cost: ~$100K
|
||||
|
||||
### Phase 2: Neural Validation (2027–2028)
|
||||
- 32-channel NV array in magnetically shielded room
|
||||
- Record alpha rhythm from human subject
|
||||
- Compare with simultaneous SQUID-MEG or OPM recording
|
||||
- Demonstrate source localization accuracy
|
||||
- Cost: ~$300K
|
||||
|
||||
### Phase 3: Prototype System (2028–2029)
|
||||
- 64-channel NV helmet with active shielding
|
||||
- Real-time connectivity graph construction
|
||||
- Demonstrate mincut-based cognitive state detection
|
||||
- First integration with RuVector pipeline
|
||||
- Cost: ~$500K
|
||||
|
||||
### Phase 4: Clinical Prototype (2029–2030)
|
||||
- 128-channel NV-MEG helmet
|
||||
- Portable form factor (helmet + backpack)
|
||||
- Validated against clinical SQUID-MEG
|
||||
- First clinical topology biomarker studies
|
||||
- Regulatory consultation
|
||||
- Cost: ~$1M
|
||||
|
||||
### Phase 5: Production System (2030+)
|
||||
- Manufactured NV arrays (cost target: <$500/chip)
|
||||
- Clinical-grade software pipeline
|
||||
- Normative topology database
|
||||
- Regulatory submission
|
||||
- Commercial deployment
|
||||
- Target system cost: $20–50K
|
||||
|
||||
---
|
||||
|
||||
## 14. Ethical and Safety Framework
|
||||
|
||||
### 14.1 Non-Invasive Nature
|
||||
|
||||
NV magnetometry is completely non-invasive:
|
||||
- No ionizing radiation
|
||||
- No strong magnetic fields (unlike MRI)
|
||||
- No electrical stimulation
|
||||
- Laser power is fiber-coupled, not directly incident on tissue
|
||||
- No known biological effects from measurement process
|
||||
|
||||
### 14.2 Privacy Considerations
|
||||
|
||||
**What NV neural sensors CAN detect**: brain network topology states (focused, relaxed,
|
||||
stressed, fatigued), pathological patterns, cognitive load level.
|
||||
|
||||
**What they CANNOT detect**: specific thoughts, memories, intentions, private mental content.
|
||||
|
||||
The topology-based approach is inherently privacy-preserving: it measures HOW the brain
|
||||
is organized, not WHAT it is computing. This is analogous to measuring traffic patterns
|
||||
in a city without reading anyone's mail.
|
||||
|
||||
### 14.3 Regulatory Classification
|
||||
|
||||
- FDA: likely Class II medical device (diagnostic aid) for clinical applications
|
||||
- No surgical risk, non-invasive, non-ionizing
|
||||
- 510(k) pathway with SQUID-MEG as predicate device
|
||||
- Additional pathway for wellness/consumer applications (lower regulatory burden)
|
||||
|
||||
---
|
||||
|
||||
## 15. Conclusion
|
||||
|
||||
NV diamond magnetometers represent the most promising medium-term technology for portable,
|
||||
affordable, high-resolution neural magnetic field measurement. While current sensitivity
|
||||
(10–100 fT/√Hz) is not yet sufficient for all neural applications, the trajectory toward
|
||||
1–10 fT/√Hz within 2–3 years makes NV a credible path to clinical-grade brain topology
|
||||
monitoring.
|
||||
|
||||
For the RuVector + dynamic mincut architecture, NV sensors offer:
|
||||
1. **Dense arrays** enabling detailed connectivity graph construction
|
||||
2. **Room-temperature operation** for wearable/portable form factors
|
||||
3. **Cost trajectory** enabling wide deployment
|
||||
4. **Spatial resolution** sufficient for 100+ brain parcel connectivity analysis
|
||||
5. **Temporal resolution** sufficient for real-time topology tracking
|
||||
|
||||
The combination of NV sensor arrays with RuVector graph memory and dynamic mincut analysis
|
||||
could create the first portable brain network topology observatory — measuring how cognition
|
||||
organizes itself in real time, without requiring the $3M SQUID MEG systems that currently
|
||||
dominate neuroimaging.
|
||||
|
||||
---
|
||||
|
||||
*This document is part of the RF Topological Sensing research series. It surveys
|
||||
nitrogen-vacancy diamond magnetometry technology and its application to neural current
|
||||
detection for brain network topology analysis.*
|
||||
@@ -0,0 +1,731 @@
|
||||
# State-of-the-Art Neural Decoding Landscape (2023–2026)
|
||||
|
||||
## SOTA Research Document — RF Topological Sensing Series (21/22)
|
||||
|
||||
**Date**: 2026-03-09
|
||||
**Domain**: Neural Decoding × Generative AI × Brain-Computer Interfaces × Quantum Sensing
|
||||
**Status**: Research Survey / Strategic Positioning
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
The field of neural decoding has undergone a phase transition between 2023 and 2026. Three
|
||||
technologies stacked together — sensors, decoders, and visualization/reconstruction systems —
|
||||
have collectively moved "brain reading" from science fiction to engineering challenge. Yet the
|
||||
popular narrative obscures a critical distinction: current systems decode *perceived* and
|
||||
*intended* content from neural activity, not arbitrary private thoughts.
|
||||
|
||||
This document maps the current state of the art across all three layers, positions the
|
||||
RuVector + dynamic mincut architecture within this landscape, and identifies the unexplored
|
||||
territory where topological brain modeling could open an entirely new research direction.
|
||||
|
||||
---
|
||||
|
||||
## 2. Layer 1: Neural Sensors — The Fidelity Floor
|
||||
|
||||
Everything in neural decoding is bounded by sensor fidelity. No algorithm can extract
|
||||
information that the sensor never captured.
|
||||
|
||||
### 2.1 Invasive Neural Interfaces (Highest Fidelity)
|
||||
|
||||
**Technology**: Microelectrode arrays implanted directly in brain tissue.
|
||||
|
||||
**Leading Systems**:
|
||||
- **Neuralink N1**: 1,024 electrodes on flexible threads, wireless telemetry
|
||||
- **Stanford BrainGate**: Utah microelectrode arrays (96 channels) in motor cortex
|
||||
- **ECoG grids**: Electrocorticography strips placed on cortical surface
|
||||
|
||||
**Capabilities Demonstrated**:
|
||||
- Decode speech intentions from motor cortex with ~74% accuracy (Stanford, 2023)
|
||||
- Control computer cursors and robotic arms in real time
|
||||
- Decode imagined handwriting at 90+ characters per minute
|
||||
- Reconstruct inner speech patterns from speech motor cortex
|
||||
|
||||
**Signal Characteristics**:
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Spatial resolution | Single neuron (~10 μm) |
|
||||
| Temporal resolution | Sub-millisecond |
|
||||
| Channel count | 96–1,024 |
|
||||
| Signal-to-noise ratio | 5–20 dB per neuron |
|
||||
| Coverage area | ~4×4 mm per array |
|
||||
| Bandwidth | DC to 10 kHz |
|
||||
|
||||
**Fundamental Limitation**: Requires brain surgery. Coverage area is tiny relative to the
|
||||
whole brain (~0.001% of cortical surface per array). Each implant covers one small patch.
|
||||
Network-level topology analysis requires coverage of many regions simultaneously — the exact
|
||||
opposite of what implants provide.
|
||||
|
||||
**Why This Matters for Mincut Architecture**: Implants give depth but not breadth. Dynamic
|
||||
mincut analysis of brain network topology requires simultaneous observation of dozens to
|
||||
hundreds of brain regions. This fundamentally favors non-invasive, whole-brain sensors.
|
||||
|
||||
### 2.2 Functional Magnetic Resonance Imaging (fMRI)
|
||||
|
||||
**Technology**: Measures blood-oxygen-level-dependent (BOLD) signal as proxy for neural
|
||||
activity.
|
||||
|
||||
**Signal Characteristics**:
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Spatial resolution | 1–3 mm voxels |
|
||||
| Temporal resolution | ~0.5–2 Hz (hemodynamic delay ~5–7 seconds) |
|
||||
| Coverage | Whole brain |
|
||||
| Cost | $2–5M per scanner |
|
||||
| Portability | None (fixed installation, 5+ ton magnet) |
|
||||
| Subject constraints | Must lie still in bore |
|
||||
|
||||
**Key Neural Decoding Results (2023–2026)**:
|
||||
- **Semantic decoding of continuous language** (Tang et al., 2023, University of Texas):
|
||||
Decoded continuous language from fMRI recordings of subjects listening to stories. Used
|
||||
GPT-based language model to map brain activity to word sequences. Achieved meaningful
|
||||
semantic recovery of story content, though not verbatim word-for-word accuracy.
|
||||
|
||||
- **Visual reconstruction** (Takagi & Nishimoto, 2023): High-fidelity reconstruction of
|
||||
viewed images from fMRI using latent diffusion models. Structural layout and semantic
|
||||
content recognizable, though fine details are lost.
|
||||
|
||||
- **Imagined image reconstruction**: Researchers achieved ~90% identification accuracy for
|
||||
seen images and ~75% for imagined images in constrained paradigms.
|
||||
|
||||
**Limitation for Topology Analysis**: The 5–7 second hemodynamic delay means fMRI cannot
|
||||
capture fast network topology transitions. Cognitive state changes that occur on millisecond
|
||||
timescales are invisible to fMRI. The technology is fundamentally a slow integrator, averaging
|
||||
neural activity over seconds.
|
||||
|
||||
### 2.3 Electroencephalography (EEG)
|
||||
|
||||
**Technology**: Scalp electrodes measuring voltage fluctuations from cortical neural activity.
|
||||
|
||||
**Signal Characteristics**:
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Spatial resolution | ~10–20 mm (severely blurred by skull) |
|
||||
| Temporal resolution | 1–1000 Hz |
|
||||
| Channel count | 32–256 |
|
||||
| Cost | $1K–50K |
|
||||
| Portability | High (wearable caps available) |
|
||||
| Setup time | 15–45 minutes |
|
||||
|
||||
**Neural Decoding Status**:
|
||||
- Motor imagery classification: 70–85% accuracy for 2–4 classes
|
||||
- P300-based BCI: reliable for character selection at ~5 characters/minute
|
||||
- Emotion recognition: 60–75% accuracy (limited by spatial resolution)
|
||||
- Cognitive workload detection: 80–90% accuracy in binary classification
|
||||
|
||||
**Limitation**: Skull conductivity smears spatial information severely. The volume conduction
|
||||
problem means that EEG measures a blurred weighted sum of many cortical sources. Source
|
||||
localization is ill-conditioned. Fine-grained network topology analysis is fundamentally
|
||||
limited by this spatial ambiguity.
|
||||
|
||||
### 2.4 Magnetoencephalography (MEG)
|
||||
|
||||
**Technology**: Measures magnetic fields generated by neuronal currents.
|
||||
|
||||
**Traditional SQUID-MEG**:
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Sensitivity | 3–5 fT/√Hz |
|
||||
| Spatial resolution | 3–5 mm (source localization) |
|
||||
| Temporal resolution | DC to 1000+ Hz |
|
||||
| Channel count | 275–306 |
|
||||
| Cost | $2–5M + $200K–2M shielded room |
|
||||
| Size | Fixed installation, liquid helium cooling |
|
||||
| Sensor-to-scalp distance | 20–30 mm (helmet gap) |
|
||||
|
||||
**Key Advantage for Topology Analysis**: MEG provides both high temporal resolution
|
||||
(millisecond) AND reasonable spatial resolution (millimeter-scale source localization). This
|
||||
combination is ideal for tracking dynamic network topology. Magnetic fields pass through the
|
||||
skull without distortion, unlike EEG.
|
||||
|
||||
**Emerging: OPM-MEG** (see Section 2.5)
|
||||
|
||||
### 2.5 Optically Pumped Magnetometers (OPMs)
|
||||
|
||||
**Technology**: Alkali vapor cells detect magnetic fields through spin-precession of
|
||||
optically pumped atoms. Operates in SERF (spin-exchange relaxation-free) regime for maximum
|
||||
sensitivity.
|
||||
|
||||
**Signal Characteristics**:
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Sensitivity | 7–15 fT/√Hz (on-head) |
|
||||
| Spatial resolution | ~3–5 mm |
|
||||
| Temporal resolution | DC to 200 Hz |
|
||||
| Sensor size | ~12×12×19 mm per channel |
|
||||
| Cost per sensor | $5K–15K |
|
||||
| Cryogenics | None (room temperature) |
|
||||
| Wearable | Yes (3D-printed helmets) |
|
||||
| Movement tolerance | High (subjects can move) |
|
||||
|
||||
**Why OPM is the Most Important Near-Term Sensor for This Architecture**:
|
||||
|
||||
1. **Wearable**: subjects can move naturally, enabling ecological paradigms
|
||||
2. **Close proximity**: sensor directly on scalp (~6 mm gap vs ~25 mm for SQUID)
|
||||
3. **Better SNR**: closer sensors → 2–3× better signal-to-noise ratio
|
||||
4. **Scalable**: add channels incrementally
|
||||
5. **Cost trajectory**: full system potentially $50K–200K vs $2M+ for SQUID
|
||||
6. **Temporal resolution**: millisecond-scale network dynamics visible
|
||||
7. **Spatial resolution**: adequate for 68–400 brain parcels
|
||||
|
||||
**Leading Groups**:
|
||||
- University of Nottingham / Cerca Magnetics: pioneered wearable OPM-MEG
|
||||
- FieldLine Inc: HEDscan commercial system
|
||||
- QuSpin: Gen-3 QZFM sensor modules
|
||||
|
||||
### 2.6 Quantum Sensors (Frontier)
|
||||
|
||||
**NV Diamond Magnetometers**:
|
||||
- Nitrogen-vacancy defects in diamond detect magnetic fields at femtotesla sensitivity
|
||||
- Room temperature operation, no cryogenics
|
||||
- Potential for miniaturization to chip scale
|
||||
- Current lab sensitivity: ~1–10 fT/√Hz
|
||||
- Advantage: can be fabricated as dense 2D arrays for high spatial resolution
|
||||
- Status: demonstrated in controlled lab conditions, not yet clinical
|
||||
|
||||
**Atomic Interferometers**:
|
||||
- Detect phase shifts in atomic wavefunctions
|
||||
- Extreme precision for magnetic and gravitational fields
|
||||
- Current status: large laboratory instruments
|
||||
- Potential: sub-femtotesla magnetic field measurement
|
||||
- Limitation: low bandwidth (1–10 Hz cycle rate), large apparatus
|
||||
|
||||
### 2.7 Sensor Comparison Matrix
|
||||
|
||||
| Sensor | Spatial Res. | Temporal Res. | Invasive | Portable | Cost | Network Topology Suitability |
|
||||
|--------|-------------|---------------|----------|----------|------|------------------------------|
|
||||
| Implants | 10 μm | <1 ms | Yes | No | $50K+ surgery | Poor (tiny coverage) |
|
||||
| fMRI | 1–3 mm | 0.5 Hz | No | No | $2–5M | Moderate (good spatial, poor temporal) |
|
||||
| EEG | 10–20 mm | 1 kHz | No | Yes | $1–50K | Poor (spatial smearing) |
|
||||
| SQUID-MEG | 3–5 mm | 1 kHz | No | No | $2–5M | Good (but fixed, expensive) |
|
||||
| OPM-MEG | 3–5 mm | 200 Hz | No | Yes | $50–200K | Excellent |
|
||||
| NV Diamond | <1 mm | 1 kHz | No | Potentially | $5–50K | Excellent (when mature) |
|
||||
| Atom Interf. | N/A | 1–10 Hz | No | No | $100K+ | Poor (bandwidth limited) |
|
||||
|
||||
**Conclusion**: OPM-MEG is the clear near-term choice for real-time brain network topology
|
||||
analysis. NV diamond arrays represent the medium-term upgrade path.
|
||||
|
||||
---
|
||||
|
||||
## 3. Layer 2: Neural Decoders — AI Meets Neuroscience
|
||||
|
||||
### 3.1 The Translation Paradigm
|
||||
|
||||
Modern neural decoding frames the problem as machine translation:
|
||||
- **Source language**: brain activity patterns (high-dimensional time series)
|
||||
- **Target language**: text, images, speech, or motor commands
|
||||
- **Translation model**: transformer or diffusion-based neural network
|
||||
|
||||
The pipeline is typically:
|
||||
```
|
||||
Brain signals → Feature extraction → Embedding space → Generative model → Output
|
||||
```
|
||||
|
||||
This paradigm has been remarkably successful for *perceived* content decoding.
|
||||
|
||||
### 3.2 Language Decoding
|
||||
|
||||
**Architecture**: Brain → embedding → language model → text
|
||||
|
||||
**Key Approaches**:
|
||||
|
||||
1. **Brain-to-embedding mapping**: Linear or nonlinear regression from brain activity
|
||||
(fMRI voxels or MEG sensors) to a shared embedding space (e.g., GPT embedding space).
|
||||
|
||||
2. **Embedding-to-text generation**: Pre-trained language model (GPT, LLaMA) generates
|
||||
text conditioned on the brain-derived embedding.
|
||||
|
||||
3. **End-to-end training**: Joint optimization of encoder and decoder, fine-tuned per
|
||||
subject.
|
||||
|
||||
**Results**:
|
||||
| Study | Modality | Task | Performance |
|
||||
|-------|----------|------|-------------|
|
||||
| Tang et al. (2023) | fMRI | Continuous speech decoding | Semantic gist recovery |
|
||||
| Défossez et al. (2023) | MEG/EEG | Speech perception | Word-level identification |
|
||||
| Willett et al. (2023) | Implant | Imagined handwriting | 94 characters/minute |
|
||||
| Metzger et al. (2023) | ECoG | Speech neuroprosthesis | 78 words/minute |
|
||||
|
||||
**Limitation**: All systems require extensive subject-specific training (typically 10–40 hours
|
||||
of calibration data). Cross-subject transfer is minimal. Decoding accuracy drops sharply for
|
||||
novel content not represented in training.
|
||||
|
||||
### 3.3 Image Reconstruction from Brain Activity
|
||||
|
||||
**Architecture**: Brain → latent vector → diffusion model → image
|
||||
|
||||
**Key Approaches**:
|
||||
|
||||
1. **fMRI-to-latent mapping**: Train a regression model from fMRI activation patterns to
|
||||
the latent space of a diffusion model (Stable Diffusion, DALL-E).
|
||||
|
||||
2. **Two-stage reconstruction**:
|
||||
- Stage 1: Decode semantic content (what is in the image)
|
||||
- Stage 2: Decode perceptual content (what it looks like)
|
||||
- Combine via conditional diffusion generation
|
||||
|
||||
3. **Brain Diffuser** (2023): Feeds fMRI representations through a variational autoencoder
|
||||
into a latent diffusion model. Reconstructs viewed images with recognizable structure
|
||||
and semantic content.
|
||||
|
||||
**Results**:
|
||||
- Viewed image reconstruction: structural layout and major objects identifiable
|
||||
- Imagined image reconstruction: ~75% identification accuracy (constrained set)
|
||||
- Cross-subject: poor (each subject needs individual model)
|
||||
|
||||
**What This Actually Recovers**:
|
||||
- High-level category (animal, building, face)
|
||||
- Spatial layout (left/right, center/periphery)
|
||||
- Color palette (approximate)
|
||||
- Semantic associations (beach scene, urban scene)
|
||||
|
||||
**What This Cannot Recover**:
|
||||
- Fine details (text, specific faces, exact objects)
|
||||
- Private imagination (untrained novel content)
|
||||
- Dreams (no training data exists during dreams)
|
||||
|
||||
### 3.4 Speech Synthesis from Neural Activity
|
||||
|
||||
**Architecture**: Motor cortex signals → articulatory model → speech synthesis
|
||||
|
||||
**Key Results**:
|
||||
- ECoG-based speech neuroprostheses decode attempted speech at 78 words/minute
|
||||
- Accuracy reaches 97% for 50-word vocabulary, drops to ~50% for open vocabulary
|
||||
- Real-time operation demonstrated for locked-in patients
|
||||
|
||||
**How This Works**:
|
||||
The motor cortex generates articulatory commands (tongue, lips, jaw, larynx positions) even
|
||||
when paralyzed. Electrodes on the motor cortex surface capture these attempted movements.
|
||||
A neural network maps motor signals to phoneme sequences, then a vocoder generates audio.
|
||||
|
||||
**Relevance to Mincut Architecture**: Speech decoding is a *content* problem. Mincut topology
|
||||
analysis is a *structure* problem. They are complementary, not competing. Mincut would detect
|
||||
when the speech network *activates* (pre-movement topology change), while the decoder would
|
||||
extract *what* is being said.
|
||||
|
||||
### 3.5 The Decoding Boundary
|
||||
|
||||
**What Current Decoders Can Access**:
|
||||
| Category | Accuracy | Modality | Training Required |
|
||||
|----------|----------|----------|-------------------|
|
||||
| Perceived speech (heard) | High | fMRI/ECoG | 10–40 hours |
|
||||
| Intended speech (attempted) | Moderate-High | ECoG/Implant | 10–40 hours |
|
||||
| Viewed images | Moderate | fMRI | 10–20 hours |
|
||||
| Imagined images | Low-Moderate | fMRI | 10–20 hours |
|
||||
| Motor intention (move left/right) | High | EEG/ECoG | 1–5 hours |
|
||||
| Semantic gist of thoughts | Low | fMRI | 10–40 hours |
|
||||
| Arbitrary private thoughts | None | Any | N/A |
|
||||
|
||||
**Why Arbitrary Thought Reading Is Extremely Unlikely**:
|
||||
|
||||
1. **Distributed representation**: Thoughts are encoded across millions of neurons in
|
||||
patterns that are not spatially localized.
|
||||
|
||||
2. **Individual specificity**: The neural code for the same concept differs between
|
||||
individuals. Transfer models fail across subjects.
|
||||
|
||||
3. **Context dependence**: The same neural pattern can represent different things depending
|
||||
on context, state, and history.
|
||||
|
||||
4. **Combinatorial complexity**: The space of possible thoughts is effectively infinite.
|
||||
Training data can never cover it.
|
||||
|
||||
5. **Temporal complexity**: Thoughts are not static patterns but dynamic trajectories
|
||||
through neural state space.
|
||||
|
||||
---
|
||||
|
||||
## 4. Layer 3: Visualization and Reconstruction
|
||||
|
||||
### 4.1 Visual Perception Reconstruction
|
||||
|
||||
**State of the Art Pipeline**:
|
||||
```
|
||||
Brain signal (fMRI/MEG)
|
||||
→ Feature extraction (voxel patterns or sensor topography)
|
||||
→ Embedding (mapped to CLIP or diffusion model latent space)
|
||||
→ Conditional generation (Stable Diffusion or similar)
|
||||
→ Reconstructed image
|
||||
```
|
||||
|
||||
**Meta AI (2023–2024)**: Demonstrated near-real-time reconstruction of visual stimuli from
|
||||
MEG signals. Used a large pre-trained visual model to map MEG topography to image embeddings,
|
||||
then generated images via diffusion. Temporal resolution was sufficient for video-like
|
||||
reconstruction of dynamic visual stimuli.
|
||||
|
||||
**Quality Assessment**:
|
||||
- High-level semantic content: 70–90% match
|
||||
- Spatial layout: 60–80% match
|
||||
- Color and texture: 40–60% match
|
||||
- Fine detail and text: <20% match
|
||||
- Novel/imagined content: 20–40% match
|
||||
|
||||
### 4.2 Speech Reconstruction
|
||||
|
||||
**Pipeline**:
|
||||
```
|
||||
Motor cortex signals (ECoG/Implant)
|
||||
→ Articulatory parameter extraction (tongue, jaw, lip positions)
|
||||
→ Phoneme sequence prediction
|
||||
→ Neural vocoder (WaveNet, HiFi-GAN)
|
||||
→ Synthesized speech audio
|
||||
```
|
||||
|
||||
**Performance**: Natural-sounding speech synthesis from neural signals demonstrated in
|
||||
multiple research groups. Quality sufficient for real-time communication in clinical BCI.
|
||||
|
||||
### 4.3 The Generative AI Amplifier
|
||||
|
||||
**Key Insight**: Generative AI (LLMs, diffusion models) dramatically amplified neural
|
||||
decoding capability by acting as a powerful *prior*. Instead of reconstructing output purely
|
||||
from neural data, the system uses neural data to *guide* a generative model that already
|
||||
knows what text and images look like.
|
||||
|
||||
This means:
|
||||
- **Less neural data needed**: The generative model fills in details
|
||||
- **Higher quality output**: Outputs look natural even with noisy input
|
||||
- **Risk of hallucination**: The model may generate plausible but incorrect content
|
||||
- **Overfitting to priors**: Reconstructions may reflect model biases, not actual thought
|
||||
|
||||
**Implication for Topology Analysis**: The RuVector/mincut approach sidesteps the hallucination
|
||||
problem entirely. It measures *structural properties* of brain activity (network topology,
|
||||
coherence boundaries) rather than trying to generate *content* (images, text). There is no
|
||||
generative prior to hallucinate — the topology either changes or it doesn't.
|
||||
|
||||
---
|
||||
|
||||
## 5. The Hard Limits
|
||||
|
||||
### 5.1 Physical Limits of Non-Invasive Sensing
|
||||
|
||||
**Magnetic field attenuation**: Neural magnetic fields drop as 1/r³ from the source.
|
||||
A cortical current dipole generating 100 fT at the scalp surface produces only ~10 fT at
|
||||
20 mm standoff (SQUID) and ~50 fT at 6 mm standoff (OPM). Deep brain structures (thalamus,
|
||||
hippocampus) generate signals attenuated by 10–100× at the scalp surface.
|
||||
|
||||
**Inverse problem ill-conditioning**: Reconstructing 3D current sources from 2D surface
|
||||
measurements is inherently ill-posed. Regularization is required, which limits spatial
|
||||
resolution. Typical resolution: 5–10 mm for cortical sources, 10–20 mm for deep sources.
|
||||
|
||||
**Noise floor**: Even with quantum sensors achieving fT/√Hz sensitivity, the fundamental
|
||||
noise floor limits signal detection from deep structures and weakly active regions.
|
||||
|
||||
### 5.2 Three Determinants of Decoding Capability
|
||||
|
||||
1. **Sensor fidelity**: Signal-to-noise ratio at the measurement point determines the
|
||||
information ceiling. No algorithm can recover information not captured by the sensor.
|
||||
|
||||
2. **Signal-to-noise ratio**: Environmental noise (urban electromagnetic interference,
|
||||
building vibrations, physiological artifacts) degrades achievable SNR in practice.
|
||||
|
||||
3. **Subject-specific training**: Neural representations are highly individual. Current
|
||||
decoders require 10–40 hours of calibration per subject. This is a fundamental barrier
|
||||
to scalable deployment.
|
||||
|
||||
### 5.3 What Is and Is Not Possible
|
||||
|
||||
**Confidently achievable with current technology**:
|
||||
- Binary cognitive state detection (focused vs. unfocused)
|
||||
- Gross motor intention (left hand vs. right hand)
|
||||
- Sleep stage classification
|
||||
- Epileptic activity detection
|
||||
- Perceived speech semantic gist (with fMRI and extensive training)
|
||||
|
||||
**Achievable with near-term advances (2–5 years)**:
|
||||
- Multi-class cognitive state classification (5–10 states)
|
||||
- Pre-movement intention detection (200–500 ms lead)
|
||||
- Real-time brain network topology visualization
|
||||
- Early neurological disease biomarkers from connectivity analysis
|
||||
- Non-invasive motor BCI with moderate accuracy
|
||||
|
||||
**Extremely unlikely**:
|
||||
- Real-time arbitrary thought reading
|
||||
- Cross-subject decoding without calibration
|
||||
- Covert brain scanning (sensors require cooperation)
|
||||
- Dream content reconstruction with meaningful accuracy
|
||||
|
||||
---
|
||||
|
||||
## 6. Where RuVector + Dynamic Mincut Fits
|
||||
|
||||
### 6.1 The Unexplored Niche
|
||||
|
||||
Most neural decoding research asks: **"What is the brain computing?"**
|
||||
|
||||
The RuVector + mincut architecture asks: **"How is the brain organizing its computation?"**
|
||||
|
||||
This is a fundamentally different question with different:
|
||||
- **Sensor requirements**: needs coverage breadth, not depth (favors non-invasive)
|
||||
- **Temporal requirements**: needs millisecond dynamics (favors MEG/OPM over fMRI)
|
||||
- **Output representation**: graphs and topology, not images or text
|
||||
- **Privacy implications**: measures state, not content
|
||||
|
||||
### 6.2 Positioning in the Landscape
|
||||
|
||||
```
|
||||
CONTENT-FOCUSED STRUCTURE-FOCUSED
|
||||
(What is thought?) (How does thought organize?)
|
||||
───────────────── ──────────────────────────────
|
||||
HIGH FIDELITY Implant BCI [Gap - no one here]
|
||||
Speech neuroprostheses
|
||||
|
||||
MEDIUM FIDELITY fMRI image reconstruction → RuVector + Mincut (OPM) ←
|
||||
fMRI language decoding Dynamic topology analysis
|
||||
|
||||
LOW FIDELITY EEG motor imagery EEG connectivity (basic)
|
||||
P300 BCI
|
||||
```
|
||||
|
||||
The RuVector + mincut architecture occupies the **medium-fidelity, structure-focused** quadrant
|
||||
— a space that is largely unexplored in current research.
|
||||
|
||||
### 6.3 What This Architecture Uniquely Enables
|
||||
|
||||
1. **Real-time network topology tracking**: No existing system monitors brain connectivity
|
||||
graph topology at millisecond resolution in real time.
|
||||
|
||||
2. **Structural transition detection**: Mincut identifies when brain networks reorganize,
|
||||
which correlates with cognitive state changes.
|
||||
|
||||
3. **Longitudinal tracking**: RuVector memory enables tracking of topology evolution over
|
||||
days, weeks, months — detecting gradual changes like neurodegeneration.
|
||||
|
||||
4. **Content-agnostic monitoring**: The system does not need to decode what is being thought.
|
||||
It detects how the brain organizes its processing, which is clinically and scientifically
|
||||
valuable without raising thought-privacy concerns.
|
||||
|
||||
5. **Cross-subject topology comparison**: While neural content representations differ between
|
||||
individuals, network *topology* properties (modularity, hub structure, integration) are
|
||||
more conserved across subjects.
|
||||
|
||||
### 6.4 Integration with Content Decoders
|
||||
|
||||
The topology analysis is complementary to content decoding, not competing:
|
||||
|
||||
```
|
||||
Quantum Sensors → Preprocessing → Source Localization → ┬─ Content Decoder (text/image)
|
||||
├─ Topology Analyzer (mincut)
|
||||
└─ Combined: state-aware decoding
|
||||
```
|
||||
|
||||
**Example**: A speech BCI could use mincut to detect when the speech network *activates*
|
||||
(pre-speech topology change at t = -300ms), then trigger the content decoder only when
|
||||
speech intention is detected. This reduces false activations and improves timing.
|
||||
|
||||
---
|
||||
|
||||
## 7. Neural Foundation Models
|
||||
|
||||
### 7.1 Emerging Direction
|
||||
|
||||
Training large models directly on brain data (analogous to LLMs trained on text):
|
||||
- **Brain-GPT** concepts: pre-train on large neural datasets, fine-tune per subject
|
||||
- **Cross-modal alignment**: align brain activity embeddings with CLIP/GPT embeddings
|
||||
- **Self-supervised learning**: predict masked brain regions from surrounding activity
|
||||
|
||||
### 7.2 Relevance to Topology Analysis
|
||||
|
||||
Foundation models could learn brain topology patterns from large datasets:
|
||||
- Pre-train on thousands of subjects' connectivity graphs
|
||||
- Learn universal topology transition patterns
|
||||
- Transfer: adapt to new subjects with minimal calibration
|
||||
- Enable cross-subject topology comparison in a shared embedding space
|
||||
|
||||
This is where RuVector's contrastive learning (AETHER) and geometric embedding become
|
||||
particularly valuable — they provide the representational framework for topology foundation
|
||||
models.
|
||||
|
||||
---
|
||||
|
||||
## 8. Five Landmark "Mind Reading" Experiments
|
||||
|
||||
### 8.1 Gallant Lab Visual Reconstruction (UC Berkeley, 2011)
|
||||
|
||||
**What they did**: Reconstructed movie clips from fMRI brain activity. Subjects watched movie
|
||||
trailers in an MRI scanner. A decoder predicted which of 1,000 random YouTube clips best
|
||||
matched the brain activity at each moment.
|
||||
|
||||
**Result**: Blurry but recognizable reconstructions of viewed video.
|
||||
|
||||
**Significance**: First demonstration that dynamic visual experience could be decoded from
|
||||
brain activity.
|
||||
|
||||
### 8.2 Tang et al. Continuous Language Decoder (UT Austin, 2023)
|
||||
|
||||
**What they did**: Decoded continuous speech from fMRI while subjects listened to stories.
|
||||
Used GPT-based language model to map fMRI activity to word sequences.
|
||||
|
||||
**Result**: Recovered semantic meaning of stories (not verbatim words).
|
||||
|
||||
**Significance**: First open-vocabulary language decoder from non-invasive imaging. Crucially,
|
||||
decoding failed when subjects were not cooperating — they could defeat the decoder by
|
||||
thinking about other things.
|
||||
|
||||
### 8.3 Takagi & Nishimoto Image Reconstruction (2023)
|
||||
|
||||
**What they did**: Fed fMRI patterns into a latent diffusion model (Stable Diffusion) to
|
||||
reconstruct viewed images.
|
||||
|
||||
**Result**: Recognizable reconstructions with correct semantic content and approximate layout.
|
||||
|
||||
**Significance**: Generative AI dramatically improved reconstruction quality over previous
|
||||
approaches.
|
||||
|
||||
### 8.4 Willett et al. Imagined Handwriting (Stanford, 2021)
|
||||
|
||||
**What they did**: Decoded imagined handwriting from motor cortex implant. Subject imagined
|
||||
writing letters; a neural network decoded the intended characters.
|
||||
|
||||
**Result**: 94.1 characters per minute with 94.1% accuracy (with language model correction).
|
||||
|
||||
**Significance**: Demonstrated that motor cortex retains detailed movement representations
|
||||
even years after paralysis.
|
||||
|
||||
### 8.5 Meta AI Real-Time MEG Reconstruction (2023–2024)
|
||||
|
||||
**What they did**: Trained a model to reconstruct viewed images from MEG signals in near
|
||||
real time.
|
||||
|
||||
**Result**: Decoded visual category and approximate layout with sub-second latency.
|
||||
|
||||
**Significance**: First demonstration of MEG-based visual decoding approaching real-time
|
||||
speed. MEG's temporal resolution enabled tracking of dynamic visual processing.
|
||||
|
||||
---
|
||||
|
||||
## 9. Strategic Implications for RuView Architecture
|
||||
|
||||
### 9.1 What the SOTA Map Tells Us
|
||||
|
||||
1. **Content decoding is advancing rapidly** but remains subject-specific and perception-bound.
|
||||
2. **Non-invasive sensors are reaching sufficient fidelity** for network-level analysis.
|
||||
3. **Generative AI amplifies decoding** but introduces hallucination risks.
|
||||
4. **Topology analysis is the unexplored dimension** — no major group is doing real-time
|
||||
mincut-based brain network analysis.
|
||||
5. **OPM-MEG is the enabling technology** — wearable, high-fidelity, affordable trajectory.
|
||||
|
||||
### 9.2 Recommended Architecture Priorities
|
||||
|
||||
| Priority | Rationale |
|
||||
|----------|-----------|
|
||||
| OPM-MEG integration first | Most mature quantum sensor, sufficient for network topology |
|
||||
| Real-time mincut pipeline | Unique capability, no competition |
|
||||
| RuVector longitudinal tracking | Clinical value for disease monitoring |
|
||||
| Content decoder integration later | Let others solve content; focus on topology |
|
||||
| NV diamond upgrade path | Higher spatial resolution when technology matures |
|
||||
|
||||
### 9.3 Competitive Landscape
|
||||
|
||||
**Who else is working on brain network topology?**
|
||||
|
||||
- **Graph neural network approaches**: Several groups apply GNNs to brain connectivity data,
|
||||
but primarily for static classification (disease vs. healthy), not real-time dynamic
|
||||
topology tracking.
|
||||
|
||||
- **Connectome analysis**: Human Connectome Project provides structural connectivity maps,
|
||||
but these are static (one scan per subject).
|
||||
|
||||
- **Dynamic functional connectivity (dFC)**: fMRI-based studies examine time-varying
|
||||
connectivity, but at ~0.5 Hz temporal resolution — too slow for real-time cognitive
|
||||
tracking.
|
||||
|
||||
- **No one is doing real-time mincut on brain networks from MEG/OPM data.** This is
|
||||
genuinely unexplored territory.
|
||||
|
||||
---
|
||||
|
||||
## 10. The Topological Difference
|
||||
|
||||
The critical reframing that separates this architecture from the mainstream neural decoding
|
||||
field:
|
||||
|
||||
**Mainstream Neural Decoding**:
|
||||
```
|
||||
Brain activity → What is the content? → Generate text/image/speech
|
||||
```
|
||||
- Requires subject-specific training
|
||||
- Limited to perceived/intended content
|
||||
- Raises profound privacy concerns
|
||||
- Subject can defeat the decoder by not cooperating
|
||||
|
||||
**Topological Brain Analysis (This Architecture)**:
|
||||
```
|
||||
Brain activity → How is the network organized? → Track topology changes
|
||||
```
|
||||
- More conserved across subjects (topology > content)
|
||||
- Measures cognitive state, not content
|
||||
- Privacy-preserving by design
|
||||
- Cannot be easily defeated (topology is involuntary)
|
||||
- Clinically valuable (disease signatures)
|
||||
- Scientifically novel (unexplored direction)
|
||||
|
||||
This is not a weaker version of mind reading. It is a fundamentally different measurement
|
||||
that reveals aspects of brain function that content decoders cannot access.
|
||||
|
||||
---
|
||||
|
||||
## 11. Conclusion
|
||||
|
||||
The 2023–2026 SOTA landscape shows that neural decoding has made remarkable progress on
|
||||
content recovery from brain activity, driven by the convergence of better sensors (OPM),
|
||||
better algorithms (transformers, diffusion models), and better training data. Yet this
|
||||
progress has not addressed the fundamental question of how cognition organizes itself
|
||||
topologically.
|
||||
|
||||
The RuVector + dynamic mincut architecture positions itself in this gap — not competing with
|
||||
content decoders but opening an entirely new dimension of brain observation. Combined with
|
||||
OPM quantum sensors, this becomes a "topological brain observatory" that measures the
|
||||
architecture of thought rather than its content.
|
||||
|
||||
The sensor fidelity is nearly sufficient. The algorithms exist. The software architecture
|
||||
(RuVector, mincut, temporal tracking) maps directly from the existing RF sensing codebase.
|
||||
The application space (clinical diagnostics, cognitive monitoring, BCI augmentation) is
|
||||
commercially viable.
|
||||
|
||||
The question is no longer "can this work?" but "who will build it first?"
|
||||
|
||||
---
|
||||
|
||||
## 12. References and Further Reading
|
||||
|
||||
### Sensor Technology
|
||||
- Boto et al. (2018). "Moving magnetoencephalography towards real-world applications with a
|
||||
wearable system." Nature.
|
||||
- Barry et al. (2020). "Sensitivity optimization for NV-diamond magnetometry." Reviews of
|
||||
Modern Physics.
|
||||
- Tierney et al. (2019). "Optically pumped magnetometers: From quantum origins to
|
||||
multi-channel magnetoencephalography." NeuroImage.
|
||||
|
||||
### Neural Decoding
|
||||
- Tang et al. (2023). "Semantic reconstruction of continuous language from non-invasive brain
|
||||
recordings." Nature Neuroscience.
|
||||
- Takagi & Nishimoto (2023). "High-resolution image reconstruction with latent diffusion
|
||||
models from human brain activity." CVPR.
|
||||
- Défossez et al. (2023). "Decoding speech perception from non-invasive brain recordings."
|
||||
Nature Machine Intelligence.
|
||||
|
||||
### Brain Network Analysis
|
||||
- Bullmore & Sporns (2009). "Complex brain networks: graph theoretical analysis." Nature
|
||||
Reviews Neuroscience.
|
||||
- Bassett & Sporns (2017). "Network neuroscience." Nature Neuroscience.
|
||||
- Vidaurre et al. (2018). "Spontaneous cortical activity transiently organises into frequency
|
||||
specific phase-coupling networks." Nature Communications.
|
||||
|
||||
### Visual Reconstruction
|
||||
- Nishimoto et al. (2011). "Reconstructing visual experiences from brain activity evoked by
|
||||
natural movies." Current Biology.
|
||||
- Ozcelik & VanRullen (2023). "Natural scene reconstruction from fMRI signals using
|
||||
generative latent diffusion." Scientific Reports.
|
||||
|
||||
### Speech BCI
|
||||
- Willett et al. (2021). "High-performance brain-to-text communication via handwriting."
|
||||
Nature.
|
||||
- Metzger et al. (2023). "A high-performance neuroprosthesis for speech decoding and avatar
|
||||
control." Nature.
|
||||
|
||||
---
|
||||
|
||||
*This document is part of the RF Topological Sensing research series. It positions the
|
||||
RuVector + dynamic mincut architecture within the 2023–2026 neural decoding landscape,
|
||||
identifying the unexplored niche of real-time brain network topology analysis.*
|
||||
@@ -0,0 +1,877 @@
|
||||
# Brain State Observatory — Ten Application Domains
|
||||
|
||||
## SOTA Research Document — RF Topological Sensing Series (22/22)
|
||||
|
||||
**Date**: 2026-03-09
|
||||
**Domain**: Clinical Diagnostics × BCI × Cognitive Science × Commercial Applications
|
||||
**Status**: Applications Roadmap / Strategic Analysis
|
||||
|
||||
---
|
||||
|
||||
## 1. Introduction — Not Mind Reading, Something Better
|
||||
|
||||
If you build a system that combines high-sensitivity neural sensing, RuVector-style geometric
|
||||
memory, and dynamic mincut topology analysis, you are not building a mind reader. You are
|
||||
building a **brain state observatory**.
|
||||
|
||||
The most valuable applications are not "reading thoughts." They are systems that measure how
|
||||
cognition organizes itself over time — and detect when that organization goes wrong.
|
||||
|
||||
This document maps ten application domains where the RuVector + dynamic mincut architecture
|
||||
becomes unusually powerful, with honest assessment of feasibility, market reality, and
|
||||
technical requirements for each.
|
||||
|
||||
---
|
||||
|
||||
## 2. Domain 1: Neurological Disease Detection
|
||||
|
||||
### 2.1 Clinical Need
|
||||
|
||||
Neurological diseases are diagnosed late. By the time symptoms are visible:
|
||||
- Alzheimer's: 40–60% of neurons in affected regions are already dead
|
||||
- Parkinson's: 60–80% of dopaminergic neurons in substantia nigra are lost
|
||||
- Epilepsy: seizures may have been building for years before clinical onset
|
||||
- Multiple Sclerosis: demyelination is often widespread before first relapse
|
||||
|
||||
The fundamental problem: structural damage is detectable only after it becomes severe.
|
||||
Functional network changes precede structural damage by years.
|
||||
|
||||
### 2.2 How Mincut Detects Disease
|
||||
|
||||
Each neurological condition has a characteristic topology signature:
|
||||
|
||||
**Alzheimer's Disease**:
|
||||
- Progressive disconnection of the default mode network (DMN)
|
||||
- Loss of hub connectivity (especially posterior cingulate, medial prefrontal)
|
||||
- Increased graph fragmentation → mincut value decreases over months/years
|
||||
- Mincut tracking detects gradual network dissolution before clinical symptoms
|
||||
|
||||
Topology signature:
|
||||
```
|
||||
Healthy: mc(DMN) = 0.82 ± 0.05 (strongly integrated)
|
||||
Prodromal: mc(DMN) = 0.61 ± 0.08 (beginning to fragment)
|
||||
Clinical: mc(DMN) = 0.34 ± 0.12 (severely fragmented)
|
||||
```
|
||||
|
||||
**Epilepsy**:
|
||||
- Pre-ictal phase: abnormal hypersynchronization of local networks
|
||||
- Focal region becomes increasingly connected internally while disconnecting from surround
|
||||
- Mincut detects the pre-seizure topology: high local coupling, low global integration
|
||||
- Prediction window: 30 seconds to 5 minutes before seizure onset
|
||||
|
||||
Topology signature:
|
||||
```
|
||||
Inter-ictal: mc(focus) = 0.45 mc(global) = 0.72
|
||||
Pre-ictal: mc(focus) = 0.12 mc(global) = 0.83 ← focus isolating
|
||||
Ictal: mc(focus) = 0.03 mc(global) = 0.95 ← hypersync
|
||||
```
|
||||
|
||||
**Parkinson's Disease**:
|
||||
- Disruption of basal ganglia–cortical motor loops
|
||||
- Beta oscillation network topology changes
|
||||
- Asymmetric degradation (one hemisphere typically leads)
|
||||
- Mincut across motor network correlates with motor symptom severity
|
||||
|
||||
**Traumatic Brain Injury (TBI)**:
|
||||
- Acute: diffuse disconnection, globally elevated mincut
|
||||
- Recovery: gradual re-integration of network modules
|
||||
- Chronic: persistent topology abnormalities correlate with cognitive deficits
|
||||
- Mincut tracking provides objective recovery metric
|
||||
|
||||
### 2.3 Clinical Implementation
|
||||
|
||||
**Input**: Neural signals from OPM-MEG or NV magnetometer array
|
||||
**Processing**: Dynamic connectivity graph → mincut analysis → longitudinal tracking
|
||||
**Output**: Network integrity report, early warning alerts, progression tracking
|
||||
|
||||
**Regulatory Pathway**: Medical device (FDA 510(k) or De Novo for diagnostic aid)
|
||||
- Predicate devices: existing MEG diagnostic systems
|
||||
- Clinical validation: prospective cohort studies comparing mincut biomarkers to
|
||||
established diagnostic criteria
|
||||
- Timeline: 3–5 years from first prototype to regulatory submission
|
||||
|
||||
### 2.4 Market Reality
|
||||
|
||||
Hospitals spend billions annually on diagnostic neuroimaging (MRI, CT, PET). Current tools
|
||||
provide structural images or slow functional snapshots (fMRI). No tool provides real-time
|
||||
functional network topology monitoring.
|
||||
|
||||
**Market size estimates**:
|
||||
| Application | Annual Market | Current Gap |
|
||||
|-------------|-------------|-------------|
|
||||
| Alzheimer's diagnostics | $6B globally | No early functional biomarker |
|
||||
| Epilepsy monitoring | $2B globally | Poor seizure prediction |
|
||||
| TBI assessment | $1.5B globally | No objective recovery metric |
|
||||
| Parkinson's monitoring | $1B globally | Limited progression tracking |
|
||||
|
||||
---
|
||||
|
||||
## 3. Domain 2: Brain-Computer Interfaces
|
||||
|
||||
### 3.1 Architecture
|
||||
|
||||
```
|
||||
Neural signals → RuVector embeddings → State memory → Decode intent → Device control
|
||||
```
|
||||
|
||||
### 3.2 Capabilities
|
||||
|
||||
| Application | Signal Source | Accuracy Target | Latency Target |
|
||||
|-------------|-------------|-----------------|----------------|
|
||||
| Prosthetic control | Motor cortex topology | 90%+ for 6 DOF | <100 ms |
|
||||
| Typing/communication | Speech network topology | 95%+ characters | <200 ms |
|
||||
| Computer cursor control | Motor intention states | 95%+ directions | <50 ms |
|
||||
| Environmental control | Cognitive state | 85%+ for 4 commands | <500 ms |
|
||||
|
||||
### 3.3 Topology-Based BCI Advantages
|
||||
|
||||
Traditional BCI decodes amplitude patterns (which neurons fire, how strongly).
|
||||
Topology-based BCI decodes network reorganization patterns.
|
||||
|
||||
**Advantages**:
|
||||
1. **More robust**: Network topology is less variable than amplitude patterns across sessions
|
||||
2. **Self-calibrating**: Topology features normalize automatically (relative, not absolute)
|
||||
3. **State-aware**: Detects when the user is "ready" vs "idle" from network structure
|
||||
4. **Pre-movement detection**: Topology changes precede motor output by 200–500 ms
|
||||
|
||||
**Disadvantage**:
|
||||
- Lower spatial specificity than invasive implants (cannot decode individual finger movements)
|
||||
- Best for categorical commands, not continuous analog control
|
||||
|
||||
### 3.4 Non-Invasive BCI Breakthrough Potential
|
||||
|
||||
Current non-invasive BCI (EEG-based) achieves ~70–85% accuracy for binary classification.
|
||||
The limitation is EEG's poor spatial resolution.
|
||||
|
||||
OPM-MEG + mincut could provide:
|
||||
- Better spatial resolution → more distinguishable states
|
||||
- Topology features that are more stable across sessions
|
||||
- Reduced calibration time (topology patterns are more conserved)
|
||||
- Potential accuracy: 85–95% for 4–8 state classification
|
||||
|
||||
**This could be the first non-invasive BCI that approaches implant-level utility for
|
||||
categorical control tasks.**
|
||||
|
||||
### 3.5 Speech Reconstruction for Paralyzed Patients
|
||||
|
||||
The most impactful near-term BCI application:
|
||||
- Detect speech intention from motor cortex network activation
|
||||
- Classify attempted speech from topology of speech motor network
|
||||
- Combine with language model for error correction
|
||||
- Target: 30–50 words per minute (current ECoG: 78 wpm)
|
||||
|
||||
Even at lower throughput, a non-invasive speech BCI eliminates the need for brain surgery.
|
||||
|
||||
---
|
||||
|
||||
## 4. Domain 3: Cognitive State Monitoring
|
||||
|
||||
### 4.1 Core Capability
|
||||
|
||||
Measure brain network organization to infer mental states without decoding content.
|
||||
|
||||
The system answers: "Is this person focused, fatigued, overloaded, or disengaged?"
|
||||
It does NOT answer: "What is this person thinking about?"
|
||||
|
||||
### 4.2 Metrics
|
||||
|
||||
| Metric | Computation | Cognitive Correlate |
|
||||
|--------|-------------|---------------------|
|
||||
| Global mincut value | Minimum cut of whole-brain graph | Integration level |
|
||||
| Modular structure | Number and size of graph modules | Cognitive mode |
|
||||
| Hub connectivity | Degree centrality of hub regions | Executive function |
|
||||
| Graph entropy | Shannon entropy of edge weight distribution | Cognitive complexity |
|
||||
| Temporal variability | Rate of topology change | Engagement level |
|
||||
| Inter-hemispheric mincut | Left-right partition strength | Lateralized processing |
|
||||
|
||||
### 4.3 Industry Applications
|
||||
|
||||
**Aviation**:
|
||||
- Pilot cognitive workload monitoring
|
||||
- Fatigue detection during long-haul flights
|
||||
- Attention allocation tracking (scan pattern vs focus)
|
||||
- Regulatory interest: FAA/EASA fatigue risk management
|
||||
|
||||
**Military**:
|
||||
- Operator cognitive load in command centers
|
||||
- Fatigue monitoring for extended missions
|
||||
- Stress detection in high-threat environments
|
||||
- DARPA has funded cognitive workload research for decades
|
||||
|
||||
**Spaceflight**:
|
||||
- Astronaut cognitive performance monitoring
|
||||
- Sleep quality assessment in microgravity
|
||||
- Isolation and confinement effects on brain topology
|
||||
- NASA human factors research priorities
|
||||
|
||||
**High-Performance Work**:
|
||||
- Surgeon fatigue monitoring during long procedures
|
||||
- Air traffic controller workload assessment
|
||||
- Nuclear plant operator vigilance monitoring
|
||||
- Financial trading desk cognitive load optimization
|
||||
|
||||
### 4.4 Latency Requirements
|
||||
|
||||
| Application | Max Latency | Consequence of Late Detection |
|
||||
|-------------|-------------|-------------------------------|
|
||||
| Aviation (fatigue alert) | <5 seconds | Delayed warning |
|
||||
| Military (overload) | <2 seconds | Decision error |
|
||||
| Surgery (fatigue) | <10 seconds | Delayed warning |
|
||||
| Industrial safety | <1 second | Accident risk |
|
||||
|
||||
### 4.5 DARPA and NASA Context
|
||||
|
||||
DARPA programs funding cognitive monitoring:
|
||||
- **DARPA N3**: Next-generation non-surgical neurotechnology
|
||||
- **DARPA NESD**: Neural Engineering System Design
|
||||
- **DARPA RAM**: Restoring Active Memory
|
||||
|
||||
NASA research:
|
||||
- Human Research Program: cognitive performance in spaceflight
|
||||
- Behavioral Health and Performance: monitoring astronaut brain function
|
||||
- Gateway lunar station: long-duration crew monitoring needs
|
||||
|
||||
---
|
||||
|
||||
## 5. Domain 4: Mental Health Diagnostics
|
||||
|
||||
### 5.1 The Diagnostic Gap
|
||||
|
||||
Most psychiatric diagnoses rely on subjective questionnaires (PHQ-9, GAD-7, DSM-5 criteria).
|
||||
There are no objective biomarkers for most mental health conditions. This leads to:
|
||||
- Diagnostic uncertainty (40% of depression cases misdiagnosed initially)
|
||||
- Treatment selection by trial-and-error
|
||||
- No objective measure of treatment response
|
||||
- Stigma from perceived subjectivity of diagnosis
|
||||
|
||||
### 5.2 Neural Topology Biomarkers
|
||||
|
||||
Each psychiatric condition has characteristic network topology disruptions:
|
||||
|
||||
**Major Depression**:
|
||||
- Default mode network (DMN) over-integration: abnormally low mincut within DMN
|
||||
- Reduced executive network connectivity
|
||||
- Disrupted DMN–executive network anticorrelation
|
||||
- Topology signature: mc(DMN) low, mc(DMN↔Executive) high
|
||||
|
||||
**Generalized Anxiety**:
|
||||
- Amygdala–prefrontal connectivity disruption
|
||||
- Hyperconnectivity of threat-processing networks
|
||||
- Reduced top-down regulation from prefrontal cortex
|
||||
- Topology signature: abnormal hub structure in salience network
|
||||
|
||||
**PTSD**:
|
||||
- Hippocampal disconnection from cortical networks
|
||||
- Amygdala hyperconnectivity
|
||||
- Disrupted fear extinction network (ventromedial PFC)
|
||||
- Topology signature: fragmented memory encoding network
|
||||
|
||||
**Schizophrenia**:
|
||||
- Global disruption of integration-segregation balance
|
||||
- Reduced small-world properties
|
||||
- Disrupted thalamo-cortical connectivity
|
||||
- Topology signature: globally altered graph metrics
|
||||
|
||||
### 5.3 Treatment Monitoring
|
||||
|
||||
**Antidepressant response tracking**:
|
||||
- Baseline topology assessment before treatment
|
||||
- Weekly/monthly topology monitoring during treatment
|
||||
- Objective measure: is the network topology normalizing?
|
||||
- Predict treatment response from early topology changes (week 1–2)
|
||||
|
||||
**Psychotherapy monitoring**:
|
||||
- Track network changes during cognitive behavioral therapy
|
||||
- Measure: is the DMN–executive anticorrelation restoring?
|
||||
- Objective progress metric for therapist and patient
|
||||
|
||||
### 5.4 Functional Brain Biomarker Platform
|
||||
|
||||
The RuVector + mincut system could become a **general-purpose functional brain biomarker
|
||||
platform**:
|
||||
|
||||
```
|
||||
Patient Assessment Flow:
|
||||
1. 15-minute OPM recording (resting state + brief tasks)
|
||||
2. Real-time connectivity graph construction
|
||||
3. Mincut analysis → topology feature extraction
|
||||
4. Compare to normative database (age/sex matched)
|
||||
5. Generate biomarker report:
|
||||
- Network integration score
|
||||
- Modular structure comparison
|
||||
- Hub connectivity profile
|
||||
- Anomaly flags for specific conditions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Domain 5: Neurofeedback and Brain Training
|
||||
|
||||
### 6.1 Real-Time Feedback Loop
|
||||
|
||||
```
|
||||
Brain activity → Topology analysis → Feedback signal → Cognitive adjustment
|
||||
↑ ↓
|
||||
└──────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 6.2 Applications
|
||||
|
||||
**Focus Training**:
|
||||
- Target: increase frontal-parietal network integration (mincut decrease in attention network)
|
||||
- Feedback: visual/auditory signal indicating network state
|
||||
- Training: 20–30 sessions of 30 minutes each
|
||||
- Evidence: EEG neurofeedback for attention has moderate effect sizes (d = 0.4–0.6)
|
||||
- OPM-based topology feedback could improve by providing more specific targets
|
||||
|
||||
**ADHD Therapy**:
|
||||
- Target: normalize fronto-striatal network connectivity
|
||||
- Current EEG neurofeedback for ADHD: some evidence, controversial
|
||||
- Topology-based approach may be more specific → better outcomes
|
||||
- Insurance coverage potential if clinical trials succeed
|
||||
|
||||
**Stress Reduction**:
|
||||
- Target: reduce amygdala–prefrontal hyperconnectivity
|
||||
- Feedback when topology normalizes toward calm-state pattern
|
||||
- Combine with meditation/breathing guidance
|
||||
- Corporate wellness and clinical stress management
|
||||
|
||||
**Peak Performance Training**:
|
||||
- Target: optimize integration-segregation balance for specific tasks
|
||||
- Elite athletes: motor network optimization
|
||||
- Musicians: auditory-motor coupling refinement
|
||||
- Financial traders: decision network optimization under pressure
|
||||
|
||||
### 6.3 Technical Requirements for Neurofeedback
|
||||
|
||||
| Parameter | Requirement | Current Capability |
|
||||
|-----------|------------|-------------------|
|
||||
| Feedback latency | <250 ms | ~100 ms achievable |
|
||||
| Session duration | 30 minutes | Battery/comfort limits |
|
||||
| Feature stability | <5% variance | Topology features stable |
|
||||
| Wearability | Comfortable helmet | OPM helmets demonstrated |
|
||||
| Home use | Portable setup | Not yet (shielding needed) |
|
||||
|
||||
---
|
||||
|
||||
## 7. Domain 6: Dream and Imagination Reconstruction
|
||||
|
||||
### 7.1 Current State
|
||||
|
||||
**What has been demonstrated**:
|
||||
- fMRI reconstruction of viewed images (waking state) using diffusion models
|
||||
- Basic decoding of imagined visual categories from fMRI
|
||||
- Sleep stage classification from EEG/MEG
|
||||
|
||||
**What has NOT been demonstrated**:
|
||||
- Real-time dream content reconstruction
|
||||
- Imagined scene reconstruction with meaningful detail
|
||||
- Dream-to-image generation
|
||||
|
||||
### 7.2 What Topology Analysis Adds
|
||||
|
||||
Mincut analysis during sleep/dreaming could:
|
||||
- **Map dream network topology**: which brain regions are co-active during dreams?
|
||||
- **Detect lucid dreaming**: characterized by frontal network re-integration
|
||||
- **Track REM vs NREM topology**: distinct network organizations
|
||||
- **Identify replay events**: hippocampal-cortical coupling during memory consolidation
|
||||
|
||||
### 7.3 Brain-to-Art Interface
|
||||
|
||||
Creative application:
|
||||
- Artist wears OPM helmet during ideation
|
||||
- Topology analysis captures network states during creative thought
|
||||
- Map topology states to generative model parameters
|
||||
- Generate visual art that reflects brain network organization (not thought content)
|
||||
- The art represents HOW the brain is organizing, not WHAT it is imagining
|
||||
|
||||
### 7.4 Honest Assessment
|
||||
|
||||
Dream reconstruction remains the most speculative application. Current technology cannot
|
||||
meaningfully decode dream content. Topology analysis during sleep is feasible but interpretation
|
||||
is limited. This domain is 10+ years from practical application.
|
||||
|
||||
---
|
||||
|
||||
## 8. Domain 7: Cognitive Research
|
||||
|
||||
### 8.1 The Scientific Opportunity
|
||||
|
||||
Instead of static brain scans, researchers get continuous graph topology of cognition. This
|
||||
enables entirely new categories of scientific questions.
|
||||
|
||||
### 8.2 Research Questions This Architecture Could Answer
|
||||
|
||||
**How do thoughts form?**
|
||||
- Track topology transitions from idle state to focused cognition
|
||||
- Measure network integration speed and sequence
|
||||
- Compare across individuals, age groups, expertise levels
|
||||
- Temporal resolution: millisecond-by-millisecond topology evolution
|
||||
|
||||
**How do ideas propagate through brain networks?**
|
||||
- Present stimulus → track topology wave propagation
|
||||
- Measure information flow direction from mincut asymmetry
|
||||
- Identify bottleneck regions (high betweenness centrality)
|
||||
- Compare sensory processing paths across modalities
|
||||
|
||||
**How does memory recall reorganize connectivity?**
|
||||
- Cue presentation → hippocampal network activation → cortical reinstatement
|
||||
- Topology signature of successful vs failed recall
|
||||
- Reconsolidation: how does recalled memory modify the network?
|
||||
- Longitudinal: how do memory networks change over weeks?
|
||||
|
||||
**How does creativity emerge?**
|
||||
- Divergent thinking: loosened topology constraints, more random connections
|
||||
- Convergent thinking: tightened topology, focused integration
|
||||
- Creative insight (aha moment): sudden topology reorganization
|
||||
- Compare creative vs non-creative individuals' topology dynamics
|
||||
|
||||
**Developmental neuroscience**:
|
||||
- How do children's brain topologies differ from adults?
|
||||
- Track topology development across childhood and adolescence
|
||||
- Sensitive periods: when do specific network topologies crystallize?
|
||||
- OPM's wearability makes pediatric studies practical
|
||||
|
||||
**Aging and neurodegeneration**:
|
||||
- Healthy aging: gradual topology changes over decades
|
||||
- Pathological aging: accelerated topology degradation
|
||||
- Cognitive reserve: maintained topology despite structural damage
|
||||
- Can topology analysis predict cognitive decline years in advance?
|
||||
|
||||
### 8.3 Methodological Advantages
|
||||
|
||||
| Current Methods | Topology Approach |
|
||||
|----------------|-------------------|
|
||||
| fMRI: 0.5 Hz temporal resolution | OPM: 200+ Hz dynamics |
|
||||
| EEG: poor spatial resolution | OPM: 3–5 mm source localization |
|
||||
| Static connectivity matrices | Dynamic time-varying graphs |
|
||||
| Single-session snapshots | Longitudinal RuVector tracking |
|
||||
| Group-level statistics | Individual topology fingerprints |
|
||||
|
||||
### 8.4 This Is Network Science of Cognition
|
||||
|
||||
The field has studied individual brain regions and pairwise connections. Topology analysis
|
||||
studies the emergent organizational principles — how the whole network self-organizes to
|
||||
produce cognition. This is analogous to studying traffic patterns in a city rather than
|
||||
individual cars.
|
||||
|
||||
---
|
||||
|
||||
## 9. Domain 8: Human-Computer Interaction
|
||||
|
||||
### 9.1 Cognition-Aware Computing
|
||||
|
||||
Computers could adapt their behavior based on the user's cognitive state.
|
||||
|
||||
### 9.2 Applications
|
||||
|
||||
**Adaptive Software Interfaces**:
|
||||
- Detect cognitive overload → simplify interface, reduce information density
|
||||
- Detect high focus → minimize interruptions, defer notifications
|
||||
- Detect confusion → provide contextual help, slow down tutorial pace
|
||||
- Detect fatigue → suggest breaks, reduce task complexity
|
||||
|
||||
**Learning Systems**:
|
||||
- Detect when student is confused (topology disruption in comprehension networks)
|
||||
- Adjust difficulty and presentation style in real time
|
||||
- Identify optimal learning moments (high engagement topology)
|
||||
- Personalize educational content to individual learning topology
|
||||
|
||||
**Immersive Experiences**:
|
||||
- VR/AR systems that respond to cognitive state
|
||||
- Game difficulty that adapts to engagement level
|
||||
- Meditation/mindfulness apps with real-time topology feedback
|
||||
- Therapeutic VR guided by brain network state
|
||||
|
||||
### 9.3 Cognition-Aware Operating System Concept
|
||||
|
||||
```
|
||||
Sensor Layer: OPM headband → continuous topology stream
|
||||
Analysis Layer: Real-time mincut → cognitive state classification
|
||||
OS Layer: CogState API → applications query current state
|
||||
App Layer: Notifications, UI complexity, timing adapt automatically
|
||||
```
|
||||
|
||||
**States the OS tracks**:
|
||||
| State | Topology Signature | OS Action |
|
||||
|-------|-------------------|-----------|
|
||||
| Deep focus | High frontal integration | Block notifications |
|
||||
| Low attention | Fragmented topology | Suggest break |
|
||||
| Creative mode | Loose coupling, high entropy | Expand workspace |
|
||||
| Stress | Amygdala-PFC disruption | Calming UI adjustments |
|
||||
| Fatigue | Reduced graph energy | Reduce complexity |
|
||||
|
||||
### 9.4 Timeline
|
||||
|
||||
- Near-term (1–3 years): Research prototypes in controlled settings
|
||||
- Medium-term (3–7 years): Professional applications (aviation, surgery)
|
||||
- Long-term (7–15 years): Consumer-grade cognition-aware computing
|
||||
|
||||
---
|
||||
|
||||
## 10. Domain 9: Brain Health Monitoring Wearables
|
||||
|
||||
### 10.1 The Brain's Apple Watch
|
||||
|
||||
If sensors become sufficiently small and affordable, continuous brain topology monitoring
|
||||
becomes possible in a wearable form factor.
|
||||
|
||||
### 10.2 Target Device
|
||||
|
||||
**Form factor**: Helmet, headband, or behind-ear device with magnetometer array
|
||||
**Sensors**: 8–32 miniaturized OPM or NV diamond sensors
|
||||
**Processing**: Edge AI chip for real-time topology analysis
|
||||
**Battery**: 8–12 hour operation
|
||||
**Connectivity**: Bluetooth/WiFi to smartphone app
|
||||
**Data**: Continuous topology metrics, alerts, daily reports
|
||||
|
||||
### 10.3 Monitoring Capabilities
|
||||
|
||||
**Sleep Quality**:
|
||||
- Sleep staging from topology transitions (wake → N1 → N2 → N3 → REM)
|
||||
- Sleep architecture quality score
|
||||
- Sleep spindle and slow wave detection
|
||||
- REM density and distribution
|
||||
- Compare to age-matched normative database
|
||||
|
||||
**Brain Health Baseline**:
|
||||
- Monthly topology assessment
|
||||
- Track gradual changes over years
|
||||
- Early warning for neurodegeneration
|
||||
- Concussion detection and recovery monitoring
|
||||
|
||||
**Concussion/TBI Risk**:
|
||||
- Pre-exposure baseline (for athletes, military)
|
||||
- Post-impact assessment: compare topology to baseline
|
||||
- Return-to-play/return-to-duty decision support
|
||||
- Longitudinal tracking during recovery
|
||||
|
||||
**Stress and Mental Health**:
|
||||
- Daily stress topology patterns
|
||||
- Chronic stress detection from sustained topology disruption
|
||||
- Correlation with self-reported well-being
|
||||
- Trigger identification from topology-event correlation
|
||||
|
||||
### 10.4 Technical Barriers to Consumer Deployment
|
||||
|
||||
| Barrier | Current Status | Required for Consumer |
|
||||
|---------|---------------|----------------------|
|
||||
| Sensor size | 12×12×19 mm (OPM) | <5×5×5 mm |
|
||||
| Magnetic shielding | Room or active coils | Integrated micro-shielding |
|
||||
| Power consumption | ~1W per sensor | <100 mW per sensor |
|
||||
| Cost per sensor | $5–15K | <$100 |
|
||||
| Ease of use | Expert setup | Self-applied in <30 seconds |
|
||||
|
||||
**Realistic timeline**: 10–15 years for consumer wearable. Near-term: clinical/professional
|
||||
devices that accept larger form factor.
|
||||
|
||||
---
|
||||
|
||||
## 11. Domain 10: Brain Network Digital Twins
|
||||
|
||||
### 11.1 The Most Advanced Concept
|
||||
|
||||
A digital twin of a person's brain network: a dynamic graph model that captures their unique
|
||||
neural topology and tracks how it evolves over time.
|
||||
|
||||
### 11.2 Architecture
|
||||
|
||||
```
|
||||
Physical Brain: Periodic OPM recordings → topology snapshots
|
||||
Digital Twin: Personalized brain graph model in RuVector
|
||||
├─ Structural connectivity (from MRI/DTI)
|
||||
├─ Functional topology (from OPM, updated periodically)
|
||||
├─ Dynamic model (predict topology transitions)
|
||||
└─ Response model (predict effects of interventions)
|
||||
|
||||
Applications:
|
||||
├─ Track brain aging trajectory
|
||||
├─ Simulate treatment responses
|
||||
├─ Personalize intervention targets
|
||||
├─ Predict cognitive decline
|
||||
└─ Optimize rehabilitation protocols
|
||||
```
|
||||
|
||||
### 11.3 Applications
|
||||
|
||||
**Tracking Brain Aging**:
|
||||
- Build topology trajectory from age 40 onwards
|
||||
- Compare individual trajectory to population norms
|
||||
- Detect accelerated aging patterns
|
||||
- Correlate with lifestyle factors (exercise, sleep, diet, social)
|
||||
- Personalized brain health optimization
|
||||
|
||||
**Simulating Treatment Responses**:
|
||||
- Patient's brain topology model + proposed treatment → predicted outcome
|
||||
- Compare: antidepressant A vs B, which normalizes topology better?
|
||||
- TMS target selection: simulate topology effects of stimulating different regions
|
||||
- Reduce trial-and-error in psychiatric treatment
|
||||
|
||||
**Personalized Neurology**:
|
||||
- Individual topology fingerprint as clinical identifier
|
||||
- Track topology before, during, and after treatment
|
||||
- Adjust treatment based on individual topology response
|
||||
- Enable precision neurology (like precision oncology)
|
||||
|
||||
**Brain Rehabilitation Modeling**:
|
||||
- Stroke recovery: model which topology trajectories lead to best outcomes
|
||||
- TBI rehabilitation: identify when topology has recovered sufficiently
|
||||
- Physical therapy optimization: correlate movement training with topology changes
|
||||
- Cognitive rehabilitation: target specific topology deficits
|
||||
|
||||
### 11.4 Data Requirements
|
||||
|
||||
| Component | Data Source | Frequency | Storage |
|
||||
|-----------|-----------|-----------|---------|
|
||||
| Structural connectome | MRI/DTI | Once (baseline) + yearly | ~1 GB |
|
||||
| Functional topology | OPM recording | Monthly 1-hour sessions | ~2 GB/session |
|
||||
| Dynamic model | Computed from above | Updated per session | ~100 MB |
|
||||
| Longitudinal trajectory | Accumulated | Growing database | ~50 GB/decade |
|
||||
|
||||
### 11.5 RuVector's Role
|
||||
|
||||
RuVector provides the embedding space for storing and comparing brain topology states:
|
||||
- Each session → set of topology embeddings stored in RuVector memory
|
||||
- Nearest-neighbor search: find past states most similar to current
|
||||
- Trajectory analysis: is the topology trajectory trending toward health or disease?
|
||||
- Cross-subject comparison: find patients with similar topology profiles
|
||||
- HNSW indexing: fast retrieval from growing longitudinal database
|
||||
|
||||
---
|
||||
|
||||
## 12. Where Dynamic Mincut Becomes Unique
|
||||
|
||||
### 12.1 Beyond Deep Learning
|
||||
|
||||
Most brain decoding systems use deep learning exclusively: neural signals → neural network →
|
||||
output labels. The model is a black box that maps input patterns to outputs.
|
||||
|
||||
Dynamic mincut adds **structural intelligence**: instead of pattern matching, it computes
|
||||
a mathematically precise property of the brain's connectivity graph.
|
||||
|
||||
### 12.2 The Key Question Shift
|
||||
|
||||
| Traditional Approach | Mincut Approach |
|
||||
|---------------------|-----------------|
|
||||
| "What is the signal?" | "Where does the network break?" |
|
||||
| Pattern matching | Structural analysis |
|
||||
| Requires large training data | Requires graph construction |
|
||||
| Black box | Interpretable (the cut is visible) |
|
||||
| Content-dependent | Content-independent |
|
||||
| Subject-specific | More transferable |
|
||||
|
||||
### 12.3 Interpretability Advantage
|
||||
|
||||
When a deep learning model classifies a brain state, explaining *why* it made that
|
||||
classification is difficult (interpretability problem). When mincut identifies a network
|
||||
partition, the explanation is inherent: "These brain regions disconnected from those brain
|
||||
regions." A clinician can directly inspect the partition and relate it to known functional
|
||||
neuroanatomy.
|
||||
|
||||
### 12.4 Mathematical Properties
|
||||
|
||||
Mincut has well-defined mathematical properties that deep learning lacks:
|
||||
- **Duality**: Max-flow/min-cut theorem provides dual interpretation
|
||||
- **Stability**: small perturbations produce small changes in cut value
|
||||
- **Monotonicity**: adding edges can only decrease mincut
|
||||
- **Submodularity**: enables efficient optimization
|
||||
- **Spectral connection**: Cheeger inequality links cut to graph Laplacian eigenvalues
|
||||
|
||||
These properties provide formal guarantees about the behavior of the analysis, unlike
|
||||
neural network classifiers which can fail unpredictably.
|
||||
|
||||
---
|
||||
|
||||
## 13. The Most Powerful Future Use — Google Maps for Cognition
|
||||
|
||||
### 13.1 The Vision
|
||||
|
||||
A real-time neural topology map. Think of it like Google Maps for the brain:
|
||||
|
||||
| Google Maps | Brain Topology Observatory |
|
||||
|------------|--------------------------|
|
||||
| Roads and highways | Neural pathways |
|
||||
| Traffic flow | Information flow |
|
||||
| Districts and neighborhoods | Functional brain modules |
|
||||
| Traffic jams | Processing bottlenecks |
|
||||
| Road closures | Disconnected pathways |
|
||||
| Construction zones | Reorganizing networks |
|
||||
| Rush hour patterns | Cognitive state patterns |
|
||||
| Navigation routing | Information routing |
|
||||
|
||||
### 13.2 What You Would See
|
||||
|
||||
A real-time display showing:
|
||||
1. **Brain regions** as nodes, colored by activity level
|
||||
2. **Connections** as edges, thickness proportional to coupling strength
|
||||
3. **Module boundaries** highlighted by mincut analysis
|
||||
4. **State transitions** animated as boundaries shift
|
||||
5. **Timeline** showing topology history
|
||||
6. **Anomaly markers** where topology deviates from baseline
|
||||
|
||||
### 13.3 How This Changes Neuroscience
|
||||
|
||||
Current neuroscience is like having satellite photos of a city — you see the buildings but
|
||||
not the traffic. This observatory adds the traffic layer: real-time flow, congestion,
|
||||
routing, and reorganization.
|
||||
|
||||
**Questions that become answerable**:
|
||||
- Which brain networks activate first during decision-making?
|
||||
- How does the network reorganize during insight?
|
||||
- What topology predicts memory formation success?
|
||||
- How does anesthesia progressively disconnect brain modules?
|
||||
- What is the topology of consciousness?
|
||||
|
||||
---
|
||||
|
||||
## 14. Hard Reality Check
|
||||
|
||||
### 14.1 Three Things That Determine Success
|
||||
|
||||
1. **Sensor fidelity**: SNR at the measurement point sets the information ceiling. Current
|
||||
OPMs: 7–15 fT/√Hz, adequate for cortical sources, marginal for deep structures.
|
||||
|
||||
2. **Signal-to-noise ratio in practice**: Environmental noise, physiological artifacts, and
|
||||
movement artifacts degrade achievable SNR. Magnetic shielding is currently required.
|
||||
|
||||
3. **Subject-specific calibration**: While topology features are more transferable than
|
||||
content features, some individual calibration is still needed for source localization
|
||||
and parcellation mapping.
|
||||
|
||||
### 14.2 What Must Improve
|
||||
|
||||
| Technology | Current | Required for Clinical Use | Timeline |
|
||||
|-----------|---------|--------------------------|----------|
|
||||
| OPM sensitivity | 7–15 fT/√Hz | 3–5 fT/√Hz | 2–3 years |
|
||||
| Magnetic shielding | Room-scale | Portable/head-mounted | 5–7 years |
|
||||
| Sensor cost | $5–15K each | $500–1K each | 5–10 years |
|
||||
| Real-time processing | Research prototype | Clinical-grade software | 2–4 years |
|
||||
| Normative database | Small research studies | 10,000+ subjects | 5–8 years |
|
||||
|
||||
### 14.3 Honest Feasibility Assessment
|
||||
|
||||
| Domain | Technical Feasibility | Timeline | Market Size |
|
||||
|--------|---------------------|----------|-------------|
|
||||
| 1. Disease detection | High | 3–5 years to pilot | $10B+ |
|
||||
| 2. BCI | Medium-High | 2–4 years to prototype | $5B |
|
||||
| 3. Cognitive monitoring | High | 1–3 years to demo | $2B |
|
||||
| 4. Mental health dx | Medium | 4–7 years to validate | $8B |
|
||||
| 5. Neurofeedback | Medium-High | 2–4 years to product | $1B |
|
||||
| 6. Dream/imagination | Low | 10+ years | Unknown |
|
||||
| 7. Cognitive research | High | 1–2 years to use | $500M (grants) |
|
||||
| 8. HCI | Medium | 5–10 years to product | $3B |
|
||||
| 9. Wearables | Low-Medium | 10–15 years | $20B+ |
|
||||
| 10. Digital twins | Low-Medium | 7–12 years | $5B+ |
|
||||
|
||||
---
|
||||
|
||||
## 15. Strategic Roadmap
|
||||
|
||||
### Phase 1: Research Platform (Year 1–2)
|
||||
|
||||
**Goal**: Demonstrate real-time brain topology tracking from OPM-MEG data.
|
||||
|
||||
**Deliverables**:
|
||||
- Software pipeline: OPM data → connectivity graph → mincut analysis → visualization
|
||||
- Proof-of-concept: distinguish rest/task/sleep from topology features
|
||||
- RuVector integration: longitudinal topology tracking across sessions
|
||||
- Publication: first paper on real-time mincut-based brain topology analysis
|
||||
|
||||
**Hardware**: 32-channel OPM system in magnetically shielded room
|
||||
**Cost**: ~$200K (sensors) + $300K (shielding) + $100K (computing) = ~$600K
|
||||
**Team**: 3–5 researchers (signal processing, neuroscience, software engineering)
|
||||
|
||||
### Phase 2: Clinical Validation (Year 2–4)
|
||||
|
||||
**Goal**: Validate topology biomarkers against clinical diagnoses.
|
||||
|
||||
**Deliverables**:
|
||||
- Clinical study: 100+ patients with known neurological conditions
|
||||
- Normative database: 500+ healthy controls
|
||||
- Sensitivity/specificity for each disease topology signature
|
||||
- Regulatory pre-submission meeting with FDA
|
||||
|
||||
**Applications to validate**:
|
||||
1. Epilepsy seizure prediction (most clear-cut clinical signal)
|
||||
2. Alzheimer's early detection (largest market need)
|
||||
3. Cognitive workload monitoring (simplest to commercialize)
|
||||
|
||||
### Phase 3: Product Development (Year 3–6)
|
||||
|
||||
**Goal**: First commercial topology monitoring system.
|
||||
|
||||
**Two parallel tracks**:
|
||||
1. **Clinical diagnostic**: OPM + topology software for hospitals
|
||||
2. **Professional monitoring**: simplified system for aviation/military
|
||||
|
||||
**Commercialization priorities**:
|
||||
- Cognitive workload monitoring (defense/aviation contracts) — fastest revenue
|
||||
- Epilepsy topology monitoring (clinical need, clear regulatory path) — largest impact
|
||||
- Brain health assessment (wellness market) — largest eventual market
|
||||
|
||||
### Phase 4: Platform Expansion (Year 5–10)
|
||||
|
||||
**Goal**: General-purpose brain topology platform.
|
||||
|
||||
**Capabilities**:
|
||||
- Digital twin construction and tracking
|
||||
- Treatment response prediction
|
||||
- Neurofeedback with topology targets
|
||||
- Consumer wearable (as sensor technology miniaturizes)
|
||||
|
||||
---
|
||||
|
||||
## 16. Two Strategic Questions
|
||||
|
||||
### Question 1: Research Platform vs. Commercial Product?
|
||||
|
||||
**Answer**: Start as research platform, spin into commercial products.
|
||||
|
||||
The RuVector + mincut core engine is the reusable technology. It should be:
|
||||
- Open-source for research adoption → builds community and validation
|
||||
- Licensed commercially for clinical and professional applications
|
||||
- The research platform generates the clinical evidence needed for commercial products
|
||||
|
||||
### Question 2: Non-Invasive Only vs. Clinical Implant Research?
|
||||
|
||||
**Answer**: Non-invasive first, implant collaboration later.
|
||||
|
||||
**Why non-invasive is the right starting point**:
|
||||
1. Mincut topology analysis needs *breadth* of coverage (many regions), which non-invasive
|
||||
excels at
|
||||
2. Implants provide *depth* (single neuron) but only from tiny patches — the opposite of
|
||||
what topology analysis needs
|
||||
3. OPM-MEG fidelity is sufficient for network-level topology analysis
|
||||
4. Regulatory pathway is simpler for non-invasive devices
|
||||
5. Market is larger (no surgery required)
|
||||
|
||||
**Future implant collaboration**:
|
||||
Once the topology framework is validated non-invasively, combine with implant data for:
|
||||
- Ground-truth validation of topology features
|
||||
- Hybrid decoding: topology (non-invasive) + content (implant)
|
||||
- Closed-loop stimulation guided by topology analysis
|
||||
|
||||
---
|
||||
|
||||
## 17. Conclusion
|
||||
|
||||
The ten application domains for a brain state observatory are not speculative science fiction.
|
||||
They are engineering challenges with clear technical requirements, identifiable markets, and
|
||||
realistic development timelines. The enabling technologies — OPM sensors, graph algorithms,
|
||||
RuVector memory, dynamic mincut — exist today or are within reach.
|
||||
|
||||
The strategic insight is this: while the rest of the field races to decode brain *content*
|
||||
(what people think, see, imagine), there is an entirely unexplored dimension of brain
|
||||
*structure* (how networks organize, reorganize, and degrade). Dynamic mincut analysis is
|
||||
the mathematical tool that makes this dimension measurable.
|
||||
|
||||
The most interesting frontier idea remains: combine quantum magnetometers, RuVector neural
|
||||
memory, and dynamic mincut coherence detection to build a topological brain observatory that
|
||||
measures how cognition organizes itself in real time. That is genuinely unexplored territory,
|
||||
and it could fundamentally change neuroscience.
|
||||
|
||||
---
|
||||
|
||||
*This document is the applications capstone of the RF Topological Sensing research series.
|
||||
It maps ten application domains for the RuVector + dynamic mincut brain state observatory,
|
||||
with honest feasibility assessment and a phased strategic roadmap.*
|
||||
+247
-36
@@ -26,15 +26,20 @@ WiFi DensePose turns commodity WiFi signals into real-time human pose estimation
|
||||
7. [Web UI](#web-ui)
|
||||
8. [Vital Sign Detection](#vital-sign-detection)
|
||||
9. [CLI Reference](#cli-reference)
|
||||
10. [Training a Model](#training-a-model)
|
||||
10. [Observatory Visualization](#observatory-visualization)
|
||||
11. [Adaptive Classifier](#adaptive-classifier)
|
||||
- [Recording Training Data](#recording-training-data)
|
||||
- [Training the Model](#training-the-model)
|
||||
- [Using the Trained Model](#using-the-trained-model)
|
||||
12. [Training a Model](#training-a-model)
|
||||
- [CRV Signal-Line Protocol](#crv-signal-line-protocol)
|
||||
11. [RVF Model Containers](#rvf-model-containers)
|
||||
12. [Hardware Setup](#hardware-setup)
|
||||
13. [RVF Model Containers](#rvf-model-containers)
|
||||
14. [Hardware Setup](#hardware-setup)
|
||||
- [ESP32-S3 Mesh](#esp32-s3-mesh)
|
||||
- [Intel 5300 / Atheros NIC](#intel-5300--atheros-nic)
|
||||
13. [Docker Compose (Multi-Service)](#docker-compose-multi-service)
|
||||
14. [Troubleshooting](#troubleshooting)
|
||||
15. [FAQ](#faq)
|
||||
15. [Docker Compose (Multi-Service)](#docker-compose-multi-service)
|
||||
16. [Troubleshooting](#troubleshooting)
|
||||
17. [FAQ](#faq)
|
||||
|
||||
---
|
||||
|
||||
@@ -42,12 +47,12 @@ WiFi DensePose turns commodity WiFi signals into real-time human pose estimation
|
||||
|
||||
| Requirement | Minimum | Recommended |
|
||||
|-------------|---------|-------------|
|
||||
| **OS** | Windows 10, macOS 10.15, Ubuntu 18.04 | Latest stable |
|
||||
| **OS** | Windows 10/11, macOS 10.15, Ubuntu 18.04 | Latest stable |
|
||||
| **RAM** | 4 GB | 8 GB+ |
|
||||
| **Disk** | 2 GB free | 5 GB free |
|
||||
| **Docker** (for Docker path) | Docker 20+ | Docker 24+ |
|
||||
| **Rust** (for source build) | 1.70+ | 1.85+ |
|
||||
| **Python** (for legacy v1) | 3.8+ | 3.11+ |
|
||||
| **Python** (for legacy v1) | 3.10+ | 3.13+ |
|
||||
|
||||
**Hardware for live sensing (optional):**
|
||||
|
||||
@@ -73,6 +78,17 @@ docker pull ruvnet/wifi-densepose:latest
|
||||
|
||||
Multi-architecture image (amd64 + arm64). Works on Intel/AMD and Apple Silicon Macs. Contains the Rust sensing server, Three.js UI, and all signal processing.
|
||||
|
||||
**Data source selection:** Use the `CSI_SOURCE` environment variable to select the sensing mode:
|
||||
|
||||
| Value | Description |
|
||||
|-------|-------------|
|
||||
| `auto` | (default) Probe for ESP32 on UDP 5005, fall back to simulation |
|
||||
| `esp32` | Receive real CSI frames from ESP32 devices over UDP |
|
||||
| `simulated` | Generate synthetic CSI frames (no hardware required) |
|
||||
| `wifi` | Host Wi-Fi RSSI (not available inside containers) |
|
||||
|
||||
Example: `docker run -e CSI_SOURCE=esp32 -p 3000:3000 -p 5005:5005/udp ruvnet/wifi-densepose:latest`
|
||||
|
||||
### From Source (Rust)
|
||||
|
||||
```bash
|
||||
@@ -82,15 +98,15 @@ cd RuView/rust-port/wifi-densepose-rs
|
||||
# Build
|
||||
cargo build --release
|
||||
|
||||
# Verify (runs 1,100+ tests)
|
||||
cargo test --workspace
|
||||
# Verify (runs 1,400+ tests)
|
||||
cargo test --workspace --no-default-features
|
||||
```
|
||||
|
||||
The compiled binary is at `target/release/sensing-server`.
|
||||
|
||||
### From crates.io (Individual Crates)
|
||||
|
||||
All 15 crates are published to crates.io at v0.3.0. Add individual crates to your own Rust project:
|
||||
All 16 crates are published to crates.io at v0.3.0. Add individual crates to your own Rust project:
|
||||
|
||||
```bash
|
||||
# Core types and traits
|
||||
@@ -113,6 +129,9 @@ cargo add wifi-densepose-ruvector --features crv
|
||||
|
||||
# WebAssembly bindings
|
||||
cargo add wifi-densepose-wasm
|
||||
|
||||
# WASM edge runtime (lightweight, for embedded/IoT)
|
||||
cargo add wifi-densepose-wasm-edge
|
||||
```
|
||||
|
||||
See the full crate list and dependency order in [CLAUDE.md](../CLAUDE.md#crate-publishing-order).
|
||||
@@ -206,25 +225,27 @@ Default in Docker. Generates synthetic CSI data exercising the full pipeline.
|
||||
```bash
|
||||
# Docker
|
||||
docker run -p 3000:3000 ruvnet/wifi-densepose:latest
|
||||
# (--source simulated is the default)
|
||||
# (--source auto is the default; falls back to simulate when no hardware detected)
|
||||
|
||||
# From source
|
||||
./target/release/sensing-server --source simulated --http-port 3000 --ws-port 3001
|
||||
./target/release/sensing-server --source simulate --http-port 3000 --ws-port 3001
|
||||
```
|
||||
|
||||
### Windows WiFi (RSSI Only)
|
||||
|
||||
Uses `netsh wlan` to capture RSSI from nearby access points. No special hardware needed, but capabilities are limited to coarse presence and motion detection (no pose estimation or vital signs).
|
||||
Uses `netsh wlan` to capture RSSI from nearby access points. No special hardware needed. Supports presence detection, motion classification, and coarse breathing rate estimation. No pose estimation (requires CSI).
|
||||
|
||||
```bash
|
||||
# From source (Windows only)
|
||||
./target/release/sensing-server --source windows --http-port 3000 --ws-port 3001 --tick-ms 500
|
||||
./target/release/sensing-server --source wifi --http-port 3000 --ws-port 3001 --tick-ms 500
|
||||
|
||||
# Docker (requires --network host on Windows)
|
||||
docker run --network host ruvnet/wifi-densepose:latest --source windows --tick-ms 500
|
||||
docker run --network host ruvnet/wifi-densepose:latest --source wifi --tick-ms 500
|
||||
```
|
||||
|
||||
See [Tutorial #36](https://github.com/ruvnet/RuView/issues/36) for a walkthrough.
|
||||
> **Community verified:** Tested on Windows 10 (10.0.26200) with Intel Wi-Fi 6 AX201 160MHz, Python 3.14, StormFiber 5 GHz network. All 7 tutorial steps passed with stable RSSI readings at -48 dBm. See [Tutorial #36](https://github.com/ruvnet/RuView/issues/36) for the full walkthrough and test results.
|
||||
|
||||
**Vital signs from RSSI:** The sensing server now supports breathing rate estimation from RSSI variance patterns (requires stationary subject near AP) and motion classification with confidence scoring. RSSI-based vital sign detection has lower fidelity than ESP32 CSI — it is best for presence detection and coarse motion classification.
|
||||
|
||||
### macOS WiFi (RSSI Only)
|
||||
|
||||
@@ -257,8 +278,8 @@ Real Channel State Information at 20 Hz with 56-192 subcarriers. Required for po
|
||||
# From source
|
||||
./target/release/sensing-server --source esp32 --udp-port 5005 --http-port 3000 --ws-port 3001
|
||||
|
||||
# Docker
|
||||
docker run -p 3000:3000 -p 3001:3001 -p 5005:5005/udp ruvnet/wifi-densepose:latest --source esp32
|
||||
# Docker (use CSI_SOURCE environment variable)
|
||||
docker run -p 3000:3000 -p 3001:3001 -p 5005:5005/udp -e CSI_SOURCE=esp32 ruvnet/wifi-densepose:latest
|
||||
```
|
||||
|
||||
The ESP32 nodes stream binary CSI frames over UDP to port 5005. See [Hardware Setup](#esp32-s3-mesh) for flashing instructions.
|
||||
@@ -315,6 +336,9 @@ Base URL: `http://localhost:3000` (Docker) or `http://localhost:8080` (binary de
|
||||
| `GET` | `/api/v1/train/status` | Training run status | `{"phase":"idle"}` |
|
||||
| `POST` | `/api/v1/train/start` | Start a training run | `{"status":"started"}` |
|
||||
| `POST` | `/api/v1/train/stop` | Stop the active training run | `{"status":"stopped"}` |
|
||||
| `POST` | `/api/v1/adaptive/train` | Train adaptive classifier from recordings | `{"success":true,"accuracy":0.85}` |
|
||||
| `GET` | `/api/v1/adaptive/status` | Adaptive model status and accuracy | `{"loaded":true,"accuracy":0.85}` |
|
||||
| `POST` | `/api/v1/adaptive/unload` | Unload adaptive model | `{"success":true}` |
|
||||
|
||||
### Example: Get Vital Signs
|
||||
|
||||
@@ -410,9 +434,16 @@ wscat -c ws://localhost:3001/ws/sensing
|
||||
|
||||
## Web UI
|
||||
|
||||
The built-in Three.js UI is served at `http://localhost:3000/` (Docker) or the configured HTTP port.
|
||||
The built-in Three.js UI is served at `http://localhost:3000/ui/` (Docker) or the configured HTTP port.
|
||||
|
||||
**What you see:**
|
||||
**Two visualization modes:**
|
||||
|
||||
| Page | URL | Purpose |
|
||||
|------|-----|---------|
|
||||
| **Dashboard** | `/ui/index.html` | Tabbed monitoring dashboard with body model, signal heatmap, phase plot, vital signs |
|
||||
| **Observatory** | `/ui/observatory.html` | Immersive 3D room visualization with cinematic lighting and wireframe figures |
|
||||
|
||||
**Dashboard panels:**
|
||||
|
||||
| Panel | Description |
|
||||
|-------|-------------|
|
||||
@@ -423,7 +454,7 @@ The built-in Three.js UI is served at `http://localhost:3000/` (Docker) or the c
|
||||
| Vital Signs | Live breathing rate (BPM) and heart rate (BPM) |
|
||||
| Dashboard | System stats, throughput, connected WebSocket clients |
|
||||
|
||||
The UI updates in real-time via the WebSocket connection.
|
||||
Both UIs update in real-time via WebSocket and auto-detect the sensing server on the same origin.
|
||||
|
||||
---
|
||||
|
||||
@@ -441,6 +472,8 @@ The system extracts breathing rate and heart rate from CSI signal fluctuations u
|
||||
- Subject within ~3-5 meters of an access point (up to ~8 m with multistatic mesh)
|
||||
- Relatively stationary subject (large movements mask vital sign oscillations)
|
||||
|
||||
**Signal smoothing:** Vital sign estimates pass through a three-stage smoothing pipeline (ADR-048): outlier rejection (±8 BPM HR, ±2 BPM BR per frame), 21-frame trimmed mean, and EMA with α=0.02. This produces stable readings that hold steady for 5-10+ seconds instead of jumping every frame. See [Adaptive Classifier](#adaptive-classifier) for details.
|
||||
|
||||
**Simulated mode** produces synthetic vital sign data for testing.
|
||||
|
||||
---
|
||||
@@ -451,7 +484,7 @@ The Rust sensing server binary accepts the following flags:
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `--source` | `auto` | Data source: `auto`, `simulated`, `windows`, `esp32` |
|
||||
| `--source` | `auto` | Data source: `auto`, `simulate`, `wifi`, `esp32` |
|
||||
| `--http-port` | `8080` | HTTP port for REST API and UI |
|
||||
| `--ws-port` | `8765` | WebSocket port |
|
||||
| `--udp-port` | `5005` | UDP port for ESP32 CSI frames |
|
||||
@@ -472,13 +505,13 @@ The Rust sensing server binary accepts the following flags:
|
||||
|
||||
```bash
|
||||
# Simulated mode with UI (development)
|
||||
./target/release/sensing-server --source simulated --http-port 3000 --ws-port 3001 --ui-path ../../ui
|
||||
./target/release/sensing-server --source simulate --http-port 3000 --ws-port 3001 --ui-path ../../ui
|
||||
|
||||
# ESP32 hardware mode
|
||||
./target/release/sensing-server --source esp32 --udp-port 5005
|
||||
|
||||
# Windows WiFi RSSI
|
||||
./target/release/sensing-server --source windows --tick-ms 500
|
||||
./target/release/sensing-server --source wifi --tick-ms 500
|
||||
|
||||
# Run benchmark
|
||||
./target/release/sensing-server --benchmark
|
||||
@@ -492,6 +525,149 @@ The Rust sensing server binary accepts the following flags:
|
||||
|
||||
---
|
||||
|
||||
## Observatory Visualization
|
||||
|
||||
The Observatory is an immersive Three.js visualization that renders WiFi sensing data as a cinematic 3D experience. It features room-scale props, wireframe human figures, WiFi signal animations, and a live data HUD.
|
||||
|
||||
**URL:** `http://localhost:3000/ui/observatory.html`
|
||||
|
||||
**Features:**
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| Room scene | Furniture, walls, floor with emissive materials and 6-point lighting |
|
||||
| Wireframe figures | Up to 4 human skeletons with joint pulsation synced to breathing |
|
||||
| Signal field | Volumetric WiFi wave visualization |
|
||||
| Live HUD | Heart rate, breathing rate, confidence, RSSI, motion level |
|
||||
| Auto-detect | Automatically connects to live ESP32 data when sensing server is running |
|
||||
| Scenario cycling | 6 preset scenarios with smooth transitions (demo mode) |
|
||||
|
||||
**Keyboard shortcuts:**
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `1-6` | Switch scenario |
|
||||
| `A` | Toggle auto-cycle |
|
||||
| `P` | Pause/resume |
|
||||
| `S` | Open settings |
|
||||
| `R` | Reset camera |
|
||||
|
||||
**Live data auto-detect:** When served by the sensing server, the Observatory probes `/health` on the same origin and automatically connects via WebSocket. The HUD badge switches from `DEMO` to `LIVE`. No configuration needed.
|
||||
|
||||
---
|
||||
|
||||
## Adaptive Classifier
|
||||
|
||||
The adaptive classifier (ADR-048) learns your environment's specific WiFi signal patterns from labeled recordings. It replaces static threshold-based classification with a trained logistic regression model that uses 15 features (7 server-computed + 8 subcarrier-derived statistics).
|
||||
|
||||
### Signal Smoothing Pipeline
|
||||
|
||||
All CSI-derived metrics pass through a three-stage pipeline before reaching the UI:
|
||||
|
||||
| Stage | What It Does | Key Parameters |
|
||||
|-------|-------------|----------------|
|
||||
| **Adaptive baseline** | Learns quiet-room noise floor, subtracts drift | α=0.003, 50-frame warm-up |
|
||||
| **EMA + median filter** | Smooths motion score and vital signs | Motion α=0.15; Vitals: 21-frame trimmed mean, α=0.02 |
|
||||
| **Hysteresis debounce** | Prevents rapid state flickering | 4 frames (~0.4s) required for state transition |
|
||||
|
||||
Vital signs use additional stabilization:
|
||||
|
||||
| Parameter | Value | Effect |
|
||||
|-----------|-------|--------|
|
||||
| HR dead-band | ±2 BPM | Prevents micro-drift |
|
||||
| BR dead-band | ±0.5 BPM | Prevents micro-drift |
|
||||
| HR max jump | 8 BPM/frame | Rejects noise spikes |
|
||||
| BR max jump | 2 BPM/frame | Rejects noise spikes |
|
||||
|
||||
### Recording Training Data
|
||||
|
||||
Record labeled CSI sessions while performing distinct activities. Each recording captures full sensing frames (features + raw subcarrier amplitudes) at ~10-25 FPS.
|
||||
|
||||
```bash
|
||||
# 1. Record empty room (leave the room for 30 seconds)
|
||||
curl -X POST http://localhost:3000/api/v1/recording/start \
|
||||
-H "Content-Type: application/json" -d '{"id":"train_empty_room"}'
|
||||
# ... wait 30 seconds ...
|
||||
curl -X POST http://localhost:3000/api/v1/recording/stop
|
||||
|
||||
# 2. Record sitting still (sit near ESP32 for 30 seconds)
|
||||
curl -X POST http://localhost:3000/api/v1/recording/start \
|
||||
-H "Content-Type: application/json" -d '{"id":"train_sitting_still"}'
|
||||
# ... wait 30 seconds ...
|
||||
curl -X POST http://localhost:3000/api/v1/recording/stop
|
||||
|
||||
# 3. Record walking (walk around the room for 30 seconds)
|
||||
curl -X POST http://localhost:3000/api/v1/recording/start \
|
||||
-H "Content-Type: application/json" -d '{"id":"train_walking"}'
|
||||
# ... wait 30 seconds ...
|
||||
curl -X POST http://localhost:3000/api/v1/recording/stop
|
||||
|
||||
# 4. Record active movement (jumping jacks, arm waving for 30 seconds)
|
||||
curl -X POST http://localhost:3000/api/v1/recording/start \
|
||||
-H "Content-Type: application/json" -d '{"id":"train_active"}'
|
||||
# ... wait 30 seconds ...
|
||||
curl -X POST http://localhost:3000/api/v1/recording/stop
|
||||
```
|
||||
|
||||
Recordings are saved as JSONL files in `data/recordings/`. Filenames must start with `train_` and contain a class keyword:
|
||||
|
||||
| Filename pattern | Class |
|
||||
|-----------------|-------|
|
||||
| `*empty*` or `*absent*` | absent |
|
||||
| `*still*` or `*sitting*` | present_still |
|
||||
| `*walking*` or `*moving*` | present_moving |
|
||||
| `*active*` or `*exercise*` | active |
|
||||
|
||||
### Training the Model
|
||||
|
||||
Train the adaptive classifier from your labeled recordings:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/v1/adaptive/train
|
||||
```
|
||||
|
||||
The server trains a multiclass logistic regression on 15 features using mini-batch SGD (200 epochs). Training completes in under 1 second for typical recording sets. The trained model is saved to `data/adaptive_model.json` and automatically loaded on server restart.
|
||||
|
||||
**Check model status:**
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000/api/v1/adaptive/status
|
||||
```
|
||||
|
||||
**Unload the model (revert to threshold-based classification):**
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/v1/adaptive/unload
|
||||
```
|
||||
|
||||
### Using the Trained Model
|
||||
|
||||
Once trained, the adaptive model runs automatically:
|
||||
|
||||
1. Each CSI frame is classified using the learned weights instead of static thresholds
|
||||
2. Model confidence is blended with smoothed threshold confidence (70/30 split)
|
||||
3. The model persists across server restarts (loaded from `data/adaptive_model.json`)
|
||||
|
||||
**Tips for better accuracy:**
|
||||
|
||||
- Record with clearly distinct activities (actually leave the room for "empty")
|
||||
- Record 30-60 seconds per activity (more data = better model)
|
||||
- Re-record and retrain if you move the ESP32 or rearrange the room
|
||||
- The model is environment-specific — retrain when the physical setup changes
|
||||
|
||||
### Adaptive Classifier API
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| `POST` | `/api/v1/adaptive/train` | Train from `train_*` recordings |
|
||||
| `GET` | `/api/v1/adaptive/status` | Model status, accuracy, class stats |
|
||||
| `POST` | `/api/v1/adaptive/unload` | Unload model, revert to thresholds |
|
||||
| `POST` | `/api/v1/recording/start` | Start recording CSI frames |
|
||||
| `POST` | `/api/v1/recording/stop` | Stop recording |
|
||||
| `GET` | `/api/v1/recording/list` | List recordings |
|
||||
|
||||
---
|
||||
|
||||
## Training a Model
|
||||
|
||||
The training pipeline is implemented in pure Rust (7,832 lines, zero external ML dependencies).
|
||||
@@ -514,9 +690,11 @@ Download the dataset files and place them in a `data/` directory.
|
||||
./target/release/sensing-server --train --dataset data/ --dataset-type mmfi --epochs 100 --save-rvf model.rvf
|
||||
|
||||
# Via Docker (mount your data directory)
|
||||
# Note: Training mode requires overriding the default entrypoint
|
||||
docker run --rm \
|
||||
-v $(pwd)/data:/data \
|
||||
-v $(pwd)/output:/output \
|
||||
--entrypoint /app/sensing-server \
|
||||
ruvnet/wifi-densepose:latest \
|
||||
--train --dataset /data --epochs 100 --export-rvf /output/model.rvf
|
||||
```
|
||||
@@ -632,14 +810,18 @@ Pre-built binaries are available at [Releases](https://github.com/ruvnet/RuView/
|
||||
|
||||
| Release | What It Includes | Tag |
|
||||
|---------|-----------------|-----|
|
||||
| [v0.2.0](https://github.com/ruvnet/RuView/releases/tag/v0.2.0-esp32) | Stable — raw CSI streaming, TDM, channel hopping, QUIC mesh | `v0.2.0-esp32` |
|
||||
| [v0.4.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.1-esp32) | **Stable** — 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.3.0-alpha](https://github.com/ruvnet/RuView/releases/tag/v0.3.0-alpha-esp32) | Alpha — adds on-device edge intelligence (ADR-039) | `v0.3.0-alpha-esp32` |
|
||||
| [v0.2.0](https://github.com/ruvnet/RuView/releases/tag/v0.2.0-esp32) | Raw CSI streaming, TDM, channel hopping, QUIC mesh | `v0.2.0-esp32` |
|
||||
|
||||
> **Important:** Firmware versions prior to v0.4.1 had CSI **disabled** in the build config, causing a runtime error (`E wifi:CSI not enabled in menuconfig!`). Always use v0.4.1 or later.
|
||||
|
||||
```bash
|
||||
# Flash an ESP32-S3 (requires esptool: pip install esptool)
|
||||
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
|
||||
write-flash --flash-mode dio --flash-size 4MB \
|
||||
0x0 bootloader.bin 0x8000 partition-table.bin 0x10000 esp32-csi-node.bin
|
||||
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
|
||||
```
|
||||
|
||||
**Provisioning:**
|
||||
@@ -720,8 +902,8 @@ Binary size: 777 KB (24% free in the 1 MB app partition).
|
||||
# From source
|
||||
./target/release/sensing-server --source esp32 --udp-port 5005 --http-port 3000 --ws-port 3001
|
||||
|
||||
# Docker
|
||||
docker run -p 3000:3000 -p 3001:3001 -p 5005:5005/udp ruvnet/wifi-densepose:latest --source esp32
|
||||
# Docker (use CSI_SOURCE environment variable)
|
||||
docker run -p 3000:3000 -p 3001:3001 -p 5005:5005/udp -e CSI_SOURCE=esp32 ruvnet/wifi-densepose:latest
|
||||
```
|
||||
|
||||
See [ADR-018](../docs/adr/ADR-018-esp32-dev-implementation.md), [ADR-029](../docs/adr/ADR-029-ruvsense-multistatic-sensing-mode.md), and [Tutorial #34](https://github.com/ruvnet/RuView/issues/34).
|
||||
@@ -788,12 +970,17 @@ Add the WebSocket port mapping:
|
||||
docker run -p 3000:3000 -p 3001:3001 ruvnet/wifi-densepose:latest
|
||||
```
|
||||
|
||||
### ESP32: "CSI not enabled in menuconfig"
|
||||
|
||||
Firmware versions prior to v0.4.1 had `CONFIG_ESP_WIFI_CSI_ENABLED` disabled in the build config. Upgrade to [v0.4.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.1-esp32) or later. If building from source, ensure `sdkconfig.defaults` exists (not just `sdkconfig.defaults.template`). See [ADR-057](../docs/adr/ADR-057-firmware-csi-build-guard.md).
|
||||
|
||||
### ESP32: No data arriving
|
||||
|
||||
1. Verify the ESP32 is connected to the same WiFi network
|
||||
2. Check the target IP matches the sensing server machine: `python firmware/esp32-csi-node/provision.py --port COM7 --target-ip <YOUR_IP>`
|
||||
3. Verify UDP port 5005 is not blocked by firewall
|
||||
4. Test with: `nc -lu 5005` (Linux) or similar UDP listener
|
||||
1. Verify firmware is v0.4.1+ (older versions had CSI disabled — see above)
|
||||
2. Verify the ESP32 is connected to the same WiFi network
|
||||
3. Check the target IP matches the sensing server machine: `python firmware/esp32-csi-node/provision.py --port COM7 --target-ip <YOUR_IP>`
|
||||
4. Verify UDP port 5005 is not blocked by firewall
|
||||
5. Test with: `nc -lu 5005` (Linux) or similar UDP listener
|
||||
|
||||
### Build: Rust compilation errors
|
||||
|
||||
@@ -805,13 +992,28 @@ rustc --version
|
||||
|
||||
### Windows: RSSI mode shows no data
|
||||
|
||||
Run the terminal as Administrator (required for `netsh wlan` access).
|
||||
Run the terminal as Administrator (required for `netsh wlan` access). Verified working on Windows 10 and 11 with Intel AX201 and Intel BE201 adapters.
|
||||
|
||||
### Vital signs show 0 BPM
|
||||
|
||||
- Vital sign detection requires CSI-capable hardware (ESP32 or research NIC)
|
||||
- RSSI-only mode (Windows WiFi) does not have sufficient resolution for vital signs
|
||||
- In simulated mode, synthetic vital signs are generated after a few seconds of warm-up
|
||||
- With real ESP32 data, vital signs take ~5 seconds to stabilize (smoothing pipeline warm-up)
|
||||
|
||||
### Vital signs jumping around
|
||||
|
||||
The server applies a 3-stage smoothing pipeline (ADR-048). If readings are still unstable:
|
||||
- Ensure the subject is relatively still (large movements mask vital sign oscillations)
|
||||
- Train the adaptive classifier for your specific environment: `curl -X POST http://localhost:3000/api/v1/adaptive/train`
|
||||
- Check signal quality: `curl http://localhost:3000/api/v1/sensing/latest` — look for `signal_quality > 0.4`
|
||||
|
||||
### Observatory shows DEMO instead of LIVE
|
||||
|
||||
- Verify the sensing server is running: `curl http://localhost:3000/health`
|
||||
- Access Observatory via the server URL: `http://localhost:3000/ui/observatory.html` (not a file:// URL)
|
||||
- Hard refresh with Ctrl+Shift+R to clear cached settings
|
||||
- The auto-detect probes `/health` on the same origin — cross-origin won't work
|
||||
|
||||
---
|
||||
|
||||
@@ -838,11 +1040,20 @@ The system uses WiFi radio signals, not cameras. No images or video are captured
|
||||
**Q: What's the Python vs Rust difference?**
|
||||
The Rust implementation (v2) is 810x faster than Python (v1) for the full CSI pipeline. The Docker image is 132 MB vs 569 MB. Rust is the primary and recommended runtime. Python v1 remains available for legacy workflows.
|
||||
|
||||
**Q: Can I use an ESP8266 instead of ESP32-S3?**
|
||||
No. The ESP8266 does not expose WiFi Channel State Information (CSI) through its SDK, has insufficient RAM (~80 KB vs 512 KB), and runs a single-core 80 MHz CPU that cannot handle the signal processing pipeline. The ESP32-S3 is the minimum supported CSI capture device. See [Issue #138](https://github.com/ruvnet/RuView/issues/138) for alternatives including using cheap Android TV boxes as aggregation hubs.
|
||||
|
||||
**Q: Does the Windows WiFi tutorial work on Windows 10?**
|
||||
Yes. Community-tested on Windows 10 (build 26200) with an Intel Wi-Fi 6 AX201 160MHz adapter on a 5 GHz network. All 7 tutorial steps passed with Python 3.14. See [Issue #36](https://github.com/ruvnet/RuView/issues/36) for full test results.
|
||||
|
||||
**Q: Can I run the sensing server on an ARM device (Raspberry Pi, TV box)?**
|
||||
ARM64 deployment is planned ([ADR-046](adr/ADR-046-android-tv-box-armbian-deployment.md)) but not yet available as a pre-built binary. You can cross-compile from source using `cross build --release --target aarch64-unknown-linux-gnu -p wifi-densepose-sensing-server` if you have the Rust cross-compilation toolchain set up.
|
||||
|
||||
---
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [Architecture Decision Records](../docs/adr/) - 43 ADRs covering all design decisions
|
||||
- [Architecture Decision Records](../docs/adr/) - 48 ADRs covering all design decisions
|
||||
- [WiFi-Mat Disaster Response Guide](wifi-mat-user-guide.md) - Search & rescue module
|
||||
- [Build Guide](build-guide.md) - Detailed build instructions
|
||||
- [RuVector](https://github.com/ruvnet/ruvector) - Signal intelligence crate ecosystem
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"running": true,
|
||||
"startedAt": "2026-03-10T14:22:41.948Z",
|
||||
"workers": {
|
||||
"map": {
|
||||
"runCount": 0,
|
||||
"successCount": 0,
|
||||
"failureCount": 0,
|
||||
"averageDurationMs": 0,
|
||||
"isRunning": false,
|
||||
"nextRun": "2026-03-10T14:22:41.948Z"
|
||||
},
|
||||
"audit": {
|
||||
"runCount": 0,
|
||||
"successCount": 0,
|
||||
"failureCount": 0,
|
||||
"averageDurationMs": 0,
|
||||
"isRunning": false,
|
||||
"nextRun": "2026-03-10T14:24:41.948Z"
|
||||
},
|
||||
"optimize": {
|
||||
"runCount": 0,
|
||||
"successCount": 0,
|
||||
"failureCount": 0,
|
||||
"averageDurationMs": 0,
|
||||
"isRunning": false,
|
||||
"nextRun": "2026-03-10T14:26:41.948Z"
|
||||
},
|
||||
"consolidate": {
|
||||
"runCount": 0,
|
||||
"successCount": 0,
|
||||
"failureCount": 0,
|
||||
"averageDurationMs": 0,
|
||||
"isRunning": false,
|
||||
"nextRun": "2026-03-10T14:28:41.949Z"
|
||||
},
|
||||
"testgaps": {
|
||||
"runCount": 0,
|
||||
"successCount": 0,
|
||||
"failureCount": 0,
|
||||
"averageDurationMs": 0,
|
||||
"isRunning": false,
|
||||
"nextRun": "2026-03-10T14:30:41.949Z"
|
||||
},
|
||||
"predict": {
|
||||
"runCount": 0,
|
||||
"successCount": 0,
|
||||
"failureCount": 0,
|
||||
"averageDurationMs": 0,
|
||||
"isRunning": false
|
||||
},
|
||||
"document": {
|
||||
"runCount": 0,
|
||||
"successCount": 0,
|
||||
"failureCount": 0,
|
||||
"averageDurationMs": 0,
|
||||
"isRunning": false
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"autoStart": false,
|
||||
"logDir": "/Users/cohen/GitHub/ruvnet/RuView/firmware/esp32-csi-node/.claude-flow/logs",
|
||||
"stateFile": "/Users/cohen/GitHub/ruvnet/RuView/firmware/esp32-csi-node/.claude-flow/daemon-state.json",
|
||||
"maxConcurrent": 2,
|
||||
"workerTimeoutMs": 300000,
|
||||
"resourceThresholds": {
|
||||
"maxCpuLoad": 2,
|
||||
"minFreeMemoryPercent": 20
|
||||
},
|
||||
"workers": [
|
||||
{
|
||||
"type": "map",
|
||||
"intervalMs": 900000,
|
||||
"offsetMs": 0,
|
||||
"priority": "normal",
|
||||
"description": "Codebase mapping",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"type": "audit",
|
||||
"intervalMs": 600000,
|
||||
"offsetMs": 120000,
|
||||
"priority": "critical",
|
||||
"description": "Security analysis",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"type": "optimize",
|
||||
"intervalMs": 900000,
|
||||
"offsetMs": 240000,
|
||||
"priority": "high",
|
||||
"description": "Performance optimization",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"type": "consolidate",
|
||||
"intervalMs": 1800000,
|
||||
"offsetMs": 360000,
|
||||
"priority": "low",
|
||||
"description": "Memory consolidation",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"type": "testgaps",
|
||||
"intervalMs": 1200000,
|
||||
"offsetMs": 480000,
|
||||
"priority": "normal",
|
||||
"description": "Test coverage analysis",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"type": "predict",
|
||||
"intervalMs": 600000,
|
||||
"offsetMs": 0,
|
||||
"priority": "low",
|
||||
"description": "Predictive preloading",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"type": "document",
|
||||
"intervalMs": 3600000,
|
||||
"offsetMs": 0,
|
||||
"priority": "low",
|
||||
"description": "Auto-documentation",
|
||||
"enabled": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"savedAt": "2026-03-10T14:22:41.949Z"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{"type":"edit","file":"unknown","timestamp":1773152422749,"sessionId":null}
|
||||
{"type":"edit","file":"unknown","timestamp":1773152444021,"sessionId":null}
|
||||
{"type":"edit","file":"unknown","timestamp":1773152460956,"sessionId":null}
|
||||
{"type":"edit","file":"unknown","timestamp":1773152493971,"sessionId":null}
|
||||
{"type":"edit","file":"unknown","timestamp":1773152501432,"sessionId":null}
|
||||
{"type":"edit","file":"unknown","timestamp":1773152510853,"sessionId":null}
|
||||
{"type":"edit","file":"unknown","timestamp":1773152596890,"sessionId":null}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": "session-1773152560779",
|
||||
"startedAt": "2026-03-10T14:22:40.779Z",
|
||||
"cwd": "/Users/cohen/GitHub/ruvnet/RuView/firmware/esp32-csi-node",
|
||||
"context": {},
|
||||
"metrics": {
|
||||
"edits": 1,
|
||||
"commands": 0,
|
||||
"tasks": 0,
|
||||
"errors": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
# Remove MSYS environment variables that trigger ESP-IDF's MinGW rejection
|
||||
Remove-Item env:MSYSTEM -ErrorAction SilentlyContinue
|
||||
Remove-Item env:MSYSTEM_CARCH -ErrorAction SilentlyContinue
|
||||
Remove-Item env:MSYSTEM_CHOST -ErrorAction SilentlyContinue
|
||||
Remove-Item env:MSYSTEM_PREFIX -ErrorAction SilentlyContinue
|
||||
Remove-Item env:MINGW_CHOST -ErrorAction SilentlyContinue
|
||||
Remove-Item env:MINGW_PACKAGE_PREFIX -ErrorAction SilentlyContinue
|
||||
Remove-Item env:MINGW_PREFIX -ErrorAction SilentlyContinue
|
||||
|
||||
$env:IDF_PATH = "C:\Users\ruv\esp\v5.4\esp-idf"
|
||||
$env:IDF_TOOLS_PATH = "C:\Espressif\tools"
|
||||
$env:IDF_PYTHON_ENV_PATH = "C:\Espressif\tools\python\v5.4\venv"
|
||||
$env:PATH = "C:\Espressif\tools\xtensa-esp-elf\esp-14.2.0_20241119\xtensa-esp-elf\bin;C:\Espressif\tools\cmake\3.30.2\cmake-3.30.2-windows-x86_64\bin;C:\Espressif\tools\ninja\1.12.1;C:\Espressif\tools\ccache\4.10.2\ccache-4.10.2-windows-x86_64;C:\Espressif\tools\idf-exe\1.0.3;C:\Espressif\tools\python\v5.4\venv\Scripts;$env:PATH"
|
||||
|
||||
Set-Location "C:\Users\ruv\Projects\wifi-densepose\firmware\esp32-csi-node"
|
||||
|
||||
$python = "$env:IDF_PYTHON_ENV_PATH\Scripts\python.exe"
|
||||
$idf = "$env:IDF_PATH\tools\idf.py"
|
||||
|
||||
Write-Host "=== Cleaning stale build cache ==="
|
||||
& $python $idf fullclean
|
||||
|
||||
Write-Host "=== Building firmware (SSID=ruv.net, target=192.168.1.20:5005) ==="
|
||||
& $python $idf build
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "=== Build succeeded! Flashing to COM7 ==="
|
||||
& $python $idf -p COM7 flash
|
||||
} else {
|
||||
Write-Host "=== Build failed with exit code $LASTEXITCODE ==="
|
||||
}
|
||||
@@ -1,6 +1,19 @@
|
||||
idf_component_register(
|
||||
SRCS "main.c" "csi_collector.c" "stream_sender.c" "nvs_config.c"
|
||||
"edge_processing.c" "ota_update.c" "power_mgmt.c"
|
||||
"wasm_runtime.c" "wasm_upload.c" "rvf_parser.c"
|
||||
INCLUDE_DIRS "."
|
||||
set(SRCS
|
||||
"main.c" "csi_collector.c" "stream_sender.c" "nvs_config.c"
|
||||
"edge_processing.c" "ota_update.c" "power_mgmt.c"
|
||||
"wasm_runtime.c" "wasm_upload.c" "rvf_parser.c"
|
||||
)
|
||||
|
||||
set(REQUIRES "")
|
||||
|
||||
# ADR-045: AMOLED display support (compile-time optional)
|
||||
if(CONFIG_DISPLAY_ENABLE)
|
||||
list(APPEND SRCS "display_hal.c" "display_ui.c" "display_task.c")
|
||||
set(REQUIRES esp_lcd esp_lcd_touch lvgl)
|
||||
endif()
|
||||
|
||||
idf_component_register(
|
||||
SRCS ${SRCS}
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES ${REQUIRES}
|
||||
)
|
||||
|
||||
@@ -85,6 +85,87 @@ menu "Edge Intelligence (ADR-039)"
|
||||
|
||||
endmenu
|
||||
|
||||
menu "AMOLED Display (ADR-045)"
|
||||
|
||||
config DISPLAY_ENABLE
|
||||
bool "Enable AMOLED display support"
|
||||
default y
|
||||
help
|
||||
Enable RM67162 QSPI AMOLED display and LVGL UI.
|
||||
Auto-detects at boot; gracefully skips if no display hardware.
|
||||
Requires SPIRAM for frame buffers.
|
||||
|
||||
config DISPLAY_FPS_LIMIT
|
||||
int "Display refresh rate limit (FPS)"
|
||||
default 30
|
||||
range 10 60
|
||||
depends on DISPLAY_ENABLE
|
||||
help
|
||||
Maximum display refresh rate. Lower values save CPU.
|
||||
|
||||
config DISPLAY_BRIGHTNESS
|
||||
int "Default backlight brightness (%)"
|
||||
default 80
|
||||
range 0 100
|
||||
depends on DISPLAY_ENABLE
|
||||
|
||||
config DISPLAY_QSPI_CS
|
||||
int "QSPI CS GPIO"
|
||||
default 6
|
||||
depends on DISPLAY_ENABLE
|
||||
|
||||
config DISPLAY_QSPI_CLK
|
||||
int "QSPI CLK GPIO"
|
||||
default 47
|
||||
depends on DISPLAY_ENABLE
|
||||
|
||||
config DISPLAY_QSPI_D0
|
||||
int "QSPI D0 GPIO"
|
||||
default 18
|
||||
depends on DISPLAY_ENABLE
|
||||
|
||||
config DISPLAY_QSPI_D1
|
||||
int "QSPI D1 GPIO"
|
||||
default 7
|
||||
depends on DISPLAY_ENABLE
|
||||
|
||||
config DISPLAY_QSPI_D2
|
||||
int "QSPI D2 GPIO"
|
||||
default 48
|
||||
depends on DISPLAY_ENABLE
|
||||
|
||||
config DISPLAY_QSPI_D3
|
||||
int "QSPI D3 GPIO"
|
||||
default 5
|
||||
depends on DISPLAY_ENABLE
|
||||
|
||||
config DISPLAY_TOUCH_SDA
|
||||
int "Touch I2C SDA GPIO"
|
||||
default 3
|
||||
depends on DISPLAY_ENABLE
|
||||
|
||||
config DISPLAY_TOUCH_SCL
|
||||
int "Touch I2C SCL GPIO"
|
||||
default 2
|
||||
depends on DISPLAY_ENABLE
|
||||
|
||||
config DISPLAY_TOUCH_INT
|
||||
int "Touch INT GPIO"
|
||||
default 21
|
||||
depends on DISPLAY_ENABLE
|
||||
|
||||
config DISPLAY_TOUCH_RST
|
||||
int "Touch RST GPIO"
|
||||
default 17
|
||||
depends on DISPLAY_ENABLE
|
||||
|
||||
config DISPLAY_BL_PIN
|
||||
int "Backlight PWM GPIO"
|
||||
default 38
|
||||
depends on DISPLAY_ENABLE
|
||||
|
||||
endmenu
|
||||
|
||||
menu "WASM Programmable Sensing (ADR-040)"
|
||||
|
||||
config WASM_ENABLE
|
||||
|
||||
@@ -21,6 +21,16 @@
|
||||
#include "esp_timer.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
/* ADR-057: Build-time guard — fail early if CSI is not enabled in sdkconfig.
|
||||
* Without this, the firmware compiles but crashes at runtime with:
|
||||
* "E (xxxx) wifi:CSI not enabled in menuconfig!"
|
||||
* which is confusing for users flashing pre-built binaries. */
|
||||
#ifndef CONFIG_ESP_WIFI_CSI_ENABLED
|
||||
#error "CONFIG_ESP_WIFI_CSI_ENABLED must be set in sdkconfig. " \
|
||||
"Run: idf.py menuconfig -> Component config -> Wi-Fi -> Enable WiFi CSI, " \
|
||||
"or copy sdkconfig.defaults.template to sdkconfig.defaults before building."
|
||||
#endif
|
||||
|
||||
static const char *TAG = "csi_collector";
|
||||
|
||||
static uint32_t s_sequence = 0;
|
||||
|
||||
@@ -0,0 +1,382 @@
|
||||
/**
|
||||
* @file display_hal.c
|
||||
* @brief ADR-045: SH8601 QSPI AMOLED HAL for Waveshare ESP32-S3-Touch-AMOLED-1.8.
|
||||
*
|
||||
* Uses ESP-IDF esp_lcd_panel_io_spi in QSPI mode (quad_mode=true, lcd_cmd_bits=32).
|
||||
* The panel_io layer handles the 0x02/0x32 QSPI command encoding.
|
||||
*
|
||||
* Hardware: SH8601 368x448, FT3168 touch, TCA9554 I/O expander for power/reset.
|
||||
*
|
||||
* Pin assignments (Waveshare ESP32-S3-Touch-AMOLED-1.8):
|
||||
* QSPI: CS=12, CLK=11, D0=4, D1=5, D2=6, D3=7
|
||||
* I2C: SDA=15, SCL=14 (shared: touch FT3168 + TCA9554 expander)
|
||||
* Touch INT=21
|
||||
*/
|
||||
|
||||
#include "display_hal.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if CONFIG_DISPLAY_ENABLE
|
||||
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_lcd_panel_io.h"
|
||||
#include "driver/spi_master.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/i2c.h"
|
||||
#include "esp_heap_caps.h"
|
||||
|
||||
static const char *TAG = "disp_hal";
|
||||
|
||||
/* ---- QSPI Pin Definitions (Waveshare board) ---- */
|
||||
#define DISP_QSPI_CS 12
|
||||
#define DISP_QSPI_CLK 11
|
||||
#define DISP_QSPI_D0 4
|
||||
#define DISP_QSPI_D1 5
|
||||
#define DISP_QSPI_D2 6
|
||||
#define DISP_QSPI_D3 7
|
||||
|
||||
/* ---- I2C (shared: touch + TCA9554 expander) ---- */
|
||||
#define I2C_SDA 15
|
||||
#define I2C_SCL 14
|
||||
#define TOUCH_INT_PIN 21
|
||||
#define I2C_MASTER_NUM I2C_NUM_0
|
||||
#define I2C_MASTER_FREQ_HZ 400000
|
||||
|
||||
/* ---- TCA9554 I/O expander ---- */
|
||||
#define TCA9554_ADDR 0x20
|
||||
#define TCA9554_REG_OUTPUT 0x01
|
||||
#define TCA9554_REG_CONFIG 0x03
|
||||
|
||||
/* ---- FT3168 touch controller ---- */
|
||||
#define FT3168_ADDR 0x38
|
||||
|
||||
/* ---- Display dimensions ---- */
|
||||
#define DISP_H_RES 368
|
||||
#define DISP_V_RES 448
|
||||
|
||||
/* ---- QSPI opcodes (packed into lcd_cmd bits [31:24]) ---- */
|
||||
#define LCD_OPCODE_WRITE_CMD 0x02
|
||||
#define LCD_OPCODE_WRITE_COLOR 0x32
|
||||
|
||||
/* ---- State ---- */
|
||||
static esp_lcd_panel_io_handle_t s_io_handle = NULL;
|
||||
static bool s_i2c_initialized = false;
|
||||
static bool s_touch_initialized = false;
|
||||
|
||||
/* ---- I2C helpers ---- */
|
||||
|
||||
static esp_err_t i2c_write_reg(uint8_t dev_addr, uint8_t reg, const uint8_t *data, size_t len)
|
||||
{
|
||||
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
|
||||
i2c_master_start(cmd);
|
||||
i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true);
|
||||
i2c_master_write_byte(cmd, reg, true);
|
||||
if (data && len > 0) {
|
||||
i2c_master_write(cmd, data, len, true);
|
||||
}
|
||||
i2c_master_stop(cmd);
|
||||
esp_err_t ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, pdMS_TO_TICKS(100));
|
||||
i2c_cmd_link_delete(cmd);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t i2c_read_reg(uint8_t dev_addr, uint8_t reg, uint8_t *data, size_t len)
|
||||
{
|
||||
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
|
||||
i2c_master_start(cmd);
|
||||
i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_WRITE, true);
|
||||
i2c_master_write_byte(cmd, reg, true);
|
||||
i2c_master_start(cmd);
|
||||
i2c_master_write_byte(cmd, (dev_addr << 1) | I2C_MASTER_READ, true);
|
||||
i2c_master_read(cmd, data, len, I2C_MASTER_LAST_NACK);
|
||||
i2c_master_stop(cmd);
|
||||
esp_err_t ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, pdMS_TO_TICKS(100));
|
||||
i2c_cmd_link_delete(cmd);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t init_i2c_bus(void)
|
||||
{
|
||||
if (s_i2c_initialized) return ESP_OK;
|
||||
|
||||
i2c_config_t i2c_cfg = {
|
||||
.mode = I2C_MODE_MASTER,
|
||||
.sda_io_num = I2C_SDA,
|
||||
.scl_io_num = I2C_SCL,
|
||||
.sda_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
.scl_pullup_en = GPIO_PULLUP_ENABLE,
|
||||
.master.clk_speed = I2C_MASTER_FREQ_HZ,
|
||||
};
|
||||
|
||||
esp_err_t ret = i2c_param_config(I2C_MASTER_NUM, &i2c_cfg);
|
||||
if (ret != ESP_OK) return ret;
|
||||
|
||||
ret = i2c_driver_install(I2C_MASTER_NUM, I2C_MODE_MASTER, 0, 0, 0);
|
||||
if (ret != ESP_OK) return ret;
|
||||
|
||||
s_i2c_initialized = true;
|
||||
ESP_LOGI(TAG, "I2C bus init OK (SDA=%d, SCL=%d)", I2C_SDA, I2C_SCL);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ---- TCA9554 I/O expander: toggle pins for display power/reset ---- */
|
||||
|
||||
static esp_err_t tca9554_init_display_power(void)
|
||||
{
|
||||
/* Set pins 0, 1, 2 as outputs */
|
||||
uint8_t cfg = 0xF8;
|
||||
esp_err_t ret = i2c_write_reg(TCA9554_ADDR, TCA9554_REG_CONFIG, &cfg, 1);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "TCA9554 not found at 0x%02X: %s", TCA9554_ADDR, esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* Set pins 0,1,2 LOW (reset state) */
|
||||
uint8_t out = 0x00;
|
||||
i2c_write_reg(TCA9554_ADDR, TCA9554_REG_OUTPUT, &out, 1);
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
|
||||
/* Set pins 0,1,2 HIGH (power on + release reset) */
|
||||
out = 0x07;
|
||||
i2c_write_reg(TCA9554_ADDR, TCA9554_REG_OUTPUT, &out, 1);
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
|
||||
ESP_LOGI(TAG, "TCA9554 display power/reset toggled");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ---- Panel IO helpers: send commands via esp_lcd QSPI panel IO ---- */
|
||||
|
||||
static esp_err_t panel_write_cmd(uint8_t dcs_cmd, const void *data, size_t data_len)
|
||||
{
|
||||
/* Pack as 32-bit lcd_cmd: [31:24]=opcode, [23:8]=dcs_cmd, [7:0]=0 */
|
||||
uint32_t lcd_cmd = ((uint32_t)LCD_OPCODE_WRITE_CMD << 24) | ((uint32_t)dcs_cmd << 8);
|
||||
return esp_lcd_panel_io_tx_param(s_io_handle, (int)lcd_cmd, data, data_len);
|
||||
}
|
||||
|
||||
static esp_err_t panel_write_color(const void *color_data, size_t data_len)
|
||||
{
|
||||
/* RAMWR (0x2C) packed as 32-bit lcd_cmd with quad opcode */
|
||||
uint32_t lcd_cmd = ((uint32_t)LCD_OPCODE_WRITE_COLOR << 24) | (0x2C << 8);
|
||||
return esp_lcd_panel_io_tx_color(s_io_handle, (int)lcd_cmd, color_data, data_len);
|
||||
}
|
||||
|
||||
/* ---- SH8601 init sequence (from Waveshare reference) ---- */
|
||||
|
||||
typedef struct {
|
||||
uint8_t cmd;
|
||||
uint8_t data[4];
|
||||
uint8_t data_len;
|
||||
uint16_t delay_ms;
|
||||
} sh8601_init_cmd_t;
|
||||
|
||||
static const sh8601_init_cmd_t sh8601_init_cmds[] = {
|
||||
{0x11, {0x00}, 0, 120}, /* Sleep Out + 120ms */
|
||||
{0x44, {0x01, 0xD1}, 2, 0}, /* Partial area */
|
||||
{0x35, {0x00}, 1, 0}, /* Tearing Effect ON */
|
||||
{0x53, {0x20}, 1, 10}, /* Write CTRL Display */
|
||||
{0x2A, {0x00, 0x00, 0x01, 0x6F}, 4, 0}, /* CASET: 0-367 */
|
||||
{0x2B, {0x00, 0x00, 0x01, 0xBF}, 4, 0}, /* RASET: 0-447 */
|
||||
{0x51, {0x00}, 1, 10}, /* Brightness: 0 */
|
||||
{0x29, {0x00}, 0, 10}, /* Display ON */
|
||||
{0x51, {0xFF}, 1, 0}, /* Brightness: max */
|
||||
{0x00, {0x00}, 0xFF, 0}, /* End sentinel */
|
||||
};
|
||||
|
||||
static esp_err_t send_init_sequence(void)
|
||||
{
|
||||
for (int i = 0; sh8601_init_cmds[i].data_len != 0xFF; i++) {
|
||||
const sh8601_init_cmd_t *cmd = &sh8601_init_cmds[i];
|
||||
esp_err_t ret = panel_write_cmd(
|
||||
cmd->cmd,
|
||||
cmd->data_len > 0 ? cmd->data : NULL,
|
||||
cmd->data_len);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "CMD 0x%02X failed: %s", cmd->cmd, esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
if (cmd->delay_ms > 0) {
|
||||
vTaskDelay(pdMS_TO_TICKS(cmd->delay_ms));
|
||||
}
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ---- Public API ---- */
|
||||
|
||||
esp_err_t display_hal_init_panel(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Initializing Waveshare AMOLED 1.8\" (SH8601 368x448)...");
|
||||
|
||||
/* Step 1: Init I2C bus */
|
||||
esp_err_t ret = init_i2c_bus();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "I2C bus init failed");
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
/* Step 2: TCA9554 display power/reset (optional — only present on Waveshare board) */
|
||||
ret = tca9554_init_display_power();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "TCA9554 not found — assuming display power is always-on (direct wiring)");
|
||||
/* Continue without TCA9554 — the display may be powered directly */
|
||||
}
|
||||
|
||||
/* Step 3: Initialize SPI bus */
|
||||
spi_bus_config_t bus_cfg = {
|
||||
.sclk_io_num = DISP_QSPI_CLK,
|
||||
.data0_io_num = DISP_QSPI_D0,
|
||||
.data1_io_num = DISP_QSPI_D1,
|
||||
.data2_io_num = DISP_QSPI_D2,
|
||||
.data3_io_num = DISP_QSPI_D3,
|
||||
.max_transfer_sz = DISP_H_RES * DISP_V_RES * 2,
|
||||
};
|
||||
|
||||
ret = spi_bus_initialize(SPI2_HOST, &bus_cfg, SPI_DMA_CH_AUTO);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "SPI bus init failed: %s", esp_err_to_name(ret));
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
/* Step 4: Create panel IO with QSPI mode */
|
||||
esp_lcd_panel_io_spi_config_t io_config = {
|
||||
.dc_gpio_num = -1, /* No DC pin in QSPI mode */
|
||||
.cs_gpio_num = DISP_QSPI_CS,
|
||||
.pclk_hz = 40 * 1000 * 1000,
|
||||
.lcd_cmd_bits = 32, /* 32-bit command: [opcode|dcs_cmd|0x00] */
|
||||
.lcd_param_bits = 8,
|
||||
.spi_mode = 0,
|
||||
.trans_queue_depth = 10,
|
||||
.flags = {
|
||||
.quad_mode = true,
|
||||
},
|
||||
};
|
||||
|
||||
ret = esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)SPI2_HOST, &io_config, &s_io_handle);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Panel IO init failed: %s", esp_err_to_name(ret));
|
||||
spi_bus_free(SPI2_HOST);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
ESP_LOGI(TAG, "QSPI panel IO created (40MHz, quad mode)");
|
||||
|
||||
/* Step 5: Send SH8601 init sequence */
|
||||
ret = send_init_sequence();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "SH8601 init sequence failed");
|
||||
esp_lcd_panel_io_del(s_io_handle);
|
||||
spi_bus_free(SPI2_HOST);
|
||||
s_io_handle = NULL;
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
/* Step 6: Draw test pattern — cyan bar at top */
|
||||
ESP_LOGI(TAG, "Drawing test pattern...");
|
||||
uint16_t *line_buf = heap_caps_malloc(DISP_H_RES * 2, MALLOC_CAP_DMA);
|
||||
if (line_buf) {
|
||||
uint8_t caset[4] = {0, 0, (DISP_H_RES - 1) >> 8, (DISP_H_RES - 1) & 0xFF};
|
||||
uint8_t raset[4] = {0, 0, (DISP_V_RES - 1) >> 8, (DISP_V_RES - 1) & 0xFF};
|
||||
panel_write_cmd(0x2A, caset, 4);
|
||||
panel_write_cmd(0x2B, raset, 4);
|
||||
|
||||
for (int y = 0; y < DISP_V_RES; y++) {
|
||||
uint16_t color = (y < 30) ? 0x07FF : 0x0841;
|
||||
for (int x = 0; x < DISP_H_RES; x++) {
|
||||
line_buf[x] = color;
|
||||
}
|
||||
panel_write_color(line_buf, DISP_H_RES * 2);
|
||||
}
|
||||
free(line_buf);
|
||||
ESP_LOGI(TAG, "Test pattern drawn");
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "SH8601 panel init OK (%dx%d)", DISP_H_RES, DISP_V_RES);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void display_hal_draw(int x_start, int y_start, int x_end, int y_end,
|
||||
const void *color_data)
|
||||
{
|
||||
if (!s_io_handle) return;
|
||||
|
||||
/* SH8601 requires coordinates divisible by 2 */
|
||||
x_start &= ~1;
|
||||
y_start &= ~1;
|
||||
if (x_end & 1) x_end++;
|
||||
if (y_end & 1) y_end++;
|
||||
if (x_end > DISP_H_RES) x_end = DISP_H_RES;
|
||||
if (y_end > DISP_V_RES) y_end = DISP_V_RES;
|
||||
|
||||
uint8_t caset[4] = {
|
||||
(x_start >> 8) & 0xFF, x_start & 0xFF,
|
||||
((x_end - 1) >> 8) & 0xFF, (x_end - 1) & 0xFF,
|
||||
};
|
||||
panel_write_cmd(0x2A, caset, 4);
|
||||
|
||||
uint8_t raset[4] = {
|
||||
(y_start >> 8) & 0xFF, y_start & 0xFF,
|
||||
((y_end - 1) >> 8) & 0xFF, (y_end - 1) & 0xFF,
|
||||
};
|
||||
panel_write_cmd(0x2B, raset, 4);
|
||||
|
||||
size_t len = (x_end - x_start) * (y_end - y_start) * 2;
|
||||
panel_write_color(color_data, len);
|
||||
}
|
||||
|
||||
esp_err_t display_hal_init_touch(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Probing FT3168 touch controller...");
|
||||
|
||||
if (!s_i2c_initialized) {
|
||||
esp_err_t ret = init_i2c_bus();
|
||||
if (ret != ESP_OK) return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
gpio_config_t int_cfg = {
|
||||
.pin_bit_mask = (1ULL << TOUCH_INT_PIN),
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_ENABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&int_cfg);
|
||||
|
||||
uint8_t chip_id = 0;
|
||||
esp_err_t ret = i2c_read_reg(FT3168_ADDR, 0xA8, &chip_id, 1);
|
||||
if (ret != ESP_OK || chip_id == 0x00 || chip_id == 0xFF) {
|
||||
ESP_LOGW(TAG, "FT3168 not found (ret=%s, id=0x%02X)", esp_err_to_name(ret), chip_id);
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
|
||||
s_touch_initialized = true;
|
||||
ESP_LOGI(TAG, "FT3168 touch init OK (chip_id=0x%02X)", chip_id);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool display_hal_touch_read(uint16_t *x, uint16_t *y)
|
||||
{
|
||||
if (!s_touch_initialized) return false;
|
||||
|
||||
uint8_t buf[7] = {0};
|
||||
esp_err_t ret = i2c_read_reg(FT3168_ADDR, 0x01, buf, 7);
|
||||
if (ret != ESP_OK) return false;
|
||||
|
||||
uint8_t num_points = buf[1];
|
||||
if (num_points == 0 || num_points > 2) return false;
|
||||
|
||||
*x = ((buf[2] & 0x0F) << 8) | buf[3];
|
||||
*y = ((buf[4] & 0x0F) << 8) | buf[5];
|
||||
return true;
|
||||
}
|
||||
|
||||
void display_hal_set_brightness(uint8_t percent)
|
||||
{
|
||||
if (!s_io_handle) return;
|
||||
if (percent > 100) percent = 100;
|
||||
uint8_t val = (uint8_t)((uint32_t)percent * 255 / 100);
|
||||
panel_write_cmd(0x51, &val, 1);
|
||||
}
|
||||
|
||||
#endif /* CONFIG_DISPLAY_ENABLE */
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @file display_hal.h
|
||||
* @brief ADR-045: RM67162 QSPI AMOLED + CST816S touch HAL.
|
||||
*
|
||||
* Hardware abstraction for the LilyGO T-Display-S3 AMOLED panel.
|
||||
* Probes hardware at boot; returns ESP_ERR_NOT_FOUND if absent.
|
||||
*/
|
||||
|
||||
#ifndef DISPLAY_HAL_H
|
||||
#define DISPLAY_HAL_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Probe and initialize the RM67162 QSPI AMOLED panel.
|
||||
*
|
||||
* Configures QSPI bus, sends panel init sequence, and fills
|
||||
* the screen with dark background to confirm it works.
|
||||
* Returns ESP_ERR_NOT_FOUND if the panel does not respond.
|
||||
*
|
||||
* @return ESP_OK on success, ESP_ERR_NOT_FOUND if no display detected.
|
||||
*/
|
||||
esp_err_t display_hal_init_panel(void);
|
||||
|
||||
/**
|
||||
* Draw a rectangle of pixels to the AMOLED.
|
||||
* Sends CASET + RASET + RAMWR directly via QSPI.
|
||||
*
|
||||
* @param x_start Left column (inclusive).
|
||||
* @param y_start Top row (inclusive).
|
||||
* @param x_end Right column (exclusive).
|
||||
* @param y_end Bottom row (exclusive).
|
||||
* @param color_data RGB565 pixel data, (x_end-x_start)*(y_end-y_start) pixels.
|
||||
*/
|
||||
void display_hal_draw(int x_start, int y_start, int x_end, int y_end,
|
||||
const void *color_data);
|
||||
|
||||
/**
|
||||
* Probe and initialize the CST816S capacitive touch controller.
|
||||
*
|
||||
* @return ESP_OK on success, ESP_ERR_NOT_FOUND if no touch IC detected.
|
||||
*/
|
||||
esp_err_t display_hal_init_touch(void);
|
||||
|
||||
/**
|
||||
* Read touch point (non-blocking).
|
||||
*
|
||||
* @param[out] x Touch X coordinate (0..535).
|
||||
* @param[out] y Touch Y coordinate (0..239).
|
||||
* @return true if touch is active, false if released.
|
||||
*/
|
||||
bool display_hal_touch_read(uint16_t *x, uint16_t *y);
|
||||
|
||||
/**
|
||||
* Set AMOLED brightness via MIPI DCS command.
|
||||
*
|
||||
* @param percent Brightness 0-100.
|
||||
*/
|
||||
void display_hal_set_brightness(uint8_t percent);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* DISPLAY_HAL_H */
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* @file display_task.c
|
||||
* @brief ADR-045: FreeRTOS display task — LVGL pump on Core 0, priority 1.
|
||||
*
|
||||
* Gracefully skips if RM67162 panel or SPIRAM is absent.
|
||||
* Reads from edge_get_vitals() / edge_get_multi_person() (thread-safe).
|
||||
*/
|
||||
|
||||
#include "display_task.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if CONFIG_DISPLAY_ENABLE
|
||||
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "lvgl.h"
|
||||
|
||||
#include "display_hal.h"
|
||||
#include "display_ui.h"
|
||||
|
||||
#define DISP_H_RES 368
|
||||
#define DISP_V_RES 448
|
||||
|
||||
static const char *TAG = "disp_task";
|
||||
|
||||
/* ---- Config ---- */
|
||||
#ifdef CONFIG_DISPLAY_FPS_LIMIT
|
||||
#define DISP_FPS_LIMIT CONFIG_DISPLAY_FPS_LIMIT
|
||||
#else
|
||||
#define DISP_FPS_LIMIT 30
|
||||
#endif
|
||||
|
||||
#define DISP_TASK_STACK (8 * 1024)
|
||||
#define DISP_TASK_PRIORITY 1
|
||||
#define DISP_TASK_CORE 0
|
||||
|
||||
#define DISP_BUF_LINES 40
|
||||
|
||||
/* ---- LVGL flush callback — calls display_hal_draw directly ---- */
|
||||
static void lvgl_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p)
|
||||
{
|
||||
display_hal_draw(area->x1, area->y1, area->x2 + 1, area->y2 + 1, color_p);
|
||||
lv_disp_flush_ready(drv);
|
||||
}
|
||||
|
||||
/* ---- LVGL touch input callback ---- */
|
||||
static void lvgl_touch_cb(lv_indev_drv_t *drv, lv_indev_data_t *data)
|
||||
{
|
||||
uint16_t x, y;
|
||||
if (display_hal_touch_read(&x, &y)) {
|
||||
data->point.x = x;
|
||||
data->point.y = y;
|
||||
data->state = LV_INDEV_STATE_PRESSED;
|
||||
} else {
|
||||
data->state = LV_INDEV_STATE_RELEASED;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Display task ---- */
|
||||
static void display_task(void *arg)
|
||||
{
|
||||
const TickType_t frame_period = pdMS_TO_TICKS(1000 / DISP_FPS_LIMIT);
|
||||
|
||||
ESP_LOGI(TAG, "Display task running on Core %d, %d fps limit",
|
||||
xPortGetCoreID(), DISP_FPS_LIMIT);
|
||||
|
||||
display_ui_create(lv_scr_act());
|
||||
|
||||
TickType_t last_wake = xTaskGetTickCount();
|
||||
while (1) {
|
||||
display_ui_update();
|
||||
lv_timer_handler();
|
||||
vTaskDelayUntil(&last_wake, frame_period);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Public API ---- */
|
||||
|
||||
esp_err_t display_task_start(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "Initializing display subsystem...");
|
||||
|
||||
bool use_psram = false;
|
||||
#if CONFIG_SPIRAM
|
||||
size_t psram_free = heap_caps_get_free_size(MALLOC_CAP_SPIRAM);
|
||||
if (psram_free >= 64 * 1024) {
|
||||
use_psram = true;
|
||||
ESP_LOGI(TAG, "PSRAM available: %u KB — using PSRAM buffers", (unsigned)(psram_free / 1024));
|
||||
} else {
|
||||
ESP_LOGW(TAG, "PSRAM too small (%u bytes) — falling back to internal DMA memory", (unsigned)psram_free);
|
||||
}
|
||||
#else
|
||||
ESP_LOGW(TAG, "SPIRAM not enabled — using internal DMA memory (smaller buffers)");
|
||||
#endif
|
||||
|
||||
/* Probe display hardware */
|
||||
esp_err_t ret = display_hal_init_panel();
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Display not available — running headless");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* Init touch (optional) */
|
||||
esp_err_t touch_ret = display_hal_init_touch();
|
||||
|
||||
/* Initialize LVGL */
|
||||
lv_init();
|
||||
|
||||
/* Double-buffered draw buffers — prefer PSRAM, fall back to internal DMA */
|
||||
size_t buf_lines = use_psram ? DISP_BUF_LINES : 10; /* Smaller buffers without PSRAM */
|
||||
size_t buf_size = DISP_H_RES * buf_lines * sizeof(lv_color_t);
|
||||
uint32_t alloc_caps = use_psram ? MALLOC_CAP_SPIRAM : (MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
|
||||
lv_color_t *buf1 = heap_caps_malloc(buf_size, alloc_caps);
|
||||
lv_color_t *buf2 = heap_caps_malloc(buf_size, alloc_caps);
|
||||
if (!buf1 || !buf2) {
|
||||
ESP_LOGE(TAG, "Failed to allocate LVGL buffers (%u bytes, caps=0x%lx)",
|
||||
(unsigned)buf_size, (unsigned long)alloc_caps);
|
||||
if (buf1) free(buf1);
|
||||
if (buf2) free(buf2);
|
||||
return ESP_OK;
|
||||
}
|
||||
ESP_LOGI(TAG, "LVGL buffers: 2x %u bytes (%u lines, %s)",
|
||||
(unsigned)buf_size, (unsigned)buf_lines, use_psram ? "PSRAM" : "internal DMA");
|
||||
|
||||
static lv_disp_draw_buf_t draw_buf;
|
||||
lv_disp_draw_buf_init(&draw_buf, buf1, buf2, DISP_H_RES * buf_lines);
|
||||
|
||||
static lv_disp_drv_t disp_drv;
|
||||
lv_disp_drv_init(&disp_drv);
|
||||
disp_drv.hor_res = DISP_H_RES;
|
||||
disp_drv.ver_res = DISP_V_RES;
|
||||
disp_drv.flush_cb = lvgl_flush_cb;
|
||||
disp_drv.draw_buf = &draw_buf;
|
||||
lv_disp_drv_register(&disp_drv);
|
||||
|
||||
if (touch_ret == ESP_OK) {
|
||||
static lv_indev_drv_t indev_drv;
|
||||
lv_indev_drv_init(&indev_drv);
|
||||
indev_drv.type = LV_INDEV_TYPE_POINTER;
|
||||
indev_drv.read_cb = lvgl_touch_cb;
|
||||
lv_indev_drv_register(&indev_drv);
|
||||
ESP_LOGI(TAG, "Touch input registered");
|
||||
}
|
||||
|
||||
BaseType_t xret = xTaskCreatePinnedToCore(
|
||||
display_task, "display", DISP_TASK_STACK,
|
||||
NULL, DISP_TASK_PRIORITY, NULL, DISP_TASK_CORE);
|
||||
|
||||
if (xret != pdPASS) {
|
||||
ESP_LOGE(TAG, "Failed to create display task");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Display task started (Core %d, priority %d, %d fps)",
|
||||
DISP_TASK_CORE, DISP_TASK_PRIORITY, DISP_FPS_LIMIT);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
#else /* !CONFIG_DISPLAY_ENABLE */
|
||||
|
||||
esp_err_t display_task_start(void)
|
||||
{
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
#endif /* CONFIG_DISPLAY_ENABLE */
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @file display_task.h
|
||||
* @brief ADR-045: FreeRTOS display task — LVGL pump on Core 0.
|
||||
*/
|
||||
|
||||
#ifndef DISPLAY_TASK_H
|
||||
#define DISPLAY_TASK_H
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Start the display task on Core 0, priority 1.
|
||||
*
|
||||
* Probes for RM67162 panel and SPIRAM. If either is absent,
|
||||
* logs a warning and returns ESP_OK (graceful skip).
|
||||
*
|
||||
* @return ESP_OK always (display is optional).
|
||||
*/
|
||||
esp_err_t display_task_start(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* DISPLAY_TASK_H */
|
||||
@@ -0,0 +1,387 @@
|
||||
/**
|
||||
* @file display_ui.c
|
||||
* @brief ADR-045: LVGL 4-view swipeable UI — Dashboard | Vitals | Presence | System.
|
||||
*
|
||||
* Dark theme (#0a0a0f background) with cyan (#00d4ff) accent.
|
||||
* Glowing line effects via layered semi-transparent chart series.
|
||||
*/
|
||||
|
||||
#include "display_ui.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#if CONFIG_DISPLAY_ENABLE
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
#include "esp_system.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "edge_processing.h"
|
||||
|
||||
static const char *TAG = "disp_ui";
|
||||
|
||||
/* ---- Theme colors ---- */
|
||||
#define COLOR_BG lv_color_make(0x0A, 0x0A, 0x0F)
|
||||
#define COLOR_CYAN lv_color_make(0x00, 0xD4, 0xFF)
|
||||
#define COLOR_AMBER lv_color_make(0xFF, 0xB0, 0x00)
|
||||
#define COLOR_GREEN lv_color_make(0x00, 0xFF, 0x80)
|
||||
#define COLOR_RED lv_color_make(0xFF, 0x40, 0x40)
|
||||
#define COLOR_DIM lv_color_make(0x30, 0x30, 0x40)
|
||||
#define COLOR_TEXT lv_color_make(0xCC, 0xCC, 0xDD)
|
||||
#define COLOR_TEXT_DIM lv_color_make(0x66, 0x66, 0x77)
|
||||
|
||||
/* ---- Chart data points ---- */
|
||||
#define CHART_POINTS 60
|
||||
|
||||
/* ---- View handles ---- */
|
||||
static lv_obj_t *s_tileview = NULL;
|
||||
|
||||
/* Dashboard */
|
||||
static lv_obj_t *s_dash_chart = NULL;
|
||||
static lv_chart_series_t *s_csi_series = NULL;
|
||||
static lv_obj_t *s_dash_persons = NULL;
|
||||
static lv_obj_t *s_dash_rssi = NULL;
|
||||
static lv_obj_t *s_dash_motion = NULL;
|
||||
|
||||
/* Vitals */
|
||||
static lv_obj_t *s_vital_chart = NULL;
|
||||
static lv_chart_series_t *s_breath_series = NULL;
|
||||
static lv_chart_series_t *s_hr_series = NULL;
|
||||
static lv_obj_t *s_vital_bpm_br = NULL;
|
||||
static lv_obj_t *s_vital_bpm_hr = NULL;
|
||||
|
||||
/* Presence */
|
||||
#define GRID_COLS 4
|
||||
#define GRID_ROWS 4
|
||||
static lv_obj_t *s_grid_cells[GRID_COLS * GRID_ROWS];
|
||||
static lv_obj_t *s_presence_label = NULL;
|
||||
|
||||
/* System */
|
||||
static lv_obj_t *s_sys_cpu = NULL;
|
||||
static lv_obj_t *s_sys_heap = NULL;
|
||||
static lv_obj_t *s_sys_psram = NULL;
|
||||
static lv_obj_t *s_sys_rssi = NULL;
|
||||
static lv_obj_t *s_sys_uptime = NULL;
|
||||
static lv_obj_t *s_sys_fps = NULL;
|
||||
static lv_obj_t *s_sys_node = NULL;
|
||||
|
||||
/* ---- Style helpers ---- */
|
||||
static lv_style_t s_style_bg;
|
||||
static lv_style_t s_style_label;
|
||||
static lv_style_t s_style_label_big;
|
||||
static bool s_styles_inited = false;
|
||||
|
||||
static void init_styles(void)
|
||||
{
|
||||
if (s_styles_inited) return;
|
||||
s_styles_inited = true;
|
||||
|
||||
lv_style_init(&s_style_bg);
|
||||
lv_style_set_bg_color(&s_style_bg, COLOR_BG);
|
||||
lv_style_set_bg_opa(&s_style_bg, LV_OPA_COVER);
|
||||
lv_style_set_border_width(&s_style_bg, 0);
|
||||
lv_style_set_pad_all(&s_style_bg, 4);
|
||||
|
||||
lv_style_init(&s_style_label);
|
||||
lv_style_set_text_color(&s_style_label, COLOR_TEXT);
|
||||
lv_style_set_text_font(&s_style_label, &lv_font_montserrat_14);
|
||||
|
||||
lv_style_init(&s_style_label_big);
|
||||
lv_style_set_text_color(&s_style_label_big, COLOR_CYAN);
|
||||
lv_style_set_text_font(&s_style_label_big, &lv_font_montserrat_14);
|
||||
}
|
||||
|
||||
static lv_obj_t *make_label(lv_obj_t *parent, const char *text, const lv_style_t *style)
|
||||
{
|
||||
lv_obj_t *lbl = lv_label_create(parent);
|
||||
lv_label_set_text(lbl, text);
|
||||
if (style) lv_obj_add_style(lbl, (lv_style_t *)style, 0);
|
||||
return lbl;
|
||||
}
|
||||
|
||||
static lv_obj_t *make_tile(lv_obj_t *tv, uint8_t col, uint8_t row)
|
||||
{
|
||||
lv_obj_t *tile = lv_tileview_add_tile(tv, col, row, LV_DIR_HOR);
|
||||
lv_obj_add_style(tile, &s_style_bg, 0);
|
||||
return tile;
|
||||
}
|
||||
|
||||
/* ---- View 0: Dashboard ---- */
|
||||
static void create_dashboard(lv_obj_t *tile)
|
||||
{
|
||||
make_label(tile, "CSI Dashboard", &s_style_label);
|
||||
|
||||
/* CSI amplitude chart */
|
||||
s_dash_chart = lv_chart_create(tile);
|
||||
lv_obj_set_size(s_dash_chart, 400, 130);
|
||||
lv_obj_align(s_dash_chart, LV_ALIGN_TOP_LEFT, 0, 24);
|
||||
lv_chart_set_type(s_dash_chart, LV_CHART_TYPE_LINE);
|
||||
lv_chart_set_point_count(s_dash_chart, CHART_POINTS);
|
||||
lv_chart_set_range(s_dash_chart, LV_CHART_AXIS_PRIMARY_Y, 0, 100);
|
||||
lv_obj_set_style_bg_color(s_dash_chart, COLOR_BG, 0);
|
||||
lv_obj_set_style_border_color(s_dash_chart, COLOR_DIM, 0);
|
||||
lv_obj_set_style_line_width(s_dash_chart, 0, LV_PART_TICKS);
|
||||
|
||||
s_csi_series = lv_chart_add_series(s_dash_chart, COLOR_CYAN, LV_CHART_AXIS_PRIMARY_Y);
|
||||
|
||||
/* Stats panel on the right */
|
||||
lv_obj_t *panel = lv_obj_create(tile);
|
||||
lv_obj_set_size(panel, 120, 130);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_RIGHT, 0, 24);
|
||||
lv_obj_set_style_bg_color(panel, lv_color_make(0x12, 0x12, 0x1A), 0);
|
||||
lv_obj_set_style_border_width(panel, 1, 0);
|
||||
lv_obj_set_style_border_color(panel, COLOR_DIM, 0);
|
||||
lv_obj_set_style_pad_all(panel, 8, 0);
|
||||
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(panel, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
||||
|
||||
make_label(panel, "Persons", &s_style_label);
|
||||
s_dash_persons = make_label(panel, "0", &s_style_label_big);
|
||||
|
||||
s_dash_rssi = make_label(panel, "RSSI: --", &s_style_label);
|
||||
s_dash_motion = make_label(panel, "Motion: 0.0", &s_style_label);
|
||||
}
|
||||
|
||||
/* ---- View 1: Vitals ---- */
|
||||
static void create_vitals(lv_obj_t *tile)
|
||||
{
|
||||
make_label(tile, "Vital Signs", &s_style_label);
|
||||
|
||||
s_vital_chart = lv_chart_create(tile);
|
||||
lv_obj_set_size(s_vital_chart, 480, 150);
|
||||
lv_obj_align(s_vital_chart, LV_ALIGN_TOP_LEFT, 0, 24);
|
||||
lv_chart_set_type(s_vital_chart, LV_CHART_TYPE_LINE);
|
||||
lv_chart_set_point_count(s_vital_chart, CHART_POINTS);
|
||||
lv_chart_set_range(s_vital_chart, LV_CHART_AXIS_PRIMARY_Y, 0, 120);
|
||||
lv_obj_set_style_bg_color(s_vital_chart, COLOR_BG, 0);
|
||||
lv_obj_set_style_border_color(s_vital_chart, COLOR_DIM, 0);
|
||||
lv_obj_set_style_line_width(s_vital_chart, 0, LV_PART_TICKS);
|
||||
|
||||
/* Breathing series (cyan) */
|
||||
s_breath_series = lv_chart_add_series(s_vital_chart, COLOR_CYAN, LV_CHART_AXIS_PRIMARY_Y);
|
||||
/* Heart rate series (amber) */
|
||||
s_hr_series = lv_chart_add_series(s_vital_chart, COLOR_AMBER, LV_CHART_AXIS_PRIMARY_Y);
|
||||
|
||||
/* BPM readouts */
|
||||
s_vital_bpm_br = make_label(tile, "Breathing: -- BPM", &s_style_label);
|
||||
lv_obj_align(s_vital_bpm_br, LV_ALIGN_BOTTOM_LEFT, 4, -8);
|
||||
lv_obj_set_style_text_color(s_vital_bpm_br, COLOR_CYAN, 0);
|
||||
|
||||
s_vital_bpm_hr = make_label(tile, "Heart Rate: -- BPM", &s_style_label);
|
||||
lv_obj_align(s_vital_bpm_hr, LV_ALIGN_BOTTOM_RIGHT, -4, -8);
|
||||
lv_obj_set_style_text_color(s_vital_bpm_hr, COLOR_AMBER, 0);
|
||||
}
|
||||
|
||||
/* ---- View 2: Presence Grid ---- */
|
||||
static void create_presence(lv_obj_t *tile)
|
||||
{
|
||||
make_label(tile, "Occupancy Map", &s_style_label);
|
||||
|
||||
int cell_w = 50;
|
||||
int cell_h = 45;
|
||||
int x_off = (368 - GRID_COLS * (cell_w + 4)) / 2;
|
||||
int y_off = 30;
|
||||
|
||||
for (int r = 0; r < GRID_ROWS; r++) {
|
||||
for (int c = 0; c < GRID_COLS; c++) {
|
||||
lv_obj_t *cell = lv_obj_create(tile);
|
||||
lv_obj_set_size(cell, cell_w, cell_h);
|
||||
lv_obj_set_pos(cell, x_off + c * (cell_w + 4), y_off + r * (cell_h + 4));
|
||||
lv_obj_set_style_bg_color(cell, COLOR_DIM, 0);
|
||||
lv_obj_set_style_bg_opa(cell, LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_color(cell, COLOR_DIM, 0);
|
||||
lv_obj_set_style_border_width(cell, 1, 0);
|
||||
lv_obj_set_style_radius(cell, 4, 0);
|
||||
s_grid_cells[r * GRID_COLS + c] = cell;
|
||||
}
|
||||
}
|
||||
|
||||
s_presence_label = make_label(tile, "Persons: 0", &s_style_label);
|
||||
lv_obj_align(s_presence_label, LV_ALIGN_BOTTOM_MID, 0, -8);
|
||||
}
|
||||
|
||||
/* ---- View 3: System ---- */
|
||||
static void create_system(lv_obj_t *tile)
|
||||
{
|
||||
make_label(tile, "System Info", &s_style_label);
|
||||
|
||||
lv_obj_t *panel = lv_obj_create(tile);
|
||||
lv_obj_set_size(panel, 500, 180);
|
||||
lv_obj_align(panel, LV_ALIGN_TOP_LEFT, 0, 24);
|
||||
lv_obj_set_style_bg_color(panel, lv_color_make(0x12, 0x12, 0x1A), 0);
|
||||
lv_obj_set_style_border_width(panel, 1, 0);
|
||||
lv_obj_set_style_border_color(panel, COLOR_DIM, 0);
|
||||
lv_obj_set_style_pad_all(panel, 10, 0);
|
||||
lv_obj_set_flex_flow(panel, LV_FLEX_FLOW_COLUMN);
|
||||
lv_obj_set_flex_align(panel, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
|
||||
|
||||
s_sys_node = make_label(panel, "Node: --", &s_style_label);
|
||||
s_sys_cpu = make_label(panel, "CPU: --%", &s_style_label);
|
||||
s_sys_heap = make_label(panel, "Heap: -- KB free", &s_style_label);
|
||||
s_sys_psram = make_label(panel, "PSRAM: -- KB free",&s_style_label);
|
||||
s_sys_rssi = make_label(panel, "WiFi RSSI: --", &s_style_label);
|
||||
s_sys_uptime = make_label(panel, "Uptime: --", &s_style_label);
|
||||
s_sys_fps = make_label(panel, "FPS: --", &s_style_label);
|
||||
}
|
||||
|
||||
/* ---- Public API ---- */
|
||||
|
||||
void display_ui_create(lv_obj_t *parent)
|
||||
{
|
||||
init_styles();
|
||||
|
||||
s_tileview = lv_tileview_create(parent);
|
||||
lv_obj_add_style(s_tileview, &s_style_bg, 0);
|
||||
lv_obj_set_style_bg_color(s_tileview, COLOR_BG, 0);
|
||||
|
||||
lv_obj_t *t0 = make_tile(s_tileview, 0, 0);
|
||||
lv_obj_t *t1 = make_tile(s_tileview, 1, 0);
|
||||
lv_obj_t *t2 = make_tile(s_tileview, 2, 0);
|
||||
lv_obj_t *t3 = make_tile(s_tileview, 3, 0);
|
||||
|
||||
create_dashboard(t0);
|
||||
create_vitals(t1);
|
||||
create_presence(t2);
|
||||
create_system(t3);
|
||||
|
||||
ESP_LOGI(TAG, "UI created: 4 views (Dashboard|Vitals|Presence|System)");
|
||||
}
|
||||
|
||||
/* ---- FPS tracking ---- */
|
||||
static uint32_t s_frame_count = 0;
|
||||
static uint32_t s_last_fps_time = 0;
|
||||
static uint32_t s_current_fps = 0;
|
||||
|
||||
void display_ui_update(void)
|
||||
{
|
||||
/* FPS counter */
|
||||
s_frame_count++;
|
||||
uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000);
|
||||
if (now_ms - s_last_fps_time >= 1000) {
|
||||
s_current_fps = s_frame_count;
|
||||
s_frame_count = 0;
|
||||
s_last_fps_time = now_ms;
|
||||
}
|
||||
|
||||
/* Read edge data (thread-safe) */
|
||||
edge_vitals_pkt_t vitals;
|
||||
bool has_vitals = edge_get_vitals(&vitals);
|
||||
|
||||
edge_person_vitals_t persons[EDGE_MAX_PERSONS];
|
||||
uint8_t n_active = 0;
|
||||
edge_get_multi_person(persons, &n_active);
|
||||
|
||||
/* ---- Dashboard update ---- */
|
||||
if (s_dash_chart && has_vitals) {
|
||||
/* Push motion energy as amplitude proxy (scaled 0-100) */
|
||||
int val = (int)(vitals.motion_energy * 10.0f);
|
||||
if (val > 100) val = 100;
|
||||
if (val < 0) val = 0;
|
||||
lv_chart_set_next_value(s_dash_chart, s_csi_series, val);
|
||||
}
|
||||
|
||||
if (s_dash_persons) {
|
||||
char buf[8];
|
||||
snprintf(buf, sizeof(buf), "%u", has_vitals ? vitals.n_persons : 0);
|
||||
lv_label_set_text(s_dash_persons, buf);
|
||||
}
|
||||
|
||||
if (s_dash_rssi && has_vitals) {
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "RSSI: %d", vitals.rssi);
|
||||
lv_label_set_text(s_dash_rssi, buf);
|
||||
}
|
||||
|
||||
if (s_dash_motion && has_vitals) {
|
||||
char buf[24];
|
||||
snprintf(buf, sizeof(buf), "Motion: %.1f", (double)vitals.motion_energy);
|
||||
lv_label_set_text(s_dash_motion, buf);
|
||||
}
|
||||
|
||||
/* ---- Vitals update ---- */
|
||||
if (s_vital_chart && has_vitals) {
|
||||
int br = (int)(vitals.breathing_rate / 100); /* Fixed-point to int BPM */
|
||||
int hr = (int)(vitals.heartrate / 10000);
|
||||
if (br > 120) br = 120;
|
||||
if (hr > 120) hr = 120;
|
||||
lv_chart_set_next_value(s_vital_chart, s_breath_series, br);
|
||||
lv_chart_set_next_value(s_vital_chart, s_hr_series, hr);
|
||||
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "Breathing: %d BPM", br);
|
||||
lv_label_set_text(s_vital_bpm_br, buf);
|
||||
|
||||
snprintf(buf, sizeof(buf), "Heart Rate: %d BPM", hr);
|
||||
lv_label_set_text(s_vital_bpm_hr, buf);
|
||||
}
|
||||
|
||||
/* ---- Presence grid update ---- */
|
||||
if (has_vitals) {
|
||||
/* Simple visualization: color cells based on motion energy distribution */
|
||||
float energy = vitals.motion_energy;
|
||||
uint8_t active_cells = (uint8_t)(energy * 2); /* Scale for visibility */
|
||||
if (active_cells > GRID_COLS * GRID_ROWS) active_cells = GRID_COLS * GRID_ROWS;
|
||||
|
||||
for (int i = 0; i < GRID_COLS * GRID_ROWS; i++) {
|
||||
if (i < active_cells) {
|
||||
/* Color gradient: green → amber → red based on intensity */
|
||||
if (energy > 5.0f) {
|
||||
lv_obj_set_style_bg_color(s_grid_cells[i], COLOR_RED, 0);
|
||||
} else if (energy > 2.0f) {
|
||||
lv_obj_set_style_bg_color(s_grid_cells[i], COLOR_AMBER, 0);
|
||||
} else {
|
||||
lv_obj_set_style_bg_color(s_grid_cells[i], COLOR_GREEN, 0);
|
||||
}
|
||||
} else {
|
||||
lv_obj_set_style_bg_color(s_grid_cells[i], COLOR_DIM, 0);
|
||||
}
|
||||
}
|
||||
|
||||
char buf[20];
|
||||
snprintf(buf, sizeof(buf), "Persons: %u", vitals.n_persons);
|
||||
lv_label_set_text(s_presence_label, buf);
|
||||
}
|
||||
|
||||
/* ---- System info update ---- */
|
||||
{
|
||||
char buf[48];
|
||||
|
||||
#ifdef CONFIG_CSI_NODE_ID
|
||||
snprintf(buf, sizeof(buf), "Node: %d", CONFIG_CSI_NODE_ID);
|
||||
#else
|
||||
snprintf(buf, sizeof(buf), "Node: --");
|
||||
#endif
|
||||
lv_label_set_text(s_sys_node, buf);
|
||||
|
||||
snprintf(buf, sizeof(buf), "Heap: %lu KB free",
|
||||
(unsigned long)(esp_get_free_heap_size() / 1024));
|
||||
lv_label_set_text(s_sys_heap, buf);
|
||||
|
||||
#if CONFIG_SPIRAM
|
||||
snprintf(buf, sizeof(buf), "PSRAM: %lu KB free",
|
||||
(unsigned long)(heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024));
|
||||
#else
|
||||
snprintf(buf, sizeof(buf), "PSRAM: N/A");
|
||||
#endif
|
||||
lv_label_set_text(s_sys_psram, buf);
|
||||
|
||||
if (has_vitals) {
|
||||
snprintf(buf, sizeof(buf), "WiFi RSSI: %d dBm", vitals.rssi);
|
||||
lv_label_set_text(s_sys_rssi, buf);
|
||||
}
|
||||
|
||||
uint32_t uptime_s = (uint32_t)(esp_timer_get_time() / 1000000);
|
||||
uint32_t h = uptime_s / 3600;
|
||||
uint32_t m = (uptime_s % 3600) / 60;
|
||||
uint32_t s = uptime_s % 60;
|
||||
snprintf(buf, sizeof(buf), "Uptime: %luh %02lum %02lus",
|
||||
(unsigned long)h, (unsigned long)m, (unsigned long)s);
|
||||
lv_label_set_text(s_sys_uptime, buf);
|
||||
|
||||
snprintf(buf, sizeof(buf), "FPS: %lu", (unsigned long)s_current_fps);
|
||||
lv_label_set_text(s_sys_fps, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* CONFIG_DISPLAY_ENABLE */
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @file display_ui.h
|
||||
* @brief ADR-045: LVGL 4-view swipeable UI for CSI node stats.
|
||||
*
|
||||
* Views: Dashboard | Vitals | Presence | System
|
||||
* Dark theme with cyan (#00d4ff) accent.
|
||||
*/
|
||||
|
||||
#ifndef DISPLAY_UI_H
|
||||
#define DISPLAY_UI_H
|
||||
|
||||
#include "lvgl.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Create all LVGL views on the given tileview parent. */
|
||||
void display_ui_create(lv_obj_t *parent);
|
||||
|
||||
/**
|
||||
* Update all views with latest data. Called every display refresh cycle.
|
||||
* Reads from edge_get_vitals() and edge_get_multi_person() internally.
|
||||
*/
|
||||
void display_ui_update(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* DISPLAY_UI_H */
|
||||
@@ -0,0 +1,10 @@
|
||||
## ESP-IDF Managed Component Dependencies (ADR-045)
|
||||
dependencies:
|
||||
## LVGL graphics library
|
||||
lvgl/lvgl: "~8.3"
|
||||
|
||||
## CST816S capacitive touch driver
|
||||
espressif/esp_lcd_touch_cst816s: "^1.0"
|
||||
|
||||
## LCD touch abstraction
|
||||
espressif/esp_lcd_touch: "^1.0"
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @file lv_conf.h
|
||||
* @brief LVGL compile-time configuration for ESP32-S3 AMOLED display (ADR-045).
|
||||
*
|
||||
* Tuned for RM67162 536x240 QSPI AMOLED with 8MB PSRAM.
|
||||
* Color depth: RGB565 (16-bit) for QSPI bandwidth.
|
||||
* Double-buffered in SPIRAM, 30fps target.
|
||||
*/
|
||||
|
||||
#ifndef LV_CONF_H
|
||||
#define LV_CONF_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* ---- Core ---- */
|
||||
#define LV_COLOR_DEPTH 16
|
||||
#define LV_COLOR_16_SWAP 1 /* Byte-swap for SPI/QSPI displays */
|
||||
#define LV_MEM_CUSTOM 1 /* Use ESP-IDF heap instead of LVGL's internal allocator */
|
||||
#define LV_MEM_CUSTOM_INCLUDE <stdlib.h>
|
||||
#define LV_MEM_CUSTOM_ALLOC malloc
|
||||
#define LV_MEM_CUSTOM_FREE free
|
||||
#define LV_MEM_CUSTOM_REALLOC realloc
|
||||
|
||||
/* ---- Display ---- */
|
||||
#define LV_HOR_RES_MAX 368
|
||||
#define LV_VER_RES_MAX 448
|
||||
#define LV_DPI_DEF 200
|
||||
|
||||
/* ---- Tick (provided by esp_timer in display_task.c) ---- */
|
||||
#define LV_TICK_CUSTOM 1
|
||||
#define LV_TICK_CUSTOM_INCLUDE "esp_timer.h"
|
||||
#define LV_TICK_CUSTOM_SYS_TIME_EXPR ((uint32_t)(esp_timer_get_time() / 1000))
|
||||
|
||||
/* ---- Drawing ---- */
|
||||
#define LV_DRAW_COMPLEX 1
|
||||
#define LV_SHADOW_CACHE_SIZE 0
|
||||
#define LV_CIRCLE_CACHE_SIZE 4
|
||||
#define LV_IMG_CACHE_DEF_SIZE 0
|
||||
|
||||
/* ---- Fonts ---- */
|
||||
#define LV_FONT_MONTSERRAT_14 1
|
||||
#define LV_FONT_MONTSERRAT_20 1
|
||||
#define LV_FONT_DEFAULT &lv_font_montserrat_14
|
||||
|
||||
/* ---- Widgets ---- */
|
||||
#define LV_USE_ARC 1
|
||||
#define LV_USE_BAR 1
|
||||
#define LV_USE_BTN 0
|
||||
#define LV_USE_BTNMATRIX 0
|
||||
#define LV_USE_CANVAS 0
|
||||
#define LV_USE_CHECKBOX 0
|
||||
#define LV_USE_DROPDOWN 0
|
||||
#define LV_USE_IMG 0
|
||||
#define LV_USE_LABEL 1
|
||||
#define LV_USE_LINE 1
|
||||
#define LV_USE_ROLLER 0
|
||||
#define LV_USE_SLIDER 0
|
||||
#define LV_USE_SWITCH 0
|
||||
#define LV_USE_TEXTAREA 0
|
||||
#define LV_USE_TABLE 0
|
||||
|
||||
/* ---- Extra widgets ---- */
|
||||
#define LV_USE_CHART 1
|
||||
#define LV_CHART_AXIS_TICK_LABEL_MAX_LEN 32
|
||||
#define LV_USE_METER 0
|
||||
#define LV_USE_SPINBOX 0
|
||||
#define LV_USE_SPAN 0
|
||||
#define LV_USE_TILEVIEW 1 /* Used for swipeable page navigation */
|
||||
#define LV_USE_TABVIEW 0
|
||||
#define LV_USE_WIN 0
|
||||
|
||||
/* ---- Themes ---- */
|
||||
#define LV_USE_THEME_DEFAULT 1
|
||||
#define LV_THEME_DEFAULT_DARK 1
|
||||
|
||||
/* ---- Logging ---- */
|
||||
#define LV_USE_LOG 0
|
||||
#define LV_USE_ASSERT_NULL 1
|
||||
#define LV_USE_ASSERT_MALLOC 1
|
||||
|
||||
/* ---- GPU / render ---- */
|
||||
#define LV_USE_GPU_ESP32_S3 0 /* No parallel LCD interface — we use QSPI */
|
||||
|
||||
/* ---- Animation ---- */
|
||||
#define LV_USE_ANIM 1
|
||||
#define LV_ANIM_DEF_TIME 200
|
||||
|
||||
/* ---- Misc ---- */
|
||||
#define LV_USE_GROUP 1 /* For touch/input device routing */
|
||||
#define LV_USE_PERF_MONITOR 0
|
||||
#define LV_USE_MEM_MONITOR 0
|
||||
#define LV_SPRINTF_CUSTOM 0
|
||||
|
||||
#endif /* LV_CONF_H */
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "power_mgmt.h"
|
||||
#include "wasm_runtime.h"
|
||||
#include "wasm_upload.h"
|
||||
#include "display_task.h"
|
||||
|
||||
#include "esp_timer.h"
|
||||
|
||||
@@ -203,6 +204,12 @@ void app_main(void)
|
||||
/* Initialize power management. */
|
||||
power_mgmt_init(g_nvs_config.power_duty);
|
||||
|
||||
/* ADR-045: Start AMOLED display task (gracefully skips if no display). */
|
||||
esp_err_t disp_ret = display_task_start();
|
||||
if (disp_ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Display init returned: %s", esp_err_to_name(disp_ret));
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "CSI streaming active → %s:%d (edge_tier=%u, OTA=%s, WASM=%s)",
|
||||
g_nvs_config.target_ip, g_nvs_config.target_port,
|
||||
g_nvs_config.edge_tier,
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#include "esp_ota_ops.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_app_desc.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "nvs.h"
|
||||
|
||||
static const char *TAG = "ota_update";
|
||||
|
||||
@@ -24,6 +26,52 @@ static const char *TAG = "ota_update";
|
||||
/** Maximum firmware size (900 KB — matches CI binary size gate). */
|
||||
#define OTA_MAX_SIZE (900 * 1024)
|
||||
|
||||
/** NVS namespace and key for the OTA pre-shared key. */
|
||||
#define OTA_NVS_NAMESPACE "security"
|
||||
#define OTA_NVS_KEY "ota_psk"
|
||||
|
||||
/** Maximum PSK length (hex-encoded SHA-256). */
|
||||
#define OTA_PSK_MAX_LEN 65
|
||||
|
||||
/** Cached PSK loaded from NVS at init time. Empty = auth disabled. */
|
||||
static char s_ota_psk[OTA_PSK_MAX_LEN] = {0};
|
||||
|
||||
/**
|
||||
* ADR-050: Verify the Authorization header contains the correct PSK.
|
||||
* Returns true if auth is disabled (no PSK provisioned) or if the
|
||||
* Bearer token matches the stored PSK.
|
||||
*/
|
||||
static bool ota_check_auth(httpd_req_t *req)
|
||||
{
|
||||
if (s_ota_psk[0] == '\0') {
|
||||
/* No PSK provisioned — auth disabled (permissive for dev). */
|
||||
return true;
|
||||
}
|
||||
|
||||
char auth_header[128] = {0};
|
||||
if (httpd_req_get_hdr_value_str(req, "Authorization", auth_header,
|
||||
sizeof(auth_header)) != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Expect "Bearer <psk>" */
|
||||
const char *prefix = "Bearer ";
|
||||
if (strncmp(auth_header, prefix, strlen(prefix)) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *token = auth_header + strlen(prefix);
|
||||
/* Constant-time comparison to prevent timing attacks. */
|
||||
size_t psk_len = strlen(s_ota_psk);
|
||||
size_t tok_len = strlen(token);
|
||||
if (psk_len != tok_len) return false;
|
||||
volatile uint8_t result = 0;
|
||||
for (size_t i = 0; i < psk_len; i++) {
|
||||
result |= (uint8_t)(s_ota_psk[i] ^ token[i]);
|
||||
}
|
||||
return result == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /ota/status — return firmware version and partition info.
|
||||
*/
|
||||
@@ -53,6 +101,14 @@ static esp_err_t ota_status_handler(httpd_req_t *req)
|
||||
*/
|
||||
static esp_err_t ota_upload_handler(httpd_req_t *req)
|
||||
{
|
||||
/* ADR-050: Authenticate before accepting firmware upload. */
|
||||
if (!ota_check_auth(req)) {
|
||||
ESP_LOGW(TAG, "OTA upload rejected: authentication failed");
|
||||
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN,
|
||||
"Authentication required. Use: Authorization: Bearer <psk>");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "OTA update started, content_length=%d", req->content_len);
|
||||
|
||||
if (req->content_len <= 0 || req->content_len > OTA_MAX_SIZE) {
|
||||
@@ -187,6 +243,20 @@ static esp_err_t ota_start_server(httpd_handle_t *out_handle)
|
||||
|
||||
esp_err_t ota_update_init(void)
|
||||
{
|
||||
/* ADR-050: Load OTA PSK from NVS if provisioned. */
|
||||
nvs_handle_t nvs;
|
||||
if (nvs_open(OTA_NVS_NAMESPACE, NVS_READONLY, &nvs) == ESP_OK) {
|
||||
size_t len = sizeof(s_ota_psk);
|
||||
if (nvs_get_str(nvs, OTA_NVS_KEY, s_ota_psk, &len) == ESP_OK) {
|
||||
ESP_LOGI(TAG, "OTA PSK loaded from NVS (%d chars) — authentication enabled", (int)len - 1);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "No OTA PSK in NVS — OTA authentication DISABLED (provision with nvs_set)");
|
||||
}
|
||||
nvs_close(nvs);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "NVS namespace '%s' not found — OTA authentication DISABLED", OTA_NVS_NAMESPACE);
|
||||
}
|
||||
|
||||
return ota_start_server(NULL);
|
||||
}
|
||||
|
||||
|
||||
@@ -107,8 +107,9 @@ static esp_err_t wasm_upload_handler(httpd_req_t *req)
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/* Verify signature if wasm_verify is enabled. */
|
||||
#ifdef CONFIG_WASM_VERIFY_SIGNATURE
|
||||
/* ADR-050: Verify signature (default-on; skip only if
|
||||
* CONFIG_WASM_SKIP_SIGNATURE is explicitly set for dev/lab). */
|
||||
#ifndef CONFIG_WASM_SKIP_SIGNATURE
|
||||
{
|
||||
/* Load pubkey from NVS config (set via provision.py --wasm-pubkey). */
|
||||
extern nvs_config_t g_nvs_config;
|
||||
@@ -173,11 +174,11 @@ static esp_err_t wasm_upload_handler(httpd_req_t *req)
|
||||
|
||||
} else if (rvf_is_raw_wasm(buf, (uint32_t)total)) {
|
||||
/* ── Raw WASM path (dev/lab only) ── */
|
||||
#ifdef CONFIG_WASM_VERIFY_SIGNATURE
|
||||
#ifndef CONFIG_WASM_SKIP_SIGNATURE
|
||||
free(buf);
|
||||
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN,
|
||||
"Raw WASM upload rejected (wasm_verify enabled). "
|
||||
"Use RVF container with signature.");
|
||||
"Raw WASM upload rejected (signature verification enabled). "
|
||||
"Use RVF container with signature, or set CONFIG_WASM_SKIP_SIGNATURE for dev.");
|
||||
return ESP_FAIL;
|
||||
#else
|
||||
format = "raw";
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
# ESP32-S3 CSI Node — 8MB flash partition table (ADR-045)
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, 0x9000, 0x6000,
|
||||
otadata, data, ota, 0xf000, 0x2000,
|
||||
phy_init, data, phy, 0x11000, 0x1000,
|
||||
ota_0, app, ota_0, 0x20000, 0x200000,
|
||||
ota_1, app, ota_1, 0x220000, 0x200000,
|
||||
spiffs, data, spiffs, 0x420000, 0x1E0000,
|
||||
|
@@ -76,7 +76,16 @@ def generate_nvs_binary(csv_content, size):
|
||||
bin_path = csv_path.replace(".csv", ".bin")
|
||||
|
||||
try:
|
||||
# Try the pip-installed version first
|
||||
# Try the pip-installed version first (esp_idf_nvs_partition_gen package)
|
||||
try:
|
||||
from esp_idf_nvs_partition_gen import nvs_partition_gen
|
||||
nvs_partition_gen.generate(csv_path, bin_path, size)
|
||||
with open(bin_path, "rb") as f:
|
||||
return f.read()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Try legacy import name (older versions)
|
||||
try:
|
||||
import nvs_partition_gen
|
||||
nvs_partition_gen.generate(csv_path, bin_path, size)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
$p = New-Object System.IO.Ports.SerialPort('COM7', 115200)
|
||||
$p.ReadTimeout = 5000
|
||||
$p.Open()
|
||||
Start-Sleep -Milliseconds 200
|
||||
|
||||
for ($i = 0; $i -lt 60; $i++) {
|
||||
try {
|
||||
$line = $p.ReadLine()
|
||||
Write-Host $line
|
||||
} catch {
|
||||
break
|
||||
}
|
||||
}
|
||||
$p.Close()
|
||||
@@ -0,0 +1,33 @@
|
||||
# ESP32-S3 CSI Node — Default SDK Configuration
|
||||
# This file is applied automatically by idf.py when no sdkconfig exists.
|
||||
|
||||
# Target: ESP32-S3
|
||||
CONFIG_IDF_TARGET="esp32s3"
|
||||
|
||||
# Use custom partition table (8MB flash with OTA — ADR-045)
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_display.csv"
|
||||
|
||||
# Flash configuration: 8MB (Quad SPI)
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE="8MB"
|
||||
|
||||
# Compiler optimization: optimize for size to reduce binary
|
||||
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
|
||||
|
||||
# Enable CSI (Channel State Information) in WiFi driver
|
||||
CONFIG_ESP_WIFI_CSI_ENABLED=y
|
||||
|
||||
# NVS encryption disabled by default (requires eFuse provisioning).
|
||||
# Enable only after burning HMAC key to eFuse block.
|
||||
# CONFIG_NVS_ENCRYPTION is not set
|
||||
|
||||
# Disable unused features to reduce binary size
|
||||
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
|
||||
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
|
||||
|
||||
# LWIP: enable extended socket options for UDP multicast
|
||||
CONFIG_LWIP_SO_RCVBUF=y
|
||||
|
||||
# FreeRTOS: increase task stack for CSI processing
|
||||
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
|
||||
@@ -0,0 +1,33 @@
|
||||
# ESP32-S3 CSI Node — Default SDK Configuration
|
||||
# This file is applied automatically by idf.py when no sdkconfig exists.
|
||||
|
||||
# Target: ESP32-S3
|
||||
CONFIG_IDF_TARGET="esp32s3"
|
||||
|
||||
# Use custom partition table (8MB flash with OTA — ADR-045)
|
||||
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_display.csv"
|
||||
|
||||
# Flash configuration: 8MB (Quad SPI)
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
|
||||
CONFIG_ESPTOOLPY_FLASHSIZE="8MB"
|
||||
|
||||
# Compiler optimization: optimize for size to reduce binary
|
||||
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
|
||||
|
||||
# Enable CSI (Channel State Information) in WiFi driver
|
||||
CONFIG_ESP_WIFI_CSI_ENABLED=y
|
||||
|
||||
# NVS encryption disabled by default (requires eFuse provisioning).
|
||||
# Enable only after burning HMAC key to eFuse block.
|
||||
# CONFIG_NVS_ENCRYPTION is not set
|
||||
|
||||
# Disable unused features to reduce binary size
|
||||
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
|
||||
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
|
||||
|
||||
# LWIP: enable extended socket options for UDP multicast
|
||||
CONFIG_LWIP_SO_RCVBUF=y
|
||||
|
||||
# FreeRTOS: increase task stack for CSI processing
|
||||
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
|
||||
Binary file not shown.
@@ -0,0 +1,10 @@
|
||||
{"type":"edit","file":"unknown","timestamp":1772820418129,"sessionId":null}
|
||||
{"type":"edit","file":"unknown","timestamp":1772820462588,"sessionId":null}
|
||||
{"type":"edit","file":"unknown","timestamp":1772820472219,"sessionId":null}
|
||||
{"type":"edit","file":"unknown","timestamp":1772832571444,"sessionId":null}
|
||||
{"type":"edit","file":"unknown","timestamp":1772832585997,"sessionId":null}
|
||||
{"type":"edit","file":"unknown","timestamp":1773099593107,"sessionId":null}
|
||||
{"type":"edit","file":"unknown","timestamp":1773115162931,"sessionId":null}
|
||||
{"type":"edit","file":"unknown","timestamp":1773115172336,"sessionId":null}
|
||||
{"type":"edit","file":"unknown","timestamp":1773147087836,"sessionId":null}
|
||||
{"type":"edit","file":"unknown","timestamp":1773149448951,"sessionId":null}
|
||||
Generated
+3533
-53
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@ members = [
|
||||
"crates/wifi-densepose-wifiscan",
|
||||
"crates/wifi-densepose-vitals",
|
||||
"crates/wifi-densepose-ruvector",
|
||||
"crates/wifi-densepose-desktop",
|
||||
]
|
||||
# ADR-040: WASM edge crate targets wasm32-unknown-unknown (no_std),
|
||||
# excluded from workspace to avoid breaking `cargo test --workspace`.
|
||||
@@ -101,7 +102,7 @@ csv = "1.3"
|
||||
indicatif = "0.17"
|
||||
|
||||
# CLI
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
clap = { version = "4.4", features = ["derive", "env"] }
|
||||
|
||||
# Testing
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/target/
|
||||
Cargo.lock
|
||||
@@ -0,0 +1,98 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"ruv-neural-core",
|
||||
"ruv-neural-sensor",
|
||||
"ruv-neural-signal",
|
||||
"ruv-neural-graph",
|
||||
"ruv-neural-mincut",
|
||||
"ruv-neural-embed",
|
||||
"ruv-neural-memory",
|
||||
"ruv-neural-decoder",
|
||||
"ruv-neural-esp32",
|
||||
"ruv-neural-wasm",
|
||||
"ruv-neural-viz",
|
||||
"ruv-neural-cli",
|
||||
]
|
||||
# WASM crate excluded from default workspace to avoid breaking `cargo test --workspace`
|
||||
# Build separately: cargo build -p ruv-neural-wasm --target wasm32-unknown-unknown --release
|
||||
exclude = [
|
||||
"ruv-neural-wasm",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["rUv <ruv@ruv.net>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/ruvnet/RuView"
|
||||
documentation = "https://docs.rs/ruv-neural"
|
||||
keywords = ["neural", "brain", "topology", "mincut", "quantum-sensing"]
|
||||
categories = ["science", "algorithms"]
|
||||
|
||||
[workspace.dependencies]
|
||||
# Core utilities
|
||||
thiserror = "1.0"
|
||||
anyhow = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
# Math and signal processing
|
||||
ndarray = { version = "0.15", features = ["serde"] }
|
||||
num-complex = "0.4"
|
||||
num-traits = "0.2"
|
||||
rustfft = "6.1"
|
||||
|
||||
# Graph algorithms
|
||||
petgraph = "0.6"
|
||||
|
||||
# Async runtime
|
||||
tokio = { version = "1.35", features = ["full"] }
|
||||
|
||||
# WASM support
|
||||
wasm-bindgen = "0.2"
|
||||
js-sys = "0.3"
|
||||
web-sys = { version = "0.3", features = ["console"] }
|
||||
|
||||
# ESP32 / embedded
|
||||
embedded-hal = "1.0"
|
||||
|
||||
# CLI
|
||||
clap = { version = "4.4", features = ["derive", "env"] }
|
||||
|
||||
# Serialization
|
||||
bincode = "1.3"
|
||||
|
||||
# Random
|
||||
rand = "0.8"
|
||||
|
||||
# Cryptographic verification
|
||||
ed25519-dalek = { version = "2.1", features = ["rand_core"] }
|
||||
sha2 = "0.10"
|
||||
|
||||
# Testing
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
proptest = "1.4"
|
||||
approx = "0.5"
|
||||
|
||||
# Internal crates
|
||||
ruv-neural-core = { version = "0.1.0", path = "ruv-neural-core" }
|
||||
ruv-neural-sensor = { version = "0.1.0", path = "ruv-neural-sensor" }
|
||||
ruv-neural-signal = { version = "0.1.0", path = "ruv-neural-signal" }
|
||||
ruv-neural-graph = { version = "0.1.0", path = "ruv-neural-graph" }
|
||||
ruv-neural-mincut = { version = "0.1.0", path = "ruv-neural-mincut" }
|
||||
ruv-neural-embed = { version = "0.1.0", path = "ruv-neural-embed" }
|
||||
ruv-neural-memory = { version = "0.1.0", path = "ruv-neural-memory" }
|
||||
ruv-neural-decoder = { version = "0.1.0", path = "ruv-neural-decoder" }
|
||||
ruv-neural-esp32 = { version = "0.1.0", path = "ruv-neural-esp32" }
|
||||
ruv-neural-viz = { version = "0.1.0", path = "ruv-neural-viz" }
|
||||
ruv-neural-cli = { version = "0.1.0", path = "ruv-neural-cli" }
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
opt-level = 3
|
||||
@@ -0,0 +1,421 @@
|
||||
# rUv Neural — Brain Topology Analysis System
|
||||
|
||||
> Quantum sensor integration x RuVector graph memory x Dynamic mincut coherence detection
|
||||
|
||||
[](https://crates.io/crates/ruv-neural-core)
|
||||
[]()
|
||||
[]()
|
||||
[]()
|
||||
|
||||
---
|
||||
|
||||
## Ethics & Responsible Use
|
||||
|
||||
> **This technology interfaces with human neural data. Use it responsibly.**
|
||||
>
|
||||
> - **Informed consent** is required before collecting neural data from any participant
|
||||
> - **Never** deploy brain-computer interfaces without IRB/ethics board approval
|
||||
> - **Data privacy**: Neural signals are among the most sensitive personal data categories. Encrypt at rest, anonymize before sharing, and comply with GDPR/HIPAA as applicable
|
||||
> - **Clinical use** requires FDA/CE clearance and must be supervised by licensed medical professionals
|
||||
> - **Do not** use this software for covert monitoring, interrogation, lie detection, or any application that violates human autonomy
|
||||
> - **Dual-use awareness**: The same technology that helps paralyzed patients communicate can be misused for surveillance. Design with safeguards
|
||||
> - This software is provided for **research and educational purposes**. The authors accept no liability for misuse
|
||||
>
|
||||
> See [IEEE Neuroethics Framework](https://standards.ieee.org/industry-connections/ec/neuroethics/) and the [Morningside Group Neurorights](https://nri.ntc.columbia.edu/content/neurorights) initiative for guidance.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
**rUv Neural** is a modular Rust crate ecosystem for real-time brain network topology
|
||||
analysis. It transforms neural magnetic field measurements from quantum sensors (NV diamond
|
||||
magnetometers, optically pumped magnetometers) into dynamic connectivity graphs, then uses
|
||||
minimum cut algorithms to detect cognitive state transitions.
|
||||
|
||||
This is not mind reading — it measures **how cognition organizes itself** by tracking the
|
||||
topology of brain networks in real time.
|
||||
|
||||
## Hardware Parts List
|
||||
|
||||
Below is a reference bill of materials for building a basic multi-channel neural sensing rig.
|
||||
Prices are approximate (2026). Links are for reference only — equivalent components from any
|
||||
vendor will work.
|
||||
|
||||
### Core: NV Diamond Magnetometer Array
|
||||
|
||||
| Component | Qty | Approx Price | Link | Notes |
|
||||
|-----------|-----|-------------|------|-------|
|
||||
| NV Diamond Sensor Chip (2x2mm, 1ppm N) | 16 | $45 ea | [AliExpress: NV Diamond Chip](https://www.aliexpress.com/w/wholesale-nv-diamond-sensor.html) | Nitrogen-vacancy center, electronic grade |
|
||||
| 532nm Green Laser Diode Module (100mW) | 4 | $12 ea | [AliExpress: 532nm Laser Module](https://www.aliexpress.com/w/wholesale-532nm-laser-module-100mw.html) | Excitation source for ODMR |
|
||||
| Microwave Signal Generator (2.87 GHz) | 1 | $85 | [AliExpress: RF Signal Generator 3GHz](https://www.aliexpress.com/w/wholesale-rf-signal-generator-3ghz.html) | For NV zero-field splitting resonance |
|
||||
| SMA Coaxial Cable (50 Ohm, 30cm) | 4 | $3 ea | [AliExpress: SMA Cable 50 Ohm](https://www.aliexpress.com/w/wholesale-sma-cable-50-ohm.html) | Microwave delivery to diamond chips |
|
||||
| Photodiode Array (Si PIN, 16-ch) | 1 | $25 | [AliExpress: Photodiode Array](https://www.aliexpress.com/w/wholesale-photodiode-array-16-channel.html) | Fluorescence detection |
|
||||
| Transimpedance Amplifier Board | 1 | $18 | [AliExpress: TIA Board](https://www.aliexpress.com/w/wholesale-transimpedance-amplifier-board.html) | Converts photocurrent to voltage |
|
||||
|
||||
### Alternative: OPM (Optically Pumped Magnetometer)
|
||||
|
||||
| Component | Qty | Approx Price | Link | Notes |
|
||||
|-----------|-----|-------------|------|-------|
|
||||
| Rb Vapor Cell (25mm, AR coated) | 8 | $35 ea | [AliExpress: Rubidium Vapor Cell](https://www.aliexpress.com/w/wholesale-rubidium-vapor-cell.html) | SERF-mode magnetometry |
|
||||
| 795nm VCSEL Laser | 8 | $8 ea | [AliExpress: 795nm VCSEL](https://www.aliexpress.com/w/wholesale-795nm-vcsel-laser.html) | D1 line pump for Rb |
|
||||
| Balanced Photodetector | 8 | $15 ea | [AliExpress: Balanced Photodetector](https://www.aliexpress.com/w/wholesale-balanced-photodetector.html) | Differential detection |
|
||||
| Magnetic Shielding Mu-Metal Cylinder | 1 | $120 | [AliExpress: Mu-Metal Shield](https://www.aliexpress.com/w/wholesale-mu-metal-magnetic-shield.html) | 3-layer, >60dB attenuation |
|
||||
|
||||
### Alternative: EEG (Electroencephalography)
|
||||
|
||||
| Component | Qty | Approx Price | Link | Notes |
|
||||
|-----------|-----|-------------|------|-------|
|
||||
| Ag/AgCl EEG Electrodes (10-20 system) | 21 | $2 ea | [AliExpress: EEG Electrode AgCl](https://www.aliexpress.com/w/wholesale-eeg-electrode-ag-agcl.html) | Reusable cup electrodes |
|
||||
| EEG Cap (10-20 placement, size M) | 1 | $45 | [AliExpress: EEG Cap 10-20](https://www.aliexpress.com/w/wholesale-eeg-cap-10-20.html) | Pre-wired 21-channel |
|
||||
| Conductive EEG Gel (250ml) | 1 | $8 | [AliExpress: EEG Gel](https://www.aliexpress.com/w/wholesale-eeg-conductive-gel.html) | Low impedance contact |
|
||||
| ADS1299 EEG AFE Board (8-ch) | 3 | $35 ea | [AliExpress: ADS1299 Board](https://www.aliexpress.com/w/wholesale-ads1299-eeg-board.html) | 24-bit, 250 SPS, TI analog front-end |
|
||||
|
||||
### Data Acquisition & Processing
|
||||
|
||||
| Component | Qty | Approx Price | Link | Notes |
|
||||
|-----------|-----|-------------|------|-------|
|
||||
| ESP32-S3 DevKit (16MB Flash, 8MB PSRAM) | 4 | $8 ea | [AliExpress: ESP32-S3 DevKit](https://www.aliexpress.com/w/wholesale-esp32-s3-devkit.html) | ADC readout + TDM sync |
|
||||
| ADS1256 24-bit ADC Module | 2 | $12 ea | [AliExpress: ADS1256 Module](https://www.aliexpress.com/w/wholesale-ads1256-module.html) | High-resolution for NV/OPM |
|
||||
| USB-C Hub (4 port, USB 3.0) | 1 | $10 | [AliExpress: USB-C Hub](https://www.aliexpress.com/w/wholesale-usb-c-hub-4-port.html) | Connect ESP32 nodes to host |
|
||||
| Shielded USB Cable (30cm, ferrite) | 4 | $3 ea | [AliExpress: Shielded USB Cable](https://www.aliexpress.com/w/wholesale-shielded-usb-cable-ferrite.html) | Reduce EMI |
|
||||
| Host PC or Raspberry Pi 5 (8GB) | 1 | $80 | [AliExpress: Raspberry Pi 5](https://www.aliexpress.com/w/wholesale-raspberry-pi-5-8gb.html) | Runs the rUv Neural pipeline |
|
||||
|
||||
### Assembly Tools
|
||||
|
||||
| Component | Qty | Approx Price | Link | Notes |
|
||||
|-----------|-----|-------------|------|-------|
|
||||
| Soldering Station (adjustable temp) | 1 | $25 | [AliExpress: Soldering Station](https://www.aliexpress.com/w/wholesale-soldering-station-adjustable.html) | For sensor board assembly |
|
||||
| Breadboard + Jumper Wire Kit | 1 | $8 | [AliExpress: Breadboard Kit](https://www.aliexpress.com/w/wholesale-breadboard-jumper-wire-kit.html) | Prototyping |
|
||||
| 3D Printed Sensor Mount (STL provided) | 1 | — | Print locally | Holds diamond chips in array |
|
||||
|
||||
**Estimated total cost:** ~$650–$900 for a 16-channel NV diamond setup, ~$500 for OPM, ~$200 for EEG.
|
||||
|
||||
### Assembly Instructions
|
||||
|
||||
1. **Sensor Array**
|
||||
- Mount NV diamond chips (or OPM vapor cells, or EEG electrodes) in the 3D-printed helmet/mount
|
||||
- For NV: align 532nm laser to each chip, position photodiodes for fluorescence collection
|
||||
- For OPM: install Rb cells inside mu-metal shield, align 795nm VCSELs
|
||||
- For EEG: apply conductive gel, place electrodes per 10-20 system
|
||||
|
||||
2. **Signal Chain**
|
||||
- Connect sensor outputs to ADS1256 (NV/OPM) or ADS1299 (EEG) ADC boards
|
||||
- Wire ADC SPI bus to ESP32-S3 GPIO (MOSI=11, MISO=13, SCK=12, CS=10)
|
||||
- Flash ESP32 with `ruv-neural-esp32` firmware: `cargo flash --chip esp32s3`
|
||||
|
||||
3. **TDM Synchronization**
|
||||
- Connect GPIO 4 across all ESP32 nodes as a shared sync line
|
||||
- The `TdmScheduler` assigns non-overlapping time slots automatically
|
||||
- Set `sync_tolerance_us: 1000` in the aggregator config
|
||||
|
||||
4. **Host Software**
|
||||
- Install Rust 1.75+ and build: `cargo build --workspace --release`
|
||||
- Run the pipeline: `cargo run -p ruv-neural-cli --release -- pipeline --channels 16 --duration 60`
|
||||
- Or use individual crates as a library (see [Use as Library](#use-as-library))
|
||||
|
||||
5. **Verification**
|
||||
- Generate a witness bundle: `cargo run -p ruv-neural-cli -- witness --output witness.json`
|
||||
- Verify Ed25519 signature: `cargo run -p ruv-neural-cli -- witness --verify witness.json`
|
||||
- Expected output: `VERDICT: PASS` (41 capability attestations, 338 tests)
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
rUv Neural Pipeline
|
||||
================================================================
|
||||
|
||||
+------------------+ +-------------------+ +------------------+
|
||||
| | | | | |
|
||||
| SENSOR LAYER |---->| SIGNAL LAYER |---->| GRAPH LAYER |
|
||||
| | | | | |
|
||||
| NV Diamond | | Bandpass Filter | | PLV / Coherence |
|
||||
| OPM | | Artifact Reject | | Brain Regions |
|
||||
| EEG | | Hilbert Phase | | Connectivity |
|
||||
| Simulated | | Spectral (PSD) | | Matrix |
|
||||
| | | | | |
|
||||
+------------------+ +-------------------+ +--------+---------+
|
||||
|
|
||||
v
|
||||
+------------------+ +-------------------+ +------------------+
|
||||
| | | | | |
|
||||
| DECODE LAYER |<----| MEMORY LAYER |<----| MINCUT LAYER |
|
||||
| | | | | |
|
||||
| Cognitive State | | HNSW Index | | Stoer-Wagner |
|
||||
| Classification | | Pattern Store | | Normalized Cut |
|
||||
| BCI Output | | Drift Detection | | Spectral Cut |
|
||||
| Transition Log | | Temporal Window | | Coherence Detect|
|
||||
| | | | | |
|
||||
+------------------+ +-------------------+ +------------------+
|
||||
^
|
||||
|
|
||||
+-------+--------+
|
||||
| |
|
||||
| EMBED LAYER |
|
||||
| |
|
||||
| Spectral Pos. |
|
||||
| Topology Vec |
|
||||
| Node2Vec |
|
||||
| RVF Export |
|
||||
| |
|
||||
+----------------+
|
||||
|
||||
Peripheral Crates:
|
||||
+----------+ +----------+ +----------+
|
||||
| ESP32 | | WASM | | VIZ |
|
||||
| Edge | | Browser | | ASCII |
|
||||
| Preproc | | Bindings | | Render |
|
||||
+----------+ +----------+ +----------+
|
||||
```
|
||||
|
||||
## Crate Map
|
||||
|
||||
All crates are published on [crates.io](https://crates.io/search?q=ruv-neural):
|
||||
|
||||
| Crate | crates.io | Description | Dependencies |
|
||||
|-------|-----------|-------------|--------------|
|
||||
| [`ruv-neural-core`](https://crates.io/crates/ruv-neural-core) | [](https://crates.io/crates/ruv-neural-core) | Core types, traits, errors, RVF format | None |
|
||||
| [`ruv-neural-sensor`](https://crates.io/crates/ruv-neural-sensor) | [](https://crates.io/crates/ruv-neural-sensor) | NV diamond, OPM, EEG sensor interfaces | core |
|
||||
| [`ruv-neural-signal`](https://crates.io/crates/ruv-neural-signal) | [](https://crates.io/crates/ruv-neural-signal) | DSP: filtering, spectral, connectivity | core |
|
||||
| [`ruv-neural-graph`](https://crates.io/crates/ruv-neural-graph) | [](https://crates.io/crates/ruv-neural-graph) | Brain connectivity graph construction | core, signal |
|
||||
| [`ruv-neural-mincut`](https://crates.io/crates/ruv-neural-mincut) | [](https://crates.io/crates/ruv-neural-mincut) | Dynamic minimum cut topology analysis | core |
|
||||
| [`ruv-neural-embed`](https://crates.io/crates/ruv-neural-embed) | [](https://crates.io/crates/ruv-neural-embed) | RuVector graph embeddings | core |
|
||||
| [`ruv-neural-memory`](https://crates.io/crates/ruv-neural-memory) | [](https://crates.io/crates/ruv-neural-memory) | Persistent neural state memory + HNSW | core |
|
||||
| [`ruv-neural-decoder`](https://crates.io/crates/ruv-neural-decoder) | [](https://crates.io/crates/ruv-neural-decoder) | Cognitive state classification + BCI | core |
|
||||
| [`ruv-neural-esp32`](https://crates.io/crates/ruv-neural-esp32) | [](https://crates.io/crates/ruv-neural-esp32) | ESP32 edge sensor integration | core |
|
||||
| `ruv-neural-wasm` | — | WebAssembly browser bindings | core |
|
||||
| [`ruv-neural-viz`](https://crates.io/crates/ruv-neural-viz) | [](https://crates.io/crates/ruv-neural-viz) | Visualization and ASCII rendering | core, graph, mincut |
|
||||
| [`ruv-neural-cli`](https://crates.io/crates/ruv-neural-cli) | [](https://crates.io/crates/ruv-neural-cli) | CLI tool (`ruv-neural` binary) | all |
|
||||
|
||||
## Dependency Graph
|
||||
|
||||
```
|
||||
ruv-neural-core
|
||||
(types, traits, errors)
|
||||
/ | | \ \
|
||||
/ | | \ \
|
||||
v v v v v
|
||||
sensor signal embed esp32 (wasm)
|
||||
|
|
||||
v
|
||||
graph --|------> viz
|
||||
|
|
||||
v
|
||||
mincut
|
||||
|
|
||||
v
|
||||
decoder <--- memory <--- embed
|
||||
|
|
||||
v
|
||||
cli (depends on all)
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
cd rust-port/wifi-densepose-rs/crates/ruv-neural
|
||||
cargo build --workspace
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
### Run CLI
|
||||
|
||||
```bash
|
||||
cargo run -p ruv-neural-cli -- simulate --channels 64 --duration 10
|
||||
cargo run -p ruv-neural-cli -- pipeline --channels 32 --duration 5 --dashboard
|
||||
cargo run -p ruv-neural-cli -- mincut --input brain_graph.json
|
||||
```
|
||||
|
||||
### Install from crates.io
|
||||
|
||||
```bash
|
||||
# Add individual crates as needed
|
||||
cargo add ruv-neural-core
|
||||
cargo add ruv-neural-sensor
|
||||
cargo add ruv-neural-signal
|
||||
cargo add ruv-neural-mincut
|
||||
cargo add ruv-neural-embed
|
||||
cargo add ruv-neural-memory
|
||||
cargo add ruv-neural-decoder
|
||||
cargo add ruv-neural-graph
|
||||
cargo add ruv-neural-viz
|
||||
cargo add ruv-neural-esp32
|
||||
cargo add ruv-neural-cli
|
||||
```
|
||||
|
||||
### Use as Library
|
||||
|
||||
```rust
|
||||
use ruv_neural_core::*;
|
||||
use ruv_neural_sensor::simulator::SimulatedSensorArray;
|
||||
use ruv_neural_signal::PreprocessingPipeline;
|
||||
use ruv_neural_mincut::DynamicMincutTracker;
|
||||
use ruv_neural_embed::NeuralEmbedding;
|
||||
|
||||
// Create simulated sensor array (64 channels, 1000 Hz)
|
||||
let mut sensor = SimulatedSensorArray::new(64, 1000.0);
|
||||
let data = sensor.acquire(1000)?;
|
||||
|
||||
// Preprocess: bandpass filter + artifact rejection
|
||||
let pipeline = PreprocessingPipeline::default();
|
||||
let clean = pipeline.process(&data)?;
|
||||
|
||||
// Compute connectivity and build graph
|
||||
let connectivity = ruv_neural_signal::compute_all_pairs(
|
||||
&clean,
|
||||
ruv_neural_signal::ConnectivityMetric::PhaseLockingValue,
|
||||
);
|
||||
|
||||
// Track topology changes via dynamic mincut
|
||||
let mut tracker = DynamicMincutTracker::new();
|
||||
let result = tracker.update(&graph)?;
|
||||
println!(
|
||||
"Mincut: {:.3}, Partitions: {} | {}",
|
||||
result.cut_value,
|
||||
result.partition_a.len(),
|
||||
result.partition_b.len()
|
||||
);
|
||||
|
||||
// Generate embedding for downstream classification
|
||||
let embedding = NeuralEmbedding::new(
|
||||
result.to_feature_vector(),
|
||||
data.timestamp,
|
||||
"spectral",
|
||||
)?;
|
||||
println!("Embedding dim: {}", embedding.dimension);
|
||||
```
|
||||
|
||||
## Mix and Match
|
||||
|
||||
Each crate is independently usable. Common combinations:
|
||||
|
||||
- **Sensor + Signal** -- Data acquisition and preprocessing only
|
||||
- **Graph + Mincut** -- Graph analysis without sensor dependency
|
||||
- **Embed + Memory** -- Embedding storage without real-time pipeline
|
||||
- **Core + WASM** -- Browser-based graph visualization
|
||||
- **ESP32 alone** -- Edge preprocessing on embedded hardware
|
||||
- **Signal + Embed** -- Feature extraction pipeline without graph construction
|
||||
- **Mincut + Viz** -- Topology analysis with ASCII dashboard output
|
||||
|
||||
## Platform Support
|
||||
|
||||
| Platform | Status | Crates Available |
|
||||
|----------|--------|-----------------|
|
||||
| Linux x86_64 | Full | All 12 |
|
||||
| macOS ARM64 | Full | All 12 |
|
||||
| Windows x86_64 | Full | All 12 |
|
||||
| WASM (browser) | Partial | core, wasm, viz |
|
||||
| ESP32 (no_std) | Partial | core, esp32 |
|
||||
|
||||
**Note:** The `ruv-neural-wasm` crate is excluded from the default workspace members.
|
||||
Build it separately with:
|
||||
|
||||
```bash
|
||||
cargo build -p ruv-neural-wasm --target wasm32-unknown-unknown --release
|
||||
```
|
||||
|
||||
## Key Algorithms
|
||||
|
||||
### Signal Processing (`ruv-neural-signal`)
|
||||
|
||||
- **Butterworth IIR filters** in second-order sections (SOS) form
|
||||
- **Welch PSD** estimation with configurable window and overlap
|
||||
- **Hilbert transform** for instantaneous phase extraction
|
||||
- **Artifact detection** -- eye blink, muscle, cardiac artifact rejection
|
||||
- **Connectivity metrics** -- PLV, coherence, imaginary coherence, AEC
|
||||
|
||||
### Minimum Cut Analysis (`ruv-neural-mincut`)
|
||||
|
||||
- **Stoer-Wagner** -- Global minimum cut in O(V^3)
|
||||
- **Normalized cut** (Shi-Malik) -- Spectral bisection via the Fiedler vector
|
||||
- **Multiway cut** -- Recursive normalized cut for k-module detection
|
||||
- **Spectral cut** -- Cheeger constant and spectral bisection bounds
|
||||
- **Dynamic tracking** -- Temporal topology transition detection
|
||||
- **Coherence events** -- Network formation, dissolution, merger, split
|
||||
|
||||
### Embeddings (`ruv-neural-embed`)
|
||||
|
||||
- **Spectral** -- Laplacian eigenvector positional encoding
|
||||
- **Topology** -- Hand-crafted topological feature vectors
|
||||
- **Node2Vec** -- Random-walk co-occurrence embeddings
|
||||
- **Combined** -- Weighted concatenation of multiple methods
|
||||
- **Temporal** -- Sliding-window context-enriched embeddings
|
||||
- **RVF export** -- Serialization to RuVector `.rvf` format
|
||||
|
||||
## RVF Format
|
||||
|
||||
RuVector File (RVF) is a binary format for neural data interchange:
|
||||
|
||||
```
|
||||
+--------+--------+---------+----------+----------+
|
||||
| Magic | Version| Type | Payload | Checksum |
|
||||
| RVF\x01| u8 | u8 | [u8; N] | u32 |
|
||||
+--------+--------+---------+----------+----------+
|
||||
```
|
||||
|
||||
- **Magic bytes**: `RVF\x01`
|
||||
- **Supported types**: brain graphs, embeddings, topology metrics, time series
|
||||
- **Binary format** for efficient storage and streaming
|
||||
- **Compatible** with the broader RuVector ecosystem
|
||||
|
||||
## Cryptographic Witness Verification
|
||||
|
||||
rUv Neural includes an Ed25519-signed capability attestation system. Every build can
|
||||
generate a witness bundle that cryptographically proves which capabilities are present
|
||||
and that all tests passed.
|
||||
|
||||
```bash
|
||||
# Generate a signed witness bundle
|
||||
cargo run -p ruv-neural-cli -- witness --output witness-bundle.json
|
||||
|
||||
# Verify (any third party can do this)
|
||||
cargo run -p ruv-neural-cli -- witness --verify witness-bundle.json
|
||||
```
|
||||
|
||||
The bundle contains:
|
||||
- **41 capability attestations** covering all 12 crates
|
||||
- **SHA-256 digest** of the capability matrix
|
||||
- **Ed25519 signature** (unique per generation)
|
||||
- **Public key** for independent verification
|
||||
- Test count and pass/fail status
|
||||
|
||||
Tampered bundles are detected — modifying any attestation invalidates the digest and
|
||||
signature verification returns `FAIL`.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all workspace tests
|
||||
cargo test --workspace
|
||||
|
||||
# Run a specific crate's tests
|
||||
cargo test -p ruv-neural-mincut
|
||||
|
||||
# Run with logging enabled
|
||||
RUST_LOG=debug cargo test --workspace -- --nocapture
|
||||
|
||||
# Run benchmarks (requires nightly or criterion)
|
||||
cargo bench -p ruv-neural-mincut
|
||||
```
|
||||
|
||||
## Crate Publishing Order
|
||||
|
||||
Crates must be published in dependency order:
|
||||
|
||||
1. `ruv-neural-core` (no internal deps)
|
||||
2. `ruv-neural-sensor` (depends on core)
|
||||
3. `ruv-neural-signal` (depends on core)
|
||||
4. `ruv-neural-esp32` (depends on core)
|
||||
5. `ruv-neural-graph` (depends on core, signal)
|
||||
6. `ruv-neural-embed` (depends on core)
|
||||
7. `ruv-neural-mincut` (depends on core)
|
||||
8. `ruv-neural-viz` (depends on core, graph)
|
||||
9. `ruv-neural-memory` (depends on core, embed)
|
||||
10. `ruv-neural-decoder` (depends on core, embed)
|
||||
11. `ruv-neural-wasm` (depends on core)
|
||||
12. `ruv-neural-cli` (depends on all)
|
||||
|
||||
## License
|
||||
|
||||
MIT OR Apache-2.0
|
||||
@@ -0,0 +1,570 @@
|
||||
# ruv-neural Crate System: Security and Performance Review
|
||||
|
||||
**Date**: 2026-03-09
|
||||
**Version**: 0.1.0
|
||||
**Scope**: All 12 workspace crates in the ruv-neural system
|
||||
**Status**: Implementation checklist for v0.1 and v0.2 milestones
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Crate Inventory](#crate-inventory)
|
||||
2. [Security Review](#security-review)
|
||||
- [Input Validation](#input-validation)
|
||||
- [Memory Safety](#memory-safety)
|
||||
- [Data Privacy](#data-privacy)
|
||||
- [Network Security (ESP32)](#network-security-esp32)
|
||||
- [Supply Chain](#supply-chain)
|
||||
- [Findings from Code Audit](#findings-from-code-audit)
|
||||
3. [Performance Review](#performance-review)
|
||||
- [Computational Complexity](#computational-complexity)
|
||||
- [Memory Usage](#memory-usage)
|
||||
- [Optimization Opportunities](#optimization-opportunities)
|
||||
- [ESP32 Constraints](#esp32-constraints)
|
||||
- [Benchmarking Recommendations](#benchmarking-recommendations)
|
||||
- [Performance Findings from Code Audit](#performance-findings-from-code-audit)
|
||||
4. [Action Items](#action-items)
|
||||
|
||||
---
|
||||
|
||||
## Crate Inventory
|
||||
|
||||
| Crate | Status | Lines (approx) | Role |
|
||||
|-------|--------|-----------------|------|
|
||||
| `ruv-neural-core` | Implemented | ~500 | Types, traits, error types, RVF format |
|
||||
| `ruv-neural-sensor` | Implemented | ~170 | Sensor data acquisition, calibration, quality |
|
||||
| `ruv-neural-signal` | Implemented | ~450 | Filtering, spectral analysis, Hilbert, connectivity |
|
||||
| `ruv-neural-graph` | Stub | ~2 | Graph construction from signals |
|
||||
| `ruv-neural-mincut` | Implemented | ~700 | Stoer-Wagner, spectral cut, Cheeger, dynamic tracking |
|
||||
| `ruv-neural-embed` | Implemented | ~350 | Spectral, topology, node2vec embeddings |
|
||||
| `ruv-neural-memory` | Implemented | ~425 | Embedding store, HNSW index |
|
||||
| `ruv-neural-decoder` | Implemented (lib) | ~25 | KNN, threshold, transition decoders |
|
||||
| `ruv-neural-esp32` | Implemented | ~265 | ADC interface, sensor readout |
|
||||
| `ruv-neural-wasm` | Stub | ~2 | WebAssembly bindings |
|
||||
| `ruv-neural-viz` | Implemented (lib) | ~20 | Visualization, ASCII rendering, export |
|
||||
| `ruv-neural-cli` | Stub | ~2 | CLI binary |
|
||||
|
||||
---
|
||||
|
||||
## Security Review
|
||||
|
||||
### Input Validation
|
||||
|
||||
All public APIs must validate their inputs at system boundaries. This section catalogs each validation requirement and its current status.
|
||||
|
||||
#### Sensor Data Validation
|
||||
|
||||
| Check | Required In | Status | Notes |
|
||||
|-------|------------|--------|-------|
|
||||
| `sample_rate_hz > 0` | `MultiChannelTimeSeries::new` | **MISSING** | Constructor accepts `sample_rate_hz` without validating it is positive and finite. Division by zero in `duration_s()` if zero. |
|
||||
| `num_channels > 0` | `MultiChannelTimeSeries::new` | PASS | Returns error if `data.len() == 0`. |
|
||||
| Channel lengths equal | `MultiChannelTimeSeries::new` | PASS | Validates all channels have the same length. |
|
||||
| Non-NaN/Inf values | All signal processing | **MISSING** | No validation that input signals contain only finite f64 values. NaN propagation through FFT, PLV, and connectivity metrics produces silent garbage. |
|
||||
| `num_samples > 0` | `AdcReader::read_samples` | PASS | Returns error if `num_samples == 0`. |
|
||||
| Channel count > 0 | `AdcReader::read_samples` | PASS | Returns error if no channels configured. |
|
||||
| Channel index bounds | `AdcReader::load_buffer` | PASS | Returns `ChannelOutOfRange` error. |
|
||||
| `sensitivity > 0` | `SensorChannel` | **MISSING** | `sensitivity_ft_sqrt_hz` is a public field with no validation on construction. |
|
||||
| `sample_rate > 0` | `SensorChannel` | **MISSING** | `sample_rate_hz` is a public field with no validation. |
|
||||
|
||||
**Recommendation**: Add a `SensorChannel::new()` constructor that validates `sensitivity_ft_sqrt_hz > 0`, `sample_rate_hz > 0`, and that the orientation vector is a unit normal. Add `sample_rate_hz > 0` and `sample_rate_hz.is_finite()` checks to `MultiChannelTimeSeries::new`. Add a `validate_finite()` utility for signal data.
|
||||
|
||||
#### Graph Construction Validation
|
||||
|
||||
| Check | Required In | Status | Notes |
|
||||
|-------|------------|--------|-------|
|
||||
| Edge indices < `num_nodes` | `BrainGraph::adjacency_matrix` | PARTIAL | Silently skips out-of-bounds edges rather than reporting an error. This masks data corruption. |
|
||||
| Edge weight is finite | `BrainGraph` | **MISSING** | `BrainEdge.weight` is not validated. NaN/Inf weights propagate silently through Stoer-Wagner and spectral analysis. |
|
||||
| `num_nodes >= 2` | `stoer_wagner_mincut` | PASS | Returns proper error. |
|
||||
| `num_nodes >= 2` | `fiedler_decomposition` | PASS | Returns proper error. |
|
||||
| `num_nodes >= 2` | `SpectralEmbedder::embed` | PASS | Returns proper error. |
|
||||
| `num_nodes >= 2` | `cheeger_constant` | PASS | Returns proper error. |
|
||||
| Self-loops | `BrainGraph` | **MISSING** | No validation that `source != target` on edges. Self-loops could inflate degree calculations. |
|
||||
|
||||
**Recommendation**: Add a `BrainGraph::validate()` method that checks all edge indices are within bounds, weights are finite, and no self-loops exist. Call it from `stoer_wagner_mincut`, `spectral_bisection`, and `SpectralEmbedder::embed`. Consider making `adjacency_matrix()` return `Result` with an error for out-of-bounds edges instead of silently ignoring them.
|
||||
|
||||
#### RVF Format Validation
|
||||
|
||||
| Check | Required In | Status | Notes |
|
||||
|-------|------------|--------|-------|
|
||||
| Magic bytes | `RvfHeader::validate` | PASS | Validates against `RVF_MAGIC`. |
|
||||
| Version | `RvfHeader::validate` | PASS | Rejects unknown versions. |
|
||||
| Header length | `RvfHeader::from_bytes` | PASS | Checks `bytes.len() < 22`. |
|
||||
| Data type tag | `RvfDataType::from_tag` | PASS | Returns error for unknown tags. |
|
||||
| `metadata_json_len` overflow | `RvfFile::read_from` | **CONCERN** | `metadata_json_len` is cast from `u32` to `usize` and used to allocate a `Vec`. A malicious file with `metadata_json_len = u32::MAX` (~4 GB) would cause an OOM allocation. |
|
||||
| Payload length | `RvfFile::read_from` | **CONCERN** | `read_to_end` reads unbounded data into memory. A malicious file could exhaust memory. |
|
||||
| JSON validity | `RvfFile::read_from` | PASS | Uses `serde_json::from_slice` which returns an error on invalid JSON. |
|
||||
| `num_entries` vs actual data | `RvfFile::read_from` | **MISSING** | The header declares `num_entries` and `embedding_dim`, but these are never cross-checked against the actual payload size. |
|
||||
|
||||
**Recommendation**: Add maximum size limits for `metadata_json_len` (e.g., 16 MB) and total payload size. Validate that `num_entries * entry_size_for_type <= data.len()` after reading. Use `Read::take()` to cap reads.
|
||||
|
||||
#### Embedding Validation
|
||||
|
||||
| Check | Required In | Status | Notes |
|
||||
|-------|------------|--------|-------|
|
||||
| Non-empty vector | `NeuralEmbedding::new` (core) | PASS | Returns error for empty vectors. |
|
||||
| Non-empty vector | `NeuralEmbedding::new` (embed) | PASS | Returns error for empty vectors. |
|
||||
| Dimension match | `cosine_similarity`, `euclidean_distance` | PASS | Returns `DimensionMismatch` error. |
|
||||
| Zero-norm handling | `cosine_similarity` | PASS | Returns 0.0 for zero-norm vectors. |
|
||||
| NaN/Inf in vector | `NeuralEmbedding::new` | **MISSING** | No check for non-finite values in the embedding vector. |
|
||||
|
||||
#### Memory Store Validation
|
||||
|
||||
| Check | Required In | Status | Notes |
|
||||
|-------|------------|--------|-------|
|
||||
| Capacity > 0 | `NeuralMemoryStore::new` | **MISSING** | Capacity 0 is accepted, producing a store that evicts on every insertion. |
|
||||
| k > 0 | `query_nearest` | **MISSING** | k=0 produces an empty result silently (acceptable but undocumented). |
|
||||
| Dimension consistency | `NeuralMemoryStore::store` | **MISSING** | No check that all stored embeddings have the same dimensionality. Mixed dimensions cause silent errors in `query_nearest`. |
|
||||
|
||||
#### JSON Parsing
|
||||
|
||||
| Check | Status | Notes |
|
||||
|-------|--------|-------|
|
||||
| Uses serde derive | PASS | All types use `#[derive(Serialize, Deserialize)]`. No manual parsing anywhere. |
|
||||
| No `unsafe` JSON parsing | PASS | Standard `serde_json` throughout. |
|
||||
|
||||
---
|
||||
|
||||
### Memory Safety
|
||||
|
||||
| Check | Status | Notes |
|
||||
|-------|--------|-------|
|
||||
| No `unsafe` code | PASS | Zero `unsafe` blocks across all crates. |
|
||||
| Vec instead of raw pointers | PASS | All data structures use `Vec`, `HashMap`, `BinaryHeap`. |
|
||||
| ndarray for matrix ops | **NOT USED** | Despite being listed in `workspace.dependencies`, matrix operations use `Vec<Vec<f64>>` throughout. This is bounds-checked but less efficient. |
|
||||
| No C FFI | PASS | No FFI calls. ESP32 code uses pure Rust types. |
|
||||
| No `std::mem::transmute` | PASS | None found. |
|
||||
| No `std::ptr` usage | PASS | None found. |
|
||||
| Bounds checking on slices | PASS | Uses `.get()`, iterator methods, and Rust's built-in bounds checks. |
|
||||
| Integer overflow | **CONCERN** | `max_raw_value()` in `adc.rs` casts `(1u32 << resolution_bits) - 1` to `i16`. If `resolution_bits > 15`, this overflows silently. Currently only 12 or 16 are intended, but 16 produces `i16::MAX` wrapping. |
|
||||
|
||||
**Recommendation**: Add a validation check on `resolution_bits` in `AdcConfig` (must be <= 15 for i16 representation, or switch to u16/i32). Consider migrating `Vec<Vec<f64>>` matrix representations to `ndarray::Array2<f64>` for better cache performance and built-in bounds checking.
|
||||
|
||||
---
|
||||
|
||||
### Data Privacy
|
||||
|
||||
Neural data is among the most sensitive personal data categories. This section covers data handling practices.
|
||||
|
||||
| Check | Status | Notes |
|
||||
|-------|--------|-------|
|
||||
| No PII in log messages | **NEEDS AUDIT** | The crate uses `tracing` in workspace dependencies but currently has no `tracing::info!` or `tracing::debug!` calls with data fields. As logging is added, ensure neural data values, subject IDs, and session IDs are never logged at INFO level or below. |
|
||||
| No neural data in error messages | PASS | Error messages contain structural information (dimensions, indices, version numbers) but not raw signal values or embeddings. |
|
||||
| `subject_id` handling | **CONCERN** | `EmbeddingMetadata.subject_id` is stored as plaintext `Option<String>`. This is PII that is included in serialized embeddings (serde), HNSW indices, and RVF files. |
|
||||
| `session_id` handling | **CONCERN** | Same concern as `subject_id`. |
|
||||
| Memory store encryption | **NOT IMPLEMENTED** | `NeuralMemoryStore` holds embeddings in plaintext `Vec<f64>`. No encryption-at-rest. |
|
||||
| Memory zeroization on drop | **NOT IMPLEMENTED** | Embedding data is not zeroed when dropped. Sensitive neural data persists in deallocated memory. |
|
||||
| WASM data boundary | STUB | WASM crate is not yet implemented. When implemented, must ensure no neural data is sent to external services without explicit user consent. |
|
||||
| RVF file privacy | **CONCERN** | `RvfFile` serializes `metadata` as JSON, which may contain `subject_id`. No option to strip or anonymize metadata before export. |
|
||||
|
||||
**Recommendations**:
|
||||
- Implement a `Redactable` trait for types that may contain PII, providing `redact()` and `anonymize()` methods.
|
||||
- Use the `zeroize` crate to zero sensitive data on drop for `NeuralEmbedding`, `NeuralMemoryStore`, and `MultiChannelTimeSeries`.
|
||||
- Add a `strip_pii()` method to `RvfFile` that removes or hashes identifiers before export.
|
||||
- Document privacy responsibilities in each crate's module documentation.
|
||||
- For v0.2: Add optional encryption-at-rest for `NeuralMemoryStore` using `ring` or `aes-gcm`.
|
||||
|
||||
---
|
||||
|
||||
### Network Security (ESP32)
|
||||
|
||||
| Check | Status | Notes |
|
||||
|-------|--------|-------|
|
||||
| Node ID authentication | **NOT IMPLEMENTED** | ESP32 crate (`ruv-neural-esp32`) is currently a local ADC reader with no network protocol. When TDM protocol is added, node IDs must be authenticated. |
|
||||
| CRC32 integrity | **NOT IMPLEMENTED** | No data packet framing or integrity checks exist yet. |
|
||||
| TLS encryption | **NOT IMPLEMENTED** | v0.1 has no network layer. Planned for v0.2. |
|
||||
| Packet size limits | **NOT IMPLEMENTED** | No packet protocol exists yet. |
|
||||
| Buffer overflow prevention | PARTIAL | `AdcReader` uses a fixed-size ring buffer (4096 samples), which prevents unbounded growth. However, `load_buffer` silently truncates data that exceeds buffer size rather than reporting it. |
|
||||
| DMA configuration | N/A | `dma_enabled` is a configuration flag only; actual DMA is not implemented in std mode. |
|
||||
|
||||
**Recommendations for v0.2 TDM Protocol**:
|
||||
- Authenticate node IDs using a pre-shared key or challenge-response.
|
||||
- Add CRC32 or CRC32-C to every data packet.
|
||||
- Set maximum packet size to 1460 bytes (single WiFi frame MTU).
|
||||
- Use DTLS or TLS 1.3 for encryption when available.
|
||||
- Rate-limit incoming packets per node to prevent flooding.
|
||||
- Validate all fields in received packets before processing.
|
||||
|
||||
---
|
||||
|
||||
### Supply Chain
|
||||
|
||||
| Check | Status | Notes |
|
||||
|-------|--------|-------|
|
||||
| Minimal dependencies | PASS | Core dependencies: `thiserror`, `serde`, `serde_json`, `num-complex`, `rustfft`, `rand`. All are well-maintained, widely-used crates. |
|
||||
| No proc macros except serde | PASS | Only `serde`'s derive macros and `thiserror`'s derive macro are used. `clap`'s derive is CLI-only. |
|
||||
| All deps from crates.io | PASS | No git dependencies or path dependencies outside the workspace. |
|
||||
| Workspace-managed versions | PASS | All dependency versions are declared in `[workspace.dependencies]`. |
|
||||
| `petgraph` usage | **UNUSED** | Listed in workspace dependencies but not imported by any crate. Remove to reduce supply chain surface. |
|
||||
| `tokio` usage | **UNUSED** | Listed in workspace dependencies but not imported by any crate. Remove unless async is planned. |
|
||||
| `ruvector-*` crates | **UNUSED** | Five RuVector crates listed but not imported by any workspace member. Remove unused dependencies. |
|
||||
| `Cargo.lock` | PRESENT | `Cargo.lock` is committed, ensuring reproducible builds. |
|
||||
|
||||
**Recommendation**: Run `cargo deny check` to audit for known vulnerabilities. Remove unused workspace dependencies (`petgraph`, `tokio`, `ruvector-*` crates) to minimize attack surface. Add `cargo audit` to CI.
|
||||
|
||||
---
|
||||
|
||||
### Findings from Code Audit
|
||||
|
||||
#### SEC-001: RVF Unbounded Allocation (Severity: Medium)
|
||||
|
||||
**Location**: `ruv-neural-core/src/rvf.rs`, line 193
|
||||
|
||||
```rust
|
||||
let mut meta_bytes = vec![0u8; header.metadata_json_len as usize];
|
||||
```
|
||||
|
||||
A crafted RVF file with `metadata_json_len = 0xFFFFFFFF` allocates 4 GB. Similarly, `read_to_end` on line 201 reads unbounded data.
|
||||
|
||||
**Fix**: Add maximum size constants and validate before allocating:
|
||||
```rust
|
||||
const MAX_METADATA_LEN: u32 = 16 * 1024 * 1024; // 16 MB
|
||||
const MAX_PAYLOAD_LEN: usize = 256 * 1024 * 1024; // 256 MB
|
||||
|
||||
if header.metadata_json_len > MAX_METADATA_LEN {
|
||||
return Err(RuvNeuralError::Serialization(
|
||||
format!("metadata_json_len {} exceeds maximum {}", header.metadata_json_len, MAX_METADATA_LEN)
|
||||
));
|
||||
}
|
||||
```
|
||||
|
||||
#### SEC-002: Missing Sample Rate Validation (Severity: Medium)
|
||||
|
||||
**Location**: `ruv-neural-core/src/signal.rs`, `MultiChannelTimeSeries::new`
|
||||
|
||||
The `sample_rate_hz` parameter is not validated. A value of 0.0 causes division by zero in `duration_s()`. A negative or NaN value causes incorrect spectral analysis throughout the pipeline.
|
||||
|
||||
**Fix**: Add validation in the constructor:
|
||||
```rust
|
||||
if sample_rate_hz <= 0.0 || !sample_rate_hz.is_finite() {
|
||||
return Err(RuvNeuralError::Signal(
|
||||
format!("sample_rate_hz must be positive and finite, got {}", sample_rate_hz)
|
||||
));
|
||||
}
|
||||
```
|
||||
|
||||
#### SEC-003: NaN Propagation in Signal Processing (Severity: Low)
|
||||
|
||||
**Location**: `ruv-neural-signal/src/connectivity.rs`, all functions
|
||||
|
||||
If either input signal contains NaN, the Hilbert transform produces NaN outputs, which propagate silently through PLV, coherence, and all connectivity metrics. The result is a brain graph with NaN edge weights, which causes undefined behavior in Stoer-Wagner (infinite loops or wrong results).
|
||||
|
||||
**Fix**: Add a `validate_signal` helper and call it at entry points:
|
||||
```rust
|
||||
fn validate_signal(signal: &[f64]) -> Result<()> {
|
||||
if signal.iter().any(|x| !x.is_finite()) {
|
||||
return Err(RuvNeuralError::Signal("Signal contains NaN or Inf values".into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
#### SEC-004: Integer Overflow in ADC (Severity: Low)
|
||||
|
||||
**Location**: `ruv-neural-esp32/src/adc.rs`, `AdcConfig::max_raw_value`
|
||||
|
||||
```rust
|
||||
pub fn max_raw_value(&self) -> i16 {
|
||||
((1u32 << self.resolution_bits) - 1) as i16
|
||||
}
|
||||
```
|
||||
|
||||
For `resolution_bits = 16`, this computes `65535 as i16 = -1`, which causes incorrect voltage conversion (division by -1 flips sign).
|
||||
|
||||
**Fix**: Change return type to `u16` or `i32`, or validate `resolution_bits <= 15`.
|
||||
|
||||
#### SEC-005: HNSW Visited Array Allocation (Severity: Low)
|
||||
|
||||
**Location**: `ruv-neural-memory/src/hnsw.rs`, `search_layer`, line 261
|
||||
|
||||
```rust
|
||||
let mut visited = vec![false; self.embeddings.len()];
|
||||
```
|
||||
|
||||
This allocates a visited array proportional to the total number of embeddings on every search call. For large indices (100K+ embeddings), this causes unnecessary allocation pressure. More critically, if `entry` is >= `self.embeddings.len()`, the indexing on line 262 panics.
|
||||
|
||||
**Fix**: Use a `HashSet<usize>` instead of a boolean array for sparse visitation. Add bounds check on `entry`.
|
||||
|
||||
---
|
||||
|
||||
## Performance Review
|
||||
|
||||
### Computational Complexity
|
||||
|
||||
| Operation | Complexity | Target Latency | Current Status |
|
||||
|-----------|-----------|----------------|----------------|
|
||||
| FFT (1024 points) | O(N log N) | <1 ms | Implemented via `rustfft` (SIMD-optimized). Meets target. |
|
||||
| Hilbert transform | O(N log N) | <1 ms | Two FFTs (forward + inverse). Meets target for N <= 4096. |
|
||||
| PLV (channel pair) | O(N) + 2x FFT | <0.5 ms | Calls `hilbert_transform` twice. Meets target for N <= 2048. |
|
||||
| Coherence (channel pair) | O(N) + 2x FFT | <0.5 ms | Same as PLV. |
|
||||
| Connectivity matrix (68 regions) | O(N^2 x M) | <10 ms | M = samples per channel, N = 68: 2,278 Hilbert pairs. May exceed target for long windows. |
|
||||
| Stoer-Wagner mincut (68 nodes) | O(V^3) | <5 ms | 68^3 = ~314K operations. Meets target. |
|
||||
| Spectral embedding (68 nodes) | O(V^2 x k x iterations) | <3 ms | With k=8, iterations=100: 68^2 x 8 x 100 = ~37M ops. May be tight. |
|
||||
| Fiedler decomposition | O(V^2 x iterations) | <2 ms | 1000 iterations x 68^2 = ~4.6M ops. Meets target. |
|
||||
| Cheeger constant (exact, n<=16) | O(2^n x n^2) | <5 ms | Exponential but capped at n=16: 65K x 256 = ~16M ops. Meets target. |
|
||||
| HNSW insert | O(log N x ef x M) | <1 ms | ef=200, M=16: ~3200 distance computations per insert. Meets target. |
|
||||
| HNSW search (10K embeddings) | O(log N x ef) | <1 ms | ef=50: ~50-200 distance computations. Meets target. |
|
||||
| Brute-force NN (10K embeddings) | O(N x d) | <5 ms | d=256, N=10K: 2.56M f64 ops. Acceptable but HNSW preferred. |
|
||||
| Full pipeline (68 regions) | - | <50 ms | Sum of above stages. Should meet target. |
|
||||
|
||||
### Memory Usage
|
||||
|
||||
| Component | Calculation | Size |
|
||||
|-----------|------------|------|
|
||||
| 64-channel x 1000 Hz x 8 bytes x 1s | 64 x 1000 x 8 | 512 KB per second |
|
||||
| Brain graph adjacency (68 nodes) | 68^2 x 8 bytes | ~37 KB |
|
||||
| Brain graph adjacency (400 nodes) | 400^2 x 8 bytes | ~1.25 MB |
|
||||
| Single embedding (256-d) | 256 x 8 bytes | 2 KB |
|
||||
| Memory store (10K embeddings, 256-d) | 10K x 2 KB | ~20 MB |
|
||||
| HNSW index (10K, M=16, 256-d) | 10K x (2KB + 16 x 16 bytes) | ~22.5 MB |
|
||||
| Stoer-Wagner working memory (68 nodes) | 2 x 68^2 x 8 + 68 x vec overhead | ~75 KB |
|
||||
| Spectral embedder (68 nodes, k=8) | k x 68 x 8 + Laplacian 68^2 x 8 | ~41 KB |
|
||||
| RVF file in memory | header + metadata + payload | Variable, unbounded (see SEC-001) |
|
||||
|
||||
### Optimization Opportunities
|
||||
|
||||
#### Immediate (v0.1)
|
||||
|
||||
1. **Eliminate redundant Hilbert transforms in connectivity matrix**
|
||||
- `compute_all_pairs` calls `hilbert_transform` twice per channel pair.
|
||||
- For 68 channels, this means 68 x 67 = 4,556 Hilbert transforms instead of 68.
|
||||
- **Fix**: Pre-compute analytic signals for all channels, then compute metrics pairwise.
|
||||
- **Expected speedup**: ~67x for connectivity matrix computation.
|
||||
|
||||
2. **Replace Vec<Vec<f64>> with flat Vec<f64> for adjacency matrices**
|
||||
- Current `Vec<Vec<f64>>` has poor cache locality due to heap-allocated inner Vecs.
|
||||
- **Fix**: Use `Vec<f64>` with manual row-major indexing, or migrate to `ndarray::Array2<f64>`.
|
||||
- **Expected speedup**: 2-4x for matrix-heavy operations (Stoer-Wagner, Laplacian).
|
||||
|
||||
3. **Avoid Vec::remove(0) in eviction**
|
||||
- `NeuralMemoryStore::evict_oldest` calls `self.embeddings.remove(0)`, which is O(n).
|
||||
- **Fix**: Use a `VecDeque` or circular buffer.
|
||||
- **Expected speedup**: O(1) eviction instead of O(n).
|
||||
|
||||
4. **Pre-allocate FFT planner**
|
||||
- `compute_psd`, `compute_stft`, and `hilbert_transform` each create a new `FftPlanner` per call.
|
||||
- **Fix**: Cache the planner or use a thread-local planner.
|
||||
- **Expected speedup**: Eliminates repeated plan computation.
|
||||
|
||||
#### Medium-term (v0.2)
|
||||
|
||||
5. **Rayon for parallel channel processing**
|
||||
- `compute_all_pairs` iterates channel pairs sequentially.
|
||||
- **Fix**: Use `rayon::par_iter` for the outer loop.
|
||||
- **Expected speedup**: Linear with core count for connectivity computation.
|
||||
|
||||
6. **SIMD for distance computations in HNSW**
|
||||
- Euclidean distance in `HnswIndex::distance` uses scalar iteration.
|
||||
- **Fix**: Use `packed_simd2` or auto-vectorization hints.
|
||||
- **Expected speedup**: 4-8x for 256-d vectors on AVX2.
|
||||
|
||||
7. **Sparse graph representation**
|
||||
- Dense adjacency matrix wastes memory for sparse brain graphs.
|
||||
- For Schaefer400, storing all 160K entries when only ~10K edges exist is wasteful.
|
||||
- **Fix**: Use compressed sparse row (CSR) format or `petgraph`'s sparse graph.
|
||||
|
||||
8. **Quantized embeddings for WASM**
|
||||
- f64 embeddings are unnecessarily precise for browser-based applications.
|
||||
- **Fix**: Support f32 embeddings in WASM builds, halving memory and transfer size.
|
||||
|
||||
#### Long-term (v0.3+)
|
||||
|
||||
9. **Streaming signal processing**
|
||||
- Current design loads entire time windows into memory.
|
||||
- **Fix**: Implement ring-buffer based streaming for real-time operation.
|
||||
|
||||
10. **GPU acceleration for large-scale spectral analysis**
|
||||
- For Schaefer400 atlas, eigendecomposition of 400x400 matrices benefits from GPU.
|
||||
- **Fix**: Optional `wgpu` or `vulkano` backend for matrix operations.
|
||||
|
||||
### ESP32 Constraints
|
||||
|
||||
| Resource | Limit | Current Usage | Status |
|
||||
|----------|-------|---------------|--------|
|
||||
| SRAM | 520 KB | Ring buffer: 4096 x channels x 2 bytes = 8 KB (1 channel) | OK |
|
||||
| SRAM (multi-channel) | 520 KB | 4096 x 16 x 2 = 128 KB (16 channels) | **TIGHT** |
|
||||
| CPU | 240 MHz dual-core | ADC sampling + data transmission | OK for 1 kHz |
|
||||
| Flash | 4 MB | Binary size with release profile | Needs measurement |
|
||||
| WiFi throughput | ~1 Mbps sustained | 64 ch x 1000 Hz x 2 bytes = 128 KB/s = 1 Mbps | **AT LIMIT** |
|
||||
|
||||
**Recommendations**:
|
||||
- Use fixed-point arithmetic (i16 or Q15) instead of f64 on ESP32.
|
||||
- Implement delta encoding or simple compression for data packets.
|
||||
- Limit on-device processing to ADC readout and basic quality checks.
|
||||
- Move all signal processing (FFT, connectivity, graph construction) to the host.
|
||||
- Profile binary size with `cargo bloat` to ensure it fits in 4 MB flash.
|
||||
- Consider reducing ring buffer size for multi-channel configurations.
|
||||
|
||||
### Benchmarking Recommendations
|
||||
|
||||
#### Per-Crate Microbenchmarks (criterion)
|
||||
|
||||
```toml
|
||||
# Add to each crate's Cargo.toml
|
||||
[[bench]]
|
||||
name = "benchmarks"
|
||||
harness = false
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { workspace = true }
|
||||
```
|
||||
|
||||
| Crate | Benchmark | Input Size | Metric |
|
||||
|-------|-----------|------------|--------|
|
||||
| `ruv-neural-signal` | `bench_hilbert_transform` | 256, 512, 1024, 2048, 4096 samples | ns/op |
|
||||
| `ruv-neural-signal` | `bench_compute_psd` | 1024, 4096 samples | ns/op |
|
||||
| `ruv-neural-signal` | `bench_plv_pair` | 1024 samples | ns/op |
|
||||
| `ruv-neural-signal` | `bench_connectivity_matrix` | 16, 32, 68 channels x 1024 samples | ms/op |
|
||||
| `ruv-neural-mincut` | `bench_stoer_wagner` | 10, 20, 50, 68, 100 nodes | us/op |
|
||||
| `ruv-neural-mincut` | `bench_spectral_bisection` | 10, 20, 50, 68, 100 nodes | us/op |
|
||||
| `ruv-neural-mincut` | `bench_cheeger_constant` | 8, 12, 16 nodes (exact), 32, 68 (approx) | us/op |
|
||||
| `ruv-neural-embed` | `bench_spectral_embed` | 20, 50, 68, 100 nodes | us/op |
|
||||
| `ruv-neural-memory` | `bench_brute_force_nn` | 100, 1K, 10K embeddings x 256-d | us/op |
|
||||
| `ruv-neural-memory` | `bench_hnsw_insert` | 1K, 10K embeddings x 256-d | us/op |
|
||||
| `ruv-neural-memory` | `bench_hnsw_search` | 1K, 10K embeddings, k=10, ef=50 | us/op |
|
||||
| `ruv-neural-esp32` | `bench_adc_read` | 100, 1000 samples x 1-16 channels | us/op |
|
||||
|
||||
#### Full Pipeline Profiling
|
||||
|
||||
```bash
|
||||
# Generate a flamegraph of the full pipeline
|
||||
cargo flamegraph --bench full_pipeline -- --bench
|
||||
|
||||
# Memory profiling with DHAT
|
||||
cargo test --features dhat-heap -- --test full_pipeline
|
||||
```
|
||||
|
||||
#### WASM Performance
|
||||
|
||||
```javascript
|
||||
// When ruv-neural-wasm is implemented, measure with:
|
||||
performance.mark('embed-start');
|
||||
const embedding = ruv_neural.embed(graphData);
|
||||
performance.mark('embed-end');
|
||||
performance.measure('embed', 'embed-start', 'embed-end');
|
||||
```
|
||||
|
||||
#### ESP32 Hardware Timing
|
||||
|
||||
```rust
|
||||
// Use esp-idf-hal's timer for hardware-level benchmarks
|
||||
let start = esp_idf_hal::timer::now();
|
||||
let samples = reader.read_samples(1000)?;
|
||||
let elapsed_us = esp_idf_hal::timer::now() - start;
|
||||
```
|
||||
|
||||
### Performance Findings from Code Audit
|
||||
|
||||
#### PERF-001: Redundant Hilbert Transforms (Severity: High)
|
||||
|
||||
**Location**: `ruv-neural-signal/src/connectivity.rs`, `compute_all_pairs`
|
||||
|
||||
Each call to `phase_locking_value`, `coherence`, `imaginary_coherence`, or `amplitude_envelope_correlation` independently calls `hilbert_transform` on both input signals. In `compute_all_pairs` with 68 channels, each channel's analytic signal is computed 67 times.
|
||||
|
||||
**Impact**: For 68 channels x 1024 samples, this means 4,556 FFTs instead of 68. Estimated waste: ~98.5% of FFT compute in the connectivity matrix.
|
||||
|
||||
**Fix**: Pre-compute all analytic signals, then pass slices to pairwise metrics:
|
||||
```rust
|
||||
pub fn compute_all_pairs_optimized(channels: &[Vec<f64>], metric: &ConnectivityMetric) -> Vec<Vec<f64>> {
|
||||
let analytics: Vec<Vec<Complex<f64>>> = channels.iter()
|
||||
.map(|ch| hilbert_transform(ch))
|
||||
.collect();
|
||||
// ... use pre-computed analytics for all pair computations
|
||||
}
|
||||
```
|
||||
|
||||
#### PERF-002: O(n) Eviction in Memory Store (Severity: Medium)
|
||||
|
||||
**Location**: `ruv-neural-memory/src/store.rs`, `evict_oldest`
|
||||
|
||||
```rust
|
||||
fn evict_oldest(&mut self) {
|
||||
self.embeddings.remove(0); // O(n) shift
|
||||
self.rebuild_index(); // O(n) rebuild
|
||||
}
|
||||
```
|
||||
|
||||
For a store with 10K embeddings, every insertion at capacity triggers an O(n) shift and full index rebuild.
|
||||
|
||||
**Fix**: Use `VecDeque<NeuralEmbedding>` and maintain the index incrementally.
|
||||
|
||||
#### PERF-003: FFT Planner Re-creation (Severity: Medium)
|
||||
|
||||
**Location**: `ruv-neural-signal/src/spectral.rs` (lines 12-13), `hilbert.rs` (lines 25-27)
|
||||
|
||||
A new `FftPlanner` is created on every function call. `rustfft` caches FFT plans internally in the planner, but creating a new planner discards the cache.
|
||||
|
||||
**Fix**: Use a thread-local or static planner:
|
||||
```rust
|
||||
thread_local! {
|
||||
static FFT_PLANNER: RefCell<FftPlanner<f64>> = RefCell::new(FftPlanner::new());
|
||||
}
|
||||
```
|
||||
|
||||
#### PERF-004: Dense Adjacency for Sparse Graphs (Severity: Low)
|
||||
|
||||
**Location**: `ruv-neural-core/src/graph.rs`, `adjacency_matrix`
|
||||
|
||||
Always allocates an N x N matrix even when the graph has far fewer edges. For Schaefer400 with ~5K edges, this allocates 1.25 MB for a matrix that is ~97% zeros.
|
||||
|
||||
**Fix**: Return a sparse representation for large graphs, or provide both `adjacency_matrix()` and `sparse_adjacency()`.
|
||||
|
||||
#### PERF-005: Power Iteration Convergence Not Checked (Severity: Low)
|
||||
|
||||
**Location**: `ruv-neural-mincut/src/spectral_cut.rs`, `largest_eigenvalue`
|
||||
|
||||
Runs a fixed 200 iterations regardless of convergence. Many graphs converge in 20-50 iterations.
|
||||
|
||||
**Fix**: Add early termination when eigenvalue change < epsilon:
|
||||
```rust
|
||||
if (eigenvalue - prev_eigenvalue).abs() < 1e-12 {
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
Note: `fiedler_decomposition` already has this check, but `largest_eigenvalue` does not.
|
||||
|
||||
---
|
||||
|
||||
## Action Items
|
||||
|
||||
### Critical (Must fix before v0.1 release)
|
||||
|
||||
- [ ] **SEC-001**: Add maximum size limits to RVF deserialization
|
||||
- [ ] **SEC-002**: Validate `sample_rate_hz > 0` and `is_finite()` in `MultiChannelTimeSeries::new`
|
||||
- [ ] **SEC-004**: Fix integer overflow in `AdcConfig::max_raw_value`
|
||||
- [ ] **PERF-001**: Pre-compute Hilbert transforms in `compute_all_pairs`
|
||||
|
||||
### Important (Should fix before v0.1 release)
|
||||
|
||||
- [ ] **SEC-003**: Add NaN/Inf validation for signal data at pipeline entry points
|
||||
- [ ] **SEC-005**: Add bounds check on HNSW entry point index
|
||||
- [ ] **PERF-002**: Replace `Vec::remove(0)` with `VecDeque` in memory store
|
||||
- [ ] **PERF-003**: Cache FFT planner across calls
|
||||
- [ ] Add `BrainGraph::validate()` for edge index bounds and weight finiteness
|
||||
- [ ] Add dimension consistency check to `NeuralMemoryStore::store`
|
||||
- [ ] Remove unused workspace dependencies (`petgraph`, `tokio`, `ruvector-*`)
|
||||
|
||||
### Recommended (Fix in v0.2)
|
||||
|
||||
- [ ] Implement `zeroize`-on-drop for `NeuralEmbedding` and `NeuralMemoryStore`
|
||||
- [ ] Add `strip_pii()` to `RvfFile`
|
||||
- [ ] Migrate `Vec<Vec<f64>>` matrices to `ndarray::Array2<f64>`
|
||||
- [ ] Add Rayon parallelism for connectivity matrix computation
|
||||
- [ ] Add criterion benchmarks for all crates
|
||||
- [ ] Implement TDM protocol with CRC32 and node authentication
|
||||
- [ ] Add `cargo deny` and `cargo audit` to CI
|
||||
- [ ] Profile and optimize binary size for ESP32
|
||||
|
||||
### Future (v0.3+)
|
||||
|
||||
- [ ] Encryption-at-rest for `NeuralMemoryStore`
|
||||
- [ ] DTLS/TLS for ESP32 network protocol
|
||||
- [ ] Sparse graph representation for large atlases
|
||||
- [ ] f32 quantized embeddings for WASM
|
||||
- [ ] Streaming signal processing pipeline
|
||||
- [ ] GPU backend for large-scale spectral analysis
|
||||
|
||||
---
|
||||
|
||||
*This document should be reviewed and updated after each milestone. All security findings should be verified as resolved before the corresponding release.*
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "ruv-neural-cli"
|
||||
description = "rUv Neural — CLI tool for brain topology analysis, simulation, and visualization"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "ruv-neural"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
ruv-neural-core = { workspace = true }
|
||||
ruv-neural-sensor = { workspace = true }
|
||||
ruv-neural-signal = { workspace = true }
|
||||
ruv-neural-graph = { workspace = true }
|
||||
ruv-neural-mincut = { workspace = true }
|
||||
ruv-neural-embed = { workspace = true }
|
||||
ruv-neural-memory = { workspace = true }
|
||||
ruv-neural-decoder = { workspace = true }
|
||||
ruv-neural-viz = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
@@ -0,0 +1,112 @@
|
||||
# ruv-neural-cli
|
||||
|
||||
CLI tool for brain topology analysis, simulation, and visualization.
|
||||
|
||||
## Overview
|
||||
|
||||
`ruv-neural-cli` is the command-line binary (`ruv-neural`) that ties together
|
||||
the entire rUv Neural crate ecosystem. It provides subcommands for simulating
|
||||
neural sensor data, analyzing brain connectivity graphs, computing minimum cuts,
|
||||
running the full processing pipeline with an optional ASCII dashboard, and
|
||||
exporting to multiple visualization formats.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Build from source
|
||||
cargo install --path .
|
||||
|
||||
# Or run directly
|
||||
cargo run -p ruv-neural-cli -- <command>
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### `simulate` -- Generate synthetic neural data
|
||||
|
||||
```bash
|
||||
ruv-neural simulate --channels 64 --duration 10 --sample-rate 1000 --output data.json
|
||||
```
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------------------|---------|------------------------------|
|
||||
| `-c, --channels` | 64 | Number of sensor channels |
|
||||
| `-d, --duration` | 10.0 | Duration in seconds |
|
||||
| `-s, --sample-rate` | 1000.0 | Sample rate in Hz |
|
||||
| `-o, --output` | (none) | Output file path (JSON) |
|
||||
|
||||
### `analyze` -- Analyze a brain connectivity graph
|
||||
|
||||
```bash
|
||||
ruv-neural analyze --input graph.json --ascii --csv metrics.csv
|
||||
```
|
||||
|
||||
| Flag | Default | Description |
|
||||
|----------------|---------|--------------------------------|
|
||||
| `-i, --input` | (required) | Input graph file (JSON) |
|
||||
| `--ascii` | false | Show ASCII visualization |
|
||||
| `--csv` | (none) | Export metrics to CSV file |
|
||||
|
||||
### `mincut` -- Compute minimum cut
|
||||
|
||||
```bash
|
||||
ruv-neural mincut --input graph.json --k 4
|
||||
```
|
||||
|
||||
| Flag | Default | Description |
|
||||
|----------------|---------|--------------------------------|
|
||||
| `-i, --input` | (required) | Input graph file (JSON) |
|
||||
| `-k` | (none) | Multi-way cut with k partitions|
|
||||
|
||||
### `pipeline` -- Full end-to-end pipeline
|
||||
|
||||
```bash
|
||||
ruv-neural pipeline --channels 32 --duration 5 --dashboard
|
||||
```
|
||||
|
||||
Runs: simulate -> preprocess -> build graph -> mincut -> embed -> decode.
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------------------|---------|--------------------------------|
|
||||
| `-c, --channels` | 32 | Number of sensor channels |
|
||||
| `-d, --duration` | 5.0 | Duration in seconds |
|
||||
| `--dashboard` | false | Show real-time ASCII dashboard |
|
||||
|
||||
### `export` -- Export to visualization format
|
||||
|
||||
```bash
|
||||
ruv-neural export --input graph.json --format dot --output graph.dot
|
||||
```
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------------------|---------|---------------------------------------|
|
||||
| `-i, --input` | (required) | Input graph file (JSON) |
|
||||
| `-f, --format` | d3 | Output format: d3, dot, gexf, csv, rvf |
|
||||
| `-o, --output` | (required) | Output file path |
|
||||
|
||||
### `info` -- Show system information
|
||||
|
||||
```bash
|
||||
ruv-neural info
|
||||
```
|
||||
|
||||
Displays crate versions, available features, and system capabilities.
|
||||
|
||||
## Global Options
|
||||
|
||||
| Flag | Description |
|
||||
|------------------|------------------------------------|
|
||||
| `-v` | Increase verbosity (up to `-vvv`) |
|
||||
| `--version` | Print version |
|
||||
| `--help` | Print help |
|
||||
|
||||
## Integration
|
||||
|
||||
Depends on all workspace crates: `ruv-neural-core`, `ruv-neural-sensor`,
|
||||
`ruv-neural-signal`, `ruv-neural-graph`, `ruv-neural-mincut`, `ruv-neural-embed`,
|
||||
`ruv-neural-memory`, `ruv-neural-decoder`, and `ruv-neural-viz`. Uses `clap`
|
||||
for argument parsing and `tokio` for async runtime.
|
||||
|
||||
## License
|
||||
|
||||
MIT OR Apache-2.0
|
||||
@@ -0,0 +1,237 @@
|
||||
//! Analyze a brain connectivity graph: compute topology metrics and display results.
|
||||
|
||||
use std::fs;
|
||||
|
||||
use ruv_neural_core::graph::BrainGraph;
|
||||
use ruv_neural_mincut::stoer_wagner_mincut;
|
||||
|
||||
/// Run the analyze command.
|
||||
pub fn run(
|
||||
input: &str,
|
||||
ascii: bool,
|
||||
csv_output: Option<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing::info!(input, "Loading brain graph");
|
||||
|
||||
let json = fs::read_to_string(input)
|
||||
.map_err(|e| format!("Failed to read {input}: {e}"))?;
|
||||
let graph: BrainGraph = serde_json::from_str(&json)
|
||||
.map_err(|e| format!("Failed to parse graph JSON: {e}"))?;
|
||||
|
||||
println!("=== rUv Neural — Graph Analysis ===");
|
||||
println!();
|
||||
println!(" Nodes: {}", graph.num_nodes);
|
||||
println!(" Edges: {}", graph.edges.len());
|
||||
println!(" Density: {:.4}", graph.density());
|
||||
println!(" Total weight: {:.4}", graph.total_weight());
|
||||
println!(" Timestamp: {:.2} s", graph.timestamp);
|
||||
println!(" Window duration: {:.2} s", graph.window_duration_s);
|
||||
println!(" Atlas: {:?}", graph.atlas);
|
||||
println!();
|
||||
|
||||
// Degree statistics.
|
||||
let degrees: Vec<f64> = (0..graph.num_nodes)
|
||||
.map(|i| graph.node_degree(i))
|
||||
.collect();
|
||||
let mean_degree = if degrees.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
degrees.iter().sum::<f64>() / degrees.len() as f64
|
||||
};
|
||||
let max_degree = degrees.iter().cloned().fold(0.0_f64, f64::max);
|
||||
let min_degree = degrees.iter().cloned().fold(f64::INFINITY, f64::min);
|
||||
|
||||
println!(" Degree statistics:");
|
||||
println!(" Mean: {mean_degree:.4}");
|
||||
println!(" Min: {min_degree:.4}");
|
||||
println!(" Max: {max_degree:.4}");
|
||||
println!();
|
||||
|
||||
// Mincut.
|
||||
match stoer_wagner_mincut(&graph) {
|
||||
Ok(mc) => {
|
||||
println!(" Minimum cut:");
|
||||
println!(" Cut value: {:.4}", mc.cut_value);
|
||||
println!(" Partition A: {} nodes {:?}", mc.partition_a.len(), mc.partition_a);
|
||||
println!(" Partition B: {} nodes {:?}", mc.partition_b.len(), mc.partition_b);
|
||||
println!(" Cut edges: {}", mc.cut_edges.len());
|
||||
println!(" Balance ratio: {:.4}", mc.balance_ratio());
|
||||
println!();
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" Minimum cut: could not compute ({e})");
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
// Edge weight distribution.
|
||||
if !graph.edges.is_empty() {
|
||||
let weights: Vec<f64> = graph.edges.iter().map(|e| e.weight).collect();
|
||||
let mean_w = weights.iter().sum::<f64>() / weights.len() as f64;
|
||||
let max_w = weights.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
||||
let min_w = weights.iter().cloned().fold(f64::INFINITY, f64::min);
|
||||
|
||||
println!(" Edge weight distribution:");
|
||||
println!(" Mean: {mean_w:.4}");
|
||||
println!(" Min: {min_w:.4}");
|
||||
println!(" Max: {max_w:.4}");
|
||||
println!();
|
||||
}
|
||||
|
||||
if ascii {
|
||||
print_ascii_graph(&graph);
|
||||
}
|
||||
|
||||
if let Some(csv_path) = csv_output {
|
||||
write_csv(&graph, °rees, &csv_path)?;
|
||||
println!(" Metrics exported to: {csv_path}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Print a simple ASCII visualization of the graph adjacency.
|
||||
fn print_ascii_graph(graph: &BrainGraph) {
|
||||
println!(" ASCII Adjacency Matrix:");
|
||||
let n = graph.num_nodes.min(20); // cap display at 20x20
|
||||
let adj = graph.adjacency_matrix();
|
||||
|
||||
// Header row.
|
||||
print!(" ");
|
||||
for j in 0..n {
|
||||
print!("{j:>4}");
|
||||
}
|
||||
println!();
|
||||
|
||||
for i in 0..n {
|
||||
print!(" {i:>3} ");
|
||||
for j in 0..n {
|
||||
let w = adj[i][j];
|
||||
if i == j {
|
||||
print!(" .");
|
||||
} else if w > 0.0 {
|
||||
// Map weight to a character.
|
||||
let ch = if w > 0.8 {
|
||||
'#'
|
||||
} else if w > 0.5 {
|
||||
'*'
|
||||
} else if w > 0.2 {
|
||||
'+'
|
||||
} else {
|
||||
'.'
|
||||
};
|
||||
print!(" {ch}");
|
||||
} else {
|
||||
print!(" ");
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
if graph.num_nodes > 20 {
|
||||
println!(" ... ({} nodes total, showing first 20)", graph.num_nodes);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Write per-node metrics to a CSV file.
|
||||
fn write_csv(
|
||||
graph: &BrainGraph,
|
||||
degrees: &[f64],
|
||||
path: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut csv = String::from("node,degree,num_edges\n");
|
||||
for i in 0..graph.num_nodes {
|
||||
let num_edges = graph
|
||||
.edges
|
||||
.iter()
|
||||
.filter(|e| e.source == i || e.target == i)
|
||||
.count();
|
||||
csv.push_str(&format!(
|
||||
"{},{:.6},{}\n",
|
||||
i,
|
||||
degrees.get(i).copied().unwrap_or(0.0),
|
||||
num_edges
|
||||
));
|
||||
}
|
||||
fs::write(path, csv)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ruv_neural_core::brain::Atlas;
|
||||
use ruv_neural_core::graph::{BrainEdge, ConnectivityMetric};
|
||||
use ruv_neural_core::signal::FrequencyBand;
|
||||
|
||||
fn test_graph() -> BrainGraph {
|
||||
BrainGraph {
|
||||
num_nodes: 4,
|
||||
edges: vec![
|
||||
BrainEdge {
|
||||
source: 0,
|
||||
target: 1,
|
||||
weight: 0.8,
|
||||
metric: ConnectivityMetric::PhaseLockingValue,
|
||||
frequency_band: FrequencyBand::Alpha,
|
||||
},
|
||||
BrainEdge {
|
||||
source: 1,
|
||||
target: 2,
|
||||
weight: 0.5,
|
||||
metric: ConnectivityMetric::PhaseLockingValue,
|
||||
frequency_band: FrequencyBand::Alpha,
|
||||
},
|
||||
BrainEdge {
|
||||
source: 2,
|
||||
target: 3,
|
||||
weight: 0.9,
|
||||
metric: ConnectivityMetric::PhaseLockingValue,
|
||||
frequency_band: FrequencyBand::Alpha,
|
||||
},
|
||||
],
|
||||
timestamp: 0.0,
|
||||
window_duration_s: 1.0,
|
||||
atlas: Atlas::Custom(4),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analyze_from_json() {
|
||||
let graph = test_graph();
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join("ruv_neural_test_analyze.json");
|
||||
let json = serde_json::to_string_pretty(&graph).unwrap();
|
||||
std::fs::write(&path, json).unwrap();
|
||||
|
||||
let result = run(&path.to_string_lossy(), false, None);
|
||||
assert!(result.is_ok());
|
||||
std::fs::remove_file(&path).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analyze_with_csv() {
|
||||
let graph = test_graph();
|
||||
let dir = std::env::temp_dir();
|
||||
let json_path = dir.join("ruv_neural_test_analyze2.json");
|
||||
let csv_path = dir.join("ruv_neural_test_analyze2.csv");
|
||||
|
||||
let json = serde_json::to_string_pretty(&graph).unwrap();
|
||||
std::fs::write(&json_path, json).unwrap();
|
||||
|
||||
let result = run(
|
||||
&json_path.to_string_lossy(),
|
||||
true,
|
||||
Some(csv_path.to_string_lossy().to_string()),
|
||||
);
|
||||
assert!(result.is_ok());
|
||||
assert!(csv_path.exists());
|
||||
|
||||
let csv_content = std::fs::read_to_string(&csv_path).unwrap();
|
||||
assert!(csv_content.starts_with("node,degree,num_edges"));
|
||||
|
||||
std::fs::remove_file(&json_path).ok();
|
||||
std::fs::remove_file(&csv_path).ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
//! Export brain graph to various visualization formats.
|
||||
|
||||
use std::fs;
|
||||
|
||||
use ruv_neural_core::graph::BrainGraph;
|
||||
|
||||
/// Run the export command.
|
||||
pub fn run(
|
||||
input: &str,
|
||||
format: &str,
|
||||
output: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing::info!(input, format, output, "Exporting brain graph");
|
||||
|
||||
let json =
|
||||
fs::read_to_string(input).map_err(|e| format!("Failed to read {input}: {e}"))?;
|
||||
let graph: BrainGraph =
|
||||
serde_json::from_str(&json).map_err(|e| format!("Failed to parse graph JSON: {e}"))?;
|
||||
|
||||
let content = match format {
|
||||
"d3" => export_d3(&graph)?,
|
||||
"dot" => export_dot(&graph),
|
||||
"gexf" => export_gexf(&graph),
|
||||
"csv" => export_csv(&graph),
|
||||
"rvf" => export_rvf(&graph)?,
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"Unknown format '{format}'. Supported: d3, dot, gexf, csv, rvf"
|
||||
)
|
||||
.into());
|
||||
}
|
||||
};
|
||||
|
||||
fs::write(output, content)?;
|
||||
|
||||
println!("=== rUv Neural — Export Complete ===");
|
||||
println!();
|
||||
println!(" Format: {format}");
|
||||
println!(" Input: {input}");
|
||||
println!(" Output: {output}");
|
||||
println!(" Nodes: {}", graph.num_nodes);
|
||||
println!(" Edges: {}", graph.edges.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Export to D3.js-compatible JSON format.
|
||||
fn export_d3(graph: &BrainGraph) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let nodes: Vec<serde_json::Value> = (0..graph.num_nodes)
|
||||
.map(|i| {
|
||||
serde_json::json!({
|
||||
"id": i,
|
||||
"degree": graph.node_degree(i),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let links: Vec<serde_json::Value> = graph
|
||||
.edges
|
||||
.iter()
|
||||
.map(|e| {
|
||||
serde_json::json!({
|
||||
"source": e.source,
|
||||
"target": e.target,
|
||||
"weight": e.weight,
|
||||
"metric": format!("{:?}", e.metric),
|
||||
"band": format!("{:?}", e.frequency_band),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let d3 = serde_json::json!({
|
||||
"nodes": nodes,
|
||||
"links": links,
|
||||
"metadata": {
|
||||
"num_nodes": graph.num_nodes,
|
||||
"num_edges": graph.edges.len(),
|
||||
"density": graph.density(),
|
||||
"total_weight": graph.total_weight(),
|
||||
"atlas": format!("{:?}", graph.atlas),
|
||||
"timestamp": graph.timestamp,
|
||||
}
|
||||
});
|
||||
|
||||
Ok(serde_json::to_string_pretty(&d3)?)
|
||||
}
|
||||
|
||||
/// Export to Graphviz DOT format.
|
||||
fn export_dot(graph: &BrainGraph) -> String {
|
||||
let mut dot = String::from("graph brain {\n");
|
||||
dot.push_str(" rankdir=LR;\n");
|
||||
dot.push_str(&format!(
|
||||
" label=\"Brain Graph ({} nodes, {} edges)\";\n",
|
||||
graph.num_nodes,
|
||||
graph.edges.len()
|
||||
));
|
||||
dot.push_str(" node [shape=circle];\n\n");
|
||||
|
||||
for i in 0..graph.num_nodes {
|
||||
let degree = graph.node_degree(i);
|
||||
let size = 0.3 + degree * 0.1;
|
||||
dot.push_str(&format!(
|
||||
" n{i} [label=\"{i}\", width={size:.2}];\n"
|
||||
));
|
||||
}
|
||||
dot.push('\n');
|
||||
|
||||
for edge in &graph.edges {
|
||||
let penwidth = 0.5 + edge.weight * 2.0;
|
||||
dot.push_str(&format!(
|
||||
" n{} -- n{} [penwidth={:.2}, label=\"{:.2}\"];\n",
|
||||
edge.source, edge.target, penwidth, edge.weight
|
||||
));
|
||||
}
|
||||
|
||||
dot.push_str("}\n");
|
||||
dot
|
||||
}
|
||||
|
||||
/// Export to GEXF (Graph Exchange XML Format).
|
||||
fn export_gexf(graph: &BrainGraph) -> String {
|
||||
let mut gexf = String::from(r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gexf xmlns="http://gexf.net/1.3" version="1.3">
|
||||
<meta>
|
||||
<creator>rUv Neural</creator>
|
||||
<description>Brain connectivity graph</description>
|
||||
</meta>
|
||||
<graph defaultedgetype="undirected">
|
||||
<nodes>
|
||||
"#);
|
||||
|
||||
for i in 0..graph.num_nodes {
|
||||
gexf.push_str(&format!(
|
||||
" <node id=\"{i}\" label=\"Region {i}\" />\n"
|
||||
));
|
||||
}
|
||||
|
||||
gexf.push_str(" </nodes>\n <edges>\n");
|
||||
|
||||
for (idx, edge) in graph.edges.iter().enumerate() {
|
||||
gexf.push_str(&format!(
|
||||
" <edge id=\"{idx}\" source=\"{}\" target=\"{}\" weight=\"{:.6}\" />\n",
|
||||
edge.source, edge.target, edge.weight
|
||||
));
|
||||
}
|
||||
|
||||
gexf.push_str(" </edges>\n </graph>\n</gexf>\n");
|
||||
gexf
|
||||
}
|
||||
|
||||
/// Export to CSV edge list.
|
||||
fn export_csv(graph: &BrainGraph) -> String {
|
||||
let mut csv = String::from("source,target,weight,metric,frequency_band\n");
|
||||
for edge in &graph.edges {
|
||||
csv.push_str(&format!(
|
||||
"{},{},{:.6},{:?},{:?}\n",
|
||||
edge.source, edge.target, edge.weight, edge.metric, edge.frequency_band
|
||||
));
|
||||
}
|
||||
csv
|
||||
}
|
||||
|
||||
/// Export to RVF (RuVector File) JSON representation.
|
||||
fn export_rvf(graph: &BrainGraph) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let rvf = serde_json::json!({
|
||||
"format": "rvf",
|
||||
"version": 1,
|
||||
"data_type": "BrainGraph",
|
||||
"num_nodes": graph.num_nodes,
|
||||
"num_edges": graph.edges.len(),
|
||||
"atlas": format!("{:?}", graph.atlas),
|
||||
"timestamp": graph.timestamp,
|
||||
"window_duration_s": graph.window_duration_s,
|
||||
"adjacency": graph.adjacency_matrix(),
|
||||
});
|
||||
Ok(serde_json::to_string_pretty(&rvf)?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ruv_neural_core::brain::Atlas;
|
||||
use ruv_neural_core::graph::{BrainEdge, ConnectivityMetric};
|
||||
use ruv_neural_core::signal::FrequencyBand;
|
||||
|
||||
fn test_graph() -> BrainGraph {
|
||||
BrainGraph {
|
||||
num_nodes: 3,
|
||||
edges: vec![
|
||||
BrainEdge {
|
||||
source: 0,
|
||||
target: 1,
|
||||
weight: 0.8,
|
||||
metric: ConnectivityMetric::PhaseLockingValue,
|
||||
frequency_band: FrequencyBand::Alpha,
|
||||
},
|
||||
BrainEdge {
|
||||
source: 1,
|
||||
target: 2,
|
||||
weight: 0.5,
|
||||
metric: ConnectivityMetric::Coherence,
|
||||
frequency_band: FrequencyBand::Beta,
|
||||
},
|
||||
],
|
||||
timestamp: 0.0,
|
||||
window_duration_s: 1.0,
|
||||
atlas: Atlas::Custom(3),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_d3_valid_json() {
|
||||
let graph = test_graph();
|
||||
let result = export_d3(&graph).unwrap();
|
||||
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
|
||||
assert!(parsed["nodes"].is_array());
|
||||
assert!(parsed["links"].is_array());
|
||||
assert_eq!(parsed["nodes"].as_array().unwrap().len(), 3);
|
||||
assert_eq!(parsed["links"].as_array().unwrap().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_dot_format() {
|
||||
let graph = test_graph();
|
||||
let result = export_dot(&graph);
|
||||
assert!(result.starts_with("graph brain {"));
|
||||
assert!(result.contains("n0 -- n1"));
|
||||
assert!(result.ends_with("}\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_gexf_format() {
|
||||
let graph = test_graph();
|
||||
let result = export_gexf(&graph);
|
||||
assert!(result.contains("<gexf"));
|
||||
assert!(result.contains("<node id=\"0\""));
|
||||
assert!(result.contains("</gexf>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_csv_format() {
|
||||
let graph = test_graph();
|
||||
let result = export_csv(&graph);
|
||||
assert!(result.starts_with("source,target,weight"));
|
||||
let lines: Vec<&str> = result.lines().collect();
|
||||
assert_eq!(lines.len(), 3); // header + 2 edges
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_rvf_valid_json() {
|
||||
let graph = test_graph();
|
||||
let result = export_rvf(&graph).unwrap();
|
||||
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
|
||||
assert_eq!(parsed["format"], "rvf");
|
||||
assert_eq!(parsed["num_nodes"], 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_all_formats() {
|
||||
let graph = test_graph();
|
||||
let dir = std::env::temp_dir();
|
||||
let json_path = dir.join("ruv_neural_test_export.json");
|
||||
let json = serde_json::to_string_pretty(&graph).unwrap();
|
||||
std::fs::write(&json_path, json).unwrap();
|
||||
|
||||
for fmt in &["d3", "dot", "gexf", "csv", "rvf"] {
|
||||
let out_path = dir.join(format!("ruv_neural_test_export.{fmt}"));
|
||||
let result = run(
|
||||
&json_path.to_string_lossy(),
|
||||
fmt,
|
||||
&out_path.to_string_lossy(),
|
||||
);
|
||||
assert!(result.is_ok(), "Failed to export format: {fmt}");
|
||||
assert!(out_path.exists(), "Output file missing for format: {fmt}");
|
||||
std::fs::remove_file(&out_path).ok();
|
||||
}
|
||||
|
||||
std::fs::remove_file(&json_path).ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//! Display system info and capabilities.
|
||||
|
||||
/// Run the info command.
|
||||
pub fn run() {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
println!("=== rUv Neural — System Information ===");
|
||||
println!();
|
||||
println!(" Version: {version}");
|
||||
println!(" Binary: ruv-neural");
|
||||
println!();
|
||||
println!(" Crate Versions:");
|
||||
println!(" ruv-neural-core {version}");
|
||||
println!(" ruv-neural-sensor {version}");
|
||||
println!(" ruv-neural-signal {version}");
|
||||
println!(" ruv-neural-graph {version}");
|
||||
println!(" ruv-neural-mincut {version}");
|
||||
println!(" ruv-neural-embed {version}");
|
||||
println!(" ruv-neural-memory {version}");
|
||||
println!(" ruv-neural-decoder {version}");
|
||||
println!(" ruv-neural-viz {version}");
|
||||
println!(" ruv-neural-cli {version}");
|
||||
println!();
|
||||
println!(" Features:");
|
||||
println!(" Sensor simulation [available]");
|
||||
println!(" Signal processing [available]");
|
||||
println!(" Bandpass filtering [available] (Butterworth IIR, SOS form)");
|
||||
println!(" Artifact rejection [available] (eye blink, muscle, cardiac)");
|
||||
println!(" PLV connectivity [available] (phase locking value)");
|
||||
println!(" Coherence metrics [available] (coherence, imaginary coherence)");
|
||||
println!(" Stoer-Wagner mincut [available] (global minimum cut)");
|
||||
println!(" Normalized cut [available] (Shi-Malik spectral bisection)");
|
||||
println!(" Multi-way cut [available] (recursive normalized cut)");
|
||||
println!(" Spectral embedding [available] (Laplacian eigenvector encoding)");
|
||||
println!(" Topology embedding [available] (hand-crafted topological features)");
|
||||
println!(" Node2Vec embedding [available] (random walk co-occurrence)");
|
||||
println!(" Threshold decoder [available] (rule-based cognitive state)");
|
||||
println!(" KNN decoder [available] (k-nearest neighbor classifier)");
|
||||
println!(" Force-directed layout [available] (Fruchterman-Reingold)");
|
||||
println!(" Anatomical layout [available] (MNI coordinate-based)");
|
||||
println!();
|
||||
println!(" Export Formats:");
|
||||
println!(" D3.js JSON [available]");
|
||||
println!(" Graphviz DOT [available]");
|
||||
println!(" GEXF (Graph Exchange) [available]");
|
||||
println!(" CSV edge list [available]");
|
||||
println!(" RVF (RuVector File) [available]");
|
||||
println!();
|
||||
println!(" Pipeline:");
|
||||
println!(" simulate -> filter -> PLV graph -> mincut -> embed -> decode");
|
||||
println!();
|
||||
println!(" Platform:");
|
||||
println!(" OS: {}", std::env::consts::OS);
|
||||
println!(" Arch: {}", std::env::consts::ARCH);
|
||||
println!(" Family: {}", std::env::consts::FAMILY);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn info_runs_without_panic() {
|
||||
run();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
//! Compute minimum cut on a brain connectivity graph.
|
||||
|
||||
use std::fs;
|
||||
|
||||
use ruv_neural_core::graph::BrainGraph;
|
||||
use ruv_neural_mincut::{multiway_cut, stoer_wagner_mincut};
|
||||
|
||||
/// Run the mincut command.
|
||||
pub fn run(input: &str, k: Option<usize>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing::info!(input, ?k, "Computing minimum cut");
|
||||
|
||||
let json =
|
||||
fs::read_to_string(input).map_err(|e| format!("Failed to read {input}: {e}"))?;
|
||||
let graph: BrainGraph =
|
||||
serde_json::from_str(&json).map_err(|e| format!("Failed to parse graph JSON: {e}"))?;
|
||||
|
||||
println!("=== rUv Neural — Minimum Cut Analysis ===");
|
||||
println!();
|
||||
println!(" Graph: {} nodes, {} edges", graph.num_nodes, graph.edges.len());
|
||||
println!();
|
||||
|
||||
match k {
|
||||
Some(k_val) if k_val > 2 => {
|
||||
// Multi-way cut.
|
||||
let result = multiway_cut(&graph, k_val)
|
||||
.map_err(|e| format!("Multiway cut failed: {e}"))?;
|
||||
|
||||
println!(" Multi-way cut (k={k_val}):");
|
||||
println!(" Total cut value: {:.4}", result.cut_value);
|
||||
println!(" Modularity: {:.4}", result.modularity);
|
||||
println!(" Partitions: {}", result.num_partitions());
|
||||
println!();
|
||||
|
||||
for (i, partition) in result.partitions.iter().enumerate() {
|
||||
println!(" Partition {i}: {} nodes {:?}", partition.len(), partition);
|
||||
}
|
||||
println!();
|
||||
|
||||
// ASCII visualization of partitions.
|
||||
print_partition_ascii(&graph, &result.partitions);
|
||||
}
|
||||
_ => {
|
||||
// Standard two-way Stoer-Wagner.
|
||||
let mc = stoer_wagner_mincut(&graph)
|
||||
.map_err(|e| format!("Stoer-Wagner mincut failed: {e}"))?;
|
||||
|
||||
println!(" Stoer-Wagner minimum cut:");
|
||||
println!(" Cut value: {:.4}", mc.cut_value);
|
||||
println!(" Partition A: {} nodes {:?}", mc.partition_a.len(), mc.partition_a);
|
||||
println!(" Partition B: {} nodes {:?}", mc.partition_b.len(), mc.partition_b);
|
||||
println!(" Balance ratio: {:.4}", mc.balance_ratio());
|
||||
println!();
|
||||
|
||||
println!(" Cut edges:");
|
||||
for (src, tgt, weight) in &mc.cut_edges {
|
||||
println!(" {src} -- {tgt} (weight: {weight:.4})");
|
||||
}
|
||||
println!();
|
||||
|
||||
// ASCII visualization of the two partitions.
|
||||
print_partition_ascii(&graph, &[mc.partition_a.clone(), mc.partition_b.clone()]);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Print an ASCII visualization of the graph partitions.
|
||||
fn print_partition_ascii(graph: &BrainGraph, partitions: &[Vec<usize>]) {
|
||||
println!(" Partition layout:");
|
||||
|
||||
// Build a node-to-partition map.
|
||||
let mut node_partition = vec![0usize; graph.num_nodes];
|
||||
for (pid, partition) in partitions.iter().enumerate() {
|
||||
for &node in partition {
|
||||
if node < graph.num_nodes {
|
||||
node_partition[node] = pid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Label characters for partitions.
|
||||
let labels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'];
|
||||
|
||||
let n = graph.num_nodes.min(40);
|
||||
print!(" ");
|
||||
for i in 0..n {
|
||||
let pid = node_partition[i];
|
||||
let ch = labels.get(pid).copied().unwrap_or('?');
|
||||
print!("{ch}");
|
||||
}
|
||||
println!();
|
||||
|
||||
if graph.num_nodes > 40 {
|
||||
println!(" ... ({} nodes total)", graph.num_nodes);
|
||||
}
|
||||
|
||||
println!();
|
||||
for (pid, partition) in partitions.iter().enumerate() {
|
||||
let ch = labels.get(pid).copied().unwrap_or('?');
|
||||
println!(" {ch} = {} nodes", partition.len());
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ruv_neural_core::brain::Atlas;
|
||||
use ruv_neural_core::graph::{BrainEdge, ConnectivityMetric};
|
||||
use ruv_neural_core::signal::FrequencyBand;
|
||||
|
||||
fn test_graph() -> BrainGraph {
|
||||
BrainGraph {
|
||||
num_nodes: 6,
|
||||
edges: vec![
|
||||
BrainEdge {
|
||||
source: 0,
|
||||
target: 1,
|
||||
weight: 5.0,
|
||||
metric: ConnectivityMetric::Coherence,
|
||||
frequency_band: FrequencyBand::Alpha,
|
||||
},
|
||||
BrainEdge {
|
||||
source: 1,
|
||||
target: 2,
|
||||
weight: 5.0,
|
||||
metric: ConnectivityMetric::Coherence,
|
||||
frequency_band: FrequencyBand::Alpha,
|
||||
},
|
||||
BrainEdge {
|
||||
source: 3,
|
||||
target: 4,
|
||||
weight: 5.0,
|
||||
metric: ConnectivityMetric::Coherence,
|
||||
frequency_band: FrequencyBand::Alpha,
|
||||
},
|
||||
BrainEdge {
|
||||
source: 4,
|
||||
target: 5,
|
||||
weight: 5.0,
|
||||
metric: ConnectivityMetric::Coherence,
|
||||
frequency_band: FrequencyBand::Alpha,
|
||||
},
|
||||
BrainEdge {
|
||||
source: 2,
|
||||
target: 3,
|
||||
weight: 0.5,
|
||||
metric: ConnectivityMetric::Coherence,
|
||||
frequency_band: FrequencyBand::Alpha,
|
||||
},
|
||||
],
|
||||
timestamp: 0.0,
|
||||
window_duration_s: 1.0,
|
||||
atlas: Atlas::Custom(6),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mincut_two_way() {
|
||||
let graph = test_graph();
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join("ruv_neural_test_mincut.json");
|
||||
let json = serde_json::to_string_pretty(&graph).unwrap();
|
||||
std::fs::write(&path, json).unwrap();
|
||||
|
||||
let result = run(&path.to_string_lossy(), None);
|
||||
assert!(result.is_ok());
|
||||
std::fs::remove_file(&path).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mincut_multiway() {
|
||||
let graph = test_graph();
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join("ruv_neural_test_mincut_k.json");
|
||||
let json = serde_json::to_string_pretty(&graph).unwrap();
|
||||
std::fs::write(&path, json).unwrap();
|
||||
|
||||
let result = run(&path.to_string_lossy(), Some(3));
|
||||
assert!(result.is_ok());
|
||||
std::fs::remove_file(&path).ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//! CLI command implementations.
|
||||
|
||||
pub mod analyze;
|
||||
pub mod export;
|
||||
pub mod info;
|
||||
pub mod mincut;
|
||||
pub mod pipeline;
|
||||
pub mod simulate;
|
||||
pub mod witness;
|
||||
+377
@@ -0,0 +1,377 @@
|
||||
//! Full end-to-end pipeline: simulate -> process -> analyze -> decode.
|
||||
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use ruv_neural_core::brain::Atlas;
|
||||
use ruv_neural_core::graph::{BrainEdge, BrainGraph, ConnectivityMetric};
|
||||
use ruv_neural_core::signal::{FrequencyBand, MultiChannelTimeSeries};
|
||||
use ruv_neural_core::topology::CognitiveState;
|
||||
use ruv_neural_decoder::ThresholdDecoder;
|
||||
use ruv_neural_embed::spectral_embed::SpectralEmbedder;
|
||||
use ruv_neural_embed::topology_embed::TopologyEmbedder;
|
||||
use ruv_neural_mincut::stoer_wagner_mincut;
|
||||
use ruv_neural_signal::connectivity::phase_locking_value;
|
||||
use ruv_neural_signal::filter::BandpassFilter;
|
||||
|
||||
/// Run the full pipeline command.
|
||||
pub fn run(
|
||||
channels: usize,
|
||||
duration: f64,
|
||||
dashboard: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let sample_rate = 1000.0;
|
||||
let num_samples = (duration * sample_rate) as usize;
|
||||
|
||||
println!("=== rUv Neural — Full Pipeline ===");
|
||||
println!();
|
||||
|
||||
// Step 1: Generate simulated sensor data.
|
||||
println!(" [1/7] Generating simulated sensor data...");
|
||||
let raw_data = generate_data(channels, num_samples, sample_rate);
|
||||
let ts = MultiChannelTimeSeries::new(raw_data.clone(), sample_rate, 0.0)
|
||||
.map_err(|e| format!("Time series creation failed: {e}"))?;
|
||||
println!(" {channels} channels, {num_samples} samples, {duration:.1}s");
|
||||
|
||||
// Step 2: Preprocess (bandpass filter 1-100 Hz).
|
||||
println!(" [2/7] Preprocessing (bandpass 1-100 Hz)...");
|
||||
let filter = BandpassFilter::new(4, 1.0, 100.0, sample_rate);
|
||||
let filtered: Vec<Vec<f64>> = raw_data
|
||||
.iter()
|
||||
.map(|ch| {
|
||||
use ruv_neural_signal::filter::SignalProcessor;
|
||||
filter.process(ch)
|
||||
})
|
||||
.collect();
|
||||
println!(" Bandpass filter applied to all channels");
|
||||
|
||||
// Step 3: Construct brain graph via PLV connectivity.
|
||||
println!(" [3/7] Constructing brain connectivity graph (PLV)...");
|
||||
let graph = build_plv_graph(&filtered, sample_rate);
|
||||
println!(
|
||||
" {} nodes, {} edges, density {:.4}",
|
||||
graph.num_nodes,
|
||||
graph.edges.len(),
|
||||
graph.density()
|
||||
);
|
||||
|
||||
// Step 4: Compute mincut and topology metrics.
|
||||
println!(" [4/7] Computing minimum cut and topology metrics...");
|
||||
let mc = stoer_wagner_mincut(&graph)
|
||||
.map_err(|e| format!("Mincut failed: {e}"))?;
|
||||
println!(" Cut value: {:.4}, balance: {:.4}", mc.cut_value, mc.balance_ratio());
|
||||
println!(
|
||||
" Partition A: {} nodes, Partition B: {} nodes",
|
||||
mc.partition_a.len(),
|
||||
mc.partition_b.len()
|
||||
);
|
||||
|
||||
// Step 5: Generate embedding.
|
||||
println!(" [5/7] Generating topology embedding...");
|
||||
let embedder = TopologyEmbedder::new();
|
||||
let embedding = embedder.embed_graph(&graph)
|
||||
.map_err(|e| format!("Embedding failed: {e}"))?;
|
||||
println!(" Dimension: {}, norm: {:.4}", embedding.dimension, embedding.norm());
|
||||
|
||||
// Also generate spectral embedding.
|
||||
let spectral_dim = channels.min(8).max(2);
|
||||
let spectral = SpectralEmbedder::new(spectral_dim);
|
||||
let spectral_emb = spectral.embed_graph(&graph)
|
||||
.map_err(|e| format!("Spectral embedding failed: {e}"))?;
|
||||
println!(
|
||||
" Spectral embedding: dim={}, norm={:.4}",
|
||||
spectral_emb.dimension,
|
||||
spectral_emb.norm()
|
||||
);
|
||||
|
||||
// Step 6: Decode cognitive state.
|
||||
println!(" [6/7] Decoding cognitive state...");
|
||||
let decoder = build_default_decoder();
|
||||
let metrics = ruv_neural_core::topology::TopologyMetrics {
|
||||
global_mincut: mc.cut_value,
|
||||
modularity: estimate_modularity(&graph),
|
||||
global_efficiency: estimate_efficiency(&graph),
|
||||
local_efficiency: 0.0,
|
||||
graph_entropy: estimate_entropy(&graph),
|
||||
fiedler_value: 0.0,
|
||||
num_modules: 2,
|
||||
timestamp: graph.timestamp,
|
||||
};
|
||||
let (state, confidence) = decoder.decode(&metrics);
|
||||
println!(" State: {state:?}");
|
||||
println!(" Confidence: {confidence:.4}");
|
||||
|
||||
// Step 7: Display results.
|
||||
println!(" [7/7] Results summary");
|
||||
println!();
|
||||
|
||||
println!(" ┌─────────────────────────────────────────┐");
|
||||
println!(" │ Pipeline Results Summary │");
|
||||
println!(" ├─────────────────────────────────────────┤");
|
||||
println!(" │ Channels: {:<20} │", channels);
|
||||
println!(" │ Duration: {:<20} │", format!("{duration:.1} s"));
|
||||
println!(" │ Graph density: {:<20} │", format!("{:.4}", graph.density()));
|
||||
println!(" │ Mincut value: {:<20} │", format!("{:.4}", mc.cut_value));
|
||||
println!(" │ Balance ratio: {:<20} │", format!("{:.4}", mc.balance_ratio()));
|
||||
println!(" │ Modularity: {:<20} │", format!("{:.4}", metrics.modularity));
|
||||
println!(" │ Graph entropy: {:<20} │", format!("{:.4}", metrics.graph_entropy));
|
||||
println!(" │ Embedding dim: {:<20} │", embedding.dimension);
|
||||
println!(" │ Cognitive state: {:<20} │", format!("{state:?}"));
|
||||
println!(" │ Confidence: {:<20} │", format!("{confidence:.4}"));
|
||||
println!(" └─────────────────────────────────────────┘");
|
||||
println!();
|
||||
|
||||
if dashboard {
|
||||
print_dashboard(&ts, &graph, &mc, &metrics);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate synthetic multi-channel neural data.
|
||||
fn generate_data(channels: usize, num_samples: usize, sample_rate: f64) -> Vec<Vec<f64>> {
|
||||
let mut data = Vec::with_capacity(channels);
|
||||
for ch in 0..channels {
|
||||
let mut channel_data = Vec::with_capacity(num_samples);
|
||||
let phase = (ch as f64) * PI / (channels as f64);
|
||||
let mut rng: u64 = (ch as u64).wrapping_mul(2862933555777941757).wrapping_add(3037000493);
|
||||
|
||||
for i in 0..num_samples {
|
||||
let t = i as f64 / sample_rate;
|
||||
let alpha = 50.0 * (2.0 * PI * 10.0 * t + phase).sin();
|
||||
let beta = 30.0 * (2.0 * PI * 20.0 * t + phase * 1.3).sin();
|
||||
let gamma = 15.0 * (2.0 * PI * 40.0 * t + phase * 0.7).sin();
|
||||
|
||||
rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||
let u1 = (rng >> 11) as f64 / (1u64 << 53) as f64;
|
||||
rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||
let u2 = (rng >> 11) as f64 / (1u64 << 53) as f64;
|
||||
let noise = if u1 > 1e-15 {
|
||||
5.0 * (-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
channel_data.push(alpha + beta + gamma + noise);
|
||||
}
|
||||
data.push(channel_data);
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
/// Build a brain graph from PLV connectivity between all channel pairs.
|
||||
fn build_plv_graph(channels: &[Vec<f64>], sample_rate: f64) -> BrainGraph {
|
||||
let n = channels.len();
|
||||
let mut edges = Vec::new();
|
||||
let plv_threshold = 0.3;
|
||||
|
||||
for i in 0..n {
|
||||
for j in (i + 1)..n {
|
||||
let plv = phase_locking_value(&channels[i], &channels[j], sample_rate, FrequencyBand::Alpha);
|
||||
if plv > plv_threshold {
|
||||
edges.push(BrainEdge {
|
||||
source: i,
|
||||
target: j,
|
||||
weight: plv,
|
||||
metric: ConnectivityMetric::PhaseLockingValue,
|
||||
frequency_band: FrequencyBand::Alpha,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BrainGraph {
|
||||
num_nodes: n,
|
||||
edges,
|
||||
timestamp: 0.0,
|
||||
window_duration_s: 1.0,
|
||||
atlas: Atlas::Custom(n),
|
||||
}
|
||||
}
|
||||
|
||||
/// Estimate modularity using a simple degree-based partition.
|
||||
fn estimate_modularity(graph: &BrainGraph) -> f64 {
|
||||
let n = graph.num_nodes;
|
||||
if n < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
let total = graph.total_weight();
|
||||
if total < 1e-12 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let adj = graph.adjacency_matrix();
|
||||
let degrees: Vec<f64> = (0..n).map(|i| graph.node_degree(i)).collect();
|
||||
let two_m = 2.0 * total;
|
||||
|
||||
// Simple bisection: first half vs second half.
|
||||
let mid = n / 2;
|
||||
let mut q = 0.0;
|
||||
for i in 0..n {
|
||||
for j in 0..n {
|
||||
let same_community = (i < mid && j < mid) || (i >= mid && j >= mid);
|
||||
if same_community {
|
||||
q += adj[i][j] - degrees[i] * degrees[j] / two_m;
|
||||
}
|
||||
}
|
||||
}
|
||||
q / two_m
|
||||
}
|
||||
|
||||
/// Estimate global efficiency (mean inverse shortest path).
|
||||
fn estimate_efficiency(graph: &BrainGraph) -> f64 {
|
||||
let n = graph.num_nodes;
|
||||
if n < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
// Use adjacency weights directly as a rough proxy.
|
||||
let adj = graph.adjacency_matrix();
|
||||
let mut sum = 0.0;
|
||||
let mut count = 0;
|
||||
for i in 0..n {
|
||||
for j in (i + 1)..n {
|
||||
if adj[i][j] > 0.0 {
|
||||
sum += adj[i][j]; // weight as proxy for efficiency
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
if count == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
sum / count as f64
|
||||
}
|
||||
|
||||
/// Estimate graph entropy from edge weight distribution.
|
||||
fn estimate_entropy(graph: &BrainGraph) -> f64 {
|
||||
let total = graph.total_weight();
|
||||
if total < 1e-12 || graph.edges.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let mut entropy = 0.0;
|
||||
for edge in &graph.edges {
|
||||
let p = edge.weight / total;
|
||||
if p > 1e-15 {
|
||||
entropy -= p * p.ln();
|
||||
}
|
||||
}
|
||||
entropy
|
||||
}
|
||||
|
||||
/// Build a threshold decoder with default state definitions.
|
||||
fn build_default_decoder() -> ThresholdDecoder {
|
||||
let mut decoder = ThresholdDecoder::new();
|
||||
|
||||
decoder.set_threshold(
|
||||
CognitiveState::Rest,
|
||||
ruv_neural_decoder::TopologyThreshold {
|
||||
mincut_range: (0.0, 5.0),
|
||||
modularity_range: (0.2, 0.6),
|
||||
efficiency_range: (0.1, 0.4),
|
||||
entropy_range: (1.0, 3.0),
|
||||
},
|
||||
);
|
||||
|
||||
decoder.set_threshold(
|
||||
CognitiveState::Focused,
|
||||
ruv_neural_decoder::TopologyThreshold {
|
||||
mincut_range: (3.0, 15.0),
|
||||
modularity_range: (0.4, 0.8),
|
||||
efficiency_range: (0.3, 0.7),
|
||||
entropy_range: (2.0, 4.0),
|
||||
},
|
||||
);
|
||||
|
||||
decoder.set_threshold(
|
||||
CognitiveState::MotorPlanning,
|
||||
ruv_neural_decoder::TopologyThreshold {
|
||||
mincut_range: (2.0, 10.0),
|
||||
modularity_range: (0.3, 0.7),
|
||||
efficiency_range: (0.2, 0.6),
|
||||
entropy_range: (1.5, 3.5),
|
||||
},
|
||||
);
|
||||
|
||||
decoder
|
||||
}
|
||||
|
||||
/// Print a real-time-style ASCII dashboard.
|
||||
fn print_dashboard(
|
||||
ts: &MultiChannelTimeSeries,
|
||||
graph: &BrainGraph,
|
||||
mc: &ruv_neural_core::topology::MincutResult,
|
||||
metrics: &ruv_neural_core::topology::TopologyMetrics,
|
||||
) {
|
||||
println!(" ╔═══════════════════════════════════════════════════╗");
|
||||
println!(" ║ rUv Neural — Live Dashboard ║");
|
||||
println!(" ╠═══════════════════════════════════════════════════╣");
|
||||
println!(" ║ ║");
|
||||
|
||||
// Signal sparkline for first few channels.
|
||||
let display_channels = ts.num_channels.min(6);
|
||||
let display_samples = ts.num_samples.min(50);
|
||||
let sparkline_chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
||||
|
||||
for ch in 0..display_channels {
|
||||
let data = &ts.data[ch];
|
||||
let min_val = data.iter().cloned().fold(f64::INFINITY, f64::min);
|
||||
let max_val = data.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
||||
let range = max_val - min_val;
|
||||
|
||||
let step = ts.num_samples / display_samples;
|
||||
let mut sparkline = String::new();
|
||||
for i in 0..display_samples {
|
||||
let val = data[i * step];
|
||||
let normalized = if range > 1e-12 {
|
||||
((val - min_val) / range * 7.0) as usize
|
||||
} else {
|
||||
4
|
||||
};
|
||||
sparkline.push(sparkline_chars[normalized.min(7)]);
|
||||
}
|
||||
println!(" ║ Ch{ch:02}: {sparkline} ║");
|
||||
}
|
||||
|
||||
println!(" ║ ║");
|
||||
println!(" ║ Graph: {} nodes, {} edges ║",
|
||||
format!("{:>3}", graph.num_nodes),
|
||||
format!("{:>4}", graph.edges.len()),
|
||||
);
|
||||
println!(" ║ Mincut: {:.4} Balance: {:.4} ║", mc.cut_value, mc.balance_ratio());
|
||||
println!(" ║ Modularity: {:.4} Entropy: {:.4} ║", metrics.modularity, metrics.graph_entropy);
|
||||
println!(" ║ ║");
|
||||
println!(" ╚═══════════════════════════════════════════════════╝");
|
||||
println!();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pipeline_runs_end_to_end() {
|
||||
let result = run(4, 1.0, false);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipeline_with_dashboard() {
|
||||
let result = run(4, 0.5, true);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plv_graph_has_edges() {
|
||||
let data = generate_data(4, 1000, 1000.0);
|
||||
let graph = build_plv_graph(&data, 1000.0);
|
||||
assert_eq!(graph.num_nodes, 4);
|
||||
// Channels with similar phase should have some PLV connectivity.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entropy_non_negative() {
|
||||
let data = generate_data(4, 1000, 1000.0);
|
||||
let graph = build_plv_graph(&data, 1000.0);
|
||||
let e = estimate_entropy(&graph);
|
||||
assert!(e >= 0.0);
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
//! Simulate neural sensor data and write to JSON or stdout.
|
||||
|
||||
use std::f64::consts::PI;
|
||||
use std::fs;
|
||||
|
||||
use ruv_neural_core::signal::MultiChannelTimeSeries;
|
||||
|
||||
/// Run the simulate command.
|
||||
///
|
||||
/// Generates synthetic multi-channel neural data with configurable alpha,
|
||||
/// beta, and gamma oscillations plus realistic noise.
|
||||
pub fn run(
|
||||
channels: usize,
|
||||
duration: f64,
|
||||
sample_rate: f64,
|
||||
output: Option<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let num_samples = (duration * sample_rate) as usize;
|
||||
if num_samples == 0 {
|
||||
return Err("Duration and sample rate must produce at least one sample".into());
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
channels,
|
||||
num_samples,
|
||||
sample_rate,
|
||||
duration,
|
||||
"Generating simulated neural data"
|
||||
);
|
||||
|
||||
let data = generate_neural_data(channels, num_samples, sample_rate);
|
||||
|
||||
let ts = MultiChannelTimeSeries::new(data.clone(), sample_rate, 0.0).map_err(|e| {
|
||||
Box::<dyn std::error::Error>::from(format!("Failed to create time series: {e}"))
|
||||
})?;
|
||||
|
||||
// Compute summary statistics.
|
||||
let mut channel_rms = Vec::with_capacity(channels);
|
||||
for ch in 0..channels {
|
||||
let rms = (data[ch].iter().map(|x| x * x).sum::<f64>() / num_samples as f64).sqrt();
|
||||
channel_rms.push(rms);
|
||||
}
|
||||
let mean_rms = channel_rms.iter().sum::<f64>() / channels as f64;
|
||||
|
||||
println!("=== rUv Neural — Simulation Complete ===");
|
||||
println!();
|
||||
println!(" Channels: {channels}");
|
||||
println!(" Samples: {num_samples}");
|
||||
println!(" Duration: {duration:.2} s");
|
||||
println!(" Sample rate: {sample_rate:.1} Hz");
|
||||
println!(" Mean RMS: {mean_rms:.4} fT");
|
||||
println!();
|
||||
|
||||
// Show frequency content summary.
|
||||
println!(" Frequency content:");
|
||||
println!(" Alpha (8-13 Hz): 10 Hz sinusoid, 50 fT amplitude");
|
||||
println!(" Beta (13-30 Hz): 20 Hz sinusoid, 30 fT amplitude");
|
||||
println!(" Gamma (30-100 Hz): 40 Hz sinusoid, 15 fT amplitude");
|
||||
println!(" Noise floor: ~10 fT/sqrt(Hz) white noise");
|
||||
println!();
|
||||
|
||||
match output {
|
||||
Some(ref path) => {
|
||||
let json = serde_json::to_string_pretty(&ts)?;
|
||||
fs::write(path, json)?;
|
||||
println!(" Output written to: {path}");
|
||||
}
|
||||
None => {
|
||||
println!(" (Use -o <file> to save output to JSON)");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate synthetic neural data with realistic oscillations and noise.
|
||||
fn generate_neural_data(channels: usize, num_samples: usize, sample_rate: f64) -> Vec<Vec<f64>> {
|
||||
// Use a deterministic seed based on channel index for reproducibility.
|
||||
let mut data = Vec::with_capacity(channels);
|
||||
|
||||
for ch in 0..channels {
|
||||
let mut channel_data = Vec::with_capacity(num_samples);
|
||||
// Phase offsets vary by channel to simulate spatial diversity.
|
||||
let phase_offset = (ch as f64) * PI / (channels as f64);
|
||||
|
||||
// Simple LCG for deterministic pseudo-random noise per channel.
|
||||
let mut rng_state: u64 = (ch as u64).wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
|
||||
for i in 0..num_samples {
|
||||
let t = i as f64 / sample_rate;
|
||||
|
||||
// Alpha rhythm: 10 Hz, 50 fT
|
||||
let alpha = 50.0 * (2.0 * PI * 10.0 * t + phase_offset).sin();
|
||||
|
||||
// Beta rhythm: 20 Hz, 30 fT
|
||||
let beta = 30.0 * (2.0 * PI * 20.0 * t + phase_offset * 1.3).sin();
|
||||
|
||||
// Gamma rhythm: 40 Hz, 15 fT
|
||||
let gamma = 15.0 * (2.0 * PI * 40.0 * t + phase_offset * 0.7).sin();
|
||||
|
||||
// White noise (~10 fT/sqrt(Hz) density).
|
||||
// Approximate Gaussian via Box-Muller with LCG.
|
||||
rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||
let u1 = (rng_state >> 11) as f64 / (1u64 << 53) as f64;
|
||||
rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||
let u2 = (rng_state >> 11) as f64 / (1u64 << 53) as f64;
|
||||
|
||||
let noise_amplitude = 10.0 * (sample_rate / 2.0).sqrt();
|
||||
let gaussian = if u1 > 1e-15 {
|
||||
(-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let noise = noise_amplitude * gaussian / (num_samples as f64).sqrt() * 0.1;
|
||||
|
||||
channel_data.push(alpha + beta + gamma + noise);
|
||||
}
|
||||
|
||||
data.push(channel_data);
|
||||
}
|
||||
|
||||
data
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn generate_correct_shape() {
|
||||
let data = generate_neural_data(8, 500, 1000.0);
|
||||
assert_eq!(data.len(), 8);
|
||||
for ch in &data {
|
||||
assert_eq!(ch.len(), 500);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simulate_produces_output() {
|
||||
let result = run(4, 1.0, 500.0, None);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simulate_writes_json() {
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join("ruv_neural_test_sim.json");
|
||||
let path_str = path.to_string_lossy().to_string();
|
||||
let result = run(2, 0.5, 250.0, Some(path_str.clone()));
|
||||
assert!(result.is_ok());
|
||||
assert!(path.exists());
|
||||
let contents = std::fs::read_to_string(&path).unwrap();
|
||||
let _ts: MultiChannelTimeSeries = serde_json::from_str(&contents).unwrap();
|
||||
std::fs::remove_file(&path).ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
//! Generate and verify Ed25519-signed capability witness bundles.
|
||||
|
||||
use ruv_neural_core::witness::{attest_capabilities, WitnessBundle};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Run the witness command.
|
||||
pub fn run(
|
||||
output: Option<PathBuf>,
|
||||
verify: Option<PathBuf>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if let Some(path) = verify {
|
||||
// Verify mode
|
||||
let json = std::fs::read_to_string(&path)?;
|
||||
let bundle: WitnessBundle = serde_json::from_str(&json)?;
|
||||
|
||||
println!("=== rUv Neural \u{2014} Witness Verification ===\n");
|
||||
println!(" Version: {}", bundle.version);
|
||||
println!(" Commit: {}", bundle.commit);
|
||||
println!(
|
||||
" Tests: {}/{} passed",
|
||||
bundle.tests_passed, bundle.total_tests
|
||||
);
|
||||
println!(" Caps: {} attestations", bundle.capabilities.len());
|
||||
println!(
|
||||
" Public Key: {}...{}",
|
||||
&bundle.public_key[..8],
|
||||
&bundle.public_key[bundle.public_key.len() - 8..]
|
||||
);
|
||||
println!();
|
||||
|
||||
// Verify digest
|
||||
let digest_ok = bundle.verify_digest();
|
||||
println!(
|
||||
" Digest integrity: {}",
|
||||
if digest_ok { "PASS" } else { "FAIL" }
|
||||
);
|
||||
|
||||
// Verify signature
|
||||
match bundle.verify() {
|
||||
Ok(true) => println!(" Ed25519 signature: PASS"),
|
||||
Ok(false) => println!(" Ed25519 signature: FAIL"),
|
||||
Err(e) => println!(" Ed25519 signature: ERROR ({e})"),
|
||||
}
|
||||
|
||||
let verdict = match bundle.verify_full() {
|
||||
Ok(true) => "PASS",
|
||||
_ => "FAIL",
|
||||
};
|
||||
println!("\n VERDICT: {verdict}");
|
||||
|
||||
if verdict == "FAIL" {
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else {
|
||||
// Generate mode
|
||||
let caps = attest_capabilities();
|
||||
let bundle = WitnessBundle::new(
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
"0.1.0",
|
||||
333,
|
||||
333,
|
||||
0,
|
||||
caps,
|
||||
);
|
||||
|
||||
let json = serde_json::to_string_pretty(&bundle)?;
|
||||
|
||||
if let Some(path) = output {
|
||||
std::fs::write(&path, &json)?;
|
||||
println!("Witness bundle written to {}", path.display());
|
||||
} else {
|
||||
println!("{json}");
|
||||
}
|
||||
|
||||
println!("\n Attestations: {}", bundle.capabilities.len());
|
||||
println!(" Digest: {}", bundle.capabilities_digest);
|
||||
println!(
|
||||
" Signature: {}...{}",
|
||||
&bundle.signature[..16],
|
||||
&bundle.signature[bundle.signature.len() - 16..]
|
||||
);
|
||||
println!(
|
||||
" Public Key: {}...{}",
|
||||
&bundle.public_key[..8],
|
||||
&bundle.public_key[bundle.public_key.len() - 8..]
|
||||
);
|
||||
println!("\n VERDICT: SIGNED");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
//! rUv Neural CLI — Brain topology analysis, simulation, and visualization.
|
||||
|
||||
mod commands;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "ruv-neural")]
|
||||
#[command(about = "rUv Neural — Brain Topology Analysis System")]
|
||||
#[command(version)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
|
||||
/// Verbosity level
|
||||
#[arg(short, long, action = clap::ArgAction::Count)]
|
||||
verbose: u8,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Simulate neural sensor data
|
||||
Simulate {
|
||||
/// Number of channels
|
||||
#[arg(short, long, default_value = "64")]
|
||||
channels: usize,
|
||||
/// Duration in seconds
|
||||
#[arg(short, long, default_value = "10.0")]
|
||||
duration: f64,
|
||||
/// Sample rate in Hz
|
||||
#[arg(short, long, default_value = "1000.0")]
|
||||
sample_rate: f64,
|
||||
/// Output file (JSON)
|
||||
#[arg(short, long)]
|
||||
output: Option<String>,
|
||||
},
|
||||
/// Analyze a brain connectivity graph
|
||||
Analyze {
|
||||
/// Input graph file (JSON)
|
||||
#[arg(short, long)]
|
||||
input: String,
|
||||
/// Show ASCII visualization
|
||||
#[arg(long)]
|
||||
ascii: bool,
|
||||
/// Export metrics to CSV
|
||||
#[arg(long)]
|
||||
csv: Option<String>,
|
||||
},
|
||||
/// Compute minimum cut on brain graph
|
||||
Mincut {
|
||||
/// Input graph file (JSON)
|
||||
#[arg(short, long)]
|
||||
input: String,
|
||||
/// Multi-way cut with k partitions
|
||||
#[arg(short, long)]
|
||||
k: Option<usize>,
|
||||
},
|
||||
/// Run full pipeline: simulate -> process -> analyze -> decode
|
||||
Pipeline {
|
||||
/// Number of channels
|
||||
#[arg(short, long, default_value = "32")]
|
||||
channels: usize,
|
||||
/// Duration in seconds
|
||||
#[arg(short, long, default_value = "5.0")]
|
||||
duration: f64,
|
||||
/// Show real-time ASCII dashboard
|
||||
#[arg(long)]
|
||||
dashboard: bool,
|
||||
},
|
||||
/// Export brain graph to visualization format
|
||||
Export {
|
||||
/// Input graph file (JSON)
|
||||
#[arg(short, long)]
|
||||
input: String,
|
||||
/// Output format: d3, dot, gexf, csv, rvf
|
||||
#[arg(short, long, default_value = "d3")]
|
||||
format: String,
|
||||
/// Output file
|
||||
#[arg(short, long)]
|
||||
output: String,
|
||||
},
|
||||
/// Show system info and capabilities
|
||||
Info,
|
||||
/// Generate or verify Ed25519-signed capability witness bundles
|
||||
Witness {
|
||||
/// Output file path for generated witness bundle (JSON)
|
||||
#[arg(short, long)]
|
||||
output: Option<String>,
|
||||
/// Path to a witness bundle to verify
|
||||
#[arg(long)]
|
||||
verify: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
fn init_tracing(verbose: u8) {
|
||||
let level = match verbose {
|
||||
0 => tracing::Level::WARN,
|
||||
1 => tracing::Level::INFO,
|
||||
2 => tracing::Level::DEBUG,
|
||||
_ => tracing::Level::TRACE,
|
||||
};
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(level)
|
||||
.with_target(false)
|
||||
.init();
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let cli = Cli::parse();
|
||||
init_tracing(cli.verbose);
|
||||
|
||||
let result = match cli.command {
|
||||
Commands::Simulate {
|
||||
channels,
|
||||
duration,
|
||||
sample_rate,
|
||||
output,
|
||||
} => commands::simulate::run(channels, duration, sample_rate, output),
|
||||
Commands::Analyze { input, ascii, csv } => commands::analyze::run(&input, ascii, csv),
|
||||
Commands::Mincut { input, k } => commands::mincut::run(&input, k),
|
||||
Commands::Pipeline {
|
||||
channels,
|
||||
duration,
|
||||
dashboard,
|
||||
} => commands::pipeline::run(channels, duration, dashboard),
|
||||
Commands::Export {
|
||||
input,
|
||||
format,
|
||||
output,
|
||||
} => commands::export::run(&input, &format, &output),
|
||||
Commands::Info => {
|
||||
commands::info::run();
|
||||
Ok(())
|
||||
}
|
||||
Commands::Witness { output, verify } => {
|
||||
commands::witness::run(
|
||||
output.map(std::path::PathBuf::from),
|
||||
verify.map(std::path::PathBuf::from),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
eprintln!("Error: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use clap::CommandFactory;
|
||||
|
||||
#[test]
|
||||
fn verify_cli() {
|
||||
Cli::command().debug_assert();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_simulate_defaults() {
|
||||
let cli = Cli::try_parse_from(["ruv-neural", "simulate"]).unwrap();
|
||||
match cli.command {
|
||||
Commands::Simulate {
|
||||
channels,
|
||||
duration,
|
||||
sample_rate,
|
||||
output,
|
||||
} => {
|
||||
assert_eq!(channels, 64);
|
||||
assert!((duration - 10.0).abs() < 1e-9);
|
||||
assert!((sample_rate - 1000.0).abs() < 1e-9);
|
||||
assert!(output.is_none());
|
||||
}
|
||||
_ => panic!("Expected Simulate command"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_simulate_with_args() {
|
||||
let cli = Cli::try_parse_from([
|
||||
"ruv-neural",
|
||||
"simulate",
|
||||
"-c",
|
||||
"32",
|
||||
"-d",
|
||||
"5.0",
|
||||
"-s",
|
||||
"500.0",
|
||||
"-o",
|
||||
"out.json",
|
||||
])
|
||||
.unwrap();
|
||||
match cli.command {
|
||||
Commands::Simulate {
|
||||
channels,
|
||||
duration,
|
||||
sample_rate,
|
||||
output,
|
||||
} => {
|
||||
assert_eq!(channels, 32);
|
||||
assert!((duration - 5.0).abs() < 1e-9);
|
||||
assert!((sample_rate - 500.0).abs() < 1e-9);
|
||||
assert_eq!(output.as_deref(), Some("out.json"));
|
||||
}
|
||||
_ => panic!("Expected Simulate command"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_analyze() {
|
||||
let cli =
|
||||
Cli::try_parse_from(["ruv-neural", "analyze", "-i", "graph.json", "--ascii"]).unwrap();
|
||||
match cli.command {
|
||||
Commands::Analyze { input, ascii, csv } => {
|
||||
assert_eq!(input, "graph.json");
|
||||
assert!(ascii);
|
||||
assert!(csv.is_none());
|
||||
}
|
||||
_ => panic!("Expected Analyze command"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mincut() {
|
||||
let cli = Cli::try_parse_from(["ruv-neural", "mincut", "-i", "graph.json", "-k", "4"])
|
||||
.unwrap();
|
||||
match cli.command {
|
||||
Commands::Mincut { input, k } => {
|
||||
assert_eq!(input, "graph.json");
|
||||
assert_eq!(k, Some(4));
|
||||
}
|
||||
_ => panic!("Expected Mincut command"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_pipeline() {
|
||||
let cli = Cli::try_parse_from([
|
||||
"ruv-neural",
|
||||
"pipeline",
|
||||
"-c",
|
||||
"16",
|
||||
"-d",
|
||||
"3.0",
|
||||
"--dashboard",
|
||||
])
|
||||
.unwrap();
|
||||
match cli.command {
|
||||
Commands::Pipeline {
|
||||
channels,
|
||||
duration,
|
||||
dashboard,
|
||||
} => {
|
||||
assert_eq!(channels, 16);
|
||||
assert!((duration - 3.0).abs() < 1e-9);
|
||||
assert!(dashboard);
|
||||
}
|
||||
_ => panic!("Expected Pipeline command"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_export() {
|
||||
let cli = Cli::try_parse_from([
|
||||
"ruv-neural",
|
||||
"export",
|
||||
"-i",
|
||||
"graph.json",
|
||||
"-f",
|
||||
"dot",
|
||||
"-o",
|
||||
"out.dot",
|
||||
])
|
||||
.unwrap();
|
||||
match cli.command {
|
||||
Commands::Export {
|
||||
input,
|
||||
format,
|
||||
output,
|
||||
} => {
|
||||
assert_eq!(input, "graph.json");
|
||||
assert_eq!(format, "dot");
|
||||
assert_eq!(output, "out.dot");
|
||||
}
|
||||
_ => panic!("Expected Export command"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_info() {
|
||||
let cli = Cli::try_parse_from(["ruv-neural", "info"]).unwrap();
|
||||
assert!(matches!(cli.command, Commands::Info));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_verbose() {
|
||||
let cli = Cli::try_parse_from(["ruv-neural", "-vvv", "info"]).unwrap();
|
||||
assert_eq!(cli.verbose, 3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "ruv-neural-core"
|
||||
description = "rUv Neural — Core types, traits, and error types for brain topology analysis"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
keywords = ["neural", "brain", "topology", "types", "core"]
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = []
|
||||
no_std = [] # For ESP32/embedded targets
|
||||
wasm = [] # For WASM targets
|
||||
rvf = [] # RuVector RVF format support
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
ed25519-dalek = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
@@ -0,0 +1,102 @@
|
||||
# ruv-neural-core
|
||||
|
||||
Core types, traits, and error types for the rUv Neural brain topology analysis system.
|
||||
|
||||
## Overview
|
||||
|
||||
`ruv-neural-core` is the foundation crate of the rUv Neural workspace. It defines all
|
||||
shared data types, trait interfaces, and the RVF binary file format used across the
|
||||
other eleven crates. This crate has **zero** internal dependencies -- every other
|
||||
ruv-neural crate depends on it.
|
||||
|
||||
## Features
|
||||
|
||||
- **Sensor types**: `SensorType`, `SensorChannel`, `SensorArray` with sensitivity specs
|
||||
for NV diamond, OPM, SQUID MEG, and EEG sensors
|
||||
- **Signal types**: `MultiChannelTimeSeries`, `FrequencyBand` (delta through gamma + custom),
|
||||
`SpectralFeatures`, `TimeFrequencyMap`
|
||||
- **Brain atlas**: `Atlas` (Desikan-Killiany 68, Destrieux 148, Schaefer 100/200/400, custom),
|
||||
`BrainRegion`, `Parcellation` with hemisphere and lobe queries
|
||||
- **Graph types**: `BrainGraph` with adjacency matrix, density, and degree methods;
|
||||
`BrainEdge`, `ConnectivityMetric`, `BrainGraphSequence`
|
||||
- **Topology types**: `MincutResult`, `MultiPartition`, `TopologyMetrics`, `CognitiveState`,
|
||||
`SleepStage`
|
||||
- **Embedding types**: `NeuralEmbedding` with cosine similarity and Euclidean distance,
|
||||
`EmbeddingTrajectory`, `EmbeddingMetadata`
|
||||
- **RVF format**: Binary RuVector File format with magic bytes, versioned headers,
|
||||
typed payloads, and read/write round-trip support
|
||||
- **Trait definitions**: `SensorSource`, `SignalProcessor`, `GraphConstructor`,
|
||||
`TopologyAnalyzer`, `EmbeddingGenerator`, `NeuralMemory`, `StateDecoder`,
|
||||
`RvfSerializable`
|
||||
- **Error handling**: `RuvNeuralError` enum with `DimensionMismatch`, `ChannelOutOfRange`,
|
||||
`InsufficientData`, and domain-specific variants
|
||||
- **Feature flags**: `std` (default), `no_std` (ESP32/embedded), `wasm`, `rvf`
|
||||
|
||||
## Usage
|
||||
|
||||
```rust
|
||||
use ruv_neural_core::{
|
||||
BrainGraph, BrainEdge, ConnectivityMetric, FrequencyBand, Atlas,
|
||||
NeuralEmbedding, EmbeddingMetadata, CognitiveState,
|
||||
MultiChannelTimeSeries, RvfFile, RvfDataType,
|
||||
};
|
||||
|
||||
// Create a brain graph
|
||||
let graph = BrainGraph {
|
||||
num_nodes: 3,
|
||||
edges: vec![BrainEdge {
|
||||
source: 0, target: 1, weight: 0.8,
|
||||
metric: ConnectivityMetric::PhaseLockingValue,
|
||||
frequency_band: FrequencyBand::Alpha,
|
||||
}],
|
||||
timestamp: 0.0,
|
||||
window_duration_s: 1.0,
|
||||
atlas: Atlas::DesikanKilliany68,
|
||||
};
|
||||
let matrix = graph.adjacency_matrix();
|
||||
let density = graph.density();
|
||||
|
||||
// Create a neural embedding
|
||||
let meta = EmbeddingMetadata {
|
||||
subject_id: Some("sub-01".into()),
|
||||
session_id: None,
|
||||
cognitive_state: Some(CognitiveState::Focused),
|
||||
source_atlas: Atlas::Schaefer100,
|
||||
embedding_method: "spectral".into(),
|
||||
};
|
||||
let emb = NeuralEmbedding::new(vec![3.0, 4.0], 1000.0, meta).unwrap();
|
||||
assert_eq!(emb.dimension, 2);
|
||||
assert!((emb.norm() - 5.0).abs() < 1e-10);
|
||||
|
||||
// Write/read RVF files
|
||||
let mut rvf = RvfFile::new(RvfDataType::BrainGraph);
|
||||
rvf.data = serde_json::to_vec(&graph).unwrap();
|
||||
let mut buf = Vec::new();
|
||||
rvf.write_to(&mut buf).unwrap();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Module | Key Types |
|
||||
|-------------|----------------------------------------------------------------|
|
||||
| `sensor` | `SensorType`, `SensorChannel`, `SensorArray` |
|
||||
| `signal` | `MultiChannelTimeSeries`, `FrequencyBand`, `SpectralFeatures` |
|
||||
| `brain` | `Atlas`, `BrainRegion`, `Parcellation`, `Hemisphere`, `Lobe` |
|
||||
| `graph` | `BrainGraph`, `BrainEdge`, `ConnectivityMetric` |
|
||||
| `topology` | `MincutResult`, `TopologyMetrics`, `CognitiveState` |
|
||||
| `embedding` | `NeuralEmbedding`, `EmbeddingTrajectory`, `EmbeddingMetadata` |
|
||||
| `rvf` | `RvfFile`, `RvfHeader`, `RvfDataType` |
|
||||
| `traits` | `SensorSource`, `SignalProcessor`, `EmbeddingGenerator`, etc. |
|
||||
| `error` | `RuvNeuralError`, `Result<T>` |
|
||||
|
||||
## Integration
|
||||
|
||||
This crate is a dependency of every other crate in the ruv-neural workspace.
|
||||
It provides the shared type vocabulary that allows crates to interoperate --
|
||||
for example, `ruv-neural-signal` produces `MultiChannelTimeSeries` values,
|
||||
`ruv-neural-graph` consumes them, and `ruv-neural-embed` outputs
|
||||
`NeuralEmbedding` values that `ruv-neural-memory` stores.
|
||||
|
||||
## License
|
||||
|
||||
MIT OR Apache-2.0
|
||||
@@ -0,0 +1,103 @@
|
||||
//! Brain region and atlas types for parcellation.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Brain atlas defining a parcellation scheme.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum Atlas {
|
||||
/// Desikan-Killiany atlas (68 cortical regions).
|
||||
DesikanKilliany68,
|
||||
/// Destrieux atlas (148 cortical regions).
|
||||
Destrieux148,
|
||||
/// Schaefer 100-parcel atlas.
|
||||
Schaefer100,
|
||||
/// Schaefer 200-parcel atlas.
|
||||
Schaefer200,
|
||||
/// Schaefer 400-parcel atlas.
|
||||
Schaefer400,
|
||||
/// Custom atlas with a specified number of regions.
|
||||
Custom(usize),
|
||||
}
|
||||
|
||||
impl Atlas {
|
||||
/// Number of regions in this atlas.
|
||||
pub fn num_regions(&self) -> usize {
|
||||
match self {
|
||||
Atlas::DesikanKilliany68 => 68,
|
||||
Atlas::Destrieux148 => 148,
|
||||
Atlas::Schaefer100 => 100,
|
||||
Atlas::Schaefer200 => 200,
|
||||
Atlas::Schaefer400 => 400,
|
||||
Atlas::Custom(n) => *n,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cerebral hemisphere.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum Hemisphere {
|
||||
Left,
|
||||
Right,
|
||||
Midline,
|
||||
}
|
||||
|
||||
/// Brain lobe classification.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum Lobe {
|
||||
Frontal,
|
||||
Parietal,
|
||||
Temporal,
|
||||
Occipital,
|
||||
Limbic,
|
||||
Subcortical,
|
||||
Cerebellar,
|
||||
}
|
||||
|
||||
/// A single brain region (parcel) within an atlas.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BrainRegion {
|
||||
/// Region index within the atlas.
|
||||
pub id: usize,
|
||||
/// Human-readable name (e.g., "superiorfrontal").
|
||||
pub name: String,
|
||||
/// Hemisphere.
|
||||
pub hemisphere: Hemisphere,
|
||||
/// Lobe classification.
|
||||
pub lobe: Lobe,
|
||||
/// Centroid in MNI coordinates (x, y, z in mm).
|
||||
pub centroid: [f64; 3],
|
||||
}
|
||||
|
||||
/// A full brain parcellation (atlas + all regions).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Parcellation {
|
||||
/// Atlas used.
|
||||
pub atlas: Atlas,
|
||||
/// All regions in the parcellation.
|
||||
pub regions: Vec<BrainRegion>,
|
||||
}
|
||||
|
||||
impl Parcellation {
|
||||
/// Number of regions.
|
||||
pub fn num_regions(&self) -> usize {
|
||||
self.regions.len()
|
||||
}
|
||||
|
||||
/// Get a region by its id.
|
||||
pub fn get_region(&self, id: usize) -> Option<&BrainRegion> {
|
||||
self.regions.iter().find(|r| r.id == id)
|
||||
}
|
||||
|
||||
/// Get all regions in a given hemisphere.
|
||||
pub fn regions_in_hemisphere(&self, hemisphere: Hemisphere) -> Vec<&BrainRegion> {
|
||||
self.regions
|
||||
.iter()
|
||||
.filter(|r| r.hemisphere == hemisphere)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get all regions in a given lobe.
|
||||
pub fn regions_in_lobe(&self, lobe: Lobe) -> Vec<&BrainRegion> {
|
||||
self.regions.iter().filter(|r| r.lobe == lobe).collect()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
//! Vector embedding types for neural state representations.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::brain::Atlas;
|
||||
use crate::error::{Result, RuvNeuralError};
|
||||
use crate::topology::CognitiveState;
|
||||
|
||||
/// Neural state embedding vector.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NeuralEmbedding {
|
||||
/// The embedding vector.
|
||||
pub vector: Vec<f64>,
|
||||
/// Dimensionality of the embedding.
|
||||
pub dimension: usize,
|
||||
/// Timestamp (Unix time).
|
||||
pub timestamp: f64,
|
||||
/// Associated metadata.
|
||||
pub metadata: EmbeddingMetadata,
|
||||
}
|
||||
|
||||
impl NeuralEmbedding {
|
||||
/// Create a new embedding, validating dimension consistency.
|
||||
pub fn new(vector: Vec<f64>, timestamp: f64, metadata: EmbeddingMetadata) -> Result<Self> {
|
||||
let dimension = vector.len();
|
||||
if dimension == 0 {
|
||||
return Err(RuvNeuralError::Embedding(
|
||||
"Embedding vector must not be empty".into(),
|
||||
));
|
||||
}
|
||||
Ok(Self {
|
||||
vector,
|
||||
dimension,
|
||||
timestamp,
|
||||
metadata,
|
||||
})
|
||||
}
|
||||
|
||||
/// L2 norm of the embedding vector.
|
||||
pub fn norm(&self) -> f64 {
|
||||
self.vector.iter().map(|x| x * x).sum::<f64>().sqrt()
|
||||
}
|
||||
|
||||
/// Cosine similarity to another embedding.
|
||||
pub fn cosine_similarity(&self, other: &NeuralEmbedding) -> Result<f64> {
|
||||
if self.dimension != other.dimension {
|
||||
return Err(RuvNeuralError::DimensionMismatch {
|
||||
expected: self.dimension,
|
||||
got: other.dimension,
|
||||
});
|
||||
}
|
||||
let dot: f64 = self
|
||||
.vector
|
||||
.iter()
|
||||
.zip(other.vector.iter())
|
||||
.map(|(a, b)| a * b)
|
||||
.sum();
|
||||
let norm_a = self.norm();
|
||||
let norm_b = other.norm();
|
||||
if norm_a == 0.0 || norm_b == 0.0 {
|
||||
return Ok(0.0);
|
||||
}
|
||||
Ok(dot / (norm_a * norm_b))
|
||||
}
|
||||
|
||||
/// Euclidean distance to another embedding.
|
||||
pub fn euclidean_distance(&self, other: &NeuralEmbedding) -> Result<f64> {
|
||||
if self.dimension != other.dimension {
|
||||
return Err(RuvNeuralError::DimensionMismatch {
|
||||
expected: self.dimension,
|
||||
got: other.dimension,
|
||||
});
|
||||
}
|
||||
let sum_sq: f64 = self
|
||||
.vector
|
||||
.iter()
|
||||
.zip(other.vector.iter())
|
||||
.map(|(a, b)| (a - b) * (a - b))
|
||||
.sum();
|
||||
Ok(sum_sq.sqrt())
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata associated with a neural embedding.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EmbeddingMetadata {
|
||||
/// Subject identifier.
|
||||
pub subject_id: Option<String>,
|
||||
/// Session identifier.
|
||||
pub session_id: Option<String>,
|
||||
/// Decoded cognitive state (if available).
|
||||
pub cognitive_state: Option<CognitiveState>,
|
||||
/// Atlas used for the source graph.
|
||||
pub source_atlas: Atlas,
|
||||
/// Name of the embedding method (e.g., "spectral", "node2vec").
|
||||
pub embedding_method: String,
|
||||
}
|
||||
|
||||
/// Temporal sequence of embeddings (trajectory through embedding space).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EmbeddingTrajectory {
|
||||
/// Ordered sequence of embeddings.
|
||||
pub embeddings: Vec<NeuralEmbedding>,
|
||||
/// Timestamps for each embedding.
|
||||
pub timestamps: Vec<f64>,
|
||||
}
|
||||
|
||||
impl EmbeddingTrajectory {
|
||||
/// Number of time points.
|
||||
pub fn len(&self) -> usize {
|
||||
self.embeddings.len()
|
||||
}
|
||||
|
||||
/// Returns true if the trajectory is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.embeddings.is_empty()
|
||||
}
|
||||
|
||||
/// Total duration in seconds.
|
||||
pub fn duration_s(&self) -> f64 {
|
||||
if self.timestamps.len() < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
self.timestamps.last().unwrap() - self.timestamps.first().unwrap()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
//! Error types for the ruv-neural pipeline.
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
/// Top-level error type for the ruv-neural system.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum RuvNeuralError {
|
||||
#[error("Sensor error: {0}")]
|
||||
Sensor(String),
|
||||
|
||||
#[error("Signal processing error: {0}")]
|
||||
Signal(String),
|
||||
|
||||
#[error("Graph construction error: {0}")]
|
||||
Graph(String),
|
||||
|
||||
#[error("Mincut computation error: {0}")]
|
||||
Mincut(String),
|
||||
|
||||
#[error("Embedding error: {0}")]
|
||||
Embedding(String),
|
||||
|
||||
#[error("Memory error: {0}")]
|
||||
Memory(String),
|
||||
|
||||
#[error("Decoder error: {0}")]
|
||||
Decoder(String),
|
||||
|
||||
#[error("Serialization error: {0}")]
|
||||
Serialization(String),
|
||||
|
||||
#[error("Invalid configuration: {0}")]
|
||||
Config(String),
|
||||
|
||||
#[error("Dimension mismatch: expected {expected}, got {got}")]
|
||||
DimensionMismatch { expected: usize, got: usize },
|
||||
|
||||
#[error("Channel {channel} out of range (max {max})")]
|
||||
ChannelOutOfRange { channel: usize, max: usize },
|
||||
|
||||
#[error("Insufficient data: need {needed} samples, have {have}")]
|
||||
InsufficientData { needed: usize, have: usize },
|
||||
}
|
||||
|
||||
/// Convenience result type for the ruv-neural system.
|
||||
pub type Result<T> = std::result::Result<T, RuvNeuralError>;
|
||||
@@ -0,0 +1,171 @@
|
||||
//! Brain connectivity graph types.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::brain::Atlas;
|
||||
use crate::error::{Result, RuvNeuralError};
|
||||
use crate::signal::FrequencyBand;
|
||||
|
||||
/// Connectivity metric used to compute edge weights.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum ConnectivityMetric {
|
||||
/// Phase locking value.
|
||||
PhaseLockingValue,
|
||||
/// Amplitude envelope correlation.
|
||||
AmplitudeEnvelopeCorrelation,
|
||||
/// Weighted phase lag index.
|
||||
WeightedPhaseLagIndex,
|
||||
/// Coherence.
|
||||
Coherence,
|
||||
/// Granger causality.
|
||||
GrangerCausality,
|
||||
/// Transfer entropy.
|
||||
TransferEntropy,
|
||||
/// Mutual information.
|
||||
MutualInformation,
|
||||
}
|
||||
|
||||
/// An edge in the brain connectivity graph.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BrainEdge {
|
||||
/// Source node index.
|
||||
pub source: usize,
|
||||
/// Target node index.
|
||||
pub target: usize,
|
||||
/// Edge weight (connectivity strength).
|
||||
pub weight: f64,
|
||||
/// Metric used to compute this edge.
|
||||
pub metric: ConnectivityMetric,
|
||||
/// Frequency band for this connectivity estimate.
|
||||
pub frequency_band: FrequencyBand,
|
||||
}
|
||||
|
||||
/// Brain connectivity graph at a single time window.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BrainGraph {
|
||||
/// Number of nodes (brain regions).
|
||||
pub num_nodes: usize,
|
||||
/// Edges with connectivity weights.
|
||||
pub edges: Vec<BrainEdge>,
|
||||
/// Timestamp of this graph window (Unix time).
|
||||
pub timestamp: f64,
|
||||
/// Duration of the analysis window in seconds.
|
||||
pub window_duration_s: f64,
|
||||
/// Atlas used for parcellation.
|
||||
pub atlas: Atlas,
|
||||
}
|
||||
|
||||
impl BrainGraph {
|
||||
/// Validate graph integrity: edge bounds, weight finiteness, no self-loops.
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
for (i, edge) in self.edges.iter().enumerate() {
|
||||
if edge.source >= self.num_nodes {
|
||||
return Err(RuvNeuralError::Graph(format!(
|
||||
"Edge {i}: source {} out of bounds (num_nodes={})",
|
||||
edge.source, self.num_nodes
|
||||
)));
|
||||
}
|
||||
if edge.target >= self.num_nodes {
|
||||
return Err(RuvNeuralError::Graph(format!(
|
||||
"Edge {i}: target {} out of bounds (num_nodes={})",
|
||||
edge.target, self.num_nodes
|
||||
)));
|
||||
}
|
||||
if edge.source == edge.target {
|
||||
return Err(RuvNeuralError::Graph(format!(
|
||||
"Edge {i}: self-loop on node {}",
|
||||
edge.source
|
||||
)));
|
||||
}
|
||||
if !edge.weight.is_finite() {
|
||||
return Err(RuvNeuralError::Graph(format!(
|
||||
"Edge {i}: non-finite weight {}",
|
||||
edge.weight
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Build a dense adjacency matrix (num_nodes x num_nodes).
|
||||
/// For duplicate edges, the last one wins.
|
||||
pub fn adjacency_matrix(&self) -> Vec<Vec<f64>> {
|
||||
let n = self.num_nodes;
|
||||
let mut mat = vec![vec![0.0; n]; n];
|
||||
for edge in &self.edges {
|
||||
if edge.source < n && edge.target < n {
|
||||
mat[edge.source][edge.target] = edge.weight;
|
||||
mat[edge.target][edge.source] = edge.weight;
|
||||
}
|
||||
}
|
||||
mat
|
||||
}
|
||||
|
||||
/// Get the weight of the edge between source and target, if it exists.
|
||||
pub fn edge_weight(&self, source: usize, target: usize) -> Option<f64> {
|
||||
self.edges
|
||||
.iter()
|
||||
.find(|e| {
|
||||
(e.source == source && e.target == target)
|
||||
|| (e.source == target && e.target == source)
|
||||
})
|
||||
.map(|e| e.weight)
|
||||
}
|
||||
|
||||
/// Weighted degree of a node (sum of incident edge weights).
|
||||
pub fn node_degree(&self, node: usize) -> f64 {
|
||||
self.edges
|
||||
.iter()
|
||||
.filter(|e| e.source == node || e.target == node)
|
||||
.map(|e| e.weight)
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Graph density: ratio of actual edges to possible edges.
|
||||
pub fn density(&self) -> f64 {
|
||||
if self.num_nodes < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
let max_edges = self.num_nodes * (self.num_nodes - 1) / 2;
|
||||
if max_edges == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
self.edges.len() as f64 / max_edges as f64
|
||||
}
|
||||
|
||||
/// Total weight of all edges.
|
||||
pub fn total_weight(&self) -> f64 {
|
||||
self.edges.iter().map(|e| e.weight).sum()
|
||||
}
|
||||
}
|
||||
|
||||
/// Temporal sequence of brain graphs.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BrainGraphSequence {
|
||||
/// Ordered sequence of graphs.
|
||||
pub graphs: Vec<BrainGraph>,
|
||||
/// Step between successive windows in seconds.
|
||||
pub window_step_s: f64,
|
||||
}
|
||||
|
||||
impl BrainGraphSequence {
|
||||
/// Number of time points.
|
||||
pub fn len(&self) -> usize {
|
||||
self.graphs.len()
|
||||
}
|
||||
|
||||
/// Returns true if the sequence is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.graphs.is_empty()
|
||||
}
|
||||
|
||||
/// Total duration covered by the sequence in seconds.
|
||||
pub fn duration_s(&self) -> f64 {
|
||||
if self.graphs.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let first = self.graphs.first().unwrap();
|
||||
let last = self.graphs.last().unwrap();
|
||||
(last.timestamp - first.timestamp) + last.window_duration_s
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,646 @@
|
||||
//! # ruv-neural-core
|
||||
//!
|
||||
//! Core types, traits, and error types for the ruv-neural brain topology
|
||||
//! analysis system.
|
||||
//!
|
||||
//! This crate is the foundation of the ruv-neural workspace. It has **zero**
|
||||
//! internal dependencies — all other ruv-neural crates depend on this one.
|
||||
//!
|
||||
//! ## Modules
|
||||
//!
|
||||
//! | Module | Contents |
|
||||
//! |-------------|---------------------------------------------------|
|
||||
//! | `error` | `RuvNeuralError` enum, `Result<T>` alias |
|
||||
//! | `sensor` | `SensorType`, `SensorChannel`, `SensorArray` |
|
||||
//! | `signal` | `MultiChannelTimeSeries`, `FrequencyBand`, spectra |
|
||||
//! | `brain` | `Atlas`, `BrainRegion`, `Parcellation` |
|
||||
//! | `graph` | `BrainGraph`, `BrainEdge`, `ConnectivityMetric` |
|
||||
//! | `topology` | `MincutResult`, `CognitiveState`, `TopologyMetrics`|
|
||||
//! | `embedding` | `NeuralEmbedding`, `EmbeddingTrajectory` |
|
||||
//! | `rvf` | RuVector File format header and I/O |
|
||||
//! | `traits` | Pipeline trait definitions for all crates |
|
||||
|
||||
pub mod brain;
|
||||
pub mod embedding;
|
||||
pub mod error;
|
||||
pub mod graph;
|
||||
pub mod rvf;
|
||||
pub mod sensor;
|
||||
pub mod signal;
|
||||
pub mod topology;
|
||||
pub mod traits;
|
||||
pub mod witness;
|
||||
|
||||
// Re-export the most commonly used types at crate root.
|
||||
pub use brain::{Atlas, BrainRegion, Hemisphere, Lobe, Parcellation};
|
||||
pub use embedding::{EmbeddingMetadata, EmbeddingTrajectory, NeuralEmbedding};
|
||||
pub use error::{Result, RuvNeuralError};
|
||||
pub use graph::{BrainEdge, BrainGraph, BrainGraphSequence, ConnectivityMetric};
|
||||
pub use rvf::{RvfDataType, RvfFile, RvfHeader};
|
||||
pub use sensor::{SensorArray, SensorChannel, SensorType};
|
||||
pub use signal::{FrequencyBand, MultiChannelTimeSeries, SpectralFeatures, TimeFrequencyMap};
|
||||
pub use topology::{
|
||||
CognitiveState, MincutResult, MultiPartition, SleepStage, TopologyMetrics,
|
||||
};
|
||||
pub use traits::{
|
||||
EmbeddingGenerator, GraphConstructor, NeuralMemory, RvfSerializable, SensorSource,
|
||||
SignalProcessor, StateDecoder, TopologyAnalyzer,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ── Error tests ─────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn error_display_formatting() {
|
||||
let err = RuvNeuralError::Sensor("calibration failed".into());
|
||||
assert!(err.to_string().contains("Sensor error"));
|
||||
assert!(err.to_string().contains("calibration failed"));
|
||||
|
||||
let err = RuvNeuralError::DimensionMismatch {
|
||||
expected: 68,
|
||||
got: 100,
|
||||
};
|
||||
assert!(err.to_string().contains("68"));
|
||||
assert!(err.to_string().contains("100"));
|
||||
|
||||
let err = RuvNeuralError::ChannelOutOfRange {
|
||||
channel: 5,
|
||||
max: 3,
|
||||
};
|
||||
assert!(err.to_string().contains("5"));
|
||||
assert!(err.to_string().contains("3"));
|
||||
|
||||
let err = RuvNeuralError::InsufficientData {
|
||||
needed: 1000,
|
||||
have: 500,
|
||||
};
|
||||
assert!(err.to_string().contains("1000"));
|
||||
assert!(err.to_string().contains("500"));
|
||||
}
|
||||
|
||||
// ── Sensor tests ────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn sensor_type_sensitivity() {
|
||||
assert!(SensorType::SquidMeg.typical_sensitivity_ft_sqrt_hz() < 5.0);
|
||||
assert!(SensorType::Eeg.typical_sensitivity_ft_sqrt_hz() > 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sensor_array_operations() {
|
||||
let array = SensorArray {
|
||||
channels: vec![
|
||||
SensorChannel {
|
||||
id: 0,
|
||||
sensor_type: SensorType::Opm,
|
||||
position: [0.0, 0.0, 0.1],
|
||||
orientation: [0.0, 0.0, 1.0],
|
||||
sensitivity_ft_sqrt_hz: 7.0,
|
||||
sample_rate_hz: 1000.0,
|
||||
label: "OPM-001".into(),
|
||||
},
|
||||
SensorChannel {
|
||||
id: 1,
|
||||
sensor_type: SensorType::Opm,
|
||||
position: [0.05, 0.0, 0.12],
|
||||
orientation: [0.0, 0.0, 1.0],
|
||||
sensitivity_ft_sqrt_hz: 7.0,
|
||||
sample_rate_hz: 1000.0,
|
||||
label: "OPM-002".into(),
|
||||
},
|
||||
],
|
||||
sensor_type: SensorType::Opm,
|
||||
name: "OPM array".into(),
|
||||
};
|
||||
|
||||
assert_eq!(array.num_channels(), 2);
|
||||
assert!(!array.is_empty());
|
||||
assert_eq!(array.get_channel(0).unwrap().label, "OPM-001");
|
||||
assert!(array.get_channel(5).is_none());
|
||||
|
||||
let (min, max) = array.bounding_box().unwrap();
|
||||
assert_eq!(min[0], 0.0);
|
||||
assert_eq!(max[0], 0.05);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sensor_serialize_roundtrip() {
|
||||
let ch = SensorChannel {
|
||||
id: 0,
|
||||
sensor_type: SensorType::NvDiamond,
|
||||
position: [1.0, 2.0, 3.0],
|
||||
orientation: [0.0, 0.0, 1.0],
|
||||
sensitivity_ft_sqrt_hz: 10.0,
|
||||
sample_rate_hz: 2000.0,
|
||||
label: "NV-001".into(),
|
||||
};
|
||||
let json = serde_json::to_string(&ch).unwrap();
|
||||
let ch2: SensorChannel = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(ch2.id, 0);
|
||||
assert_eq!(ch2.sensor_type, SensorType::NvDiamond);
|
||||
}
|
||||
|
||||
// ── Signal tests ────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn frequency_band_ranges() {
|
||||
assert_eq!(FrequencyBand::Delta.range_hz(), (1.0, 4.0));
|
||||
assert_eq!(FrequencyBand::Alpha.range_hz(), (8.0, 13.0));
|
||||
assert_eq!(FrequencyBand::Gamma.range_hz(), (30.0, 100.0));
|
||||
assert_eq!(
|
||||
FrequencyBand::Custom {
|
||||
low_hz: 50.0,
|
||||
high_hz: 70.0
|
||||
}
|
||||
.range_hz(),
|
||||
(50.0, 70.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frequency_band_center_and_bandwidth() {
|
||||
assert!((FrequencyBand::Alpha.center_hz() - 10.5).abs() < 1e-10);
|
||||
assert!((FrequencyBand::Alpha.bandwidth_hz() - 5.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_series_creation_valid() {
|
||||
let data = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
|
||||
let ts = MultiChannelTimeSeries::new(data, 100.0, 1000.0).unwrap();
|
||||
assert_eq!(ts.num_channels, 2);
|
||||
assert_eq!(ts.num_samples, 3);
|
||||
assert!((ts.duration_s() - 0.03).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_series_dimension_mismatch() {
|
||||
let data = vec![vec![1.0, 2.0], vec![3.0]];
|
||||
let result = MultiChannelTimeSeries::new(data, 100.0, 0.0);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_series_channel_access() {
|
||||
let data = vec![vec![10.0, 20.0], vec![30.0, 40.0]];
|
||||
let ts = MultiChannelTimeSeries::new(data, 100.0, 0.0).unwrap();
|
||||
assert_eq!(ts.channel(0).unwrap(), &[10.0, 20.0]);
|
||||
assert!(ts.channel(5).is_err());
|
||||
}
|
||||
|
||||
// ── Brain / Atlas tests ─────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn atlas_region_counts() {
|
||||
assert_eq!(Atlas::DesikanKilliany68.num_regions(), 68);
|
||||
assert_eq!(Atlas::Destrieux148.num_regions(), 148);
|
||||
assert_eq!(Atlas::Schaefer100.num_regions(), 100);
|
||||
assert_eq!(Atlas::Schaefer200.num_regions(), 200);
|
||||
assert_eq!(Atlas::Schaefer400.num_regions(), 400);
|
||||
assert_eq!(Atlas::Custom(42).num_regions(), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parcellation_query() {
|
||||
let parcellation = Parcellation {
|
||||
atlas: Atlas::Custom(3),
|
||||
regions: vec![
|
||||
BrainRegion {
|
||||
id: 0,
|
||||
name: "left_frontal".into(),
|
||||
hemisphere: Hemisphere::Left,
|
||||
lobe: Lobe::Frontal,
|
||||
centroid: [-30.0, 20.0, 40.0],
|
||||
},
|
||||
BrainRegion {
|
||||
id: 1,
|
||||
name: "right_frontal".into(),
|
||||
hemisphere: Hemisphere::Right,
|
||||
lobe: Lobe::Frontal,
|
||||
centroid: [30.0, 20.0, 40.0],
|
||||
},
|
||||
BrainRegion {
|
||||
id: 2,
|
||||
name: "left_temporal".into(),
|
||||
hemisphere: Hemisphere::Left,
|
||||
lobe: Lobe::Temporal,
|
||||
centroid: [-50.0, -10.0, 0.0],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(parcellation.num_regions(), 3);
|
||||
assert_eq!(
|
||||
parcellation.regions_in_hemisphere(Hemisphere::Left).len(),
|
||||
2
|
||||
);
|
||||
assert_eq!(parcellation.regions_in_lobe(Lobe::Frontal).len(), 2);
|
||||
assert_eq!(parcellation.regions_in_lobe(Lobe::Temporal).len(), 1);
|
||||
assert!(parcellation.get_region(1).is_some());
|
||||
assert!(parcellation.get_region(99).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn brain_region_serialize_roundtrip() {
|
||||
let region = BrainRegion {
|
||||
id: 42,
|
||||
name: "postcentral".into(),
|
||||
hemisphere: Hemisphere::Left,
|
||||
lobe: Lobe::Parietal,
|
||||
centroid: [-40.0, -25.0, 55.0],
|
||||
};
|
||||
let json = serde_json::to_string(®ion).unwrap();
|
||||
let r2: BrainRegion = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(r2.id, 42);
|
||||
assert_eq!(r2.hemisphere, Hemisphere::Left);
|
||||
}
|
||||
|
||||
// ── Graph tests ─────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn brain_graph_adjacency_matrix() {
|
||||
let graph = BrainGraph {
|
||||
num_nodes: 3,
|
||||
edges: vec![
|
||||
BrainEdge {
|
||||
source: 0,
|
||||
target: 1,
|
||||
weight: 0.8,
|
||||
metric: ConnectivityMetric::PhaseLockingValue,
|
||||
frequency_band: FrequencyBand::Alpha,
|
||||
},
|
||||
BrainEdge {
|
||||
source: 1,
|
||||
target: 2,
|
||||
weight: 0.5,
|
||||
metric: ConnectivityMetric::Coherence,
|
||||
frequency_band: FrequencyBand::Beta,
|
||||
},
|
||||
],
|
||||
timestamp: 100.0,
|
||||
window_duration_s: 1.0,
|
||||
atlas: Atlas::Custom(3),
|
||||
};
|
||||
|
||||
let mat = graph.adjacency_matrix();
|
||||
assert_eq!(mat.len(), 3);
|
||||
assert!((mat[0][1] - 0.8).abs() < 1e-10);
|
||||
assert!((mat[1][0] - 0.8).abs() < 1e-10);
|
||||
assert!((mat[1][2] - 0.5).abs() < 1e-10);
|
||||
assert!((mat[0][2] - 0.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn brain_graph_edge_weight_lookup() {
|
||||
let graph = BrainGraph {
|
||||
num_nodes: 2,
|
||||
edges: vec![BrainEdge {
|
||||
source: 0,
|
||||
target: 1,
|
||||
weight: 0.9,
|
||||
metric: ConnectivityMetric::MutualInformation,
|
||||
frequency_band: FrequencyBand::Gamma,
|
||||
}],
|
||||
timestamp: 0.0,
|
||||
window_duration_s: 0.5,
|
||||
atlas: Atlas::Custom(2),
|
||||
};
|
||||
|
||||
assert!((graph.edge_weight(0, 1).unwrap() - 0.9).abs() < 1e-10);
|
||||
assert!((graph.edge_weight(1, 0).unwrap() - 0.9).abs() < 1e-10);
|
||||
assert!(graph.edge_weight(0, 0).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn brain_graph_node_degree() {
|
||||
let graph = BrainGraph {
|
||||
num_nodes: 3,
|
||||
edges: vec![
|
||||
BrainEdge {
|
||||
source: 0,
|
||||
target: 1,
|
||||
weight: 0.3,
|
||||
metric: ConnectivityMetric::Coherence,
|
||||
frequency_band: FrequencyBand::Alpha,
|
||||
},
|
||||
BrainEdge {
|
||||
source: 0,
|
||||
target: 2,
|
||||
weight: 0.7,
|
||||
metric: ConnectivityMetric::Coherence,
|
||||
frequency_band: FrequencyBand::Alpha,
|
||||
},
|
||||
],
|
||||
timestamp: 0.0,
|
||||
window_duration_s: 1.0,
|
||||
atlas: Atlas::Custom(3),
|
||||
};
|
||||
|
||||
assert!((graph.node_degree(0) - 1.0).abs() < 1e-10);
|
||||
assert!((graph.node_degree(1) - 0.3).abs() < 1e-10);
|
||||
assert!((graph.node_degree(2) - 0.7).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn brain_graph_density() {
|
||||
let graph = BrainGraph {
|
||||
num_nodes: 4,
|
||||
edges: vec![
|
||||
BrainEdge {
|
||||
source: 0,
|
||||
target: 1,
|
||||
weight: 1.0,
|
||||
metric: ConnectivityMetric::PhaseLockingValue,
|
||||
frequency_band: FrequencyBand::Alpha,
|
||||
},
|
||||
BrainEdge {
|
||||
source: 2,
|
||||
target: 3,
|
||||
weight: 1.0,
|
||||
metric: ConnectivityMetric::PhaseLockingValue,
|
||||
frequency_band: FrequencyBand::Alpha,
|
||||
},
|
||||
BrainEdge {
|
||||
source: 0,
|
||||
target: 3,
|
||||
weight: 1.0,
|
||||
metric: ConnectivityMetric::PhaseLockingValue,
|
||||
frequency_band: FrequencyBand::Alpha,
|
||||
},
|
||||
],
|
||||
timestamp: 0.0,
|
||||
window_duration_s: 1.0,
|
||||
atlas: Atlas::Custom(4),
|
||||
};
|
||||
|
||||
assert!((graph.density() - 0.5).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graph_sequence_duration() {
|
||||
let seq = BrainGraphSequence {
|
||||
graphs: vec![
|
||||
BrainGraph {
|
||||
num_nodes: 2,
|
||||
edges: vec![],
|
||||
timestamp: 0.0,
|
||||
window_duration_s: 1.0,
|
||||
atlas: Atlas::Custom(2),
|
||||
},
|
||||
BrainGraph {
|
||||
num_nodes: 2,
|
||||
edges: vec![],
|
||||
timestamp: 0.5,
|
||||
window_duration_s: 1.0,
|
||||
atlas: Atlas::Custom(2),
|
||||
},
|
||||
BrainGraph {
|
||||
num_nodes: 2,
|
||||
edges: vec![],
|
||||
timestamp: 1.0,
|
||||
window_duration_s: 1.0,
|
||||
atlas: Atlas::Custom(2),
|
||||
},
|
||||
],
|
||||
window_step_s: 0.5,
|
||||
};
|
||||
|
||||
assert_eq!(seq.len(), 3);
|
||||
assert!(!seq.is_empty());
|
||||
assert!((seq.duration_s() - 2.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
// ── Topology tests ──────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn mincut_result_properties() {
|
||||
let result = MincutResult {
|
||||
cut_value: 1.5,
|
||||
partition_a: vec![0, 1],
|
||||
partition_b: vec![2, 3, 4],
|
||||
cut_edges: vec![(1, 2, 0.8), (0, 3, 0.7)],
|
||||
timestamp: 100.0,
|
||||
};
|
||||
|
||||
assert_eq!(result.num_nodes(), 5);
|
||||
assert_eq!(result.num_cut_edges(), 2);
|
||||
assert!((result.balance_ratio() - 2.0 / 3.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_partition_properties() {
|
||||
let mp = MultiPartition {
|
||||
partitions: vec![vec![0, 1], vec![2, 3], vec![4]],
|
||||
cut_value: 2.0,
|
||||
modularity: 0.4,
|
||||
};
|
||||
assert_eq!(mp.num_partitions(), 3);
|
||||
assert_eq!(mp.num_nodes(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cognitive_state_serialize_roundtrip() {
|
||||
let states = vec![
|
||||
CognitiveState::Rest,
|
||||
CognitiveState::Focused,
|
||||
CognitiveState::Sleep(SleepStage::Rem),
|
||||
CognitiveState::Unknown,
|
||||
];
|
||||
let json = serde_json::to_string(&states).unwrap();
|
||||
let deserialized: Vec<CognitiveState> = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(states, deserialized);
|
||||
}
|
||||
|
||||
// ── Embedding tests ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn embedding_creation_and_norm() {
|
||||
let meta = EmbeddingMetadata {
|
||||
subject_id: Some("sub-01".into()),
|
||||
session_id: Some("ses-01".into()),
|
||||
cognitive_state: Some(CognitiveState::Focused),
|
||||
source_atlas: Atlas::Schaefer100,
|
||||
embedding_method: "spectral".into(),
|
||||
};
|
||||
let emb = NeuralEmbedding::new(vec![3.0, 4.0], 1000.0, meta).unwrap();
|
||||
assert_eq!(emb.dimension, 2);
|
||||
assert!((emb.norm() - 5.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedding_cosine_similarity() {
|
||||
let meta = || EmbeddingMetadata {
|
||||
subject_id: None,
|
||||
session_id: None,
|
||||
cognitive_state: None,
|
||||
source_atlas: Atlas::Custom(2),
|
||||
embedding_method: "test".into(),
|
||||
};
|
||||
|
||||
let a = NeuralEmbedding::new(vec![1.0, 0.0], 0.0, meta()).unwrap();
|
||||
let b = NeuralEmbedding::new(vec![1.0, 0.0], 0.0, meta()).unwrap();
|
||||
let c = NeuralEmbedding::new(vec![0.0, 1.0], 0.0, meta()).unwrap();
|
||||
|
||||
assert!((a.cosine_similarity(&b).unwrap() - 1.0).abs() < 1e-10);
|
||||
assert!((a.cosine_similarity(&c).unwrap() - 0.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedding_euclidean_distance() {
|
||||
let meta = || EmbeddingMetadata {
|
||||
subject_id: None,
|
||||
session_id: None,
|
||||
cognitive_state: None,
|
||||
source_atlas: Atlas::Custom(2),
|
||||
embedding_method: "test".into(),
|
||||
};
|
||||
|
||||
let a = NeuralEmbedding::new(vec![0.0, 0.0], 0.0, meta()).unwrap();
|
||||
let b = NeuralEmbedding::new(vec![3.0, 4.0], 0.0, meta()).unwrap();
|
||||
assert!((a.euclidean_distance(&b).unwrap() - 5.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedding_dimension_mismatch() {
|
||||
let meta = || EmbeddingMetadata {
|
||||
subject_id: None,
|
||||
session_id: None,
|
||||
cognitive_state: None,
|
||||
source_atlas: Atlas::Custom(2),
|
||||
embedding_method: "test".into(),
|
||||
};
|
||||
|
||||
let a = NeuralEmbedding::new(vec![1.0, 2.0], 0.0, meta()).unwrap();
|
||||
let b = NeuralEmbedding::new(vec![1.0, 2.0, 3.0], 0.0, meta()).unwrap();
|
||||
assert!(a.cosine_similarity(&b).is_err());
|
||||
assert!(a.euclidean_distance(&b).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedding_trajectory() {
|
||||
let meta = || EmbeddingMetadata {
|
||||
subject_id: None,
|
||||
session_id: None,
|
||||
cognitive_state: None,
|
||||
source_atlas: Atlas::Custom(2),
|
||||
embedding_method: "test".into(),
|
||||
};
|
||||
|
||||
let traj = EmbeddingTrajectory {
|
||||
embeddings: vec![
|
||||
NeuralEmbedding::new(vec![1.0], 0.0, meta()).unwrap(),
|
||||
NeuralEmbedding::new(vec![2.0], 1.0, meta()).unwrap(),
|
||||
NeuralEmbedding::new(vec![3.0], 2.0, meta()).unwrap(),
|
||||
],
|
||||
timestamps: vec![0.0, 1.0, 2.0],
|
||||
};
|
||||
|
||||
assert_eq!(traj.len(), 3);
|
||||
assert!(!traj.is_empty());
|
||||
assert!((traj.duration_s() - 2.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
// ── RVF tests ───────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn rvf_data_type_tag_roundtrip() {
|
||||
for dt in [
|
||||
RvfDataType::BrainGraph,
|
||||
RvfDataType::NeuralEmbedding,
|
||||
RvfDataType::TopologyMetrics,
|
||||
RvfDataType::MincutResult,
|
||||
RvfDataType::TimeSeriesChunk,
|
||||
] {
|
||||
let tag = dt.to_tag();
|
||||
let recovered = RvfDataType::from_tag(tag).unwrap();
|
||||
assert_eq!(dt, recovered);
|
||||
}
|
||||
assert!(RvfDataType::from_tag(255).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rvf_header_encode_decode() {
|
||||
let header = RvfHeader::new(RvfDataType::NeuralEmbedding, 42, 128);
|
||||
let bytes = header.to_bytes();
|
||||
assert_eq!(bytes.len(), 22);
|
||||
|
||||
let decoded = RvfHeader::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(decoded.magic, rvf::RVF_MAGIC);
|
||||
assert_eq!(decoded.version, rvf::RVF_VERSION);
|
||||
assert_eq!(decoded.data_type, RvfDataType::NeuralEmbedding);
|
||||
assert_eq!(decoded.num_entries, 42);
|
||||
assert_eq!(decoded.embedding_dim, 128);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rvf_header_validation() {
|
||||
let mut header = RvfHeader::new(RvfDataType::BrainGraph, 1, 0);
|
||||
assert!(header.validate().is_ok());
|
||||
|
||||
header.magic = [0, 0, 0, 0];
|
||||
assert!(header.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rvf_file_write_read_roundtrip() {
|
||||
let mut file = RvfFile::new(RvfDataType::TopologyMetrics);
|
||||
file.header.num_entries = 1;
|
||||
file.metadata = serde_json::json!({ "subject": "sub-01" });
|
||||
file.data = vec![1, 2, 3, 4, 5];
|
||||
|
||||
let mut buf = Vec::new();
|
||||
file.write_to(&mut buf).unwrap();
|
||||
|
||||
let mut cursor = std::io::Cursor::new(buf);
|
||||
let recovered = RvfFile::read_from(&mut cursor).unwrap();
|
||||
|
||||
assert_eq!(recovered.header.data_type, RvfDataType::TopologyMetrics);
|
||||
assert_eq!(recovered.header.num_entries, 1);
|
||||
assert_eq!(recovered.metadata["subject"], "sub-01");
|
||||
assert_eq!(recovered.data, vec![1, 2, 3, 4, 5]);
|
||||
}
|
||||
|
||||
// ── Serialization roundtrip tests ───────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn graph_serialize_roundtrip() {
|
||||
let graph = BrainGraph {
|
||||
num_nodes: 2,
|
||||
edges: vec![BrainEdge {
|
||||
source: 0,
|
||||
target: 1,
|
||||
weight: 0.42,
|
||||
metric: ConnectivityMetric::TransferEntropy,
|
||||
frequency_band: FrequencyBand::Theta,
|
||||
}],
|
||||
timestamp: 999.0,
|
||||
window_duration_s: 2.0,
|
||||
atlas: Atlas::Schaefer200,
|
||||
};
|
||||
let json = serde_json::to_string(&graph).unwrap();
|
||||
let g2: BrainGraph = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(g2.num_nodes, 2);
|
||||
assert_eq!(g2.edges.len(), 1);
|
||||
assert!((g2.edges[0].weight - 0.42).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn topology_metrics_serialize_roundtrip() {
|
||||
let metrics = TopologyMetrics {
|
||||
global_mincut: 3.14,
|
||||
modularity: 0.55,
|
||||
global_efficiency: 0.72,
|
||||
local_efficiency: 0.68,
|
||||
graph_entropy: 2.3,
|
||||
fiedler_value: 0.12,
|
||||
num_modules: 4,
|
||||
timestamp: 500.0,
|
||||
};
|
||||
let json = serde_json::to_string(&metrics).unwrap();
|
||||
let m2: TopologyMetrics = serde_json::from_str(&json).unwrap();
|
||||
assert!((m2.global_mincut - 3.14).abs() < 1e-10);
|
||||
assert_eq!(m2.num_modules, 4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
//! RuVector File (RVF) format types for serialization.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::{Result, RuvNeuralError};
|
||||
|
||||
/// Magic bytes for the RVF file format.
|
||||
pub const RVF_MAGIC: [u8; 4] = [b'R', b'V', b'F', 0x01];
|
||||
|
||||
/// Current RVF format version.
|
||||
pub const RVF_VERSION: u8 = 1;
|
||||
|
||||
/// Maximum allowed metadata JSON length (16 MiB).
|
||||
pub const MAX_METADATA_LEN: u32 = 16 * 1024 * 1024;
|
||||
|
||||
/// Maximum allowed payload length when reading (256 MiB).
|
||||
pub const MAX_PAYLOAD_LEN: usize = 256 * 1024 * 1024;
|
||||
|
||||
/// Data type stored in an RVF file.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum RvfDataType {
|
||||
/// Brain connectivity graph.
|
||||
BrainGraph,
|
||||
/// Neural embedding vector.
|
||||
NeuralEmbedding,
|
||||
/// Topology metrics snapshot.
|
||||
TopologyMetrics,
|
||||
/// Mincut result.
|
||||
MincutResult,
|
||||
/// Time series chunk.
|
||||
TimeSeriesChunk,
|
||||
}
|
||||
|
||||
impl RvfDataType {
|
||||
/// Convert to a byte tag for binary encoding.
|
||||
pub fn to_tag(&self) -> u8 {
|
||||
match self {
|
||||
RvfDataType::BrainGraph => 0,
|
||||
RvfDataType::NeuralEmbedding => 1,
|
||||
RvfDataType::TopologyMetrics => 2,
|
||||
RvfDataType::MincutResult => 3,
|
||||
RvfDataType::TimeSeriesChunk => 4,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a byte tag back to a data type.
|
||||
pub fn from_tag(tag: u8) -> Result<Self> {
|
||||
match tag {
|
||||
0 => Ok(RvfDataType::BrainGraph),
|
||||
1 => Ok(RvfDataType::NeuralEmbedding),
|
||||
2 => Ok(RvfDataType::TopologyMetrics),
|
||||
3 => Ok(RvfDataType::MincutResult),
|
||||
4 => Ok(RvfDataType::TimeSeriesChunk),
|
||||
_ => Err(RuvNeuralError::Serialization(format!(
|
||||
"Unknown RVF data type tag: {}",
|
||||
tag
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// RVF file header (fixed-size, 20 bytes).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RvfHeader {
|
||||
/// Magic bytes: `b"RVF\x01"`.
|
||||
pub magic: [u8; 4],
|
||||
/// Format version.
|
||||
pub version: u8,
|
||||
/// Type of data stored.
|
||||
pub data_type: RvfDataType,
|
||||
/// Number of entries in the file.
|
||||
pub num_entries: u64,
|
||||
/// Embedding dimensionality (0 if not applicable).
|
||||
pub embedding_dim: u32,
|
||||
/// Length of the JSON metadata section in bytes.
|
||||
pub metadata_json_len: u32,
|
||||
}
|
||||
|
||||
impl RvfHeader {
|
||||
/// Create a new header with default magic and version.
|
||||
pub fn new(data_type: RvfDataType, num_entries: u64, embedding_dim: u32) -> Self {
|
||||
Self {
|
||||
magic: RVF_MAGIC,
|
||||
version: RVF_VERSION,
|
||||
data_type,
|
||||
num_entries,
|
||||
embedding_dim,
|
||||
metadata_json_len: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate that this header has correct magic bytes and a known version.
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
if self.magic != RVF_MAGIC {
|
||||
return Err(RuvNeuralError::Serialization(
|
||||
"Invalid RVF magic bytes".into(),
|
||||
));
|
||||
}
|
||||
if self.version != RVF_VERSION {
|
||||
return Err(RuvNeuralError::Serialization(format!(
|
||||
"Unsupported RVF version: {} (expected {})",
|
||||
self.version, RVF_VERSION
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Encode the header to bytes (little-endian).
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(20);
|
||||
buf.extend_from_slice(&self.magic);
|
||||
buf.push(self.version);
|
||||
buf.push(self.data_type.to_tag());
|
||||
buf.extend_from_slice(&self.num_entries.to_le_bytes());
|
||||
buf.extend_from_slice(&self.embedding_dim.to_le_bytes());
|
||||
buf.extend_from_slice(&self.metadata_json_len.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
/// Decode a header from bytes.
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
if bytes.len() < 22 {
|
||||
return Err(RuvNeuralError::Serialization(format!(
|
||||
"RVF header too short: {} bytes (need 22)",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
let mut magic = [0u8; 4];
|
||||
magic.copy_from_slice(&bytes[0..4]);
|
||||
let version = bytes[4];
|
||||
let data_type = RvfDataType::from_tag(bytes[5])?;
|
||||
let num_entries = u64::from_le_bytes(bytes[6..14].try_into().unwrap());
|
||||
let embedding_dim = u32::from_le_bytes(bytes[14..18].try_into().unwrap());
|
||||
let metadata_json_len = u32::from_le_bytes(bytes[18..22].try_into().unwrap());
|
||||
|
||||
Ok(Self {
|
||||
magic,
|
||||
version,
|
||||
data_type,
|
||||
num_entries,
|
||||
embedding_dim,
|
||||
metadata_json_len,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// An RVF file containing header, metadata, and binary data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RvfFile {
|
||||
/// File header.
|
||||
pub header: RvfHeader,
|
||||
/// JSON metadata.
|
||||
pub metadata: serde_json::Value,
|
||||
/// Raw binary payload.
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl RvfFile {
|
||||
/// Create a new empty RVF file for a given data type.
|
||||
pub fn new(data_type: RvfDataType) -> Self {
|
||||
Self {
|
||||
header: RvfHeader::new(data_type, 0, 0),
|
||||
metadata: serde_json::Value::Object(serde_json::Map::new()),
|
||||
data: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write the RVF file to a writer.
|
||||
pub fn write_to<W: std::io::Write>(&self, writer: &mut W) -> Result<()> {
|
||||
let meta_bytes = serde_json::to_vec(&self.metadata)
|
||||
.map_err(|e| RuvNeuralError::Serialization(e.to_string()))?;
|
||||
|
||||
let mut header = self.header.clone();
|
||||
header.metadata_json_len = meta_bytes.len() as u32;
|
||||
|
||||
writer
|
||||
.write_all(&header.to_bytes())
|
||||
.map_err(|e| RuvNeuralError::Serialization(e.to_string()))?;
|
||||
writer
|
||||
.write_all(&meta_bytes)
|
||||
.map_err(|e| RuvNeuralError::Serialization(e.to_string()))?;
|
||||
writer
|
||||
.write_all(&self.data)
|
||||
.map_err(|e| RuvNeuralError::Serialization(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read an RVF file from a reader.
|
||||
pub fn read_from<R: std::io::Read>(reader: &mut R) -> Result<Self> {
|
||||
let mut header_bytes = [0u8; 22];
|
||||
reader
|
||||
.read_exact(&mut header_bytes)
|
||||
.map_err(|e| RuvNeuralError::Serialization(e.to_string()))?;
|
||||
|
||||
let header = RvfHeader::from_bytes(&header_bytes)?;
|
||||
header.validate()?;
|
||||
|
||||
if header.metadata_json_len > MAX_METADATA_LEN {
|
||||
return Err(RuvNeuralError::Serialization(format!(
|
||||
"RVF metadata length {} exceeds maximum {}",
|
||||
header.metadata_json_len, MAX_METADATA_LEN
|
||||
)));
|
||||
}
|
||||
|
||||
let mut meta_bytes = vec![0u8; header.metadata_json_len as usize];
|
||||
reader
|
||||
.read_exact(&mut meta_bytes)
|
||||
.map_err(|e| RuvNeuralError::Serialization(e.to_string()))?;
|
||||
|
||||
let metadata: serde_json::Value = serde_json::from_slice(&meta_bytes)
|
||||
.map_err(|e| RuvNeuralError::Serialization(e.to_string()))?;
|
||||
|
||||
let mut data = Vec::new();
|
||||
reader
|
||||
.read_to_end(&mut data)
|
||||
.map_err(|e| RuvNeuralError::Serialization(e.to_string()))?;
|
||||
|
||||
if data.len() > MAX_PAYLOAD_LEN {
|
||||
return Err(RuvNeuralError::Serialization(format!(
|
||||
"RVF payload length {} exceeds maximum {}",
|
||||
data.len(), MAX_PAYLOAD_LEN
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
header,
|
||||
metadata,
|
||||
data,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
//! Sensor types for brain signal acquisition.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Sensor technology type.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum SensorType {
|
||||
/// Nitrogen-vacancy diamond magnetometer.
|
||||
NvDiamond,
|
||||
/// Optically pumped magnetometer.
|
||||
Opm,
|
||||
/// Electroencephalography.
|
||||
Eeg,
|
||||
/// Superconducting quantum interference device MEG.
|
||||
SquidMeg,
|
||||
/// Atom interferometer for gravitational neural sensing.
|
||||
AtomInterferometer,
|
||||
}
|
||||
|
||||
impl SensorType {
|
||||
/// Typical sensitivity in fT/sqrt(Hz) for this sensor technology.
|
||||
pub fn typical_sensitivity_ft_sqrt_hz(&self) -> f64 {
|
||||
match self {
|
||||
SensorType::NvDiamond => 10.0,
|
||||
SensorType::Opm => 7.0,
|
||||
SensorType::Eeg => 1000.0,
|
||||
SensorType::SquidMeg => 3.0,
|
||||
SensorType::AtomInterferometer => 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sensor channel metadata.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SensorChannel {
|
||||
/// Channel index.
|
||||
pub id: usize,
|
||||
/// Type of sensor.
|
||||
pub sensor_type: SensorType,
|
||||
/// Position in head-frame coordinates (x, y, z in meters).
|
||||
pub position: [f64; 3],
|
||||
/// Orientation unit normal vector.
|
||||
pub orientation: [f64; 3],
|
||||
/// Sensitivity in fT/sqrt(Hz).
|
||||
pub sensitivity_ft_sqrt_hz: f64,
|
||||
/// Sampling rate in Hz.
|
||||
pub sample_rate_hz: f64,
|
||||
/// Human-readable label (e.g., "Fz", "OPM-L01").
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
/// Sensor array configuration (a collection of channels of one type).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SensorArray {
|
||||
/// All channels in the array.
|
||||
pub channels: Vec<SensorChannel>,
|
||||
/// Sensor technology used by this array.
|
||||
pub sensor_type: SensorType,
|
||||
/// Human-readable name for the array.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl SensorArray {
|
||||
/// Number of channels in the array.
|
||||
pub fn num_channels(&self) -> usize {
|
||||
self.channels.len()
|
||||
}
|
||||
|
||||
/// Returns true if the array has no channels.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.channels.is_empty()
|
||||
}
|
||||
|
||||
/// Get a channel by its index within this array.
|
||||
pub fn get_channel(&self, index: usize) -> Option<&SensorChannel> {
|
||||
self.channels.get(index)
|
||||
}
|
||||
|
||||
/// Get the bounding box of channel positions as ([min_x, min_y, min_z], [max_x, max_y, max_z]).
|
||||
pub fn bounding_box(&self) -> Option<([f64; 3], [f64; 3])> {
|
||||
if self.channels.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut min = [f64::INFINITY; 3];
|
||||
let mut max = [f64::NEG_INFINITY; 3];
|
||||
for ch in &self.channels {
|
||||
for i in 0..3 {
|
||||
if ch.position[i] < min[i] {
|
||||
min[i] = ch.position[i];
|
||||
}
|
||||
if ch.position[i] > max[i] {
|
||||
max[i] = ch.position[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
Some((min, max))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
//! Time series and signal types for neural data.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::{Result, RuvNeuralError};
|
||||
|
||||
/// Multi-channel time series data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MultiChannelTimeSeries {
|
||||
/// Raw data: `data[channel][sample]`.
|
||||
pub data: Vec<Vec<f64>>,
|
||||
/// Sampling rate in Hz.
|
||||
pub sample_rate_hz: f64,
|
||||
/// Number of channels.
|
||||
pub num_channels: usize,
|
||||
/// Number of samples per channel.
|
||||
pub num_samples: usize,
|
||||
/// Unix timestamp of the first sample.
|
||||
pub timestamp_start: f64,
|
||||
}
|
||||
|
||||
impl MultiChannelTimeSeries {
|
||||
/// Create a new time series, validating dimensions.
|
||||
pub fn new(data: Vec<Vec<f64>>, sample_rate_hz: f64, timestamp_start: f64) -> Result<Self> {
|
||||
if !sample_rate_hz.is_finite() || sample_rate_hz <= 0.0 {
|
||||
return Err(RuvNeuralError::Signal(
|
||||
"sample_rate_hz must be finite and positive".into(),
|
||||
));
|
||||
}
|
||||
let num_channels = data.len();
|
||||
if num_channels == 0 {
|
||||
return Err(RuvNeuralError::Signal(
|
||||
"Time series must have at least one channel".into(),
|
||||
));
|
||||
}
|
||||
let num_samples = data[0].len();
|
||||
for (i, ch) in data.iter().enumerate() {
|
||||
if ch.len() != num_samples {
|
||||
return Err(RuvNeuralError::DimensionMismatch {
|
||||
expected: num_samples,
|
||||
got: ch.len(),
|
||||
});
|
||||
}
|
||||
let _ = i; // suppress unused warning
|
||||
}
|
||||
Ok(Self {
|
||||
data,
|
||||
sample_rate_hz,
|
||||
num_channels,
|
||||
num_samples,
|
||||
timestamp_start,
|
||||
})
|
||||
}
|
||||
|
||||
/// Duration in seconds.
|
||||
pub fn duration_s(&self) -> f64 {
|
||||
self.num_samples as f64 / self.sample_rate_hz
|
||||
}
|
||||
|
||||
/// Get a single channel's data.
|
||||
pub fn channel(&self, index: usize) -> Result<&[f64]> {
|
||||
if index >= self.num_channels {
|
||||
return Err(RuvNeuralError::ChannelOutOfRange {
|
||||
channel: index,
|
||||
max: self.num_channels.saturating_sub(1),
|
||||
});
|
||||
}
|
||||
Ok(&self.data[index])
|
||||
}
|
||||
}
|
||||
|
||||
/// Frequency band definition for neural oscillations.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub enum FrequencyBand {
|
||||
/// Delta: 1-4 Hz (deep sleep, unconscious processing).
|
||||
Delta,
|
||||
/// Theta: 4-8 Hz (memory, navigation, meditation).
|
||||
Theta,
|
||||
/// Alpha: 8-13 Hz (relaxation, idling, inhibition).
|
||||
Alpha,
|
||||
/// Beta: 13-30 Hz (active thinking, focus, motor planning).
|
||||
Beta,
|
||||
/// Gamma: 30-100 Hz (binding, perception, consciousness).
|
||||
Gamma,
|
||||
/// High gamma: 100-200 Hz (cortical processing, fine motor).
|
||||
HighGamma,
|
||||
/// Custom frequency range.
|
||||
Custom {
|
||||
/// Lower bound in Hz.
|
||||
low_hz: f64,
|
||||
/// Upper bound in Hz.
|
||||
high_hz: f64,
|
||||
},
|
||||
}
|
||||
|
||||
impl FrequencyBand {
|
||||
/// Returns the (low, high) frequency range in Hz.
|
||||
pub fn range_hz(&self) -> (f64, f64) {
|
||||
match self {
|
||||
FrequencyBand::Delta => (1.0, 4.0),
|
||||
FrequencyBand::Theta => (4.0, 8.0),
|
||||
FrequencyBand::Alpha => (8.0, 13.0),
|
||||
FrequencyBand::Beta => (13.0, 30.0),
|
||||
FrequencyBand::Gamma => (30.0, 100.0),
|
||||
FrequencyBand::HighGamma => (100.0, 200.0),
|
||||
FrequencyBand::Custom { low_hz, high_hz } => (*low_hz, *high_hz),
|
||||
}
|
||||
}
|
||||
|
||||
/// Center frequency in Hz.
|
||||
pub fn center_hz(&self) -> f64 {
|
||||
let (lo, hi) = self.range_hz();
|
||||
(lo + hi) / 2.0
|
||||
}
|
||||
|
||||
/// Bandwidth in Hz.
|
||||
pub fn bandwidth_hz(&self) -> f64 {
|
||||
let (lo, hi) = self.range_hz();
|
||||
hi - lo
|
||||
}
|
||||
}
|
||||
|
||||
/// Spectral features for one channel at one time window.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SpectralFeatures {
|
||||
/// Power in each frequency band.
|
||||
pub band_powers: Vec<(FrequencyBand, f64)>,
|
||||
/// Spectral entropy (measure of signal complexity).
|
||||
pub spectral_entropy: f64,
|
||||
/// Peak frequency in Hz.
|
||||
pub peak_frequency_hz: f64,
|
||||
/// Total power across all bands.
|
||||
pub total_power: f64,
|
||||
}
|
||||
|
||||
/// Time-frequency representation (spectrogram-like).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TimeFrequencyMap {
|
||||
/// Data matrix: `data[time_window][frequency_bin]`.
|
||||
pub data: Vec<Vec<f64>>,
|
||||
/// Time points in seconds.
|
||||
pub time_points: Vec<f64>,
|
||||
/// Frequency bin centers in Hz.
|
||||
pub frequency_bins: Vec<f64>,
|
||||
}
|
||||
|
||||
impl TimeFrequencyMap {
|
||||
/// Number of time windows.
|
||||
pub fn num_time_points(&self) -> usize {
|
||||
self.time_points.len()
|
||||
}
|
||||
|
||||
/// Number of frequency bins.
|
||||
pub fn num_frequency_bins(&self) -> usize {
|
||||
self.frequency_bins.len()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
//! Topology analysis result types (mincut, partition, metrics).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Result of a minimum cut computation on a brain graph.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MincutResult {
|
||||
/// Value of the minimum cut.
|
||||
pub cut_value: f64,
|
||||
/// Node indices in partition A.
|
||||
pub partition_a: Vec<usize>,
|
||||
/// Node indices in partition B.
|
||||
pub partition_b: Vec<usize>,
|
||||
/// Cut edges: (source, target, weight).
|
||||
pub cut_edges: Vec<(usize, usize, f64)>,
|
||||
/// Timestamp of the source graph.
|
||||
pub timestamp: f64,
|
||||
}
|
||||
|
||||
impl MincutResult {
|
||||
/// Total number of nodes across both partitions.
|
||||
pub fn num_nodes(&self) -> usize {
|
||||
self.partition_a.len() + self.partition_b.len()
|
||||
}
|
||||
|
||||
/// Number of edges crossing the cut.
|
||||
pub fn num_cut_edges(&self) -> usize {
|
||||
self.cut_edges.len()
|
||||
}
|
||||
|
||||
/// Balance ratio: min(|A|, |B|) / max(|A|, |B|).
|
||||
pub fn balance_ratio(&self) -> f64 {
|
||||
let a = self.partition_a.len() as f64;
|
||||
let b = self.partition_b.len() as f64;
|
||||
if a == 0.0 || b == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
a.min(b) / a.max(b)
|
||||
}
|
||||
}
|
||||
|
||||
/// Multi-way partition result.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MultiPartition {
|
||||
/// Each inner vec is a set of node indices forming one partition.
|
||||
pub partitions: Vec<Vec<usize>>,
|
||||
/// Total cut value.
|
||||
pub cut_value: f64,
|
||||
/// Newman-Girvan modularity score.
|
||||
pub modularity: f64,
|
||||
}
|
||||
|
||||
impl MultiPartition {
|
||||
/// Number of partitions (modules).
|
||||
pub fn num_partitions(&self) -> usize {
|
||||
self.partitions.len()
|
||||
}
|
||||
|
||||
/// Total number of nodes.
|
||||
pub fn num_nodes(&self) -> usize {
|
||||
self.partitions.iter().map(|p| p.len()).sum()
|
||||
}
|
||||
}
|
||||
|
||||
/// Cognitive state derived from brain topology analysis.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum CognitiveState {
|
||||
Rest,
|
||||
Focused,
|
||||
MotorPlanning,
|
||||
SpeechProcessing,
|
||||
MemoryEncoding,
|
||||
MemoryRetrieval,
|
||||
Creative,
|
||||
Stressed,
|
||||
Fatigued,
|
||||
Sleep(SleepStage),
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Sleep stage classification.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum SleepStage {
|
||||
Wake,
|
||||
N1,
|
||||
N2,
|
||||
N3,
|
||||
Rem,
|
||||
}
|
||||
|
||||
/// Topology metrics computed from a brain graph at a single time point.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TopologyMetrics {
|
||||
/// Global minimum cut value.
|
||||
pub global_mincut: f64,
|
||||
/// Newman-Girvan modularity.
|
||||
pub modularity: f64,
|
||||
/// Global efficiency (inverse path length).
|
||||
pub global_efficiency: f64,
|
||||
/// Mean local efficiency.
|
||||
pub local_efficiency: f64,
|
||||
/// Graph entropy (edge weight distribution).
|
||||
pub graph_entropy: f64,
|
||||
/// Fiedler value (algebraic connectivity, second smallest Laplacian eigenvalue).
|
||||
pub fiedler_value: f64,
|
||||
/// Number of detected modules.
|
||||
pub num_modules: usize,
|
||||
/// Timestamp of the source graph.
|
||||
pub timestamp: f64,
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user