mirror of
https://github.com/ruvnet/RuView
synced 2026-07-31 18:51:42 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 88a4cf7729 |
@@ -79,13 +79,13 @@ jobs:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
v2/target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('v2/Cargo.lock') }}
|
||||
rust-port/wifi-densepose-rs/target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('rust-port/wifi-densepose-rs/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-
|
||||
|
||||
- name: Run Rust tests
|
||||
working-directory: v2
|
||||
working-directory: rust-port/wifi-densepose-rs
|
||||
run: cargo test --workspace --no-default-features
|
||||
|
||||
# Unit and Integration Tests
|
||||
@@ -310,27 +310,26 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [code-quality, test, rust-tests, performance-test, docker-build, docs]
|
||||
if: always()
|
||||
# GitHub Actions does not allow `secrets.X` directly in step-level `if:`
|
||||
# expressions — only `env.X`. Promote the secret to env at job scope so
|
||||
# the gating expression below is parseable.
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
steps:
|
||||
- name: Notify Slack on success
|
||||
if: ${{ env.SLACK_WEBHOOK_URL != '' && needs.code-quality.result == 'success' && needs.test.result == 'success' && needs.docker-build.result == 'success' }}
|
||||
if: ${{ secrets.SLACK_WEBHOOK_URL != '' && needs.code-quality.result == 'success' && needs.test.result == 'success' && needs.docker-build.result == 'success' }}
|
||||
uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: success
|
||||
channel: '#ci-cd'
|
||||
text: '✅ CI pipeline completed successfully for ${{ github.ref }}'
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
- name: Notify Slack on failure
|
||||
if: ${{ env.SLACK_WEBHOOK_URL != '' && (needs.code-quality.result == 'failure' || needs.test.result == 'failure' || needs.docker-build.result == 'failure') }}
|
||||
if: ${{ secrets.SLACK_WEBHOOK_URL != '' && (needs.code-quality.result == 'failure' || needs.test.result == 'failure' || needs.docker-build.result == 'failure') }}
|
||||
uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: failure
|
||||
channel: '#ci-cd'
|
||||
text: '❌ CI pipeline failed for ${{ github.ref }}'
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: github.ref == 'refs/heads/main' && needs.docker-build.result == 'success'
|
||||
|
||||
@@ -40,18 +40,18 @@ jobs:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: v2/crates/wifi-densepose-desktop/ui
|
||||
working-directory: rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui
|
||||
run: npm ci
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: v2/crates/wifi-densepose-desktop/ui
|
||||
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: v2/crates/wifi-densepose-desktop
|
||||
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 }}
|
||||
@@ -68,14 +68,14 @@ jobs:
|
||||
|
||||
- name: Package macOS app
|
||||
run: |
|
||||
cd v2/target/${{ matrix.target }}/release/bundle/macos
|
||||
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: v2/target/${{ matrix.target }}/release/bundle/macos/*.zip
|
||||
path: rust-port/wifi-densepose-rs/target/${{ matrix.target }}/release/bundle/macos/*.zip
|
||||
|
||||
build-windows:
|
||||
name: Build Windows
|
||||
@@ -93,18 +93,18 @@ jobs:
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: v2/crates/wifi-densepose-desktop/ui
|
||||
working-directory: rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/ui
|
||||
run: npm ci
|
||||
|
||||
- name: Build frontend
|
||||
working-directory: v2/crates/wifi-densepose-desktop/ui
|
||||
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: v2/crates/wifi-densepose-desktop
|
||||
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 }}
|
||||
@@ -114,13 +114,13 @@ jobs:
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ruview-windows-msi
|
||||
path: v2/target/release/bundle/msi/*.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: v2/target/release/bundle/nsis/*.exe
|
||||
path: rust-port/wifi-densepose-rs/target/release/bundle/nsis/*.exe
|
||||
|
||||
create-release:
|
||||
name: Create Release
|
||||
|
||||
@@ -12,50 +12,31 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ESP32-S3 Firmware (${{ matrix.variant }})
|
||||
name: Build ESP32-S3 Firmware
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: espressif/idf:v5.4
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- variant: 8mb
|
||||
sdkconfig: sdkconfig.defaults
|
||||
partition_table_name: partitions_display.csv
|
||||
size_limit_kb: 1100
|
||||
artifact_app: esp32-csi-node.bin
|
||||
artifact_pt: partition-table.bin
|
||||
- variant: 4mb
|
||||
sdkconfig: sdkconfig.defaults.4mb
|
||||
partition_table_name: partitions_4mb.csv
|
||||
size_limit_kb: 1100
|
||||
artifact_app: esp32-csi-node-4mb.bin
|
||||
artifact_pt: partition-table-4mb.bin
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build firmware (${{ matrix.variant }})
|
||||
- name: Build firmware
|
||||
working-directory: firmware/esp32-csi-node
|
||||
run: |
|
||||
. $IDF_PATH/export.sh
|
||||
if [ "${{ matrix.variant }}" != "8mb" ]; then
|
||||
cp "${{ matrix.sdkconfig }}" sdkconfig.defaults
|
||||
fi
|
||||
idf.py set-target esp32s3
|
||||
idf.py build
|
||||
|
||||
- name: Verify binary size (< ${{ matrix.size_limit_kb }} KB gate)
|
||||
- name: Verify binary size (< 1100 KB gate)
|
||||
working-directory: firmware/esp32-csi-node
|
||||
run: |
|
||||
BIN=build/esp32-csi-node.bin
|
||||
SIZE=$(stat -c%s "$BIN")
|
||||
MAX=$((${{ matrix.size_limit_kb }} * 1024))
|
||||
MAX=$((1100 * 1024))
|
||||
echo "Binary size: $SIZE bytes ($(( SIZE / 1024 )) KB)"
|
||||
echo "Size limit: $MAX bytes (${{ matrix.size_limit_kb }} KB)"
|
||||
echo "Size limit: $MAX bytes (1100 KB — includes WASM runtime + HTTP client for Seed swarm bridge)"
|
||||
if [ "$SIZE" -gt "$MAX" ]; then
|
||||
echo "::error::Firmware binary exceeds ${{ matrix.size_limit_kb }} KB size gate ($SIZE > $MAX)"
|
||||
echo "::error::Firmware binary exceeds 1100 KB size gate ($SIZE > $MAX)"
|
||||
exit 1
|
||||
fi
|
||||
echo "Binary size OK: $SIZE <= $MAX"
|
||||
@@ -66,11 +47,14 @@ jobs:
|
||||
ERRORS=0
|
||||
BIN=build/esp32-csi-node.bin
|
||||
|
||||
# Check binary exists and is non-empty.
|
||||
if [ ! -s "$BIN" ]; then
|
||||
echo "::error::Binary not found or empty"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check partition table magic (0xAA50 at offset 0).
|
||||
# Use od instead of xxd (xxd not available in espressif/idf container).
|
||||
PT=build/partition_table/partition-table.bin
|
||||
if [ -f "$PT" ]; then
|
||||
MAGIC=$(od -A n -t x1 -N 2 "$PT" | tr -d ' ')
|
||||
@@ -80,12 +64,14 @@ jobs:
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check bootloader exists.
|
||||
BL=build/bootloader/bootloader.bin
|
||||
if [ ! -s "$BL" ]; then
|
||||
echo "::warning::Bootloader binary missing or empty"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
|
||||
# Verify non-zero data in binary (not all 0xFF padding).
|
||||
NONZERO=$(od -A n -t x1 -N 1024 "$BIN" | tr -d ' f\n' | wc -c)
|
||||
if [ "$NONZERO" -lt 100 ]; then
|
||||
echo "::error::Binary appears to be mostly padding (non-zero chars: $NONZERO)"
|
||||
@@ -98,27 +84,19 @@ jobs:
|
||||
echo "Flash image integrity verified"
|
||||
fi
|
||||
|
||||
- name: Stage release binaries with variant-specific names
|
||||
working-directory: firmware/esp32-csi-node
|
||||
run: |
|
||||
mkdir -p release-staging
|
||||
cp build/esp32-csi-node.bin release-staging/${{ matrix.artifact_app }}
|
||||
cp build/partition_table/partition-table.bin release-staging/${{ matrix.artifact_pt }}
|
||||
if [ "${{ matrix.variant }}" = "8mb" ]; then
|
||||
cp build/bootloader/bootloader.bin release-staging/bootloader.bin
|
||||
cp build/ota_data_initial.bin release-staging/ota_data_initial.bin
|
||||
fi
|
||||
ls -la release-staging/
|
||||
|
||||
- name: Check QEMU ESP32-S3 support status
|
||||
run: |
|
||||
echo "::notice::ESP32-S3 QEMU support is experimental in ESP-IDF v5.4. "
|
||||
echo "Full smoke testing requires QEMU 8.2+ with xtensa-esp32s3 target."
|
||||
echo "See: https://github.com/espressif/qemu/wiki"
|
||||
|
||||
- name: Upload firmware artifact (${{ matrix.variant }})
|
||||
- name: Upload firmware artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: esp32-csi-node-firmware-${{ matrix.variant }}
|
||||
path: firmware/esp32-csi-node/release-staging/
|
||||
name: esp32-csi-node-firmware
|
||||
path: |
|
||||
firmware/esp32-csi-node/build/esp32-csi-node.bin
|
||||
firmware/esp32-csi-node/build/bootloader/bootloader.bin
|
||||
firmware/esp32-csi-node/build/partition_table/partition-table.bin
|
||||
firmware/esp32-csi-node/build/ota_data_initial.bin
|
||||
retention-days: 90
|
||||
|
||||
@@ -377,11 +377,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [sast, dependency-scan, container-scan, iac-scan, secret-scan, license-scan, compliance-check]
|
||||
if: always()
|
||||
# Promote secret to env-scope so the gating `if:` on the Slack-notify
|
||||
# step below is parseable (GitHub Actions rejects `secrets.X` in
|
||||
# step-level `if:` expressions).
|
||||
env:
|
||||
SECURITY_SLACK_WEBHOOK_URL: ${{ secrets.SECURITY_SLACK_WEBHOOK_URL }}
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -407,11 +402,8 @@ jobs:
|
||||
name: security-summary
|
||||
path: security-summary.md
|
||||
|
||||
# GitHub Actions does not allow `secrets.X` in step-level `if:` —
|
||||
# use env.X instead. Inherits SECURITY_SLACK_WEBHOOK_URL from the
|
||||
# job-level env block (added below).
|
||||
- name: Notify security team on critical findings
|
||||
if: ${{ env.SECURITY_SLACK_WEBHOOK_URL != '' && (needs.sast.result == 'failure' || needs.dependency-scan.result == 'failure' || needs.container-scan.result == 'failure') }}
|
||||
if: ${{ secrets.SECURITY_SLACK_WEBHOOK_URL != '' && (needs.sast.result == 'failure' || needs.dependency-scan.result == 'failure' || needs.container-scan.result == 'failure') }}
|
||||
uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: failure
|
||||
@@ -423,7 +415,7 @@ jobs:
|
||||
Workflow: ${{ github.workflow }}
|
||||
Please review the security scan results immediately.
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ env.SECURITY_SLACK_WEBHOOK_URL }}
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SECURITY_SLACK_WEBHOOK_URL }}
|
||||
|
||||
- name: Create security issue on critical findings
|
||||
if: needs.sast.result == 'failure' || needs.dependency-scan.result == 'failure'
|
||||
|
||||
@@ -4,16 +4,16 @@ on:
|
||||
push:
|
||||
branches: [ main, master, 'claude/**' ]
|
||||
paths:
|
||||
- 'archive/v1/src/core/**'
|
||||
- 'archive/v1/src/hardware/**'
|
||||
- 'archive/v1/data/proof/**'
|
||||
- 'v1/src/core/**'
|
||||
- 'v1/src/hardware/**'
|
||||
- 'v1/data/proof/**'
|
||||
- '.github/workflows/verify-pipeline.yml'
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
paths:
|
||||
- 'archive/v1/src/core/**'
|
||||
- 'archive/v1/src/hardware/**'
|
||||
- 'archive/v1/data/proof/**'
|
||||
- 'v1/src/core/**'
|
||||
- 'v1/src/hardware/**'
|
||||
- 'v1/data/proof/**'
|
||||
- '.github/workflows/verify-pipeline.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -37,19 +37,19 @@ jobs:
|
||||
- name: Install pinned dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r archive/v1/requirements-lock.txt
|
||||
pip install -r v1/requirements-lock.txt
|
||||
|
||||
- name: Verify reference signal is reproducible
|
||||
run: |
|
||||
echo "=== Regenerating reference signal ==="
|
||||
python archive/v1/data/proof/generate_reference_signal.py
|
||||
python v1/data/proof/generate_reference_signal.py
|
||||
echo ""
|
||||
echo "=== Checking data file matches committed version ==="
|
||||
# The regenerated file should be identical to the committed one
|
||||
# (We compare the metadata file since data file is large)
|
||||
python -c "
|
||||
import json, hashlib
|
||||
with open('archive/v1/data/proof/sample_csi_meta.json') as f:
|
||||
with open('v1/data/proof/sample_csi_meta.json') as f:
|
||||
meta = json.load(f)
|
||||
assert meta['is_synthetic'] == True, 'Metadata must mark signal as synthetic'
|
||||
assert meta['numpy_seed'] == 42, 'Seed must be 42'
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
echo "=== Scanning for unseeded np.random usage in production code ==="
|
||||
# Search for np.random calls without a seed in production code
|
||||
# Exclude test files, proof data generators, and known parser placeholders
|
||||
VIOLATIONS=$(grep -rn "np\.random\." archive/v1/src/ \
|
||||
VIOLATIONS=$(grep -rn "np\.random\." v1/src/ \
|
||||
--include="*.py" \
|
||||
--exclude-dir="__pycache__" \
|
||||
| grep -v "np\.random\.RandomState" \
|
||||
|
||||
@@ -250,5 +250,3 @@ v1/src/sensing/mac_wifi
|
||||
# Local build scripts
|
||||
firmware/esp32-csi-node/build_firmware.batdata/
|
||||
models/
|
||||
demo_pointcloud.ply
|
||||
demo_splats.json
|
||||
|
||||
+2
-142
@@ -7,146 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- **Ghost skeletons in live UI with multi-node ESP32 setups** (#420, ADR-082) —
|
||||
`tracker_bridge::tracker_to_person_detections` documented itself as filtering
|
||||
to `is_alive()` tracks but in fact passed every non-Terminated track to the
|
||||
WebSocket stream. `Lost` tracks — kept inside `reid_window` for
|
||||
re-identification but not currently observed — were rendering as phantom
|
||||
skeletons, accumulating to 22-24 with 3 nodes × 10 Hz CSI while
|
||||
`estimated_persons` correctly reported 1. Added
|
||||
`PoseTracker::confirmed_tracks()` (Tentative + Active only) and rewired the
|
||||
bridge to use it. Lost tracks remain in the tracker for re-ID; they just
|
||||
no longer ship to the UI. Regression test:
|
||||
`test_lost_tracks_excluded_from_bridge_output`.
|
||||
- **Rust workspace build with `--no-default-features` on Windows** (#366, #415) —
|
||||
`wifi-densepose-mat`, `wifi-densepose-sensing-server`, and `wifi-densepose-train`
|
||||
all depended on `wifi-densepose-signal` with default features enabled, which
|
||||
pulled `ndarray-linalg` → `openblas-src` → vcpkg/system-BLAS through the entire
|
||||
workspace. `--no-default-features` at the workspace root then could not opt out
|
||||
of BLAS, breaking `cargo build` / `cargo test` on Windows without vcpkg. All
|
||||
three consumers now declare `wifi-densepose-signal = { ..., default-features = false }`,
|
||||
so `cargo test --workspace --no-default-features` builds cleanly without
|
||||
vcpkg/openblas. Validated: 1,538 tests pass, 0 fail, 8 ignored.
|
||||
- **`signal` test `test_estimate_occupancy_noise_only` failed without `eigenvalue`** —
|
||||
The test unwrapped the `NotCalibrated` stub returned when the BLAS-backed
|
||||
`estimate_occupancy` is compiled out. Gated with `#[cfg(feature = "eigenvalue")]`
|
||||
so it only runs when the real implementation is available.
|
||||
|
||||
## [v0.6.2-esp32] — 2026-04-20
|
||||
|
||||
Firmware release cutting ADR-081 and the Timer Svc stack fix discovered during
|
||||
on-hardware validation. Cut from `main` at commit pointing to this entry.
|
||||
Tested on ESP32-S3 (QFN56 rev v0.2, MAC `3c:0f:02:e9:b5:f8`), 30 s continuous
|
||||
run: no crashes, 149 `rv_feature_state_t` emissions (~5 Hz), medium/slow ticks
|
||||
firing cleanly, HEALTH mesh packets sent.
|
||||
|
||||
### Fixed
|
||||
- **Firmware: Timer Svc stack overflow on ADR-081 fast loop** — `emit_feature_state()` runs inside the FreeRTOS Timer Svc task via the fast-loop callback; it calls `stream_sender` network I/O which pushes past the ESP-IDF 2 KiB default timer stack and panics ~1 s after boot. Bumped `CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH` to 8 KiB in `sdkconfig.defaults`, `sdkconfig.defaults.template`, and `sdkconfig.defaults.4mb`. Follow-up (tracked separately): move heavy work out of the timer daemon into a dedicated worker task.
|
||||
- **Firmware: `adaptive_controller.c` implicit declaration** (#404) — `fast_loop_cb` called `emit_feature_state()` before its static definition, triggering `-Werror=implicit-function-declaration`. Added a forward declaration above the first use.
|
||||
|
||||
### Changed
|
||||
- **CI: firmware build matrix (8MB + 4MB)** — `firmware-ci.yml` now matrix-builds both the default 8MB (`sdkconfig.defaults`) and 4MB SuperMini (`sdkconfig.defaults.4mb`) variants, uploading distinct artifacts and producing variant-named release binaries (`esp32-csi-node.bin` / `esp32-csi-node-4mb.bin`, `partition-table.bin` / `partition-table-4mb.bin`).
|
||||
|
||||
### Added
|
||||
- **ADR-081: Adaptive CSI Mesh Firmware Kernel** — New 5-layer architecture
|
||||
(Radio Abstraction Layer / Adaptive Controller / Mesh Sensing Plane /
|
||||
On-device Feature Extraction / Rust handoff) that reframes the existing
|
||||
ESP32 firmware modules as components of a chipset-agnostic kernel. ADR
|
||||
in `docs/adr/ADR-081-adaptive-csi-mesh-firmware-kernel.md`. Goal: swap
|
||||
one radio family for another without changing the Rust signal /
|
||||
ruvector / train / mat crates.
|
||||
- **Firmware: radio abstraction vtable (`rv_radio_ops_t`)** — New
|
||||
`firmware/esp32-csi-node/main/rv_radio_ops.{h}` defines the
|
||||
chipset-agnostic ops (init, set_channel, set_mode, set_csi_enabled,
|
||||
set_capture_profile, get_health), profile enum
|
||||
(`RV_PROFILE_PASSIVE_LOW_RATE` / `ACTIVE_PROBE` / `RESP_HIGH_SENS` /
|
||||
`FAST_MOTION` / `CALIBRATION`), and health snapshot struct.
|
||||
`rv_radio_ops_esp32.c` provides the ESP32 binding wrapping
|
||||
`csi_collector` + `esp_wifi_*`. A second binding (mock or alternate
|
||||
chipset) is the portability acceptance test for ADR-081.
|
||||
- **Firmware: `rv_feature_state_t` packet (magic `0xC5110006`)** — New
|
||||
60-byte compact per-node sensing state (packed, verified by
|
||||
`_Static_assert`) in `firmware/esp32-csi-node/main/rv_feature_state.h`:
|
||||
motion, presence, respiration BPM/conf, heartbeat BPM/conf, anomaly
|
||||
score, env-shift score, node coherence, quality flags, IEEE CRC32.
|
||||
Replaces raw ADR-018 CSI as the default upstream stream (~99.7%
|
||||
bandwidth reduction: 300 B/s at 5 Hz vs. ~100 KB/s raw).
|
||||
- **Firmware: mock radio ops binding for QEMU** — New
|
||||
`firmware/esp32-csi-node/main/rv_radio_ops_mock.c`, compiled only when
|
||||
`CONFIG_CSI_MOCK_ENABLED`. Satisfies ADR-081's portability acceptance
|
||||
test: a second `rv_radio_ops_t` binding compiles and runs against the
|
||||
same controller + mesh-plane code as the ESP32 binding.
|
||||
- **Firmware: feature-state emitter wired into controller fast loop** —
|
||||
`adaptive_controller.c` now emits one 60-byte `rv_feature_state_t` per
|
||||
fast tick (default 200 ms → 5 Hz), pulling from the latest edge vitals
|
||||
and controller observation. This is the first end-to-end Layer 4/5
|
||||
path for ADR-081.
|
||||
- **Firmware: `csi_collector_get_pkt_yield_per_sec()` /
|
||||
`_get_send_fail_count()` accessors** — Expose the CSI callback rate
|
||||
and UDP send-failure counter so the ESP32 radio ops binding can
|
||||
populate `rv_radio_health_t.pkt_yield_per_sec` and `.send_fail_count`,
|
||||
closing the adaptive controller's observation loop.
|
||||
- **Firmware: host-side unit test suite for ADR-081 pure logic** — New
|
||||
`firmware/esp32-csi-node/tests/host/` (Makefile + 2 test files + shim
|
||||
`esp_err.h`). Exercises `adaptive_controller_decide()` (9 test cases:
|
||||
degraded gate on pkt-yield collapse + coherence loss, anomaly > motion,
|
||||
motion → SENSE_ACTIVE, aggressive cadence, stable presence →
|
||||
RESP_HIGH_SENS, empty-room default, hysteresis, NULL safety) and
|
||||
`rv_feature_state_*` helpers (size assertion, IEEE CRC32 known
|
||||
vectors, determinism, receiver-side verification). 33/33 assertions
|
||||
pass. Benchmarks: decide() 3.2 ns/call, CRC32(56 B) 614 ns/pkt
|
||||
(87 MB/s), full finalize() 616 ns/call. Pure function
|
||||
`adaptive_controller_decide()` extracted to
|
||||
`adaptive_controller_decide.c` so the firmware build and the host
|
||||
tests share a single source-of-truth implementation.
|
||||
- **Scripts: `validate_qemu_output.py` ADR-081 checks** — Validator
|
||||
(invoked by ADR-061 `scripts/qemu-esp32s3-test.sh` in CI) gains three
|
||||
checks for adaptive controller boot line, mock radio ops
|
||||
registration, and slow-loop heartbeat, so QEMU runs regression-gate
|
||||
Layer 1/2 presence.
|
||||
- **Firmware: ADR-081 Layer 3 mesh sensing plane** — New
|
||||
`firmware/esp32-csi-node/main/rv_mesh.{h,c}` defines 4 node roles
|
||||
(Anchor / Observer / Fusion relay / Coordinator), 7 on-wire message
|
||||
types (TIME_SYNC, ROLE_ASSIGN, CHANNEL_PLAN, CALIBRATION_START,
|
||||
FEATURE_DELTA, HEALTH, ANOMALY_ALERT), 3 authorization classes
|
||||
(None / HMAC-SHA256-session / Ed25519-batch), `rv_node_status_t`
|
||||
(28 B), `rv_anomaly_alert_t` (28 B), `rv_time_sync_t`,
|
||||
`rv_role_assign_t`, `rv_channel_plan_t`, `rv_calibration_start_t`.
|
||||
Pure-C encoder/decoder (`rv_mesh_encode()` / `rv_mesh_decode()`) with
|
||||
16-byte envelope + payload + IEEE CRC32 trailer; convenience encoders
|
||||
for each message type. Controller now emits `HEALTH` every slow-loop
|
||||
tick (30 s default) and `ANOMALY_ALERT` on state transitions to ALERT
|
||||
or DEGRADED. Host tests: `test_rv_mesh` exercises 27 assertions
|
||||
covering roundtrip, bad magic, truncation, CRC flipping, oversize
|
||||
payload rejection, and encode+decode throughput (1.0 μs/roundtrip
|
||||
on host).
|
||||
- **Rust: ADR-081 Layer 1/3 mirror module** — New
|
||||
`crates/wifi-densepose-hardware/src/radio_ops.rs` mirrors the
|
||||
firmware-side `rv_radio_ops_t` vtable as the Rust `RadioOps` trait
|
||||
(init, set_channel, set_mode, set_csi_enabled, set_capture_profile,
|
||||
get_health) and provides `MockRadio` for offline testing.
|
||||
Also mirrors the `rv_mesh.h` types (`MeshHeader`, `NodeStatus`,
|
||||
`AnomalyAlert`, `MeshRole`, `MeshMsgType`, `AuthClass`) and ships
|
||||
byte-identical `crc32_ieee()`, `decode_mesh()`, `decode_node_status()`,
|
||||
`decode_anomaly_alert()`, and `encode_health()`. Exported from
|
||||
`lib.rs`. 8 unit tests pass; `crc32_matches_firmware_vectors`
|
||||
verifies parity with the firmware-side test vectors
|
||||
(`0xCBF43926` for `"123456789"`, `0xD202EF8D` for single-byte zero),
|
||||
and `mesh_constants_match_firmware` asserts `MESH_MAGIC`,
|
||||
`MESH_VERSION`, `MESH_HEADER_SIZE`, and `MESH_MAX_PAYLOAD` match
|
||||
`rv_mesh.h` byte-for-byte. Satisfies ADR-081's portability
|
||||
acceptance test: signal/ruvector/train/mat crates are untouched.
|
||||
- **Firmware: adaptive controller** — New
|
||||
`firmware/esp32-csi-node/main/adaptive_controller.{c,h}` implements
|
||||
the three-loop closed-loop control specified by ADR-081: fast
|
||||
(~200 ms) for cadence and active probing, medium (~1 s) for channel
|
||||
selection and role transitions, slow (~30 s) for baseline
|
||||
recalibration. Pure `adaptive_controller_decide()` policy function is
|
||||
exposed in the header for offline unit testing. Default policy is
|
||||
conservative (`enable_channel_switch` and `enable_role_change` off);
|
||||
Kconfig surface added under "Adaptive Controller (ADR-081)".
|
||||
|
||||
### Fixed
|
||||
- **`provision.py` esptool v5 compat** (#391) — Stale `write_flash` (underscore) syntax in the dry-run manual-flash hint now uses `write-flash` (hyphenated) for esptool >= 5.x. The primary flash command was already correct.
|
||||
- **`provision.py` silent NVS wipe** (#391) — The script replaces the entire `csi_cfg` NVS namespace on every run, so partial invocations were silently erasing WiFi credentials and causing `Retrying WiFi connection (10/10)` in the field. Now refuses to run without `--ssid`, `--password`, and `--target-ip` unless `--force-partial` is passed. `--force-partial` prints a warning listing which keys will be wiped.
|
||||
@@ -520,7 +380,7 @@ Major release: complete Rust sensing server, full DensePose training pipeline, R
|
||||
- `PresenceClassifier` — rule-based 3-state classification (ABSENT / PRESENT_STILL / ACTIVE)
|
||||
- Cross-receiver agreement scoring for multi-AP confidence boosting
|
||||
- WebSocket sensing server (`ws_server.py`) broadcasting JSON at 2 Hz
|
||||
- Deterministic CSI proof bundles for reproducible verification (`archive/v1/data/proof/`)
|
||||
- Deterministic CSI proof bundles for reproducible verification (`v1/data/proof/`)
|
||||
- Commodity sensing unit tests (`b391638`)
|
||||
|
||||
### Changed
|
||||
@@ -528,7 +388,7 @@ Major release: complete Rust sensing server, full DensePose training pipeline, R
|
||||
|
||||
### Fixed
|
||||
- Review fixes for end-to-end training pipeline (`45f0304`)
|
||||
- Dockerfile paths updated from `src/` to `archive/v1/src/` (`7872987`)
|
||||
- Dockerfile paths updated from `src/` to `v1/src/` (`7872987`)
|
||||
- IoT profile installer instructions updated for aggregator CLI (`f460097`)
|
||||
- `process.env` reference removed from browser ES module (`e320bc9`)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## Project: wifi-densepose
|
||||
|
||||
WiFi-based human pose estimation using Channel State Information (CSI).
|
||||
Dual codebase: Python v1 (`v1/`) and Rust port (`v2/`).
|
||||
Dual codebase: Python v1 (`v1/`) and Rust port (`rust-port/wifi-densepose-rs/`).
|
||||
### Key Rust Crates
|
||||
| Crate | Description |
|
||||
|-------|-------------|
|
||||
@@ -84,17 +84,17 @@ All 5 ruvector crates integrated in workspace:
|
||||
### Build & Test Commands (this repo)
|
||||
```bash
|
||||
# Rust — full workspace tests (1,031+ tests, ~2 min)
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo test --workspace --no-default-features
|
||||
|
||||
# Rust — single crate check (no GPU needed)
|
||||
cargo check -p wifi-densepose-train --no-default-features
|
||||
|
||||
# Python — deterministic proof verification (SHA-256)
|
||||
python archive/v1/data/proof/verify.py
|
||||
python v1/data/proof/verify.py
|
||||
|
||||
# Python — test suite
|
||||
cd archive/v1 && python -m pytest tests/ -x -q
|
||||
cd v1 && python -m pytest tests/ -x -q
|
||||
```
|
||||
|
||||
### ESP32 Firmware Build (Windows — Python subprocess required)
|
||||
@@ -151,12 +151,12 @@ Crates must be published in dependency order:
|
||||
|
||||
```bash
|
||||
# 1. Rust tests — must be 1,031+ passed, 0 failed
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo test --workspace --no-default-features
|
||||
|
||||
# 2. Python proof — must print VERDICT: PASS
|
||||
cd ..
|
||||
python archive/v1/data/proof/verify.py
|
||||
cd ../..
|
||||
python v1/data/proof/verify.py
|
||||
|
||||
# 3. Generate witness bundle (includes both above + firmware hashes)
|
||||
bash scripts/generate-witness-bundle.sh
|
||||
@@ -169,8 +169,8 @@ bash VERIFY.sh
|
||||
**If the Python proof hash changes** (e.g., numpy/scipy version update):
|
||||
```bash
|
||||
# Regenerate the expected hash, then verify it passes
|
||||
python archive/v1/data/proof/verify.py --generate-hash
|
||||
python archive/v1/data/proof/verify.py
|
||||
python v1/data/proof/verify.py --generate-hash
|
||||
python v1/data/proof/verify.py
|
||||
```
|
||||
|
||||
**Witness bundle contents** (`dist/witness-bundle-ADR028-<sha>.tar.gz`):
|
||||
@@ -183,9 +183,9 @@ python archive/v1/data/proof/verify.py
|
||||
- `VERIFY.sh` — One-command self-verification for recipients
|
||||
|
||||
**Key proof artifacts:**
|
||||
- `archive/v1/data/proof/verify.py` — Trust Kill Switch: feeds reference signal through production pipeline, hashes output
|
||||
- `archive/v1/data/proof/expected_features.sha256` — Published expected hash
|
||||
- `archive/v1/data/proof/sample_csi_data.json` — 1,000 synthetic CSI frames (seed=42)
|
||||
- `v1/data/proof/verify.py` — Trust Kill Switch: feeds reference signal through production pipeline, hashes output
|
||||
- `v1/data/proof/expected_features.sha256` — Published expected hash
|
||||
- `v1/data/proof/sample_csi_data.json` — 1,000 synthetic CSI frames (seed=42)
|
||||
- `docs/WITNESS-LOG-028.md` — 11-step reproducible verification procedure
|
||||
- `docs/adr/ADR-028-esp32-capability-audit.md` — Complete audit record
|
||||
|
||||
@@ -211,13 +211,13 @@ Active feature branch: `ruvsense-full-implementation` (PR #77)
|
||||
- NEVER save to root folder — use the directories below
|
||||
- `docs/adr/` — Architecture Decision Records (43 ADRs)
|
||||
- `docs/ddd/` — Domain-Driven Design models
|
||||
- `v2/crates/` — Rust workspace crates (15 crates)
|
||||
- `v2/crates/wifi-densepose-signal/src/ruvsense/` — RuvSense multistatic modules (14 files)
|
||||
- `v2/crates/wifi-densepose-ruvector/src/viewpoint/` — Cross-viewpoint fusion (5 files)
|
||||
- `v2/crates/wifi-densepose-hardware/src/esp32/` — ESP32 TDM protocol
|
||||
- `rust-port/wifi-densepose-rs/crates/` — Rust workspace crates (15 crates)
|
||||
- `rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/` — RuvSense multistatic modules (14 files)
|
||||
- `rust-port/wifi-densepose-rs/crates/wifi-densepose-ruvector/src/viewpoint/` — Cross-viewpoint fusion (5 files)
|
||||
- `rust-port/wifi-densepose-rs/crates/wifi-densepose-hardware/src/esp32/` — ESP32 TDM protocol
|
||||
- `firmware/esp32-csi-node/main/` — ESP32 C firmware (channel hopping, NVS config, TDM)
|
||||
- `archive/v1/src/` — Python source (core, hardware, services, api)
|
||||
- `archive/v1/data/proof/` — Deterministic CSI proof bundles
|
||||
- `v1/src/` — Python source (core, hardware, services, api)
|
||||
- `v1/data/proof/` — Deterministic CSI proof bundles
|
||||
- `.claude-flow/` — Claude Flow coordination state (committed for team sharing)
|
||||
- `.claude/` — Claude Code settings, agents, memory (committed for team sharing)
|
||||
|
||||
@@ -243,7 +243,7 @@ Active feature branch: `ruvsense-full-implementation` (PR #77)
|
||||
Before merging any PR, verify each item applies and is addressed:
|
||||
|
||||
1. **Rust tests pass** — `cargo test --workspace --no-default-features` (1,031+ passed, 0 failed)
|
||||
2. **Python proof passes** — `python archive/v1/data/proof/verify.py` (VERDICT: PASS)
|
||||
2. **Python proof passes** — `python v1/data/proof/verify.py` (VERDICT: PASS)
|
||||
3. **README.md** — Update platform tables, crate descriptions, hardware tables, feature summaries if scope changed
|
||||
4. **CLAUDE.md** — Update crate table, ADR list, module tables, version if scope changed
|
||||
5. **CHANGELOG.md** — Add entry under `[Unreleased]` with what was added/fixed/changed
|
||||
|
||||
@@ -92,51 +92,10 @@ node scripts/mincut-person-counter.js --port 5006 # Correct person counting
|
||||
> | **Research NIC** | Intel 5300 / Atheros AR9580 | ~$50-100 | Yes | Full CSI with 3x3 MIMO |
|
||||
> | **Any WiFi** | Windows, macOS, or Linux laptop | $0 | No | RSSI-only: coarse presence and motion |
|
||||
>
|
||||
> No hardware? Verify the signal processing pipeline with the deterministic reference signal: `python archive/v1/data/proof/verify.py`
|
||||
> No hardware? Verify the signal processing pipeline with the deterministic reference signal: `python v1/data/proof/verify.py`
|
||||
>
|
||||
---
|
||||
|
||||
### Real-Time Dense Point Cloud (NEW)
|
||||
|
||||
RuView now generates **real-time 3D point clouds** by fusing camera depth + WiFi CSI + mmWave radar. All sensors stream simultaneously into a unified spatial model.
|
||||
|
||||
| Sensor | Data | Integration |
|
||||
|--------|------|-------------|
|
||||
| **Camera** | MiDaS monocular depth (GPU) | 640×480 → 19,200+ depth points per frame |
|
||||
| **ESP32 CSI** | ADR-018 binary frames (UDP) | RF tomography → 8×8×4 occupancy grid |
|
||||
| **WiFlow Pose** | 17 COCO keypoints from CSI | Skeleton overlay on point cloud |
|
||||
| **Vital Signs** | Breathing rate from CSI phase | Stored in ruOS brain every 60s |
|
||||
| **Motion** | CSI amplitude variance | Adaptive capture rate (skip depth when still) |
|
||||
|
||||
**Quick start:**
|
||||
```bash
|
||||
cd v2
|
||||
cargo build --release -p wifi-densepose-pointcloud
|
||||
./target/release/ruview-pointcloud serve --bind 127.0.0.1:9880
|
||||
# Open http://localhost:9880 for live 3D viewer
|
||||
```
|
||||
|
||||
**CLI commands:**
|
||||
```bash
|
||||
ruview-pointcloud demo # synthetic demo
|
||||
ruview-pointcloud serve --bind 127.0.0.1:9880 # live server + Three.js viewer
|
||||
ruview-pointcloud capture --output room.ply # capture to PLY
|
||||
ruview-pointcloud train # depth calibration + DPO pairs
|
||||
ruview-pointcloud cameras # list available cameras
|
||||
ruview-pointcloud csi-test --count 100 # send test CSI frames
|
||||
ruview-pointcloud fingerprint office --seconds 5 # record named CSI room fingerprint
|
||||
```
|
||||
|
||||
The HTTP/viewer server defaults to **loopback (`127.0.0.1`)** — exposing live camera/CSI/vitals on `0.0.0.0` is an explicit opt-in. Brain URL defaults to `http://127.0.0.1:9876` and is overridable via `RUVIEW_BRAIN_URL` env var or the `--brain` flag on `serve`/`train`.
|
||||
|
||||
The pose overlay currently uses an **amplitude-energy heuristic** (`heuristic_pose_from_amplitude`) rather than trained WiFlow inference — real ONNX/Candle inference is tracked as a follow-up.
|
||||
|
||||
**Performance:** 22ms pipeline, 905 req/s API, 40K voxel room model from 20 frames.
|
||||
|
||||
**Brain integration:** Spatial observations (motion, vitals, skeleton, occupancy) sync to the ruOS brain every 60 seconds for agent reasoning.
|
||||
|
||||
See [PR #405](https://github.com/ruvnet/RuView/pull/405) for full details.
|
||||
|
||||
### What's New in v0.7.0
|
||||
|
||||
<details>
|
||||
@@ -381,7 +340,7 @@ See [ADR-069](docs/adr/ADR-069-cognitum-seed-csi-pipeline.md), [ADR-071](docs/ad
|
||||
| [Build Guide](docs/build-guide.md) | Building from source (Rust and Python) |
|
||||
| [Architecture Decisions](docs/adr/README.md) | 79 ADRs — why each technical choice was made, organized by domain (hardware, signal processing, ML, platform, infrastructure) |
|
||||
| [Domain Models](docs/ddd/README.md) | 7 DDD models (RuvSense, Signal Processing, Training Pipeline, Hardware Platform, Sensing Server, WiFi-Mat, CHCI) — bounded contexts, aggregates, domain events, and ubiquitous language |
|
||||
| [Desktop App](v2/crates/wifi-densepose-desktop/README.md) | **WIP** — Tauri v2 desktop app for node management, OTA updates, WASM deployment, and mesh visualization |
|
||||
| [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 |
|
||||
| [Medical Examples](examples/medical/README.md) | Contactless blood pressure, heart rate, breathing rate via 60 GHz mmWave radar — $15 hardware, no wearable |
|
||||
|
||||
---
|
||||
@@ -581,24 +540,24 @@ Small programs that run directly on the ESP32 sensor — no internet needed, no
|
||||
| ⚛️ | [**Quantum-Inspired**](docs/edge-modules/autonomous.md) | Uses quantum-inspired math to map room-wide signal coherence and search for optimal sensor configurations |
|
||||
| 🤖 | [**Autonomous & Exotic**](docs/edge-modules/autonomous.md) | Self-managing sensor mesh — auto-heals dropped nodes, plans its own actions, and explores experimental signal representations |
|
||||
|
||||
All implemented modules are `no_std` Rust, share a [common utility library](v2/crates/wifi-densepose-wasm-edge/src/vendor_common.rs), and talk to the host through a 12-function API. Full documentation: [**Edge Modules Guide**](docs/edge-modules/README.md). See the [complete implemented module list](#edge-module-list) below.
|
||||
All implemented modules are `no_std` Rust, share a [common utility library](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/vendor_common.rs), and talk to the host through a 12-function API. Full documentation: [**Edge Modules Guide**](docs/edge-modules/README.md). See the [complete implemented module list](#edge-module-list) below.
|
||||
|
||||
<details id="edge-module-list">
|
||||
<summary><strong>🧩 Edge Intelligence — <a href="docs/edge-modules/README.md">All 65 Modules Implemented</a></strong> (ADR-041 complete)</summary>
|
||||
|
||||
All 60 modules are implemented, tested (609 tests passing), and ready to deploy. They compile to `wasm32-unknown-unknown`, run on ESP32-S3 via WASM3, and share a [common utility library](v2/crates/wifi-densepose-wasm-edge/src/vendor_common.rs). Source: [`crates/wifi-densepose-wasm-edge/src/`](v2/crates/wifi-densepose-wasm-edge/src/)
|
||||
All 60 modules are implemented, tested (609 tests passing), and ready to deploy. They compile to `wasm32-unknown-unknown`, run on ESP32-S3 via WASM3, and share a [common utility library](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/vendor_common.rs). Source: [`crates/wifi-densepose-wasm-edge/src/`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/)
|
||||
|
||||
**Core modules** (ADR-040 flagship + early implementations):
|
||||
|
||||
| Module | File | What It Does |
|
||||
|--------|------|-------------|
|
||||
| Gesture Classifier | [`gesture.rs`](v2/crates/wifi-densepose-wasm-edge/src/gesture.rs) | DTW template matching for hand gestures |
|
||||
| Coherence Filter | [`coherence.rs`](v2/crates/wifi-densepose-wasm-edge/src/coherence.rs) | Phase coherence gating for signal quality |
|
||||
| Adversarial Detector | [`adversarial.rs`](v2/crates/wifi-densepose-wasm-edge/src/adversarial.rs) | Detects physically impossible signal patterns |
|
||||
| Intrusion Detector | [`intrusion.rs`](v2/crates/wifi-densepose-wasm-edge/src/intrusion.rs) | Human vs non-human motion classification |
|
||||
| Occupancy Counter | [`occupancy.rs`](v2/crates/wifi-densepose-wasm-edge/src/occupancy.rs) | Zone-level person counting |
|
||||
| Vital Trend | [`vital_trend.rs`](v2/crates/wifi-densepose-wasm-edge/src/vital_trend.rs) | Long-term breathing and heart rate trending |
|
||||
| RVF Parser | [`rvf.rs`](v2/crates/wifi-densepose-wasm-edge/src/rvf.rs) | RVF container format parsing |
|
||||
| Gesture Classifier | [`gesture.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/gesture.rs) | DTW template matching for hand gestures |
|
||||
| Coherence Filter | [`coherence.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/coherence.rs) | Phase coherence gating for signal quality |
|
||||
| Adversarial Detector | [`adversarial.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/adversarial.rs) | Detects physically impossible signal patterns |
|
||||
| Intrusion Detector | [`intrusion.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/intrusion.rs) | Human vs non-human motion classification |
|
||||
| Occupancy Counter | [`occupancy.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/occupancy.rs) | Zone-level person counting |
|
||||
| Vital Trend | [`vital_trend.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/vital_trend.rs) | Long-term breathing and heart rate trending |
|
||||
| RVF Parser | [`rvf.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/rvf.rs) | RVF container format parsing |
|
||||
|
||||
**Vendor-integrated modules** (24 modules, ADR-041 Category 7):
|
||||
|
||||
@@ -606,128 +565,128 @@ All 60 modules are implemented, tested (609 tests passing), and ready to deploy.
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Flash Attention | [`sig_flash_attention.rs`](v2/crates/wifi-densepose-wasm-edge/src/sig_flash_attention.rs) | Tiled attention over 8 subcarrier groups — finds spatial focus regions and entropy | S (<5ms) |
|
||||
| Coherence Gate | [`sig_coherence_gate.rs`](v2/crates/wifi-densepose-wasm-edge/src/sig_coherence_gate.rs) | Z-score phasor gating with hysteresis: Accept / PredictOnly / Reject / Recalibrate | L (<2ms) |
|
||||
| Temporal Compress | [`sig_temporal_compress.rs`](v2/crates/wifi-densepose-wasm-edge/src/sig_temporal_compress.rs) | 3-tier adaptive quantization (8-bit hot / 5-bit warm / 3-bit cold) | L (<2ms) |
|
||||
| Sparse Recovery | [`sig_sparse_recovery.rs`](v2/crates/wifi-densepose-wasm-edge/src/sig_sparse_recovery.rs) | ISTA L1 reconstruction for dropped subcarriers | H (<10ms) |
|
||||
| Person Match | [`sig_mincut_person_match.rs`](v2/crates/wifi-densepose-wasm-edge/src/sig_mincut_person_match.rs) | Hungarian-lite bipartite assignment for multi-person tracking | S (<5ms) |
|
||||
| Optimal Transport | [`sig_optimal_transport.rs`](v2/crates/wifi-densepose-wasm-edge/src/sig_optimal_transport.rs) | Sliced Wasserstein-1 distance with 4 projections | L (<2ms) |
|
||||
| Flash Attention | [`sig_flash_attention.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sig_flash_attention.rs) | Tiled attention over 8 subcarrier groups — finds spatial focus regions and entropy | S (<5ms) |
|
||||
| Coherence Gate | [`sig_coherence_gate.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sig_coherence_gate.rs) | Z-score phasor gating with hysteresis: Accept / PredictOnly / Reject / Recalibrate | L (<2ms) |
|
||||
| Temporal Compress | [`sig_temporal_compress.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sig_temporal_compress.rs) | 3-tier adaptive quantization (8-bit hot / 5-bit warm / 3-bit cold) | L (<2ms) |
|
||||
| Sparse Recovery | [`sig_sparse_recovery.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sig_sparse_recovery.rs) | ISTA L1 reconstruction for dropped subcarriers | H (<10ms) |
|
||||
| Person Match | [`sig_mincut_person_match.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sig_mincut_person_match.rs) | Hungarian-lite bipartite assignment for multi-person tracking | S (<5ms) |
|
||||
| Optimal Transport | [`sig_optimal_transport.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sig_optimal_transport.rs) | Sliced Wasserstein-1 distance with 4 projections | L (<2ms) |
|
||||
|
||||
**🧠 Adaptive Learning** — On-device learning without cloud connectivity
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| DTW Gesture Learn | [`lrn_dtw_gesture_learn.rs`](v2/crates/wifi-densepose-wasm-edge/src/lrn_dtw_gesture_learn.rs) | User-teachable gesture recognition — 3-rehearsal protocol, 16 templates | S (<5ms) |
|
||||
| Anomaly Attractor | [`lrn_anomaly_attractor.rs`](v2/crates/wifi-densepose-wasm-edge/src/lrn_anomaly_attractor.rs) | 4D dynamical system attractor classification with Lyapunov exponents | H (<10ms) |
|
||||
| Meta Adapt | [`lrn_meta_adapt.rs`](v2/crates/wifi-densepose-wasm-edge/src/lrn_meta_adapt.rs) | Hill-climbing self-optimization with safety rollback | L (<2ms) |
|
||||
| EWC Lifelong | [`lrn_ewc_lifelong.rs`](v2/crates/wifi-densepose-wasm-edge/src/lrn_ewc_lifelong.rs) | Elastic Weight Consolidation — remembers past tasks while learning new ones | S (<5ms) |
|
||||
| DTW Gesture Learn | [`lrn_dtw_gesture_learn.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/lrn_dtw_gesture_learn.rs) | User-teachable gesture recognition — 3-rehearsal protocol, 16 templates | S (<5ms) |
|
||||
| Anomaly Attractor | [`lrn_anomaly_attractor.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/lrn_anomaly_attractor.rs) | 4D dynamical system attractor classification with Lyapunov exponents | H (<10ms) |
|
||||
| Meta Adapt | [`lrn_meta_adapt.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/lrn_meta_adapt.rs) | Hill-climbing self-optimization with safety rollback | L (<2ms) |
|
||||
| EWC Lifelong | [`lrn_ewc_lifelong.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/lrn_ewc_lifelong.rs) | Elastic Weight Consolidation — remembers past tasks while learning new ones | S (<5ms) |
|
||||
|
||||
**🗺️ Spatial Reasoning** — Location, proximity, and influence mapping
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| PageRank Influence | [`spt_pagerank_influence.rs`](v2/crates/wifi-densepose-wasm-edge/src/spt_pagerank_influence.rs) | 4x4 cross-correlation graph with power iteration PageRank | L (<2ms) |
|
||||
| Micro HNSW | [`spt_micro_hnsw.rs`](v2/crates/wifi-densepose-wasm-edge/src/spt_micro_hnsw.rs) | 64-vector navigable small-world graph for nearest-neighbor search | S (<5ms) |
|
||||
| Spiking Tracker | [`spt_spiking_tracker.rs`](v2/crates/wifi-densepose-wasm-edge/src/spt_spiking_tracker.rs) | 32 LIF neurons + 4 output zone neurons with STDP learning | S (<5ms) |
|
||||
| PageRank Influence | [`spt_pagerank_influence.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/spt_pagerank_influence.rs) | 4x4 cross-correlation graph with power iteration PageRank | L (<2ms) |
|
||||
| Micro HNSW | [`spt_micro_hnsw.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/spt_micro_hnsw.rs) | 64-vector navigable small-world graph for nearest-neighbor search | S (<5ms) |
|
||||
| Spiking Tracker | [`spt_spiking_tracker.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/spt_spiking_tracker.rs) | 32 LIF neurons + 4 output zone neurons with STDP learning | S (<5ms) |
|
||||
|
||||
**⏱️ Temporal Analysis** — Activity patterns, logic verification, autonomous planning
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Pattern Sequence | [`tmp_pattern_sequence.rs`](v2/crates/wifi-densepose-wasm-edge/src/tmp_pattern_sequence.rs) | Activity routine detection and deviation alerts | S (<5ms) |
|
||||
| Temporal Logic Guard | [`tmp_temporal_logic_guard.rs`](v2/crates/wifi-densepose-wasm-edge/src/tmp_temporal_logic_guard.rs) | LTL formula verification on CSI event streams | S (<5ms) |
|
||||
| GOAP Autonomy | [`tmp_goap_autonomy.rs`](v2/crates/wifi-densepose-wasm-edge/src/tmp_goap_autonomy.rs) | Goal-Oriented Action Planning for autonomous module management | S (<5ms) |
|
||||
| Pattern Sequence | [`tmp_pattern_sequence.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/tmp_pattern_sequence.rs) | Activity routine detection and deviation alerts | S (<5ms) |
|
||||
| Temporal Logic Guard | [`tmp_temporal_logic_guard.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/tmp_temporal_logic_guard.rs) | LTL formula verification on CSI event streams | S (<5ms) |
|
||||
| GOAP Autonomy | [`tmp_goap_autonomy.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/tmp_goap_autonomy.rs) | Goal-Oriented Action Planning for autonomous module management | S (<5ms) |
|
||||
|
||||
**🛡️ AI Security** — Tamper detection and behavioral anomaly profiling
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Prompt Shield | [`ais_prompt_shield.rs`](v2/crates/wifi-densepose-wasm-edge/src/ais_prompt_shield.rs) | FNV-1a replay detection, injection detection (10x amplitude), jamming (SNR) | L (<2ms) |
|
||||
| Behavioral Profiler | [`ais_behavioral_profiler.rs`](v2/crates/wifi-densepose-wasm-edge/src/ais_behavioral_profiler.rs) | 6D behavioral profile with Mahalanobis anomaly scoring | S (<5ms) |
|
||||
| Prompt Shield | [`ais_prompt_shield.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ais_prompt_shield.rs) | FNV-1a replay detection, injection detection (10x amplitude), jamming (SNR) | L (<2ms) |
|
||||
| Behavioral Profiler | [`ais_behavioral_profiler.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ais_behavioral_profiler.rs) | 6D behavioral profile with Mahalanobis anomaly scoring | S (<5ms) |
|
||||
|
||||
**⚛️ Quantum-Inspired** — Quantum computing metaphors applied to CSI analysis
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Quantum Coherence | [`qnt_quantum_coherence.rs`](v2/crates/wifi-densepose-wasm-edge/src/qnt_quantum_coherence.rs) | Bloch sphere mapping, Von Neumann entropy, decoherence detection | S (<5ms) |
|
||||
| Interference Search | [`qnt_interference_search.rs`](v2/crates/wifi-densepose-wasm-edge/src/qnt_interference_search.rs) | 16 room-state hypotheses with Grover-inspired oracle + diffusion | S (<5ms) |
|
||||
| Quantum Coherence | [`qnt_quantum_coherence.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/qnt_quantum_coherence.rs) | Bloch sphere mapping, Von Neumann entropy, decoherence detection | S (<5ms) |
|
||||
| Interference Search | [`qnt_interference_search.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/qnt_interference_search.rs) | 16 room-state hypotheses with Grover-inspired oracle + diffusion | S (<5ms) |
|
||||
|
||||
**🤖 Autonomous Systems** — Self-governing and self-healing behaviors
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Psycho-Symbolic | [`aut_psycho_symbolic.rs`](v2/crates/wifi-densepose-wasm-edge/src/aut_psycho_symbolic.rs) | 16-rule forward-chaining knowledge base with contradiction detection | S (<5ms) |
|
||||
| Self-Healing Mesh | [`aut_self_healing_mesh.rs`](v2/crates/wifi-densepose-wasm-edge/src/aut_self_healing_mesh.rs) | 8-node mesh with health tracking, degradation/recovery, coverage healing | S (<5ms) |
|
||||
| Psycho-Symbolic | [`aut_psycho_symbolic.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/aut_psycho_symbolic.rs) | 16-rule forward-chaining knowledge base with contradiction detection | S (<5ms) |
|
||||
| Self-Healing Mesh | [`aut_self_healing_mesh.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/aut_self_healing_mesh.rs) | 8-node mesh with health tracking, degradation/recovery, coverage healing | S (<5ms) |
|
||||
|
||||
**🔮 Exotic (Vendor)** — Novel mathematical models for CSI interpretation
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Time Crystal | [`exo_time_crystal.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_time_crystal.rs) | Autocorrelation subharmonic detection in 256-frame history | S (<5ms) |
|
||||
| Hyperbolic Space | [`exo_hyperbolic_space.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_hyperbolic_space.rs) | Poincare ball embedding with 32 reference locations, hyperbolic distance | S (<5ms) |
|
||||
| Time Crystal | [`exo_time_crystal.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_time_crystal.rs) | Autocorrelation subharmonic detection in 256-frame history | S (<5ms) |
|
||||
| Hyperbolic Space | [`exo_hyperbolic_space.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_hyperbolic_space.rs) | Poincare ball embedding with 32 reference locations, hyperbolic distance | S (<5ms) |
|
||||
|
||||
**🏥 Medical & Health** (Category 1) — Contactless health monitoring
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Sleep Apnea | [`med_sleep_apnea.rs`](v2/crates/wifi-densepose-wasm-edge/src/med_sleep_apnea.rs) | Detects breathing pauses during sleep | S (<5ms) |
|
||||
| Cardiac Arrhythmia | [`med_cardiac_arrhythmia.rs`](v2/crates/wifi-densepose-wasm-edge/src/med_cardiac_arrhythmia.rs) | Monitors heart rate for irregular rhythms | S (<5ms) |
|
||||
| Respiratory Distress | [`med_respiratory_distress.rs`](v2/crates/wifi-densepose-wasm-edge/src/med_respiratory_distress.rs) | Alerts on abnormal breathing patterns | S (<5ms) |
|
||||
| Gait Analysis | [`med_gait_analysis.rs`](v2/crates/wifi-densepose-wasm-edge/src/med_gait_analysis.rs) | Tracks walking patterns and detects changes | S (<5ms) |
|
||||
| Seizure Detection | [`med_seizure_detect.rs`](v2/crates/wifi-densepose-wasm-edge/src/med_seizure_detect.rs) | 6-state machine for tonic-clonic seizure recognition | S (<5ms) |
|
||||
| Sleep Apnea | [`med_sleep_apnea.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/med_sleep_apnea.rs) | Detects breathing pauses during sleep | S (<5ms) |
|
||||
| Cardiac Arrhythmia | [`med_cardiac_arrhythmia.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/med_cardiac_arrhythmia.rs) | Monitors heart rate for irregular rhythms | S (<5ms) |
|
||||
| Respiratory Distress | [`med_respiratory_distress.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/med_respiratory_distress.rs) | Alerts on abnormal breathing patterns | S (<5ms) |
|
||||
| Gait Analysis | [`med_gait_analysis.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/med_gait_analysis.rs) | Tracks walking patterns and detects changes | S (<5ms) |
|
||||
| Seizure Detection | [`med_seizure_detect.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/med_seizure_detect.rs) | 6-state machine for tonic-clonic seizure recognition | S (<5ms) |
|
||||
|
||||
**🔐 Security & Safety** (Category 2) — Perimeter and threat detection
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Perimeter Breach | [`sec_perimeter_breach.rs`](v2/crates/wifi-densepose-wasm-edge/src/sec_perimeter_breach.rs) | Detects boundary crossings with approach/departure | S (<5ms) |
|
||||
| Weapon Detection | [`sec_weapon_detect.rs`](v2/crates/wifi-densepose-wasm-edge/src/sec_weapon_detect.rs) | Metal anomaly detection via CSI amplitude shifts | S (<5ms) |
|
||||
| Tailgating | [`sec_tailgating.rs`](v2/crates/wifi-densepose-wasm-edge/src/sec_tailgating.rs) | Detects unauthorized follow-through at access points | S (<5ms) |
|
||||
| Loitering | [`sec_loitering.rs`](v2/crates/wifi-densepose-wasm-edge/src/sec_loitering.rs) | Alerts when someone lingers too long in a zone | S (<5ms) |
|
||||
| Panic Motion | [`sec_panic_motion.rs`](v2/crates/wifi-densepose-wasm-edge/src/sec_panic_motion.rs) | Detects fleeing, struggling, or panic movement | S (<5ms) |
|
||||
| Perimeter Breach | [`sec_perimeter_breach.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sec_perimeter_breach.rs) | Detects boundary crossings with approach/departure | S (<5ms) |
|
||||
| Weapon Detection | [`sec_weapon_detect.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sec_weapon_detect.rs) | Metal anomaly detection via CSI amplitude shifts | S (<5ms) |
|
||||
| Tailgating | [`sec_tailgating.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sec_tailgating.rs) | Detects unauthorized follow-through at access points | S (<5ms) |
|
||||
| Loitering | [`sec_loitering.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sec_loitering.rs) | Alerts when someone lingers too long in a zone | S (<5ms) |
|
||||
| Panic Motion | [`sec_panic_motion.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/sec_panic_motion.rs) | Detects fleeing, struggling, or panic movement | S (<5ms) |
|
||||
|
||||
**🏢 Smart Building** (Category 3) — Automation and energy efficiency
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| HVAC Presence | [`bld_hvac_presence.rs`](v2/crates/wifi-densepose-wasm-edge/src/bld_hvac_presence.rs) | Occupancy-driven HVAC control with departure countdown | S (<5ms) |
|
||||
| Lighting Zones | [`bld_lighting_zones.rs`](v2/crates/wifi-densepose-wasm-edge/src/bld_lighting_zones.rs) | Auto-dim/off lighting based on zone activity | S (<5ms) |
|
||||
| Elevator Count | [`bld_elevator_count.rs`](v2/crates/wifi-densepose-wasm-edge/src/bld_elevator_count.rs) | Counts people entering/leaving with overload warning | S (<5ms) |
|
||||
| Meeting Room | [`bld_meeting_room.rs`](v2/crates/wifi-densepose-wasm-edge/src/bld_meeting_room.rs) | Tracks meeting lifecycle: start, headcount, end, availability | S (<5ms) |
|
||||
| Energy Audit | [`bld_energy_audit.rs`](v2/crates/wifi-densepose-wasm-edge/src/bld_energy_audit.rs) | Tracks after-hours usage and room utilization rates | S (<5ms) |
|
||||
| HVAC Presence | [`bld_hvac_presence.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/bld_hvac_presence.rs) | Occupancy-driven HVAC control with departure countdown | S (<5ms) |
|
||||
| Lighting Zones | [`bld_lighting_zones.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/bld_lighting_zones.rs) | Auto-dim/off lighting based on zone activity | S (<5ms) |
|
||||
| Elevator Count | [`bld_elevator_count.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/bld_elevator_count.rs) | Counts people entering/leaving with overload warning | S (<5ms) |
|
||||
| Meeting Room | [`bld_meeting_room.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/bld_meeting_room.rs) | Tracks meeting lifecycle: start, headcount, end, availability | S (<5ms) |
|
||||
| Energy Audit | [`bld_energy_audit.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/bld_energy_audit.rs) | Tracks after-hours usage and room utilization rates | S (<5ms) |
|
||||
|
||||
**🛒 Retail & Hospitality** (Category 4) — Customer insights without cameras
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Queue Length | [`ret_queue_length.rs`](v2/crates/wifi-densepose-wasm-edge/src/ret_queue_length.rs) | Estimates queue size and wait times | S (<5ms) |
|
||||
| Dwell Heatmap | [`ret_dwell_heatmap.rs`](v2/crates/wifi-densepose-wasm-edge/src/ret_dwell_heatmap.rs) | Shows where people spend time (hot/cold zones) | S (<5ms) |
|
||||
| Customer Flow | [`ret_customer_flow.rs`](v2/crates/wifi-densepose-wasm-edge/src/ret_customer_flow.rs) | Counts ins/outs and tracks net occupancy | S (<5ms) |
|
||||
| Table Turnover | [`ret_table_turnover.rs`](v2/crates/wifi-densepose-wasm-edge/src/ret_table_turnover.rs) | Restaurant table lifecycle: seated, dining, vacated | S (<5ms) |
|
||||
| Shelf Engagement | [`ret_shelf_engagement.rs`](v2/crates/wifi-densepose-wasm-edge/src/ret_shelf_engagement.rs) | Detects browsing, considering, and reaching for products | S (<5ms) |
|
||||
| Queue Length | [`ret_queue_length.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ret_queue_length.rs) | Estimates queue size and wait times | S (<5ms) |
|
||||
| Dwell Heatmap | [`ret_dwell_heatmap.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ret_dwell_heatmap.rs) | Shows where people spend time (hot/cold zones) | S (<5ms) |
|
||||
| Customer Flow | [`ret_customer_flow.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ret_customer_flow.rs) | Counts ins/outs and tracks net occupancy | S (<5ms) |
|
||||
| Table Turnover | [`ret_table_turnover.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ret_table_turnover.rs) | Restaurant table lifecycle: seated, dining, vacated | S (<5ms) |
|
||||
| Shelf Engagement | [`ret_shelf_engagement.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ret_shelf_engagement.rs) | Detects browsing, considering, and reaching for products | S (<5ms) |
|
||||
|
||||
**🏭 Industrial & Specialized** (Category 5) — Safety and compliance
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Forklift Proximity | [`ind_forklift_proximity.rs`](v2/crates/wifi-densepose-wasm-edge/src/ind_forklift_proximity.rs) | Warns when people get too close to vehicles | S (<5ms) |
|
||||
| Confined Space | [`ind_confined_space.rs`](v2/crates/wifi-densepose-wasm-edge/src/ind_confined_space.rs) | OSHA-compliant worker monitoring with extraction alerts | S (<5ms) |
|
||||
| Clean Room | [`ind_clean_room.rs`](v2/crates/wifi-densepose-wasm-edge/src/ind_clean_room.rs) | Occupancy limits and turbulent motion detection | S (<5ms) |
|
||||
| Livestock Monitor | [`ind_livestock_monitor.rs`](v2/crates/wifi-densepose-wasm-edge/src/ind_livestock_monitor.rs) | Animal presence, stillness, and escape alerts | S (<5ms) |
|
||||
| Structural Vibration | [`ind_structural_vibration.rs`](v2/crates/wifi-densepose-wasm-edge/src/ind_structural_vibration.rs) | Seismic events, mechanical resonance, structural drift | S (<5ms) |
|
||||
| Forklift Proximity | [`ind_forklift_proximity.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ind_forklift_proximity.rs) | Warns when people get too close to vehicles | S (<5ms) |
|
||||
| Confined Space | [`ind_confined_space.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ind_confined_space.rs) | OSHA-compliant worker monitoring with extraction alerts | S (<5ms) |
|
||||
| Clean Room | [`ind_clean_room.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ind_clean_room.rs) | Occupancy limits and turbulent motion detection | S (<5ms) |
|
||||
| Livestock Monitor | [`ind_livestock_monitor.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ind_livestock_monitor.rs) | Animal presence, stillness, and escape alerts | S (<5ms) |
|
||||
| Structural Vibration | [`ind_structural_vibration.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/ind_structural_vibration.rs) | Seismic events, mechanical resonance, structural drift | S (<5ms) |
|
||||
|
||||
**🔮 Exotic & Research** (Category 6) — Experimental sensing applications
|
||||
|
||||
| Module | File | What It Does | Budget |
|
||||
|--------|------|-------------|--------|
|
||||
| Dream Stage | [`exo_dream_stage.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_dream_stage.rs) | Contactless sleep stage classification (wake/light/deep/REM) | S (<5ms) |
|
||||
| Emotion Detection | [`exo_emotion_detect.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_emotion_detect.rs) | Arousal, stress, and calm detection from micro-movements | S (<5ms) |
|
||||
| Gesture Language | [`exo_gesture_language.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_gesture_language.rs) | Sign language letter recognition via WiFi | S (<5ms) |
|
||||
| Music Conductor | [`exo_music_conductor.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_music_conductor.rs) | Tempo and dynamic tracking from conducting gestures | S (<5ms) |
|
||||
| Plant Growth | [`exo_plant_growth.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_plant_growth.rs) | Monitors plant growth, circadian rhythms, wilt detection | S (<5ms) |
|
||||
| Ghost Hunter | [`exo_ghost_hunter.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_ghost_hunter.rs) | Environmental anomaly classification (draft/insect/wind/unknown) | S (<5ms) |
|
||||
| Rain Detection | [`exo_rain_detect.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_rain_detect.rs) | Detects rain onset, intensity, and cessation via signal scatter | S (<5ms) |
|
||||
| Breathing Sync | [`exo_breathing_sync.rs`](v2/crates/wifi-densepose-wasm-edge/src/exo_breathing_sync.rs) | Detects synchronized breathing between multiple people | S (<5ms) |
|
||||
| Dream Stage | [`exo_dream_stage.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_dream_stage.rs) | Contactless sleep stage classification (wake/light/deep/REM) | S (<5ms) |
|
||||
| Emotion Detection | [`exo_emotion_detect.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_emotion_detect.rs) | Arousal, stress, and calm detection from micro-movements | S (<5ms) |
|
||||
| Gesture Language | [`exo_gesture_language.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_gesture_language.rs) | Sign language letter recognition via WiFi | S (<5ms) |
|
||||
| Music Conductor | [`exo_music_conductor.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_music_conductor.rs) | Tempo and dynamic tracking from conducting gestures | S (<5ms) |
|
||||
| Plant Growth | [`exo_plant_growth.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_plant_growth.rs) | Monitors plant growth, circadian rhythms, wilt detection | S (<5ms) |
|
||||
| Ghost Hunter | [`exo_ghost_hunter.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_ghost_hunter.rs) | Environmental anomaly classification (draft/insect/wind/unknown) | S (<5ms) |
|
||||
| Rain Detection | [`exo_rain_detect.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_rain_detect.rs) | Detects rain onset, intensity, and cessation via signal scatter | S (<5ms) |
|
||||
| Breathing Sync | [`exo_breathing_sync.rs`](rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_breathing_sync.rs) | Detects synchronized breathing between multiple people | S (<5ms) |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -855,7 +814,7 @@ git clone https://github.com/ruvnet/RuView.git
|
||||
cd RuView
|
||||
|
||||
# Rust (primary — 810x faster)
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo build --release
|
||||
cargo test --workspace
|
||||
|
||||
@@ -945,12 +904,10 @@ cargo add wifi-densepose-ruvector # RuVector v2.0.4 integration layer (ADR-017
|
||||
| [`wifi-densepose-api`](https://crates.io/crates/wifi-densepose-api) | REST + WebSocket API layer | -- | [](https://crates.io/crates/wifi-densepose-api) |
|
||||
| [`wifi-densepose-config`](https://crates.io/crates/wifi-densepose-config) | Configuration management | -- | [](https://crates.io/crates/wifi-densepose-config) |
|
||||
| [`wifi-densepose-db`](https://crates.io/crates/wifi-densepose-db) | Database persistence (PostgreSQL, SQLite, Redis) | -- | [](https://crates.io/crates/wifi-densepose-db) |
|
||||
| `wifi-densepose-pointcloud` | Real-time dense point cloud from camera + WiFi CSI fusion (Three.js viewer, brain bridge). Workspace-only for now. | -- | — |
|
||||
| `wifi-densepose-geo` | Geospatial context (Sentinel-2 tiles, SRTM elevation, OSM, weather, night-mode). Workspace-only for now. | -- | — |
|
||||
|
||||
All crates integrate with [RuVector v2.0.4](https://github.com/ruvnet/ruvector) — see [AI Backbone](#ai-backbone-ruvector) below.
|
||||
|
||||
**[rUv Neural](v2/crates/ruv-neural/)** — A separate 12-crate workspace for brain network topology analysis, neural decoding, and medical sensing. See [rUv Neural](#ruv-neural) in Models & Training.
|
||||
**[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>
|
||||
|
||||
@@ -1050,7 +1007,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](v2/crates/ruv-neural/README.md) |
|
||||
| [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) |
|
||||
@@ -1168,10 +1125,10 @@ Bundle verify: 7/7 checks PASS
|
||||
**Verify it yourself** (no hardware needed):
|
||||
```bash
|
||||
# Run all tests
|
||||
cd v2 && cargo test --workspace --no-default-features
|
||||
cd rust-port/wifi-densepose-rs && cargo test --workspace --no-default-features
|
||||
|
||||
# Run the deterministic proof
|
||||
python archive/v1/data/proof/verify.py
|
||||
python v1/data/proof/verify.py
|
||||
|
||||
# Generate + verify the witness bundle
|
||||
bash scripts/generate-witness-bundle.sh
|
||||
@@ -1484,7 +1441,7 @@ See [firmware/esp32-csi-node/README.md](firmware/esp32-csi-node/README.md), [ADR
|
||||
| WASM Support | No | Yes |
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo build --release
|
||||
cargo test --workspace
|
||||
cargo bench --package wifi-densepose-signal
|
||||
@@ -1781,7 +1738,7 @@ The full RuVector ecosystem includes 90+ crates. See [github.com/ruvnet/ruvector
|
||||
<details>
|
||||
<summary><a id="ruv-neural"></a><strong>🧠 rUv Neural</strong> — Brain topology analysis ecosystem for neural decoding and medical sensing</summary>
|
||||
|
||||
[**rUv Neural**](v2/crates/ruv-neural/README.md) is a 12-crate Rust ecosystem that extends RuView's signal processing into brain network topology analysis. It transforms neural magnetic field measurements from quantum sensors (NV diamond magnetometers, optically pumped magnetometers) into dynamic connectivity graphs, using minimum cut algorithms to detect cognitive state transitions in real time. The ecosystem includes crates for signal processing (`ruv-neural-signal`), graph construction (`ruv-neural-graph`), HNSW-indexed pattern memory (`ruv-neural-memory`), graph embeddings (`ruv-neural-embed`), cognitive state decoding (`ruv-neural-decoder`), and ESP32/WASM edge targets. Medical and research applications include early neurological disease detection via topology signatures, brain-computer interfaces, clinical neurofeedback, and non-invasive biomedical sensing -- bridging RuView's RF sensing architecture with the emerging field of quantum biomedical diagnostics.
|
||||
[**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>
|
||||
|
||||
@@ -2154,7 +2111,7 @@ wifi-densepose tasks list # List background tasks
|
||||
|
||||
```bash
|
||||
# Rust tests (primary — 542+ tests)
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo test --workspace
|
||||
|
||||
# Sensing server tests (229 tests)
|
||||
@@ -2164,7 +2121,7 @@ cargo test -p wifi-densepose-sensing-server
|
||||
./target/release/sensing-server --benchmark
|
||||
|
||||
# Python tests
|
||||
python -m pytest archive/v1/tests/ -v
|
||||
python -m pytest v1/tests/ -v
|
||||
|
||||
# Pipeline verification (no hardware needed)
|
||||
./verify
|
||||
@@ -2258,7 +2215,7 @@ git clone https://github.com/ruvnet/RuView.git
|
||||
cd RuView
|
||||
|
||||
# Rust development
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo build --release
|
||||
cargo test --workspace
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
# Archive
|
||||
|
||||
Frozen, no-longer-active components of RuView preserved for historical
|
||||
reference, reproducibility, and load-bearing legacy paths the active
|
||||
codebase still depends on.
|
||||
|
||||
## What lives here
|
||||
|
||||
| Path | What it is | Why it's archived | Still load-bearing? |
|
||||
|------|------------|-------------------|---------------------|
|
||||
| `v1/` | Original Python implementation of RuView (CSI processing, hardware adapters, services, FastAPI) | Superseded by the Rust workspace at `v2/`; ~810× slower in benchmarks. Kept rather than deleted because the deterministic proof bundle (`v1/data/proof/`) is part of the pre-merge witness verification process per ADR-011 / ADR-028. | **Yes — for the proof bundle only.** Active code lives in `v2/`. |
|
||||
|
||||
## What "archived" means
|
||||
|
||||
- **Do not add new features here.** New work goes in `v2/`.
|
||||
- **Do not refactor or modernize the archived code beyond what is
|
||||
strictly necessary** to keep the load-bearing paths working. The
|
||||
Python proof bundle is intentionally frozen so that its SHA-256
|
||||
reproducibility holds across releases (per ADR-028's witness
|
||||
verification requirement).
|
||||
- **Bug fixes inside archived code are allowed** when the bug affects a
|
||||
still-load-bearing path (currently: only the Python proof). All
|
||||
other "bugs" in archived code are out-of-scope — they are part of
|
||||
the historical record and any fix would unnecessarily churn the
|
||||
witness hashes.
|
||||
- **CI continues to verify the load-bearing paths.**
|
||||
`.github/workflows/verify-pipeline.yml` runs the Python proof on
|
||||
every push and PR; if you change anything inside `archive/v1/src/`
|
||||
or `archive/v1/data/proof/`, expect the determinism check to flag
|
||||
it.
|
||||
|
||||
## Quick reference for the load-bearing paths
|
||||
|
||||
```bash
|
||||
# Run the deterministic Python proof (must print VERDICT: PASS)
|
||||
python archive/v1/data/proof/verify.py
|
||||
|
||||
# Regenerate the expected hash (only if numpy/scipy version legitimately changed)
|
||||
python archive/v1/data/proof/verify.py --generate-hash
|
||||
|
||||
# Run the full Python test suite (legacy, still maintained)
|
||||
cd archive/v1&& python -m pytest tests/ -x -q
|
||||
```
|
||||
|
||||
## Why we keep `v1/` rather than delete it
|
||||
|
||||
1. **Trust kill-switch.** The proof at `v1/data/proof/verify.py` feeds
|
||||
a known reference signal through the full pipeline and hashes the
|
||||
output. If the active code's behavior drifts, the hash changes and
|
||||
CI fails. This is what stops accidental regression in the science
|
||||
layer of the codebase.
|
||||
|
||||
2. **Witness verification.** ADR-028's witness-bundle process bundles
|
||||
the proof, the rust workspace test results, and firmware hashes
|
||||
into a tarball recipients can self-verify. Removing v1 would break
|
||||
that chain.
|
||||
|
||||
3. **Historical reference.** ADR-011 documents the "no mocks in
|
||||
production code" decision; the original violations and their fixes
|
||||
live in this Python codebase. The ADRs reference these paths.
|
||||
|
||||
If the time comes to retire the proof bundle (e.g., a Rust port of
|
||||
the proof exists and the Python version is no longer canonical), the
|
||||
right move is a single follow-up that simultaneously: ports the
|
||||
witness-bundle process, updates `verify-pipeline.yml`, and either
|
||||
deletes `archive/v1/` or moves it to a separate read-only repository.
|
||||
That decision belongs in its own ADR.
|
||||
|
||||
## See also
|
||||
|
||||
- `docs/adr/ADR-011-python-proof-of-reality-mock-elimination.md`
|
||||
- `docs/adr/ADR-028-esp32-capability-audit.md`
|
||||
- `archive/v1/data/proof/README.md` (if present)
|
||||
- `docs/WITNESS-LOG-028.md`
|
||||
@@ -10,16 +10,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies
|
||||
COPY archive/v1/requirements-lock.txt /app/requirements.txt
|
||||
COPY v1/requirements-lock.txt /app/requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt \
|
||||
&& pip install --no-cache-dir websockets uvicorn fastapi
|
||||
|
||||
# Copy application code
|
||||
COPY archive/v1/ /app/v1/
|
||||
COPY v1/ /app/v1/
|
||||
COPY ui/ /app/ui/
|
||||
|
||||
# Copy sensing modules
|
||||
COPY archive/v1/src/sensing/ /app/v1/src/sensing/
|
||||
COPY v1/src/sensing/ /app/v1/src/sensing/
|
||||
|
||||
EXPOSE 8765
|
||||
EXPOSE 8080
|
||||
|
||||
+6
-14
@@ -8,8 +8,8 @@ FROM rust:1.85-bookworm AS builder
|
||||
WORKDIR /build
|
||||
|
||||
# Copy workspace files
|
||||
COPY v2/Cargo.toml v2/Cargo.lock ./
|
||||
COPY v2/crates/ ./crates/
|
||||
COPY rust-port/wifi-densepose-rs/Cargo.toml rust-port/wifi-densepose-rs/Cargo.lock ./
|
||||
COPY rust-port/wifi-densepose-rs/crates/ ./crates/
|
||||
|
||||
# Copy vendored RuVector crates
|
||||
COPY vendor/ruvector/ /build/vendor/ruvector/
|
||||
@@ -50,15 +50,7 @@ ENV RUST_LOG=info
|
||||
# Override at runtime: docker run -e CSI_SOURCE=esp32 ...
|
||||
ENV CSI_SOURCE=auto
|
||||
|
||||
# MODELS_DIR controls where the server scans for .rvf model files.
|
||||
# Mount a host directory here to make models visible to the API:
|
||||
# docker run -v /path/to/models:/app/models -e MODELS_DIR=/app/models ...
|
||||
ENV MODELS_DIR=data/models
|
||||
|
||||
COPY docker/docker-entrypoint.sh /app/docker-entrypoint.sh
|
||||
|
||||
# Exec-form ENTRYPOINT so Docker appends user arguments correctly.
|
||||
# Pass flags directly: docker run <image> --source esp32 --tick-ms 500
|
||||
# Or use env vars: docker run -e CSI_SOURCE=esp32 <image>
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
CMD []
|
||||
ENTRYPOINT ["/bin/sh", "-c"]
|
||||
# Shell-form CMD allows $CSI_SOURCE to be substituted at container start.
|
||||
# The ENV default above (CSI_SOURCE=auto) applies when the variable is unset.
|
||||
CMD ["/app/sensing-server --source ${CSI_SOURCE} --tick-ms 100 --ui-path /app/ui --http-port 3000 --ws-port 3001"]
|
||||
|
||||
@@ -18,13 +18,8 @@ services:
|
||||
# wifi — use host Wi-Fi RSSI/scan data (Windows netsh)
|
||||
# simulated — generate synthetic CSI data (no hardware required)
|
||||
- CSI_SOURCE=${CSI_SOURCE:-auto}
|
||||
# MODELS_DIR controls where the server scans for .rvf model files.
|
||||
# Mount a host directory and set this to make models visible:
|
||||
# volumes: ["/path/to/models:/app/models"]
|
||||
# MODELS_DIR=/app/models
|
||||
- MODELS_DIR=${MODELS_DIR:-data/models}
|
||||
# No explicit command needed — docker-entrypoint.sh uses CSI_SOURCE.
|
||||
# Override with: command: ["--source", "esp32", "--tick-ms", "500"]
|
||||
# command is passed as arguments to ENTRYPOINT (/bin/sh -c), so $CSI_SOURCE is expanded by the shell.
|
||||
command: ["/app/sensing-server --source ${CSI_SOURCE:-auto} --tick-ms 100 --ui-path /app/ui --http-port 3000 --ws-port 3001"]
|
||||
|
||||
python-sensing:
|
||||
build:
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Docker entrypoint for WiFi-DensePose sensing server.
|
||||
#
|
||||
# Supports two usage patterns:
|
||||
#
|
||||
# 1. No arguments — use defaults from environment:
|
||||
# docker run -e CSI_SOURCE=esp32 ruvnet/wifi-densepose:latest
|
||||
#
|
||||
# 2. Pass CLI flags directly:
|
||||
# docker run ruvnet/wifi-densepose:latest --source esp32 --tick-ms 500
|
||||
# docker run ruvnet/wifi-densepose:latest --model /app/models/my.rvf
|
||||
#
|
||||
# Environment variables:
|
||||
# CSI_SOURCE — data source: auto (default), esp32, wifi, simulated
|
||||
# MODELS_DIR — directory to scan for .rvf model files (default: data/models)
|
||||
set -e
|
||||
|
||||
# If the first argument looks like a flag (starts with -), prepend the
|
||||
# server binary so users can just pass flags:
|
||||
# docker run <image> --source esp32 --tick-ms 500
|
||||
if [ "${1#-}" != "$1" ] || [ -z "$1" ]; then
|
||||
set -- /app/sensing-server \
|
||||
--source "${CSI_SOURCE:-auto}" \
|
||||
--tick-ms 100 \
|
||||
--ui-path /app/ui \
|
||||
--http-port 3000 \
|
||||
--ws-port 3001 \
|
||||
--bind-addr 0.0.0.0 \
|
||||
"$@"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
@@ -1,111 +0,0 @@
|
||||
# RuView Troubleshooting Guide
|
||||
|
||||
Known issues and fixes from the rebase-to-upstream branch (upstream #301).
|
||||
|
||||
---
|
||||
|
||||
## 1. Node not appearing in /api/v1/nodes
|
||||
|
||||
**Symptom:** ESP32-S3 node associates with WiFi, LED blinks, but no CSI frames arrive at the server. Node missing from `/api/v1/spatial/nodes`.
|
||||
|
||||
**Root cause:** After USB flash, the node enters a limping state where WiFi associates but the UDP CSI sender silently fails. The SoftAP + mDNS stack initializes but the CSI callback never fires.
|
||||
|
||||
**Fix:** Power cycle the node (unplug USB, wait 2s, replug). If that doesn't work, send DTR reset via serial: `python -m serial.tools.miniterm --dtr 0 COMx 115200` then Ctrl+C.
|
||||
|
||||
**Prevention:** Firmware 0.8.0+ includes a watchdog that detects zero CSI frames for 30s and triggers a software reset automatically. Nodes 1-10 are still on old firmware and lack this recovery (OTA-vs-BLE chicken-and-egg; see issue #6).
|
||||
|
||||
---
|
||||
|
||||
## 2. Person count stuck at 1
|
||||
|
||||
**Symptom:** `estimated_persons` always returns 1 regardless of how many people are in the room.
|
||||
|
||||
**Root cause (ADR-044):** Eight converging bugs:
|
||||
1. `score_to_person_count` had a ceiling of 3
|
||||
2. `fuse_multi_node_features` used `.max()` instead of sum — N identical readings collapsed to 1
|
||||
3. Four `.max(1)` clamps forced minimum count to 1 even when absent
|
||||
4. `field_model.estimate_occupancy` capped at `.min(3)`
|
||||
5. Normalization saturated (dividing by hardcoded thresholds instead of adaptive p95)
|
||||
6. No field model auto-calibration — eigenvalue path never activated
|
||||
7. Vitals-path clamps were asymmetric
|
||||
8. Tomography produced one blob (CC=1) so dedup gave wrong count
|
||||
|
||||
**Fix applied (Waves 1-3):**
|
||||
- Wave 1 (`9cc5f604`): ceiling 3→10, `.max()` → sum/3 aggregation, softened `.max(1)` clamps
|
||||
- Wave 2 (`306f1262`): RollingP95 adaptive normalization, field_model 30s auto-calibration, vitals clamp symmetry
|
||||
- Wave 3 (`c3df375a`+`0d4bfb09`+`6ac70ddf`): CC flood-fill infrastructure, lambda 0.1→5.0, threshold 0.01→0.15, CC>1 gate
|
||||
|
||||
**Current state:** `estimated_persons` = 6-8 for 5 bodies (3 humans + 2 dogs). Overcounts because the sum/3 dedup factor is a guess. Tomography still produces one blob (CC=1), so the CC path doesn't activate. Runtime-configurable lambda would help tune without redeployment.
|
||||
|
||||
---
|
||||
|
||||
## 3. Heart rate / breathing rate jitter
|
||||
|
||||
**Symptom:** HR and BR readings jump wildly between frames. BR CV was 23.3%, HR CV was 12.9%.
|
||||
|
||||
**Root cause (ADR-045):** 11 ESP32 nodes each compute independent vitals. The server used last-write-wins — whichever node's UDP packet arrived last overwrote the global vitals. At ~20 fps per node, this meant vitals randomly interleaved from different vantage points every 50ms.
|
||||
|
||||
**Fix applied (`46fbc061`):** Best-node selection. Each node's vitals are smoothed independently via median filter + EMA. The node with the highest combined `breathing_confidence + heartbeat_confidence` is selected as authoritative. Result: BR CV 23.3% → 12.6%, HR CV 12.9% → 11.6%.
|
||||
|
||||
**Known limitation:** The `wifi-densepose-vitals` crate has a superior 4-stage pipeline (bandpass → Hilbert envelope → autocorrelation → peak detection) but is not yet wired into the sensing server. The current `VitalSignDetector` uses a simpler FFT approach with 4 BPM frequency resolution.
|
||||
|
||||
---
|
||||
|
||||
## 4. Signal quality shows 50% always
|
||||
|
||||
**Symptom:** The dashboard signal quality gauge was always stuck at ~50%.
|
||||
|
||||
**Root cause:** Signal quality was a hardcoded placeholder value, not derived from actual CSI data.
|
||||
|
||||
**Fix applied:** ADR-044 Wave 2 replaced the fake gauge with RollingP95 adaptive normalization. The UI honesty pass (`b2070ab4`) added beta tags to unvalidated metrics, replaced the fake gauge with per-node pill indicators, and surfaced the actual per-node signal data.
|
||||
|
||||
---
|
||||
|
||||
## 5. Dashboard freezes every 2-4 seconds
|
||||
|
||||
**Symptom:** The spatial view and dashboard would freeze, then reconnect, creating a visible stutter every 2-4 seconds.
|
||||
|
||||
**Root cause:** The WebSocket broadcast channel's `recv()` returned `Err(Lagged)` when a client fell behind. The server treated this as a fatal error and dropped the connection. The client immediately reconnected, creating a connect/disconnect cycle.
|
||||
|
||||
**Fix applied (`581daf4f`):**
|
||||
- Server: `Lagged` error → `continue` (skip missed frames instead of disconnecting)
|
||||
- Server: 30s ping/pong keepalive to prevent Caddy proxy idle timeouts
|
||||
- Result: 154 frames over 8 seconds sustained, zero disconnects
|
||||
|
||||
---
|
||||
|
||||
## 6. OTA update crashes at 59%
|
||||
|
||||
**Symptom:** OTA firmware update via `/api/v1/firmware/download` progresses to ~59% then the node crashes with `StoreProhibited` on Core 1.
|
||||
|
||||
**Root cause:** NimBLE BLE advertising/scanning runs on Core 1. During OTA, the HTTP client also runs on Core 1. BLE and OTA compete for stack space, and the BLE scan callback triggers a memory access violation during the OTA write.
|
||||
|
||||
**Fix:**
|
||||
1. Stop NimBLE advertising and scanning before calling `esp_https_ota_begin()`
|
||||
2. Increase httpd stack from 4KB to 8KB (`CONFIG_HTTPD_MAX_REQ_HDR_LEN` and task stack)
|
||||
3. Resume BLE after OTA completes or fails
|
||||
|
||||
**Caveat:** Nodes running old firmware (1-10) can't receive this fix via OTA because the crash happens during the OTA itself. These nodes must be USB-flashed with firmware 0.8.0+ first, then future OTA updates will work. Node 11 was USB-flashed with the watchdog firmware and can receive OTA updates.
|
||||
|
||||
---
|
||||
|
||||
## 7. Can't SSH to babycube via LAN
|
||||
|
||||
**Symptom:** `ssh thyhack@10.0.10.10` hangs at banner exchange. Ping works, TCP port 22 is open, but SSH never completes the handshake.
|
||||
|
||||
**Workaround:** Use the Tailscale IP instead:
|
||||
```
|
||||
ssh thyhack@100.90.238.87
|
||||
```
|
||||
|
||||
**Not the cause:** CrowdSec. The 10.0.0.0/8 range is whitelisted in CrowdSec (`cscli decisions list` shows no active decisions for LAN IPs). The banner hang occurs before any authentication attempt, so it's not a firewall block.
|
||||
|
||||
**Suspected cause:** Unknown. Possibly MTU/fragmentation issue on the LAN segment, or a network stack bug in the babycube's NIC driver. The Tailscale overlay network (WireGuard UDP) bypasses whatever is causing the LAN TCP issue.
|
||||
|
||||
---
|
||||
|
||||
## 8. Right USB-C port doesn't work on some ESP32-S3 boards
|
||||
|
||||
**Symptom:** Plugging into the right USB-C port (when facing the board with USB-C toward you) shows no serial device on the host.
|
||||
|
||||
**Fix:** Use the left USB-C port. On most ESP32-S3-DevKitC boards, the left port is the USB-to-UART bridge (CP2102/CH340) used for flashing and serial monitor. The right port is the native USB (USB-JTAG) which requires different drivers and isn't used by the RuView firmware.
|
||||
@@ -35,7 +35,7 @@ git checkout 96b01008
|
||||
### Step 2: Rust Workspace — Full Test Suite
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo test --workspace --no-default-features
|
||||
```
|
||||
|
||||
@@ -89,7 +89,7 @@ ls firmware/esp32-csi-node/build/*.bin 2>/dev/null || echo "App binary in build/
|
||||
### Step 6: Verify ADR-018 Binary Frame Parser
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo test -p wifi-densepose-hardware --no-default-features
|
||||
```
|
||||
|
||||
@@ -133,7 +133,7 @@ cargo test -p wifi-densepose-train --no-default-features
|
||||
### Step 9: Verify Python Proof System
|
||||
|
||||
```bash
|
||||
python archive/v1/data/proof/verify.py
|
||||
python v1/data/proof/verify.py
|
||||
```
|
||||
|
||||
**Expected:** PASS (hash `8c0680d7...` matches `expected_features.sha256`).
|
||||
|
||||
@@ -216,4 +216,4 @@ full = ["mincut-matching", "attn-mincut", "temporal-compress", "solver-interpola
|
||||
- [Elastic Weight Consolidation](https://arxiv.org/abs/1612.00796)
|
||||
- [Raft Consensus](https://raft.github.io/raft.pdf)
|
||||
- [ML-DSA (FIPS 204)](https://csrc.nist.gov/pubs/fips/204/final)
|
||||
- [WiFi-DensePose Rust ADR-001: Workspace Structure](../v2/docs/adr/ADR-001-workspace-structure.md)
|
||||
- [WiFi-DensePose Rust ADR-001: Workspace Structure](../rust-port/wifi-densepose-rs/docs/adr/ADR-001-workspace-structure.md)
|
||||
|
||||
@@ -20,31 +20,31 @@ The following code paths produce fake data **in the default configuration** or a
|
||||
|
||||
| File | Line | Issue | Impact |
|
||||
|------|------|-------|--------|
|
||||
| `archive/v1/src/core/csi_processor.py` | 390 | `doppler_shift = np.random.rand(10) # Placeholder` | **Real feature extractor returns random Doppler** - kills credibility of entire feature pipeline |
|
||||
| `archive/v1/src/hardware/csi_extractor.py` | 83-84 | `amplitude = np.random.rand(...)` in CSI extraction fallback | Random data silently substituted when parsing fails |
|
||||
| `archive/v1/src/hardware/csi_extractor.py` | 129-135 | `_parse_atheros()` returns `np.random.rand()` with comment "placeholder implementation" | Named as if it parses real data, actually random |
|
||||
| `archive/v1/src/hardware/router_interface.py` | 211-212 | `np.random.rand(3, 56)` in fallback path | Silent random fallback |
|
||||
| `archive/v1/src/services/pose_service.py` | 431 | `mock_csi = np.random.randn(64, 56, 3) # Mock CSI data` | Mock CSI in production code path |
|
||||
| `archive/v1/src/services/pose_service.py` | 293-356 | `_generate_mock_poses()` with `random.randint` throughout | Entire mock pose generator in service layer |
|
||||
| `archive/v1/src/services/pose_service.py` | 489-607 | Multiple `random.randint` for occupancy, historical data | Fake statistics that look real in API responses |
|
||||
| `archive/v1/src/api/dependencies.py` | 82, 408 | "return a mock user for development" | Auth bypass in default path |
|
||||
| `v1/src/core/csi_processor.py` | 390 | `doppler_shift = np.random.rand(10) # Placeholder` | **Real feature extractor returns random Doppler** - kills credibility of entire feature pipeline |
|
||||
| `v1/src/hardware/csi_extractor.py` | 83-84 | `amplitude = np.random.rand(...)` in CSI extraction fallback | Random data silently substituted when parsing fails |
|
||||
| `v1/src/hardware/csi_extractor.py` | 129-135 | `_parse_atheros()` returns `np.random.rand()` with comment "placeholder implementation" | Named as if it parses real data, actually random |
|
||||
| `v1/src/hardware/router_interface.py` | 211-212 | `np.random.rand(3, 56)` in fallback path | Silent random fallback |
|
||||
| `v1/src/services/pose_service.py` | 431 | `mock_csi = np.random.randn(64, 56, 3) # Mock CSI data` | Mock CSI in production code path |
|
||||
| `v1/src/services/pose_service.py` | 293-356 | `_generate_mock_poses()` with `random.randint` throughout | Entire mock pose generator in service layer |
|
||||
| `v1/src/services/pose_service.py` | 489-607 | Multiple `random.randint` for occupancy, historical data | Fake statistics that look real in API responses |
|
||||
| `v1/src/api/dependencies.py` | 82, 408 | "return a mock user for development" | Auth bypass in default path |
|
||||
|
||||
#### Moderate Severity (mock gated behind flags but confusing)
|
||||
|
||||
| File | Line | Issue |
|
||||
|------|------|-------|
|
||||
| `archive/v1/src/config/settings.py` | 144-145 | `mock_hardware=False`, `mock_pose_data=False` defaults - correct, but mock infrastructure exists |
|
||||
| `archive/v1/src/core/router_interface.py` | 27-300 | 270+ lines of mock data generation infrastructure in production code |
|
||||
| `archive/v1/src/services/pose_service.py` | 84-88 | Silent conditional: `if not self.settings.mock_pose_data` with no logging of real-mode |
|
||||
| `archive/v1/src/services/hardware_service.py` | 72-375 | Interleaved mock/real paths throughout |
|
||||
| `v1/src/config/settings.py` | 144-145 | `mock_hardware=False`, `mock_pose_data=False` defaults - correct, but mock infrastructure exists |
|
||||
| `v1/src/core/router_interface.py` | 27-300 | 270+ lines of mock data generation infrastructure in production code |
|
||||
| `v1/src/services/pose_service.py` | 84-88 | Silent conditional: `if not self.settings.mock_pose_data` with no logging of real-mode |
|
||||
| `v1/src/services/hardware_service.py` | 72-375 | Interleaved mock/real paths throughout |
|
||||
|
||||
#### Low Severity (placeholders/TODOs)
|
||||
|
||||
| File | Line | Issue |
|
||||
|------|------|-------|
|
||||
| `archive/v1/src/core/router_interface.py` | 198 | "Collect real CSI data from router (placeholder implementation)" |
|
||||
| `archive/v1/src/api/routers/health.py` | 170-171 | `uptime_seconds = 0.0 # TODO` |
|
||||
| `archive/v1/src/services/pose_service.py` | 739 | `"uptime_seconds": 0.0 # TODO` |
|
||||
| `v1/src/core/router_interface.py` | 198 | "Collect real CSI data from router (placeholder implementation)" |
|
||||
| `v1/src/api/routers/health.py` | 170-171 | `uptime_seconds = 0.0 # TODO` |
|
||||
| `v1/src/services/pose_service.py` | 739 | `"uptime_seconds": 0.0 # TODO` |
|
||||
|
||||
### Root Cause Analysis
|
||||
|
||||
@@ -119,7 +119,7 @@ def _parse_atheros(self, raw_data: bytes) -> CSIData:
|
||||
**All mock code moves to a dedicated module. Default execution NEVER touches mock paths.**
|
||||
|
||||
```
|
||||
archive/v1/src/
|
||||
v1/src/
|
||||
├── core/
|
||||
│ ├── csi_processor.py # Real processing only
|
||||
│ └── router_interface.py # Real hardware interface only
|
||||
@@ -157,7 +157,7 @@ if MOCK_MODE:
|
||||
A small real CSI capture file + one-command verification pipeline:
|
||||
|
||||
```
|
||||
archive/v1/data/proof/
|
||||
v1/data/proof/
|
||||
├── README.md # How to verify
|
||||
├── sample_csi_capture.bin # Real CSI data (1 second, ~50 KB)
|
||||
├── sample_csi_capture_meta.json # Capture metadata (hardware, env)
|
||||
@@ -172,7 +172,7 @@ archive/v1/data/proof/
|
||||
"""Verify WiFi-DensePose pipeline produces deterministic output from real CSI data.
|
||||
|
||||
Usage:
|
||||
python archive/v1/data/proof/verify.py
|
||||
python v1/data/proof/verify.py
|
||||
|
||||
Expected output:
|
||||
PASS: Pipeline output matches expected hash
|
||||
@@ -265,13 +265,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
WORKDIR /app
|
||||
|
||||
# Pinned requirements (not a reference to missing file)
|
||||
COPY archive/v1/requirements-lock.txt ./requirements.txt
|
||||
COPY v1/requirements-lock.txt ./requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY archive/v1/ ./v1/
|
||||
COPY v1/ ./v1/
|
||||
|
||||
# Proof of reality: verify pipeline on build
|
||||
RUN cd archive/v1 && python data/proof/verify.py
|
||||
RUN cd v1 && python data/proof/verify.py
|
||||
|
||||
EXPOSE 8000
|
||||
# Default: REAL mode (mock requires explicit opt-in)
|
||||
@@ -281,7 +281,7 @@ CMD ["uvicorn", "v1.src.api.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
||||
**Key change**: `RUN python data/proof/verify.py` **during build** means the Docker image cannot be created unless the pipeline produces correct output from real CSI data.
|
||||
|
||||
**Requirements lockfile** (`archive/v1/requirements-lock.txt`):
|
||||
**Requirements lockfile** (`v1/requirements-lock.txt`):
|
||||
```
|
||||
# Core (required)
|
||||
fastapi==0.115.6
|
||||
@@ -307,9 +307,9 @@ name: Verify Signal Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
paths: ['archive/v1/src/**', 'archive/v1/data/proof/**']
|
||||
paths: ['v1/src/**', 'v1/data/proof/**']
|
||||
pull_request:
|
||||
paths: ['archive/v1/src/**']
|
||||
paths: ['v1/src/**']
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
@@ -322,11 +322,11 @@ jobs:
|
||||
- name: Install minimal deps
|
||||
run: pip install numpy scipy pydantic pydantic-settings
|
||||
- name: Verify pipeline determinism
|
||||
run: python archive/v1/data/proof/verify.py
|
||||
run: python v1/data/proof/verify.py
|
||||
- name: Verify no random in production paths
|
||||
run: |
|
||||
# Fail if np.random appears in production code (not in testing/)
|
||||
! grep -r "np\.random\.\(rand\|randn\|randint\)" archive/v1/src/ \
|
||||
! grep -r "np\.random\.\(rand\|randn\|randint\)" v1/src/ \
|
||||
--include="*.py" \
|
||||
--exclude-dir=testing \
|
||||
|| (echo "FAIL: np.random found in production code" && exit 1)
|
||||
@@ -336,23 +336,23 @@ jobs:
|
||||
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `archive/v1/src/core/csi_processor.py:390` | **Replace** | Real Doppler extraction from temporal CSI history |
|
||||
| `archive/v1/src/hardware/csi_extractor.py:83-84` | **Replace** | Hard error with descriptive message when parsing fails |
|
||||
| `archive/v1/src/hardware/csi_extractor.py:129-135` | **Replace** | Real Atheros CSI parser or hard error with hardware instructions |
|
||||
| `archive/v1/src/hardware/router_interface.py:198-212` | **Replace** | Hard error for unimplemented hardware, or real `iwconfig` + CSI tool integration |
|
||||
| `archive/v1/src/services/pose_service.py:293-356` | **Move** | Move `_generate_mock_poses()` to `archive/v1/src/testing/mock_pose_generator.py` |
|
||||
| `archive/v1/src/services/pose_service.py:430-431` | **Remove** | Remove mock CSI generation from production path |
|
||||
| `archive/v1/src/services/pose_service.py:489-607` | **Replace** | Real statistics from database, or explicit "no data" response |
|
||||
| `archive/v1/src/core/router_interface.py:60-300` | **Move** | Move mock generator to `archive/v1/src/testing/mock_csi_generator.py` |
|
||||
| `archive/v1/src/api/dependencies.py:82,408` | **Replace** | Real auth check or explicit dev-mode bypass with logging |
|
||||
| `archive/v1/data/proof/` | **Create** | Proof bundle (sample capture + expected hash + verify script) |
|
||||
| `archive/v1/requirements-lock.txt` | **Create** | Pinned minimal dependencies |
|
||||
| `v1/src/core/csi_processor.py:390` | **Replace** | Real Doppler extraction from temporal CSI history |
|
||||
| `v1/src/hardware/csi_extractor.py:83-84` | **Replace** | Hard error with descriptive message when parsing fails |
|
||||
| `v1/src/hardware/csi_extractor.py:129-135` | **Replace** | Real Atheros CSI parser or hard error with hardware instructions |
|
||||
| `v1/src/hardware/router_interface.py:198-212` | **Replace** | Hard error for unimplemented hardware, or real `iwconfig` + CSI tool integration |
|
||||
| `v1/src/services/pose_service.py:293-356` | **Move** | Move `_generate_mock_poses()` to `v1/src/testing/mock_pose_generator.py` |
|
||||
| `v1/src/services/pose_service.py:430-431` | **Remove** | Remove mock CSI generation from production path |
|
||||
| `v1/src/services/pose_service.py:489-607` | **Replace** | Real statistics from database, or explicit "no data" response |
|
||||
| `v1/src/core/router_interface.py:60-300` | **Move** | Move mock generator to `v1/src/testing/mock_csi_generator.py` |
|
||||
| `v1/src/api/dependencies.py:82,408` | **Replace** | Real auth check or explicit dev-mode bypass with logging |
|
||||
| `v1/data/proof/` | **Create** | Proof bundle (sample capture + expected hash + verify script) |
|
||||
| `v1/requirements-lock.txt` | **Create** | Pinned minimal dependencies |
|
||||
| `.github/workflows/verify-pipeline.yml` | **Create** | CI verification |
|
||||
|
||||
### Hardware Documentation
|
||||
|
||||
```
|
||||
archive/v1/docs/hardware-setup.md (to be created)
|
||||
v1/docs/hardware-setup.md (to be created)
|
||||
|
||||
# Supported Hardware Matrix
|
||||
|
||||
@@ -368,17 +368,17 @@ archive/v1/docs/hardware-setup.md (to be created)
|
||||
2. Capture 10 seconds of empty-room baseline
|
||||
3. Have one person walk through at normal pace
|
||||
4. Capture 10 seconds during walk-through
|
||||
5. Run calibration: `python archive/v1/scripts/calibrate.py --baseline empty.dat --activity walk.dat`
|
||||
5. Run calibration: `python v1/scripts/calibrate.py --baseline empty.dat --activity walk.dat`
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- **"Clone, build, verify" in one command**: `docker build . && docker run --rm wifi-densepose python archive/v1/data/proof/verify.py` produces a deterministic PASS
|
||||
- **"Clone, build, verify" in one command**: `docker build . && docker run --rm wifi-densepose python v1/data/proof/verify.py` produces a deterministic PASS
|
||||
- **No silent fakes**: Random data never appears in production output
|
||||
- **CI enforcement**: PRs that introduce `np.random` in production paths fail automatically
|
||||
- **Credibility anchor**: SHA-256 verified output from real CSI capture is unchallengeable proof
|
||||
- **Clear mock boundary**: Mock code exists only in `archive/v1/src/testing/`, never imported by production modules
|
||||
- **Clear mock boundary**: Mock code exists only in `v1/src/testing/`, never imported by production modules
|
||||
|
||||
### Negative
|
||||
- **Requires real CSI capture**: Someone must capture and commit a real CSI sample (one-time effort)
|
||||
@@ -390,7 +390,7 @@ archive/v1/docs/hardware-setup.md (to be created)
|
||||
|
||||
A stranger can:
|
||||
1. `git clone` the repository
|
||||
2. Run ONE command (`docker build .` or `python archive/v1/data/proof/verify.py`)
|
||||
2. Run ONE command (`docker build .` or `python v1/data/proof/verify.py`)
|
||||
3. See `PASS: Pipeline output matches expected hash` with a specific SHA-256
|
||||
4. Confirm no `np.random` in any non-test file via CI badge
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ typedef struct {
|
||||
The aggregator runs on any machine with WiFi/Ethernet to the nodes:
|
||||
|
||||
```rust
|
||||
// In v2/, new module: crates/wifi-densepose-hardware/src/esp32/
|
||||
// In wifi-densepose-rs, new module: crates/wifi-densepose-hardware/src/esp32/
|
||||
pub struct Esp32Aggregator {
|
||||
/// UDP socket listening for node streams
|
||||
socket: UdpSocket,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ADR-013: Feature-Level Sensing on Commodity Gear (Option 3)
|
||||
|
||||
## Status
|
||||
Accepted — Implemented (36/36 unit tests pass, see `archive/v1/src/sensing/` and `archive/v1/tests/unit/test_sensing.py`)
|
||||
Accepted — Implemented (36/36 unit tests pass, see `v1/src/sensing/` and `v1/tests/unit/test_sensing.py`)
|
||||
|
||||
## Date
|
||||
2026-02-28
|
||||
@@ -323,7 +323,7 @@ class PresenceClassifier:
|
||||
### Proof Bundle for Commodity Sensing
|
||||
|
||||
```
|
||||
archive/v1/data/proof/commodity/
|
||||
v1/data/proof/commodity/
|
||||
├── rssi_capture_30sec.json # 30 seconds of RSSI from 3 receivers
|
||||
├── rssi_capture_meta.json # Hardware: Intel AX200, Router: TP-Link AX1800
|
||||
├── scenario.txt # "Person walks through room at t=10s, sits at t=20s"
|
||||
@@ -375,7 +375,7 @@ class CommodityBackend(SensingBackend):
|
||||
|
||||
### Implementation Status
|
||||
|
||||
The full commodity sensing pipeline is implemented in `archive/v1/src/sensing/`:
|
||||
The full commodity sensing pipeline is implemented in `v1/src/sensing/`:
|
||||
|
||||
| Module | File | Description |
|
||||
|--------|------|-------------|
|
||||
@@ -384,7 +384,7 @@ The full commodity sensing pipeline is implemented in `archive/v1/src/sensing/`:
|
||||
| Classifier | `classifier.py` | `PresenceClassifier` with ABSENT/PRESENT_STILL/ACTIVE levels, confidence scoring |
|
||||
| Backend | `backend.py` | `CommodityBackend` wiring collector → extractor → classifier, reports PRESENCE + MOTION capabilities |
|
||||
|
||||
**Test coverage**: 36 tests in `archive/v1/tests/unit/test_sensing.py` — all passing:
|
||||
**Test coverage**: 36 tests in `v1/tests/unit/test_sensing.py` — all passing:
|
||||
- `TestRingBuffer` (4), `TestSimulatedCollector` (5), `TestFeatureExtractor` (8), `TestCusum` (4), `TestPresenceClassifier` (7), `TestCommodityBackend` (6), `TestBandPower` (2)
|
||||
|
||||
**Dependencies**: `numpy`, `scipy` (for FFT and spectral analysis)
|
||||
|
||||
@@ -510,7 +510,7 @@ impl CompressedHeartbeatSpectrogram {
|
||||
|
||||
## Dependency Changes Required
|
||||
|
||||
Add to `v2/Cargo.toml` workspace (already present from ADR-016):
|
||||
Add to `rust-port/wifi-densepose-rs/Cargo.toml` workspace (already present from ADR-016):
|
||||
```toml
|
||||
ruvector-mincut = "2.0.4" # already present
|
||||
ruvector-attn-mincut = "2.0.4" # already present
|
||||
|
||||
@@ -22,8 +22,8 @@ This ADR answers *how* to build it — the concrete development sequence, the sp
|
||||
| Frame types | `wifi-densepose-hardware/src/csi_frame.rs` | Complete — `CsiFrame`, `CsiMetadata`, `SubcarrierData`, `to_amplitude_phase()` |
|
||||
| Parse error types | `wifi-densepose-hardware/src/error.rs` | Complete — `ParseError` enum with 6 variants |
|
||||
| Signal processing pipeline | `wifi-densepose-signal` crate | Complete — Hampel, Fresnel, BVP, Doppler, spectrogram |
|
||||
| CSI extractor (Python) | `archive/v1/src/hardware/csi_extractor.py` | Stub — `_read_raw_data()` raises `NotImplementedError` |
|
||||
| Router interface (Python) | `archive/v1/src/hardware/router_interface.py` | Stub — `_parse_csi_response()` raises `RouterConnectionError` |
|
||||
| CSI extractor (Python) | `v1/src/hardware/csi_extractor.py` | Stub — `_read_raw_data()` raises `NotImplementedError` |
|
||||
| Router interface (Python) | `v1/src/hardware/router_interface.py` | Stub — `_parse_csi_response()` raises `RouterConnectionError` |
|
||||
|
||||
**Not yet implemented:**
|
||||
|
||||
@@ -211,10 +211,10 @@ The bridge test: parse a known binary frame, convert to `CsiData`, assert `ampli
|
||||
|
||||
### Layer 4 — Python `_read_raw_data()` Real Implementation
|
||||
|
||||
Replace the `NotImplementedError` stub in `archive/v1/src/hardware/csi_extractor.py` with a UDP socket reader. This allows the Python pipeline to receive real CSI from the aggregator while the Rust pipeline is being integrated.
|
||||
Replace the `NotImplementedError` stub in `v1/src/hardware/csi_extractor.py` with a UDP socket reader. This allows the Python pipeline to receive real CSI from the aggregator while the Rust pipeline is being integrated.
|
||||
|
||||
```python
|
||||
# archive/v1/src/hardware/csi_extractor.py
|
||||
# v1/src/hardware/csi_extractor.py
|
||||
# Replace _read_raw_data() stub:
|
||||
|
||||
import socket as _socket
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
The WiFi-DensePose UI was originally built to require the full FastAPI DensePose backend (`localhost:8000`) for all functionality. This backend depends on heavy Python packages (PyTorch ~2GB, torchvision, OpenCV, SQLAlchemy, Redis) making it impractical for lightweight sensing-only deployments where the user simply wants to visualize live WiFi signal data from ESP32 CSI or Windows RSSI collectors.
|
||||
|
||||
A Rust port exists (`v2`) using Axum with lighter runtime footprint (~10MB binary, ~5MB RAM), but it still requires libtorch C++ bindings and OpenBLAS for compilation—a non-trivial build.
|
||||
A Rust port exists (`rust-port/wifi-densepose-rs`) using Axum with lighter runtime footprint (~10MB binary, ~5MB RAM), but it still requires libtorch C++ bindings and OpenBLAS for compilation—a non-trivial build.
|
||||
|
||||
Users need a way to run the UI with **only the sensing pipeline** active, without installing the full DensePose backend stack.
|
||||
|
||||
@@ -34,7 +34,7 @@ Implement a **sensing-only UI mode** that:
|
||||
- Breathing ring modulation when breathing-band power detected
|
||||
- Side panel with RSSI sparkline, feature meters, and classification badge
|
||||
|
||||
4. **Python WebSocket bridge** (`archive/v1/src/sensing/ws_server.py`) that:
|
||||
4. **Python WebSocket bridge** (`v1/src/sensing/ws_server.py`) that:
|
||||
- Auto-detects ESP32 UDP CSI stream on port 5005 (ADR-018 binary frames)
|
||||
- Falls back to `WindowsWifiCollector` → `SimulatedCollector`
|
||||
- Runs `RssiFeatureExtractor` → `PresenceClassifier` pipeline
|
||||
@@ -80,7 +80,7 @@ Windows WiFi RSSI ───┘ │ │
|
||||
### Created
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `archive/v1/src/sensing/ws_server.py` | Python asyncio WebSocket server with auto-detect collectors |
|
||||
| `v1/src/sensing/ws_server.py` | Python asyncio WebSocket server with auto-detect collectors |
|
||||
| `ui/components/SensingTab.js` | Sensing tab UI with Three.js integration |
|
||||
| `ui/components/gaussian-splats.js` | Custom GLSL Gaussian splat renderer |
|
||||
| `ui/services/sensing.service.js` | WebSocket client with reconnect + simulation fallback |
|
||||
|
||||
@@ -22,7 +22,7 @@ The current Python DensePose backend requires ~2GB+ of dependencies:
|
||||
|
||||
This makes the DensePose backend impractical for edge deployments, CI pipelines, and developer laptops where users only need WiFi sensing + pose estimation.
|
||||
|
||||
Meanwhile, the Rust port at `v2/` already has:
|
||||
Meanwhile, the Rust port at `rust-port/wifi-densepose-rs/` already has:
|
||||
|
||||
- **12 workspace crates** covering core, signal, nn, api, db, config, hardware, wasm, cli, mat, train
|
||||
- **5 RuVector crates** (v2.0.4, published on crates.io) integrated into signal, mat, and train crates
|
||||
@@ -40,8 +40,8 @@ Use the `wifi-densepose-nn` crate with `default-features = ["onnx"]` only. This
|
||||
|
||||
| Component | Rust Crate | Replaces Python |
|
||||
|-----------|-----------|-----------------|
|
||||
| CSI processing | `wifi-densepose-signal::csi_processor` | `archive/v1/src/sensing/feature_extractor.py` |
|
||||
| Motion detection | `wifi-densepose-signal::motion` | `archive/v1/src/sensing/classifier.py` |
|
||||
| CSI processing | `wifi-densepose-signal::csi_processor` | `v1/src/sensing/feature_extractor.py` |
|
||||
| Motion detection | `wifi-densepose-signal::motion` | `v1/src/sensing/classifier.py` |
|
||||
| BVP extraction | `wifi-densepose-signal::bvp` | N/A (new capability) |
|
||||
| Fresnel geometry | `wifi-densepose-signal::fresnel` | N/A (new capability) |
|
||||
| Subcarrier selection | `wifi-densepose-signal::subcarrier_selection` | N/A (new capability) |
|
||||
@@ -143,7 +143,7 @@ The `wifi-densepose-nn::onnx` module loads `.onnx` files directly.
|
||||
|
||||
```bash
|
||||
# Build the Rust workspace (ONNX-only, no libtorch)
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo check --workspace 2>&1
|
||||
|
||||
# Build release binary
|
||||
|
||||
@@ -34,7 +34,7 @@ The `vendor/ruvector` codebase provides a rich set of signal processing primitiv
|
||||
|
||||
### Current Project State
|
||||
|
||||
The Rust port (`v2/`) already contains:
|
||||
The Rust port (`rust-port/wifi-densepose-rs/`) already contains:
|
||||
|
||||
- **`wifi-densepose-signal`**: CSI processing, BVP extraction, phase sanitization, Hampel filter, spectrogram generation, Fresnel geometry, motion detection, subcarrier selection
|
||||
- **`wifi-densepose-sensing-server`**: Axum server receiving ESP32 CSI frames (UDP 5005), WebSocket broadcasting sensing updates, signal field generation, with three data source modes:
|
||||
@@ -108,7 +108,7 @@ ESP32 CSI (UDP:5005) ──▶│ ┌──────────────
|
||||
### Module Structure
|
||||
|
||||
```
|
||||
v2/crates/wifi-densepose-vitals/
|
||||
rust-port/wifi-densepose-rs/crates/wifi-densepose-vitals/
|
||||
├── Cargo.toml
|
||||
└── src/
|
||||
├── lib.rs # Public API and re-exports
|
||||
|
||||
@@ -592,7 +592,7 @@ impl FrameBuilder {
|
||||
### 3.3 Module Structure
|
||||
|
||||
```
|
||||
v2/crates/wifi-densepose-wifiscan/
|
||||
rust-port/wifi-densepose-rs/crates/wifi-densepose-wifiscan/
|
||||
├── Cargo.toml
|
||||
└── src/
|
||||
├── lib.rs # Public API, re-exports
|
||||
|
||||
@@ -699,28 +699,28 @@ let dashboard = container.load_dashboard()?;
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `v2/.../wifi-densepose-train/src/dataset_mmfi.rs` | MM-Fi dataset loader with subcarrier resampling |
|
||||
| `v2/.../wifi-densepose-train/src/dataset_wipose.rs` | Wi-Pose dataset loader |
|
||||
| `v2/.../wifi-densepose-train/src/graph_transformer.rs` | Graph transformer integration |
|
||||
| `v2/.../wifi-densepose-train/src/body_gnn.rs` | GNN body graph reasoning |
|
||||
| `v2/.../wifi-densepose-train/src/adaptation.rs` | SONA LoRA + EWC++ adaptation |
|
||||
| `v2/.../wifi-densepose-train/src/trainer.rs` | Training loop with multi-term loss |
|
||||
| `rust-port/.../wifi-densepose-train/src/dataset_mmfi.rs` | MM-Fi dataset loader with subcarrier resampling |
|
||||
| `rust-port/.../wifi-densepose-train/src/dataset_wipose.rs` | Wi-Pose dataset loader |
|
||||
| `rust-port/.../wifi-densepose-train/src/graph_transformer.rs` | Graph transformer integration |
|
||||
| `rust-port/.../wifi-densepose-train/src/body_gnn.rs` | GNN body graph reasoning |
|
||||
| `rust-port/.../wifi-densepose-train/src/adaptation.rs` | SONA LoRA + EWC++ adaptation |
|
||||
| `rust-port/.../wifi-densepose-train/src/trainer.rs` | Training loop with multi-term loss |
|
||||
| `scripts/generate_densepose_labels.py` | Teacher-student UV label generation |
|
||||
| `scripts/benchmark_inference.py` | Inference latency benchmarking |
|
||||
| `v2/.../wifi-densepose-train/src/rvf_builder.rs` | RVF container build pipeline |
|
||||
| `v2/.../wifi-densepose-train/src/bin/build_rvf.rs` | CLI binary for building `.rvf` containers |
|
||||
| `v2/.../wifi-densepose-train/src/bin/verify_rvf.rs` | CLI binary for verifying `.rvf` containers |
|
||||
| `rust-port/.../wifi-densepose-train/src/rvf_builder.rs` | RVF container build pipeline |
|
||||
| `rust-port/.../wifi-densepose-train/src/bin/build_rvf.rs` | CLI binary for building `.rvf` containers |
|
||||
| `rust-port/.../wifi-densepose-train/src/bin/verify_rvf.rs` | CLI binary for verifying `.rvf` containers |
|
||||
|
||||
### Modified Files
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `v2/.../wifi-densepose-train/Cargo.toml` | Add ruvector-gnn, graph-transformer, sona, sparse-inference, math, rvf-types, rvf-wire, rvf-manifest, rvf-index, rvf-quant, rvf-crypto, rvf-runtime deps |
|
||||
| `v2/.../wifi-densepose-train/src/model.rs` | Integrate graph transformer + GNN layers |
|
||||
| `v2/.../wifi-densepose-train/src/losses.rs` | Add optimal transport + GNN edge consistency loss terms |
|
||||
| `v2/.../wifi-densepose-train/src/config.rs` | Add training hyperparameters for new components |
|
||||
| `v2/.../sensing-server/Cargo.toml` | Add rvf-runtime, rvf-types, rvf-index, rvf-quant deps |
|
||||
| `v2/.../sensing-server/src/main.rs` | Add `--model` flag, load `.rvf` container, progressive startup, serve embedded dashboard |
|
||||
| `rust-port/.../wifi-densepose-train/Cargo.toml` | Add ruvector-gnn, graph-transformer, sona, sparse-inference, math, rvf-types, rvf-wire, rvf-manifest, rvf-index, rvf-quant, rvf-crypto, rvf-runtime deps |
|
||||
| `rust-port/.../wifi-densepose-train/src/model.rs` | Integrate graph transformer + GNN layers |
|
||||
| `rust-port/.../wifi-densepose-train/src/losses.rs` | Add optimal transport + GNN edge consistency loss terms |
|
||||
| `rust-port/.../wifi-densepose-train/src/config.rs` | Add training hyperparameters for new components |
|
||||
| `rust-port/.../sensing-server/Cargo.toml` | Add rvf-runtime, rvf-types, rvf-index, rvf-quant deps |
|
||||
| `rust-port/.../sensing-server/src/main.rs` | Add `--model` flag, load `.rvf` container, progressive startup, serve embedded dashboard |
|
||||
|
||||
## Consequences
|
||||
|
||||
|
||||
@@ -371,7 +371,7 @@ ESP32 SRAM budget: 520 KB. Model at INT8: 53-60 KB = 10-12% of SRAM. Ample margi
|
||||
|
||||
### 2.6 Concrete Module Additions
|
||||
|
||||
All new/modified files in `v2/crates/wifi-densepose-sensing-server/src/`:
|
||||
All new/modified files in `rust-port/wifi-densepose-rs/crates/wifi-densepose-sensing-server/src/`:
|
||||
|
||||
#### 2.6.1 `embedding.rs` (NEW, ~450 lines)
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ Implement a **macOS CoreWLAN sensing adapter** as a Swift helper binary + Rust a
|
||||
|
||||
### 3.2 Swift Helper Binary
|
||||
|
||||
**File:** `v2/tools/macos-wifi-scan/main.swift`
|
||||
**File:** `rust-port/wifi-densepose-rs/tools/macos-wifi-scan/main.swift`
|
||||
|
||||
```swift
|
||||
// Modes:
|
||||
|
||||
@@ -232,10 +232,10 @@ python scripts/provision.py --port COM7 \
|
||||
|
||||
| Component | File | Purpose |
|
||||
|-----------|------|---------|
|
||||
| Reference signal | `archive/v1/data/proof/sample_csi_data.json` | 1,000 synthetic CSI frames, seed=42 |
|
||||
| Generator | `archive/v1/data/proof/generate_reference_signal.py` | Deterministic multipath model |
|
||||
| Verifier | `archive/v1/data/proof/verify.py` | SHA-256 hash comparison |
|
||||
| Expected hash | `archive/v1/data/proof/expected_features.sha256` | `0b82bd45...` |
|
||||
| Reference signal | `v1/data/proof/sample_csi_data.json` | 1,000 synthetic CSI frames, seed=42 |
|
||||
| Generator | `v1/data/proof/generate_reference_signal.py` | Deterministic multipath model |
|
||||
| Verifier | `v1/data/proof/verify.py` | SHA-256 hash comparison |
|
||||
| Expected hash | `v1/data/proof/expected_features.sha256` | `0b82bd45...` |
|
||||
|
||||
**Audit-time result:** PASS. Hash regenerated with numpy 2.4.2 + scipy 1.17.1. Pipeline hash: `8c0680d7d285739ea9597715e84959d9c356c87ee3ad35b5f1e69a4ca41151c6`.
|
||||
|
||||
|
||||
@@ -198,16 +198,16 @@ When a `.rvf` model is loaded:
|
||||
### New Files
|
||||
- `ui/components/ModelPanel.js` — Model library, inspector, load/unload controls
|
||||
- `ui/components/TrainingPanel.js` — Recording controls, training progress, metric charts
|
||||
- `v2/.../sensing-server/src/recording.rs` — CSI recording API handlers
|
||||
- `v2/.../sensing-server/src/training_api.rs` — Training API handlers + WS progress stream
|
||||
- `v2/.../sensing-server/src/model_manager.rs` — Model loading, hot-swap, 32LoRA activation
|
||||
- `rust-port/.../sensing-server/src/recording.rs` — CSI recording API handlers
|
||||
- `rust-port/.../sensing-server/src/training_api.rs` — Training API handlers + WS progress stream
|
||||
- `rust-port/.../sensing-server/src/model_manager.rs` — Model loading, hot-swap, 32LoRA activation
|
||||
- `data/models/` — Default model storage directory
|
||||
|
||||
### Modified Files
|
||||
- `v2/.../sensing-server/src/main.rs` — Wire recording, training, and model APIs
|
||||
- `v2/.../train/src/trainer.rs` — Add WebSocket progress callback, LoRA training mode
|
||||
- `v2/.../train/src/dataset.rs` — MM-Fi and Wi-Pose dataset loaders
|
||||
- `v2/.../nn/src/onnx.rs` — LoRA weight injection, INT8 quantization support
|
||||
- `rust-port/.../sensing-server/src/main.rs` — Wire recording, training, and model APIs
|
||||
- `rust-port/.../train/src/trainer.rs` — Add WebSocket progress callback, LoRA training mode
|
||||
- `rust-port/.../train/src/dataset.rs` — MM-Fi and Wi-Pose dataset loaders
|
||||
- `rust-port/.../nn/src/onnx.rs` — LoRA weight injection, INT8 quantization support
|
||||
- `ui/components/LiveDemoTab.js` — Model selector, LoRA dropdown, A/B spsplit view
|
||||
- `ui/components/SettingsPanel.js` — Model and training configuration sections
|
||||
- `ui/components/PoseDetectionCanvas.js` — Pose trail rendering, confidence heatmap overlay
|
||||
|
||||
@@ -128,7 +128,7 @@ All configurable via `provision.py --edge-tier 2 --pres-thresh 0.05 ...`
|
||||
- `firmware/esp32-csi-node/main/edge_processing.h` — Types and API
|
||||
- `firmware/esp32-csi-node/main/ota_update.c/h` — HTTP OTA endpoint
|
||||
- `firmware/esp32-csi-node/main/power_mgmt.c/h` — Power management
|
||||
- `v2/.../wifi-densepose-sensing-server/src/main.rs` — Vitals parser + REST endpoint
|
||||
- `rust-port/.../wifi-densepose-sensing-server/src/main.rs` — Vitals parser + REST endpoint
|
||||
- `scripts/provision.py` — Edge config CLI arguments
|
||||
- `.github/workflows/firmware-ci.yml` — CI build + size gate (updated to 950 KB for Tier 3)
|
||||
|
||||
|
||||
@@ -164,8 +164,8 @@ Core 1 (DSP Task)
|
||||
- `firmware/esp32-csi-node/main/wasm_runtime.c/h` — Runtime host with 12 API bindings + manifest
|
||||
- `firmware/esp32-csi-node/main/wasm_upload.c/h` — HTTP REST endpoints (RVF-aware)
|
||||
- `firmware/esp32-csi-node/main/rvf_parser.c/h` — RVF container parser and verifier
|
||||
- `v2/.../wifi-densepose-wasm-edge/` — Rust WASM crate (gesture, coherence, adversarial, rvf, occupancy, vital_trend, intrusion)
|
||||
- `v2/.../wifi-densepose-sensing-server/src/main.rs` — `0xC5110004` parser
|
||||
- `rust-port/.../wifi-densepose-wasm-edge/` — Rust WASM crate (gesture, coherence, adversarial, rvf, occupancy, vital_trend, intrusion)
|
||||
- `rust-port/.../wifi-densepose-sensing-server/src/main.rs` — `0xC5110004` parser
|
||||
- `docs/adr/ADR-039-esp32-edge-intelligence.md` — Updated with Tier 3 reference
|
||||
|
||||
---
|
||||
|
||||
@@ -289,7 +289,7 @@ Startup creates `data/models/` and `data/recordings/` directories and populates
|
||||
|
||||
```bash
|
||||
# 1. Start sensing server with auto source (simulated fallback)
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo run -p wifi-densepose-sensing-server -- --http-port 3000 --source auto
|
||||
|
||||
# 2. Verify model endpoints return 200
|
||||
@@ -312,11 +312,11 @@ curl -s http://localhost:3000/api/v1/models/lora/profiles | jq '.'
|
||||
# Navigate to http://localhost:3000/ui/
|
||||
|
||||
# 7. Run mobile tests
|
||||
cd ../ui/mobile
|
||||
cd ../../ui/mobile
|
||||
npx jest --no-coverage
|
||||
|
||||
# 8. Run Rust workspace tests (must pass, 1031+ tests)
|
||||
cd ../../v2
|
||||
cd ../../rust-port/wifi-densepose-rs
|
||||
cargo test --workspace --no-default-features
|
||||
```
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
# ADR-044: Geospatial Satellite Integration
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
RuView generates real-time 3D point clouds from camera + WiFi CSI, but these exist in a local coordinate frame with no geographic reference. Integrating free satellite imagery, terrain elevation, and map data provides environmental context that enables the ruOS brain to reason about the physical world beyond the room.
|
||||
|
||||
## Decision
|
||||
|
||||
### Data Sources (all free, no API keys)
|
||||
| Source | Data | Resolution | Update | Format |
|
||||
|--------|------|-----------|--------|--------|
|
||||
| EOX Sentinel-2 Cloudless | Satellite tiles | 10m | Static mosaic | XYZ/JPEG |
|
||||
| SRTM GL1 (NASA) | Elevation/DEM | 30m (1-arcsec) | Static | Binary HGT |
|
||||
| Overpass API (OSM) | Buildings, roads | Vector | Real-time | JSON |
|
||||
| ip-api.com | IP geolocation | ~1km | Per-request | JSON |
|
||||
| Sentinel-2 STAC | Temporal satellite | 10m | Every 5 days | COG/STAC |
|
||||
| Open Meteo | Weather | Point | Hourly | JSON |
|
||||
|
||||
### Architecture
|
||||
Pure Rust implementation in `wifi-densepose-geo` crate. No GDAL/PROJ/GEOS — coordinate transforms implemented directly (~250 LOC). Tile caching on disk at `~/.local/share/ruview/geo-cache/`.
|
||||
|
||||
### Coordinate System
|
||||
- WGS84 for geographic coordinates
|
||||
- ENU (East-North-Up) as the bridge between local sensor frame and world
|
||||
- Local sensor frame: camera origin, +Z forward, +Y up
|
||||
|
||||
### Temporal Awareness
|
||||
Nightly scheduled fetch of Sentinel-2 latest imagery + OSM diffs + weather.
|
||||
Changes detected via image comparison and stored as brain memories for
|
||||
contrastive learning.
|
||||
|
||||
### Brain Integration
|
||||
Geospatial context stored as brain memories:
|
||||
- `spatial-geo`: location, elevation, nearby landmarks
|
||||
- `spatial-change`: detected changes in satellite/OSM data
|
||||
- `spatial-weather`: current conditions + forecast
|
||||
- `spatial-season`: vegetation index, snow cover, seasonal patterns
|
||||
- `spatial-local`: hyperlocal web context from Common Crawl WET
|
||||
|
||||
### Extended Data Sources (via ruvector WET/Common Crawl)
|
||||
| Source | Data | Use |
|
||||
|--------|------|-----|
|
||||
| Common Crawl WET | Web text near location | Local business info, reviews, events |
|
||||
| Wikidata | Structured knowledge | Building names, POI descriptions |
|
||||
| NASA FIRMS | Active fire (3-hour) | Safety alerts |
|
||||
| USGS Earthquakes | Seismic events | Safety context |
|
||||
| OpenAQ | Air quality (PM2.5) | Environmental health |
|
||||
| Overture Maps | Building footprints (Meta/MS) | Higher quality than OSM |
|
||||
|
||||
The ruvector brain server has existing `web_ingest` + Common Crawl support.
|
||||
WET files filtered by geographic URL patterns provide hyperlocal context.
|
||||
|
||||
## Consequences
|
||||
### Positive
|
||||
- Agent gains environmental awareness beyond the room
|
||||
- Temporal data enables seasonal calibration of CSI sensing
|
||||
- Change detection finds construction, vegetation, weather effects
|
||||
- All data sources are genuinely free with no API keys
|
||||
|
||||
### Negative
|
||||
- Initial data fetch requires internet (~2MB tiles + ~25MB DEM)
|
||||
- Cached data becomes stale (mitigated by nightly refresh)
|
||||
- IP geolocation has ~1km accuracy (mitigated by manual override)
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# ADR-050: Provisioning Tool Enhancements
|
||||
# ADR-044: Provisioning Tool Enhancements
|
||||
|
||||
**Status**: Proposed
|
||||
**Date**: 2026-03-03
|
||||
@@ -108,7 +108,7 @@ Remove duplicated platform-detection logic from `ws_server.py` and `install.sh`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
1. Add `create_collector()` and `BaseCollector.is_available()` to `archive/v1/src/sensing/rssi_collector.py`
|
||||
1. Add `create_collector()` and `BaseCollector.is_available()` to `v1/src/sensing/rssi_collector.py`
|
||||
2. Refactor `ws_server.py` `_init_collector()` to call `create_collector()`
|
||||
3. Update `install.sh` `detect_wifi_hardware()` to use shared detection logic
|
||||
4. Add unit tests for each platform path (mock `/proc/net/wireless` presence/absence)
|
||||
|
||||
@@ -29,7 +29,7 @@ There is no single tool that provides a unified view of the entire deployment
|
||||
|
||||
A browser-based UI cannot access serial ports (for flashing), raw UDP sockets (for node discovery), or the local filesystem (for firmware binaries). A desktop application is required for hardware management. Tauri v2 is the natural choice because:
|
||||
|
||||
1. **Rust backend** — integrates directly with the existing Rust workspace (`v2/`). Crates like `wifi-densepose-hardware` (serial port parsing), `wifi-densepose-config`, and `wifi-densepose-sensing-server` can be linked as library dependencies.
|
||||
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.
|
||||
@@ -52,7 +52,7 @@ Build a Tauri v2 desktop application as a new crate in the Rust workspace. The f
|
||||
Add a new crate to the workspace:
|
||||
|
||||
```
|
||||
v2/
|
||||
rust-port/wifi-densepose-rs/
|
||||
Cargo.toml # Add "crates/wifi-densepose-desktop" to members
|
||||
crates/
|
||||
wifi-densepose-desktop/ # NEW — Tauri app crate
|
||||
@@ -621,11 +621,11 @@ chrono = { version = "0.4", features = ["serde"] }
|
||||
```bash
|
||||
# Prerequisites
|
||||
cargo install tauri-cli@^2
|
||||
cd v2/crates/wifi-densepose-desktop/frontend
|
||||
cd rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/frontend
|
||||
npm install
|
||||
|
||||
# Development (hot-reload frontend + Rust rebuild)
|
||||
cd v2/crates/wifi-densepose-desktop
|
||||
cd rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop
|
||||
cargo tauri dev
|
||||
|
||||
# Production build
|
||||
@@ -805,6 +805,6 @@ Total estimated effort: ~11 weeks for a single developer.
|
||||
- ADR-051: Sensing Server Decomposition
|
||||
- `firmware/esp32-csi-node/` — ESP32 firmware source
|
||||
- `firmware/esp32-csi-node/provision.py` — Current provisioning script
|
||||
- `v2/crates/wifi-densepose-sensing-server/` — Sensing server
|
||||
- `v2/crates/wifi-densepose-hardware/` — Hardware crate
|
||||
- `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
|
||||
|
||||
@@ -214,7 +214,7 @@ examples/wasm-browser-pose/
|
||||
set -e
|
||||
|
||||
# Build wifi-densepose-wasm (CSI processing)
|
||||
wasm-pack build ../../v2/crates/wifi-densepose-wasm \
|
||||
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)
|
||||
|
||||
@@ -191,5 +191,5 @@ Also does not give per-person subcarrier assignments.
|
||||
|
||||
- Stoer, M. & Wagner, F. (1997). "A Simple Min-Cut Algorithm." JACM 44(4).
|
||||
- `vendor/ruvector/crates/ruvector-mincut/src/algorithm/mod.rs` — DynamicMinCut API
|
||||
- `v2/.../sig_mincut_person_match.rs` — current (broken) WASM edge matcher
|
||||
- `rust-port/.../sig_mincut_person_match.rs` — current (broken) WASM edge matcher
|
||||
- `scripts/rf-scan.js` — CSI packet parsing and subcarrier classification
|
||||
|
||||
@@ -17,19 +17,19 @@ Address the 15 prioritized issues from the QE analysis in three waves: P0 (immed
|
||||
|
||||
### 1. Rate Limiter Bypass (Security HIGH)
|
||||
|
||||
- **Location:** `archive/v1/src/middleware/rate_limit.py:200-206`
|
||||
- **Location:** `v1/src/middleware/rate_limit.py:200-206`
|
||||
- **Problem:** Trusts `X-Forwarded-For` without validation. Any client bypasses rate limits via header spoofing.
|
||||
- **Fix:** Validate forwarded headers against trusted proxy list, or use connection IP directly.
|
||||
|
||||
### 2. Exception Details Leaked in Responses (Security HIGH)
|
||||
|
||||
- **Location:** `archive/v1/src/api/routers/pose.py:140`, `stream.py:297`, +5 endpoints
|
||||
- **Location:** `v1/src/api/routers/pose.py:140`, `stream.py:297`, +5 endpoints
|
||||
- **Problem:** Stack traces visible regardless of environment.
|
||||
- **Fix:** Wrap with generic error responses in production; log details server-side only.
|
||||
|
||||
### 3. WebSocket JWT in URL (Security HIGH, CWE-598)
|
||||
|
||||
- **Location:** `archive/v1/src/api/routers/stream.py:74`, `archive/v1/src/middleware/auth.py:243`
|
||||
- **Location:** `v1/src/api/routers/stream.py:74`, `v1/src/middleware/auth.py:243`
|
||||
- **Problem:** Tokens in query strings visible in logs/proxies/browser history.
|
||||
- **Fix:** Use WebSocket subprotocol or first-message auth pattern.
|
||||
|
||||
|
||||
@@ -1,503 +0,0 @@
|
||||
# ADR-081: Adaptive CSI Mesh Firmware Kernel
|
||||
|
||||
| Field | Value |
|
||||
|-------------|-----------------------------------------------------------------------|
|
||||
| **Status** | Accepted — Layers 1/2/3/4/5 implemented and host-tested; mesh RX path and Ed25519 signing tracked as Phase 3.5 polish |
|
||||
| **Date** | 2026-04-19 |
|
||||
| **Authors** | ruv |
|
||||
| **Depends** | ADR-018, ADR-028, ADR-029, ADR-031, ADR-032, ADR-039, ADR-066, ADR-073 |
|
||||
|
||||
## Context
|
||||
|
||||
RuView's firmware grew bottom-up. ADR-018 defined a binary CSI frame, ADR-029
|
||||
added channel hopping and TDM, ADR-039 added a tiered edge-intelligence
|
||||
pipeline, ADR-040 added programmable WASM modules, ADR-060 added per-node
|
||||
channel and MAC overrides, ADR-066 added a swarm bridge to a coordinator, and
|
||||
ADR-073 added multifrequency mesh scanning. Each one was a sound local
|
||||
decision. Together they produced a firmware that works on ESP32-S3 but is
|
||||
**implicitly coupled** to that chipset through `csi_collector.c` calling
|
||||
`esp_wifi_*` directly and through hard-coded assumptions about the WiFi driver
|
||||
callback shape.
|
||||
|
||||
This is a problem for three reasons:
|
||||
|
||||
1. **Portability.** Espressif exposes CSI through an official driver API. On
|
||||
locked Broadcom and Cypress chips, projects like Nexmon achieve the same
|
||||
thing by patching the firmware blob — but only for specific chip and
|
||||
firmware build combinations. Future RuView nodes will likely span both
|
||||
models plus eventually a custom silicon path. Today, none of the modules
|
||||
above can be reused unchanged on any non-ESP32 chip.
|
||||
|
||||
2. **Adaptivity.** The current firmware reacts to configuration, not to
|
||||
conditions. Channel hop intervals, edge tier, vitals cadence, top-K
|
||||
subcarriers, fall threshold, and power duty are all read from NVS at boot
|
||||
and never revisited. There is no closed-loop control: if a channel becomes
|
||||
congested, if motion spikes, if inter-node coherence drops, or if the
|
||||
environment is stable enough to coast at lower cadence, nothing changes
|
||||
onboard. The adaptive classifier in `wifi-densepose-sensing-server` does
|
||||
adapt — but only on the host side, after the data has already traversed the
|
||||
network at fixed rate.
|
||||
|
||||
3. **Mesh as an afterthought.** ADR-029 wired in a `TdmCoordinator` and ADR-066
|
||||
added a swarm bridge to a Cognitum Seed, but there is no first-class node
|
||||
role enumeration (anchor / observer / fusion-relay / coordinator), no
|
||||
role-assignment protocol, no `FEATURE_DELTA` message type, no
|
||||
coordinator-driven channel plan, and no automatic role re-election when a
|
||||
node drops. Multi-node deployments today are stitched together by manual
|
||||
per-node NVS provisioning.
|
||||
|
||||
The hard truth is that the firmware hack — getting raw CSI off a radio — is
|
||||
not the moat. The moat is **adaptive control, multi-node fusion, compact
|
||||
state encoding, persistent memory, and contrastive reasoning on top of the
|
||||
radio layer**. The current architecture does not name those layers, so they
|
||||
get reinvented inline by every new ADR.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt a **5-layer adaptive RF sensing kernel** as the canonical RuView
|
||||
firmware architecture, and refactor the existing modules to fit underneath
|
||||
it. The five layers, top to bottom:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Layer 5 — Rust handoff │
|
||||
│ Two streams only: feature_state (default) and debug_csi_frame (gated) │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Layer 4 — On-device feature extraction │
|
||||
│ 100 ms motion, 1 s respiration, 5 s baseline windows │
|
||||
│ Emits compact rv_feature_state_t (magic 0xC5110006) │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Layer 3 — Mesh sensing plane │
|
||||
│ Roles: Anchor / Observer / Fusion relay / Coordinator │
|
||||
│ Messages: TIME_SYNC, ROLE_ASSIGN, CHANNEL_PLAN, CALIBRATION_START, │
|
||||
│ FEATURE_DELTA, HEALTH, ANOMALY_ALERT │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Layer 2 — Adaptive controller │
|
||||
│ Fast loop ~200 ms — packet rate, active probing │
|
||||
│ Medium loop ~1 s — channel selection, role changes │
|
||||
│ Slow loop ~30 s — baseline recalibration │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Layer 1 — Radio Abstraction Layer (rv_radio_ops_t vtable) │
|
||||
│ ESP32 binding, future Nexmon binding, future custom silicon binding │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Layer 1 — Radio Abstraction Layer
|
||||
|
||||
A single function-pointer vtable, `rv_radio_ops_t`, defined in
|
||||
`firmware/esp32-csi-node/main/rv_radio_ops.h`:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
int (*init)(void);
|
||||
int (*set_channel)(uint8_t ch, uint8_t bw);
|
||||
int (*set_mode)(uint8_t mode); /* RV_RADIO_MODE_* */
|
||||
int (*set_csi_enabled)(bool en);
|
||||
int (*set_capture_profile)(uint8_t profile_id);
|
||||
int (*get_health)(rv_radio_health_t *out);
|
||||
} rv_radio_ops_t;
|
||||
```
|
||||
|
||||
Capture profiles, named not numbered:
|
||||
|
||||
| Profile | Intent |
|
||||
|--------------------------------|-------------------------------------------------------|
|
||||
| `RV_PROFILE_PASSIVE_LOW_RATE` | Default idle: minimum cadence, presence only |
|
||||
| `RV_PROFILE_ACTIVE_PROBE` | Inject NDP frames at high rate |
|
||||
| `RV_PROFILE_RESP_HIGH_SENS` | Quietest channel, longest window, vitals-only |
|
||||
| `RV_PROFILE_FAST_MOTION` | Short window, high cadence |
|
||||
| `RV_PROFILE_CALIBRATION` | Synchronized burst across nodes |
|
||||
|
||||
Two bindings ship in this ADR:
|
||||
|
||||
- **ESP32 binding** (`rv_radio_ops_esp32.c`) wraps `csi_collector.c`,
|
||||
`esp_wifi_set_channel()`, `esp_wifi_set_csi()`, and
|
||||
`csi_inject_ndp_frame()`.
|
||||
- **Mock binding** (`rv_radio_ops_mock.c`) wraps `mock_csi.c` so QEMU
|
||||
scenarios can exercise the controller and mesh plane without a radio.
|
||||
|
||||
A third binding (Nexmon-patched Broadcom) is reserved but not implemented
|
||||
here.
|
||||
|
||||
### Layer 2 — Adaptive controller
|
||||
|
||||
`firmware/esp32-csi-node/main/adaptive_controller.{c,h}`. A single FreeRTOS
|
||||
task with three cooperating timers:
|
||||
|
||||
| Loop | Period | Inputs | Outputs |
|
||||
|--------|---------|------------------------------------------------------------------------|------------------------------------------------------|
|
||||
| Fast | ~200 ms | packet yield, retry/drop rate, motion score | cadence (vital_interval_ms), active vs passive probe |
|
||||
| Medium | ~1 s | CSI variance, RSSI median, channel occupancy, inter-node agreement | channel selection (via radio ops), role transitions |
|
||||
| Slow | ~30 s | drift profile (Stable/Linear/StepChange), respiration confidence | baseline recalibration, switch to delta-only mode |
|
||||
|
||||
The controller publishes its decisions through the radio ops vtable
|
||||
(`set_capture_profile`, `set_channel`) and through the mesh plane
|
||||
(`CHANNEL_PLAN`, `ROLE_ASSIGN`). Default policy is conservative and matches
|
||||
today's behavior; aggressive adaptation is opt-in via Kconfig.
|
||||
|
||||
### Layer 3 — Mesh sensing plane
|
||||
|
||||
Extends `swarm_bridge.c` with explicit node roles (Anchor / Observer /
|
||||
Fusion relay / Coordinator) and a 7-message type protocol:
|
||||
|
||||
| Message | Cadence | Sender(s) | Purpose |
|
||||
|----------------------|--------------------|------------------|-----------------------------------------------|
|
||||
| `TIME_SYNC` | 100 ms | Anchor | Reuse ADR-032 `SyncBeacon` (28 bytes, HMAC) |
|
||||
| `ROLE_ASSIGN` | event-driven | Coordinator | Node ID → role mapping |
|
||||
| `CHANNEL_PLAN` | event-driven | Coordinator | Per-node channel + dwell schedule |
|
||||
| `CALIBRATION_START` | event-driven | Coordinator | Synchronized calibration burst |
|
||||
| `FEATURE_DELTA` | 1–10 Hz | Observer / Relay | Compact feature delta (see Layer 4) |
|
||||
| `HEALTH` | 1 Hz | All | `rv_node_status_t` (see below) |
|
||||
| `ANOMALY_ALERT` | event-driven | Observer | Phase-physics violation, multi-link mismatch |
|
||||
|
||||
Node status payload:
|
||||
|
||||
```c
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint8_t node_id[8];
|
||||
uint64_t local_time_us;
|
||||
uint8_t role;
|
||||
uint8_t current_channel;
|
||||
uint8_t current_bw;
|
||||
int8_t noise_floor_dbm;
|
||||
uint16_t pkt_yield;
|
||||
uint16_t sync_error_us;
|
||||
uint16_t health_flags;
|
||||
} rv_node_status_t;
|
||||
```
|
||||
|
||||
Time-sync target is an engineering goal, not a guaranteed constant — it
|
||||
depends on the clock quality of the chosen radio family. The first
|
||||
acceptance test (Phase 2) measures it on real hardware.
|
||||
|
||||
### Layer 4 — On-device feature extraction
|
||||
|
||||
Defined in `firmware/esp32-csi-node/main/rv_feature_state.h`. Single
|
||||
on-the-wire packet, **60 bytes packed** (verified by `_Static_assert` and
|
||||
host unit test), magic `0xC5110006` (next free after ADR-039's
|
||||
`0xC5110002`, ADR-069's `0xC5110003`, ADR-063's `0xC5110004`, and ADR-039's
|
||||
compressed `0xC5110005`):
|
||||
|
||||
```c
|
||||
#define RV_FEATURE_STATE_MAGIC 0xC5110006u
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t magic; /* RV_FEATURE_STATE_MAGIC */
|
||||
uint8_t node_id;
|
||||
uint8_t mode; /* RV_PROFILE_* identifier */
|
||||
uint16_t seq; /* monotonic per-node sequence */
|
||||
uint64_t ts_us; /* node-local microseconds */
|
||||
float motion_score;
|
||||
float presence_score;
|
||||
float respiration_bpm;
|
||||
float respiration_conf;
|
||||
float heartbeat_bpm;
|
||||
float heartbeat_conf;
|
||||
float anomaly_score;
|
||||
float env_shift_score;
|
||||
float node_coherence;
|
||||
uint16_t quality_flags;
|
||||
uint16_t reserved;
|
||||
uint32_t crc32; /* IEEE polynomial over bytes [0..end-4] */
|
||||
} rv_feature_state_t;
|
||||
|
||||
_Static_assert(sizeof(rv_feature_state_t) == 60,
|
||||
"rv_feature_state_t must be 60 bytes on the wire");
|
||||
```
|
||||
|
||||
Three windows feed it: 100 ms (motion), 1 s (respiration), 5 s (baseline /
|
||||
env shift). Each `rv_feature_state_t` represents the most recent state of
|
||||
all three; mode field tells the receiver which window dominates this
|
||||
update.
|
||||
|
||||
`rv_feature_state_t` does not replace ADR-039's `edge_vitals_pkt_t`
|
||||
(0xC5110002) or ADR-063's `edge_fused_vitals_pkt_t` (0xC5110004). Those
|
||||
remain the wire format for vitals-specific consumers. `rv_feature_state_t`
|
||||
is the **default upstream payload** for the sensing pipeline; vitals
|
||||
packets are now an alternate emission mode for backward compatibility.
|
||||
|
||||
### Layer 5 — Rust handoff
|
||||
|
||||
The Rust side sees only two streams from a node:
|
||||
|
||||
1. **`feature_state` stream** — `rv_feature_state_t`, default-on, 1–10 Hz.
|
||||
2. **`debug_csi_frame` stream** — ADR-018 raw frames (magic 0xC5110001),
|
||||
default-off, opt-in via NVS or `CHANNEL_PLAN`. Used for calibration,
|
||||
debugging, training-set capture.
|
||||
|
||||
The Rust handoff is mirrored as a trait in
|
||||
`crates/wifi-densepose-hardware/src/radio_ops.rs` so test harnesses (and
|
||||
eventually the Rust-side controller for centralized coordinator nodes) can
|
||||
swap radio backends without touching `wifi-densepose-signal`,
|
||||
`wifi-densepose-ruvector`, `wifi-densepose-train`, or
|
||||
`wifi-densepose-mat`. Rust-side mirror trait is **out of scope for the
|
||||
firmware-only PR** that ships this ADR; tracked as Phase 4 follow-up.
|
||||
|
||||
## State Machine
|
||||
|
||||
```
|
||||
BOOT → SELF_TEST → RADIO_INIT → TIME_SYNC → CALIBRATION → SENSE_IDLE
|
||||
↓ ↑
|
||||
SENSE_ACTIVE
|
||||
↓
|
||||
ALERT
|
||||
↓
|
||||
DEGRADED
|
||||
```
|
||||
|
||||
Transitions:
|
||||
|
||||
- **CALIBRATION** on boot, on role change, on sustained inter-node
|
||||
disagreement.
|
||||
- **SENSE_ACTIVE** when motion or anomaly score crosses threshold.
|
||||
- **DEGRADED** when packet yield, sync quality, or memory pressure drops
|
||||
below threshold; falls back to ADR-039 Tier-0 raw passthrough as the
|
||||
last-resort survivable mode.
|
||||
|
||||
## Data budgets
|
||||
|
||||
| Stream | Default rate | Notes |
|
||||
|-------------------------|-----------------------------|----------------------------------------------|
|
||||
| Raw capture (internal) | 50–200 pps per observer | Stays on-device unless debug stream enabled |
|
||||
| `rv_feature_state_t` | 1–10 Hz per node | Default upstream |
|
||||
| `ANOMALY_ALERT` | event-driven | Burst-bounded |
|
||||
| Debug ADR-018 raw CSI | 0 (off by default) | Burst-only via `CHANNEL_PLAN` debug flag |
|
||||
|
||||
ADR-039 measured raw CSI at ~5 KB/frame and ~100 KB/s per node. The default
|
||||
upstream with ADR-081's 60-byte `rv_feature_state_t` at 5 Hz is **300 B/s
|
||||
per node — a 99.7% reduction**. A 50-node deployment at 5 Hz fits in
|
||||
15 KB/s total, easily carried by a single-AP backhaul.
|
||||
|
||||
## Channel planning policy
|
||||
|
||||
Codified rules — these are constraints on the controller, not just defaults:
|
||||
|
||||
- Keep one anchor on a stable channel; observers distributed across the
|
||||
least-congested channels.
|
||||
- Rotate **one** observer at a time. Never change all nodes simultaneously.
|
||||
- Pin `RV_PROFILE_RESP_HIGH_SENS` to the quietest stable channel for the
|
||||
duration of a respiration window.
|
||||
- Use a short active burst on a quiet channel for calibration, then return
|
||||
to passive capture.
|
||||
|
||||
This generalizes the per-deployment policy in ADR-073 ("node 1: ch 1/6/11,
|
||||
node 2: ch 3/5/9") into a controller-driven plan that the coordinator can
|
||||
publish via `CHANNEL_PLAN`. IEEE 802.11bf is the standards direction this
|
||||
points toward.
|
||||
|
||||
## Security & integrity
|
||||
|
||||
- Every `FEATURE_DELTA` carries node id, monotonic seq, ts_us, and CRC32
|
||||
(IEEE polynomial), per the struct above.
|
||||
- Every control message (`ROLE_ASSIGN`, `CHANNEL_PLAN`, `CALIBRATION_START`)
|
||||
carries sender role, epoch, replay window index, and authorization class,
|
||||
reusing the HMAC-SHA256 + 16-frame replay window from ADR-032
|
||||
(`secure_tdm.rs`).
|
||||
- Optional Ed25519 signature at session/batch granularity for signed
|
||||
`CHANNEL_PLAN` and `CALIBRATION_START` messages, reusing the
|
||||
ADR-040/RVF Ed25519 path already shipping in firmware.
|
||||
|
||||
## Reuse map (do not rewrite)
|
||||
|
||||
| Concern | Existing component |
|
||||
|-----------------------------|----------------------------------------------------------------------------------------------------------|
|
||||
| ADR-018 binary frame | `firmware/esp32-csi-node/main/csi_collector.c` (magic `0xC5110001`) |
|
||||
| ESP32 CSI driver glue | `firmware/esp32-csi-node/main/csi_collector.c:225-303` |
|
||||
| Channel hopping | `csi_collector_set_hop_table()` and `csi_collector_start_hop_timer()` |
|
||||
| NDP injection | `csi_inject_ndp_frame()` (placeholder, sufficient for L1 binding) |
|
||||
| TDM scheduling | `crates/wifi-densepose-hardware/src/esp32/tdm.rs` |
|
||||
| Secure beacons | `crates/wifi-densepose-hardware/src/esp32/secure_tdm.rs` (HMAC + replay) |
|
||||
| Edge intelligence (Tier 1/2)| `firmware/esp32-csi-node/main/edge_processing.c` (magic `0xC5110002`/`0xC5110005`) |
|
||||
| Fused vitals | ADR-063 `edge_fused_vitals_pkt_t` (magic `0xC5110004`) |
|
||||
| Swarm bridge | `firmware/esp32-csi-node/main/swarm_bridge.c` |
|
||||
| WASM Tier 3 modules | `firmware/esp32-csi-node/main/wasm_runtime.c` (ADR-040) |
|
||||
| Multistatic fusion | `crates/wifi-densepose-ruvector/src/viewpoint/fusion.rs` |
|
||||
| Adaptive classifier | `crates/wifi-densepose-sensing-server/src/adaptive_classifier.rs:61-75` |
|
||||
| Feature primitives (Rust) | `crates/wifi-densepose-signal/src/{motion.rs,features.rs,ruvsense/coherence.rs}` |
|
||||
|
||||
## Implementation status (2026-04-19)
|
||||
|
||||
This ADR ships **with** the initial implementation, not ahead of it.
|
||||
Artifacts delivered alongside the ADR:
|
||||
|
||||
| Component | File | State |
|
||||
|-----------------------------------------|-------------------------------------------------------------------------|-------------|
|
||||
| L1 vtable + profile/mode/health enums | `firmware/esp32-csi-node/main/rv_radio_ops.h` | Implemented |
|
||||
| L1 ESP32 binding | `firmware/esp32-csi-node/main/rv_radio_ops_esp32.c` | Implemented |
|
||||
| L1 Mock (QEMU) binding | `firmware/esp32-csi-node/main/rv_radio_ops_mock.c` | Implemented |
|
||||
| L2 Controller FreeRTOS plumbing | `firmware/esp32-csi-node/main/adaptive_controller.c` | Implemented |
|
||||
| L2 Pure decision policy (testable) | `firmware/esp32-csi-node/main/adaptive_controller_decide.c` | Implemented |
|
||||
| L3 Mesh-plane types + encoder/decoder | `firmware/esp32-csi-node/main/rv_mesh.{h,c}` | Implemented |
|
||||
| L3 HEALTH emit (slow loop, 30 s) | `adaptive_controller.c:slow_loop_cb()` | Implemented |
|
||||
| L3 ANOMALY_ALERT on state transition | `adaptive_controller.c:apply_decision()` | Implemented |
|
||||
| L3 Role tracking + epoch monotonicity | `adaptive_controller.c` (`s_role`, `s_mesh_epoch`) | Implemented |
|
||||
| L4 Feature state packet + helpers | `firmware/esp32-csi-node/main/rv_feature_state.{h,c}` | Implemented |
|
||||
| L4 Emitter from fast loop (5 Hz) | `adaptive_controller.c:emit_feature_state()` | Implemented |
|
||||
| L1 Packet yield + send-fail accessors | `csi_collector.c:csi_collector_get_pkt_yield_per_sec()` + send fail | Implemented |
|
||||
| L5 Rust mirror trait + mesh decoder | `crates/wifi-densepose-hardware/src/radio_ops.rs` | Implemented |
|
||||
| Host C unit tests (60 assertions) | `firmware/esp32-csi-node/tests/host/` | **60/60 ✓** |
|
||||
| Rust unit tests (8 assertions) | `crates/wifi-densepose-hardware` (`radio_ops::tests`) | **8/8 ✓** |
|
||||
| QEMU validator hooks (3 new checks) | `scripts/validate_qemu_output.py` (check 17/18/19) | Passing |
|
||||
| L3 mesh RX path (receive + dispatch) | — | Phase 3.5 |
|
||||
| Ed25519 signing for CHANNEL_PLAN etc. | — | Phase 3.5 |
|
||||
| Hardware validation on COM7 | — | Pending |
|
||||
|
||||
## Measured performance
|
||||
|
||||
Host-side benchmarks (`firmware/esp32-csi-node/tests/host/`), x86-64,
|
||||
gcc `-O2`, 2026-04-19. Numbers are illustrative of algorithmic cost on
|
||||
a modern CPU; on-target ESP32-S3 Xtensa LX7 at 240 MHz is ~5–10×
|
||||
slower for bit-by-bit CRC and broadly comparable for the decide
|
||||
function after inlining.
|
||||
|
||||
| Operation | Cost per call | Notes |
|
||||
|---------------------------------------------|---------------------|-------------------------------------|
|
||||
| `adaptive_controller_decide()` | **3.2 ns** (host) | O(1) policy, 9 branches evaluated |
|
||||
| `rv_feature_state_crc32()` (56 B hashed) | **612 ns** (host) | 87 MB/s — bit-by-bit IEEE CRC32 |
|
||||
| `rv_feature_state_finalize()` (full) | **592 ns** (host) | CRC-dominated |
|
||||
| `rv_mesh_encode_health()` + `_decode()` | **1010 ns** (host) | Full roundtrip, hdr+payload+CRC |
|
||||
|
||||
Projected on-target cost at 5 Hz cadence:
|
||||
|
||||
| Budget | Value |
|
||||
|--------------------------------------------|---------------------|
|
||||
| Controller fast-loop tick work (ESP32-S3) | < 10 μs (est.) |
|
||||
| CRC32 per feature packet (ESP32-S3) | ~3–6 μs (est.) |
|
||||
| Feature-state emit cost @ 5 Hz | ~30 μs/sec (0.003%) |
|
||||
| UDP send cost (existing stream_sender) | — unchanged — |
|
||||
|
||||
**Bandwidth:**
|
||||
|
||||
| Mode | Rate |
|
||||
|---------------------------------------------|-------------|
|
||||
| Raw ADR-018 CSI (pre-ADR-081) | ~100 KB/s |
|
||||
| ADR-039 compressed CSI (Tier 1) | ~50–70 KB/s |
|
||||
| ADR-039 vitals packet (32 B @ 1 Hz) | 32 B/s |
|
||||
| **ADR-081 feature state (60 B @ 5 Hz)** | **300 B/s** |
|
||||
|
||||
**Memory:**
|
||||
|
||||
| Component | Static RAM |
|
||||
|---------------------------------------------|---------------------|
|
||||
| Controller state (s_cfg + s_last_obs + …) | ~80 bytes |
|
||||
| Feature-state emit packet (stack, per tick) | 60 bytes |
|
||||
| CRC lookup table | 0 (bit-by-bit) |
|
||||
| Three FreeRTOS software timers | ~3 × 56 B overhead |
|
||||
|
||||
**Tests:**
|
||||
|
||||
| Suite | Assertions | Result |
|
||||
|---------------------------------------------|-----------:|------------|
|
||||
| `test_adaptive_controller` (host C) | 18 | **PASS** |
|
||||
| `test_rv_feature_state` (host C) | 15 | **PASS** |
|
||||
| `test_rv_mesh` (host C) | 27 | **PASS** |
|
||||
| `radio_ops::tests` (Rust) | 8 | **PASS** |
|
||||
| **Total** | **68** | **68/68** |
|
||||
| QEMU validator (`ADR-061` pipeline) | +3 checks | hooked |
|
||||
|
||||
Cross-language parity: the Rust `crc32_ieee()` is verified against the
|
||||
same known vectors used by the C test (`0xCBF43926` for `"123456789"`,
|
||||
`0xD202EF8D` for a single zero byte), and the `mesh_constants_match_firmware`
|
||||
test asserts `MESH_MAGIC`, `MESH_VERSION`, `MESH_HEADER_SIZE`, and
|
||||
`MESH_MAX_PAYLOAD` match the C header byte-for-byte. Any drift between
|
||||
the two implementations fails CI.
|
||||
|
||||
## New components this ADR authorizes
|
||||
|
||||
| New file | Purpose |
|
||||
|-------------------------------------------------------------------------------------------|--------------------------------------------------------|
|
||||
| `firmware/esp32-csi-node/main/rv_radio_ops.h` | `rv_radio_ops_t` vtable + profile/mode/health enums |
|
||||
| `firmware/esp32-csi-node/main/rv_radio_ops_esp32.c` | ESP32 binding wrapping `csi_collector` + `esp_wifi_*` |
|
||||
| `firmware/esp32-csi-node/main/rv_feature_state.h` | `rv_feature_state_t` packet + `RV_FEATURE_STATE_MAGIC` |
|
||||
| `firmware/esp32-csi-node/main/adaptive_controller.h` | Controller API + observation/decision structs |
|
||||
| `firmware/esp32-csi-node/main/adaptive_controller.c` | 200 ms / 1 s / 30 s loops, FreeRTOS task |
|
||||
| `crates/wifi-densepose-hardware/src/radio_ops.rs` *(Phase 4 follow-up)* | Rust mirror trait for backend swapping |
|
||||
|
||||
## Roadmap
|
||||
|
||||
| Phase | Scope | Status |
|
||||
|-------|--------------------------------------------|--------------------------------------------------|
|
||||
| 1 | Single supported-CSI node + features → Rust | Largely done via ADR-018, ADR-039 |
|
||||
| 2 | 3-node Seed v2 mesh + time-sync + plan | Partially done (ADR-029, ADR-066, ADR-073) |
|
||||
| 3 | Adaptive controller, delta reporting, DEGRADED | **This ADR** authorizes the firmware skeleton |
|
||||
| 4 | Cross-chipset bindings (Nexmon, custom) | Reserved; gated by Phase 3 stability |
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. **Portability gate.** A second `rv_radio_ops_t` binding (mock or
|
||||
alternate chipset) compiles and runs the controller + mesh plane code
|
||||
unchanged. The signal/ruvector/train/mat crates compile against a Rust
|
||||
mirror trait without modification.
|
||||
2. **Mesh resilience benchmark.** A 3-node prototype maintains stable
|
||||
`presence_score` and `motion_score` when one observer changes channel
|
||||
or drops out for 5 seconds.
|
||||
3. **Default upstream is compact.** Raw ADR-018 CSI is off by default; the
|
||||
default upstream is `rv_feature_state_t` at 1–10 Hz.
|
||||
4. **Integrity.** Every `FEATURE_DELTA` carries node id, seq, ts_us, CRC32.
|
||||
Every control message carries epoch + replay-window + authorization
|
||||
class, verified against ADR-032's existing HMAC machinery.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- The firmware hack is no longer the moat. The 5 layers are explicit and
|
||||
separately testable.
|
||||
- Default upstream bandwidth drops ~99% vs. raw ADR-018, making 50+ node
|
||||
deployments practical.
|
||||
- A documented vtable + Kconfig surface gates new features ("which layer
|
||||
does this belong in?") instead of letting them accrete inline.
|
||||
- Adaptive control of cadence, channel, and role becomes a first-class
|
||||
firmware concern — the user-facing knob ("be smarter when busy, save
|
||||
power when idle") finally has a home.
|
||||
|
||||
### Negative
|
||||
|
||||
- An abstraction tax on the single-chipset case: `rv_radio_ops_t` is a
|
||||
vtable for a family currently of size 1.
|
||||
- Adds ~5–8 KB SRAM for controller state and the new feature-state ring.
|
||||
- Requires re-routing existing `swarm_bridge` traffic through the mesh
|
||||
plane message types over time (incremental, not breaking).
|
||||
|
||||
### Neutral
|
||||
|
||||
- This ADR introduces no new dependencies, no new networking stacks, and
|
||||
no new hardware requirements.
|
||||
- ADR-039, ADR-063, ADR-066, ADR-069, ADR-073 are **not superseded**; they
|
||||
are reframed as components of Layer 3 / Layer 4.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
# Host-side C unit tests (no ESP-IDF, no QEMU required)
|
||||
cd firmware/esp32-csi-node/tests/host
|
||||
make check
|
||||
# → test_adaptive_controller: 18/18 pass, decide() = 3.2 ns/call
|
||||
# → test_rv_feature_state: 15/15 pass, CRC32(56 B) = 612 ns/pkt
|
||||
# → test_rv_mesh: 27/27 pass, HEALTH roundtrip = 1.0 µs
|
||||
|
||||
# Rust-side radio_ops trait + mesh decoder tests
|
||||
cd v2
|
||||
cargo test -p wifi-densepose-hardware --no-default-features --lib radio_ops
|
||||
# → 8 passed; verifies MockRadio, CRC32 parity with firmware vectors,
|
||||
# HEALTH encode/decode roundtrip, bad-magic/short/CRC rejection,
|
||||
# and that MESH_MAGIC/VERSION/HEADER_SIZE match rv_mesh.h
|
||||
|
||||
# QEMU end-to-end (requires ESP-IDF + qemu-system-xtensa, see ADR-061)
|
||||
bash scripts/qemu-esp32s3-test.sh
|
||||
# → Validator now runs 19 checks; new ADR-081 checks 17/18/19 verify
|
||||
# adaptive_ctrl boot line, rv_radio_mock binding registration, and
|
||||
# slow-loop heartbeat.
|
||||
|
||||
# Full workspace
|
||||
cargo test --workspace --no-default-features
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
ADR-018, ADR-028, ADR-029, ADR-030, ADR-031, ADR-032, ADR-039, ADR-040,
|
||||
ADR-060, ADR-061, ADR-063, ADR-066, ADR-069, ADR-073, ADR-078.
|
||||
@@ -1,185 +0,0 @@
|
||||
# ADR-082: Pose Tracker Confirmed-Track Output Filter
|
||||
|
||||
| Field | Value |
|
||||
|-------------|-----------------------------------------------------------------------|
|
||||
| **Status** | Accepted — implemented in commit landing this ADR |
|
||||
| **Date** | 2026-04-25 |
|
||||
| **Authors** | ruv |
|
||||
| **Issue** | [#420 — "24 ghost people in the UI with 3× ESP32-S3 nodes"](https://github.com/ruvnet/RuView/issues/420) |
|
||||
| **Depends** | ADR-026 (track lifecycle), ADR-024 (AETHER re-ID embeddings) |
|
||||
|
||||
## Context
|
||||
|
||||
Multiple users running the Rust sensing server with 3 ESP32-S3 nodes have
|
||||
reported the same symptom: the live UI renders 22–24 phantom skeletons that
|
||||
flicker at high rate, while `GET /api/v1/sensing/latest` correctly reports
|
||||
`estimated_persons: 1`. The problem is reproducible across both Docker and
|
||||
native deployments and is independent of the firmware MGMT-only mitigation
|
||||
shipped for #396.
|
||||
|
||||
The two-number contradiction (1 in the snapshot, ~24 in the WebSocket stream)
|
||||
narrows the bug to the path that produces `update.persons`. That path is
|
||||
`tracker_bridge::tracker_update` → `tracker_bridge::tracker_to_person_detections`
|
||||
→ WebSocket frame.
|
||||
|
||||
### Pose tracker lifecycle (per ADR-026)
|
||||
|
||||
`signal::ruvsense::pose_tracker::TrackLifecycleState` has four states:
|
||||
|
||||
```
|
||||
Tentative -> Active -> Lost -> Terminated
|
||||
```
|
||||
|
||||
The state machine and its predicates:
|
||||
|
||||
| State | `is_alive()` | `accepts_updates()` | Meaning |
|
||||
|--------------|--------------|---------------------|---------|
|
||||
| `Tentative` | true | true | New detection, < 2 confirmed hits |
|
||||
| `Active` | true | true | Confirmed track, currently observed |
|
||||
| `Lost` | **true** | false | Confirmed track, missed `loss_misses` updates, still inside `reid_window` |
|
||||
| `Terminated` | false | false | Removed on next `prune_terminated()` |
|
||||
|
||||
`PoseTracker::active_tracks()` filters by `is_alive()`, which means it returns
|
||||
`Tentative ∪ Active ∪ Lost` — every track that has not yet been Terminated.
|
||||
|
||||
### Root cause
|
||||
|
||||
`crates/wifi-densepose-sensing-server/src/tracker_bridge.rs` exposes the
|
||||
tracker output to the WebSocket stream via:
|
||||
|
||||
```rust
|
||||
/// Convert active PoseTracker tracks back into server-side PersonDetection values.
|
||||
///
|
||||
/// Only tracks whose lifecycle `is_alive()` are included.
|
||||
pub fn tracker_to_person_detections(tracker: &PoseTracker) -> Vec<PersonDetection> {
|
||||
tracker
|
||||
.active_tracks()
|
||||
.into_iter()
|
||||
.map(|track| { /* ... */ })
|
||||
.collect()
|
||||
}
|
||||
```
|
||||
|
||||
The doc comment is correct as a description of `is_alive()`, but `is_alive()`
|
||||
is the wrong gate for *rendering*. `Lost` tracks have not received a
|
||||
measurement in `loss_misses` ticks; they are kept around only so the
|
||||
re-identification machinery can attempt to match them when a similar
|
||||
detection reappears within `reid_window`. They are not currently observed and
|
||||
must not appear as live skeletons in the UI.
|
||||
|
||||
With 3 ESP32-S3 nodes streaming CSI at ~10 Hz each, `derive_pose_from_sensing`
|
||||
emits a per-node detection every tick. Detections that fall outside the
|
||||
Mahalanobis gate (cost ≥ 9.0) cannot match an existing track, so a new
|
||||
`Tentative` track is created and the previous one ages into `Lost`. With
|
||||
`reid_window ≈ 30` ticks (~3 s at 10 Hz), up to 30 ticks × 3 nodes ≈ 90
|
||||
phantom Lost tracks can co-exist before any of them reach `Terminated`.
|
||||
The actually-observed-now person is one of them; the other ~22–89 are ghosts.
|
||||
|
||||
The snapshot endpoint `/api/v1/sensing/latest` reads `estimated_persons` from
|
||||
the multistatic eigenvalue counter (`signal::ruvsense::field_model`), which
|
||||
operates on the CSI data directly and reports 1. The WebSocket stream reads
|
||||
`update.persons`, which is the unfiltered `is_alive()` set — hence the
|
||||
22-vs-1 mismatch.
|
||||
|
||||
This is a documentation/implementation discrepancy in `tracker_bridge`, not a
|
||||
flaw in the lifecycle state machine itself.
|
||||
|
||||
## Decision
|
||||
|
||||
Introduce a **confirmed-track filter** at the bridge boundary that returns
|
||||
only tracks the UI is meant to render:
|
||||
|
||||
* `Active` — confirmed and currently observed; always render.
|
||||
* `Tentative` — confirmed for the *current* tick (created or matched this
|
||||
cycle); render so first-frame visibility latency stays at one tick.
|
||||
* `Lost` — **never** render. They exist only to support re-ID over the
|
||||
`reid_window` and have, by definition, not been observed for at least
|
||||
`loss_misses` ticks.
|
||||
* `Terminated` — never render (already excluded by `is_alive()`).
|
||||
|
||||
### Naming
|
||||
|
||||
Add `PoseTracker::confirmed_tracks()` — the name reflects "tracks the system
|
||||
is currently confirming a person is present at this position." Keep
|
||||
`active_tracks()` unchanged so callers that legitimately need the re-ID set
|
||||
(re-identification, soft-confidence overlays, debug UIs) still have it.
|
||||
|
||||
The bridge’s public surface stays the same; only the internal accessor
|
||||
swaps. WebSocket consumers see the corrected `update.persons` automatically.
|
||||
|
||||
### Why include `Tentative`
|
||||
|
||||
A walking person’s first detection lands in `Tentative` until two consecutive
|
||||
hits arrive (~0.1 s at 10 Hz). Excluding `Tentative` makes the UI
|
||||
under-render by one tick on every entry; the gain (filtering out spurious
|
||||
single-detection ghosts) is real but small relative to the much larger Lost
|
||||
problem and isn’t worth the visible latency. If single-tick ghosts become
|
||||
the dominant complaint after this ADR ships, escalate to `Active`-only and
|
||||
revisit `birth_hits` calibration.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
* `update.persons.length` matches `estimated_persons` within ±1 (Tentative
|
||||
vs. Active hand-off frame) under steady state. #420 closed.
|
||||
* No change to the lifecycle state machine, no change to `reid_window` or
|
||||
`loss_misses`, no change to the WebSocket schema. Pure filter at egress.
|
||||
* `PoseTracker::active_tracks()` keeps its semantics for re-ID consumers;
|
||||
this avoids breaking ADR-024 (AETHER) call sites.
|
||||
|
||||
### Negative / risks
|
||||
|
||||
* Existing test `test_tracker_update_stable_ids` exercises three sequential
|
||||
identical-person updates and asserts the ID is stable across all three.
|
||||
Filtering Lost out doesn’t affect it (the track stays in `Tentative` →
|
||||
`Active`, never Lost during the test). Confirmed by reading the test;
|
||||
no regression expected.
|
||||
* Single-tick `Tentative` exposure means very-spurious one-frame detections
|
||||
*can* still flicker briefly. Acceptable trade-off as discussed above.
|
||||
|
||||
### Neutral
|
||||
|
||||
* `prune_terminated()` and the existing transition logic
|
||||
(`predict_all` → `mark_lost` → `terminate`) are unchanged.
|
||||
|
||||
## Implementation
|
||||
|
||||
1. **`signal::ruvsense::pose_tracker`** — add:
|
||||
```rust
|
||||
/// Tracks the UI is meant to render: Tentative + Active.
|
||||
/// Excludes Lost (re-ID candidates) and Terminated.
|
||||
pub fn confirmed_tracks(&self) -> Vec<&PoseTrack> {
|
||||
self.tracks
|
||||
.iter()
|
||||
.filter(|t| matches!(
|
||||
t.lifecycle,
|
||||
TrackLifecycleState::Tentative | TrackLifecycleState::Active
|
||||
))
|
||||
.collect()
|
||||
}
|
||||
```
|
||||
2. **`sensing-server::tracker_bridge`** — change
|
||||
`tracker_to_person_detections` to call `tracker.confirmed_tracks()` and
|
||||
update the doc comment to describe the new contract.
|
||||
3. **Regression test** in `tracker_bridge.rs::tests`:
|
||||
* Drive a track to `Active` over two updates.
|
||||
* Submit empty detections for `loss_misses + 1` predict cycles to push
|
||||
the track to `Lost`.
|
||||
* Assert `tracker_update(... empty ...)` returns an empty `Vec`.
|
||||
4. **Validation**: workspace tests + ESP32-S3 on COM7 streaming round-trip.
|
||||
|
||||
## Validation
|
||||
|
||||
* `cargo test --workspace --no-default-features` — must stay green
|
||||
(≥ 1,538 passed, 0 failed; new regression test adds one).
|
||||
* Live verification on ESP32 setup: WebSocket `update.persons.length`
|
||||
must equal `estimated_persons` ± 1 in steady state.
|
||||
|
||||
## Related
|
||||
|
||||
* ADR-026 — Track lifecycle state machine (this ADR doesn’t change it)
|
||||
* ADR-024 — AETHER re-ID embeddings (uses `active_tracks()`, unchanged)
|
||||
* PR #425 — Workspace `--no-default-features` build fix (unrelated, just
|
||||
the prior PR on this branch line)
|
||||
* Issue #420 — original report
|
||||
@@ -1,245 +0,0 @@
|
||||
# ADR-083: Per-Cluster Pi Compute Hop
|
||||
|
||||
| Field | Value |
|
||||
|----------------|--------------------------------------------------------------------------------------|
|
||||
| **Status** | Proposed — pending field evidence on three-tier proposal scope |
|
||||
| **Date** | 2026-04-26 |
|
||||
| **Authors** | ruv |
|
||||
| **Supersedes** | — |
|
||||
| **Refines** | ADR-028 (capability audit), ADR-081 (5-layer kernel), ADR-066 (swarm bridge) |
|
||||
| **Companion** | `docs/research/architecture/three-tier-rust-node.md`, `docs/research/architecture/decision-tree.md`, `docs/research/sota/2026-Q2-rf-sensing-and-edge-rust.md` |
|
||||
|
||||
## Context
|
||||
|
||||
ADR-028 established the per-node BOM at ~$9 (ESP32-S3 8MB) — ~$15 with a
|
||||
mmWave sensor — and ADR-081 framed the firmware as a 5-layer adaptive
|
||||
kernel running entirely on a single ESP32-S3 die. Both decisions are
|
||||
correct for the **per-node** dimension; deployments that fit the
|
||||
"sensor talks UDP to a server somewhere" shape work fine on this stack.
|
||||
|
||||
The three-tier-node research exploration
|
||||
(`docs/research/architecture/three-tier-rust-node.md`) raised a separate
|
||||
question: **what changes when a deployment scales past one or two rooms,
|
||||
and where should the heavy compute live?** The exploration's answer
|
||||
("dual ESP32-S3 + Pi Zero 2W per node") is one shape, but the
|
||||
companion decision-tree (`decision-tree.md` §1, §3 L3, §5) identifies a
|
||||
materially cheaper path: keep today's single-S3 sensor node unchanged
|
||||
and add **one Pi per cluster of 3–6 sensor nodes**. The 2026-Q2 SOTA
|
||||
survey (`sota/2026-Q2-rf-sensing-and-edge-rust.md`) confirms that the
|
||||
load this path needs to carry — model inference, QUIC backhaul, and a
|
||||
real secure-boot story — fits comfortably on a Pi-class SoC, while the
|
||||
load it doesn't need to carry — CSI capture, ISR-precise wake control —
|
||||
is exactly what the ESP32-S3 already does well.
|
||||
|
||||
The three things this ADR is about, all of which the current single-S3
|
||||
deployment shape pushes onto the cloud or onto every individual node:
|
||||
|
||||
1. **Per-deployment ML inference.** WiFlow / DT-Pose / GraphPose-Fi
|
||||
class models (4–10M params, 0.5–1.5 GFLOPs) want a Cortex-A53-class
|
||||
target. The ESP32-S3 cannot host these; the cloud can but only at
|
||||
the cost of round-trip latency. A per-cluster Pi inference hop is
|
||||
the natural home.
|
||||
2. **QUIC backhaul.** `quinn` + `rustls` is mature on Linux but does
|
||||
not run on ESP32-class hardware in any production-grade form
|
||||
(SOTA §5). A Pi terminating QUIC for a cluster gives every sensor
|
||||
node QUIC's loss/handoff/multiplex properties without porting QUIC
|
||||
to the MCU.
|
||||
3. **Secure-boot anchor for OTA.** ESP-IDF Secure Boot V2 covers each
|
||||
sensor node, but cluster-wide policy (which model is current, which
|
||||
sensor MCU image is canary, what is the rollout ring) needs a
|
||||
higher-trust local store. A Pi running buildroot + dm-verity +
|
||||
signed FIT is a defensible anchor without the BOM hit of CM4 / Pi 5
|
||||
(the latter is its own decision; see ADR-085 sketch below and
|
||||
decision-tree.md L6).
|
||||
|
||||
The cluster-Pi shape does **not** require any change to ADR-028 or
|
||||
ADR-081. The sensor node continues to be a single-MCU ESP32-S3 running
|
||||
the 5-layer kernel. Everything new lives at the cluster boundary.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt **a per-cluster Pi hop** as the canonical RuView mid-scale
|
||||
deployment shape. A "cluster" is **3–6 ESP32-S3 sensor nodes within
|
||||
WiFi mesh range of one Pi**.
|
||||
|
||||
Specifically:
|
||||
|
||||
1. **Sensor nodes are unchanged.** They continue to run the ADR-081
|
||||
5-layer kernel on a single ESP32-S3, emit `rv_feature_state_t`
|
||||
packets (60 byte, ~5 Hz, ~300 B/s) over UDP, and connect via
|
||||
ESP-WIFI-MESH or direct WiFi to the cluster Pi.
|
||||
2. **Each cluster has exactly one Pi** acting as:
|
||||
- **Sensor aggregator**: ingests UDP from all cluster sensor
|
||||
nodes, runs feature-level fusion (multistatic + viewpoint
|
||||
attention from the existing `wifi-densepose-ruvector` crate).
|
||||
- **ML inference target**: hosts the WiFi-pose model and runs
|
||||
inference at the cluster boundary, not on each sensor MCU.
|
||||
- **QUIC client to the cloud / gateway**: terminates QUIC mTLS,
|
||||
batches cluster-level events.
|
||||
- **OTA + secure-boot anchor for its sensor nodes**: holds signed
|
||||
manifests, stages canary rollouts, owns provisioning state.
|
||||
3. **Cluster Pi SoC choice is deferred** to a future ADR (sketched
|
||||
below as ADR-085). The acceptable candidates are Pi Zero 2W, Pi 4,
|
||||
Pi 5, and CM4. The decision tree's L6 distinguishes these by
|
||||
secure-boot threat model; this ADR does not pre-commit.
|
||||
4. **The single-node deployment shape is not deprecated.** A
|
||||
home-lab / single-room / development deployment can still run a
|
||||
single ESP32-S3 talking UDP directly to the existing
|
||||
`wifi-densepose-sensing-server`, no Pi required. The cluster Pi
|
||||
becomes the recommended shape for fleets ≥ 3 sensor nodes.
|
||||
|
||||
### Boundary contract
|
||||
|
||||
The cluster Pi exposes two interfaces:
|
||||
|
||||
| Interface | Direction | Schema |
|
||||
|------------------------|-------------------|-----------------------------------------------------------------------|
|
||||
| **UDP `rv_feature_state_t` ingest** | sensor → Pi | Existing 60-byte packed struct from ADR-081 (magic `0xC5110006`) |
|
||||
| **QUIC mTLS uplink** | Pi → gateway/cloud | New: cluster-level event envelope (CBOR), batched, ~10 KB/min upper bound |
|
||||
|
||||
Sensor → Pi is **the same wire as today's sensor → server**. Cluster Pi
|
||||
uplink is **new** and is what the existing `wifi-densepose-sensing-server`
|
||||
becomes — relocated from the user's laptop / container to the cluster
|
||||
node. Concretely: the sensing server already exists in
|
||||
`crates/wifi-densepose-sensing-server`; it cross-compiles to ARMv7 /
|
||||
AArch64 today via `cargo build --target aarch64-unknown-linux-gnu`. The
|
||||
relocation is a deployment change, not a re-implementation.
|
||||
|
||||
### Three-tier vs cluster hop
|
||||
|
||||
This ADR's cluster-Pi shape is the L3-hybrid path in
|
||||
`decision-tree.md` §2 — **not** the full three-tier (dual-MCU + per-node
|
||||
Pi) shape. It captures most of the value (ML, QUIC, secure-boot anchor)
|
||||
at minimal BOM impact. The full three-tier shape remains the long-term
|
||||
exploration target, blocked behind L4 (no_std CSI maturity) and L2
|
||||
(per-node ISR-jitter evidence).
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **Pose-grade ML on edge becomes deployable**, not just possible. A
|
||||
Pi (any of the eligible SoCs) hosts WiFlow-class models with
|
||||
≤ 100 ms latency per cluster, vs ≥ 1 s round-trip if pose runs in the
|
||||
cloud (SOTA §1, §3).
|
||||
- **QUIC arrives without an MCU port.** `quinn` + `rustls` runs on the
|
||||
Pi as it does on a server (SOTA §5). The sensor MCU keeps UDP — the
|
||||
cheapest, highest-tested wire it already speaks.
|
||||
- **Cluster-level secure boot becomes coherent.** Per-sensor Secure
|
||||
Boot V2 + flash encryption (ADR-028 baseline) is unchanged. The Pi
|
||||
buildroot + dm-verity image is the cluster trust anchor and signs
|
||||
the OTA manifests for its sensors. The cluster-level threat model is
|
||||
expressible without per-sensor BOM regression.
|
||||
- **No PCB respin.** Sensor nodes are bit-for-bit identical to today's
|
||||
ADR-028 baseline. The cluster Pi is a separate device on the cluster
|
||||
WiFi (and / or Ethernet, if available).
|
||||
- **Deployment cost scales sub-linearly with sensor count.** One
|
||||
$25–$60 Pi per 3–6 sensor nodes adds ~$5–$20 per sensor amortized,
|
||||
vs ~$25–$50 per sensor for the per-node-Pi shape.
|
||||
|
||||
### Negative
|
||||
|
||||
- **The cluster Pi is a new piece of infrastructure to provision,
|
||||
monitor, and update.** It is the right place for cluster-level
|
||||
responsibilities, but it is not free; it adds a Linux box to every
|
||||
multi-room deployment. Mitigated by buildroot images and the
|
||||
existing OTA tooling story (see Implementation §4).
|
||||
- **Cluster Pi failure takes the cluster offline** (sensor nodes
|
||||
cannot uplink without a working aggregator on the WiFi LAN). For
|
||||
high-availability deployments, this ADR is the floor; an HA-pair
|
||||
cluster Pi would be a follow-up.
|
||||
- **One more network hop on the sensing path.** Sensor → Pi → cloud
|
||||
adds ~5–20 ms over Sensor → cloud (depending on link quality).
|
||||
Pose latency budgets are 100s of ms, so this is well inside spec.
|
||||
|
||||
### Neutral
|
||||
|
||||
- ADR-028 (capability audit), ADR-081 (5-layer kernel), and ADR-066
|
||||
(swarm bridge) are unchanged. This ADR adds a new device class above
|
||||
the sensor; it does not modify the sensor itself.
|
||||
- The home-lab single-node shape continues to work; this ADR adds a
|
||||
recommended path for fleets, it does not deprecate the existing one.
|
||||
|
||||
## Implementation
|
||||
|
||||
The implementation is intentionally light because most of the pieces
|
||||
already exist; the ADR is largely about formalizing where they live.
|
||||
|
||||
1. **Cluster-Pi cross-compile target.** Add to
|
||||
`rust-port/wifi-densepose-rs/.cargo/config.toml` (or the equivalent
|
||||
per-crate target spec) an `aarch64-unknown-linux-gnu` target so
|
||||
`wifi-densepose-sensing-server` builds for Pi 4 / 5 / CM4 by
|
||||
default. Also retain `armv7-unknown-linux-gnueabihf` for Pi Zero 2W
|
||||
compatibility while the Pi-SoC decision (ADR-085 sketch) is open.
|
||||
2. **Cluster-Pi service unit.** Add a systemd unit file under
|
||||
`firmware/cluster-pi/` (new directory) that runs
|
||||
`wifi-densepose-sensing-server` with the cluster's UDP/QUIC ports
|
||||
and drops privileges. Buildroot integration is a separate ADR if
|
||||
the SoC choice goes to Pi Zero 2W (where there's no RPi-OS path).
|
||||
3. **QUIC uplink module.** Add `wifi-densepose-sensing-server` a
|
||||
feature-gated `quic-uplink` module using `quinn` + `rustls`. The
|
||||
feature is **off by default** in the home-lab shape and on for the
|
||||
cluster Pi.
|
||||
4. **OTA + signed-manifest flow.** Out of scope for this ADR; tracked
|
||||
as I4 in `decision-tree.md` §4. The cluster Pi's role is to *hold*
|
||||
the manifest store, not to define the manifest format. Use the
|
||||
existing ADR-066 swarm bridge channel for OTA staging.
|
||||
5. **Documentation update.** README's hardware-table gains a
|
||||
"Cluster compute" row. CLAUDE.md gets a one-paragraph cluster-Pi
|
||||
section under Architecture. User-guide gets a cluster-deployment
|
||||
section.
|
||||
6. **Validation.** A 3-sensor cluster + 1 Pi fixture in the lab.
|
||||
Pass criteria: end-to-end CSI → cluster fusion → cloud ingest;
|
||||
measured latency under 100 ms per cluster; cluster Pi reboot
|
||||
without sensor data loss > 5 s; OTA staging round-trip across all
|
||||
sensors in the cluster.
|
||||
|
||||
## Validation
|
||||
|
||||
This ADR is **proposed**, not accepted. Acceptance requires:
|
||||
|
||||
1. The cluster-Pi `wifi-densepose-sensing-server` cross-compiles
|
||||
cleanly on `aarch64-unknown-linux-gnu` and `armv7-unknown-linux-gnueabihf`
|
||||
targets with the existing workspace tests passing.
|
||||
2. A 3-sensor + 1-Pi field test demonstrates ≥ 4 hours stable
|
||||
end-to-end CSI → fusion → cloud round-trip with latency
|
||||
≤ 100 ms per cluster and zero phantom-skeleton regressions
|
||||
(ADR-082 holds across the new uplink).
|
||||
3. The cluster-Pi ↔ sensor secure-boot story is approved alongside
|
||||
ADR-085's SoC choice.
|
||||
|
||||
When the above pass, this ADR moves from **Proposed** → **Accepted**
|
||||
and the README + CLAUDE.md are updated to reflect cluster-Pi as the
|
||||
recommended fleet-shape.
|
||||
|
||||
## Related ADRs (current and proposed)
|
||||
|
||||
- **ADR-028** (Accepted) — ESP32 capability audit. Single-node BOM
|
||||
baseline. Unchanged by this ADR.
|
||||
- **ADR-029** (Proposed) — RuvSense multistatic sensing mode. Pairs
|
||||
naturally with cluster-Pi: cluster Pi is the natural home for
|
||||
multi-sensor fusion.
|
||||
- **ADR-066** — Swarm bridge to coordinator. The cluster-Pi is the
|
||||
per-cluster swarm coordinator endpoint.
|
||||
- **ADR-081** (Accepted) — 5-layer adaptive CSI mesh firmware kernel.
|
||||
Unchanged by this ADR.
|
||||
- **ADR-082** (Accepted) — Pose tracker confirmed-track output filter.
|
||||
Holds across UDP and QUIC uplinks identically.
|
||||
- **Future ADR (sketched in `decision-tree.md` L4)** — `no_std` CSI
|
||||
capture maturity benchmark. Gates the dual-MCU shape; not required
|
||||
for the cluster-Pi shape proposed here.
|
||||
- **Future ADR (sketched in `decision-tree.md` L6)** — Cluster-Pi SoC
|
||||
choice (Pi Zero 2W vs CM4 vs Pi 5). Pure secure-boot decision.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Cluster size sweet spot.** "3–6 nodes" is a planning estimate. The
|
||||
3-sensor lab fixture in §Implementation will inform whether the
|
||||
upper bound is closer to 4, 6, or 8 in practice.
|
||||
- **Cluster-Pi failure semantics.** Default behavior: sensor MCUs hold
|
||||
the last 60 s of feature packets in RAM and replay on reconnect.
|
||||
HA-pair cluster Pi is a separate ADR if needed.
|
||||
- **Mesh control-plane interaction.** If the deployment moves to
|
||||
Thread (decision-tree.md L5), the cluster Pi may need a Thread
|
||||
Border Router role. This ADR doesn't pre-commit; it's compatible
|
||||
with both ESP-WIFI-MESH and Thread futures.
|
||||
@@ -1,276 +0,0 @@
|
||||
# ADR-084: RaBitQ Similarity Sensor for CSI / Pose / Memory Routing
|
||||
|
||||
| Field | Value |
|
||||
|----------------|-----------------------------------------------------------------------------------------|
|
||||
| **Status** | Proposed |
|
||||
| **Date** | 2026-04-26 |
|
||||
| **Authors** | ruv |
|
||||
| **Refines** | ADR-024 (AETHER re-ID embeddings), ADR-027 (cross-environment domain generalization), ADR-076 (CSI spectrogram embeddings), ADR-081 (5-layer firmware kernel) |
|
||||
| **Companion** | ADR-083 (per-cluster Pi compute hop) |
|
||||
| **Implements** | `vendor/ruvector/crates/ruvector-core/src/quantization.rs::BinaryQuantized` |
|
||||
|
||||
## Context
|
||||
|
||||
RuView's signal pipeline already produces several **dense float
|
||||
embeddings** at different layers:
|
||||
|
||||
- AETHER 128-d re-ID embeddings on each `PoseTrack` (ADR-024)
|
||||
- 64–256-d CSI spectrogram embeddings (ADR-076)
|
||||
- per-room field-model eigenmode vectors (ADR-030)
|
||||
- per-frame multistatic fused vectors (ADR-029)
|
||||
|
||||
Every one of these eventually answers the same shape of question:
|
||||
**"have I seen something like this before?"** Today the answer is
|
||||
computed by full float dot-product / Mahalanobis comparisons against a
|
||||
candidate set. That cost grows linearly with stored vectors and
|
||||
quadratically when used inside dynamic-mincut graph maintenance,
|
||||
re-identification re-scoring, and cross-environment domain detection.
|
||||
|
||||
The vendored `ruvector-core` crate already ships a 1-bit quantization
|
||||
(`BinaryQuantized`, 32× compression, SIMD popcnt + hamming distance)
|
||||
that is functionally equivalent to the **RaBitQ** family of binary
|
||||
sketches: a vector is reduced to one bit per dimension, compared via
|
||||
hamming distance, and used as a coarse pre-filter before full
|
||||
precision refinement. The same module also exposes `ScalarQuantized`
|
||||
(int8, 4×) and `ProductQuantized` (PQ, 8–16×), so the tiered
|
||||
quantization story is already implemented; the *deployment pattern* is
|
||||
not.
|
||||
|
||||
The user observation that motivates this ADR: **RaBitQ-style sketches
|
||||
are not just a vector compression trick — they are a cheap similarity
|
||||
sensor.** Used as a sensor, they unlock:
|
||||
|
||||
- always-on novelty / anomaly gating that wakes heavy CNNs only on
|
||||
meaningful change
|
||||
- cluster-Pi memory routing (which shard / room / model to query first)
|
||||
- cross-node mesh exchange of compressed sketches instead of raw vectors
|
||||
- privacy-preserving event logs (sketches, not reconstructable signals)
|
||||
|
||||
This ADR formalizes the deployment pattern across the RuView stack and
|
||||
commits to `ruvector::quantization::BinaryQuantized` as the canonical
|
||||
implementation.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt **RaBitQ-style binary sketches as a first-class, cheap
|
||||
similarity sensor** at four points in the RuView pipeline:
|
||||
|
||||
1. **CSI / pose embedding hot-cache filter** at the cluster Pi.
|
||||
2. **Drift / novelty sensor** between live observation and a
|
||||
per-room normal-state bank.
|
||||
3. **Mesh-exchange compression** between sensor nodes when reporting
|
||||
cross-cluster events.
|
||||
4. **Privacy-preserving event log** at the cluster Pi and gateway.
|
||||
|
||||
The canonical pattern at every point is:
|
||||
|
||||
```text
|
||||
dense embedding ──► RaBitQ sketch ──► hamming/popcnt compare
|
||||
├──► candidate set (top-K)
|
||||
└──► novelty score (0..1)
|
||||
│
|
||||
▼
|
||||
┌── below threshold ──► emit summary, no escalation
|
||||
│
|
||||
└── above threshold ──► full-precision refinement
|
||||
├──► ruvector mincut / HNSW
|
||||
├──► AETHER re-ID rescoring
|
||||
└──► pose model / CNN wake
|
||||
```
|
||||
|
||||
### Implementation home
|
||||
|
||||
- **Sketch type and SIMD primitives**:
|
||||
`vendor/ruvector/crates/ruvector-core/src/quantization.rs::BinaryQuantized`
|
||||
— already implemented, already SIMD-accelerated (NEON on aarch64,
|
||||
POPCNT on x86_64). Re-export through a new
|
||||
`crates/wifi-densepose-ruvector/src/sketch.rs` module so consumers in
|
||||
`signal`, `train`, `mat`, and `sensing-server` see a stable
|
||||
RuView-flavored API and don't bind directly to the vendor crate.
|
||||
|
||||
- **Per-room normal-state bank**: lives at the cluster Pi (ADR-083),
|
||||
not on the sensor MCU. Sensor MCUs continue to emit dense embeddings
|
||||
in the existing `rv_feature_state_t` packet shape; sketching happens
|
||||
on the Pi where the candidate bank is.
|
||||
|
||||
- **Sketch versioning**: each sketch carries a 16-bit `sketch_version`
|
||||
field so the Pi can tell incompatible sketches apart when an
|
||||
embedding model upgrades. Bumped on every embedding-model change.
|
||||
|
||||
### Where the sensor sits in the pipeline
|
||||
|
||||
| Pipeline stage | Today (full float) | With RaBitQ similarity sensor |
|
||||
|---|---|---|
|
||||
| AETHER re-ID match | full 128-d cosine on every active track × candidate | hamming pre-filter to top-K, then full cosine on K |
|
||||
| Mincut subcarrier selection | full graph re-evaluation | sketch-flagged "likely-changed" boundary edges, full mincut on those |
|
||||
| CSI room fingerprint | trained classifier on full embedding | sketch hamming to per-room sketch, classifier on miss |
|
||||
| Field-model novelty (ADR-030) | residual-energy threshold | sketch novelty as second gate before SVD redo |
|
||||
| Mesh / inter-cluster sync | dense embedding broadcast | sketch broadcast; full vector only on miss |
|
||||
| Event log retention | full embedding stored | sketch + witness hash stored; raw embedding ephemeral |
|
||||
|
||||
In every row, the **decision boundary is unchanged** — full precision
|
||||
still owns the final answer. The sketch is a sensor that only gates
|
||||
which comparisons run, not what they decide.
|
||||
|
||||
### Acceptance criterion (per the source proposal)
|
||||
|
||||
The system-level acceptance test is:
|
||||
|
||||
> RaBitQ should reduce compare cost by **8× to 30×** while preserving
|
||||
> top-k decisions well enough that full refinement changes **fewer
|
||||
> than 10%** of final results.
|
||||
|
||||
Concretely, this means:
|
||||
|
||||
- Sketch compare must be measurably **8× cheaper** than the float
|
||||
comparison it replaces (criterion-bench in `signal/`).
|
||||
- Top-K candidate set chosen by sketch must contain ≥ 90% of the
|
||||
candidates the full-float pass would have picked (offline replay
|
||||
against recorded CSI).
|
||||
- End-to-end pose / re-ID accuracy must regress by **less than 1
|
||||
percentage point** vs the full-float baseline on the existing
|
||||
evaluation set.
|
||||
|
||||
If any of these three fail, the sensor is rolled back at that point in
|
||||
the pipeline and the failing site reverts to full float; the rest of
|
||||
the pipeline keeps using sketches. This is point-by-point, not
|
||||
all-or-nothing.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- **Cheaper hot path everywhere a "have I seen this" question lives.**
|
||||
AETHER re-ID, mincut maintenance, room fingerprinting, novelty
|
||||
detection, mesh sync, and event-log retention all run a 32×-smaller,
|
||||
popcnt-friendly comparison first.
|
||||
- **Always-on anomaly gating becomes affordable.** The CNN / pose
|
||||
model only wakes when sketch novelty crosses a threshold. Energy
|
||||
budget per node drops materially in steady-state quiet rooms.
|
||||
- **Privacy story improves.** Event logs and inter-cluster mesh
|
||||
traffic carry sketches and witness hashes, not reconstructable
|
||||
embeddings. The 1-bit quantization is *not* invertible to the
|
||||
original CSI.
|
||||
- **Composes cleanly with ADR-083.** The cluster Pi is the natural
|
||||
home for the sketch bank; sensor MCUs remain unchanged.
|
||||
- **No new dependency.** `BinaryQuantized` is already in the vendored
|
||||
`ruvector-core` and already SIMD-accelerated.
|
||||
|
||||
### Negative / risks
|
||||
|
||||
- **Sketch quality depends on embedding distribution.** Pure 1-bit
|
||||
sign quantization (which `BinaryQuantized` implements) works best
|
||||
when the embedding space is roughly zero-centered and isotropic.
|
||||
AETHER and CSI spectrogram embeddings need to be benchmarked for
|
||||
this assumption; if either fails, a randomized rotation
|
||||
(Johnson-Lindenstrauss / RaBitQ-paper-style) must be added before
|
||||
sketching. Out-of-scope for this ADR; tracked as a follow-up if
|
||||
the acceptance test fails.
|
||||
- **Top-K coverage degrades for small candidate sets.** With < 16
|
||||
candidates, the sketch compare can pick the wrong K. Site-by-site
|
||||
fallback to full float is part of the rollout plan.
|
||||
- **Sketch-version skew during model upgrades.** A model change
|
||||
invalidates all stored sketches; the cluster Pi must re-sketch the
|
||||
candidate bank when `sketch_version` bumps. Cost is bounded but
|
||||
non-zero.
|
||||
|
||||
### Neutral
|
||||
|
||||
- ADR-024, ADR-027, ADR-029, ADR-030, ADR-076 are unchanged in
|
||||
*what* they compute. They gain a sketch pre-filter at the comparison
|
||||
step.
|
||||
- ADR-082's confirmed-track output filter is upstream of the sketch
|
||||
layer; it stays correct.
|
||||
|
||||
## Implementation
|
||||
|
||||
The implementation lands in five passes, each independently testable.
|
||||
Every pass is gated by the acceptance criterion above; if any fail,
|
||||
that site rolls back and the rest continue.
|
||||
|
||||
1. **`wifi-densepose-ruvector::sketch` module.** Re-export
|
||||
`BinaryQuantized` plus a thin RuView-flavored API
|
||||
(`Sketch::from_embedding`, `Sketch::distance`, `SketchBank::topk`).
|
||||
Add `sketch_version: u16` and `embedding_dim: u16` fields to the
|
||||
public type. Criterion benches: sketch ↔ float compare-cost ratio.
|
||||
|
||||
2. **AETHER re-ID pre-filter.** In
|
||||
`wifi-densepose-signal/src/ruvsense/pose_tracker.rs`, before
|
||||
computing the full 128-d cosine across active tracks × candidates,
|
||||
sketch both sides and reduce to top-K via hamming. Bench: re-ID
|
||||
pass time per frame, ID-stability under cross-room transitions.
|
||||
|
||||
3. **Cluster-Pi novelty sensor.** In
|
||||
`wifi-densepose-sensing-server`, maintain a per-room
|
||||
`SketchBank` of "normal-state" sketches; on each incoming
|
||||
`rv_feature_state_t`, compute embedding sketch, score novelty
|
||||
against the bank, and emit `novelty_score` as a new field on the
|
||||
WebSocket update envelope. Heavy CNN wake gate uses this score.
|
||||
|
||||
4. **Mesh-exchange compression.** Inter-cluster broadcasts (the
|
||||
ADR-066 swarm-bridge channel) carry sketch + witness instead of
|
||||
the full embedding when novelty is low. Full embedding only
|
||||
exchanged when novelty crosses threshold.
|
||||
|
||||
5. **Privacy-preserving event log.** Event log table on the cluster
|
||||
Pi stores `(sketch_bytes, sketch_version, novelty_score,
|
||||
witness_sha256)` instead of raw embeddings. Existing log readers
|
||||
are unchanged in API; only the storage layer rewrites.
|
||||
|
||||
Each pass adds tests: a property test (sketch ↔ float top-K agreement
|
||||
≥ 90%), a criterion bench (≥ 8× compare cost reduction), and an
|
||||
end-to-end accuracy regression test (< 1 pp drop).
|
||||
|
||||
## Validation
|
||||
|
||||
This ADR is **proposed**, not accepted. Acceptance requires the three
|
||||
acceptance numbers above to hold on **at least three of the five
|
||||
implementation passes** (the sites where the bulk of the load sits:
|
||||
AETHER re-ID, cluster-Pi novelty, and event log). The mesh-exchange
|
||||
and mincut prefilter passes are nice-to-haves; they can ship
|
||||
afterward if their per-site numbers hold.
|
||||
|
||||
Validation runs against:
|
||||
|
||||
- the existing 1,539-test workspace suite (must stay green)
|
||||
- a new `tests/integration/rabitq_sketch_pipeline.rs` integration test
|
||||
driving recorded CSI through the full pipeline with and without
|
||||
sketches, comparing top-K decisions and end-to-end pose accuracy
|
||||
- ESP32-S3 on COM7 — sensor MCU unchanged; sketch happens at the
|
||||
cluster Pi, so this validation is a smoke test that the
|
||||
sensor → Pi UDP path still works after the cluster Pi gains the
|
||||
sketch bank
|
||||
|
||||
## Related
|
||||
|
||||
- **ADR-024** (Accepted) — AETHER re-ID embeddings. Primary consumer
|
||||
of the sketch pre-filter.
|
||||
- **ADR-027** (Accepted) — Cross-environment domain generalization
|
||||
(MERIDIAN). Per-room sketch bank is the natural data structure for
|
||||
domain detection.
|
||||
- **ADR-030** (Proposed) — RuvSense persistent field model. Sketch
|
||||
novelty is the cheap second gate before SVD recompute.
|
||||
- **ADR-066** — Swarm bridge to coordinator. Inter-cluster sketch
|
||||
exchange.
|
||||
- **ADR-076** (Accepted) — CSI spectrogram embeddings. Sketch
|
||||
consumer; embedding source.
|
||||
- **ADR-081** (Accepted) — 5-layer adaptive CSI mesh firmware kernel.
|
||||
Sensor MCU unchanged by this ADR; sketches happen at the cluster Pi.
|
||||
- **ADR-083** (Proposed) — Per-cluster Pi compute hop. Defines the
|
||||
device class that hosts the sketch bank.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Does `BinaryQuantized` need a randomized rotation pre-pass for
|
||||
RuView's embedding distributions?** Pure sign quantization assumes
|
||||
zero-centered, isotropic embeddings. If AETHER / spectrogram
|
||||
distributions are skewed (likely for spectrogram), add a
|
||||
`randomized_rotation` pre-pass following the original RaBitQ paper
|
||||
(Gao & Long, SIGMOD 2024). Decided after pass-1 benchmark.
|
||||
- **Sketch dimension target.** Default to the embedding's native
|
||||
dimension (128 for AETHER, 256 for spectrogram). Higher-dimensional
|
||||
sketches (Johnson-Lindenstrauss-projected to 512) trade compute for
|
||||
recall; benchmark before committing.
|
||||
- **Per-room vs per-deployment sketch banks.** Defaulting to per-room
|
||||
for novelty detection. Cross-room re-ID may want a shared bank;
|
||||
decide once cross-room AETHER traces are available.
|
||||
+20
-20
@@ -29,7 +29,7 @@ This runs three phases:
|
||||
|
||||
1. **Environment checks** -- confirms Python, numpy, scipy, and proof files are present.
|
||||
2. **Proof pipeline replay** -- feeds a published reference signal through the full signal processing chain (noise filtering, Hamming windowing, amplitude normalization, FFT-based Doppler extraction, power spectral density via scipy.fft) and computes a SHA-256 hash of the output.
|
||||
3. **Production code integrity scan** -- scans `archive/v1/src/` for `np.random.rand` / `np.random.randn` calls in production code (test helpers are excluded).
|
||||
3. **Production code integrity scan** -- scans `v1/src/` for `np.random.rand` / `np.random.randn` calls in production code (test helpers are excluded).
|
||||
|
||||
Exit codes:
|
||||
- `0` PASS -- pipeline hash matches the published expected hash
|
||||
@@ -51,7 +51,7 @@ make verify-audit
|
||||
If the expected hash file is missing, regenerate it:
|
||||
|
||||
```bash
|
||||
python3 archive/v1/data/proof/verify.py --generate-hash
|
||||
python3 v1/data/proof/verify.py --generate-hash
|
||||
```
|
||||
|
||||
### Minimal dependencies for verification only
|
||||
@@ -63,7 +63,7 @@ pip install numpy==1.26.4 scipy==1.14.1
|
||||
Or install the pinned set that guarantees hash reproducibility:
|
||||
|
||||
```bash
|
||||
pip install -r archive/v1/requirements-lock.txt
|
||||
pip install -r v1/requirements-lock.txt
|
||||
```
|
||||
|
||||
The lock file pins: `numpy==1.26.4`, `scipy==1.14.1`, `pydantic==2.10.4`, `pydantic-settings==2.7.1`.
|
||||
@@ -82,7 +82,7 @@ The Python pipeline lives under `v1/` and provides the full API server, signal p
|
||||
### Install (verification-only -- lightweight)
|
||||
|
||||
```bash
|
||||
pip install -r archive/v1/requirements-lock.txt
|
||||
pip install -r v1/requirements-lock.txt
|
||||
```
|
||||
|
||||
This installs only the four packages needed for deterministic pipeline verification.
|
||||
@@ -98,7 +98,7 @@ This pulls in FastAPI, uvicorn, torch, OpenCV, SQLAlchemy, Redis client, and all
|
||||
### Verify the pipeline
|
||||
|
||||
```bash
|
||||
python3 archive/v1/data/proof/verify.py
|
||||
python3 v1/data/proof/verify.py
|
||||
```
|
||||
|
||||
Same as `./verify` but calls the Python script directly, skipping the bash wrapper's codebase scan phase.
|
||||
@@ -124,7 +124,7 @@ uvicorn v1.src.api.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
|
||||
### Run with commodity WiFi (RSSI sensing -- no custom hardware)
|
||||
|
||||
The commodity sensing module (`archive/v1/src/sensing/`) extracts presence and motion features from standard Linux WiFi metrics (RSSI, noise floor, link quality) without any hardware modification. See [ADR-013](adr/ADR-013-feature-level-sensing-commodity-gear.md) for full design details.
|
||||
The commodity sensing module (`v1/src/sensing/`) extracts presence and motion features from standard Linux WiFi metrics (RSSI, noise floor, link quality) without any hardware modification. See [ADR-013](adr/ADR-013-feature-level-sensing-commodity-gear.md) for full design details.
|
||||
|
||||
Requirements:
|
||||
- Any Linux machine with a WiFi interface (laptop, Raspberry Pi, etc.)
|
||||
@@ -191,7 +191,7 @@ A high-performance Rust port with ~810x speedup over the Python pipeline for the
|
||||
### Build
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
@@ -200,7 +200,7 @@ Release profile is configured with LTO, single codegen unit, and `-O3` for maxim
|
||||
### Test
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
@@ -209,7 +209,7 @@ Runs 107 tests across all workspace crates.
|
||||
### Benchmark
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo bench --package wifi-densepose-signal
|
||||
```
|
||||
|
||||
@@ -468,7 +468,7 @@ The aggregator collects UDP streams from all ESP32 nodes, performs feature-level
|
||||
docker compose -f docker-compose.esp32.yml up
|
||||
|
||||
# Or run the Rust aggregator directly
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo run --release --package wifi-densepose-hardware -- --mode esp32-aggregator --port 5000
|
||||
```
|
||||
|
||||
@@ -516,7 +516,7 @@ rustup target add wasm32-unknown-unknown
|
||||
Build:
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
|
||||
# Build WASM package (outputs to pkg/)
|
||||
wasm-pack build crates/wifi-densepose-wasm --target web --release
|
||||
@@ -601,7 +601,7 @@ uvicorn v1.src.api.main:app \
|
||||
--workers 4
|
||||
|
||||
# Or run the Rust API server
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo run --release --package wifi-densepose-api
|
||||
```
|
||||
|
||||
@@ -631,7 +631,7 @@ pytest --cov=wifi_densepose --cov-report=html
|
||||
Rust:
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
|
||||
# Build in debug mode (faster compilation)
|
||||
cargo build
|
||||
@@ -667,14 +667,14 @@ python3 -m http.server 3000 --directory ui
|
||||
|------|---------|
|
||||
| `./verify` | Trust kill switch -- one-command pipeline proof |
|
||||
| `Makefile` | `make verify`, `make verify-verbose`, `make verify-audit` |
|
||||
| `archive/v1/requirements-lock.txt` | Pinned Python deps for hash reproducibility |
|
||||
| `v1/requirements-lock.txt` | Pinned Python deps for hash reproducibility |
|
||||
| `requirements.txt` | Full Python deps (API server, torch, etc.) |
|
||||
| `archive/v1/data/proof/verify.py` | Python verification script |
|
||||
| `archive/v1/data/proof/sample_csi_data.json` | Deterministic reference signal |
|
||||
| `archive/v1/data/proof/expected_features.sha256` | Published expected hash |
|
||||
| `archive/v1/src/api/main.py` | FastAPI application entry point |
|
||||
| `archive/v1/src/sensing/` | Commodity WiFi sensing module (RSSI) |
|
||||
| `v2/Cargo.toml` | Rust workspace root |
|
||||
| `v1/data/proof/verify.py` | Python verification script |
|
||||
| `v1/data/proof/sample_csi_data.json` | Deterministic reference signal |
|
||||
| `v1/data/proof/expected_features.sha256` | Published expected hash |
|
||||
| `v1/src/api/main.py` | FastAPI application entry point |
|
||||
| `v1/src/sensing/` | Commodity WiFi sensing module (RSSI) |
|
||||
| `rust-port/wifi-densepose-rs/Cargo.toml` | Rust workspace root |
|
||||
| `ui/viz.html` | Three.js 3D visualization |
|
||||
| `Dockerfile` | Multi-stage Docker build (dev/prod/test/security) |
|
||||
| `docker-compose.yml` | Development stack (Postgres, Redis, Prometheus, Grafana) |
|
||||
|
||||
@@ -14,7 +14,7 @@ This document defines the system using [Domain-Driven Design](https://martinfowl
|
||||
| 4 | [Aggregation](#4-aggregation-context) | Server-side CSI frame reception, timestamp alignment, multi-node feature fusion | [ADR-012](../adr/ADR-012-esp32-csi-sensor-mesh.md) | `crates/wifi-densepose-hardware/src/esp32/` |
|
||||
| 5 | [Provisioning](#5-provisioning-context) | NVS configuration, firmware lifecycle, fleet management, deployment presets | [ADR-044](../adr/ADR-044-provisioning-tool-enhancements.md) | `firmware/esp32-csi-node/provision.py` |
|
||||
|
||||
All firmware paths are relative to the repository root. Rust crate paths are relative to `v2/`.
|
||||
All firmware paths are relative to the repository root. Rust crate paths are relative to `rust-port/wifi-densepose-rs/`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ This document defines the system using [Domain-Driven Design](https://martinfowl
|
||||
| 6 | [Spatial Identity](#6-spatial-identity-context) | Cross-room tracking via environment fingerprints | [ADR-030](../adr/ADR-030-ruvsense-persistent-field-model.md) | `signal/src/ruvsense/cross_room.rs` |
|
||||
| 7 | [Edge Intelligence](#7-edge-intelligence-context) | On-device sensing (no server needed) | [ADR-039](../adr/ADR-039-esp32-edge-intelligence.md), [ADR-040](../adr/ADR-040-wasm-programmable-sensing.md) | `firmware/esp32-csi-node/main/edge_processing.c` |
|
||||
|
||||
All code paths shown are relative to `v2/crates/wifi-densepose-` unless otherwise noted.
|
||||
All code paths shown are relative to `rust-port/wifi-densepose-rs/crates/wifi-densepose-` unless otherwise noted.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ This document defines the system using [Domain-Driven Design](https://martinfowl
|
||||
| 4 | [Training Pipeline](#4-training-pipeline-context) | Background training runs, progress streaming, contrastive pretraining | [ADR-043](../adr/ADR-043-sensing-server-ui-api-completion.md) | `sensing-server/src/training_api.rs` |
|
||||
| 5 | [Visualization](#5-visualization-context) | WebSocket streaming to web UI, Gaussian splat rendering, data transparency | [ADR-019](../adr/ADR-019-sensing-only-ui-mode.md), [ADR-035](../adr/ADR-035-live-sensing-ui-accuracy.md) | `ui/` |
|
||||
|
||||
All code paths shown are relative to `v2/crates/wifi-densepose-` unless otherwise noted.
|
||||
All code paths shown are relative to `rust-port/wifi-densepose-rs/crates/wifi-densepose-` unless otherwise noted.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ This document defines the system using [Domain-Driven Design](https://martinfowl
|
||||
| 3 | [Training Orchestration](#3-training-orchestration-context) | Run the training loop, compute composite loss, checkpoint, and verify deterministic proofs | [ADR-015](../adr/ADR-015-public-dataset-training-strategy.md), [ADR-016](../adr/ADR-016-ruvector-integration.md) | `train/src/trainer.rs`, `train/src/losses.rs`, `train/src/metrics.rs`, `train/src/proof.rs` |
|
||||
| 4 | [Embedding & Transfer](#4-embedding--transfer-context) | Produce AETHER contrastive embeddings, MERIDIAN domain-generalized features, and LoRA adapters | [ADR-024](../adr/ADR-024-contrastive-csi-embedding-model.md), [ADR-027](../adr/ADR-027-cross-environment-domain-generalization.md) | `train/src/embedding.rs`, `train/src/domain.rs`, `train/src/sona.rs` |
|
||||
|
||||
All code paths shown are relative to `v2/crates/wifi-densepose-` unless otherwise noted.
|
||||
All code paths shown are relative to `rust-port/wifi-densepose-rs/crates/wifi-densepose-` unless otherwise noted.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
```bash
|
||||
# Build all modules for ESP32
|
||||
cd v2/crates/wifi-densepose-wasm-edge
|
||||
cd rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge
|
||||
cargo build --target wasm32-unknown-unknown --release
|
||||
|
||||
# Run all 632 tests
|
||||
@@ -144,4 +144,4 @@ Every module talks to the ESP32 through 12 functions:
|
||||
- [ADR-039](../adr/ADR-039-esp32-edge-intelligence.md) — Edge processing tiers
|
||||
- [ADR-040](../adr/ADR-040-wasm-programmable-sensing.md) — WASM runtime design
|
||||
- [ADR-041](../adr/ADR-041-wasm-module-collection.md) — Full module specification
|
||||
- [Source code](../../v2/crates/wifi-densepose-wasm-edge/src/)
|
||||
- [Source code](../../rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/)
|
||||
|
||||
@@ -481,7 +481,7 @@ std::fs::write("my-gesture-v2.rvf", &rvf_mut)?;
|
||||
From the crate directory:
|
||||
|
||||
```bash
|
||||
cd v2/crates/wifi-densepose-wasm-edge
|
||||
cd rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge
|
||||
cargo test --features std -- gesture coherence adversarial intrusion occupancy vital_trend rvf
|
||||
```
|
||||
|
||||
|
||||
@@ -618,7 +618,7 @@ for _ in 0..100 {
|
||||
All medical modules include comprehensive unit tests covering initialization, normal operation, clinical scenario detection, edge cases, and cooldown behavior.
|
||||
|
||||
```bash
|
||||
cd v2/crates/wifi-densepose-wasm-edge
|
||||
cd rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge
|
||||
cargo test --features std -- med_
|
||||
```
|
||||
|
||||
|
||||
@@ -556,7 +556,7 @@ for &(event_id, value) in events {
|
||||
|
||||
```bash
|
||||
# Run all security module tests (requires std feature)
|
||||
cd v2/crates/wifi-densepose-wasm-edge
|
||||
cd rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge
|
||||
cargo test --features std -- sec_ intrusion
|
||||
```
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ The project implements WiFi-based human pose estimation using Channel State Info
|
||||
| Architecture Decision Records | Strong | 79 ADRs documented in `docs/adr/` |
|
||||
| CI/CD pipelines | Strong | 8 GitHub Actions workflows (CI, CD, security scan, firmware CI, QEMU, desktop release, verify pipeline, submodules) |
|
||||
| Security scanning | Strong | Dedicated `security-scan.yml` with Bandit, Semgrep, Safety; runs daily on schedule |
|
||||
| Deterministic verification | Strong | SHA-256 proof pipeline (`archive/v1/data/proof/verify.py`) with witness bundles (ADR-028) |
|
||||
| Deterministic verification | Strong | SHA-256 proof pipeline (`v1/data/proof/verify.py`) with witness bundles (ADR-028) |
|
||||
| Code formatting | Moderate | Black/Flake8 enforced for Python in CI; no `rustfmt.toml` found for Rust |
|
||||
| Type checking | Moderate | MyPy configured in CI for Python; Rust has native type safety |
|
||||
| Dependency management | Strong | Workspace-level Cargo.toml with pinned versions; `requirements.txt` for Python |
|
||||
|
||||
@@ -368,7 +368,7 @@ or macro-based approach would reduce this to a fraction of the code.
|
||||
| wifi-densepose-wifiscan | 75/100 | EASY | Platform-specific but well-abstracted |
|
||||
| wifi-densepose-sensing-server | 32/100 | VERY DIFFICULT | God object, coupled state, async |
|
||||
| wifi-densepose-wasm-edge | 55/100 | MODERATE | Repetitive but self-contained |
|
||||
| archive/v1/src (Python) | 70/100 | MODERATE | Good DI, some tight coupling |
|
||||
| v1/src (Python) | 70/100 | MODERATE | Good DI, some tight coupling |
|
||||
| firmware (C) | 40/100 | DIFFICULT | Hardware deps, global state |
|
||||
| ui/mobile (TypeScript) | 72/100 | MODERATE | Component isolation is good |
|
||||
|
||||
|
||||
@@ -35,20 +35,20 @@ This security review examined all security-sensitive code across the wifi-densep
|
||||
**Severity:** HIGH
|
||||
**OWASP:** A07:2021 -- Identification and Authentication Failures
|
||||
**Files:**
|
||||
- `archive/v1/src/api/routers/stream.py:74` (WebSocket `token` query parameter)
|
||||
- `archive/v1/src/middleware/auth.py:243` (fallback to `request.query_params.get("token")`)
|
||||
- `archive/v1/src/api/middleware/auth.py:173` (`request.query_params.get("token")`)
|
||||
- `v1/src/api/routers/stream.py:74` (WebSocket `token` query parameter)
|
||||
- `v1/src/middleware/auth.py:243` (fallback to `request.query_params.get("token")`)
|
||||
- `v1/src/api/middleware/auth.py:173` (`request.query_params.get("token")`)
|
||||
|
||||
**Description:**
|
||||
JWT tokens are accepted via URL query parameters for WebSocket connections. URL parameters are logged in web server access logs, browser history, proxy logs, and HTTP Referer headers. This creates multiple credential leakage vectors.
|
||||
|
||||
```python
|
||||
# archive/v1/src/api/routers/stream.py:74
|
||||
# v1/src/api/routers/stream.py:74
|
||||
token: Optional[str] = Query(None, description="Authentication token")
|
||||
```
|
||||
|
||||
```python
|
||||
# archive/v1/src/middleware/auth.py:243
|
||||
# v1/src/middleware/auth.py:243
|
||||
if request.url.path.startswith("/ws"):
|
||||
token = request.query_params.get("token")
|
||||
```
|
||||
@@ -66,13 +66,13 @@ if request.url.path.startswith("/ws"):
|
||||
|
||||
**Severity:** HIGH
|
||||
**OWASP:** A05:2021 -- Security Misconfiguration
|
||||
**File:** `archive/v1/src/middleware/rate_limit.py:200-206`
|
||||
**File:** `v1/src/middleware/rate_limit.py:200-206`
|
||||
|
||||
**Description:**
|
||||
The `_get_client_ip` method trusts the `X-Forwarded-For` header without any validation. An attacker can spoof this header to bypass IP-based rate limiting entirely by rotating forged IP addresses on each request.
|
||||
|
||||
```python
|
||||
# archive/v1/src/middleware/rate_limit.py:200-206
|
||||
# v1/src/middleware/rate_limit.py:200-206
|
||||
def _get_client_ip(self, request: Request) -> str:
|
||||
forwarded_for = request.headers.get("X-Forwarded-For")
|
||||
if forwarded_for:
|
||||
@@ -99,17 +99,17 @@ def _get_client_ip(self, request: Request) -> str:
|
||||
**Severity:** HIGH
|
||||
**OWASP:** A09:2021 -- Security Logging and Monitoring Failures
|
||||
**Files:**
|
||||
- `archive/v1/src/api/routers/pose.py:140-141` -- `detail=f"Pose estimation failed: {str(e)}"`
|
||||
- `archive/v1/src/api/routers/pose.py:176-177` -- `detail=f"Pose analysis failed: {str(e)}"`
|
||||
- `archive/v1/src/api/routers/stream.py:297` -- `detail=f"Failed to get stream status: {str(e)}"`
|
||||
- All exception handlers in `archive/v1/src/api/routers/stream.py` (lines 326, 351, 404, 442, 463)
|
||||
- `archive/v1/src/middleware/error_handler.py:101-104` -- traceback in development mode
|
||||
- `v1/src/api/routers/pose.py:140-141` -- `detail=f"Pose estimation failed: {str(e)}"`
|
||||
- `v1/src/api/routers/pose.py:176-177` -- `detail=f"Pose analysis failed: {str(e)}"`
|
||||
- `v1/src/api/routers/stream.py:297` -- `detail=f"Failed to get stream status: {str(e)}"`
|
||||
- All exception handlers in `v1/src/api/routers/stream.py` (lines 326, 351, 404, 442, 463)
|
||||
- `v1/src/middleware/error_handler.py:101-104` -- traceback in development mode
|
||||
|
||||
**Description:**
|
||||
Multiple API endpoints directly interpolate Python exception messages into HTTP error responses. While the global error handler in `error_handler.py` correctly suppresses details in production, the per-endpoint `HTTPException` handlers bypass this and always expose `str(e)` regardless of environment.
|
||||
|
||||
```python
|
||||
# archive/v1/src/api/routers/pose.py:140-141
|
||||
# v1/src/api/routers/pose.py:140-141
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Pose estimation failed: {str(e)}"
|
||||
@@ -130,14 +130,14 @@ raise HTTPException(
|
||||
**Severity:** MEDIUM
|
||||
**OWASP:** A05:2021 -- Security Misconfiguration
|
||||
**Files:**
|
||||
- `archive/v1/src/config/settings.py:33-34` -- defaults: `cors_origins=["*"]`, `cors_allow_credentials=True`
|
||||
- `archive/v1/src/middleware/cors.py:255-256` -- development config combines `allow_origins=["*"]` + `allow_credentials=True`
|
||||
- `v1/src/config/settings.py:33-34` -- defaults: `cors_origins=["*"]`, `cors_allow_credentials=True`
|
||||
- `v1/src/middleware/cors.py:255-256` -- development config combines `allow_origins=["*"]` + `allow_credentials=True`
|
||||
|
||||
**Description:**
|
||||
The default settings allow CORS from all origins (`*`) with credentials (`allow_credentials=True`). Per the CORS specification, `Access-Control-Allow-Origin: *` cannot be used with `Access-Control-Allow-Credentials: true`. However, the `CORSMiddleware` implementation echoes the requesting origin header verbatim, effectively granting credentialed access from any origin.
|
||||
|
||||
```python
|
||||
# archive/v1/src/middleware/cors.py:255-256 (development_config)
|
||||
# v1/src/middleware/cors.py:255-256 (development_config)
|
||||
"allow_origins": ["*"],
|
||||
"allow_credentials": True,
|
||||
```
|
||||
@@ -158,8 +158,8 @@ The `validate_cors_config` function at line 354 correctly flags this combination
|
||||
**Severity:** MEDIUM
|
||||
**OWASP:** A04:2021 -- Insecure Design
|
||||
**Files:**
|
||||
- `archive/v1/src/api/routers/stream.py:127-128` -- `message = await websocket.receive_text()` with no size limit
|
||||
- `archive/v1/src/api/websocket/connection_manager.py` -- no `max_size` configuration
|
||||
- `v1/src/api/routers/stream.py:127-128` -- `message = await websocket.receive_text()` with no size limit
|
||||
- `v1/src/api/websocket/connection_manager.py` -- no `max_size` configuration
|
||||
|
||||
**Description:**
|
||||
WebSocket endpoints accept incoming messages of arbitrary size. The `receive_text()` call at `stream.py:127` has no size limit, allowing a client to send extremely large messages that consume server memory.
|
||||
@@ -179,7 +179,7 @@ Additionally, the `ConnectionManager` does not enforce a maximum number of conne
|
||||
|
||||
**Severity:** MEDIUM
|
||||
**OWASP:** A07:2021 -- Identification and Authentication Failures
|
||||
**File:** `archive/v1/src/api/middleware/auth.py:246-252`
|
||||
**File:** `v1/src/api/middleware/auth.py:246-252`
|
||||
|
||||
**Description:**
|
||||
The `TokenBlacklist` class clears all blacklisted tokens every hour, regardless of their actual expiry time. This means:
|
||||
@@ -187,7 +187,7 @@ The `TokenBlacklist` class clears all blacklisted tokens every hour, regardless
|
||||
2. Tokens revoked just before a clear cycle have nearly zero effective blacklist time.
|
||||
|
||||
```python
|
||||
# archive/v1/src/api/middleware/auth.py:246-252
|
||||
# v1/src/api/middleware/auth.py:246-252
|
||||
def _cleanup_if_needed(self):
|
||||
now = datetime.utcnow()
|
||||
if (now - self._last_cleanup).total_seconds() > self._cleanup_interval:
|
||||
@@ -306,8 +306,8 @@ if (s_cfg.seed_token[0] != '\0') {
|
||||
**Severity:** MEDIUM
|
||||
**OWASP:** A04:2021 -- Insecure Design
|
||||
**Files:**
|
||||
- `archive/v1/src/api/middleware/rate_limit.py:28-29` -- `self.request_counts = defaultdict(lambda: deque())`
|
||||
- `archive/v1/src/middleware/rate_limit.py:132` -- `self._sliding_windows: Dict[str, SlidingWindowCounter] = {}`
|
||||
- `v1/src/api/middleware/rate_limit.py:28-29` -- `self.request_counts = defaultdict(lambda: deque())`
|
||||
- `v1/src/middleware/rate_limit.py:132` -- `self._sliding_windows: Dict[str, SlidingWindowCounter] = {}`
|
||||
|
||||
**Description:**
|
||||
Both rate limiter implementations store per-client sliding window data in unbounded in-memory dictionaries. An attacker sending requests from many spoofed IPs (see HIGH-002) can create millions of entries, each containing a `deque` of timestamps. The cleanup tasks run only periodically (every 5 minutes or on-demand) and cannot keep pace with a high-rate attack.
|
||||
@@ -349,8 +349,8 @@ While marked with a comment indicating it should be changed, this file is checke
|
||||
**Severity:** LOW
|
||||
**OWASP:** A01:2021 -- Broken Access Control
|
||||
**Files:**
|
||||
- `archive/v1/src/middleware/auth.py:298-299` -- `response.headers["X-User"] = user_info["username"]` and `response.headers["X-User-Roles"] = ",".join(user_info["roles"])`
|
||||
- `archive/v1/src/api/middleware/auth.py:111` -- `response.headers["X-User-ID"] = request.state.user.get("id", "")`
|
||||
- `v1/src/middleware/auth.py:298-299` -- `response.headers["X-User"] = user_info["username"]` and `response.headers["X-User-Roles"] = ",".join(user_info["roles"])`
|
||||
- `v1/src/api/middleware/auth.py:111` -- `response.headers["X-User-ID"] = request.state.user.get("id", "")`
|
||||
|
||||
**Description:**
|
||||
Authenticated user information (username, roles, user ID) is included in HTTP response headers. These headers are visible to any intermediary (CDN, reverse proxy, browser extensions) and in browser developer tools.
|
||||
@@ -380,7 +380,7 @@ Replace all instances of `datetime.utcnow()` with `datetime.now(datetime.timezon
|
||||
|
||||
**Severity:** LOW
|
||||
**OWASP:** A02:2021 -- Cryptographic Failures
|
||||
**File:** `archive/v1/src/config/settings.py:30` -- `jwt_algorithm: str = Field(default="HS256")`
|
||||
**File:** `v1/src/config/settings.py:30` -- `jwt_algorithm: str = Field(default="HS256")`
|
||||
|
||||
**Description:**
|
||||
The default JWT algorithm is HS256 (HMAC-SHA256), a symmetric algorithm. This means the same secret is used for both signing and verification, requiring the secret to be distributed to every service that needs to verify tokens. For multi-service architectures, asymmetric algorithms (RS256, ES256) are preferred.
|
||||
@@ -398,7 +398,7 @@ Additionally, the `jwt_algorithm` setting is not validated against a safe algori
|
||||
|
||||
**Severity:** LOW
|
||||
**OWASP:** A07:2021 -- Identification and Authentication Failures
|
||||
**File:** `archive/v1/src/middleware/auth.py:115` -- `create_user()` method
|
||||
**File:** `v1/src/middleware/auth.py:115` -- `create_user()` method
|
||||
|
||||
**Description:**
|
||||
The `create_user()` method accepts any password without minimum length, complexity, or entropy requirements. Test credentials in `v1/test_auth_rate_limit.py:21-23` demonstrate weak passwords ("admin123", "user123").
|
||||
@@ -413,9 +413,9 @@ The `create_user()` method accepts any password without minimum length, complexi
|
||||
### INFORMATIONAL-001: Rust API, DB, and Config Crates Are Stubs
|
||||
|
||||
**Files:**
|
||||
- `v2/crates/wifi-densepose-api/src/lib.rs` -- `//! WiFi-DensePose REST API (stub)`
|
||||
- `v2/crates/wifi-densepose-db/src/lib.rs` -- `//! WiFi-DensePose database layer (stub)`
|
||||
- `v2/crates/wifi-densepose-config/src/lib.rs` -- `//! WiFi-DensePose configuration (stub)`
|
||||
- `rust-port/wifi-densepose-rs/crates/wifi-densepose-api/src/lib.rs` -- `//! WiFi-DensePose REST API (stub)`
|
||||
- `rust-port/wifi-densepose-rs/crates/wifi-densepose-db/src/lib.rs` -- `//! WiFi-DensePose database layer (stub)`
|
||||
- `rust-port/wifi-densepose-rs/crates/wifi-densepose-config/src/lib.rs` -- `//! WiFi-DensePose configuration (stub)`
|
||||
|
||||
**Description:**
|
||||
The Rust API, database, and configuration crates contain only single-line stub comments. No security review of Rust API endpoints, database queries, or configuration handling was possible because no implementation exists. The `wifi-densepose-sensing-server` crate contains the actual Rust server implementation.
|
||||
@@ -426,7 +426,7 @@ The Rust API, database, and configuration crates contain only single-line stub c
|
||||
|
||||
### INFORMATIONAL-002: Rust `unsafe` Blocks in WASM Edge Crate
|
||||
|
||||
**Files:** `v2/crates/wifi-densepose-wasm-edge/src/*.rs` (multiple files)
|
||||
**Files:** `rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/*.rs` (multiple files)
|
||||
|
||||
**Description:**
|
||||
The `wifi-densepose-wasm-edge` crate contains approximately 40 `unsafe` blocks, primarily for:
|
||||
@@ -460,7 +460,7 @@ This is a positive finding reflecting good security practices.
|
||||
| `paramiko>=3.0.0` | LOW -- SSH library. Ensure latest minor version for CVE patches. |
|
||||
| `fastapi>=0.95.0` | LOW -- Version floor is old. Pin to latest stable for security patches. |
|
||||
|
||||
**Recommendation:** Run `pip audit` or `safety check` against the locked dependency file (`archive/v1/requirements-lock.txt`) to identify known CVEs.
|
||||
**Recommendation:** Run `pip audit` or `safety check` against the locked dependency file (`v1/requirements-lock.txt`) to identify known CVEs.
|
||||
|
||||
### Rust Dependencies (`Cargo.toml`)
|
||||
|
||||
@@ -484,11 +484,11 @@ The following areas demonstrate security-conscious design:
|
||||
|
||||
3. **RVF build hash validation** (`firmware/esp32-csi-node/main/rvf_parser.c:126-137`): SHA-256 hash of the WASM payload is verified against the manifest before loading, preventing tampered module execution.
|
||||
|
||||
4. **Password hashing with bcrypt** (`archive/v1/src/middleware/auth.py:21`): Proper use of `passlib` with `bcrypt` scheme.
|
||||
4. **Password hashing with bcrypt** (`v1/src/middleware/auth.py:21`): Proper use of `passlib` with `bcrypt` scheme.
|
||||
|
||||
5. **Protected user fields** (`archive/v1/src/middleware/auth.py:139`): `update_user()` prevents modification of `username`, `created_at`, and `hashed_password`.
|
||||
5. **Protected user fields** (`v1/src/middleware/auth.py:139`): `update_user()` prevents modification of `username`, `created_at`, and `hashed_password`.
|
||||
|
||||
6. **Production error suppression** (`archive/v1/src/middleware/error_handler.py:214-218`): The centralized error handler correctly suppresses internal details in production mode.
|
||||
6. **Production error suppression** (`v1/src/middleware/error_handler.py:214-218`): The centralized error handler correctly suppresses internal details in production mode.
|
||||
|
||||
7. **No hardcoded secrets in source** (verified via entropy-based search across entire repository): No API keys, passwords, or tokens found in source files (the test script placeholder at `test_auth_rate_limit.py:26` is marked as requiring replacement).
|
||||
|
||||
@@ -502,23 +502,23 @@ The following areas demonstrate security-conscious design:
|
||||
|
||||
## Files Examined
|
||||
|
||||
### Python (archive/v1/src/)
|
||||
- `archive/v1/src/middleware/auth.py` (457 lines) -- JWT auth, user management, middleware
|
||||
- `archive/v1/src/middleware/rate_limit.py` (465 lines) -- Rate limiting with sliding window
|
||||
- `archive/v1/src/middleware/cors.py` (375 lines) -- CORS middleware and validation
|
||||
- `archive/v1/src/middleware/error_handler.py` (505 lines) -- Error handling middleware
|
||||
- `archive/v1/src/api/middleware/auth.py` (303 lines) -- API-layer JWT auth
|
||||
- `archive/v1/src/api/middleware/rate_limit.py` (326 lines) -- API-layer rate limiting
|
||||
- `archive/v1/src/api/websocket/connection_manager.py` (461 lines) -- WebSocket manager
|
||||
- `archive/v1/src/api/websocket/pose_stream.py` (384 lines) -- Pose streaming handler
|
||||
- `archive/v1/src/api/routers/pose.py` (420 lines) -- Pose API endpoints
|
||||
- `archive/v1/src/api/routers/stream.py` (465 lines) -- Streaming API endpoints
|
||||
- `archive/v1/src/config/settings.py` (436 lines) -- Application settings
|
||||
- `archive/v1/src/sensing/rssi_collector.py` (partial) -- Subprocess usage review
|
||||
- `archive/v1/src/tasks/backup.py` (partial) -- Subprocess command construction
|
||||
### Python (v1/src/)
|
||||
- `v1/src/middleware/auth.py` (457 lines) -- JWT auth, user management, middleware
|
||||
- `v1/src/middleware/rate_limit.py` (465 lines) -- Rate limiting with sliding window
|
||||
- `v1/src/middleware/cors.py` (375 lines) -- CORS middleware and validation
|
||||
- `v1/src/middleware/error_handler.py` (505 lines) -- Error handling middleware
|
||||
- `v1/src/api/middleware/auth.py` (303 lines) -- API-layer JWT auth
|
||||
- `v1/src/api/middleware/rate_limit.py` (326 lines) -- API-layer rate limiting
|
||||
- `v1/src/api/websocket/connection_manager.py` (461 lines) -- WebSocket manager
|
||||
- `v1/src/api/websocket/pose_stream.py` (384 lines) -- Pose streaming handler
|
||||
- `v1/src/api/routers/pose.py` (420 lines) -- Pose API endpoints
|
||||
- `v1/src/api/routers/stream.py` (465 lines) -- Streaming API endpoints
|
||||
- `v1/src/config/settings.py` (436 lines) -- Application settings
|
||||
- `v1/src/sensing/rssi_collector.py` (partial) -- Subprocess usage review
|
||||
- `v1/src/tasks/backup.py` (partial) -- Subprocess command construction
|
||||
- `v1/test_auth_rate_limit.py` (partial) -- Test credentials review
|
||||
|
||||
### Rust (v2/)
|
||||
### Rust (rust-port/wifi-densepose-rs/)
|
||||
- `crates/wifi-densepose-api/src/lib.rs` (1 line -- stub)
|
||||
- `crates/wifi-densepose-db/src/lib.rs` (1 line -- stub)
|
||||
- `crates/wifi-densepose-config/src/lib.rs` (1 line -- stub)
|
||||
|
||||
@@ -40,7 +40,7 @@ The WiFi-DensePose codebase is a real-time sensing system targeting 20 Hz output
|
||||
|
||||
### FINDING PERF-R01: Tomography Weight Matrix -- O(L * nx * ny * nz) per Link [CRITICAL]
|
||||
|
||||
**File**: `v2/crates/wifi-densepose-signal/src/ruvsense/tomography.rs`
|
||||
**File**: `rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/tomography.rs`
|
||||
**Lines**: 345-383 (`compute_link_weights`)
|
||||
|
||||
The `compute_link_weights` function iterates over every voxel in the grid for every link to compute Fresnel-zone intersection weights:
|
||||
@@ -76,7 +76,7 @@ for iz in 0..config.nz {
|
||||
|
||||
### FINDING PERF-R02: Multistatic Fusion -- sin()/cos() per Subcarrier per Node [HIGH]
|
||||
|
||||
**File**: `v2/crates/wifi-densepose-signal/src/ruvsense/multistatic.rs`
|
||||
**File**: `rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/multistatic.rs`
|
||||
**Lines**: 287-298 (`attention_weighted_fusion`)
|
||||
|
||||
```rust
|
||||
@@ -105,7 +105,7 @@ for (n, (&, &ph)) in amplitudes.iter().zip(phases.iter()).enumerate() {
|
||||
|
||||
### FINDING PERF-R03: Pose Tracker find_track -- Linear Search [MEDIUM]
|
||||
|
||||
**File**: `v2/crates/wifi-densepose-signal/src/ruvsense/pose_tracker.rs`
|
||||
**File**: `rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/pose_tracker.rs`
|
||||
**Lines**: 546-553
|
||||
|
||||
```rust
|
||||
@@ -124,7 +124,7 @@ pub fn find_track(&self, id: TrackId) -> Option<&PoseTrack> {
|
||||
|
||||
### FINDING PERF-R04: Multistatic FusedSensingFrame -- Deep Clone of node_frames [HIGH]
|
||||
|
||||
**File**: `v2/crates/wifi-densepose-signal/src/ruvsense/multistatic.rs`
|
||||
**File**: `rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/multistatic.rs`
|
||||
**Line**: 222
|
||||
|
||||
```rust
|
||||
@@ -150,7 +150,7 @@ Ok(FusedSensingFrame {
|
||||
|
||||
### FINDING PERF-R05: Coherence Score -- Efficient but exp() in Hot Loop [LOW]
|
||||
|
||||
**File**: `v2/crates/wifi-densepose-signal/src/ruvsense/coherence.rs`
|
||||
**File**: `rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/coherence.rs`
|
||||
**Lines**: 224-252 (`coherence_score`)
|
||||
|
||||
```rust
|
||||
@@ -174,7 +174,7 @@ for i in 0..n {
|
||||
|
||||
### FINDING PERF-R06: Gesture DTW -- O(N * M) per Template [MEDIUM]
|
||||
|
||||
**File**: `v2/crates/wifi-densepose-signal/src/ruvsense/gesture.rs`
|
||||
**File**: `rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/gesture.rs`
|
||||
**Lines**: 288-328 (`dtw_distance`)
|
||||
|
||||
The DTW implementation uses the Sakoe-Chiba band constraint (good), but allocates two full Vec<f64> per call:
|
||||
@@ -199,7 +199,7 @@ With T templates and band_width=5, complexity is O(T * N * band_width * feature_
|
||||
|
||||
### FINDING PERF-R07: Field Model Covariance -- O(S^2) Memory [MEDIUM]
|
||||
|
||||
**File**: `v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs`
|
||||
**File**: `rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/field_model.rs`
|
||||
**Line**: 330 (`covariance_sum: Option<Array2<f64>>`)
|
||||
|
||||
The full covariance matrix for SVD is S x S where S = number of subcarriers. With S=56, this is 56 * 56 * 8 = 25 KB -- reasonable. But the diagonal_fallback (lines 338-383) creates unnecessary intermediate allocations.
|
||||
@@ -212,7 +212,7 @@ The full covariance matrix for SVD is S x S where S = number of subcarriers. Wit
|
||||
|
||||
### FINDING PERF-R08: Multiband Duplicate Frequency Check -- O(N^2) [LOW]
|
||||
|
||||
**File**: `v2/crates/wifi-densepose-signal/src/ruvsense/multiband.rs`
|
||||
**File**: `rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/multiband.rs`
|
||||
**Lines**: 126-135
|
||||
|
||||
```rust
|
||||
@@ -235,7 +235,7 @@ for i in 0..self.frequencies.len() {
|
||||
|
||||
### FINDING PERF-R09: Adversarial Detector -- Potential O(L^2) Consistency Check [MEDIUM]
|
||||
|
||||
**File**: `v2/crates/wifi-densepose-signal/src/ruvsense/adversarial.rs`
|
||||
**File**: `rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/adversarial.rs`
|
||||
**Lines**: 147+
|
||||
|
||||
The multi-link consistency check compares energy ratios across all links. With L=12 links, the pairwise comparison (if implemented) would be O(L^2) = 144. Combined with the four independent checks (consistency, field model, temporal, energy), this runs on every frame.
|
||||
@@ -259,7 +259,7 @@ The multi-link consistency check compares energy ratios across all links. With L
|
||||
|
||||
### FINDING PERF-NN01: Serial Batch Inference [CRITICAL]
|
||||
|
||||
**File**: `v2/crates/wifi-densepose-nn/src/inference.rs`
|
||||
**File**: `rust-port/wifi-densepose-rs/crates/wifi-densepose-nn/src/inference.rs`
|
||||
**Lines**: 334-336
|
||||
|
||||
```rust
|
||||
@@ -283,7 +283,7 @@ pub fn infer_batch(&self, inputs: &[Tensor]) -> NnResult<Vec<Tensor>> {
|
||||
|
||||
### FINDING PERF-NN02: Async Stats Update Spawns Tokio Task per Inference [HIGH]
|
||||
|
||||
**File**: `v2/crates/wifi-densepose-nn/src/inference.rs`
|
||||
**File**: `rust-port/wifi-densepose-rs/crates/wifi-densepose-nn/src/inference.rs`
|
||||
**Lines**: 311-315
|
||||
|
||||
```rust
|
||||
@@ -307,7 +307,7 @@ tokio::spawn(async move {
|
||||
|
||||
### FINDING PERF-NN03: Tensor Clone in run_single [MEDIUM]
|
||||
|
||||
**File**: `v2/crates/wifi-densepose-nn/src/inference.rs`
|
||||
**File**: `rust-port/wifi-densepose-rs/crates/wifi-densepose-nn/src/inference.rs`
|
||||
**Lines**: 122
|
||||
|
||||
```rust
|
||||
@@ -326,7 +326,7 @@ fn run_single(&self, input: &Tensor) -> NnResult<Tensor> {
|
||||
|
||||
### FINDING PERF-NN04: WiFiDensePosePipeline -- Two Sequential Inferences [MEDIUM]
|
||||
|
||||
**File**: `v2/crates/wifi-densepose-nn/src/inference.rs`
|
||||
**File**: `rust-port/wifi-densepose-rs/crates/wifi-densepose-nn/src/inference.rs`
|
||||
**Lines**: 389-413
|
||||
|
||||
```rust
|
||||
@@ -352,16 +352,16 @@ pub fn run(&self, csi_input: &Tensor) -> NnResult<DensePoseOutput> {
|
||||
|
||||
| File | Lines | Role |
|
||||
|------|-------|------|
|
||||
| `archive/v1/src/core/csi_processor.py` | 467 | CSI processing pipeline |
|
||||
| `archive/v1/src/services/pose_service.py` | 200+ | Pose estimation service |
|
||||
| `archive/v1/src/api/websocket/connection_manager.py` | 461 | WebSocket management |
|
||||
| `archive/v1/src/sensing/feature_extractor.py` | 150+ | RSSI feature extraction |
|
||||
| `v1/src/core/csi_processor.py` | 467 | CSI processing pipeline |
|
||||
| `v1/src/services/pose_service.py` | 200+ | Pose estimation service |
|
||||
| `v1/src/api/websocket/connection_manager.py` | 461 | WebSocket management |
|
||||
| `v1/src/sensing/feature_extractor.py` | 150+ | RSSI feature extraction |
|
||||
|
||||
---
|
||||
|
||||
### FINDING PERF-PY01: Doppler Feature Extraction -- list() Conversion of deque [CRITICAL]
|
||||
|
||||
**File**: `archive/v1/src/core/csi_processor.py`
|
||||
**File**: `v1/src/core/csi_processor.py`
|
||||
**Lines**: 412-414
|
||||
|
||||
```python
|
||||
@@ -391,7 +391,7 @@ class CircularBuffer:
|
||||
|
||||
### FINDING PERF-PY02: CSI Preprocessing Creates 3 New CSIData Objects per Frame [HIGH]
|
||||
|
||||
**File**: `archive/v1/src/core/csi_processor.py`
|
||||
**File**: `v1/src/core/csi_processor.py`
|
||||
**Lines**: 118-377
|
||||
|
||||
The preprocessing pipeline creates a new CSIData object at each step:
|
||||
@@ -417,7 +417,7 @@ Each CSIData construction copies metadata via `{**csi_data.metadata, 'key': True
|
||||
|
||||
### FINDING PERF-PY03: Correlation Matrix -- Full np.corrcoef on Every Frame [MEDIUM]
|
||||
|
||||
**File**: `archive/v1/src/core/csi_processor.py`
|
||||
**File**: `v1/src/core/csi_processor.py`
|
||||
**Lines**: 391-395
|
||||
|
||||
```python
|
||||
@@ -436,7 +436,7 @@ def _extract_correlation_features(self, csi_data: CSIData) -> np.ndarray:
|
||||
|
||||
### FINDING PERF-PY04: WebSocket Broadcast -- Sequential Send to All Clients [MEDIUM]
|
||||
|
||||
**File**: `archive/v1/src/api/websocket/connection_manager.py`
|
||||
**File**: `v1/src/api/websocket/connection_manager.py`
|
||||
**Lines**: 230-264
|
||||
|
||||
```python
|
||||
@@ -461,7 +461,7 @@ results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
### FINDING PERF-PY05: get_recent_history -- Copies Entire History [LOW]
|
||||
|
||||
**File**: `archive/v1/src/core/csi_processor.py`
|
||||
**File**: `v1/src/core/csi_processor.py`
|
||||
**Lines**: 284-297
|
||||
|
||||
```python
|
||||
@@ -634,7 +634,7 @@ uint32_t next = (s_ring.head + 1) & (EDGE_RING_SLOTS - 1);
|
||||
|
||||
### FINDING PERF-XC01: Missing Parallelism in Multistatic Pipeline [HIGH]
|
||||
|
||||
**File**: `v2/crates/wifi-densepose-signal/src/ruvsense/mod.rs`
|
||||
**File**: `rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/mod.rs`
|
||||
**Lines**: 183-232
|
||||
|
||||
The `RuvSensePipeline` orchestrator processes stages sequentially. The multiband fusion and phase alignment stages for each node are independent and could run in parallel using Rayon:
|
||||
@@ -756,26 +756,26 @@ The following patterns were checked and found to be well-implemented:
|
||||
## Appendix A: File Paths Analyzed
|
||||
|
||||
### Rust Signal Processing
|
||||
- `/workspaces/ruview/v2/crates/wifi-densepose-signal/src/ruvsense/mod.rs`
|
||||
- `/workspaces/ruview/v2/crates/wifi-densepose-signal/src/ruvsense/tomography.rs`
|
||||
- `/workspaces/ruview/v2/crates/wifi-densepose-signal/src/ruvsense/multistatic.rs`
|
||||
- `/workspaces/ruview/v2/crates/wifi-densepose-signal/src/ruvsense/pose_tracker.rs`
|
||||
- `/workspaces/ruview/v2/crates/wifi-densepose-signal/src/ruvsense/field_model.rs`
|
||||
- `/workspaces/ruview/v2/crates/wifi-densepose-signal/src/ruvsense/gesture.rs`
|
||||
- `/workspaces/ruview/v2/crates/wifi-densepose-signal/src/ruvsense/coherence.rs`
|
||||
- `/workspaces/ruview/v2/crates/wifi-densepose-signal/src/ruvsense/coherence_gate.rs`
|
||||
- `/workspaces/ruview/v2/crates/wifi-densepose-signal/src/ruvsense/multiband.rs`
|
||||
- `/workspaces/ruview/v2/crates/wifi-densepose-signal/src/ruvsense/phase_align.rs`
|
||||
- `/workspaces/ruview/v2/crates/wifi-densepose-signal/src/ruvsense/adversarial.rs`
|
||||
- `/workspaces/ruview/v2/crates/wifi-densepose-signal/src/ruvsense/intention.rs`
|
||||
- `/workspaces/ruview/v2/crates/wifi-densepose-signal/src/ruvsense/longitudinal.rs`
|
||||
- `/workspaces/ruview/v2/crates/wifi-densepose-signal/src/ruvsense/cross_room.rs`
|
||||
- `/workspaces/ruview/v2/crates/wifi-densepose-signal/src/ruvsense/temporal_gesture.rs`
|
||||
- `/workspaces/ruview/v2/crates/wifi-densepose-signal/src/ruvsense/attractor_drift.rs`
|
||||
- `/workspaces/ruview/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/mod.rs`
|
||||
- `/workspaces/ruview/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/tomography.rs`
|
||||
- `/workspaces/ruview/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/multistatic.rs`
|
||||
- `/workspaces/ruview/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/pose_tracker.rs`
|
||||
- `/workspaces/ruview/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/field_model.rs`
|
||||
- `/workspaces/ruview/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/gesture.rs`
|
||||
- `/workspaces/ruview/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/coherence.rs`
|
||||
- `/workspaces/ruview/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/coherence_gate.rs`
|
||||
- `/workspaces/ruview/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/multiband.rs`
|
||||
- `/workspaces/ruview/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/phase_align.rs`
|
||||
- `/workspaces/ruview/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/adversarial.rs`
|
||||
- `/workspaces/ruview/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/intention.rs`
|
||||
- `/workspaces/ruview/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/longitudinal.rs`
|
||||
- `/workspaces/ruview/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/cross_room.rs`
|
||||
- `/workspaces/ruview/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/temporal_gesture.rs`
|
||||
- `/workspaces/ruview/rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/attractor_drift.rs`
|
||||
|
||||
### Rust Neural Network
|
||||
- `/workspaces/ruview/v2/crates/wifi-densepose-nn/src/inference.rs`
|
||||
- `/workspaces/ruview/v2/crates/wifi-densepose-nn/src/tensor.rs`
|
||||
- `/workspaces/ruview/rust-port/wifi-densepose-rs/crates/wifi-densepose-nn/src/inference.rs`
|
||||
- `/workspaces/ruview/rust-port/wifi-densepose-rs/crates/wifi-densepose-nn/src/tensor.rs`
|
||||
|
||||
### Python Pipeline
|
||||
- `/workspaces/ruview/v1/src/core/csi_processor.py`
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
**Project:** wifi-densepose (ruview)
|
||||
**Date:** 2026-04-05
|
||||
**Analyst:** QE Test Architect (V3)
|
||||
**Scope:** All test suites across Python (v1), Rust (v2), and Mobile (ui/mobile)
|
||||
**Scope:** All test suites across Python (v1), Rust (rust-port), and Mobile (ui/mobile)
|
||||
|
||||
---
|
||||
|
||||
@@ -14,7 +14,7 @@ The wifi-densepose project contains **3,353 total test functions** across three
|
||||
| Stack | Test Functions | Files | Frameworks |
|
||||
|-------|---------------|-------|------------|
|
||||
| Rust (inline + integration) | 2,658 | 292 source files + 16 integration test files | `#[test]`, Rust built-in |
|
||||
| Python (archive/v1/tests/) | 491 | 30 test files | pytest, pytest-asyncio |
|
||||
| Python (v1/tests/) | 491 | 30 test files | pytest, pytest-asyncio |
|
||||
| Mobile (ui/mobile) | 204 | 25 test files | Jest, React Testing Library |
|
||||
| **Total** | **3,353** | **363** | |
|
||||
|
||||
@@ -26,7 +26,7 @@ The wifi-densepose project contains **3,353 total test functions** across three
|
||||
|
||||
---
|
||||
|
||||
## 1. Python Test Suite Analysis (archive/v1/tests/)
|
||||
## 1. Python Test Suite Analysis (v1/tests/)
|
||||
|
||||
### 1.1 Test Distribution
|
||||
|
||||
@@ -229,7 +229,7 @@ All 14 tests use `MockPoseModel` with `asyncio.sleep()` simulating inference tim
|
||||
|
||||
### 1.10 Test Infrastructure Quality
|
||||
|
||||
**Fixtures (`archive/v1/tests/fixtures/csi_data.py`):**
|
||||
**Fixtures (`v1/tests/fixtures/csi_data.py`):**
|
||||
|
||||
Well-designed `CSIDataGenerator` class (487 lines) with:
|
||||
- Multiple scenario generators (empty room, single person, multi-person)
|
||||
@@ -238,7 +238,7 @@ Well-designed `CSIDataGenerator` class (487 lines) with:
|
||||
- Time series generation
|
||||
- Validation utilities (`validate_csi_sample`)
|
||||
|
||||
**Mocks (`archive/v1/tests/mocks/hardware_mocks.py`):**
|
||||
**Mocks (`v1/tests/mocks/hardware_mocks.py`):**
|
||||
|
||||
Comprehensive mock infrastructure (716 lines) including:
|
||||
- `MockWiFiRouter` with realistic CSI streaming
|
||||
@@ -448,9 +448,9 @@ This is the best-tested service in the mobile suite.
|
||||
|
||||
**High maintenance cost files:**
|
||||
|
||||
1. `archive/v1/tests/mocks/hardware_mocks.py` (716 lines) -- Complex mock infrastructure that must evolve with the production code. Any hardware interface change requires updating this file.
|
||||
1. `v1/tests/mocks/hardware_mocks.py` (716 lines) -- Complex mock infrastructure that must evolve with the production code. Any hardware interface change requires updating this file.
|
||||
|
||||
2. `archive/v1/tests/fixtures/csi_data.py` (487 lines) -- Rich data generation but duplicates some logic from the production `SimulatedCollector`.
|
||||
2. `v1/tests/fixtures/csi_data.py` (487 lines) -- Rich data generation but duplicates some logic from the production `SimulatedCollector`.
|
||||
|
||||
3. The 5 CSI extractor test files collectively contain ~3,000 lines of test code for a single module. Merging to one file would reduce this to ~600 lines.
|
||||
|
||||
@@ -468,20 +468,20 @@ This is the best-tested service in the mobile suite.
|
||||
|
||||
| File | Why It's Good |
|
||||
|------|---------------|
|
||||
| `archive/v1/tests/unit/test_sensing.py` | 45 tests with mathematical rigor, known-signal validation, domain-specific edge cases, cross-receiver agreement, band isolation. No mocks for core logic. |
|
||||
| `archive/v1/tests/unit/test_esp32_binary_parser.py` | Real UDP socket testing, struct-level binary validation, ADR-018 compliance. Tests actual I/Q to amplitude/phase math. |
|
||||
| `v2/.../tests/validation_test.rs` | Physics-based validation (Doppler, phase unwrapping, spectral analysis). Tests prove algorithm correctness, not just non-failure. |
|
||||
| `v2/.../tests/test_losses.rs` | Deterministic data, feature-gated, tests mathematical properties (zero loss for identical inputs, non-zero for mismatched). |
|
||||
| `v1/tests/unit/test_sensing.py` | 45 tests with mathematical rigor, known-signal validation, domain-specific edge cases, cross-receiver agreement, band isolation. No mocks for core logic. |
|
||||
| `v1/tests/unit/test_esp32_binary_parser.py` | Real UDP socket testing, struct-level binary validation, ADR-018 compliance. Tests actual I/Q to amplitude/phase math. |
|
||||
| `rust-port/.../tests/validation_test.rs` | Physics-based validation (Doppler, phase unwrapping, spectral analysis). Tests prove algorithm correctness, not just non-failure. |
|
||||
| `rust-port/.../tests/test_losses.rs` | Deterministic data, feature-gated, tests mathematical properties (zero loss for identical inputs, non-zero for mismatched). |
|
||||
| `ui/mobile/.../utils/ringBuffer.test.ts` | Comprehensive boundary testing (NaN, Infinity, 0, negative, overflow). Tests copy semantics. |
|
||||
|
||||
### 5.2 Worst Test Files (Needs Improvement)
|
||||
|
||||
| File | Issues |
|
||||
|------|--------|
|
||||
| `archive/v1/tests/performance/test_inference_speed.py` | Tests `asyncio.sleep()` accuracy, not model performance. `MockPoseModel` simulates inference with sleep. |
|
||||
| `archive/v1/tests/e2e/test_healthcare_scenario.py` | Not a real E2E test -- defines its own mock classes. Test names contain stale "should_fail_initially" text. |
|
||||
| `archive/v1/tests/unit/test_csi_processor_tdd.py` | 14/25 tests mock the SUT's own private methods. Tests verify mock calls, not behavior. |
|
||||
| `archive/v1/tests/unit/test_phase_sanitizer_tdd.py` | 12/31 tests mock internal methods. Same anti-pattern as csi_processor_tdd. |
|
||||
| `v1/tests/performance/test_inference_speed.py` | Tests `asyncio.sleep()` accuracy, not model performance. `MockPoseModel` simulates inference with sleep. |
|
||||
| `v1/tests/e2e/test_healthcare_scenario.py` | Not a real E2E test -- defines its own mock classes. Test names contain stale "should_fail_initially" text. |
|
||||
| `v1/tests/unit/test_csi_processor_tdd.py` | 14/25 tests mock the SUT's own private methods. Tests verify mock calls, not behavior. |
|
||||
| `v1/tests/unit/test_phase_sanitizer_tdd.py` | 12/31 tests mock internal methods. Same anti-pattern as csi_processor_tdd. |
|
||||
| `ui/mobile/.../components/GaugeArc.test.tsx` | All 4 tests are `expect(toJSON()).not.toBeNull()` -- smoke tests with no behavioral verification. |
|
||||
|
||||
---
|
||||
|
||||
@@ -31,18 +31,18 @@ The WiFi-DensePose system demonstrates strong architectural foundations with a w
|
||||
### Key Findings
|
||||
|
||||
**Strengths:**
|
||||
- Comprehensive error handling middleware with structured error responses, request IDs, and environment-aware detail levels (`archive/v1/src/middleware/error_handler.py`)
|
||||
- Comprehensive error handling middleware with structured error responses, request IDs, and environment-aware detail levels (`v1/src/middleware/error_handler.py`)
|
||||
- Robust WebSocket reconnection with exponential backoff and automatic simulation fallback in the mobile app (`ui/mobile/src/services/ws.service.ts`)
|
||||
- Well-designed health check architecture with component-level status, readiness probes, and liveness endpoints (`archive/v1/src/api/routers/health.py`)
|
||||
- Strong input validation on API models with Pydantic, including range constraints and clear field descriptions (`archive/v1/src/api/routers/pose.py`)
|
||||
- Well-designed health check architecture with component-level status, readiness probes, and liveness endpoints (`v1/src/api/routers/health.py`)
|
||||
- Strong input validation on API models with Pydantic, including range constraints and clear field descriptions (`v1/src/api/routers/pose.py`)
|
||||
- Persistent settings with AsyncStorage in the mobile app, surviving app restarts (`ui/mobile/src/stores/settingsStore.ts`)
|
||||
- Server URL validation with test-before-save workflow in mobile settings (`ui/mobile/src/screens/SettingsScreen/ServerUrlInput.tsx`)
|
||||
|
||||
**Critical Issues:**
|
||||
- API documentation is disabled in production (`docs_url=None`, `redoc_url=None` when `is_production=True`), leaving production API consumers without discoverability (in `archive/v1/src/api/main.py` line 146-148)
|
||||
- No user-facing progress indicator during calibration -- the calibration endpoint returns an estimated duration but there is no polling endpoint progress beyond percentage (`archive/v1/src/api/routers/pose.py` lines 320-361)
|
||||
- Rate limit responses lack a human-readable `Retry-After` message body; the client receives a bare `"Rate limit exceeded"` string with retry information only in HTTP headers (`archive/v1/src/middleware/rate_limit.py` line 323)
|
||||
- CLI `status` command uses emoji/Unicode characters that break in terminals without UTF-8 support (`archive/v1/src/commands/status.py` lines 360-474)
|
||||
- API documentation is disabled in production (`docs_url=None`, `redoc_url=None` when `is_production=True`), leaving production API consumers without discoverability (in `v1/src/api/main.py` line 146-148)
|
||||
- No user-facing progress indicator during calibration -- the calibration endpoint returns an estimated duration but there is no polling endpoint progress beyond percentage (`v1/src/api/routers/pose.py` lines 320-361)
|
||||
- Rate limit responses lack a human-readable `Retry-After` message body; the client receives a bare `"Rate limit exceeded"` string with retry information only in HTTP headers (`v1/src/middleware/rate_limit.py` line 323)
|
||||
- CLI `status` command uses emoji/Unicode characters that break in terminals without UTF-8 support (`v1/src/commands/status.py` lines 360-474)
|
||||
- Mobile app `MainTabs.tsx` passes an inline arrow function as the `component` prop to `Tab.Screen` (line 130), causing unnecessary re-renders on every parent render cycle
|
||||
|
||||
**Top 3 Recommendations:**
|
||||
@@ -166,7 +166,7 @@ WS /api/v1/stream/events - Event stream
|
||||
|
||||
### 4.2 Error Handling (Score: 85/100)
|
||||
|
||||
The `ErrorHandler` class in `archive/v1/src/middleware/error_handler.py` is well-designed:
|
||||
The `ErrorHandler` class in `v1/src/middleware/error_handler.py` is well-designed:
|
||||
|
||||
**Strengths:**
|
||||
- Structured error responses with consistent format: `{ "error": { "code": "...", "message": "...", "timestamp": "...", "request_id": "..." } }`
|
||||
@@ -401,7 +401,7 @@ The `ServerUrlInput` component in the Settings screen provides:
|
||||
|
||||
**Strengths:**
|
||||
- Rust workspace has 1,031+ tests with a single command: `cargo test --workspace --no-default-features`
|
||||
- Deterministic proof verification via `python archive/v1/data/proof/verify.py` with SHA-256 hash checking
|
||||
- Deterministic proof verification via `python v1/data/proof/verify.py` with SHA-256 hash checking
|
||||
- Mobile app has comprehensive test coverage with tests for components, hooks, screens, services, stores, and utilities
|
||||
- Witness bundle verification with `VERIFY.sh` providing 7/7 pass/fail attestation
|
||||
|
||||
@@ -706,20 +706,20 @@ The `provision.py` script in `firmware/esp32-csi-node/` handles WiFi credential
|
||||
This Quality Experience analysis was performed by examining source code across all touchpoints of the WiFi-DensePose system. Files analyzed include:
|
||||
|
||||
**API Layer (9 files):**
|
||||
- `archive/v1/src/api/main.py` -- FastAPI application setup, middleware configuration, exception handlers
|
||||
- `archive/v1/src/api/routers/health.py` -- Health check endpoints
|
||||
- `archive/v1/src/api/routers/pose.py` -- Pose estimation endpoints
|
||||
- `archive/v1/src/api/routers/stream.py` -- WebSocket streaming endpoints
|
||||
- `archive/v1/src/api/websocket/connection_manager.py` -- WebSocket connection lifecycle
|
||||
- `archive/v1/src/api/dependencies.py` -- Dependency injection, authentication, authorization
|
||||
- `archive/v1/src/middleware/error_handler.py` -- Error handling middleware
|
||||
- `archive/v1/src/middleware/rate_limit.py` -- Rate limiting middleware
|
||||
- `v1/src/api/main.py` -- FastAPI application setup, middleware configuration, exception handlers
|
||||
- `v1/src/api/routers/health.py` -- Health check endpoints
|
||||
- `v1/src/api/routers/pose.py` -- Pose estimation endpoints
|
||||
- `v1/src/api/routers/stream.py` -- WebSocket streaming endpoints
|
||||
- `v1/src/api/websocket/connection_manager.py` -- WebSocket connection lifecycle
|
||||
- `v1/src/api/dependencies.py` -- Dependency injection, authentication, authorization
|
||||
- `v1/src/middleware/error_handler.py` -- Error handling middleware
|
||||
- `v1/src/middleware/rate_limit.py` -- Rate limiting middleware
|
||||
|
||||
**CLI Layer (4 files):**
|
||||
- `archive/v1/src/cli.py` -- Click CLI entry point
|
||||
- `archive/v1/src/commands/start.py` -- Server start command
|
||||
- `archive/v1/src/commands/stop.py` -- Server stop command
|
||||
- `archive/v1/src/commands/status.py` -- Server status command
|
||||
- `v1/src/cli.py` -- Click CLI entry point
|
||||
- `v1/src/commands/start.py` -- Server start command
|
||||
- `v1/src/commands/stop.py` -- Server stop command
|
||||
- `v1/src/commands/status.py` -- Server status command
|
||||
|
||||
**Mobile Layer (15 files):**
|
||||
- `ui/mobile/src/screens/LiveScreen/index.tsx` -- Live visualization screen
|
||||
|
||||
@@ -75,7 +75,7 @@ The wifi-densepose project is an ambitious WiFi-based human pose estimation syst
|
||||
**Test Ideas:**
|
||||
| # | Priority | Test Idea | Automation |
|
||||
|---|----------|-----------|------------|
|
||||
| S-08 | P0 | Run `python archive/v1/data/proof/verify.py` in CI on every PR that touches `archive/v1/src/core/` or `archive/v1/src/hardware/` to catch proof-breaking changes | CI |
|
||||
| S-08 | P0 | Run `python v1/data/proof/verify.py` in CI on every PR that touches `v1/src/core/` or `v1/src/hardware/` to catch proof-breaking changes | CI |
|
||||
| S-09 | P2 | Pin numpy/scipy versions in requirements.txt and confirm `verify.py --generate-hash` produces the same hash across Python 3.10, 3.11, and 3.12 | Integration |
|
||||
|
||||
---
|
||||
@@ -222,7 +222,7 @@ The Rust `Esp32CsiParser::parse_frame` takes raw bytes and returns structured `C
|
||||
|
||||
#### D3: Proof Data Integrity
|
||||
|
||||
**Finding:** The proof-of-reality system (`archive/v1/data/proof/verify.py`) is a deterministic pipeline verification tool. It feeds 1,000 synthetic CSI frames through the production CSI processor, hashes the output with SHA-256, and compares against a published hash. This is a strong engineering practice.
|
||||
**Finding:** The proof-of-reality system (`v1/data/proof/verify.py`) is a deterministic pipeline verification tool. It feeds 1,000 synthetic CSI frames through the production CSI processor, hashes the output with SHA-256, and compares against a published hash. This is a strong engineering practice.
|
||||
|
||||
**Risk: LOW**
|
||||
- The proof only exercises the Python v1 pipeline. The Rust port has no equivalent proof-of-reality check.
|
||||
@@ -448,7 +448,7 @@ The ESP32-S3 is the primary sensing node. The mmWave sensors are auxiliary.
|
||||
**Test Ideas:**
|
||||
| # | Priority | Test Idea | Automation |
|
||||
|---|----------|-----------|------------|
|
||||
| O-06 | P0 | Run the complete developer setup workflow from a clean Ubuntu 22.04 VM: clone, install deps, `cargo test --workspace --no-default-features`, `python archive/v1/data/proof/verify.py` -- measure total setup time and document any manual steps | Human Exploration |
|
||||
| O-06 | P0 | Run the complete developer setup workflow from a clean Ubuntu 22.04 VM: clone, install deps, `cargo test --workspace --no-default-features`, `python v1/data/proof/verify.py` -- measure total setup time and document any manual steps | Human Exploration |
|
||||
| O-07 | P1 | Simulate a MAT scan with 5 survivors at varying signal strengths (strong, weak, borderline) and confirm the triage classification matches expected START protocol categories | Integration |
|
||||
|
||||
#### O4: Extreme Use
|
||||
|
||||
@@ -287,22 +287,22 @@
|
||||
| 1 | `firmware/main/wasm_runtime.c` | Firmware | 867 | **Critical** | 0.98 | WASM execution on embedded device, untested attack surface |
|
||||
| 2 | `firmware/main/ota_update.c` | Firmware | 266 | **Critical** | 0.97 | OTA firmware update -- integrity/authentication critical |
|
||||
| 3 | `firmware/main/swarm_bridge.c` | Firmware | 327 | **Critical** | 0.96 | Multi-node mesh networking, untested protocol |
|
||||
| 4 | `archive/v1/src/services/pose_service.py` | Python | 855 | **Critical** | 0.95 | Core production path, highest complexity, no unit tests |
|
||||
| 5 | `archive/v1/src/middleware/auth.py` | Python | 456 | **Critical** | 0.94 | Authentication -- security-critical, no unit tests |
|
||||
| 6 | `archive/v1/src/api/websocket/connection_manager.py` | Python | 460 | **Critical** | 0.93 | WebSocket lifecycle, connection state, no tests |
|
||||
| 4 | `v1/src/services/pose_service.py` | Python | 855 | **Critical** | 0.95 | Core production path, highest complexity, no unit tests |
|
||||
| 5 | `v1/src/middleware/auth.py` | Python | 456 | **Critical** | 0.94 | Authentication -- security-critical, no unit tests |
|
||||
| 6 | `v1/src/api/websocket/connection_manager.py` | Python | 460 | **Critical** | 0.93 | WebSocket lifecycle, connection state, no tests |
|
||||
| 7 | `firmware/main/mmwave_sensor.c` | Firmware | 571 | **Critical** | 0.92 | 60GHz FMCW sensor driver, hardware-critical |
|
||||
| 8 | `firmware/main/wasm_upload.c` | Firmware | 432 | **Critical** | 0.91 | OTA WASM upload, code injection risk |
|
||||
| 9 | `archive/v1/src/services/orchestrator.py` | Python | 394 | **Critical** | 0.90 | Service lifecycle management, no tests |
|
||||
| 10 | `archive/v1/src/database/connection.py` | Python | 639 | **Critical** | 0.89 | DB + Redis connection management, pooling |
|
||||
| 11 | `archive/v1/src/middleware/error_handler.py` | Python | 504 | **High** | 0.87 | Global error handler, affects all requests |
|
||||
| 12 | `archive/v1/src/tasks/monitoring.py` | Python | 771 | **High** | 0.86 | System monitoring, DB queries, async tasks |
|
||||
| 13 | `archive/v1/src/services/hardware_service.py` | Python | 481 | **High** | 0.85 | Hardware abstraction, device management |
|
||||
| 14 | `archive/v1/src/middleware/rate_limit.py` | Python | 464 | **High** | 0.84 | Rate limiting -- DoS protection |
|
||||
| 15 | `archive/v1/src/services/health_check.py` | Python | 464 | **High** | 0.83 | Health monitoring, dependency checks |
|
||||
| 16 | `archive/v1/src/tasks/backup.py` | Python | 609 | **High** | 0.82 | Data backup operations |
|
||||
| 17 | `archive/v1/src/tasks/cleanup.py` | Python | 597 | **High** | 0.81 | Data retention, cleanup logic |
|
||||
| 9 | `v1/src/services/orchestrator.py` | Python | 394 | **Critical** | 0.90 | Service lifecycle management, no tests |
|
||||
| 10 | `v1/src/database/connection.py` | Python | 639 | **Critical** | 0.89 | DB + Redis connection management, pooling |
|
||||
| 11 | `v1/src/middleware/error_handler.py` | Python | 504 | **High** | 0.87 | Global error handler, affects all requests |
|
||||
| 12 | `v1/src/tasks/monitoring.py` | Python | 771 | **High** | 0.86 | System monitoring, DB queries, async tasks |
|
||||
| 13 | `v1/src/services/hardware_service.py` | Python | 481 | **High** | 0.85 | Hardware abstraction, device management |
|
||||
| 14 | `v1/src/middleware/rate_limit.py` | Python | 464 | **High** | 0.84 | Rate limiting -- DoS protection |
|
||||
| 15 | `v1/src/services/health_check.py` | Python | 464 | **High** | 0.83 | Health monitoring, dependency checks |
|
||||
| 16 | `v1/src/tasks/backup.py` | Python | 609 | **High** | 0.82 | Data backup operations |
|
||||
| 17 | `v1/src/tasks/cleanup.py` | Python | 597 | **High** | 0.81 | Data retention, cleanup logic |
|
||||
| 18 | `firmware/main/rvf_parser.c` | Firmware | 239 | **High** | 0.80 | Binary format parsing -- buffer overflow risk |
|
||||
| 19 | `archive/v1/src/api/routers/pose.py` | Python | 419 | **High** | 0.79 | Pose API endpoint handlers |
|
||||
| 19 | `v1/src/api/routers/pose.py` | Python | 419 | **High** | 0.79 | Pose API endpoint handlers |
|
||||
| 20 | `mobile/hooks/useWebViewBridge.ts` | Mobile | 30 | **High** | 0.78 | Native-WebView IPC bridge |
|
||||
|
||||
---
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
|
||||
| # | Issue | File(s) | Impact |
|
||||
|---|-------|---------|--------|
|
||||
| 1 | **Rate limiter bypass** -- trusts `X-Forwarded-For` without validation | `archive/v1/src/middleware/rate_limit.py:200-206` | Any client can bypass rate limits via header spoofing |
|
||||
| 2 | **Exception details leaked** in HTTP responses regardless of environment | `archive/v1/src/api/routers/pose.py:140`, `stream.py:297`, +5 others | Stack traces visible to attackers |
|
||||
| 3 | **WebSocket JWT in URL** -- tokens visible in logs, browser history, proxies | `archive/v1/src/api/routers/stream.py:74`, `archive/v1/src/middleware/auth.py:243` | Token exposure (CWE-598) |
|
||||
| 1 | **Rate limiter bypass** -- trusts `X-Forwarded-For` without validation | `v1/src/middleware/rate_limit.py:200-206` | Any client can bypass rate limits via header spoofing |
|
||||
| 2 | **Exception details leaked** in HTTP responses regardless of environment | `v1/src/api/routers/pose.py:140`, `stream.py:297`, +5 others | Stack traces visible to attackers |
|
||||
| 3 | **WebSocket JWT in URL** -- tokens visible in logs, browser history, proxies | `v1/src/api/routers/stream.py:74`, `v1/src/middleware/auth.py:243` | Token exposure (CWE-598) |
|
||||
| 4 | **Rust tests not in CI** -- 2,618 tests in largest codebase never run in pipeline | No `cargo test` in any GitHub Actions workflow | Regressions ship undetected |
|
||||
| 5 | **WebSocket path mismatch** -- mobile app sends to wrong endpoint | `ui/mobile/src/services/ws.service.ts:104` vs `constants/websocket.ts:1` | Mobile WebSocket connections fail silently |
|
||||
|
||||
@@ -39,16 +39,16 @@
|
||||
| 7 | **O(L*V) tomography voxel scan** per frame | `ruvsense/tomography.rs:345-383` | ~10ms wasted per frame; use DDA ray march for 5-10x speedup |
|
||||
| 8 | **Sequential neural inference** -- defeats GPU batching | `wifi-densepose-nn inference.rs:334-336` | 2-4x latency penalty |
|
||||
| 9 | **720 `.unwrap()` calls** in Rust production code | Across entire Rust workspace | Each is a potential panic in real-time/safety-critical paths |
|
||||
| 10 | **Python Doppler: 112KB alloc per frame** at 20Hz | `archive/v1/src/core/csi_processor.py:412-414` | Converts deque -> list -> numpy every frame |
|
||||
| 10 | **Python Doppler: 112KB alloc per frame** at 20Hz | `v1/src/core/csi_processor.py:412-414` | Converts deque -> list -> numpy every frame |
|
||||
|
||||
## P2 -- Fix This Quarter (Coverage + Safety)
|
||||
|
||||
| # | Issue | File(s) | Impact |
|
||||
|---|-------|---------|--------|
|
||||
| 11 | **11/12 Python modules untested** -- only CSI extraction has unit tests | `archive/v1/src/services/`, `middleware/`, `database/`, `tasks/` | 12,280 LOC with zero unit tests |
|
||||
| 11 | **11/12 Python modules untested** -- only CSI extraction has unit tests | `v1/src/services/`, `middleware/`, `database/`, `tasks/` | 12,280 LOC with zero unit tests |
|
||||
| 12 | **Firmware at 19% coverage** -- WASM runtime, OTA, swarm bridge untested | `firmware/esp32-csi-node/main/wasm_runtime.c` (867 LOC) | Security-critical code with no tests |
|
||||
| 13 | **MAT simulation fallback** -- disaster tool auto-falls back to simulated data | `ui/mobile/src/screens/MATScreen/index.tsx` | Risk of operators monitoring fake data during real incidents |
|
||||
| 14 | **Token blacklist never consulted** during auth | `archive/v1/src/api/middleware/auth.py:246-252` | Revoked tokens remain valid |
|
||||
| 14 | **Token blacklist never consulted** during auth | `v1/src/api/middleware/auth.py:246-252` | Revoked tokens remain valid |
|
||||
| 15 | **50ms frame budget never benchmarked** -- no latency CI gate | No benchmark harness exists | Real-time requirement is aspirational, not verified |
|
||||
|
||||
## P3 -- Technical Debt
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
# Three-Tier Node — Decision Tree
|
||||
|
||||
| Field | Value |
|
||||
|--------------|------------------------------------------------------------------------|
|
||||
| **Status** | Reference — informs whether/how to adopt the three-tier proposal |
|
||||
| **Date** | 2026-04-25 |
|
||||
| **Companion**| `architecture/three-tier-rust-node.md`, `sota/2026-Q2-rf-sensing-and-edge-rust.md` |
|
||||
|
||||
This document maps each load-bearing decision in the three-tier proposal
|
||||
to (a) what it depends on, (b) what evidence would justify yes/no, and
|
||||
(c) which ADR slot would house the decision once made. It is intentionally
|
||||
short — the prose lives in the SOTA survey and the seed exploration.
|
||||
|
||||
---
|
||||
|
||||
## 1. Load-bearing vs independent decisions
|
||||
|
||||
Six decisions are **load-bearing** — they unblock or block other
|
||||
decisions:
|
||||
|
||||
| # | Decision | Blocks |
|
||||
|----|----------------------------------|------------------------------------------|
|
||||
| L1 | Per-node BOM ceiling | Hardware split, Pi shape, all ADRs below |
|
||||
| L2 | Single-MCU vs dual-MCU node | Sensor-MCU runtime, ISR strategy |
|
||||
| L3 | One-Pi-per-node vs one-per-cluster | OTA shape, secure-boot story, BOM |
|
||||
| L4 | CSI no_std maturity gate | Sensor-MCU language choice |
|
||||
| L5 | Mesh control-plane technology | Comms MCU choice (S3 vs C6) |
|
||||
| L6 | Heavy-compute SoC choice | Secure-boot path, ML model class |
|
||||
|
||||
Five decisions are **independent** of the three-tier shape and can be
|
||||
made in parallel:
|
||||
|
||||
| # | Decision |
|
||||
|----|----------------------------------|
|
||||
| I1 | LoRa fallback chip (SX1262 vs LR1121) |
|
||||
| I2 | Charger / PMIC (BQ24074 vs BQ25798) |
|
||||
| I3 | QUIC vs MQTT-over-TLS for backhaul |
|
||||
| I4 | OTA mechanism per die |
|
||||
| I5 | Provisioning protocol (BLE vs USB) |
|
||||
|
||||
---
|
||||
|
||||
## 2. Decision tree (Mermaid)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
L1{"L1: BOM ceiling per node?"}
|
||||
L1 -->|"<= $15"| KEEP_TODAY["Keep ADR-028 single-S3 node.<br/>Three-tier proposal is out of budget."]
|
||||
L1 -->|"$15-$30"| L3
|
||||
L1 -->|"> $30"| L3
|
||||
|
||||
L3{"L3: Heavy compute per node<br/>or per cluster?"}
|
||||
L3 -->|"per cluster (1 Pi / 3-6 nodes)"| HYBRID["Hybrid path:<br/>single-S3 sensor + cluster Pi.<br/>Cheapest viable upgrade."]
|
||||
L3 -->|"per node"| L2
|
||||
|
||||
L2{"L2: Single-MCU or dual-MCU<br/>per node?"}
|
||||
L2 -->|"single MCU"| L4_SINGLE["ADR-081 already covers this.<br/>Investigate WHY a dual-MCU is needed."]
|
||||
L2 -->|"dual MCU (sensor + comms)"| L4
|
||||
|
||||
L4{"L4: Is no_std CSI capture<br/>production-quality?"}
|
||||
L4 -->|"no / unknown"| L4_NO["Hold dual-MCU shape until<br/>esp-csi-rs / esp-radio matches<br/>esp_wifi_set_csi_rx_cb in jitter & quality."]
|
||||
L4 -->|"yes (benchmarked)"| L5
|
||||
|
||||
L5{"L5: Mesh control plane:<br/>WiFi or 802.15.4?"}
|
||||
L5 -->|"WiFi (ESP-WIFI-MESH)"| L5_WIFI["Comms MCU = ESP32-S3.<br/>Stays on existing ADR-029 shape."]
|
||||
L5 -->|"802.15.4 (Thread)"| L5_THREAD["Comms MCU = ESP32-C6.<br/>Hybrid: WiFi data + Thread control."]
|
||||
|
||||
L6{"L6: Heavy compute SoC?"}
|
||||
L6 -->|"Pi Zero 2W"| L6_ZERO["dm-verity + signed FIT.<br/>NOT immutable-ROM secure boot."]
|
||||
L6 -->|"CM4 / Pi 5"| L6_CM4["RPi-foundation secure boot path.<br/>+~$30-50 BOM."]
|
||||
|
||||
HYBRID --> L6
|
||||
L5_WIFI --> L6
|
||||
L5_THREAD --> L6
|
||||
|
||||
L4_NO -.->|"if gated long-term"| HYBRID
|
||||
|
||||
style KEEP_TODAY fill:#cfe
|
||||
style HYBRID fill:#cfe
|
||||
style L4_NO fill:#fec
|
||||
style L4_SINGLE fill:#cfe
|
||||
```
|
||||
|
||||
The tree's recommended cheapest-first path is:
|
||||
**L1 → L3 (per-cluster) → HYBRID**, which keeps today's ESP32-S3 sensor
|
||||
nodes and adds one Pi per 3–6 nodes. This captures most of the QUIC /
|
||||
ML / secure-boot value without re-spinning the per-node PCB.
|
||||
|
||||
---
|
||||
|
||||
## 3. Decision detail — what evidence justifies each branch
|
||||
|
||||
### L1 — Per-node BOM ceiling
|
||||
|
||||
| Branch | Evidence required | ADR slot |
|
||||
|-----------------------|--------------------------------------------------------------------|--------------------------------------|
|
||||
| ≤ $15 | Today's $9 BOM, ADR-028 witness; deployment-cost analysis | No new ADR — keep ADR-028 baseline |
|
||||
| $15–$30 | Cost analysis showing single-MCU + cluster-Pi path < $30 | New ADR (e.g., ADR-083) |
|
||||
| > $30 | Deployment-cost analysis showing per-node Pi pays for itself | Two ADRs (per-node Pi, BOM revision) |
|
||||
|
||||
### L2 — Single vs dual MCU per node
|
||||
|
||||
| Branch | Evidence required | ADR slot |
|
||||
|--------------|--------------------------------------------------------------------------------------------|--------------------------------|
|
||||
| Single MCU | ADR-081 5-layer kernel measurements (already 60 byte feature packets, 0.003% CPU at 5 Hz) | No new ADR — keep ADR-081 |
|
||||
| Dual MCU | Measured ISR-jitter problem on single-MCU node; or no_std-CSI maturity demonstrated | New ADR (firmware split) |
|
||||
|
||||
### L3 — Per-node vs per-cluster heavy compute
|
||||
|
||||
| Branch | Evidence required | ADR slot |
|
||||
|---------------|-----------------------------------------------------------------------------------------------|--------------------------------|
|
||||
| Per cluster | Throughput math: 6 nodes × 5 Hz × 60 B = 1.8 KB/s per cluster; well within USB/Ethernet to Pi | New ADR (cluster-Pi shape) |
|
||||
| Per node | Need: per-node ML, per-node QUIC, per-node secure boot, deployment without LAN gateway | New ADR (per-node Pi shape) |
|
||||
|
||||
### L4 — CSI no_std maturity gate
|
||||
|
||||
| Branch | Evidence required | ADR slot |
|
||||
|------------|--------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------|
|
||||
| Mature | esp-csi-rs (or replacement) on real S3 board: matches esp_wifi_set_csi_rx_cb capture rate, frame-loss, ISR-jitter | Phase-4 of ADR-081 + a `no_std` migration ADR |
|
||||
| Not mature | Side-by-side benchmark shows ≥10% drop in capture quality, or ISR-jitter > 100 µs | Defer — remain on ESP-IDF C path |
|
||||
|
||||
### L5 — Mesh control-plane technology
|
||||
|
||||
| Branch | Evidence required | ADR slot |
|
||||
|-----------------|--------------------------------------------------------------------------------------------------------------|---------------------------------------------|
|
||||
| ESP-WIFI-MESH | ≤ 25-node target; existing ADR-029 + ADR-073 hold | No new ADR — keep ADR-029 |
|
||||
| Thread | ≥ 50-node target; field test showing ESP-WIFI-MESH degradation; comms-MCU change to ESP32-C6 acceptable | New ADR (Thread control plane) |
|
||||
| `esp-mesh-lite` | Wanting IP-layer routing for QUIC + WiFi homogeneity, but staying on S3 | New ADR (mesh-lite migration) |
|
||||
|
||||
### L6 — Heavy-compute SoC choice
|
||||
|
||||
| Branch | Evidence required | ADR slot |
|
||||
|------------|--------------------------------------------------------------------------------------------------------------|-----------------------------------------|
|
||||
| Pi Zero 2W | Buildroot + dm-verity + signed FIT meets the threat model; cost / power matters more than ROM-rooted boot | New ADR (Pi Zero 2W image / OTA) |
|
||||
| CM4 / Pi 5 | True ROM-rooted secure boot is deployment-required (e.g., regulated environment) | New ADR (CM4 image / OTA) |
|
||||
|
||||
---
|
||||
|
||||
## 4. Independent decisions — make in parallel
|
||||
|
||||
Each of these can be evaluated in isolation; none depend on the L-decisions.
|
||||
|
||||
| # | Decision | Default recommendation | ADR slot |
|
||||
|----|---------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------|
|
||||
| I1 | LoRa fallback chip | **SX1262.** LR1121 only if global / 2.4 GHz / satellite roaming is a deployment requirement. (SOTA §6) | ADR (LoRa fallback) |
|
||||
| I2 | PMIC choice | **BQ24074 if panel ≤ 2 W**, **BQ25798 if panel ≥ 5 W or solar-only**. SPV1050 only for sub-watt energy harvesting. (SOTA §7) | ADR (power path) |
|
||||
| I3 | Backhaul protocol | **QUIC (`quinn` + `rustls`)** if bidirectional / large payload / mobile-network handoff matters. **MQTT-over-TLS** for low-rate publish-only. (SOTA §5) | ADR (backhaul) |
|
||||
| I4 | OTA per die | **`embassy-boot` two-slot** on no_std MCUs. **ESP-IDF native OTA** on ESP-IDF MCUs. **A/B + signed FIT** on Pi. (SOTA §3, §9) | ADR (OTA) |
|
||||
| I5 | Provisioning protocol | **BLE provisioning via `esp-idf-svc`** for any in-field reprovisioning; **USB / serial** for factory provisioning only. (No SOTA section — well-trodden ground.) | ADR (provisioning) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Recommended ADR sequence
|
||||
|
||||
If the three-tier proposal is partially adopted, the recommended ADR
|
||||
sequence is **outside-in** — address the cheapest, most independent
|
||||
decisions first, gate the load-bearing ones on real evidence:
|
||||
|
||||
1. **Independent ADRs first** (any order):
|
||||
- I1 LoRa fallback chip choice.
|
||||
- I2 Power-path / PMIC choice (probably BQ24074 if panel stays ≤ 2 W,
|
||||
BQ25798 otherwise).
|
||||
- I3 QUIC vs MQTT-over-TLS (likely MQTT for the heartbeat-only case,
|
||||
QUIC if model updates and fleet sync are real).
|
||||
2. **Per-cluster-Pi ADR** (L3, hybrid branch) — the high-value, low-cost
|
||||
first step. One Pi per 3–6 nodes. Captures most of the ML/QUIC/
|
||||
secure-boot value at minimal per-sensor BOM impact.
|
||||
3. **Mesh control-plane ADR** (L5) — only if deployments target > 25
|
||||
nodes. Otherwise stays on ESP-WIFI-MESH per ADR-029.
|
||||
4. **CSI no_std maturity benchmark ADR** (L4 evidence) — investigate,
|
||||
but do not commit to dual-MCU until benchmarked.
|
||||
5. **Dual-MCU node ADR** (L2) — only after L4 evidence + a clear ML or
|
||||
ISR-jitter problem on the single-MCU node.
|
||||
6. **Three-tier-PCB ADR** (full proposal) — last, only if BOM / threat-
|
||||
model / scale all justify it.
|
||||
|
||||
This ordering deliberately keeps the bulk of the deployable surface on
|
||||
today's ADR-028 / ADR-081 baseline while letting each separable
|
||||
upgrade be evaluated on its own evidence.
|
||||
|
||||
---
|
||||
|
||||
## 6. Out-of-scope for this document
|
||||
|
||||
- **Re-evaluating ADR-029 mesh choices** beyond mentioning Thread as
|
||||
alternative — that belongs in a Mesh-control-plane ADR.
|
||||
- **Specific PCB layout** of any of the candidate boards.
|
||||
- **Cloud-side architecture** (gateway, fleet-sync target, time-series
|
||||
storage). Out of scope of the node architecture proposal.
|
||||
- **Cross-environment domain generalization (ADR-027)** — orthogonal to
|
||||
the hardware shape.
|
||||
- **Multistatic fusion algorithms** (`wifi-densepose-ruvector::viewpoint`)
|
||||
— orthogonal to the hardware shape.
|
||||
|
||||
---
|
||||
|
||||
## 7. References to other documents in this set
|
||||
|
||||
- `architecture/three-tier-rust-node.md` — the seed proposal.
|
||||
- `sota/2026-Q2-rf-sensing-and-edge-rust.md` — SOTA evidence per topic.
|
||||
- `architecture/implementation-plan.md` — earlier (2026-04-02) GOAP plan
|
||||
for ESP32-S3 + Pi Zero 2 W; the three-tier proposal is most usefully
|
||||
read as an extension of this plan.
|
||||
- `architecture/ruvsense-multistatic-fidelity-architecture.md` —
|
||||
multistatic fusion architecture, orthogonal to node hardware shape.
|
||||
@@ -1,434 +0,0 @@
|
||||
# Three-Tier Rust Node — Exploratory Architecture
|
||||
|
||||
| Field | Value |
|
||||
|--------------|------------------------------------------------------------------------|
|
||||
| **Status** | Exploratory / not yet decided |
|
||||
| **Date** | 2026-04-25 |
|
||||
| **Authors** | ruv (proposal), filed by goal-planner research agent |
|
||||
| **Classifies as** | Speculative architectural alternative to ADR-028 / ADR-081 baseline |
|
||||
| **Companion**| `docs/research/sota/2026-Q2-rf-sensing-and-edge-rust.md` (SOTA), `docs/research/architecture/decision-tree.md` (decisions) |
|
||||
|
||||
> **Reading note.** This document files a long architectural exploration the
|
||||
> author wrote before any commitment. It is intentionally optimistic in places
|
||||
> and will be tempered by the SOTA survey filed alongside it. The decision
|
||||
> tree document maps each load-bearing claim to the evidence that would
|
||||
> justify acting on it. Nothing in this document supersedes ADR-028 (the
|
||||
> capability audit) or ADR-081 (the 5-layer adaptive kernel). Both already
|
||||
> describe a working, single-MCU node; this document describes a
|
||||
> hypothetical *three-tier* node that would replace it on PCBs that ship
|
||||
> Pi-class compute next to two ESP32-class radios on a solar-powered HAT.
|
||||
|
||||
---
|
||||
|
||||
## 1. ADRs this proposal would touch
|
||||
|
||||
If pursued, this proposal evolves the following decisions. None are
|
||||
overturned outright; all need re-read in this light.
|
||||
|
||||
- **ADR-028 — ESP32 Capability Audit.** Today's witnessed node is a single
|
||||
ESP32-S3 streaming raw ADR-018 frames over UDP. A three-tier node changes
|
||||
the audit subject from "one MCU" to "two MCUs + a Pi", with implications
|
||||
for the witness bundle, firmware-manifest hashes, and per-node BOM.
|
||||
- **ADR-081 — Adaptive CSI Mesh Firmware Kernel.** The 5-layer kernel
|
||||
already separates radio abstraction (L1), adaptive control (L2), mesh
|
||||
plane (L3), feature extraction (L4), and Rust handoff (L5). A three-tier
|
||||
node would split L1–L2 onto a no_std sensor MCU, L3 onto an ESP-IDF
|
||||
comms MCU, and Layer-5+ Rust workload onto the Pi. The split is
|
||||
compatible with the kernel; it is a deployment shape rather than a
|
||||
redesign.
|
||||
- **ADR-018 — ESP32 Dev Implementation.** ADR-018 binary CSI frames remain
|
||||
the wire format between the sensor MCU and whoever consumes them. The
|
||||
three-tier proposal tightens the contract: ADR-018 frames flow from
|
||||
sensor MCU into the comms MCU only, never directly off the node.
|
||||
- **ADR-029 / ADR-031 — Multistatic and sensing-first RF mode.** A
|
||||
hardware-gated Pi Zero 2W enables the sensing-first mode to actually
|
||||
hibernate the heavy compute, which ADR-031's power model assumes but the
|
||||
current node cannot deliver because heavy compute lives off-node.
|
||||
- **ADR-032 — Multistatic mesh security hardening.** HMAC-SHA256 beacon
|
||||
auth + SipHash-2-4 frame integrity in ADR-032 already cover the
|
||||
inter-node bus. The proposal adds Secure Boot V2 + flash encryption
|
||||
at-rest on each MCU, and a signed Pi A/B image, which are *complements*
|
||||
to ADR-032, not substitutes.
|
||||
|
||||
---
|
||||
|
||||
## 2. Motivating thesis
|
||||
|
||||
A WiFi/RF sensing node has three jobs that prefer three different
|
||||
runtimes:
|
||||
|
||||
1. **Strict-real-time radio capture and DSP** — sub-millisecond ISR
|
||||
discipline, no allocator surprises, predictable interrupt latency.
|
||||
2. **Networking, OTA, mesh, time sync** — TCP/IP, TLS, BLE provisioning,
|
||||
ESP-WIFI-MESH, OTA bootloaders, NVS. The full battery of WiFi-stack
|
||||
features that come with ESP-IDF and FreeRTOS.
|
||||
3. **Heavy compute, ML inference, storage, fleet sync** — gigabytes of
|
||||
model weights, vision inference, persistent storage, QUIC-based fleet
|
||||
sync, optional cloud APIs.
|
||||
|
||||
Today's RuView node tries to fit jobs 1 and 2 onto one ESP32-S3, and job 3
|
||||
either runs on a separate machine (the "sensing-server" host) or is
|
||||
absent. The thesis of this proposal is that **collapsing all three onto
|
||||
a single PCB but onto three separate dies** captures most of the
|
||||
"single node" simplicity without sacrificing the runtime properties of
|
||||
each layer. Concretely:
|
||||
|
||||
- **Sensor MCU** — ESP32-S3, no_std, `esp-hal` + Embassy + `heapless` +
|
||||
`postcard`. ISR-driven CSI capture, channel hopping, short-window DSP.
|
||||
No WiFi stack of its own (the radio is in the comms MCU); a private
|
||||
UART or SPI link to the comms MCU carries serialized frames. *(See SOTA
|
||||
survey, §3, for the ISR-safety caveat that tempers this.)*
|
||||
- **Comms MCU** — second ESP32-S3, ESP-IDF, `esp-idf-svc` + `esp-idf-sys`,
|
||||
TLS/HTTPS/OTA/ESP-WIFI-MESH, NVS provisioning, BLE provisioning, LoRa
|
||||
fallback. Owns the "outside world."
|
||||
- **Pi Zero 2W** — *normally power-gated*. Wakes on event from the comms
|
||||
MCU, runs heavy ML or fleet-sync work, optionally streams QUIC to a
|
||||
gateway, then power-gates again. `tokio` + `quinn` + `rustls` + `axum`.
|
||||
|
||||
A single PCB, a single 1S Li-ion + 2 W solar + linear charger, a single
|
||||
enclosure. Three separate cores each running the runtime they are
|
||||
actually good at.
|
||||
|
||||
---
|
||||
|
||||
## 3. Hardware shape (proposed)
|
||||
|
||||
### 3.1 Bill of materials (per node, target)
|
||||
|
||||
| Slot | Part | Notes |
|
||||
|---------------------|--------------------------------------------------|---------------------------------------------------|
|
||||
| Sensor MCU | ESP32-S3-WROOM-1 (8 MB flash, 8 MB PSRAM) | no_std, Embassy, esp-radio. Always-on. |
|
||||
| Comms MCU | ESP32-S3-MINI-1 or -WROOM-1 (4 MB flash) | ESP-IDF, ESP-WIFI-MESH, OTA, TLS. Mostly-on. |
|
||||
| Heavy compute | Pi Zero 2W (1 GB RAM) | Power-gated by default. Wake on event. |
|
||||
| LoRa fallback | Semtech SX1262 module | Heartbeat + recovery only. Sub-GHz. |
|
||||
| Charger / PMIC | TI BQ24074 (linear) or BQ25798 (buck-boost MPPT) | See SOTA §7 for trade-off. |
|
||||
| Battery | 1S Li-ion 18650 (3.0 Ah class) | Standard cell, easy to source. |
|
||||
| Solar panel | ~2 W, 6 V, IP-rated | Roof-mount or window-mount. |
|
||||
| Pi power gate | Logic-level P-FET high-side switch + ESP GPIO | Hard-cut when idle (350 mA → ~0 mA). |
|
||||
| Inter-MCU bus | UART or SPI between sensor MCU and comms MCU | Postcard-framed binary on a 4-wire link. |
|
||||
| Comms-to-Pi bus | UART (115200–921600 bps) or SPI | Pi-side `tokio-serial`/`spidev`. |
|
||||
| Enclosure | IP54 or IP65 with antenna pass-through | - |
|
||||
| Estimated BOM | $40–55 | At small build qty; falls with volume. |
|
||||
|
||||
This is roughly 4–6× the ~$9 single-S3 node, which is the largest
|
||||
single mark against the proposal. See §7.4 for whether the cost makes
|
||||
sense.
|
||||
|
||||
### 3.2 Power-state hierarchy (proposed)
|
||||
|
||||
| State | Sensor MCU | Comms MCU | Pi Zero 2W | Approx draw |
|
||||
|----------------|------------------|-----------------|------------------|-----------------|
|
||||
| Deep idle | light sleep | DTIM-modulated | hard-off | < 5 mA |
|
||||
| Sample window | active CSI | passive listen | hard-off | ~80 mA |
|
||||
| Event publish | active CSI | TX burst | hard-off | ~150 mA peak |
|
||||
| Escalation | active CSI | TX + bring-up | booting | ~350 mA peak |
|
||||
| ML in progress | active CSI | passive | inferencing | ~450 mA |
|
||||
| Recovery | sleep | LoRa heartbeat | hard-off | ~30 mA |
|
||||
|
||||
The Pi is treated as the heavyweight worker that **must** be hard-power-
|
||||
gated — not soft-suspended — when not in use. ARM SoCs leak in
|
||||
suspend; a 350 mA "off" leakage destroys solar viability.
|
||||
|
||||
### 3.3 Energy budget sketch
|
||||
|
||||
- **Daily load** (sketch, *not measured*): ~1.4 Wh/day assuming Pi wakes
|
||||
≤ 2 minutes/day on average, sensor MCU light-sleeps when idle, comms
|
||||
MCU DTIM-3 most of the time.
|
||||
- **Daily harvest**: 2 W panel × 4 PSH × 0.7 system efficiency ≈ 5.6
|
||||
Wh/day in the seasonal worst case for mid-latitudes.
|
||||
|
||||
Headroom is roughly 4×. If a deployment skews colder/cloudier, or the
|
||||
inter-MCU bus runs hotter, headroom is 2–3×. SOTA §7 covers whether
|
||||
the linear-charger + supercap-buffered topology actually delivers this
|
||||
math, or whether MPPT is needed on a panel this small.
|
||||
|
||||
---
|
||||
|
||||
## 4. Software shape (proposed)
|
||||
|
||||
### 4.1 Sensor MCU — no_std embedded Rust
|
||||
|
||||
| Concern | Crate(s) |
|
||||
|----------------------|--------------------------------------------------------------|
|
||||
| HAL / async runtime | `esp-hal` 1.x + Embassy executor |
|
||||
| Time / timers | `embassy-time` |
|
||||
| Static allocations | `heapless` (`Vec`, `String`, `Deque`, MPMC channels) |
|
||||
| Wire format | `postcard` over `serde` for compact, schema-stable bytes |
|
||||
| CRC | `crc` crate (already used host-side for the L4 packet check) |
|
||||
| RF capture | `esp-radio` (the rename of `esp-wifi`) — CSI hooks via PR |
|
||||
| Inter-MCU bus | `embassy-uart` or `embedded-hal-async` SPI |
|
||||
| Power management | `esp-hal::system::sleep::*` + light-sleep wake on GPIO/timer |
|
||||
|
||||
Boundary: the sensor MCU does **not** initialize a WiFi stack. It owns
|
||||
the PHY for CSI capture only. All actual WiFi connectivity is on the
|
||||
comms MCU. This is the load-bearing simplification of the proposal: it
|
||||
sidesteps the embassy-on-ESP-IDF ISR-safety question by not running
|
||||
ESP-IDF on this die at all.
|
||||
|
||||
### 4.2 Comms MCU — std + ESP-IDF Rust
|
||||
|
||||
| Concern | Crate(s) |
|
||||
|----------------------|--------------------------------------------------------------------------|
|
||||
| FreeRTOS bindings | `esp-idf-sys` |
|
||||
| Service abstractions | `esp-idf-svc` (HTTPS, OTA, NVS, mDNS, BLE, MQTT, ESP-NOW) |
|
||||
| Async runtime | `esp-idf-svc::timer::EspTaskTimerService` (NOT Embassy directly — see §6)|
|
||||
| TLS | mbedTLS via `esp-idf-svc` |
|
||||
| Mesh | ESP-WIFI-MESH (or ESP-MESH-LITE — see SOTA §8) |
|
||||
| OTA | ESP-IDF native OTA (signed images, A/B partitions) |
|
||||
| LoRa fallback | `lora-phy` or vendor C driver via `esp-idf-sys` |
|
||||
| Inter-MCU bus | UART driver (`esp-idf-svc::uart`) framed with postcard |
|
||||
| BLE provisioning | NimBLE via `esp-idf-svc` |
|
||||
|
||||
The comms MCU is the *only* die that needs the full WiFi-stack security
|
||||
surface. That makes it the obvious place to enforce Secure Boot V2 +
|
||||
flash encryption + signed OTA.
|
||||
|
||||
### 4.3 Pi Zero 2W — std Rust on Linux
|
||||
|
||||
| Concern | Crate(s) |
|
||||
|----------------------|-----------------------------------------------------------------------|
|
||||
| Async runtime | `tokio` |
|
||||
| QUIC | `quinn` + `rustls` |
|
||||
| HTTP server (local) | `axum` |
|
||||
| RPC to comms MCU | `tokio-serial` (UART) or `spidev` (SPI), framed with postcard |
|
||||
| ML inference | `tract` (ONNX), `candle` (Pytorch-flavored), or `ort` (ONNX Runtime) |
|
||||
| Persistent storage | `sled` or `redb` |
|
||||
| OS | Buildroot-based custom image, A/B partitions, dm-verity, signed |
|
||||
|
||||
Crucial constraint: the Pi runs **buildroot**, not Raspberry Pi OS. The
|
||||
Raspberry Pi Foundation does not officially support secure boot on the
|
||||
Pi Zero 2W; the secure-boot path is Pi 4/5-only. The cleanest path on a
|
||||
Pi Zero 2W is buildroot + signed FIT image + dm-verity on the rootfs +
|
||||
A/B partitions for OTA. See SOTA §9 for the realistic version of this.
|
||||
|
||||
### 4.4 OTA on three dies
|
||||
|
||||
| Die | OTA mechanism |
|
||||
|--------------|-----------------------------------------------------------------------|
|
||||
| Sensor MCU | `embassy-boot`-style two-slot OTA, signed images, ed25519 verification|
|
||||
| Comms MCU | ESP-IDF native OTA, signed by project key, dual app partitions |
|
||||
| Pi Zero 2W | A/B rootfs, signed FIT, fwupd or homemade `update-agent` binary |
|
||||
|
||||
OTA is the area where the three-tier shape is most defensible. Each die's
|
||||
update is a separate, independently rollback-able artifact. The comms
|
||||
MCU acts as the *broker* — it pulls signed images for all three dies,
|
||||
verifies them, and pushes them onto the sensor MCU and Pi over their
|
||||
respective buses.
|
||||
|
||||
---
|
||||
|
||||
## 5. Networking shape (proposed)
|
||||
|
||||
Three concentric rings:
|
||||
|
||||
1. **Inner ring — node-local IPC.** Postcard over UART/SPI between the
|
||||
three dies. Length-prefixed, CRC-checked, no encryption (it's on a
|
||||
trace, not a wire).
|
||||
2. **Middle ring — RuView mesh.** ESP-WIFI-MESH (or ESP-MESH-LITE)
|
||||
between comms MCUs across nodes, carrying L3 mesh-plane messages
|
||||
from ADR-081 (TIME_SYNC, ROLE_ASSIGN, CHANNEL_PLAN, FEATURE_DELTA,
|
||||
HEALTH, ANOMALY_ALERT). Authenticated with HMAC-SHA256 per ADR-032.
|
||||
3. **Outer ring — backhaul.** QUIC from the Pi to a gateway/cloud
|
||||
target (`quinn` + `rustls`), with the gateway optionally being
|
||||
another node's Pi acting as a fusion-relay. LoRa is the *fallback*
|
||||
ring for heartbeats and recovery commands when the WiFi mesh is
|
||||
degraded.
|
||||
|
||||
LoRa duty-cycle math (EU868 1% in the relevant sub-band, US915 dwell-
|
||||
time-only) is friendly to "20 bytes every minute" heartbeats; at SF7,
|
||||
125 kHz, the airtime is ~40 ms per packet — far under the 36 s/hour
|
||||
EU868 limit. See SOTA §6 for the citation.
|
||||
|
||||
---
|
||||
|
||||
## 6. Security posture (proposed)
|
||||
|
||||
The proposal layers four mechanisms on each MCU:
|
||||
|
||||
- **Secure Boot V2** — RSA-3072 or ECDSA signed bootloader, immutable
|
||||
primary key digest in eFuse.
|
||||
- **Flash encryption** — AES-XTS-256 with per-device key burned in eFuse,
|
||||
hardware-isolated.
|
||||
- **Disabled ROM download** — `DIS_DOWNLOAD_MODE` fuse blown after
|
||||
provisioning so the device cannot be coerced back into a UART-ROM
|
||||
state.
|
||||
- **Signed OTA images** — separate signing key from the secure-boot key,
|
||||
per-image rollback counter, anti-rollback eFuse counter.
|
||||
|
||||
On the Pi: dm-verity over a read-only rootfs, signed FIT image with the
|
||||
RPi-foundation-blessed (where possible) bootcode, A/B partitions, and a
|
||||
signed manifest of the three dies' image hashes shipped together. The
|
||||
comms MCU validates the manifest before consuming any image.
|
||||
|
||||
This is **complementary** to ADR-032's HMAC-SHA256 + SipHash-2-4 mesh
|
||||
hardening — those protect frames in flight; Secure Boot + flash
|
||||
encryption protect images at rest.
|
||||
|
||||
---
|
||||
|
||||
## 7. Honest critique of this proposal
|
||||
|
||||
This section is required by the project conventions. The companion SOTA
|
||||
survey expands each of these.
|
||||
|
||||
### 7.1 The cost story is bad before volume
|
||||
|
||||
A single ESP32-S3 node is ~$9 today. A three-tier node is closer to
|
||||
$40–55. RuView's design point of "many cheap nodes" rewards low BOM. The
|
||||
three-tier shape is justified only if each node *also* replaces a
|
||||
sensing-server host (i.e., a Pi or laptop running the sensing pipeline)
|
||||
that would have cost more than the marginal Pi-on-each-node. In a
|
||||
deployment with 3 nodes feeding one $80 host, the host already amortizes
|
||||
across the nodes. In a 50-node deployment, the math changes.
|
||||
|
||||
### 7.2 The embassy-on-ESP-IDF ISR-safety question is real
|
||||
|
||||
The proposal *avoids* this question by giving the sensor MCU a no_std
|
||||
runtime instead of putting embassy on top of esp-idf-svc. The reason
|
||||
this matters: per esp-idf-svc maintainers, **embassy-executor is not
|
||||
ISR-safe** in the esp-idf-svc setup (it relies on `critical-section`,
|
||||
which on esp-idf-hal is implemented over FreeRTOS task suspension). On
|
||||
no_std with `esp-hal`, embassy is fine; on top of ESP-IDF, it is not.
|
||||
The two-MCU split is the cleanest engineering answer to the question;
|
||||
the alternative is keeping ESP-IDF on the single MCU (today's design)
|
||||
and not introducing embassy at all. SOTA §3 documents the citation.
|
||||
|
||||
### 7.3 esp-radio replaces esp-wifi, and CSI no_std support is partial
|
||||
|
||||
The crate that the sensor MCU would use to capture CSI (in the
|
||||
`esp-rs/esp-hal` 1.x ecosystem) was renamed to `esp-radio`. Third-party
|
||||
`esp-csi-rs` exists and targets no_std but is described as
|
||||
"early development." The 5-layer kernel today runs on top of ESP-IDF
|
||||
v5.4 in C — a bird in the hand. Migrating CSI capture to no_std is a
|
||||
distinct project, not a side effect of the three-tier shape. SOTA §2
|
||||
covers the maturity matrix.
|
||||
|
||||
### 7.4 The Pi Zero 2W secure-boot story is weaker than the proposal implies
|
||||
|
||||
The Raspberry Pi Foundation's official secure-boot path is **Pi 4 / Pi 5
|
||||
only**, with a USB-rooted RSA chain. There is no official secure-boot
|
||||
bring-up document for the Pi Zero 2W. Buildroot + signed FIT + dm-verity
|
||||
gets you most of the threat surface — but the proposal's "Pi 4 + buildroot
|
||||
is the strongest path" line is not a Pi Zero 2W story. If true secure
|
||||
boot matters for the deployment, the heavy-compute die should arguably
|
||||
be a Pi 4 Compute Module (CM4) and not a Pi Zero 2W. SOTA §9 covers it.
|
||||
|
||||
### 7.5 ESP-WIFI-MESH at 50–500 nodes is an open question
|
||||
|
||||
Espressif documents up to 1,000 nodes and 25 layers as theoretical limits
|
||||
for ESP-WIFI-MESH, with a recommended fan-out of 6 per node. There is
|
||||
limited public evidence of stable 100+ node deployments in adversarial
|
||||
RF environments. Comms-MCU mesh handling at scale is *not free*: the
|
||||
mesh stack runs in the comms MCU's main loop, sharing CPU with TLS, OTA,
|
||||
and BLE. SOTA §8 covers BLE Mesh / Thread / Zigbee comparison. None of
|
||||
those replace WiFi-stack-sharing for CSI capture, but they could replace
|
||||
ESP-WIFI-MESH for control-plane traffic if scale becomes a problem.
|
||||
|
||||
### 7.6 MPPT vs linear charger at 2 W panel
|
||||
|
||||
The proposal's BQ24074-based linear-charger topology is fine for a 2 W
|
||||
panel; the efficiency loss vs MPPT is real but small at this scale.
|
||||
At 2 W, the MPPT die (BQ25798) silicon, inductor, and code complexity
|
||||
costs partly cancel its efficiency gain. SOTA §7 has the math.
|
||||
|
||||
### 7.7 The QUIC outer ring is overkill for the heartbeat case
|
||||
|
||||
QUIC is a strong choice when the Pi has lots of bursty data and is
|
||||
behind a NAT or on flaky cellular. For a node that wakes 2 minutes/day
|
||||
and emits a few KB of summarized features, MQTT-over-TLS or even
|
||||
plain HTTPS is simpler and adequate. QUIC's value goes up if the Pi
|
||||
also runs bidirectional model updates or large-batch fleet sync.
|
||||
SOTA §5.
|
||||
|
||||
---
|
||||
|
||||
## 8. What evidence would justify acting on this proposal
|
||||
|
||||
This section maps to the decision tree in
|
||||
`docs/research/architecture/decision-tree.md`. The short version:
|
||||
|
||||
1. **Per-node cost ceiling.** Decide the BOM ceiling per node. The
|
||||
three-tier shape only makes sense above ~$30/node and at deployments
|
||||
where the host computer is *not* a separate cost.
|
||||
2. **CSI no_std maturity gate.** `esp-csi-rs` (or the replacement under
|
||||
`esp-radio`) must demonstrate equivalent capture quality to today's
|
||||
`esp-wifi-set-csi-rx-cb`-based path on a real ESP32-S3 board, with
|
||||
ISR-jitter measured. Until this is verified, the sensor-MCU Rust
|
||||
story is risk.
|
||||
3. **Inter-MCU bus saturation.** Postcard-framed UART/SPI between the
|
||||
sensor MCU and comms MCU must carry ADR-018 frames at the target
|
||||
capture rate without backpressure-induced drops at the sensor MCU.
|
||||
4. **Pi power-gate budget.** Measured leakage of the gated Pi Zero 2W,
|
||||
with proven cold-boot wake-up under 5 s, is required before the
|
||||
energy budget closes.
|
||||
5. **Mesh scale evidence.** A 12+ node ESP-WIFI-MESH (or alternative)
|
||||
field test at sustained 1–10 Hz `rv_feature_state_t` upload is
|
||||
required to validate the middle ring at >>3 nodes.
|
||||
6. **Secure-boot path on Pi Zero 2W.** Either accept that the Pi cannot
|
||||
be fully secure-booted, or upgrade the heavy-compute die to a CM4 /
|
||||
CM5 / Pi 5 if true secure boot is a deployment requirement.
|
||||
|
||||
---
|
||||
|
||||
## 9. Open questions
|
||||
|
||||
The proposal as written elides answers to these:
|
||||
|
||||
- **Why two ESP32-S3 dies and not one ESP32-S3 plus one ESP32-C6?** The
|
||||
C6 is RISC-V, has 802.15.4 + WiFi 6, and would let the comms MCU
|
||||
handle BLE Mesh / Thread / Zigbee natively. The two-S3 split chose
|
||||
homogeneity and Xtensa toolchain; the C6 split chooses richer
|
||||
protocol coverage on the comms die.
|
||||
- **Is the sensor MCU strictly necessary?** Today, the single-MCU node
|
||||
(ADR-028 / ADR-081) handles CSI capture and ESP-IDF networking on one
|
||||
S3, in C, and works. The two-MCU-on-board case is justified mainly by
|
||||
*ISR purity* and *Rust no_std*, not by a missing capability today.
|
||||
- **Why a Pi Zero 2W rather than the Pi being the gateway?** The
|
||||
proposal puts a Pi *on every node*. A more conservative shape is one
|
||||
Pi per *site* (or per cluster of 3–6 nodes), with the nodes staying
|
||||
single-MCU. That keeps the BOM near today's $9/node for sensors,
|
||||
isolates heavy compute, and concentrates secure boot on a smaller
|
||||
number of more capable dies. This is the deployment shape implicit in
|
||||
ADR-031's sensing-first mode and is worth comparing head-to-head.
|
||||
- **What does a single 50-node deployment cost** under each of: today's
|
||||
shape (one S3 + one host), one-Pi-per-site (one S3 + one Pi per ~6
|
||||
nodes), and the proposal (3-die-per-node)? The cost crossover point
|
||||
determines which architecture is correct.
|
||||
|
||||
---
|
||||
|
||||
## 10. Recommendation
|
||||
|
||||
This document records the proposal accurately. It does not recommend
|
||||
adopting it. The recommendation, if a decision is forced, is:
|
||||
|
||||
1. **Do not build a three-tier-per-node PCB now.** The current shape
|
||||
(single ESP32-S3 + ADR-081 5-layer kernel) is the witnessed system.
|
||||
2. **Investigate one-Pi-per-site as the cheaper variant** (proposal §9
|
||||
bullet 3). It captures most of the heavy-compute and QUIC-backhaul
|
||||
benefits at a fraction of the BOM.
|
||||
3. **Spend the first chunk of effort on the three "evidence" gates from
|
||||
§8** — CSI no_std maturity, ESP-WIFI-MESH at scale, and Pi
|
||||
secure-boot reality — *before* committing to a hardware re-spin.
|
||||
4. **Reserve the three-tier shape** for a future "RuView Pro" SKU
|
||||
targeting deployments where per-node BOM is not the dominant cost
|
||||
and full secure-boot + dm-verity at the edge is mandatory.
|
||||
|
||||
The decision tree document codifies these gates as branch points so
|
||||
they can be checked off independently rather than as one large
|
||||
all-or-nothing ADR.
|
||||
|
||||
---
|
||||
|
||||
## 11. Companion documents
|
||||
|
||||
- **SOTA survey.** `docs/research/sota/2026-Q2-rf-sensing-and-edge-rust.md`
|
||||
— citations, primary sources, what's true in 2026 for each load-bearing
|
||||
claim above.
|
||||
- **Decision tree.** `docs/research/architecture/decision-tree.md` — the
|
||||
Mermaid map from each load-bearing decision to its dependencies and
|
||||
ADR slot.
|
||||
- **Existing implementation plan.** `docs/research/architecture/implementation-plan.md`
|
||||
— the ESP32-S3 + Pi Zero 2W goal-state plan from 2026-04-02. The
|
||||
three-tier proposal is most usefully read as an evolution of *that*
|
||||
plan rather than a replacement of ADR-028.
|
||||
@@ -337,7 +337,7 @@ Usage in rf_topology:
|
||||
### 3.1 Module Location
|
||||
|
||||
```
|
||||
v2/crates/wifi-densepose-signal/src/ruvsense/
|
||||
rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/
|
||||
rf_topology.rs <-- New module (primary)
|
||||
rf_topology/
|
||||
graph.rs <-- RfGraph aggregate root
|
||||
@@ -351,7 +351,7 @@ v2/crates/wifi-densepose-signal/src/ruvsense/
|
||||
Alternatively, rf_topology could be a standalone crate:
|
||||
|
||||
```
|
||||
v2/crates/wifi-densepose-topology/
|
||||
rust-port/wifi-densepose-rs/crates/wifi-densepose-topology/
|
||||
src/
|
||||
lib.rs
|
||||
graph.rs
|
||||
|
||||
@@ -1,601 +0,0 @@
|
||||
# SOTA Survey — RF Sensing and Edge Rust (2026 Q2)
|
||||
|
||||
| Field | Value |
|
||||
|--------------|------------------------------------------------------------------------|
|
||||
| **Status** | Reference / informs `architecture/three-tier-rust-node.md` |
|
||||
| **Date** | 2026-04-25 |
|
||||
| **Author** | goal-planner research agent |
|
||||
| **Scope** | What's true in 2026, what holds up in the three-tier proposal, what to reconsider |
|
||||
| **Word target** | ~3,500 words |
|
||||
|
||||
> **Conventions.** Each section answers (a) what's true in 2026, (b) what
|
||||
> claims in the three-tier proposal hold up, (c) what to reconsider, and
|
||||
> (d) primary references. Where no primary source could be located, the
|
||||
> claim is explicitly marked **"no primary source found, mark as
|
||||
> conjecture."**
|
||||
|
||||
---
|
||||
|
||||
## 1. WiFi CSI through-wall pose / occupancy estimation
|
||||
|
||||
### 1.1 What's true in 2026
|
||||
|
||||
The CSI-to-pose literature has matured along three orthogonal axes since
|
||||
DensePose-from-WiFi (2022) lit the fuse:
|
||||
|
||||
- **Lightweight architectures.** WiFlow (Feb 2026) demonstrated a
|
||||
spatio-temporal-decoupled network with 4.82 M parameters, 0.47 GFLOPs,
|
||||
PCK@20 = 97.0% and MPJPE ≈ 8 mm on the random-split MM-Fi benchmark,
|
||||
3–4× smaller than WPformer and ~25× smaller than WiSPPN.
|
||||
- **Domain generalization.** PerceptAlign (DT-Pose) and the
|
||||
cross-environment evaluation in MM-Fi made the cross-subject and
|
||||
cross-layout numbers honest. PerceptAlign reports MPJPE 222 mm on Scene
|
||||
4 and 317 mm on Scene 5 in cross-layout test, beating prior SOTA by
|
||||
>50% — but those are still order-of-magnitude worse than in-domain.
|
||||
- **Topological priors.** GraphPose-Fi (2025) and topology-constrained
|
||||
decoders (DT-Pose) explicitly use the human skeleton as a graph,
|
||||
improving plausibility under occlusion.
|
||||
- **Multistatic geometry.** RuView's own ADR-029/ADR-031 line is the
|
||||
practical multistatic story; ISAC-Fi (Aug 2024) and the multistatic
|
||||
ISAC-MIMO papers (2024–2025) describe similar geometry as a 6G research
|
||||
topic. IEEE 802.11bf-2025 (published 26 September 2025) is the
|
||||
standardization vector.
|
||||
|
||||
### 1.2 What holds up
|
||||
|
||||
The proposal's claim that "3–6 ESP32-S3 nodes can do meaningful pose
|
||||
work" is consistent with WiFlow's network sizes (4.82 M params, INT8
|
||||
~5 MB) and with the MM-Fi multi-link benchmark. The CSI pipeline does
|
||||
not need a Pi *per node* to run inference; one Pi per cluster is
|
||||
sufficient. RuView's existing ESP32-mesh + sensing-server already
|
||||
demonstrates the shape.
|
||||
|
||||
### 1.3 What to reconsider
|
||||
|
||||
- **Through-wall claims are still aggressive.** Published WiFi sensing
|
||||
papers focus on line-of-sight or single-wall cases; published
|
||||
through-multiple-walls numbers in 2025–2026 are scarce. The
|
||||
three-tier proposal's "through-wall" framing should be tempered to
|
||||
"through-thin-wall" without primary evidence. *No primary source
|
||||
found for through-multiple-walls, mark as conjecture.*
|
||||
- **Nexmon-on-Pi is not obviously a win.** Nexmon CSI on a Pi 4 captures
|
||||
up to 80 MHz BW on Broadcom chips and gives more subcarriers per frame
|
||||
than ESP32, but the Pi platform has no equivalent of ESP32 Secure Boot
|
||||
V2, and the Broadcom firmware-patch path is fragile across kernel
|
||||
releases. RuView's existing ESP32-S3 mesh already beats Nexmon-on-Pi
|
||||
on cost, security posture, and provisioning.
|
||||
- **USRP/SDR is overkill for occupancy and pose**, and is far over the
|
||||
proposal's BOM ceiling. It would only become attractive for
|
||||
research-grade beamforming or sub-cm ranging.
|
||||
|
||||
### 1.4 Primary references
|
||||
|
||||
- WiFlow: [arXiv:2602.08661](https://arxiv.org/html/2602.08661) — Feb 2026.
|
||||
- DT-Pose: [arXiv:2501.09411](https://arxiv.org/abs/2501.09411) — Jan 2025.
|
||||
- GraphPose-Fi: [arXiv:2511.19105](https://arxiv.org/abs/2511.19105) — Nov 2025.
|
||||
- Geometry-aware cross-layout HPE: [arXiv:2601.12252](https://arxiv.org/html/2601.12252).
|
||||
- Nexmon CSI: [seemoo-lab/nexmon_csi](https://github.com/seemoo-lab/nexmon_csi).
|
||||
|
||||
---
|
||||
|
||||
## 2. IEEE 802.11bf and multistatic ISAC
|
||||
|
||||
### 2.1 What's true in 2026
|
||||
|
||||
**IEEE Std 802.11bf-2025 was published 26 September 2025** and is the
|
||||
ratified amendment for WLAN sensing in license-exempt bands 1–7.125 GHz
|
||||
and >45 GHz. The 3rd SA Ballot Recirculation closed 16 January 2025
|
||||
with 98% approval. P802.11bf/D8.0 (March 2025) was the last public
|
||||
draft. The standard defines sensing operation on top of HE/EHT PHYs and
|
||||
on the DMG/EDMG (60 GHz) PHYs.
|
||||
|
||||
3GPP RAN #108 (June 2025) admitted ISAC into the 6G study scope as a
|
||||
"Day 1" 6G feature. ISAC-Fi (Aug 2024) demonstrated *monostatic* sensing
|
||||
over commodity WiFi by repurposing the communication waveform.
|
||||
Multistatic ISAC over cell-free MIMO (2024–2025) is the analytical
|
||||
direction.
|
||||
|
||||
### 2.2 What holds up
|
||||
|
||||
The three-tier proposal's framing of "WiFi mesh + multistatic sensing"
|
||||
is well-aligned with where the standard is moving. ADR-029's existing
|
||||
multistatic mode and ADR-073's multifrequency mesh scan are the kind of
|
||||
pre-standard implementations that 802.11bf is now codifying.
|
||||
|
||||
### 2.3 What to reconsider
|
||||
|
||||
- **802.11bf does not turn an ESP32 into an 802.11bf sensor.** It
|
||||
defines a *protocol* for sensing-aware exchanges between APs and
|
||||
STAs. Off-the-shelf ESP32-S3 silicon was designed before the standard;
|
||||
CSI extraction on ESP32 will keep being a side channel, not a
|
||||
standards-blessed feature, until Espressif ships a chip with the
|
||||
802.11bf MAC primitives. *No primary source found for an Espressif
|
||||
802.11bf-aware product, mark as conjecture.*
|
||||
- **ISAC-Fi's monostatic-on-commodity-WiFi result** is interesting but
|
||||
requires PHY changes; not a path to ESP32 today.
|
||||
- **The proposal should claim "802.11bf-compatible feature set" rather
|
||||
than "802.11bf-compliant"** until silicon exists.
|
||||
|
||||
### 2.4 Primary references
|
||||
|
||||
- IEEE 802.11bf-2025: [standards.ieee.org](https://standards.ieee.org/ieee/802.11bf/11574/).
|
||||
- ISAC-Fi: [arXiv:2408.09851](https://arxiv.org/abs/2408.09851).
|
||||
- IEEE 802.11bf overview paper: [arXiv:2207.04859](https://arxiv.org/pdf/2207.04859).
|
||||
- NIST overview: [nist.gov/publications/ieee-80211bf](https://www.nist.gov/publications/ieee-80211bf-enabling-widespread-adoption-wi-fi-sensing).
|
||||
|
||||
---
|
||||
|
||||
## 3. Embedded Rust ecosystem for ESP32-S3 (2026)
|
||||
|
||||
### 3.1 What's true in 2026
|
||||
|
||||
The esp-rs ecosystem has matured but rebranded:
|
||||
|
||||
- **`esp-hal` is at 1.x.** `esp-hal 1.0.0` shipped October 2023; `1.1.0`
|
||||
was released April 2024. Stabilized HAL APIs, async drivers, but with
|
||||
the constraint that "async drivers can no longer be sent between
|
||||
cores and executors."
|
||||
- **`esp-wifi` was renamed to `esp-radio`** in the 1.x line. The
|
||||
scheduler functionality moved to a new crate `esp-rtos`. Existing
|
||||
`esp-wifi` references in tutorials are pre-1.x.
|
||||
- **Embassy on ESP** is split: on no_std ESP-HAL it's a first-class
|
||||
citizen, but the Embassy team and Espressif explicitly steer Embassy
|
||||
use *toward* `esp-rtos` over time.
|
||||
- **Embassy on top of `esp-idf-svc` (std)** has a documented gotcha:
|
||||
**embassy-executor is not ISR-safe** because it depends on
|
||||
`critical-section`, which `esp-idf-hal` implements over FreeRTOS task
|
||||
suspension. The recommended std executor is `edge-executor` or the
|
||||
built-in `esp-idf-hal` executor.
|
||||
- **CSI capture on no_std** via `esp-csi-rs` (third-party crate) exists
|
||||
but is documented as "still in early development." The
|
||||
production-blessed CSI path remains `esp_wifi_set_csi_rx_cb()` in
|
||||
ESP-IDF C — exactly what `firmware/esp32-csi-node/main/csi_collector.c`
|
||||
uses today.
|
||||
|
||||
### 3.2 What holds up
|
||||
|
||||
The three-tier proposal's choice to put the **sensor MCU on no_std**
|
||||
(`esp-hal` + Embassy) avoids the ESP-IDF ISR-safety question entirely,
|
||||
which is the right architectural answer to a real problem. The proposal
|
||||
is correct that `heapless` + `postcard` + `embassy-time` is the modern
|
||||
no_std default.
|
||||
|
||||
### 3.3 What to reconsider
|
||||
|
||||
- **Update the toolchain names.** The proposal lists `esp-wifi`; in 1.x
|
||||
this is `esp-radio`. It lists `embassy-executor` on the comms MCU
|
||||
by implication; on the comms MCU the executor must be
|
||||
`edge-executor` or `esp-idf-hal`'s built-in executor, not Embassy.
|
||||
- **CSI maturity is the gating risk.** `esp-csi-rs` is early
|
||||
development and the production CSI path is still C. Migrating CSI to
|
||||
no_std Rust is a project unto itself, not a free side effect of
|
||||
splitting the dies.
|
||||
- **`esp-idf-svc` parity with C ESP-IDF is good but not 100%.** OTA,
|
||||
HTTPS, NVS, BLE provisioning, ESP-WIFI-MESH all have wrappers. Some
|
||||
niche ESP-IDF C APIs still need `esp-idf-sys` raw FFI. This is fine
|
||||
but means the comms MCU is not "all-Rust" — there's a layer of unsafe
|
||||
wrapping at the bottom.
|
||||
|
||||
### 3.4 Primary references
|
||||
|
||||
- esp-hal releases: [github.com/esp-rs/esp-hal/releases](https://github.com/esp-rs/esp-hal/releases).
|
||||
- esp-idf-svc CHANGELOG: [github.com/esp-rs/esp-idf-svc/blob/master/CHANGELOG.md](https://github.com/esp-rs/esp-idf-svc/blob/master/CHANGELOG.md).
|
||||
- Embassy ISR-safety gotcha: [esp-idf-svc#342](https://github.com/esp-rs/esp-idf-svc/issues/342) and esp-idf-svc CHANGELOG.
|
||||
- esp-csi-rs crate: [crates.io/crates/esp-csi-rs](https://crates.io/crates/esp-csi-rs).
|
||||
- Embassy Book: [embassy.dev/book](https://embassy.dev/book/).
|
||||
|
||||
---
|
||||
|
||||
## 4. Edge ML for CSI on ESP32-class hardware
|
||||
|
||||
### 4.1 What's true in 2026
|
||||
|
||||
- **TFLite Micro on ESP32-S3** is the most-cited path. Reported
|
||||
numbers: wake-word inference at 50–60 ms latency, model size ~240 KB
|
||||
flash, ~350 KB RAM. INT8 quantization reportedly delivers >6× speedup
|
||||
over float on S3. Espressif's `esp-tflite-micro` is the reference
|
||||
port.
|
||||
- **`tract`** (Sonos's pure-Rust ONNX/NNEF runtime) targets std Linux
|
||||
primarily; there is no widely-adopted no_std no-alloc port.
|
||||
- **`candle`** (Hugging Face's Pytorch-flavored Rust ML library) is std
|
||||
Linux/macOS/Windows; not designed for MCU class.
|
||||
- **ONNX Runtime (`ort` Rust binding)** is a wrapper over the C++
|
||||
runtime; on ARMv8 (Pi Zero 2W) it works, on Xtensa it does not.
|
||||
- **ESP-DL** is Espressif's own DL framework for ESP32-S2/S3, optimized
|
||||
for the AI extensions of the Xtensa LX7 (which ESP32-S3 has). It is C,
|
||||
not Rust.
|
||||
|
||||
For a 4.82 M-param INT8 WiFlow at 0.47 GFLOPs:
|
||||
|
||||
- On a Pi Zero 2W (Cortex-A53 quad, NEON), inference is plausibly in
|
||||
the 50–100 ms range. *No primary measurement found for WiFlow on Pi
|
||||
Zero 2W; mark as conjecture.*
|
||||
- On an ESP32-S3 (Xtensa LX7, 240 MHz, AI extensions), even INT8 4.82M
|
||||
is outside the 8 MB flash + 8 MB PSRAM envelope when intermediate
|
||||
tensors are counted. WiFlow on S3 would require additional pruning or
|
||||
a smaller model class.
|
||||
|
||||
### 4.2 What holds up
|
||||
|
||||
The proposal's split between "sensor MCU does ISR-clean DSP" and "Pi
|
||||
runs the model" is the right shape. ML inference at the WiFlow scale is
|
||||
*not* an ESP32 workload in 2026.
|
||||
|
||||
### 4.3 What to reconsider
|
||||
|
||||
- **The sensor MCU's ML role should be tiny-feature inference, not
|
||||
pose.** Motion classification, presence binary, anomaly thresholding —
|
||||
the ADR-039 Tier-0/Tier-1 outputs — fit on ESP32-S3 with TFLite Micro
|
||||
or hand-written DSP. They do not fit `tract` or `candle` no_std.
|
||||
- **For Rust-on-MCU-ML**, the realistic path is hand-rolled INT8
|
||||
inference (RuView's `wifi-densepose-nn` already has FFI hooks) or a
|
||||
Rust port of a tiny TFLM-style runtime. **No mainstream Rust
|
||||
no_std-no_alloc ONNX runtime exists in production at 2026 Q2.**
|
||||
- **The Pi Zero 2W's 1 GB RAM is fine for WiFlow but tight for larger
|
||||
pose models.** A CM4/CM5 with 4 GB unlocks Hugging-Face-class models;
|
||||
whether the deployment needs that is a use-case question.
|
||||
|
||||
### 4.4 Primary references
|
||||
|
||||
- esp-tflite-micro: [github.com/espressif/esp-tflite-micro](https://github.com/espressif/esp-tflite-micro).
|
||||
- ESP32-S3 TFLite Micro practical guide: [zediot.com](https://zediot.com/blog/esp32-s3-tensorflow-lite-micro/).
|
||||
- WiFlow architecture (parameters/FLOPs): [arXiv:2602.08661](https://arxiv.org/html/2602.08661).
|
||||
- ESP32-S3 TinyML INT8 speedup: [zediot.com TinyML optimization](https://zediot.com/blog/esp32-s3-tinyml-optimization/).
|
||||
|
||||
---
|
||||
|
||||
## 5. QUIC for IoT backhaul
|
||||
|
||||
### 5.1 What's true in 2026
|
||||
|
||||
- **`quinn` + `rustls` is the production Rust QUIC stack.** Both target
|
||||
std Linux, both work fine on ARMv8 (Pi Zero 2W). `rustls` is
|
||||
FIPS-validatable via the AWS-LC backend.
|
||||
- **MQTT-over-QUIC is the emerging IoT pattern.** EMQX 5.x and NanoMQ
|
||||
both ship MQTT-over-QUIC; published benchmarks show comparable or
|
||||
better tail-latency than MQTT-over-TLS-over-TCP, especially under
|
||||
packet loss and mobile-network handoff conditions.
|
||||
- **For low-rate telemetry** (a few KB at minute granularity), the
|
||||
difference between QUIC and TLS-over-TCP is small in steady-state. The
|
||||
win is in connection-establishment cost (~1 RTT vs ~3 RTT) and in
|
||||
graceful behavior across IP changes.
|
||||
|
||||
### 5.2 What holds up
|
||||
|
||||
The proposal's choice of `quinn` for the Pi-to-cloud ring is sound and
|
||||
matches what EMQX, NanoMQ, and Microsoft (MsQuic) are converging on.
|
||||
`rustls` is a strong default.
|
||||
|
||||
### 5.3 What to reconsider
|
||||
|
||||
- **Heartbeat-only deployments don't need QUIC.** If the Pi wakes 2
|
||||
minutes/day to push aggregated features, an MQTT-over-TLS publish on
|
||||
port 8883 is one library, well-supported, and cheaper to operate.
|
||||
- **QUIC pays off when bidirectional or large-payload traffic is real.**
|
||||
Model updates, fleet sync, on-demand video — these are the cases
|
||||
where the 1-RTT handshake and connection-migration matter.
|
||||
- **Don't terminate QUIC inside the comms MCU.** ESP-IDF has no
|
||||
production QUIC stack; QUIC belongs on the Pi or gateway, not on the
|
||||
MCU.
|
||||
|
||||
### 5.4 Primary references
|
||||
|
||||
- quinn: [docs.rs/quinn](https://docs.rs/quinn).
|
||||
- MQTT-over-QUIC IIoT evaluation: [MDPI Sensors 21:5737](https://www.mdpi.com/1424-8220/21/17/5737).
|
||||
- EMQX MQTT trends: [emqx.com 2025 trends](https://www.emqx.com/en/blog/mqtt-trends-for-2025-and-beyond).
|
||||
|
||||
---
|
||||
|
||||
## 6. LoRa for sensor mesh fallback
|
||||
|
||||
### 6.1 What's true in 2026
|
||||
|
||||
- **SX1262** — Semtech's mainstream Gen-2 sub-GHz LoRa transceiver,
|
||||
+22 dBm TX, 4.2 mA RX. The default for low-rate, long-range battery
|
||||
applications. Mature ecosystem, low BOM cost, supported by `lora-phy`
|
||||
and most Meshtastic boards.
|
||||
- **LR1110** — adds GNSS scan + WiFi scan. Designed for asset-tracking
|
||||
workflows where the device opportunistically reports GNSS+WiFi
|
||||
fingerprints to a cloud-side resolver.
|
||||
- **LR1121** — Gen-3, sub-GHz + 2.4 GHz + S/L-band satellite. ~4.5 dB
|
||||
better Sub-GHz sensitivity vs SX1262. Cost premium and more system
|
||||
complexity.
|
||||
- **Duty cycles**: EU868 imposes 1% in most sub-bands and 0.1% in the
|
||||
863–865 MHz sub-band. US915 uses dwell-time (400 ms) instead of
|
||||
duty-cycle limits. Raw-LoRa peer-to-peer must still respect the
|
||||
regional regulatory constraint, even though LoRaWAN is not on the
|
||||
wire.
|
||||
|
||||
For a 20-byte heartbeat at SF7, BW 125 kHz, the airtime is ~40 ms. At
|
||||
the EU868 1% duty cycle, that's 36 s/hour available — more than 900
|
||||
heartbeats per hour theoretical max.
|
||||
|
||||
### 6.2 What holds up
|
||||
|
||||
SX1262 for fallback heartbeats is the correct, well-priced choice. The
|
||||
proposal's "bytes per minute" framing is well within EU868 1% and US915
|
||||
dwell-time budgets.
|
||||
|
||||
### 6.3 What to reconsider
|
||||
|
||||
- **LR1121 is not justified for fallback heartbeats.** The
|
||||
satellite/2.4 GHz capabilities are deployment-shape choices, not
|
||||
fallback-radio choices.
|
||||
- **Raw LoRa P2P, not LoRaWAN.** The proposal already implies P2P; this
|
||||
should be explicit. LoRaWAN gateways add infrastructure cost without
|
||||
improving fallback reliability, and they don't help direct
|
||||
node-to-node fallback recovery.
|
||||
- **LoRa cannot carry CSI features at any meaningful rate.** SF7 BW125
|
||||
raw rate is ~5.5 kbps; ADR-081 `rv_feature_state_t` at 5 Hz is 2.4
|
||||
kbps gross, 480 B/s, well within budget if compressed and gated.
|
||||
Raw ADR-018 frames at 100 KB/s/node are not LoRa-shaped.
|
||||
|
||||
### 6.4 Primary references
|
||||
|
||||
- Semtech SX1262 datasheet via DigiKey: [forum.digikey.com LoRa breakdown](https://forum.digikey.com/t/lora-hardware-breakdown-key-chips-and-modules-for-iot-applications/52243).
|
||||
- LR1121 / SX1262 / LR2021 comparison: [nicerf.com](https://www.nicerf.com/news/lr2021-vs-sx1262-vs-lr1121.html).
|
||||
- TTN duty cycle reference: [thethingsnetwork.org](https://www.thethingsnetwork.org/docs/lorawan/duty-cycle/).
|
||||
- TTN regional EU863-870: [thethingsnetwork.org regional](https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/eu868/).
|
||||
|
||||
---
|
||||
|
||||
## 7. Solar + Li-ion power-path for 350 mA bursty IoT loads
|
||||
|
||||
### 7.1 What's true in 2026
|
||||
|
||||
- **TI BQ24074** — small, simple, linear charger; dual input
|
||||
(DC + USB); has the input-voltage-limit feature that crudely
|
||||
approximates MPPT for small panels. Adafruit's "Universal" charger
|
||||
product is built on it. Low silicon cost, no inductors.
|
||||
- **TI BQ25798** — newer (2025-class) buck-boost charger with **true
|
||||
Voc-sampling MPPT**, dual-input, supports 1–4S Li-ion, 5 A capability,
|
||||
3.6–24 V input range. Adafruit launched a development module in May
|
||||
2025.
|
||||
- **Analog Devices LTC4015** — multi-chemistry, two-phase MPPT (15-min
|
||||
global sweep + 1-second local dither). High-cost, high-capability;
|
||||
overkill for sub-5 W panels.
|
||||
- **Silergy SPV1050** — purpose-built for sub-watt IoT solar (e.g.
|
||||
energy-harvesting sensors). Constant-voltage-ratio MPPT, 70 mA solar
|
||||
/ 100 mA USB charge limit. Best for *very small* (<1 W) panels and
|
||||
micro-energy budgets.
|
||||
|
||||
### 7.2 What holds up
|
||||
|
||||
For a 2 W panel and a node-average load that bursts to 350 mA, the
|
||||
BQ24074 (linear) is sufficient. The proposal's choice is fine.
|
||||
|
||||
### 7.3 What to reconsider
|
||||
|
||||
- **MPPT becomes attractive when panel power × variability is high.**
|
||||
At 2 W, the efficiency delta between linear-with-input-voltage-limit
|
||||
and true MPPT is on the order of 10–20% in cloudy conditions. For a
|
||||
4× harvest-to-load headroom, this is not the binding constraint.
|
||||
- **If the deployment ever scales to a 5–10 W panel** (e.g., to support
|
||||
a Pi that wakes more often than 2 minutes/day), BQ25798's MPPT pays
|
||||
off.
|
||||
- **A super-cap on the input rail** is cheap insurance against the Pi's
|
||||
~350 mA boot inrush; the proposal should consider one.
|
||||
|
||||
### 7.4 Primary references
|
||||
|
||||
- BQ25798 launch coverage (Adafruit, May 2025): [blog.adafruit.com](https://blog.adafruit.com/2025/05/15/eye-on-npi-ti-bq25798-i2c-controlled-1-to-4-cell-5-a-buck-boost-battery-charger-mppt-for-solar-panels-eyeonnpi-digikey-digikey-adafruit/).
|
||||
- BQ25798 datasheet: [ti.com](https://www.ti.com/lit/ds/symlink/bq25798.pdf).
|
||||
- BQ24074 product (Adafruit): [adafruit.com/product/4755](https://www.adafruit.com/product/4755).
|
||||
- SPV1050 application reference: [DFRobot wiki](https://wiki.dfrobot.com/dfr0579/).
|
||||
|
||||
---
|
||||
|
||||
## 8. Mesh routing alternatives to ESP-WIFI-MESH
|
||||
|
||||
### 8.1 What's true in 2026
|
||||
|
||||
- **ESP-WIFI-MESH** documents support up to ~1,000 nodes in 25 layers,
|
||||
with a recommended fan-out of 6/node (hardware AP-mode limit is 10).
|
||||
Espressif's own newer `esp-mesh-lite` is the lighter, IP-layer-routable
|
||||
alternative.
|
||||
- **Thread / OpenThread** — IPv6-native 802.15.4 mesh, self-healing,
|
||||
designed for 250+ node networks per partition. Strong scalability and
|
||||
security story. Hardware: ESP32-C6, ESP32-H2, Nordic nRF52840, Silicon
|
||||
Labs EFR32.
|
||||
- **Zigbee** — 802.15.4 like Thread, but with a much older application
|
||||
layer. Scales reasonably to ~100 nodes in practice, with congestion
|
||||
challenges in dense deployments.
|
||||
- **BLE Mesh** — managed flooding, optimized for sporadic traffic. Good
|
||||
for ~50 nodes; not the right shape for always-on infrastructure.
|
||||
|
||||
### 8.2 What holds up
|
||||
|
||||
For < 25-node deployments, ESP-WIFI-MESH (or `esp-mesh-lite`) is the
|
||||
direct continuation of today's RuView mesh and the proposal's choice is
|
||||
defensible.
|
||||
|
||||
### 8.3 What to reconsider
|
||||
|
||||
- **For 50–500 node deployments, Thread is the better fit.** It was
|
||||
designed for that scale; ESP-WIFI-MESH was not. Using Thread *for the
|
||||
control plane* (TIME_SYNC, ROLE_ASSIGN, CHANNEL_PLAN, HEALTH) while
|
||||
keeping ADR-018 CSI frames on WiFi is a viable hybrid.
|
||||
- **The comms MCU choice changes.** ESP-WIFI-MESH stays on ESP32-S3.
|
||||
Thread/Zigbee/BLE Mesh prefer ESP32-C6 (which has 802.15.4 + WiFi 6)
|
||||
or a separate radio. The proposal's two-S3 die choice forecloses on
|
||||
this hybrid; a one-S3 + one-C6 split is worth evaluating.
|
||||
- **Thread's IPv6-native routing pairs nicely with QUIC.** Both speak
|
||||
IP; ESP-WIFI-MESH does not (it uses its own L2-style routing and
|
||||
bridges IP).
|
||||
|
||||
### 8.4 Primary references
|
||||
|
||||
- ESP-WIFI-MESH overview: [docs.espressif.com](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/esp-wifi-mesh.html).
|
||||
- esp-mesh-lite: [github.com/espressif/esp-mesh-lite](https://github.com/espressif/esp-mesh-lite).
|
||||
- Silicon Labs benchmarking: [silabs.com mesh-performance](https://www.silabs.com/wireless/multiprotocol/mesh-performance).
|
||||
- Bluetooth/Thread/Zigbee comparison: [eetimes.com](https://www.eetimes.com/bluetooth-thread-zigbee-mesh-compared/).
|
||||
- Zigbee vs Matter-over-Thread (2026): [arXiv:2603.04221](https://arxiv.org/html/2603.04221v1).
|
||||
|
||||
---
|
||||
|
||||
## 9. Pi Zero 2W secure-boot reality
|
||||
|
||||
### 9.1 What's true in 2026
|
||||
|
||||
- **Raspberry Pi Foundation's official secure-boot path is Pi 4 / Pi 5
|
||||
/ CM4.** It uses the RPi-bootloader ROM, USB-rooted RSA chain, and
|
||||
the `usbboot` tooling. There is no equivalent on the Pi Zero 2W
|
||||
(BCM2710A1).
|
||||
- **Buildroot does support Pi Zero 2W** (April 2025 defconfig update
|
||||
uses the same ARM64 `bcm2711_defconfig` as the Pi 4).
|
||||
- **dm-verity + signed FIT image** is the realistic Pi-Zero-2W path:
|
||||
buildroot produces a read-only rootfs, dm-verity covers it with a
|
||||
signed Merkle tree, the boot partition has signed kernel/initramfs.
|
||||
This delivers integrity but not "secure boot" in the immutable-ROM
|
||||
sense.
|
||||
- **A/B partitions for OTA** is straightforward in buildroot.
|
||||
`swupdate` and `RAUC` are the well-known frameworks; both work on Pi
|
||||
Zero 2W.
|
||||
|
||||
### 9.2 What holds up
|
||||
|
||||
The proposal's "buildroot, not Raspberry Pi OS" instinct is correct.
|
||||
RPi OS does not support secure boot on any Pi.
|
||||
|
||||
### 9.3 What to reconsider
|
||||
|
||||
- **The "Pi 4 + buildroot is the strongest path" line is true but not a
|
||||
Pi Zero 2W story.** If true secure boot with an immutable ROM-rooted
|
||||
chain is required, the heavy-compute die should be a CM4 or Pi 5, not
|
||||
a Pi Zero 2W.
|
||||
- **For the proposal's deployment shape** (mostly-off Pi, infrequent
|
||||
wake-ups), dm-verity + signed FIT + A/B is probably enough threat
|
||||
cover and avoids the cost of a CM4. Document this as an explicit
|
||||
tradeoff, not as "the strongest path."
|
||||
- **`fwupd` is the package-manager-style update agent**; or a
|
||||
self-rolled "update-agent" binary signed by the project key. Either
|
||||
works; project-style fits with the homogeneous Rust toolchain better.
|
||||
|
||||
### 9.4 Primary references
|
||||
|
||||
- Raspberry Pi USB-boot secure-boot example: [github.com/raspberrypi/usbboot](https://github.com/raspberrypi/usbboot/blob/master/secure-boot-example/README.md).
|
||||
- Raspberry Pi forum on secure boot: [forums.raspberrypi.com 352061](https://forums.raspberrypi.com/viewtopic.php?t=352061).
|
||||
- Buildroot Pi Zero 2W defconfig (April 2025): [lists.buildroot.org](https://lists.buildroot.org/pipermail/buildroot/2025-April/776753.html).
|
||||
|
||||
---
|
||||
|
||||
## 10. Cross-cutting takeaways
|
||||
|
||||
A short list of items that affect more than one section:
|
||||
|
||||
1. **The biggest single risk in the proposal is the no_std CSI maturity
|
||||
gate.** If `esp-csi-rs` (or whatever replaces it under `esp-radio`)
|
||||
does not match `esp_wifi_set_csi_rx_cb` in capture quality and
|
||||
ISR-jitter, the sensor-MCU shape collapses back to "C ESP-IDF on the
|
||||
sensor MCU too" and the value of the split shrinks.
|
||||
2. **The cost story improves dramatically if the heavy-compute die is
|
||||
shared across nodes.** "One Pi per cluster of 6" is closer to today's
|
||||
$9-per-sensor BOM at the per-sensor edge while still adding the
|
||||
QUIC/ML/secure-boot story at the cluster level.
|
||||
3. **IEEE 802.11bf-2025's ratification** changes the regulatory and
|
||||
ecosystem landscape but does not change what off-the-shelf ESP32
|
||||
silicon can do today. RuView's pre-standard work (ADR-029, ADR-073,
|
||||
ADR-081) is well-aligned with the standard's direction; nothing in
|
||||
the proposal makes it more or less compatible.
|
||||
4. **The right "comms MCU" might be ESP32-C6 instead of a second S3.**
|
||||
C6 has 802.15.4 (Thread/Zigbee), WiFi 6, and BLE 5.4. For a
|
||||
deployment that scales beyond ~25 nodes, the Thread control plane is
|
||||
a meaningful upgrade.
|
||||
5. **Power gating the Pi is the load-bearing power decision.** Soft
|
||||
suspend leaks; hard FET cut does not. The proposal's instinct is
|
||||
right, but the supercap/transient story has to be designed in.
|
||||
|
||||
---
|
||||
|
||||
## 11. Items where no primary source was found
|
||||
|
||||
This section is required by the project conventions and lists each
|
||||
non-trivial claim where a primary source could not be located in this
|
||||
research pass:
|
||||
|
||||
- **Through-multiple-walls CSI pose accuracy at room scale.** Published
|
||||
papers focus on line-of-sight or single-wall environments. *Mark as
|
||||
conjecture for now.*
|
||||
- **WiFlow inference latency on Pi Zero 2W (Cortex-A53).** Estimated at
|
||||
50–100 ms; no measurement found. *Mark as conjecture; benchmark
|
||||
before claiming.*
|
||||
- **Espressif silicon roadmap for 802.11bf-aware MAC primitives.** No
|
||||
public announcement from Espressif as of 2026 Q2. *Mark as
|
||||
conjecture.*
|
||||
- **Pi Zero 2W gated cold-boot wake-up time under 5 s with the proposed
|
||||
buildroot image.** Mentioned in the proposal as a constraint, no
|
||||
measurement found. *Mark as benchmark target.*
|
||||
- **ESP-WIFI-MESH stable-state tested deployment beyond ~25 nodes.**
|
||||
Espressif documents 1,000-node theoretical ceilings but published
|
||||
third-party deployment data at scale is sparse. *Mark as conjecture
|
||||
pending field test.*
|
||||
|
||||
---
|
||||
|
||||
## 12. Source list
|
||||
|
||||
(Primary references are inlined per-section. This is the unique
|
||||
domains list for quick reuse.)
|
||||
|
||||
- IEEE Standards Association — `standards.ieee.org`
|
||||
- arXiv — `arxiv.org`
|
||||
- IEEE Xplore — `ieeexplore.ieee.org`
|
||||
- Espressif documentation — `docs.espressif.com`
|
||||
- Espressif GitHub — `github.com/espressif`
|
||||
- esp-rs project — `github.com/esp-rs`, `crates.io/crates/esp-csi-rs`,
|
||||
`docs.rs/esp-idf-hal`
|
||||
- Embassy project — `embassy.dev`
|
||||
- The Things Network — `thethingsnetwork.org`
|
||||
- Texas Instruments — `ti.com`
|
||||
- Adafruit — `adafruit.com`, `blog.adafruit.com`
|
||||
- Buildroot — `lists.buildroot.org`
|
||||
- Silicon Labs — `silabs.com`
|
||||
- DigiKey forum — `forum.digikey.com`
|
||||
- NIST — `nist.gov`
|
||||
- MDPI Sensors — `mdpi.com`
|
||||
- EMQ technical blog — `emqx.com`
|
||||
- Raspberry Pi forum / GitHub — `forums.raspberrypi.com`,
|
||||
`github.com/raspberrypi/usbboot`
|
||||
- nicerf comparison guide — `nicerf.com`
|
||||
- DFRobot wiki — `wiki.dfrobot.com`
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
- [WiFlow: A Lightweight WiFi-based Continuous Human Pose Estimation Network](https://arxiv.org/html/2602.08661)
|
||||
- [Towards Robust and Realistic Human Pose Estimation via WiFi Signals (DT-Pose)](https://arxiv.org/abs/2501.09411)
|
||||
- [Graph-based 3D Human Pose Estimation using WiFi Signals (GraphPose-Fi)](https://arxiv.org/abs/2511.19105)
|
||||
- [IEEE 802.11bf-2025](https://standards.ieee.org/ieee/802.11bf/11574/)
|
||||
- [An Overview on IEEE 802.11bf: WLAN Sensing](https://arxiv.org/pdf/2207.04859)
|
||||
- [IEEE 802.11bf NIST page](https://www.nist.gov/publications/ieee-80211bf-enabling-widespread-adoption-wi-fi-sensing)
|
||||
- [ISAC-Fi: Enabling Full-Fledged Monostatic Sensing Over Wi-Fi](https://arxiv.org/abs/2408.09851)
|
||||
- [Multistatic ISAC Macro–Micro Cooperation](https://www.mdpi.com/1424-8220/24/8/2498)
|
||||
- [esp-rs/esp-hal releases](https://github.com/esp-rs/esp-hal/releases)
|
||||
- [esp-idf-svc CHANGELOG](https://github.com/esp-rs/esp-idf-svc/blob/master/CHANGELOG.md)
|
||||
- [esp-idf-svc Embassy ISR-safety issue #342](https://github.com/esp-rs/esp-idf-svc/issues/342)
|
||||
- [esp-csi-rs crate](https://crates.io/crates/esp-csi-rs)
|
||||
- [Embassy Book](https://embassy.dev/book/)
|
||||
- [esp-tflite-micro](https://github.com/espressif/esp-tflite-micro)
|
||||
- [ESP32-S3 TFLite Micro practical guide](https://zediot.com/blog/esp32-s3-tensorflow-lite-micro/)
|
||||
- [ESP32-S3 TinyML Optimization](https://zediot.com/blog/esp32-s3-tinyml-optimization/)
|
||||
- [quinn QUIC](https://docs.rs/quinn)
|
||||
- [MQTT-over-QUIC IIoT evaluation (MDPI)](https://www.mdpi.com/1424-8220/21/17/5737)
|
||||
- [MQTT trends for 2025 (EMQ)](https://www.emqx.com/en/blog/mqtt-trends-for-2025-and-beyond)
|
||||
- [LoRa SX1262 / LR1121 / LR2021 comparison](https://www.nicerf.com/news/lr2021-vs-sx1262-vs-lr1121.html)
|
||||
- [LoRa hardware breakdown (DigiKey)](https://forum.digikey.com/t/lora-hardware-breakdown-key-chips-and-modules-for-iot-applications/52243)
|
||||
- [LoRaWAN duty cycle (TTN)](https://www.thethingsnetwork.org/docs/lorawan/duty-cycle/)
|
||||
- [LoRaWAN regional EU868 (TTN)](https://www.thethingsnetwork.org/docs/lorawan/regional-parameters/eu868/)
|
||||
- [BQ25798 launch coverage (Adafruit/DigiKey)](https://blog.adafruit.com/2025/05/15/eye-on-npi-ti-bq25798-i2c-controlled-1-to-4-cell-5-a-buck-boost-battery-charger-mppt-for-solar-panels-eyeonnpi-digikey-digikey-adafruit/)
|
||||
- [BQ25798 datasheet](https://www.ti.com/lit/ds/symlink/bq25798.pdf)
|
||||
- [BQ24074 product page](https://www.adafruit.com/product/4755)
|
||||
- [SPV1050 reference](https://wiki.dfrobot.com/dfr0579/)
|
||||
- [ESP-WIFI-MESH guide](https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/esp-wifi-mesh.html)
|
||||
- [esp-mesh-lite](https://github.com/espressif/esp-mesh-lite)
|
||||
- [Silicon Labs mesh benchmarking](https://www.silabs.com/wireless/multiprotocol/mesh-performance)
|
||||
- [Bluetooth/Thread/Zigbee comparison (EE Times)](https://www.eetimes.com/bluetooth-thread-zigbee-mesh-compared/)
|
||||
- [Zigbee vs Matter-over-Thread (arXiv 2603.04221)](https://arxiv.org/html/2603.04221v1)
|
||||
- [Raspberry Pi USB-boot secure-boot example](https://github.com/raspberrypi/usbboot/blob/master/secure-boot-example/README.md)
|
||||
- [Raspberry Pi forum: secure boot](https://forums.raspberrypi.com/viewtopic.php?t=352061)
|
||||
- [Buildroot Pi Zero 2 W defconfig (April 2025)](https://lists.buildroot.org/pipermail/buildroot/2025-April/776753.html)
|
||||
- [Nexmon CSI](https://github.com/seemoo-lab/nexmon_csi)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Date**: 2026-03-03
|
||||
**Auditor**: Security Auditor Agent (Claude Opus 4.6)
|
||||
**Scope**: All 29 `.rs` files in `v2/crates/wifi-densepose-wasm-edge/src/`
|
||||
**Scope**: All 29 `.rs` files in `rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/`
|
||||
**Crate version**: 0.3.0
|
||||
**Target**: `wasm32-unknown-unknown` (ESP32-S3 WASM3 interpreter)
|
||||
|
||||
|
||||
@@ -909,7 +909,7 @@ For users with the Rust toolchain, the `wifi-densepose-train` crate
|
||||
provides the full training pipeline with RuVector integration:
|
||||
|
||||
```bash
|
||||
cd v2
|
||||
cd rust-port/wifi-densepose-rs
|
||||
cargo run -p wifi-densepose-train -- \
|
||||
--data pretrain-vectors.rvf \
|
||||
--epochs 50 \
|
||||
|
||||
+2
-142
@@ -103,23 +103,9 @@ Example: `docker run -e CSI_SOURCE=esp32 -p 3000:3000 -p 5005:5005/udp ruvnet/wi
|
||||
|
||||
### From Source (Rust)
|
||||
|
||||
On Debian/Ubuntu-based Linux systems, install the native desktop prerequisites before the first Rust release build:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y \
|
||||
build-essential pkg-config \
|
||||
libglib2.0-dev libgtk-3-dev \
|
||||
libsoup-3.0-dev \
|
||||
libjavascriptcoregtk-4.1-dev \
|
||||
libwebkit2gtk-4.1-dev
|
||||
```
|
||||
|
||||
This prepares the native GTK/WebKit dependencies used by the desktop/Tauri crates in this workspace.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ruvnet/RuView.git
|
||||
cd RuView/v2
|
||||
cd RuView/rust-port/wifi-densepose-rs
|
||||
|
||||
# Build
|
||||
cargo build --release
|
||||
@@ -279,7 +265,7 @@ Uses CoreWLAN via a Swift helper binary. macOS Sonoma 14.4+ redacts real BSSIDs;
|
||||
|
||||
```bash
|
||||
# Compile the Swift helper (once)
|
||||
swiftc -O archive/v1/src/sensing/mac_wifi.swift -o mac_wifi
|
||||
swiftc -O v1/src/sensing/mac_wifi.swift -o mac_wifi
|
||||
|
||||
# Run natively
|
||||
./target/release/sensing-server --source macos --http-port 3000 --ws-port 3001 --tick-ms 500
|
||||
@@ -550,110 +536,6 @@ Both UIs update in real-time via WebSocket and auto-detect the sensing server on
|
||||
|
||||
---
|
||||
|
||||
## Dense Point Cloud (Camera + WiFi CSI Fusion)
|
||||
|
||||
RuView can generate real-time 3D point clouds by fusing camera depth estimation with WiFi CSI spatial sensing. This creates a spatial model of the environment that updates in real-time.
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Build the pointcloud binary
|
||||
cd v2
|
||||
cargo build --release -p wifi-densepose-pointcloud
|
||||
|
||||
# Start the server (auto-detects camera + CSI). Loopback-only by default.
|
||||
./target/release/ruview-pointcloud serve --bind 127.0.0.1:9880
|
||||
```
|
||||
|
||||
Open `http://localhost:9880` for the interactive Three.js 3D viewer.
|
||||
|
||||
> **Security note.** The server exposes live camera, skeleton, vitals, and occupancy over HTTP. The `--bind` flag defaults to `127.0.0.1:9880` (loopback-only). Exposing on `0.0.0.0` or a LAN IP is opt-in — the server logs a warning when it does, but there is no auth/TLS layer. Put a reverse proxy in front if you need remote access.
|
||||
|
||||
> **Brain URL.** Observations are POSTed to `http://127.0.0.1:9876` by default. Override via the `RUVIEW_BRAIN_URL` environment variable or the `--brain <url>` flag on `serve` / `train`.
|
||||
|
||||
### Sensors
|
||||
|
||||
| Sensor | Auto-detected | Data |
|
||||
|--------|--------------|------|
|
||||
| Camera (`/dev/video0`) | Yes (Linux UVC) | RGB frames → MiDaS depth → 3D points |
|
||||
| ESP32 CSI (UDP:3333) | Yes (if provisioned) | ADR-018 binary → occupancy + pose + vitals |
|
||||
| MiDaS depth server (port 9885) | Optional | GPU-accelerated neural depth estimation |
|
||||
|
||||
### Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `ruview-pointcloud serve --bind 127.0.0.1:9880` | Start HTTP server + Three.js viewer (loopback-only by default) |
|
||||
| `ruview-pointcloud demo` | Generate synthetic point cloud (no hardware needed) |
|
||||
| `ruview-pointcloud capture --output room.ply` | Capture single frame to PLY file |
|
||||
| `ruview-pointcloud cameras` | List available cameras |
|
||||
| `ruview-pointcloud train --data-dir ./data [--brain URL]` | Depth calibration + occupancy training (writes under canonicalized `data-dir`; refuses `..` traversal) |
|
||||
| `ruview-pointcloud csi-test --count 100` | Send test CSI frames (no ESP32 needed) |
|
||||
| `ruview-pointcloud fingerprint <name> [--seconds 5]` | Record a named CSI room fingerprint for later matching |
|
||||
|
||||
### Pipeline Components
|
||||
|
||||
1. **ADR-018 Parser** — Decodes ESP32 CSI binary frames from UDP (magic `0xC5110001` raw CSI and `0xC5110006` feature state), extracts I/Q subcarrier amplitudes and phases. Lives in `parser.rs`; unit-tested against hand-rolled test vectors.
|
||||
2. **Pose (stub)** — 17 COCO keypoint *layout* generated by `heuristic_pose_from_amplitude` from CSI amplitude energy. This is **not** the trained WiFlow model — it is a placeholder so the viewer has a skeleton to render. Wiring to real Candle/ONNX inference from the `wifi-densepose-nn` crate is a planned follow-up.
|
||||
3. **Vital Signs** — Breathing rate from CSI phase analysis (peak counting on stable subcarrier)
|
||||
4. **Motion Detection** — CSI amplitude variance over 20 frames, triggers adaptive capture
|
||||
5. **RF Tomography** — Backprojection from per-node RSSI to 8×8×4 occupancy grid
|
||||
6. **Camera Depth** — MiDaS monocular depth (GPU) with luminance+edge fallback
|
||||
7. **Sensor Fusion** — Voxel-grid merging of camera depth + CSI occupancy
|
||||
8. **Brain Bridge** — Stores spatial observations in the ruOS brain every 60 seconds
|
||||
|
||||
### API Endpoints
|
||||
|
||||
| Endpoint | Method | Returns |
|
||||
|----------|--------|---------|
|
||||
| `/health` | GET | `{"status": "ok"}` |
|
||||
| `/api/status` | GET | Camera, CSI, pipeline state, vitals, motion |
|
||||
| `/api/cloud` | GET | Point cloud (up to 1000 points) + pipeline data |
|
||||
| `/api/splats` | GET | Gaussian splats for Three.js rendering |
|
||||
| `/` | GET | Interactive Three.js 3D viewer |
|
||||
|
||||
### Training
|
||||
|
||||
The training pipeline calibrates depth estimation and occupancy detection:
|
||||
|
||||
```bash
|
||||
ruview-pointcloud train --data-dir ~/.local/share/ruview/training --brain http://127.0.0.1:9876
|
||||
```
|
||||
|
||||
This captures frames, runs depth calibration (grid search over scale/offset/gamma), trains occupancy thresholds, exports DPO preference pairs, and submits results to the ruOS brain.
|
||||
|
||||
### Output Formats
|
||||
|
||||
- **PLY** — Standard 3D point cloud (ASCII, with RGB color)
|
||||
- **Gaussian Splats** — JSON format for Three.js rendering
|
||||
- **Brain Memories** — Spatial observations stored as `spatial-observation`, `spatial-motion`, `spatial-vitals`
|
||||
|
||||
### Deep Room Scan
|
||||
|
||||
Capture a high-quality 3D model of the room:
|
||||
|
||||
```bash
|
||||
# Stop the live server first (frees the camera)
|
||||
# Then capture 20 frames and process with MiDaS
|
||||
ruview-pointcloud capture --frames 20 --output room_model.ply
|
||||
```
|
||||
|
||||
Result: 40,000+ voxels at 5cm resolution, 12,000+ Gaussian splats.
|
||||
|
||||
### ESP32 Provisioning for CSI
|
||||
|
||||
To send CSI data to the pointcloud server:
|
||||
|
||||
```bash
|
||||
python3 firmware/esp32-csi-node/provision.py \
|
||||
--port /dev/ttyACM0 \
|
||||
--ssid "YourWiFi" --password "YourPassword" \
|
||||
--target-ip 192.168.1.123 --target-port 3333 \
|
||||
--node-id 1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Vital Sign Detection
|
||||
|
||||
The system extracts breathing rate and heart rate from CSI signal fluctuations using FFT peak detection.
|
||||
@@ -1700,28 +1582,6 @@ rustup update stable
|
||||
rustc --version
|
||||
```
|
||||
|
||||
### Build: Linux native desktop prerequisites
|
||||
|
||||
If you are compiling the Rust workspace on a Debian/Ubuntu-based Linux system, install the native desktop development packages first:
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install -y \
|
||||
build-essential pkg-config \
|
||||
libglib2.0-dev libgtk-3-dev \
|
||||
libsoup-3.0-dev \
|
||||
libjavascriptcoregtk-4.1-dev \
|
||||
libwebkit2gtk-4.1-dev
|
||||
```
|
||||
|
||||
Then rerun:
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
This is the same Linux pre-step referenced in the Rust source build section and covers the common GTK/WebKit `pkg-config` requirements used by the desktop build.
|
||||
|
||||
### Windows: RSSI mode shows no data
|
||||
|
||||
Run the terminal as Administrator (required for `netsh wlan` access). Verified working on Windows 10 and 11 with Intel AX201 and Intel BE201 adapters.
|
||||
|
||||
@@ -92,7 +92,7 @@ sudo apt-get install -y build-essential pkg-config libssl-dev
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/ruvnet/wifi-densepose.git
|
||||
cd wifi-densepose/v2
|
||||
cd wifi-densepose/rust-port/wifi-densepose-rs
|
||||
|
||||
# Build the wifi-mat crate
|
||||
cargo build --release --package wifi-densepose-mat
|
||||
|
||||
@@ -159,7 +159,7 @@ The happiness scoring algorithm also exists as a WASM module for on-device execu
|
||||
|
||||
```bash
|
||||
# Build the happiness scorer WASM
|
||||
cd v2/crates/wifi-densepose-wasm-edge
|
||||
cd rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge
|
||||
cargo build --bin ghost_hunter --target wasm32-unknown-unknown --release --no-default-features
|
||||
|
||||
# Output: target/wasm32-unknown-unknown/release/ghost_hunter.wasm (5.7 KB)
|
||||
@@ -201,6 +201,6 @@ This system is designed to be privacy-preserving by construction:
|
||||
|
||||
- [ADR-065](../../docs/adr/ADR-065-happiness-scoring-seed-bridge.md) — Happiness scoring pipeline architecture
|
||||
- [ADR-066](../../docs/adr/ADR-066-esp32-swarm-seed-coordinator.md) — ESP32 swarm with Seed coordinator
|
||||
- [exo_happiness_score.rs](../../v2/crates/wifi-densepose-wasm-edge/src/exo_happiness_score.rs) — WASM edge module (Rust)
|
||||
- [exo_happiness_score.rs](../../rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm-edge/src/exo_happiness_score.rs) — WASM edge module (Rust)
|
||||
- [swarm_bridge.c](../../firmware/esp32-csi-node/main/swarm_bridge.c) — ESP32 firmware swarm bridge
|
||||
- [ruview_live.py](../ruview_live.py) — RuView Live dashboard with `--mode happiness`
|
||||
|
||||
@@ -4,18 +4,13 @@ set(SRCS
|
||||
"wasm_runtime.c" "wasm_upload.c" "rvf_parser.c"
|
||||
"mmwave_sensor.c"
|
||||
"swarm_bridge.c"
|
||||
# ADR-081 — adaptive CSI mesh firmware kernel
|
||||
"rv_radio_ops_esp32.c"
|
||||
"rv_feature_state.c"
|
||||
"rv_mesh.c"
|
||||
"adaptive_controller.c"
|
||||
)
|
||||
|
||||
set(REQUIRES "")
|
||||
|
||||
# ADR-061: Mock CSI generator for QEMU testing + ADR-081 mock radio binding
|
||||
# ADR-061: Mock CSI generator for QEMU testing
|
||||
if(CONFIG_CSI_MOCK_ENABLED)
|
||||
list(APPEND SRCS "mock_csi.c" "rv_radio_ops_mock.c")
|
||||
list(APPEND SRCS "mock_csi.c")
|
||||
endif()
|
||||
|
||||
# ADR-045: AMOLED display support (compile-time optional)
|
||||
|
||||
@@ -87,89 +87,6 @@ menu "Edge Intelligence (ADR-039)"
|
||||
|
||||
endmenu
|
||||
|
||||
menu "Adaptive Controller (ADR-081)"
|
||||
|
||||
config ADAPTIVE_FAST_LOOP_MS
|
||||
int "Fast loop period (ms)"
|
||||
default 200
|
||||
range 50 2000
|
||||
help
|
||||
Period of the fast control loop. The fast loop reads radio
|
||||
health and edge-derived motion/presence/anomaly scores and
|
||||
updates the active capture profile. Default 200 ms matches
|
||||
the ADR-081 spec.
|
||||
|
||||
config ADAPTIVE_MEDIUM_LOOP_MS
|
||||
int "Medium loop period (ms)"
|
||||
default 1000
|
||||
range 200 30000
|
||||
help
|
||||
Period of the medium control loop. The medium loop is where
|
||||
channel selection and role transitions happen (when
|
||||
enable_channel_switch / enable_role_change are on).
|
||||
|
||||
config ADAPTIVE_SLOW_LOOP_MS
|
||||
int "Slow loop period (ms)"
|
||||
default 30000
|
||||
range 1000 300000
|
||||
help
|
||||
Period of the slow control loop. The slow loop publishes
|
||||
HEALTH messages and may request CALIBRATION_START on
|
||||
sustained drift.
|
||||
|
||||
config ADAPTIVE_AGGRESSIVE
|
||||
bool "Aggressive adaptation"
|
||||
default n
|
||||
help
|
||||
When enabled, the controller reacts to motion / anomaly
|
||||
sooner and uses a tighter cadence in SENSE_ACTIVE. Default
|
||||
off matches today's conservative behavior.
|
||||
|
||||
config ADAPTIVE_ENABLE_CHANNEL_SWITCH
|
||||
bool "Allow controller to change WiFi channel"
|
||||
default n
|
||||
help
|
||||
When disabled, the controller never calls set_channel() —
|
||||
channel hopping (ADR-029) and channel override (ADR-060)
|
||||
remain in charge. Enable only after Phase 3 follow-up
|
||||
work has wired the channel-plan mesh message.
|
||||
|
||||
config ADAPTIVE_ENABLE_ROLE_CHANGE
|
||||
bool "Allow controller to change mesh role"
|
||||
default n
|
||||
help
|
||||
When disabled, the controller never advertises a different
|
||||
role to the swarm bridge. Enable after the mesh-plane
|
||||
ROLE_ASSIGN protocol is in place.
|
||||
|
||||
config ADAPTIVE_MOTION_THRESH_PERMIL
|
||||
int "Motion threshold (per-mille)"
|
||||
default 200
|
||||
range 1 1000
|
||||
help
|
||||
Motion score above which the controller transitions to
|
||||
SENSE_ACTIVE and selects RV_PROFILE_FAST_MOTION. Expressed
|
||||
in per-mille (200 = 0.20).
|
||||
|
||||
config ADAPTIVE_ANOMALY_THRESH_PERMIL
|
||||
int "Anomaly threshold (per-mille)"
|
||||
default 600
|
||||
range 1 1000
|
||||
help
|
||||
Anomaly score above which the controller transitions to
|
||||
ALERT. Per-mille (600 = 0.60).
|
||||
|
||||
config ADAPTIVE_MIN_PKT_YIELD
|
||||
int "Minimum packet yield before DEGRADED (pps)"
|
||||
default 5
|
||||
range 0 100
|
||||
help
|
||||
CSI callback rate (per second) below which the controller
|
||||
falls back to DEGRADED mode and pins the radio to
|
||||
RV_PROFILE_PASSIVE_LOW_RATE. 0 disables the degraded gate.
|
||||
|
||||
endmenu
|
||||
|
||||
menu "AMOLED Display (ADR-045)"
|
||||
|
||||
config DISPLAY_ENABLE
|
||||
|
||||
@@ -1,414 +0,0 @@
|
||||
/**
|
||||
* @file adaptive_controller.c
|
||||
* @brief ADR-081 Layer 2 — Adaptive sensing controller implementation.
|
||||
*
|
||||
* The decide() function is pure and unit-testable; the FreeRTOS plumbing
|
||||
* around it (timers, observation snapshot) is the only ESP-IDF surface.
|
||||
*
|
||||
* Default policy is conservative: it will not change channels unless
|
||||
* enable_channel_switch is true, and it will not change roles unless
|
||||
* enable_role_change is true. With both off the controller still tracks
|
||||
* state and feeds the mesh plane's HEALTH messages, so it is safe to
|
||||
* enable in production before the mesh plane is fully in place.
|
||||
*/
|
||||
|
||||
#include "adaptive_controller.h"
|
||||
#include "rv_radio_ops.h"
|
||||
#include "rv_feature_state.h"
|
||||
#include "rv_mesh.h"
|
||||
#include "edge_processing.h"
|
||||
#include "stream_sender.h"
|
||||
#include "csi_collector.h"
|
||||
|
||||
#include <string.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "freertos/timers.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
static const char *TAG = "adaptive_ctrl";
|
||||
|
||||
/* ---- Module state ---- */
|
||||
|
||||
static bool s_inited = false;
|
||||
static adapt_config_t s_cfg;
|
||||
static adapt_state_t s_state = ADAPT_STATE_BOOT;
|
||||
static adapt_observation_t s_last_obs;
|
||||
static bool s_obs_valid = false;
|
||||
static portMUX_TYPE s_obs_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
static TimerHandle_t s_fast_timer = NULL;
|
||||
static TimerHandle_t s_medium_timer = NULL;
|
||||
static TimerHandle_t s_slow_timer = NULL;
|
||||
|
||||
/* Forward decl: defined below, called from fast_loop_cb. */
|
||||
static void emit_feature_state(void);
|
||||
|
||||
/* ---- Defaults ---- */
|
||||
|
||||
#ifndef CONFIG_ADAPTIVE_FAST_LOOP_MS
|
||||
#define CONFIG_ADAPTIVE_FAST_LOOP_MS 200
|
||||
#endif
|
||||
#ifndef CONFIG_ADAPTIVE_MEDIUM_LOOP_MS
|
||||
#define CONFIG_ADAPTIVE_MEDIUM_LOOP_MS 1000
|
||||
#endif
|
||||
#ifndef CONFIG_ADAPTIVE_SLOW_LOOP_MS
|
||||
#define CONFIG_ADAPTIVE_SLOW_LOOP_MS 30000
|
||||
#endif
|
||||
#ifndef CONFIG_ADAPTIVE_MIN_PKT_YIELD
|
||||
#define CONFIG_ADAPTIVE_MIN_PKT_YIELD 5
|
||||
#endif
|
||||
/* Defaults expressed as integer permille so Kconfig can carry them. */
|
||||
#ifndef CONFIG_ADAPTIVE_MOTION_THRESH_PERMIL
|
||||
#define CONFIG_ADAPTIVE_MOTION_THRESH_PERMIL 200 /* 0.20 */
|
||||
#endif
|
||||
#ifndef CONFIG_ADAPTIVE_ANOMALY_THRESH_PERMIL
|
||||
#define CONFIG_ADAPTIVE_ANOMALY_THRESH_PERMIL 600 /* 0.60 */
|
||||
#endif
|
||||
|
||||
static void apply_defaults(adapt_config_t *cfg)
|
||||
{
|
||||
cfg->fast_loop_ms = CONFIG_ADAPTIVE_FAST_LOOP_MS;
|
||||
cfg->medium_loop_ms = CONFIG_ADAPTIVE_MEDIUM_LOOP_MS;
|
||||
cfg->slow_loop_ms = CONFIG_ADAPTIVE_SLOW_LOOP_MS;
|
||||
#ifdef CONFIG_ADAPTIVE_AGGRESSIVE
|
||||
cfg->aggressive = true;
|
||||
#else
|
||||
cfg->aggressive = false;
|
||||
#endif
|
||||
#ifdef CONFIG_ADAPTIVE_ENABLE_CHANNEL_SWITCH
|
||||
cfg->enable_channel_switch = true;
|
||||
#else
|
||||
cfg->enable_channel_switch = false;
|
||||
#endif
|
||||
#ifdef CONFIG_ADAPTIVE_ENABLE_ROLE_CHANGE
|
||||
cfg->enable_role_change = true;
|
||||
#else
|
||||
cfg->enable_role_change = false;
|
||||
#endif
|
||||
cfg->motion_threshold = (float)CONFIG_ADAPTIVE_MOTION_THRESH_PERMIL / 1000.0f;
|
||||
cfg->anomaly_threshold = (float)CONFIG_ADAPTIVE_ANOMALY_THRESH_PERMIL / 1000.0f;
|
||||
cfg->min_pkt_yield = CONFIG_ADAPTIVE_MIN_PKT_YIELD;
|
||||
}
|
||||
|
||||
/* Pure decision policy lives in its own file so it can link under
|
||||
* host unit tests without FreeRTOS. It is part of this translation
|
||||
* unit via #include to preserve a single object at build time. */
|
||||
#include "adaptive_controller_decide.c"
|
||||
|
||||
/* ---- Observation collection ---- */
|
||||
|
||||
static void collect_observation(adapt_observation_t *out)
|
||||
{
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
/* Radio health from the active binding. */
|
||||
const rv_radio_ops_t *ops = rv_radio_ops_get();
|
||||
if (ops != NULL && ops->get_health != NULL) {
|
||||
rv_radio_health_t h;
|
||||
if (ops->get_health(&h) == ESP_OK) {
|
||||
out->pkt_yield_per_sec = h.pkt_yield_per_sec;
|
||||
out->send_fail_count = h.send_fail_count;
|
||||
out->rssi_median_dbm = h.rssi_median_dbm;
|
||||
out->noise_floor_dbm = h.noise_floor_dbm;
|
||||
}
|
||||
}
|
||||
|
||||
/* Edge-derived state. The ADR-039 vitals packet exposes presence_score
|
||||
* and motion_energy directly; we treat motion_energy as a proxy for
|
||||
* motion_score by clamping to [0,1]. anomaly_score and node_coherence
|
||||
* are not yet emitted by edge_processing — placeholder until Layer 4
|
||||
* extraction lands. */
|
||||
edge_vitals_pkt_t vitals;
|
||||
if (edge_get_vitals(&vitals)) {
|
||||
out->presence_score = vitals.presence_score;
|
||||
float m = vitals.motion_energy;
|
||||
if (m < 0.0f) m = 0.0f;
|
||||
if (m > 1.0f) m = 1.0f;
|
||||
out->motion_score = m;
|
||||
}
|
||||
out->anomaly_score = 0.0f;
|
||||
out->node_coherence = 1.0f;
|
||||
}
|
||||
|
||||
/* ---- Decision application ---- */
|
||||
|
||||
/* ADR-081 L3: epoch monotonically advances per mesh session. Seeded at
|
||||
* init; every major state transition or role change bumps it so
|
||||
* receivers can order events. */
|
||||
static uint32_t s_mesh_epoch = 1;
|
||||
|
||||
/* ADR-081 L3: current node role. Updated by ROLE_ASSIGN receipt (future
|
||||
* mesh-plane RX path) or forced by tests. Default Observer. */
|
||||
static uint8_t s_role = RV_ROLE_OBSERVER;
|
||||
|
||||
/* 8-byte node id. Upper 7 bytes are zero by default; byte 0 is the
|
||||
* legacy CSI node id for compatibility with the ADR-018 header. */
|
||||
static void node_id_bytes(uint8_t out[8])
|
||||
{
|
||||
memset(out, 0, 8);
|
||||
out[0] = csi_collector_get_node_id();
|
||||
}
|
||||
|
||||
static void apply_decision(const adapt_decision_t *dec)
|
||||
{
|
||||
const rv_radio_ops_t *ops = rv_radio_ops_get();
|
||||
adapt_state_t prev = s_state;
|
||||
|
||||
if (dec->change_state) {
|
||||
ESP_LOGI(TAG, "state %u → %u",
|
||||
(unsigned)s_state, (unsigned)dec->new_state);
|
||||
s_state = (adapt_state_t)dec->new_state;
|
||||
|
||||
/* ADR-081 L3: on transition to ALERT, emit ANOMALY_ALERT on the
|
||||
* mesh plane. On any role-relevant transition, bump the epoch. */
|
||||
if (s_state == ADAPT_STATE_ALERT && prev != ADAPT_STATE_ALERT) {
|
||||
uint8_t nid[8];
|
||||
node_id_bytes(nid);
|
||||
adapt_observation_t obs;
|
||||
float motion = 0.0f, anomaly = 0.0f;
|
||||
portENTER_CRITICAL(&s_obs_lock);
|
||||
if (s_obs_valid) { obs = s_last_obs; motion = obs.motion_score;
|
||||
anomaly = obs.anomaly_score; }
|
||||
portEXIT_CRITICAL(&s_obs_lock);
|
||||
uint8_t severity = (uint8_t)(anomaly * 255.0f);
|
||||
rv_mesh_send_anomaly(s_role, s_mesh_epoch, nid,
|
||||
RV_ANOMALY_COHERENCE_LOSS, severity,
|
||||
anomaly, motion);
|
||||
}
|
||||
if (s_state == ADAPT_STATE_DEGRADED && prev != ADAPT_STATE_DEGRADED) {
|
||||
uint8_t nid[8];
|
||||
node_id_bytes(nid);
|
||||
rv_mesh_send_anomaly(s_role, s_mesh_epoch, nid,
|
||||
RV_ANOMALY_PKT_YIELD_COLLAPSE,
|
||||
200, 1.0f, 0.0f);
|
||||
}
|
||||
s_mesh_epoch++;
|
||||
}
|
||||
|
||||
if (dec->change_profile && ops != NULL && ops->set_capture_profile != NULL) {
|
||||
ops->set_capture_profile(dec->new_profile);
|
||||
}
|
||||
|
||||
if (dec->change_channel && s_cfg.enable_channel_switch &&
|
||||
ops != NULL && ops->set_channel != NULL) {
|
||||
ops->set_channel(dec->new_channel, 20);
|
||||
}
|
||||
|
||||
/* suggested_vital_interval_ms: the controller publishes a hint; the
|
||||
* edge pipeline picks it up via edge_processing on its next emit. We
|
||||
* don't yet have edge_set_vital_interval(); recorded for Phase 3. */
|
||||
(void)dec->request_calibration;
|
||||
}
|
||||
|
||||
/* ---- Loop callbacks ---- */
|
||||
|
||||
static void fast_loop_cb(TimerHandle_t t)
|
||||
{
|
||||
(void)t;
|
||||
adapt_observation_t obs;
|
||||
collect_observation(&obs);
|
||||
|
||||
portENTER_CRITICAL(&s_obs_lock);
|
||||
s_last_obs = obs;
|
||||
s_obs_valid = true;
|
||||
portEXIT_CRITICAL(&s_obs_lock);
|
||||
|
||||
adapt_decision_t dec;
|
||||
adaptive_controller_decide(&s_cfg, s_state, &obs, &dec);
|
||||
apply_decision(&dec);
|
||||
|
||||
/* ADR-081 Layer 4/5: emit compact feature state on every fast tick
|
||||
* (default 200 ms → 5 Hz, within the 1–10 Hz spec). Replaces raw
|
||||
* ADR-018 CSI as the default upstream; raw remains available as a
|
||||
* debug stream gated by the channel plan. */
|
||||
emit_feature_state();
|
||||
}
|
||||
|
||||
static void medium_loop_cb(TimerHandle_t t)
|
||||
{
|
||||
(void)t;
|
||||
/* Phase 3 stub: when enable_channel_switch is on, choose a channel
|
||||
* based on RSSI/noise/yield. Today, log the snapshot so operators can
|
||||
* see the controller is running. */
|
||||
adapt_observation_t obs;
|
||||
portENTER_CRITICAL(&s_obs_lock);
|
||||
obs = s_last_obs;
|
||||
portEXIT_CRITICAL(&s_obs_lock);
|
||||
|
||||
if (s_obs_valid) {
|
||||
ESP_LOGI(TAG, "medium tick: state=%u yield=%upps motion=%.2f presence=%.2f rssi=%d",
|
||||
(unsigned)s_state,
|
||||
(unsigned)obs.pkt_yield_per_sec,
|
||||
(double)obs.motion_score,
|
||||
(double)obs.presence_score,
|
||||
(int)obs.rssi_median_dbm);
|
||||
}
|
||||
}
|
||||
|
||||
/* ADR-081 Layer 4: emit one rv_feature_state_t packet onto the wire.
|
||||
*
|
||||
* Pulls from the latest observation + latest vitals + the active capture
|
||||
* profile. Send is best-effort — stream_sender will report its own
|
||||
* failures; we don't re-queue. At 5 Hz default cadence this is 300 B/s
|
||||
* per node, vs. ~100 KB/s for raw ADR-018 CSI. */
|
||||
static uint16_t s_feature_state_seq = 0;
|
||||
|
||||
static void emit_feature_state(void)
|
||||
{
|
||||
rv_feature_state_t pkt;
|
||||
memset(&pkt, 0, sizeof(pkt));
|
||||
|
||||
adapt_observation_t obs;
|
||||
bool have_obs = false;
|
||||
portENTER_CRITICAL(&s_obs_lock);
|
||||
if (s_obs_valid) {
|
||||
obs = s_last_obs;
|
||||
have_obs = true;
|
||||
}
|
||||
portEXIT_CRITICAL(&s_obs_lock);
|
||||
|
||||
if (have_obs) {
|
||||
pkt.motion_score = obs.motion_score;
|
||||
pkt.presence_score = obs.presence_score;
|
||||
pkt.anomaly_score = obs.anomaly_score;
|
||||
pkt.node_coherence = obs.node_coherence;
|
||||
}
|
||||
|
||||
/* Fill vitals from edge_processing's latest packet. */
|
||||
edge_vitals_pkt_t v;
|
||||
if (edge_get_vitals(&v)) {
|
||||
pkt.respiration_bpm = (float)v.breathing_rate / 100.0f;
|
||||
pkt.heartbeat_bpm = (float)v.heartrate / 10000.0f;
|
||||
/* Confidence proxies: presence score for resp, 1.0 if heart BPM
|
||||
* is within physiological range. */
|
||||
pkt.respiration_conf = (v.breathing_rate > 0) ? v.presence_score : 0.0f;
|
||||
pkt.heartbeat_conf = (v.heartrate > 400000u && v.heartrate < 1800000u)
|
||||
? 0.8f : 0.0f;
|
||||
if (pkt.respiration_bpm > 0.0f) pkt.quality_flags |= RV_QFLAG_RESPIRATION_VALID;
|
||||
if (pkt.heartbeat_bpm > 0.0f) pkt.quality_flags |= RV_QFLAG_HEARTBEAT_VALID;
|
||||
if (pkt.presence_score >= 0.5f) pkt.quality_flags |= RV_QFLAG_PRESENCE_VALID;
|
||||
if (v.flags & 0x02) pkt.quality_flags |= RV_QFLAG_ANOMALY_TRIGGERED; /* fall bit */
|
||||
}
|
||||
|
||||
if (s_state == ADAPT_STATE_DEGRADED) pkt.quality_flags |= RV_QFLAG_DEGRADED_MODE;
|
||||
if (s_state == ADAPT_STATE_CALIBRATION) pkt.quality_flags |= RV_QFLAG_CALIBRATING;
|
||||
|
||||
/* Active profile, for receiver-side weighting. */
|
||||
const rv_radio_ops_t *ops = rv_radio_ops_get();
|
||||
uint8_t profile = RV_PROFILE_PASSIVE_LOW_RATE;
|
||||
if (ops != NULL && ops->get_health != NULL) {
|
||||
rv_radio_health_t h;
|
||||
if (ops->get_health(&h) == ESP_OK) profile = h.current_profile;
|
||||
}
|
||||
|
||||
rv_feature_state_finalize(&pkt,
|
||||
csi_collector_get_node_id(),
|
||||
s_feature_state_seq++,
|
||||
(uint64_t)esp_timer_get_time(),
|
||||
profile);
|
||||
|
||||
int sent = stream_sender_send((const uint8_t *)&pkt, sizeof(pkt));
|
||||
if (sent < 0) {
|
||||
ESP_LOGW(TAG, "feature_state emit failed");
|
||||
}
|
||||
}
|
||||
|
||||
static void slow_loop_cb(TimerHandle_t t)
|
||||
{
|
||||
(void)t;
|
||||
/* ADR-081 L3: publish a HEALTH mesh message every slow tick
|
||||
* (default 30 s). The coordinator uses these to track liveness and
|
||||
* detect sync-error drift. */
|
||||
uint8_t nid[8];
|
||||
node_id_bytes(nid);
|
||||
rv_mesh_send_health(s_role, s_mesh_epoch, nid);
|
||||
|
||||
ESP_LOGI(TAG, "slow tick (state=%u, feature_state_seq=%u, role=%u, epoch=%u) HEALTH sent",
|
||||
(unsigned)s_state, (unsigned)s_feature_state_seq,
|
||||
(unsigned)s_role, (unsigned)s_mesh_epoch);
|
||||
}
|
||||
|
||||
/* ---- Public API ---- */
|
||||
|
||||
esp_err_t adaptive_controller_init(const adapt_config_t *cfg)
|
||||
{
|
||||
if (s_inited) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
if (cfg != NULL) {
|
||||
s_cfg = *cfg;
|
||||
} else {
|
||||
apply_defaults(&s_cfg);
|
||||
}
|
||||
|
||||
/* Sanity clamps. */
|
||||
if (s_cfg.fast_loop_ms < 50) s_cfg.fast_loop_ms = 50;
|
||||
if (s_cfg.medium_loop_ms < 200) s_cfg.medium_loop_ms = 200;
|
||||
if (s_cfg.slow_loop_ms < 1000) s_cfg.slow_loop_ms = 1000;
|
||||
|
||||
s_state = ADAPT_STATE_RADIO_INIT;
|
||||
|
||||
s_fast_timer = xTimerCreate("adapt_fast",
|
||||
pdMS_TO_TICKS(s_cfg.fast_loop_ms),
|
||||
pdTRUE, NULL, fast_loop_cb);
|
||||
s_medium_timer = xTimerCreate("adapt_med",
|
||||
pdMS_TO_TICKS(s_cfg.medium_loop_ms),
|
||||
pdTRUE, NULL, medium_loop_cb);
|
||||
s_slow_timer = xTimerCreate("adapt_slow",
|
||||
pdMS_TO_TICKS(s_cfg.slow_loop_ms),
|
||||
pdTRUE, NULL, slow_loop_cb);
|
||||
|
||||
if (s_fast_timer == NULL || s_medium_timer == NULL || s_slow_timer == NULL) {
|
||||
ESP_LOGE(TAG, "timer create failed");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
if (xTimerStart(s_fast_timer, 0) != pdPASS ||
|
||||
xTimerStart(s_medium_timer, 0) != pdPASS ||
|
||||
xTimerStart(s_slow_timer, 0) != pdPASS) {
|
||||
ESP_LOGE(TAG, "timer start failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
s_state = ADAPT_STATE_SENSE_IDLE;
|
||||
s_inited = true;
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"adaptive controller online: fast=%ums med=%ums slow=%ums "
|
||||
"(channel_switch=%d role_change=%d aggressive=%d)",
|
||||
(unsigned)s_cfg.fast_loop_ms,
|
||||
(unsigned)s_cfg.medium_loop_ms,
|
||||
(unsigned)s_cfg.slow_loop_ms,
|
||||
(int)s_cfg.enable_channel_switch,
|
||||
(int)s_cfg.enable_role_change,
|
||||
(int)s_cfg.aggressive);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
adapt_state_t adaptive_controller_state(void)
|
||||
{
|
||||
return s_state;
|
||||
}
|
||||
|
||||
bool adaptive_controller_observation(adapt_observation_t *out)
|
||||
{
|
||||
if (out == NULL) return false;
|
||||
bool ok = false;
|
||||
portENTER_CRITICAL(&s_obs_lock);
|
||||
if (s_obs_valid) {
|
||||
*out = s_last_obs;
|
||||
ok = true;
|
||||
}
|
||||
portEXIT_CRITICAL(&s_obs_lock);
|
||||
return ok;
|
||||
}
|
||||
|
||||
void adaptive_controller_force_state(adapt_state_t st)
|
||||
{
|
||||
ESP_LOGI(TAG, "force state %u → %u", (unsigned)s_state, (unsigned)st);
|
||||
s_state = st;
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
/**
|
||||
* @file adaptive_controller.h
|
||||
* @brief ADR-081 Layer 2 — Adaptive sensing controller.
|
||||
*
|
||||
* Closed-loop firmware control over cadence, capture profile, channel, and
|
||||
* mesh role. Three cooperating loops:
|
||||
*
|
||||
* Fast (~200 ms): packet rate, active probing
|
||||
* Medium (~1 s) : channel selection, role transitions
|
||||
* Slow (~30 s) : baseline recalibration
|
||||
*
|
||||
* Outputs are routed through:
|
||||
* - rv_radio_ops_t (Layer 1) for set_channel / set_capture_profile
|
||||
* - swarm_bridge / mesh plane (Layer 3) for CHANNEL_PLAN, ROLE_ASSIGN
|
||||
* - edge_processing (Layer 4) for cadence and threshold updates
|
||||
*
|
||||
* Default policy is conservative — matches today's behavior. Aggressive
|
||||
* adaptation is opt-in via Kconfig (ADAPTIVE_CONTROLLER_AGGRESSIVE).
|
||||
*/
|
||||
|
||||
#ifndef ADAPTIVE_CONTROLLER_H
|
||||
#define ADAPTIVE_CONTROLLER_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Controller-level state machine (ADR-081 firmware FSM). */
|
||||
typedef enum {
|
||||
ADAPT_STATE_BOOT = 0,
|
||||
ADAPT_STATE_SELF_TEST = 1,
|
||||
ADAPT_STATE_RADIO_INIT = 2,
|
||||
ADAPT_STATE_TIME_SYNC = 3,
|
||||
ADAPT_STATE_CALIBRATION = 4,
|
||||
ADAPT_STATE_SENSE_IDLE = 5,
|
||||
ADAPT_STATE_SENSE_ACTIVE = 6,
|
||||
ADAPT_STATE_ALERT = 7,
|
||||
ADAPT_STATE_DEGRADED = 8,
|
||||
} adapt_state_t;
|
||||
|
||||
/** Observation window aggregated each fast tick. */
|
||||
typedef struct {
|
||||
uint16_t pkt_yield_per_sec; /**< From rv_radio_health.pkt_yield_per_sec. */
|
||||
uint16_t send_fail_count; /**< UDP/socket send failures. */
|
||||
int8_t rssi_median_dbm;
|
||||
int8_t noise_floor_dbm;
|
||||
float motion_score; /**< Pulled from edge_processing. */
|
||||
float presence_score;
|
||||
float anomaly_score;
|
||||
float node_coherence; /**< Inter-link coherence; 1.0 if single node. */
|
||||
} adapt_observation_t;
|
||||
|
||||
/** Decisions emitted by a controller tick. */
|
||||
typedef struct {
|
||||
bool change_profile;
|
||||
uint8_t new_profile; /**< rv_capture_profile_t. */
|
||||
bool change_channel;
|
||||
uint8_t new_channel;
|
||||
bool change_state;
|
||||
uint8_t new_state; /**< adapt_state_t. */
|
||||
bool request_calibration; /**< Coordinator should issue CALIBRATION_START. */
|
||||
uint16_t suggested_vital_interval_ms;
|
||||
} adapt_decision_t;
|
||||
|
||||
/** Controller config (loaded from NVS / Kconfig). */
|
||||
typedef struct {
|
||||
uint16_t fast_loop_ms; /**< Default 200 ms. */
|
||||
uint16_t medium_loop_ms; /**< Default 1000 ms. */
|
||||
uint16_t slow_loop_ms; /**< Default 30000 ms. */
|
||||
bool aggressive; /**< true = react sooner / more often. */
|
||||
bool enable_channel_switch; /**< false = controller may never hop. */
|
||||
bool enable_role_change;
|
||||
float motion_threshold; /**< 0..1, enter SENSE_ACTIVE above this. */
|
||||
float anomaly_threshold; /**< 0..1, enter ALERT above this. */
|
||||
uint16_t min_pkt_yield; /**< pps below this → DEGRADED. */
|
||||
} adapt_config_t;
|
||||
|
||||
/**
|
||||
* Initialize the adaptive controller.
|
||||
*
|
||||
* Spawns one FreeRTOS task that runs the three loops via FreeRTOS timers.
|
||||
* Idempotent — second call is a no-op.
|
||||
*
|
||||
* @param cfg Config (NULL = use Kconfig defaults).
|
||||
* @return ESP_OK on success.
|
||||
*/
|
||||
esp_err_t adaptive_controller_init(const adapt_config_t *cfg);
|
||||
|
||||
/** Get the current state. */
|
||||
adapt_state_t adaptive_controller_state(void);
|
||||
|
||||
/**
|
||||
* Snapshot the latest observation (most recent fast-loop sample).
|
||||
* Useful for telemetry and the `HEALTH` mesh message.
|
||||
*
|
||||
* @param out Output buffer.
|
||||
* @return true if a valid observation has been recorded.
|
||||
*/
|
||||
bool adaptive_controller_observation(adapt_observation_t *out);
|
||||
|
||||
/**
|
||||
* Force a state transition (e.g. from a remote ROLE_ASSIGN message).
|
||||
* Logged at INFO; controller may immediately transition again on next tick.
|
||||
*/
|
||||
void adaptive_controller_force_state(adapt_state_t st);
|
||||
|
||||
/**
|
||||
* Pure-function policy: given an observation + current state + config,
|
||||
* compute the decision. Exposed in the header so it can be unit-tested
|
||||
* offline (no FreeRTOS / ESP-IDF dependency in the body).
|
||||
*/
|
||||
void adaptive_controller_decide(const adapt_config_t *cfg,
|
||||
adapt_state_t current,
|
||||
const adapt_observation_t *obs,
|
||||
adapt_decision_t *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* ADAPTIVE_CONTROLLER_H */
|
||||
@@ -1,83 +0,0 @@
|
||||
/**
|
||||
* @file adaptive_controller_decide.c
|
||||
* @brief ADR-081 Layer 2 — pure decision policy.
|
||||
*
|
||||
* Extracted so host unit tests can link this without ESP-IDF / FreeRTOS.
|
||||
* adaptive_controller.c includes this file; the host Makefile links it
|
||||
* directly against the test harness.
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "adaptive_controller.h"
|
||||
#include "rv_radio_ops.h"
|
||||
|
||||
void adaptive_controller_decide(const adapt_config_t *cfg,
|
||||
adapt_state_t current,
|
||||
const adapt_observation_t *obs,
|
||||
adapt_decision_t *out)
|
||||
{
|
||||
if (cfg == NULL || obs == NULL || out == NULL) {
|
||||
return;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->new_state = (uint8_t)current;
|
||||
out->new_profile = RV_PROFILE_PASSIVE_LOW_RATE;
|
||||
|
||||
/* Degraded gate: pkt yield collapse or severe coherence loss → DEGRADED. */
|
||||
if (obs->pkt_yield_per_sec < cfg->min_pkt_yield ||
|
||||
obs->node_coherence < 0.20f) {
|
||||
if (current != ADAPT_STATE_DEGRADED) {
|
||||
out->change_state = true;
|
||||
out->new_state = ADAPT_STATE_DEGRADED;
|
||||
}
|
||||
out->change_profile = (current != ADAPT_STATE_DEGRADED);
|
||||
out->new_profile = RV_PROFILE_PASSIVE_LOW_RATE;
|
||||
out->suggested_vital_interval_ms = 2000;
|
||||
return;
|
||||
}
|
||||
|
||||
/* Anomaly trumps motion. */
|
||||
if (obs->anomaly_score >= cfg->anomaly_threshold) {
|
||||
if (current != ADAPT_STATE_ALERT) {
|
||||
out->change_state = true;
|
||||
out->new_state = ADAPT_STATE_ALERT;
|
||||
}
|
||||
out->change_profile = true;
|
||||
out->new_profile = RV_PROFILE_FAST_MOTION;
|
||||
out->suggested_vital_interval_ms = 100;
|
||||
return;
|
||||
}
|
||||
|
||||
/* Motion → SENSE_ACTIVE with FAST_MOTION profile. */
|
||||
if (obs->motion_score >= cfg->motion_threshold) {
|
||||
if (current != ADAPT_STATE_SENSE_ACTIVE) {
|
||||
out->change_state = true;
|
||||
out->new_state = ADAPT_STATE_SENSE_ACTIVE;
|
||||
}
|
||||
out->change_profile = true;
|
||||
out->new_profile = RV_PROFILE_FAST_MOTION;
|
||||
out->suggested_vital_interval_ms = cfg->aggressive ? 100 : 200;
|
||||
return;
|
||||
}
|
||||
|
||||
/* Stable presence + quiet → high-sensitivity respiration. */
|
||||
if (obs->presence_score >= 0.5f && obs->motion_score < 0.05f) {
|
||||
if (current != ADAPT_STATE_SENSE_IDLE) {
|
||||
out->change_state = true;
|
||||
out->new_state = ADAPT_STATE_SENSE_IDLE;
|
||||
}
|
||||
out->change_profile = true;
|
||||
out->new_profile = RV_PROFILE_RESP_HIGH_SENS;
|
||||
out->suggested_vital_interval_ms = 1000;
|
||||
return;
|
||||
}
|
||||
|
||||
/* Default: passive low rate. */
|
||||
if (current != ADAPT_STATE_SENSE_IDLE) {
|
||||
out->change_state = true;
|
||||
out->new_state = ADAPT_STATE_SENSE_IDLE;
|
||||
}
|
||||
out->change_profile = (current != ADAPT_STATE_SENSE_IDLE);
|
||||
out->new_profile = RV_PROFILE_PASSIVE_LOW_RATE;
|
||||
out->suggested_vital_interval_ms = cfg->aggressive ? 500 : 1000;
|
||||
}
|
||||
@@ -308,43 +308,6 @@ uint8_t csi_collector_get_node_id(void)
|
||||
return s_node_id;
|
||||
}
|
||||
|
||||
/* ---- ADR-081: packet yield accessor for the radio abstraction layer ---- */
|
||||
|
||||
uint16_t csi_collector_get_pkt_yield_per_sec(void)
|
||||
{
|
||||
/* Simple sliding window: record the callback count at ~1 s ago, return
|
||||
* the delta. Called from adaptive_controller's fast loop (200 ms), so
|
||||
* we update the snapshot every ~5 calls. */
|
||||
static int64_t s_yield_window_start_us = 0;
|
||||
static uint32_t s_yield_window_start_cb = 0;
|
||||
static uint16_t s_last_yield = 0;
|
||||
|
||||
int64_t now = esp_timer_get_time();
|
||||
if (s_yield_window_start_us == 0) {
|
||||
s_yield_window_start_us = now;
|
||||
s_yield_window_start_cb = s_cb_count;
|
||||
return 0;
|
||||
}
|
||||
int64_t elapsed = now - s_yield_window_start_us;
|
||||
if (elapsed < 1000000LL) {
|
||||
return s_last_yield;
|
||||
}
|
||||
uint32_t delta = s_cb_count - s_yield_window_start_cb;
|
||||
/* Scale back to per-second if the window ran long (shouldn't, but be safe). */
|
||||
uint64_t per_sec = ((uint64_t)delta * 1000000ULL) / (uint64_t)elapsed;
|
||||
if (per_sec > 0xFFFFu) per_sec = 0xFFFFu;
|
||||
s_last_yield = (uint16_t)per_sec;
|
||||
s_yield_window_start_us = now;
|
||||
s_yield_window_start_cb = s_cb_count;
|
||||
return s_last_yield;
|
||||
}
|
||||
|
||||
uint16_t csi_collector_get_send_fail_count(void)
|
||||
{
|
||||
uint32_t f = s_send_fail;
|
||||
return (f > 0xFFFFu) ? 0xFFFFu : (uint16_t)f;
|
||||
}
|
||||
|
||||
/* ---- ADR-029: Channel hopping ---- */
|
||||
|
||||
void csi_collector_set_hop_table(const uint8_t *channels, uint8_t hop_count, uint32_t dwell_ms)
|
||||
|
||||
@@ -94,23 +94,4 @@ void csi_collector_start_hop_timer(void);
|
||||
*/
|
||||
esp_err_t csi_inject_ndp_frame(void);
|
||||
|
||||
/**
|
||||
* Get the recent CSI callback rate (per second).
|
||||
*
|
||||
* Computed as a sliding 1-second window over the internal s_cb_count
|
||||
* counter. Used by the ADR-081 radio abstraction layer to fill the
|
||||
* pkt_yield_per_sec field of rv_radio_health_t.
|
||||
*
|
||||
* @return Callbacks observed in the trailing ~1 second.
|
||||
*/
|
||||
uint16_t csi_collector_get_pkt_yield_per_sec(void);
|
||||
|
||||
/**
|
||||
* Get the cumulative UDP send-failure counter since boot.
|
||||
*
|
||||
* @return Number of stream_sender_send() failures recorded by the
|
||||
* CSI callback path.
|
||||
*/
|
||||
uint16_t csi_collector_get_send_fail_count(void);
|
||||
|
||||
#endif /* CSI_COLLECTOR_H */
|
||||
|
||||
@@ -30,8 +30,6 @@
|
||||
#include "display_task.h"
|
||||
#include "mmwave_sensor.h"
|
||||
#include "swarm_bridge.h"
|
||||
#include "rv_radio_ops.h" /* ADR-081 Layer 1 — Radio Abstraction Layer. */
|
||||
#include "adaptive_controller.h" /* ADR-081 Layer 2 — Adaptive controller. */
|
||||
#ifdef CONFIG_CSI_MOCK_ENABLED
|
||||
#include "mock_csi.h"
|
||||
#endif
|
||||
@@ -280,31 +278,6 @@ void app_main(void)
|
||||
ESP_LOGI(TAG, "Mock CSI mode: skipping swarm bridge");
|
||||
#endif
|
||||
|
||||
/* ADR-081 Layer 1: register the active radio ops binding.
|
||||
* - Real hardware: ESP32 binding wrapping csi_collector + esp_wifi.
|
||||
* - QEMU / offline: mock binding wrapping mock_csi.c.
|
||||
* Either way, the layers above (adaptive controller, mesh plane,
|
||||
* feature extraction) address the radio through the same vtable —
|
||||
* this is the portability acceptance test in ADR-081. */
|
||||
#ifdef CONFIG_CSI_MOCK_ENABLED
|
||||
rv_radio_ops_mock_register();
|
||||
#else
|
||||
rv_radio_ops_esp32_register();
|
||||
#endif
|
||||
const rv_radio_ops_t *radio_ops = rv_radio_ops_get();
|
||||
if (radio_ops != NULL && radio_ops->init != NULL) {
|
||||
radio_ops->init();
|
||||
}
|
||||
|
||||
/* ADR-081 Layer 2: start the adaptive controller. NULL config → use
|
||||
* Kconfig defaults. Default policy is conservative: no channel
|
||||
* switching, no role change. Operators opt in via menuconfig. */
|
||||
esp_err_t adapt_ret = adaptive_controller_init(NULL);
|
||||
if (adapt_ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Adaptive controller init failed: %s",
|
||||
esp_err_to_name(adapt_ret));
|
||||
}
|
||||
|
||||
/* Initialize power management. */
|
||||
power_mgmt_init(g_nvs_config.power_duty);
|
||||
|
||||
@@ -316,14 +289,13 @@ void app_main(void)
|
||||
}
|
||||
#endif
|
||||
|
||||
ESP_LOGI(TAG, "CSI streaming active → %s:%d (edge_tier=%u, OTA=%s, WASM=%s, mmWave=%s, swarm=%s, adapt=%s)",
|
||||
ESP_LOGI(TAG, "CSI streaming active → %s:%d (edge_tier=%u, OTA=%s, WASM=%s, mmWave=%s, swarm=%s)",
|
||||
g_nvs_config.target_ip, g_nvs_config.target_port,
|
||||
g_nvs_config.edge_tier,
|
||||
(ota_ret == ESP_OK) ? "ready" : "off",
|
||||
(wasm_ret == ESP_OK) ? "ready" : "off",
|
||||
(mmwave_ret == ESP_OK) ? "active" : "off",
|
||||
(swarm_ret == ESP_OK) ? g_nvs_config.seed_url : "off",
|
||||
(adapt_ret == ESP_OK) ? "on" : "off");
|
||||
(swarm_ret == ESP_OK) ? g_nvs_config.seed_url : "off");
|
||||
|
||||
/* Main loop — keep alive */
|
||||
while (1) {
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/**
|
||||
* @file rv_feature_state.c
|
||||
* @brief ADR-081 Layer 4 — Feature state packet helpers.
|
||||
*/
|
||||
|
||||
#include "rv_feature_state.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
uint32_t rv_feature_state_crc32(const uint8_t *data, size_t len)
|
||||
{
|
||||
/* IEEE CRC32 (poly 0xEDB88320), bit-by-bit. Small (~80 byte) input at
|
||||
* low cadence — no need for a 1 KB lookup table. */
|
||||
uint32_t crc = 0xFFFFFFFFu;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
crc ^= data[i];
|
||||
for (int b = 0; b < 8; b++) {
|
||||
uint32_t mask = -(crc & 1u);
|
||||
crc = (crc >> 1) ^ (0xEDB88320u & mask);
|
||||
}
|
||||
}
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
void rv_feature_state_finalize(rv_feature_state_t *pkt,
|
||||
uint8_t node_id,
|
||||
uint16_t seq,
|
||||
uint64_t ts_us,
|
||||
uint8_t mode)
|
||||
{
|
||||
if (pkt == NULL) {
|
||||
return;
|
||||
}
|
||||
pkt->magic = RV_FEATURE_STATE_MAGIC;
|
||||
pkt->node_id = node_id;
|
||||
pkt->mode = mode;
|
||||
pkt->seq = seq;
|
||||
pkt->ts_us = ts_us;
|
||||
pkt->reserved = 0;
|
||||
|
||||
/* CRC32 over everything except the trailing crc32 field itself. */
|
||||
const size_t crc_offset = sizeof(rv_feature_state_t) - sizeof(uint32_t);
|
||||
pkt->crc32 = rv_feature_state_crc32((const uint8_t *)pkt, crc_offset);
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
/**
|
||||
* @file rv_feature_state.h
|
||||
* @brief ADR-081 Layer 4 — Compact on-wire feature state packet.
|
||||
*
|
||||
* The default upstream payload from a node. Replaces raw ADR-018 CSI as the
|
||||
* primary stream; ADR-018 raw frames remain available as a debug stream
|
||||
* gated by the controller / channel plan.
|
||||
*
|
||||
* Magic numbers in use across the firmware:
|
||||
* 0xC5110001 — ADR-018 raw CSI frame (csi_collector.h)
|
||||
* 0xC5110002 — ADR-039 vitals packet (edge_processing.h)
|
||||
* 0xC5110003 — ADR-069 feature vector (edge_processing.h)
|
||||
* 0xC5110004 — ADR-063 fused vitals (edge_processing.h)
|
||||
* 0xC5110005 — ADR-039 compressed CSI (edge_processing.h)
|
||||
* 0xC5110006 — ADR-081 feature state (this file) ← new
|
||||
*/
|
||||
|
||||
#ifndef RV_FEATURE_STATE_H
|
||||
#define RV_FEATURE_STATE_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Magic number for ADR-081 rv_feature_state_t. */
|
||||
#define RV_FEATURE_STATE_MAGIC 0xC5110006u
|
||||
|
||||
/** Quality flag bits. */
|
||||
#define RV_QFLAG_PRESENCE_VALID (1u << 0)
|
||||
#define RV_QFLAG_RESPIRATION_VALID (1u << 1)
|
||||
#define RV_QFLAG_HEARTBEAT_VALID (1u << 2)
|
||||
#define RV_QFLAG_ANOMALY_TRIGGERED (1u << 3)
|
||||
#define RV_QFLAG_ENV_SHIFT_DETECTED (1u << 4)
|
||||
#define RV_QFLAG_DEGRADED_MODE (1u << 5)
|
||||
#define RV_QFLAG_CALIBRATING (1u << 6)
|
||||
#define RV_QFLAG_RECOMMEND_RECAL (1u << 7)
|
||||
|
||||
/**
|
||||
* Compact per-node sensing state. Sent at 1-10 Hz by default, replacing the
|
||||
* raw ADR-018 stream as the primary upstream payload.
|
||||
*
|
||||
* Mode field carries the rv_capture_profile_t value of the dominant window
|
||||
* — receivers can use it to weight features (a sample emitted under
|
||||
* RV_PROFILE_FAST_MOTION will have a stale respiration_bpm, etc.).
|
||||
*
|
||||
* CRC32 is the IEEE polynomial computed over bytes [0 .. sizeof - 4].
|
||||
*/
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t magic; /**< RV_FEATURE_STATE_MAGIC. */
|
||||
uint8_t node_id; /**< Source node id. */
|
||||
uint8_t mode; /**< rv_capture_profile_t at emit time. */
|
||||
uint16_t seq; /**< Monotonic per-node sequence. */
|
||||
uint64_t ts_us; /**< Node-local microseconds. */
|
||||
float motion_score; /**< 0..1, 100 ms window. */
|
||||
float presence_score; /**< 0..1, 1 s window. */
|
||||
float respiration_bpm; /**< Breaths per minute. */
|
||||
float respiration_conf; /**< 0..1. */
|
||||
float heartbeat_bpm; /**< Beats per minute. */
|
||||
float heartbeat_conf; /**< 0..1. */
|
||||
float anomaly_score; /**< 0..1, z-score-derived. */
|
||||
float env_shift_score; /**< 0..1, baseline drift. */
|
||||
float node_coherence; /**< 0..1, multi-link agreement. */
|
||||
uint16_t quality_flags; /**< RV_QFLAG_* bitmap. */
|
||||
uint16_t reserved;
|
||||
uint32_t crc32; /**< IEEE CRC32 over bytes [0..end-4]. */
|
||||
} rv_feature_state_t;
|
||||
|
||||
_Static_assert(sizeof(rv_feature_state_t) == 60,
|
||||
"rv_feature_state_t must be 60 bytes on the wire");
|
||||
|
||||
/**
|
||||
* Compute IEEE CRC32 over a byte buffer.
|
||||
*
|
||||
* Provided here (not in a separate util) because the firmware does not yet
|
||||
* have a shared CRC32 helper — only zlib's via lwIP, which is not always
|
||||
* exposed. This implementation is bit-by-bit; ~80 bytes/packet at low
|
||||
* cadence has negligible CPU cost.
|
||||
*
|
||||
* @param data Input buffer.
|
||||
* @param len Input length in bytes.
|
||||
* @return IEEE CRC32 of the input.
|
||||
*/
|
||||
uint32_t rv_feature_state_crc32(const uint8_t *data, size_t len);
|
||||
|
||||
/**
|
||||
* Finalize an rv_feature_state_t by populating magic, seq, ts_us, and crc32.
|
||||
* Caller fills the remaining fields in-place before calling this. After
|
||||
* finalize() the packet is ready to send on the wire.
|
||||
*
|
||||
* @param pkt Packet to finalize (caller-owned).
|
||||
* @param node_id Source node id (typically csi_collector_get_node_id()).
|
||||
* @param seq Monotonic sequence (caller-managed).
|
||||
* @param ts_us Node-local microseconds (typically esp_timer_get_time()).
|
||||
* @param mode Active rv_capture_profile_t.
|
||||
*/
|
||||
void rv_feature_state_finalize(rv_feature_state_t *pkt,
|
||||
uint8_t node_id,
|
||||
uint16_t seq,
|
||||
uint64_t ts_us,
|
||||
uint8_t mode);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* RV_FEATURE_STATE_H */
|
||||
@@ -1,251 +0,0 @@
|
||||
/**
|
||||
* @file rv_mesh.c
|
||||
* @brief ADR-081 Layer 3 — Mesh Sensing Plane implementation.
|
||||
*
|
||||
* Encoder/decoder are pure functions (no ESP-IDF deps) and therefore
|
||||
* host-unit-testable. The send helpers wrap stream_sender so the
|
||||
* firmware can use a single upstream socket for all payload types.
|
||||
*/
|
||||
|
||||
#include "rv_mesh.h"
|
||||
#include "rv_feature_state.h"
|
||||
#include "rv_radio_ops.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#ifndef RV_MESH_HOST_TEST
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "stream_sender.h"
|
||||
#include "csi_collector.h"
|
||||
#include "adaptive_controller.h"
|
||||
static const char *TAG = "rv_mesh";
|
||||
#endif
|
||||
|
||||
/* ---- Encoder ---- */
|
||||
|
||||
size_t rv_mesh_encode(uint8_t type,
|
||||
uint8_t sender_role,
|
||||
uint8_t auth_class,
|
||||
uint32_t epoch,
|
||||
const void *payload,
|
||||
uint16_t payload_len,
|
||||
uint8_t *buf,
|
||||
size_t buf_cap)
|
||||
{
|
||||
if (buf == NULL) return 0;
|
||||
if (payload == NULL && payload_len != 0) return 0;
|
||||
if (payload_len > RV_MESH_MAX_PAYLOAD) return 0;
|
||||
|
||||
size_t total = sizeof(rv_mesh_header_t) + (size_t)payload_len + 4u;
|
||||
if (buf_cap < total) return 0;
|
||||
|
||||
rv_mesh_header_t hdr;
|
||||
hdr.magic = RV_MESH_MAGIC;
|
||||
hdr.version = (uint8_t)RV_MESH_VERSION;
|
||||
hdr.type = type;
|
||||
hdr.sender_role = sender_role;
|
||||
hdr.auth_class = auth_class;
|
||||
hdr.epoch = epoch;
|
||||
hdr.payload_len = payload_len;
|
||||
hdr.reserved = 0;
|
||||
|
||||
memcpy(buf, &hdr, sizeof(hdr));
|
||||
if (payload_len > 0) {
|
||||
memcpy(buf + sizeof(hdr), payload, payload_len);
|
||||
}
|
||||
|
||||
/* IEEE CRC32 over header + payload. Reuses the CRC32 from
|
||||
* rv_feature_state.c so there is exactly one implementation. */
|
||||
uint32_t crc = rv_feature_state_crc32(buf, sizeof(hdr) + payload_len);
|
||||
memcpy(buf + sizeof(hdr) + payload_len, &crc, 4);
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
esp_err_t rv_mesh_decode(const uint8_t *buf, size_t buf_len,
|
||||
rv_mesh_header_t *out_hdr,
|
||||
const uint8_t **out_payload,
|
||||
uint16_t *out_payload_len)
|
||||
{
|
||||
if (buf == NULL || out_hdr == NULL ||
|
||||
out_payload == NULL || out_payload_len == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (buf_len < sizeof(rv_mesh_header_t) + 4u) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
rv_mesh_header_t hdr;
|
||||
memcpy(&hdr, buf, sizeof(hdr));
|
||||
|
||||
if (hdr.magic != RV_MESH_MAGIC) {
|
||||
return ESP_ERR_INVALID_VERSION; /* repurpose: wrong magic */
|
||||
}
|
||||
if (hdr.version != RV_MESH_VERSION) {
|
||||
return ESP_ERR_INVALID_VERSION;
|
||||
}
|
||||
if (hdr.payload_len > RV_MESH_MAX_PAYLOAD) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
size_t needed = sizeof(hdr) + (size_t)hdr.payload_len + 4u;
|
||||
if (buf_len < needed) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
uint32_t got_crc;
|
||||
memcpy(&got_crc, buf + sizeof(hdr) + hdr.payload_len, 4);
|
||||
uint32_t want_crc = rv_feature_state_crc32(buf,
|
||||
sizeof(hdr) + hdr.payload_len);
|
||||
if (got_crc != want_crc) {
|
||||
return ESP_ERR_INVALID_CRC;
|
||||
}
|
||||
|
||||
*out_hdr = hdr;
|
||||
*out_payload = (hdr.payload_len > 0) ? buf + sizeof(hdr) : NULL;
|
||||
*out_payload_len = hdr.payload_len;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ---- Typed convenience encoders ---- */
|
||||
|
||||
size_t rv_mesh_encode_health(uint8_t sender_role,
|
||||
uint32_t epoch,
|
||||
const rv_node_status_t *status,
|
||||
uint8_t *buf, size_t buf_cap)
|
||||
{
|
||||
if (status == NULL) return 0;
|
||||
return rv_mesh_encode(RV_MSG_HEALTH, sender_role, RV_AUTH_NONE,
|
||||
epoch, status, sizeof(*status), buf, buf_cap);
|
||||
}
|
||||
|
||||
size_t rv_mesh_encode_anomaly_alert(uint8_t sender_role,
|
||||
uint32_t epoch,
|
||||
const rv_anomaly_alert_t *alert,
|
||||
uint8_t *buf, size_t buf_cap)
|
||||
{
|
||||
if (alert == NULL) return 0;
|
||||
return rv_mesh_encode(RV_MSG_ANOMALY_ALERT, sender_role, RV_AUTH_NONE,
|
||||
epoch, alert, sizeof(*alert), buf, buf_cap);
|
||||
}
|
||||
|
||||
size_t rv_mesh_encode_feature_delta(uint8_t sender_role,
|
||||
uint32_t epoch,
|
||||
const rv_feature_state_t *fs,
|
||||
uint8_t *buf, size_t buf_cap)
|
||||
{
|
||||
if (fs == NULL) return 0;
|
||||
return rv_mesh_encode(RV_MSG_FEATURE_DELTA, sender_role, RV_AUTH_NONE,
|
||||
epoch, fs, sizeof(*fs), buf, buf_cap);
|
||||
}
|
||||
|
||||
size_t rv_mesh_encode_time_sync(uint8_t sender_role,
|
||||
uint32_t epoch,
|
||||
const rv_time_sync_t *ts,
|
||||
uint8_t *buf, size_t buf_cap)
|
||||
{
|
||||
if (ts == NULL) return 0;
|
||||
return rv_mesh_encode(RV_MSG_TIME_SYNC, sender_role, RV_AUTH_HMAC_SESSION,
|
||||
epoch, ts, sizeof(*ts), buf, buf_cap);
|
||||
}
|
||||
|
||||
size_t rv_mesh_encode_role_assign(uint8_t sender_role,
|
||||
uint32_t epoch,
|
||||
const rv_role_assign_t *ra,
|
||||
uint8_t *buf, size_t buf_cap)
|
||||
{
|
||||
if (ra == NULL) return 0;
|
||||
return rv_mesh_encode(RV_MSG_ROLE_ASSIGN, sender_role, RV_AUTH_HMAC_SESSION,
|
||||
epoch, ra, sizeof(*ra), buf, buf_cap);
|
||||
}
|
||||
|
||||
size_t rv_mesh_encode_channel_plan(uint8_t sender_role,
|
||||
uint32_t epoch,
|
||||
const rv_channel_plan_t *cp,
|
||||
uint8_t *buf, size_t buf_cap)
|
||||
{
|
||||
if (cp == NULL) return 0;
|
||||
return rv_mesh_encode(RV_MSG_CHANNEL_PLAN, sender_role, RV_AUTH_ED25519_BATCH,
|
||||
epoch, cp, sizeof(*cp), buf, buf_cap);
|
||||
}
|
||||
|
||||
size_t rv_mesh_encode_calibration_start(uint8_t sender_role,
|
||||
uint32_t epoch,
|
||||
const rv_calibration_start_t *cs,
|
||||
uint8_t *buf, size_t buf_cap)
|
||||
{
|
||||
if (cs == NULL) return 0;
|
||||
return rv_mesh_encode(RV_MSG_CALIBRATION_START, sender_role,
|
||||
RV_AUTH_ED25519_BATCH, epoch, cs, sizeof(*cs),
|
||||
buf, buf_cap);
|
||||
}
|
||||
|
||||
/* ---- Send helpers (firmware-only; hidden from host tests) ---- */
|
||||
|
||||
#ifndef RV_MESH_HOST_TEST
|
||||
|
||||
esp_err_t rv_mesh_send(const uint8_t *frame, size_t len)
|
||||
{
|
||||
if (frame == NULL || len == 0) return ESP_ERR_INVALID_ARG;
|
||||
int sent = stream_sender_send(frame, len);
|
||||
if (sent < 0) {
|
||||
ESP_LOGW(TAG, "rv_mesh_send: stream_sender failed (len=%u)",
|
||||
(unsigned)len);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t rv_mesh_send_health(uint8_t role, uint32_t epoch,
|
||||
const uint8_t node_id[8])
|
||||
{
|
||||
if (node_id == NULL) return ESP_ERR_INVALID_ARG;
|
||||
|
||||
rv_node_status_t st;
|
||||
memset(&st, 0, sizeof(st));
|
||||
memcpy(st.node_id, node_id, 8);
|
||||
st.local_time_us = (uint64_t)esp_timer_get_time();
|
||||
st.role = role;
|
||||
|
||||
const rv_radio_ops_t *ops = rv_radio_ops_get();
|
||||
if (ops != NULL && ops->get_health != NULL) {
|
||||
rv_radio_health_t h;
|
||||
if (ops->get_health(&h) == ESP_OK) {
|
||||
st.current_channel = h.current_channel;
|
||||
st.current_bw = h.current_bw_mhz;
|
||||
st.noise_floor_dbm = h.noise_floor_dbm;
|
||||
st.pkt_yield = h.pkt_yield_per_sec;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t buf[RV_MESH_MAX_FRAME_BYTES];
|
||||
size_t n = rv_mesh_encode_health(role, epoch, &st, buf, sizeof(buf));
|
||||
if (n == 0) return ESP_FAIL;
|
||||
return rv_mesh_send(buf, n);
|
||||
}
|
||||
|
||||
esp_err_t rv_mesh_send_anomaly(uint8_t role, uint32_t epoch,
|
||||
const uint8_t node_id[8],
|
||||
uint8_t reason,
|
||||
uint8_t severity,
|
||||
float anomaly_score,
|
||||
float motion_score)
|
||||
{
|
||||
if (node_id == NULL) return ESP_ERR_INVALID_ARG;
|
||||
rv_anomaly_alert_t a;
|
||||
memset(&a, 0, sizeof(a));
|
||||
memcpy(a.node_id, node_id, 8);
|
||||
a.ts_us = (uint64_t)esp_timer_get_time();
|
||||
a.reason = reason;
|
||||
a.severity = severity;
|
||||
a.anomaly_score = anomaly_score;
|
||||
a.motion_score = motion_score;
|
||||
|
||||
uint8_t buf[RV_MESH_MAX_FRAME_BYTES];
|
||||
size_t n = rv_mesh_encode_anomaly_alert(role, epoch, &a, buf, sizeof(buf));
|
||||
if (n == 0) return ESP_FAIL;
|
||||
return rv_mesh_send(buf, n);
|
||||
}
|
||||
|
||||
#endif /* !RV_MESH_HOST_TEST */
|
||||
@@ -1,296 +0,0 @@
|
||||
/**
|
||||
* @file rv_mesh.h
|
||||
* @brief ADR-081 Layer 3 — Mesh Sensing Plane.
|
||||
*
|
||||
* Defines node roles, the 7 on-wire message types, and the
|
||||
* rv_node_status_t health payload that nodes exchange to behave as a
|
||||
* distributed sensor rather than a collection of independent radios.
|
||||
*
|
||||
* Framing: every mesh message starts with rv_mesh_header_t (magic,
|
||||
* version, type, sender_role, epoch, length) so a receiver can dispatch
|
||||
* without reading the whole body. The trailing 4 bytes of every message
|
||||
* are an IEEE CRC32 over the preceding bytes. Authentication
|
||||
* (HMAC-SHA256 + replay window) is layered on top by
|
||||
* wifi-densepose-hardware/src/esp32/secure_tdm.rs (ADR-032) for control
|
||||
* messages that cross the swarm; FEATURE_DELTA uses the integrity
|
||||
* protection already present in rv_feature_state_t (CRC + monotonic seq).
|
||||
*/
|
||||
|
||||
#ifndef RV_MESH_H
|
||||
#define RV_MESH_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "esp_err.h"
|
||||
#include "rv_feature_state.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ---- Magic + version ---- */
|
||||
|
||||
/** ADR-081 mesh envelope magic. Distinct from the ADR-018 CSI magic. */
|
||||
#define RV_MESH_MAGIC 0xC5118100u
|
||||
|
||||
/** Protocol version. Bumped on any wire-format change. */
|
||||
#define RV_MESH_VERSION 1u
|
||||
|
||||
/** Maximum mesh payload size (excluding header + CRC). */
|
||||
#define RV_MESH_MAX_PAYLOAD 256u
|
||||
|
||||
/* ---- Node roles (ADR-081 Layer 3) ---- */
|
||||
|
||||
typedef enum {
|
||||
RV_ROLE_UNASSIGNED = 0,
|
||||
RV_ROLE_ANCHOR = 1, /**< Emits timed probes + global time beacons. */
|
||||
RV_ROLE_OBSERVER = 2, /**< Captures CSI + local metadata. */
|
||||
RV_ROLE_FUSION_RELAY = 3, /**< Aggregates summaries, forwards deltas. */
|
||||
RV_ROLE_COORDINATOR = 4, /**< Elects channels, assigns roles. */
|
||||
RV_ROLE_COUNT
|
||||
} rv_mesh_role_t;
|
||||
|
||||
/* ---- Authorization classes for control messages ---- */
|
||||
|
||||
typedef enum {
|
||||
RV_AUTH_NONE = 0, /**< Telemetry; integrity via CRC only. */
|
||||
RV_AUTH_HMAC_SESSION = 1, /**< HMAC-SHA256 with session key (ADR-032). */
|
||||
RV_AUTH_ED25519_BATCH = 2, /**< Ed25519 signature at batch/session. */
|
||||
} rv_mesh_auth_class_t;
|
||||
|
||||
/* ---- Message types ---- */
|
||||
|
||||
typedef enum {
|
||||
RV_MSG_TIME_SYNC = 0x01,
|
||||
RV_MSG_ROLE_ASSIGN = 0x02,
|
||||
RV_MSG_CHANNEL_PLAN = 0x03,
|
||||
RV_MSG_CALIBRATION_START = 0x04,
|
||||
RV_MSG_FEATURE_DELTA = 0x05, /**< Carries rv_feature_state_t. */
|
||||
RV_MSG_HEALTH = 0x06,
|
||||
RV_MSG_ANOMALY_ALERT = 0x07,
|
||||
} rv_mesh_msg_type_t;
|
||||
|
||||
/* ---- Common envelope header (16 bytes) ---- */
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint32_t magic; /**< RV_MESH_MAGIC. */
|
||||
uint8_t version; /**< RV_MESH_VERSION. */
|
||||
uint8_t type; /**< rv_mesh_msg_type_t. */
|
||||
uint8_t sender_role; /**< rv_mesh_role_t of the sender at send time. */
|
||||
uint8_t auth_class; /**< rv_mesh_auth_class_t. */
|
||||
uint32_t epoch; /**< Monotonic epoch or session counter. */
|
||||
uint16_t payload_len; /**< Body length excluding header + trailing CRC. */
|
||||
uint16_t reserved;
|
||||
} rv_mesh_header_t;
|
||||
|
||||
_Static_assert(sizeof(rv_mesh_header_t) == 16,
|
||||
"rv_mesh_header_t must be 16 bytes");
|
||||
|
||||
/* ---- Node health payload (RV_MSG_HEALTH) ---- */
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint8_t node_id[8]; /**< 8-byte node identity. */
|
||||
uint64_t local_time_us; /**< Sender-local microseconds. */
|
||||
uint8_t role; /**< rv_mesh_role_t. */
|
||||
uint8_t current_channel;
|
||||
uint8_t current_bw; /**< MHz (20, 40). */
|
||||
int8_t noise_floor_dbm;
|
||||
uint16_t pkt_yield; /**< CSI callbacks/sec over the last window. */
|
||||
uint16_t sync_error_us; /**< Absolute drift vs. anchor. */
|
||||
uint16_t health_flags;
|
||||
uint16_t reserved;
|
||||
} rv_node_status_t;
|
||||
|
||||
_Static_assert(sizeof(rv_node_status_t) == 28,
|
||||
"rv_node_status_t must be 28 bytes");
|
||||
|
||||
/* ---- TIME_SYNC payload ---- */
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint64_t anchor_time_us; /**< Anchor's local µs at emit. */
|
||||
uint32_t cycle_id;
|
||||
uint32_t cycle_period_us;
|
||||
} rv_time_sync_t;
|
||||
|
||||
_Static_assert(sizeof(rv_time_sync_t) == 16,
|
||||
"rv_time_sync_t must be 16 bytes");
|
||||
|
||||
/* ---- ROLE_ASSIGN payload ---- */
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint8_t target_node_id[8];
|
||||
uint8_t new_role; /**< rv_mesh_role_t. */
|
||||
uint8_t reserved[3];
|
||||
uint32_t effective_epoch;
|
||||
} rv_role_assign_t;
|
||||
|
||||
_Static_assert(sizeof(rv_role_assign_t) == 16,
|
||||
"rv_role_assign_t must be 16 bytes");
|
||||
|
||||
/* ---- CHANNEL_PLAN payload ---- */
|
||||
|
||||
#define RV_CHANNEL_PLAN_MAX 8
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint8_t target_node_id[8];
|
||||
uint8_t channel_count;
|
||||
uint8_t dwell_ms_hi; /**< dwell_ms, big-endian to fit u16 in two bytes */
|
||||
uint8_t dwell_ms_lo;
|
||||
uint8_t debug_raw_csi; /**< 1 = enable raw ADR-018 stream; 0 = feature_state only. */
|
||||
uint8_t channels[RV_CHANNEL_PLAN_MAX];
|
||||
uint32_t effective_epoch;
|
||||
} rv_channel_plan_t;
|
||||
|
||||
_Static_assert(sizeof(rv_channel_plan_t) == 24,
|
||||
"rv_channel_plan_t must be 24 bytes");
|
||||
|
||||
/* ---- CALIBRATION_START payload ---- */
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint64_t t0_anchor_us; /**< Start time on anchor clock. */
|
||||
uint32_t duration_ms;
|
||||
uint32_t effective_epoch;
|
||||
uint8_t calibration_profile; /**< rv_capture_profile_t (usually CALIBRATION). */
|
||||
uint8_t reserved[3];
|
||||
} rv_calibration_start_t;
|
||||
|
||||
_Static_assert(sizeof(rv_calibration_start_t) == 20,
|
||||
"rv_calibration_start_t must be 20 bytes");
|
||||
|
||||
/* ---- ANOMALY_ALERT payload ---- */
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint8_t node_id[8];
|
||||
uint64_t ts_us;
|
||||
uint8_t severity; /**< 0..255 scaled anomaly. */
|
||||
uint8_t reason; /**< rv_anomaly_reason_t. */
|
||||
uint16_t reserved;
|
||||
float anomaly_score;
|
||||
float motion_score;
|
||||
} rv_anomaly_alert_t;
|
||||
|
||||
_Static_assert(sizeof(rv_anomaly_alert_t) == 28,
|
||||
"rv_anomaly_alert_t must be 28 bytes");
|
||||
|
||||
typedef enum {
|
||||
RV_ANOMALY_NONE = 0,
|
||||
RV_ANOMALY_PHYSICS_VIOLATION = 1,
|
||||
RV_ANOMALY_MULTI_LINK_MISMATCH = 2,
|
||||
RV_ANOMALY_PKT_YIELD_COLLAPSE = 3,
|
||||
RV_ANOMALY_FALL = 4,
|
||||
RV_ANOMALY_COHERENCE_LOSS = 5,
|
||||
} rv_anomaly_reason_t;
|
||||
|
||||
/* ---- Encoder / decoder API ---- */
|
||||
|
||||
/** Maximum on-wire mesh frame: header + max payload + crc. */
|
||||
#define RV_MESH_MAX_FRAME_BYTES (sizeof(rv_mesh_header_t) + RV_MESH_MAX_PAYLOAD + 4u)
|
||||
|
||||
/**
|
||||
* Encode a typed mesh message into a contiguous buffer.
|
||||
*
|
||||
* Writes header(16) + payload(payload_len) + crc32(4). The caller owns
|
||||
* the buffer; buf_cap must be at least sizeof(rv_mesh_header_t) +
|
||||
* payload_len + 4. The payload pointer may be NULL iff payload_len == 0.
|
||||
*
|
||||
* @return bytes written on success, or 0 on error (bad args / overflow).
|
||||
*/
|
||||
size_t rv_mesh_encode(uint8_t type,
|
||||
uint8_t sender_role,
|
||||
uint8_t auth_class,
|
||||
uint32_t epoch,
|
||||
const void *payload,
|
||||
uint16_t payload_len,
|
||||
uint8_t *buf,
|
||||
size_t buf_cap);
|
||||
|
||||
/**
|
||||
* Validate + parse a mesh frame received from the wire.
|
||||
*
|
||||
* Checks magic, version, sizeof(rv_mesh_header_t) bounds, payload_len
|
||||
* bounds, and CRC32. On success, fills *out_hdr with the header and sets
|
||||
* *out_payload to point at the payload inside buf (aliasing, not copied)
|
||||
* plus *out_payload_len to the payload byte count.
|
||||
*
|
||||
* @return ESP_OK on success, or an ESP_ERR_* code on failure.
|
||||
*/
|
||||
esp_err_t rv_mesh_decode(const uint8_t *buf, size_t buf_len,
|
||||
rv_mesh_header_t *out_hdr,
|
||||
const uint8_t **out_payload,
|
||||
uint16_t *out_payload_len);
|
||||
|
||||
/**
|
||||
* Convenience helpers — encode a specific message type into buf.
|
||||
* Each returns the number of bytes written, 0 on error.
|
||||
*/
|
||||
size_t rv_mesh_encode_health(uint8_t sender_role,
|
||||
uint32_t epoch,
|
||||
const rv_node_status_t *status,
|
||||
uint8_t *buf, size_t buf_cap);
|
||||
|
||||
size_t rv_mesh_encode_anomaly_alert(uint8_t sender_role,
|
||||
uint32_t epoch,
|
||||
const rv_anomaly_alert_t *alert,
|
||||
uint8_t *buf, size_t buf_cap);
|
||||
|
||||
size_t rv_mesh_encode_feature_delta(uint8_t sender_role,
|
||||
uint32_t epoch,
|
||||
const rv_feature_state_t *fs,
|
||||
uint8_t *buf, size_t buf_cap);
|
||||
|
||||
size_t rv_mesh_encode_time_sync(uint8_t sender_role,
|
||||
uint32_t epoch,
|
||||
const rv_time_sync_t *ts,
|
||||
uint8_t *buf, size_t buf_cap);
|
||||
|
||||
size_t rv_mesh_encode_role_assign(uint8_t sender_role,
|
||||
uint32_t epoch,
|
||||
const rv_role_assign_t *ra,
|
||||
uint8_t *buf, size_t buf_cap);
|
||||
|
||||
size_t rv_mesh_encode_channel_plan(uint8_t sender_role,
|
||||
uint32_t epoch,
|
||||
const rv_channel_plan_t *cp,
|
||||
uint8_t *buf, size_t buf_cap);
|
||||
|
||||
size_t rv_mesh_encode_calibration_start(uint8_t sender_role,
|
||||
uint32_t epoch,
|
||||
const rv_calibration_start_t *cs,
|
||||
uint8_t *buf, size_t buf_cap);
|
||||
|
||||
/* ---- Send API ---- */
|
||||
|
||||
/**
|
||||
* Send a pre-encoded mesh frame over the primary upstream UDP socket
|
||||
* (the same one stream_sender uses for ADR-018 and rv_feature_state_t).
|
||||
*
|
||||
* @return ESP_OK on success.
|
||||
*/
|
||||
esp_err_t rv_mesh_send(const uint8_t *frame, size_t len);
|
||||
|
||||
/**
|
||||
* Convenience: build + send a HEALTH message for this node.
|
||||
*
|
||||
* Fills the rv_node_status_t from the live radio ops + controller
|
||||
* observation, then encodes and sends in one call. Safe to call from a
|
||||
* FreeRTOS timer.
|
||||
*/
|
||||
esp_err_t rv_mesh_send_health(uint8_t role, uint32_t epoch,
|
||||
const uint8_t node_id[8]);
|
||||
|
||||
/**
|
||||
* Convenience: build + send an ANOMALY_ALERT.
|
||||
*/
|
||||
esp_err_t rv_mesh_send_anomaly(uint8_t role, uint32_t epoch,
|
||||
const uint8_t node_id[8],
|
||||
uint8_t reason,
|
||||
uint8_t severity,
|
||||
float anomaly_score,
|
||||
float motion_score);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* RV_MESH_H */
|
||||
@@ -1,142 +0,0 @@
|
||||
/**
|
||||
* @file rv_radio_ops.h
|
||||
* @brief ADR-081 Layer 1 — Radio Abstraction Layer.
|
||||
*
|
||||
* A single function-pointer vtable (rv_radio_ops_t) that isolates chipset
|
||||
* specific capture details from the layers above (adaptive controller, mesh
|
||||
* plane, feature extraction, Rust handoff).
|
||||
*
|
||||
* Two bindings ship today:
|
||||
* - rv_radio_ops_esp32.c — wraps csi_collector + esp_wifi_*
|
||||
* - rv_radio_ops_mock.c — wraps mock_csi.c (when CONFIG_CSI_MOCK_ENABLED)
|
||||
*
|
||||
* A third binding (Nexmon-patched Broadcom/Cypress) is reserved but not
|
||||
* implemented here. The whole point of the vtable is that the controller
|
||||
* and mesh-plane code above never need to know which one is active.
|
||||
*/
|
||||
|
||||
#ifndef RV_RADIO_OPS_H
|
||||
#define RV_RADIO_OPS_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ---- Modes ---- */
|
||||
|
||||
/** Radio operating modes (set_mode argument). */
|
||||
typedef enum {
|
||||
RV_RADIO_MODE_DISABLED = 0, /**< Receiver off. */
|
||||
RV_RADIO_MODE_PASSIVE_RX = 1, /**< Listen-only, no TX. */
|
||||
RV_RADIO_MODE_ACTIVE_PROBE = 2, /**< Inject NDP frames at high rate. */
|
||||
RV_RADIO_MODE_CALIBRATION = 3, /**< Synchronized calibration burst. */
|
||||
} rv_radio_mode_t;
|
||||
|
||||
/* ---- Capture profiles ---- */
|
||||
|
||||
/**
|
||||
* Named capture profiles. The adaptive controller selects one of these
|
||||
* via set_capture_profile(); the binding maps it to chipset-specific
|
||||
* register/driver state.
|
||||
*/
|
||||
typedef enum {
|
||||
RV_PROFILE_PASSIVE_LOW_RATE = 0, /**< Default idle: minimum cadence. */
|
||||
RV_PROFILE_ACTIVE_PROBE = 1, /**< High-rate NDP injection. */
|
||||
RV_PROFILE_RESP_HIGH_SENS = 2, /**< Quietest channel, vitals-only. */
|
||||
RV_PROFILE_FAST_MOTION = 3, /**< Short window, high cadence. */
|
||||
RV_PROFILE_CALIBRATION = 4, /**< Synchronized burst across nodes. */
|
||||
RV_PROFILE_COUNT
|
||||
} rv_capture_profile_t;
|
||||
|
||||
/* ---- Health snapshot ---- */
|
||||
|
||||
/** Radio-layer health, polled by the adaptive controller. */
|
||||
typedef struct {
|
||||
uint16_t pkt_yield_per_sec; /**< CSI callbacks/second observed. */
|
||||
uint16_t send_fail_count; /**< UDP/socket send failures since last poll. */
|
||||
int8_t rssi_median_dbm; /**< Median RSSI over the last 1 s. */
|
||||
int8_t noise_floor_dbm; /**< Latest noise floor estimate. */
|
||||
uint8_t current_channel; /**< Channel currently configured. */
|
||||
uint8_t current_bw_mhz; /**< Bandwidth currently configured. */
|
||||
uint8_t current_profile; /**< Active rv_capture_profile_t. */
|
||||
uint8_t reserved;
|
||||
} rv_radio_health_t;
|
||||
|
||||
/* ---- The vtable ---- */
|
||||
|
||||
/**
|
||||
* Radio Abstraction Layer ops.
|
||||
*
|
||||
* All function pointers are required (no NULL slots). Each binding must
|
||||
* provide all six. Return values follow ESP-IDF conventions: 0/ESP_OK on
|
||||
* success, negative or ESP_ERR_* on failure.
|
||||
*/
|
||||
typedef struct {
|
||||
/** One-time init (driver register, callback wire-up). */
|
||||
int (*init)(void);
|
||||
|
||||
/**
|
||||
* Tune to a primary channel with the given bandwidth.
|
||||
* @param ch Channel number (1-13 for 2.4 GHz, 36-177 for 5 GHz).
|
||||
* @param bw Bandwidth in MHz (20 or 40; 80/160 reserved for future).
|
||||
*/
|
||||
int (*set_channel)(uint8_t ch, uint8_t bw);
|
||||
|
||||
/** Switch operating mode (rv_radio_mode_t). */
|
||||
int (*set_mode)(uint8_t mode);
|
||||
|
||||
/** Enable or disable the CSI capture path. */
|
||||
int (*set_csi_enabled)(bool en);
|
||||
|
||||
/** Apply a named capture profile (rv_capture_profile_t). */
|
||||
int (*set_capture_profile)(uint8_t profile_id);
|
||||
|
||||
/** Snapshot the radio-layer health (non-blocking). */
|
||||
int (*get_health)(rv_radio_health_t *out);
|
||||
} rv_radio_ops_t;
|
||||
|
||||
/* ---- Registration ---- */
|
||||
|
||||
/**
|
||||
* Register the active radio ops binding.
|
||||
*
|
||||
* Called once at boot by the chipset binding's init code (e.g.
|
||||
* rv_radio_ops_esp32_register()). The pointer must remain valid for the
|
||||
* lifetime of the process — typically a static const inside the binding.
|
||||
*/
|
||||
void rv_radio_ops_register(const rv_radio_ops_t *ops);
|
||||
|
||||
/**
|
||||
* Get the active radio ops binding.
|
||||
*
|
||||
* @return Pointer to the registered ops table, or NULL if no binding has
|
||||
* been registered yet (e.g. before init).
|
||||
*/
|
||||
const rv_radio_ops_t *rv_radio_ops_get(void);
|
||||
|
||||
/* ---- Convenience: ESP32 binding registration ---- */
|
||||
|
||||
/**
|
||||
* Register the ESP32 binding as the active radio ops.
|
||||
*
|
||||
* Call this once at boot, after csi_collector_init() has run. Idempotent.
|
||||
* Defined in rv_radio_ops_esp32.c.
|
||||
*/
|
||||
void rv_radio_ops_esp32_register(void);
|
||||
|
||||
/**
|
||||
* Register the mock binding (QEMU / offline) as the active radio ops.
|
||||
*
|
||||
* Defined in rv_radio_ops_mock.c; only built when CONFIG_CSI_MOCK_ENABLED.
|
||||
*/
|
||||
void rv_radio_ops_mock_register(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* RV_RADIO_OPS_H */
|
||||
@@ -1,176 +0,0 @@
|
||||
/**
|
||||
* @file rv_radio_ops_esp32.c
|
||||
* @brief ADR-081 Layer 1 — ESP32 binding for rv_radio_ops_t.
|
||||
*
|
||||
* Wraps the existing csi_collector + esp_wifi_* surface so the adaptive
|
||||
* controller, mesh plane, and feature-extraction layers can address the
|
||||
* radio through a single chipset-agnostic vtable.
|
||||
*
|
||||
* This is intentionally thin. The heavy lifting still lives in
|
||||
* csi_collector.c (CSI callback, channel hopping, NDP injection); this file
|
||||
* is the contract that lets a second chipset (Nexmon Broadcom, custom
|
||||
* silicon) drop in without touching the layers above.
|
||||
*/
|
||||
|
||||
#include "rv_radio_ops.h"
|
||||
#include "csi_collector.h"
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_wifi.h"
|
||||
|
||||
static const char *TAG = "rv_radio_esp32";
|
||||
|
||||
/* ---- Active ops registry ---- */
|
||||
|
||||
static const rv_radio_ops_t *s_active_ops = NULL;
|
||||
|
||||
void rv_radio_ops_register(const rv_radio_ops_t *ops)
|
||||
{
|
||||
s_active_ops = ops;
|
||||
}
|
||||
|
||||
const rv_radio_ops_t *rv_radio_ops_get(void)
|
||||
{
|
||||
return s_active_ops;
|
||||
}
|
||||
|
||||
/* ---- ESP32 binding state ---- */
|
||||
|
||||
static uint8_t s_current_channel = 1;
|
||||
static uint8_t s_current_bw = 20;
|
||||
static uint8_t s_current_profile = RV_PROFILE_PASSIVE_LOW_RATE;
|
||||
static uint8_t s_current_mode = RV_RADIO_MODE_PASSIVE_RX;
|
||||
static bool s_csi_enabled = true;
|
||||
|
||||
/* ---- Vtable implementations ---- */
|
||||
|
||||
static int esp32_init(void)
|
||||
{
|
||||
/* csi_collector_init() is called from app_main() before the controller
|
||||
* starts; nothing to do here for the ESP32 binding. We just confirm a
|
||||
* valid current channel was captured by csi_collector_init(). */
|
||||
ESP_LOGI(TAG, "ESP32 radio ops: init (current ch=%u bw=%u)",
|
||||
(unsigned)s_current_channel, (unsigned)s_current_bw);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static int esp32_set_channel(uint8_t ch, uint8_t bw)
|
||||
{
|
||||
wifi_second_chan_t second = WIFI_SECOND_CHAN_NONE;
|
||||
if (bw == 40) {
|
||||
/* HT40+: secondary channel above primary. The controller never asks
|
||||
* for HT40 today (sensing prefers HT20), but the mapping is here so
|
||||
* a future profile can. */
|
||||
second = WIFI_SECOND_CHAN_ABOVE;
|
||||
} else if (bw != 20) {
|
||||
ESP_LOGW(TAG, "set_channel: unsupported bw=%u, treating as 20 MHz",
|
||||
(unsigned)bw);
|
||||
bw = 20;
|
||||
}
|
||||
|
||||
esp_err_t err = esp_wifi_set_channel(ch, second);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "set_channel(%u, bw=%u) failed: %s",
|
||||
(unsigned)ch, (unsigned)bw, esp_err_to_name(err));
|
||||
return (int)err;
|
||||
}
|
||||
s_current_channel = ch;
|
||||
s_current_bw = bw;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static int esp32_set_mode(uint8_t mode)
|
||||
{
|
||||
/* Persist the mode for the health snapshot; actual TX behavior is
|
||||
* triggered by the controller calling csi_inject_ndp_frame() directly
|
||||
* once the controller PR lands. For now this is bookkeeping plus a
|
||||
* passive/active probe gate. */
|
||||
switch (mode) {
|
||||
case RV_RADIO_MODE_DISABLED:
|
||||
case RV_RADIO_MODE_PASSIVE_RX:
|
||||
case RV_RADIO_MODE_ACTIVE_PROBE:
|
||||
case RV_RADIO_MODE_CALIBRATION:
|
||||
s_current_mode = mode;
|
||||
return ESP_OK;
|
||||
default:
|
||||
ESP_LOGW(TAG, "set_mode: unknown mode %u", (unsigned)mode);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
}
|
||||
|
||||
static int esp32_set_csi_enabled(bool en)
|
||||
{
|
||||
esp_err_t err = esp_wifi_set_csi(en);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "set_csi(%d) failed: %s", (int)en, esp_err_to_name(err));
|
||||
return (int)err;
|
||||
}
|
||||
s_csi_enabled = en;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static int esp32_set_capture_profile(uint8_t profile_id)
|
||||
{
|
||||
if (profile_id >= RV_PROFILE_COUNT) {
|
||||
ESP_LOGW(TAG, "set_capture_profile: invalid id %u", (unsigned)profile_id);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
/* Profiles are advisory at this layer — the controller uses them to
|
||||
* decide cadence/window/threshold for the layers above. The radio
|
||||
* binding records the active profile for health reporting and may
|
||||
* adjust the underlying TX/RX mode in future bindings. */
|
||||
s_current_profile = profile_id;
|
||||
|
||||
/* For ACTIVE_PROBE and CALIBRATION, switch the radio mode to match. */
|
||||
if (profile_id == RV_PROFILE_ACTIVE_PROBE) {
|
||||
esp32_set_mode(RV_RADIO_MODE_ACTIVE_PROBE);
|
||||
} else if (profile_id == RV_PROFILE_CALIBRATION) {
|
||||
esp32_set_mode(RV_RADIO_MODE_CALIBRATION);
|
||||
} else {
|
||||
esp32_set_mode(RV_RADIO_MODE_PASSIVE_RX);
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static int esp32_get_health(rv_radio_health_t *out)
|
||||
{
|
||||
if (out == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
out->pkt_yield_per_sec = csi_collector_get_pkt_yield_per_sec();
|
||||
out->send_fail_count = csi_collector_get_send_fail_count();
|
||||
out->current_channel = s_current_channel;
|
||||
out->current_bw_mhz = s_current_bw;
|
||||
out->current_profile = s_current_profile;
|
||||
|
||||
wifi_ap_record_t ap = {0};
|
||||
if (esp_wifi_sta_get_ap_info(&ap) == ESP_OK) {
|
||||
out->rssi_median_dbm = ap.rssi;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/* ---- The vtable instance ---- */
|
||||
|
||||
static const rv_radio_ops_t s_esp32_ops = {
|
||||
.init = esp32_init,
|
||||
.set_channel = esp32_set_channel,
|
||||
.set_mode = esp32_set_mode,
|
||||
.set_csi_enabled = esp32_set_csi_enabled,
|
||||
.set_capture_profile = esp32_set_capture_profile,
|
||||
.get_health = esp32_get_health,
|
||||
};
|
||||
|
||||
void rv_radio_ops_esp32_register(void)
|
||||
{
|
||||
if (s_active_ops == &s_esp32_ops) {
|
||||
return; /* idempotent */
|
||||
}
|
||||
rv_radio_ops_register(&s_esp32_ops);
|
||||
ESP_LOGI(TAG, "ESP32 radio ops registered as active binding");
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
/**
|
||||
* @file rv_radio_ops_mock.c
|
||||
* @brief ADR-081 Layer 1 — Mock binding for QEMU / offline testing.
|
||||
*
|
||||
* When CONFIG_CSI_MOCK_ENABLED is set (ADR-061 QEMU flow), there is no
|
||||
* real WiFi driver to wrap. This binding provides the same ops table as
|
||||
* the ESP32 binding but records state into in-process statics and
|
||||
* accepts every call. It exists primarily to satisfy ADR-081's
|
||||
* portability acceptance test: a second binding must compile against
|
||||
* the same controller and mesh-plane code without modification.
|
||||
*
|
||||
* Only compiled when CONFIG_CSI_MOCK_ENABLED is set. Registered from
|
||||
* main.c in the mock branch.
|
||||
*/
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef CONFIG_CSI_MOCK_ENABLED
|
||||
|
||||
#include "rv_radio_ops.h"
|
||||
#include "mock_csi.h"
|
||||
|
||||
#include <string.h>
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "rv_radio_mock";
|
||||
|
||||
static uint8_t s_channel = 6;
|
||||
static uint8_t s_bw = 20;
|
||||
static uint8_t s_profile = RV_PROFILE_PASSIVE_LOW_RATE;
|
||||
static uint8_t s_mode = RV_RADIO_MODE_PASSIVE_RX;
|
||||
static bool s_csi_on = true;
|
||||
|
||||
static int mock_init(void)
|
||||
{
|
||||
ESP_LOGI(TAG, "mock radio ops: init");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static int mock_set_channel(uint8_t ch, uint8_t bw)
|
||||
{
|
||||
s_channel = ch;
|
||||
s_bw = (bw == 40) ? 40 : 20;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static int mock_set_mode(uint8_t mode)
|
||||
{
|
||||
s_mode = mode;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static int mock_set_csi_enabled(bool en)
|
||||
{
|
||||
s_csi_on = en;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static int mock_set_capture_profile(uint8_t profile_id)
|
||||
{
|
||||
if (profile_id >= RV_PROFILE_COUNT) return ESP_ERR_INVALID_ARG;
|
||||
s_profile = profile_id;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static int mock_get_health(rv_radio_health_t *out)
|
||||
{
|
||||
if (out == NULL) return ESP_ERR_INVALID_ARG;
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
/* Mock yield: mirror mock_csi's generator rate so the adaptive
|
||||
* controller sees a sensible pkt_yield in QEMU. */
|
||||
out->pkt_yield_per_sec = 20; /* MOCK_CSI_INTERVAL_MS = 50 → 20 Hz */
|
||||
out->rssi_median_dbm = -55;
|
||||
out->noise_floor_dbm = -95;
|
||||
out->current_channel = s_channel;
|
||||
out->current_bw_mhz = s_bw;
|
||||
out->current_profile = s_profile;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static const rv_radio_ops_t s_mock_ops = {
|
||||
.init = mock_init,
|
||||
.set_channel = mock_set_channel,
|
||||
.set_mode = mock_set_mode,
|
||||
.set_csi_enabled = mock_set_csi_enabled,
|
||||
.set_capture_profile = mock_set_capture_profile,
|
||||
.get_health = mock_get_health,
|
||||
};
|
||||
|
||||
void rv_radio_ops_mock_register(void)
|
||||
{
|
||||
rv_radio_ops_register(&s_mock_ops);
|
||||
ESP_LOGI(TAG, "mock radio ops registered (QEMU / offline mode)");
|
||||
}
|
||||
|
||||
#endif /* CONFIG_CSI_MOCK_ENABLED */
|
||||
@@ -31,7 +31,3 @@ CONFIG_LWIP_SO_RCVBUF=y
|
||||
|
||||
# FreeRTOS: increase task stack for CSI processing
|
||||
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
|
||||
|
||||
# ADR-081: adaptive_controller runs emit_feature_state + stream_sender
|
||||
# network I/O inside Timer Svc callbacks, exceeding the 2 KiB default.
|
||||
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=8192
|
||||
|
||||
@@ -27,7 +27,3 @@ CONFIG_LOG_DEFAULT_LEVEL_INFO=y
|
||||
|
||||
CONFIG_LWIP_SO_RCVBUF=y
|
||||
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
|
||||
|
||||
# ADR-081: adaptive_controller runs emit_feature_state + stream_sender
|
||||
# network I/O inside Timer Svc callbacks, exceeding the 2 KiB default.
|
||||
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=8192
|
||||
|
||||
@@ -31,7 +31,3 @@ CONFIG_LWIP_SO_RCVBUF=y
|
||||
|
||||
# FreeRTOS: increase task stack for CSI processing
|
||||
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
|
||||
|
||||
# ADR-081: adaptive_controller runs emit_feature_state + stream_sender
|
||||
# network I/O inside Timer Svc callbacks, exceeding the 2 KiB default.
|
||||
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=8192
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# Compiled host-test binaries
|
||||
test_adaptive_controller
|
||||
test_rv_feature_state
|
||||
test_rv_mesh
|
||||
*.o
|
||||
@@ -1,59 +0,0 @@
|
||||
# Host-side unit tests for ADR-081 pure-C logic.
|
||||
#
|
||||
# These tests exercise adaptive_controller_decide() and the rv_feature_state
|
||||
# helpers (CRC32, finalize) using plain gcc/clang, with a minimal esp_err.h
|
||||
# shim. No ESP-IDF, no FreeRTOS, no QEMU required.
|
||||
#
|
||||
# Usage:
|
||||
# cd firmware/esp32-csi-node/tests/host
|
||||
# make
|
||||
# ./test_adaptive_controller
|
||||
# ./test_rv_feature_state
|
||||
|
||||
MAIN_DIR := ../../main
|
||||
CC ?= cc
|
||||
CFLAGS ?= -O2 -std=c11 -Wall -Wextra -Wno-unused-parameter \
|
||||
-D_POSIX_C_SOURCE=199309L \
|
||||
-I. -I$(MAIN_DIR)
|
||||
LDLIBS ?= -lrt
|
||||
|
||||
# Pure-C sources under test. We compile only the files that have no
|
||||
# ESP-IDF dependency in their bodies: rv_feature_state.c is 100% pure.
|
||||
# adaptive_controller.c uses FreeRTOS for the timer plumbing, so for the
|
||||
# host test we compile only the decide() portion by isolating it in a
|
||||
# small unity file (TEST_ADAPT_PURE below).
|
||||
FEATURE_STATE_SRCS := $(MAIN_DIR)/rv_feature_state.c
|
||||
|
||||
# adaptive_controller.c pulls in FreeRTOS headers that don't exist on
|
||||
# host; we include its decide() function by defining TEST_ADAPT_PURE
|
||||
# before including the .c. The decide() body itself has no ESP-IDF deps.
|
||||
# Simpler: just recompile decide() here via a small shim.
|
||||
|
||||
TESTS := test_adaptive_controller test_rv_feature_state test_rv_mesh
|
||||
|
||||
all: $(TESTS)
|
||||
|
||||
test_adaptive_controller: test_adaptive_controller.c $(MAIN_DIR)/adaptive_controller_decide.c $(MAIN_DIR)/adaptive_controller.h $(MAIN_DIR)/rv_radio_ops.h
|
||||
$(CC) $(CFLAGS) test_adaptive_controller.c $(MAIN_DIR)/adaptive_controller_decide.c -o $@ $(LDLIBS)
|
||||
|
||||
test_rv_feature_state: test_rv_feature_state.c $(FEATURE_STATE_SRCS) $(MAIN_DIR)/rv_feature_state.h $(MAIN_DIR)/rv_radio_ops.h
|
||||
$(CC) $(CFLAGS) test_rv_feature_state.c $(FEATURE_STATE_SRCS) -o $@ $(LDLIBS)
|
||||
|
||||
# Mesh plane encoder/decoder: compile rv_mesh.c with RV_MESH_HOST_TEST
|
||||
# so the firmware-only send helpers (stream_sender, esp_log) are hidden.
|
||||
test_rv_mesh: test_rv_mesh.c $(MAIN_DIR)/rv_mesh.c $(MAIN_DIR)/rv_mesh.h $(FEATURE_STATE_SRCS) $(MAIN_DIR)/rv_radio_ops.h
|
||||
$(CC) $(CFLAGS) -DRV_MESH_HOST_TEST=1 \
|
||||
test_rv_mesh.c $(MAIN_DIR)/rv_mesh.c $(FEATURE_STATE_SRCS) \
|
||||
-o $@ $(LDLIBS)
|
||||
|
||||
check: all
|
||||
./test_adaptive_controller
|
||||
@echo ""
|
||||
./test_rv_feature_state
|
||||
@echo ""
|
||||
./test_rv_mesh
|
||||
|
||||
clean:
|
||||
rm -f $(TESTS) *.o
|
||||
|
||||
.PHONY: all check clean
|
||||
@@ -1,19 +0,0 @@
|
||||
/* Host test shim for esp_err.h. Allows us to compile the pure-C
|
||||
* portions of the firmware (adaptive_controller_decide, rv_feature_state
|
||||
* CRC + finalize) under plain gcc/clang without the ESP-IDF toolchain. */
|
||||
#ifndef HOST_ESP_ERR_SHIM_H
|
||||
#define HOST_ESP_ERR_SHIM_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef int esp_err_t;
|
||||
|
||||
#define ESP_OK 0
|
||||
#define ESP_FAIL -1
|
||||
#define ESP_ERR_NO_MEM 0x101
|
||||
#define ESP_ERR_INVALID_ARG 0x102
|
||||
#define ESP_ERR_INVALID_SIZE 0x104
|
||||
#define ESP_ERR_INVALID_VERSION 0x10A
|
||||
#define ESP_ERR_INVALID_CRC 0x10B
|
||||
|
||||
#endif
|
||||
@@ -1,216 +0,0 @@
|
||||
/*
|
||||
* Host unit test for adaptive_controller_decide().
|
||||
*
|
||||
* The ADR-081 controller decision function is deliberately pure: it takes
|
||||
* (cfg, current_state, observation) and produces a decision. No FreeRTOS,
|
||||
* no ESP-IDF, no side effects. This test exercises every documented branch
|
||||
* of the policy.
|
||||
*
|
||||
* Build + run (from this directory):
|
||||
* make -f Makefile
|
||||
* ./test_adaptive_controller
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "adaptive_controller.h"
|
||||
#include "rv_radio_ops.h"
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
|
||||
#define CHECK(cond, msg) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf(" FAIL: %s (line %d)\n", msg, __LINE__); } \
|
||||
} while (0)
|
||||
|
||||
static adapt_config_t default_cfg(void) {
|
||||
adapt_config_t c = {
|
||||
.fast_loop_ms = 200,
|
||||
.medium_loop_ms = 1000,
|
||||
.slow_loop_ms = 30000,
|
||||
.aggressive = false,
|
||||
.enable_channel_switch = false,
|
||||
.enable_role_change = false,
|
||||
.motion_threshold = 0.20f,
|
||||
.anomaly_threshold = 0.60f,
|
||||
.min_pkt_yield = 5,
|
||||
};
|
||||
return c;
|
||||
}
|
||||
|
||||
static adapt_observation_t quiet_obs(void) {
|
||||
adapt_observation_t o = {
|
||||
.pkt_yield_per_sec = 50,
|
||||
.send_fail_count = 0,
|
||||
.rssi_median_dbm = -60,
|
||||
.noise_floor_dbm = -95,
|
||||
.motion_score = 0.01f,
|
||||
.presence_score = 0.0f,
|
||||
.anomaly_score = 0.0f,
|
||||
.node_coherence = 1.0f,
|
||||
};
|
||||
return o;
|
||||
}
|
||||
|
||||
static void test_degraded_gate_on_pkt_yield_collapse(void) {
|
||||
printf("test: degraded gate on pkt yield collapse\n");
|
||||
adapt_config_t cfg = default_cfg();
|
||||
adapt_observation_t obs = quiet_obs();
|
||||
obs.pkt_yield_per_sec = 2; /* below min_pkt_yield=5 */
|
||||
|
||||
adapt_decision_t dec;
|
||||
adaptive_controller_decide(&cfg, ADAPT_STATE_SENSE_IDLE, &obs, &dec);
|
||||
|
||||
CHECK(dec.change_state, "should change state");
|
||||
CHECK(dec.new_state == ADAPT_STATE_DEGRADED, "new state == DEGRADED");
|
||||
CHECK(dec.new_profile == RV_PROFILE_PASSIVE_LOW_RATE,
|
||||
"profile pinned to PASSIVE_LOW_RATE in degraded");
|
||||
CHECK(dec.suggested_vital_interval_ms == 2000,
|
||||
"cadence relaxed to 2s in degraded");
|
||||
}
|
||||
|
||||
static void test_degraded_gate_on_coherence_loss(void) {
|
||||
printf("test: degraded gate on coherence loss\n");
|
||||
adapt_config_t cfg = default_cfg();
|
||||
adapt_observation_t obs = quiet_obs();
|
||||
obs.node_coherence = 0.15f; /* below 0.20 threshold */
|
||||
|
||||
adapt_decision_t dec;
|
||||
adaptive_controller_decide(&cfg, ADAPT_STATE_SENSE_IDLE, &obs, &dec);
|
||||
CHECK(dec.new_state == ADAPT_STATE_DEGRADED, "coherence loss → DEGRADED");
|
||||
}
|
||||
|
||||
static void test_anomaly_trumps_motion(void) {
|
||||
printf("test: anomaly trumps motion\n");
|
||||
adapt_config_t cfg = default_cfg();
|
||||
adapt_observation_t obs = quiet_obs();
|
||||
obs.motion_score = 0.9f; /* high motion */
|
||||
obs.anomaly_score = 0.8f; /* but anomaly is above threshold */
|
||||
|
||||
adapt_decision_t dec;
|
||||
adaptive_controller_decide(&cfg, ADAPT_STATE_SENSE_IDLE, &obs, &dec);
|
||||
|
||||
CHECK(dec.new_state == ADAPT_STATE_ALERT, "anomaly → ALERT");
|
||||
CHECK(dec.new_profile == RV_PROFILE_FAST_MOTION,
|
||||
"alert uses FAST_MOTION profile");
|
||||
CHECK(dec.suggested_vital_interval_ms == 100, "alert cadence 100ms");
|
||||
}
|
||||
|
||||
static void test_motion_triggers_sense_active(void) {
|
||||
printf("test: motion → SENSE_ACTIVE\n");
|
||||
adapt_config_t cfg = default_cfg();
|
||||
adapt_observation_t obs = quiet_obs();
|
||||
obs.motion_score = 0.50f;
|
||||
|
||||
adapt_decision_t dec;
|
||||
adaptive_controller_decide(&cfg, ADAPT_STATE_SENSE_IDLE, &obs, &dec);
|
||||
|
||||
CHECK(dec.new_state == ADAPT_STATE_SENSE_ACTIVE, "motion → SENSE_ACTIVE");
|
||||
CHECK(dec.new_profile == RV_PROFILE_FAST_MOTION, "profile FAST_MOTION");
|
||||
CHECK(dec.suggested_vital_interval_ms == 200,
|
||||
"non-aggressive cadence 200ms");
|
||||
}
|
||||
|
||||
static void test_aggressive_cadence(void) {
|
||||
printf("test: aggressive cadence is tighter\n");
|
||||
adapt_config_t cfg = default_cfg();
|
||||
cfg.aggressive = true;
|
||||
adapt_observation_t obs = quiet_obs();
|
||||
obs.motion_score = 0.50f;
|
||||
|
||||
adapt_decision_t dec;
|
||||
adaptive_controller_decide(&cfg, ADAPT_STATE_SENSE_IDLE, &obs, &dec);
|
||||
CHECK(dec.suggested_vital_interval_ms == 100,
|
||||
"aggressive motion cadence 100ms");
|
||||
}
|
||||
|
||||
static void test_stable_presence_uses_resp_high_sens(void) {
|
||||
printf("test: stable presence → RESP_HIGH_SENS\n");
|
||||
adapt_config_t cfg = default_cfg();
|
||||
adapt_observation_t obs = quiet_obs();
|
||||
obs.presence_score = 0.8f;
|
||||
obs.motion_score = 0.01f;
|
||||
|
||||
adapt_decision_t dec;
|
||||
adaptive_controller_decide(&cfg, ADAPT_STATE_SENSE_IDLE, &obs, &dec);
|
||||
CHECK(dec.new_profile == RV_PROFILE_RESP_HIGH_SENS,
|
||||
"stable presence uses respiration profile");
|
||||
CHECK(dec.suggested_vital_interval_ms == 1000,
|
||||
"respiration cadence 1s");
|
||||
}
|
||||
|
||||
static void test_empty_room_default_is_passive(void) {
|
||||
printf("test: empty room → PASSIVE_LOW_RATE\n");
|
||||
adapt_config_t cfg = default_cfg();
|
||||
adapt_observation_t obs = quiet_obs();
|
||||
|
||||
adapt_decision_t dec;
|
||||
adaptive_controller_decide(&cfg, ADAPT_STATE_SENSE_IDLE, &obs, &dec);
|
||||
CHECK(dec.new_profile == RV_PROFILE_PASSIVE_LOW_RATE,
|
||||
"empty → passive low rate");
|
||||
}
|
||||
|
||||
static void test_hysteresis_no_flap(void) {
|
||||
printf("test: no change_state when already in target state\n");
|
||||
adapt_config_t cfg = default_cfg();
|
||||
adapt_observation_t obs = quiet_obs();
|
||||
obs.motion_score = 0.50f;
|
||||
|
||||
adapt_decision_t dec;
|
||||
adaptive_controller_decide(&cfg, ADAPT_STATE_SENSE_ACTIVE, &obs, &dec);
|
||||
CHECK(!dec.change_state,
|
||||
"already in SENSE_ACTIVE — no redundant change_state");
|
||||
}
|
||||
|
||||
static void test_null_safety(void) {
|
||||
printf("test: NULL args are no-ops (no crash)\n");
|
||||
adapt_decision_t dec = {0};
|
||||
adaptive_controller_decide(NULL, ADAPT_STATE_SENSE_IDLE, NULL, &dec);
|
||||
/* if we got here, no segfault — pass */
|
||||
g_pass++;
|
||||
printf(" OK\n");
|
||||
}
|
||||
|
||||
static void benchmark_decide(void) {
|
||||
printf("bench: adaptive_controller_decide() throughput\n");
|
||||
adapt_config_t cfg = default_cfg();
|
||||
adapt_observation_t obs = quiet_obs();
|
||||
adapt_decision_t dec;
|
||||
|
||||
const int N = 10000000;
|
||||
struct timespec a, b;
|
||||
clock_gettime(CLOCK_MONOTONIC, &a);
|
||||
for (int i = 0; i < N; i++) {
|
||||
/* Vary input slightly so the compiler can't fold the call. */
|
||||
obs.motion_score = (i & 0xff) / 255.0f;
|
||||
adaptive_controller_decide(&cfg, ADAPT_STATE_SENSE_IDLE, &obs, &dec);
|
||||
}
|
||||
clock_gettime(CLOCK_MONOTONIC, &b);
|
||||
double ns_per_call = ((b.tv_sec - a.tv_sec) * 1e9 +
|
||||
(b.tv_nsec - a.tv_nsec)) / (double)N;
|
||||
printf(" %d calls, %.1f ns/call\n", N, ns_per_call);
|
||||
/* Sanity: decide() is O(constant) — must be under 10us even on a
|
||||
* slow emulator. Real ESP32 will be ~100-300ns. */
|
||||
CHECK(ns_per_call < 10000.0, "decide() must be under 10us/call");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("=== adaptive_controller_decide() host tests ===\n\n");
|
||||
|
||||
test_degraded_gate_on_pkt_yield_collapse();
|
||||
test_degraded_gate_on_coherence_loss();
|
||||
test_anomaly_trumps_motion();
|
||||
test_motion_triggers_sense_active();
|
||||
test_aggressive_cadence();
|
||||
test_stable_presence_uses_resp_high_sens();
|
||||
test_empty_room_default_is_passive();
|
||||
test_hysteresis_no_flap();
|
||||
test_null_safety();
|
||||
benchmark_decide();
|
||||
|
||||
printf("\n=== result: %d pass, %d fail ===\n", g_pass, g_fail);
|
||||
return g_fail > 0 ? 1 : 0;
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
/*
|
||||
* Host unit test for rv_feature_state_* helpers.
|
||||
*
|
||||
* Validates:
|
||||
* - Packet layout is exactly 80 bytes
|
||||
* - IEEE CRC32 matches well-known reference vectors
|
||||
* - finalize() populates magic/seq/ts/crc correctly
|
||||
* - CRC32 throughput benchmark
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "rv_feature_state.h"
|
||||
#include "rv_radio_ops.h"
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
#define CHECK(cond, msg) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf(" FAIL: %s (line %d)\n", msg, __LINE__); } \
|
||||
} while (0)
|
||||
|
||||
static void test_packet_size(void) {
|
||||
printf("test: rv_feature_state_t is 60 bytes on the wire\n");
|
||||
CHECK(sizeof(rv_feature_state_t) == 60, "sizeof == 60");
|
||||
}
|
||||
|
||||
static void test_crc_known_vectors(void) {
|
||||
printf("test: IEEE CRC32 known vectors\n");
|
||||
/* IEEE CRC32 of "123456789" == 0xCBF43926 (well-known). */
|
||||
uint32_t c1 = rv_feature_state_crc32((const uint8_t *)"123456789", 9);
|
||||
CHECK(c1 == 0xCBF43926u, "CRC32('123456789') == 0xCBF43926");
|
||||
|
||||
/* Empty input → 0x00000000 (before final inversion, 0xFFFFFFFF);
|
||||
* IEEE convention with post-invert → 0x00000000 reversed — but with
|
||||
* our implementation the empty-input CRC is 0x00000000 after post-
|
||||
* invert on ~0xFFFFFFFF = 0x00000000. */
|
||||
uint32_t c2 = rv_feature_state_crc32(NULL, 0);
|
||||
CHECK(c2 == 0x00000000u, "CRC32(empty) == 0");
|
||||
|
||||
/* Single zero byte: IEEE CRC32 of 0x00 = 0xD202EF8D. */
|
||||
uint8_t zero = 0;
|
||||
uint32_t c3 = rv_feature_state_crc32(&zero, 1);
|
||||
CHECK(c3 == 0xD202EF8Du, "CRC32(0x00) == 0xD202EF8D");
|
||||
}
|
||||
|
||||
static void test_finalize(void) {
|
||||
printf("test: finalize populates required fields\n");
|
||||
rv_feature_state_t pkt;
|
||||
memset(&pkt, 0, sizeof(pkt));
|
||||
pkt.motion_score = 0.25f;
|
||||
pkt.presence_score = 0.75f;
|
||||
pkt.respiration_bpm = 14.5f;
|
||||
pkt.quality_flags = RV_QFLAG_PRESENCE_VALID | RV_QFLAG_RESPIRATION_VALID;
|
||||
|
||||
rv_feature_state_finalize(&pkt, /*node*/ 7, /*seq*/ 42,
|
||||
/*ts*/ 1234567ULL, RV_PROFILE_RESP_HIGH_SENS);
|
||||
|
||||
CHECK(pkt.magic == RV_FEATURE_STATE_MAGIC, "magic");
|
||||
CHECK(pkt.node_id == 7, "node_id");
|
||||
CHECK(pkt.seq == 42, "seq");
|
||||
CHECK(pkt.ts_us == 1234567ULL, "ts_us");
|
||||
CHECK(pkt.mode == RV_PROFILE_RESP_HIGH_SENS, "mode");
|
||||
CHECK(pkt.reserved == 0, "reserved cleared");
|
||||
CHECK(pkt.crc32 != 0, "crc32 populated (non-trivial input)");
|
||||
|
||||
/* Re-finalize must produce identical CRC (deterministic). */
|
||||
uint32_t crc1 = pkt.crc32;
|
||||
rv_feature_state_finalize(&pkt, 7, 42, 1234567ULL, RV_PROFILE_RESP_HIGH_SENS);
|
||||
CHECK(pkt.crc32 == crc1, "finalize is deterministic");
|
||||
|
||||
/* Changing a payload byte must change the CRC. */
|
||||
pkt.motion_score = 0.26f;
|
||||
rv_feature_state_finalize(&pkt, 7, 42, 1234567ULL, RV_PROFILE_RESP_HIGH_SENS);
|
||||
CHECK(pkt.crc32 != crc1, "CRC changes when payload changes");
|
||||
}
|
||||
|
||||
static void test_crc_verifiability(void) {
|
||||
printf("test: receiver can verify CRC\n");
|
||||
rv_feature_state_t pkt;
|
||||
memset(&pkt, 0, sizeof(pkt));
|
||||
pkt.motion_score = 0.33f;
|
||||
pkt.presence_score = 0.66f;
|
||||
rv_feature_state_finalize(&pkt, 1, 100, 555ULL, RV_PROFILE_PASSIVE_LOW_RATE);
|
||||
|
||||
/* Receiver recomputes CRC over all bytes except the trailing crc32. */
|
||||
uint32_t expected = rv_feature_state_crc32(
|
||||
(const uint8_t *)&pkt, sizeof(pkt) - sizeof(uint32_t));
|
||||
CHECK(pkt.crc32 == expected, "receiver-side CRC check matches");
|
||||
}
|
||||
|
||||
static void benchmark_crc(void) {
|
||||
printf("bench: CRC32 over 60-byte packet (56 B hashed, excl trailing crc32)\n");
|
||||
rv_feature_state_t pkt;
|
||||
memset(&pkt, 0x5A, sizeof(pkt));
|
||||
|
||||
const int N = 5000000;
|
||||
struct timespec a, b;
|
||||
clock_gettime(CLOCK_MONOTONIC, &a);
|
||||
volatile uint32_t sink = 0;
|
||||
for (int i = 0; i < N; i++) {
|
||||
pkt.seq = (uint16_t)i; /* vary input so compiler can't fold */
|
||||
sink ^= rv_feature_state_crc32(
|
||||
(const uint8_t *)&pkt, sizeof(pkt) - sizeof(uint32_t));
|
||||
}
|
||||
clock_gettime(CLOCK_MONOTONIC, &b);
|
||||
(void)sink;
|
||||
double ns_per_call = ((b.tv_sec - a.tv_sec) * 1e9 +
|
||||
(b.tv_nsec - a.tv_nsec)) / (double)N;
|
||||
double mb_per_sec = (double)(sizeof(pkt) - sizeof(uint32_t)) / ns_per_call
|
||||
* 1e9 / (1024.0 * 1024.0);
|
||||
printf(" %d calls, %.1f ns/packet, %.1f MB/s\n",
|
||||
N, ns_per_call, mb_per_sec);
|
||||
/* At 10 Hz feature-state cadence, CRC budget is <100us/packet — we
|
||||
* expect bit-by-bit CRC32 to run ~1 MB/s on host, ~100-300 KB/s on
|
||||
* ESP32-S3 Xtensa LX7. 76-byte CRC takes <1 ms either way. */
|
||||
CHECK(ns_per_call < 50000.0, "CRC32(80B) must be under 50us/packet");
|
||||
}
|
||||
|
||||
static void benchmark_finalize(void) {
|
||||
printf("bench: full finalize() cost\n");
|
||||
rv_feature_state_t pkt;
|
||||
memset(&pkt, 0x33, sizeof(pkt));
|
||||
|
||||
const int N = 5000000;
|
||||
struct timespec a, b;
|
||||
clock_gettime(CLOCK_MONOTONIC, &a);
|
||||
for (int i = 0; i < N; i++) {
|
||||
rv_feature_state_finalize(&pkt, 1, (uint16_t)i, (uint64_t)i,
|
||||
RV_PROFILE_PASSIVE_LOW_RATE);
|
||||
}
|
||||
clock_gettime(CLOCK_MONOTONIC, &b);
|
||||
double ns_per_call = ((b.tv_sec - a.tv_sec) * 1e9 +
|
||||
(b.tv_nsec - a.tv_nsec)) / (double)N;
|
||||
printf(" %d calls, %.1f ns/call (includes CRC)\n", N, ns_per_call);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("=== rv_feature_state_* host tests ===\n\n");
|
||||
|
||||
test_packet_size();
|
||||
test_crc_known_vectors();
|
||||
test_finalize();
|
||||
test_crc_verifiability();
|
||||
benchmark_crc();
|
||||
benchmark_finalize();
|
||||
|
||||
printf("\n=== result: %d pass, %d fail ===\n", g_pass, g_fail);
|
||||
return g_fail > 0 ? 1 : 0;
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
/*
|
||||
* Host unit test for ADR-081 Layer 3 mesh plane encode/decode.
|
||||
*
|
||||
* rv_mesh_encode() and rv_mesh_decode() are the pure halves of the
|
||||
* mesh plane — no ESP-IDF, no sockets — so we exercise them with the
|
||||
* RV_MESH_HOST_TEST flag that disables the send helpers.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "rv_mesh.h"
|
||||
#include "rv_feature_state.h"
|
||||
#include "rv_radio_ops.h" /* for RV_PROFILE_* enum values */
|
||||
|
||||
static int g_pass = 0, g_fail = 0;
|
||||
#define CHECK(cond, msg) do { \
|
||||
if (cond) { g_pass++; } \
|
||||
else { g_fail++; printf(" FAIL: %s (line %d)\n", msg, __LINE__); } \
|
||||
} while (0)
|
||||
|
||||
static void test_header_size(void) {
|
||||
printf("test: rv_mesh_header_t is 16 bytes\n");
|
||||
CHECK(sizeof(rv_mesh_header_t) == 16, "sizeof(header) == 16");
|
||||
}
|
||||
|
||||
static void test_encode_health_roundtrip(void) {
|
||||
printf("test: HEALTH roundtrip\n");
|
||||
rv_node_status_t st;
|
||||
memset(&st, 0, sizeof(st));
|
||||
st.node_id[0] = 7;
|
||||
st.local_time_us = 1234567890ULL;
|
||||
st.role = RV_ROLE_OBSERVER;
|
||||
st.current_channel = 6;
|
||||
st.current_bw = 20;
|
||||
st.noise_floor_dbm = -93;
|
||||
st.pkt_yield = 42;
|
||||
st.sync_error_us = 12;
|
||||
|
||||
uint8_t buf[RV_MESH_MAX_FRAME_BYTES];
|
||||
size_t n = rv_mesh_encode_health(RV_ROLE_OBSERVER, /*epoch*/ 100,
|
||||
&st, buf, sizeof(buf));
|
||||
CHECK(n > 0, "encode returns non-zero");
|
||||
CHECK(n == sizeof(rv_mesh_header_t) + sizeof(st) + 4,
|
||||
"encoded size = hdr+payload+crc");
|
||||
|
||||
rv_mesh_header_t hdr;
|
||||
const uint8_t *payload = NULL;
|
||||
uint16_t payload_len = 0;
|
||||
esp_err_t rc = rv_mesh_decode(buf, n, &hdr, &payload, &payload_len);
|
||||
CHECK(rc == ESP_OK, "decode OK");
|
||||
CHECK(hdr.type == RV_MSG_HEALTH, "type == HEALTH");
|
||||
CHECK(hdr.epoch == 100, "epoch survives");
|
||||
CHECK(hdr.payload_len == sizeof(st), "payload_len matches");
|
||||
CHECK(payload != NULL, "payload pointer set");
|
||||
CHECK(memcmp(payload, &st, sizeof(st)) == 0, "payload bytes match");
|
||||
}
|
||||
|
||||
static void test_encode_anomaly_roundtrip(void) {
|
||||
printf("test: ANOMALY_ALERT roundtrip\n");
|
||||
rv_anomaly_alert_t a;
|
||||
memset(&a, 0, sizeof(a));
|
||||
a.node_id[0] = 3;
|
||||
a.ts_us = 999999ULL;
|
||||
a.reason = RV_ANOMALY_FALL;
|
||||
a.severity = 200;
|
||||
a.anomaly_score = 0.85f;
|
||||
a.motion_score = 0.9f;
|
||||
|
||||
uint8_t buf[RV_MESH_MAX_FRAME_BYTES];
|
||||
size_t n = rv_mesh_encode_anomaly_alert(RV_ROLE_OBSERVER, 7, &a,
|
||||
buf, sizeof(buf));
|
||||
CHECK(n > 0, "encoded");
|
||||
|
||||
rv_mesh_header_t hdr;
|
||||
const uint8_t *payload = NULL;
|
||||
uint16_t payload_len = 0;
|
||||
esp_err_t rc = rv_mesh_decode(buf, n, &hdr, &payload, &payload_len);
|
||||
CHECK(rc == ESP_OK, "decoded");
|
||||
CHECK(hdr.type == RV_MSG_ANOMALY_ALERT, "type ok");
|
||||
rv_anomaly_alert_t got;
|
||||
memcpy(&got, payload, sizeof(got));
|
||||
CHECK(got.reason == RV_ANOMALY_FALL, "reason survived");
|
||||
CHECK(got.severity == 200, "severity survived");
|
||||
}
|
||||
|
||||
static void test_encode_feature_delta_wraps_feature_state(void) {
|
||||
printf("test: FEATURE_DELTA wraps rv_feature_state_t\n");
|
||||
rv_feature_state_t fs;
|
||||
memset(&fs, 0, sizeof(fs));
|
||||
fs.motion_score = 0.5f;
|
||||
rv_feature_state_finalize(&fs, /*node*/ 9, /*seq*/ 17,
|
||||
/*ts*/ 111ULL, RV_PROFILE_FAST_MOTION);
|
||||
|
||||
uint8_t buf[RV_MESH_MAX_FRAME_BYTES];
|
||||
size_t n = rv_mesh_encode_feature_delta(RV_ROLE_OBSERVER, 2, &fs,
|
||||
buf, sizeof(buf));
|
||||
CHECK(n == sizeof(rv_mesh_header_t) + sizeof(fs) + 4, "size check");
|
||||
|
||||
rv_mesh_header_t hdr;
|
||||
const uint8_t *payload = NULL;
|
||||
uint16_t len = 0;
|
||||
CHECK(rv_mesh_decode(buf, n, &hdr, &payload, &len) == ESP_OK,
|
||||
"decode OK");
|
||||
rv_feature_state_t got;
|
||||
memcpy(&got, payload, sizeof(got));
|
||||
CHECK(got.magic == RV_FEATURE_STATE_MAGIC, "inner magic preserved");
|
||||
CHECK(got.node_id == 9, "inner node_id preserved");
|
||||
CHECK(got.seq == 17, "inner seq preserved");
|
||||
/* Inner CRC is end-to-end even though the mesh frame has its own
|
||||
* CRC too — two checks for two failure modes. */
|
||||
uint32_t inner_crc = rv_feature_state_crc32(
|
||||
(const uint8_t *)&got, sizeof(got) - sizeof(uint32_t));
|
||||
CHECK(inner_crc == got.crc32, "inner feature_state CRC still valid");
|
||||
}
|
||||
|
||||
static void test_decode_rejects_bad_magic(void) {
|
||||
printf("test: decode rejects bad magic\n");
|
||||
uint8_t buf[sizeof(rv_mesh_header_t) + 4];
|
||||
memset(buf, 0xFF, sizeof(buf));
|
||||
|
||||
rv_mesh_header_t hdr;
|
||||
const uint8_t *p = NULL;
|
||||
uint16_t plen = 0;
|
||||
esp_err_t rc = rv_mesh_decode(buf, sizeof(buf), &hdr, &p, &plen);
|
||||
CHECK(rc != ESP_OK, "bad magic rejected");
|
||||
}
|
||||
|
||||
static void test_decode_rejects_truncated(void) {
|
||||
printf("test: decode rejects truncated frame\n");
|
||||
uint8_t buf[sizeof(rv_mesh_header_t) - 1];
|
||||
memset(buf, 0, sizeof(buf));
|
||||
rv_mesh_header_t hdr;
|
||||
const uint8_t *p = NULL;
|
||||
uint16_t plen = 0;
|
||||
esp_err_t rc = rv_mesh_decode(buf, sizeof(buf), &hdr, &p, &plen);
|
||||
CHECK(rc != ESP_OK, "truncated rejected");
|
||||
}
|
||||
|
||||
static void test_decode_rejects_bad_crc(void) {
|
||||
printf("test: decode rejects CRC mismatch\n");
|
||||
rv_node_status_t st;
|
||||
memset(&st, 0, sizeof(st));
|
||||
st.role = RV_ROLE_OBSERVER;
|
||||
uint8_t buf[RV_MESH_MAX_FRAME_BYTES];
|
||||
size_t n = rv_mesh_encode_health(RV_ROLE_OBSERVER, 1, &st,
|
||||
buf, sizeof(buf));
|
||||
CHECK(n > 0, "encoded");
|
||||
|
||||
/* Flip a byte in the payload — CRC must now mismatch. */
|
||||
buf[sizeof(rv_mesh_header_t) + 4] ^= 0x10;
|
||||
|
||||
rv_mesh_header_t hdr;
|
||||
const uint8_t *p = NULL;
|
||||
uint16_t plen = 0;
|
||||
esp_err_t rc = rv_mesh_decode(buf, n, &hdr, &p, &plen);
|
||||
CHECK(rc != ESP_OK, "CRC mismatch rejected");
|
||||
}
|
||||
|
||||
static void test_encode_rejects_oversize_payload(void) {
|
||||
printf("test: encode rejects oversize payload\n");
|
||||
uint8_t junk[RV_MESH_MAX_PAYLOAD + 1] = {0};
|
||||
uint8_t buf[RV_MESH_MAX_FRAME_BYTES + 8];
|
||||
size_t n = rv_mesh_encode(RV_MSG_HEALTH, RV_ROLE_OBSERVER, RV_AUTH_NONE,
|
||||
0, junk, sizeof(junk), buf, sizeof(buf));
|
||||
CHECK(n == 0, "oversize payload → 0");
|
||||
}
|
||||
|
||||
static void test_encode_rejects_small_buf(void) {
|
||||
printf("test: encode rejects too-small buffer\n");
|
||||
rv_node_status_t st = {0};
|
||||
uint8_t buf[16]; /* header fits but not payload */
|
||||
size_t n = rv_mesh_encode_health(RV_ROLE_OBSERVER, 0, &st,
|
||||
buf, sizeof(buf));
|
||||
CHECK(n == 0, "small buf → 0");
|
||||
}
|
||||
|
||||
static void benchmark_encode(void) {
|
||||
printf("bench: encode+decode HEALTH roundtrip\n");
|
||||
rv_node_status_t st;
|
||||
memset(&st, 0x33, sizeof(st));
|
||||
uint8_t buf[RV_MESH_MAX_FRAME_BYTES];
|
||||
|
||||
const int N = 2000000;
|
||||
struct timespec a, b;
|
||||
clock_gettime(CLOCK_MONOTONIC, &a);
|
||||
for (int i = 0; i < N; i++) {
|
||||
st.pkt_yield = (uint16_t)i;
|
||||
size_t n = rv_mesh_encode_health(RV_ROLE_OBSERVER, (uint32_t)i,
|
||||
&st, buf, sizeof(buf));
|
||||
rv_mesh_header_t hdr;
|
||||
const uint8_t *p = NULL;
|
||||
uint16_t plen = 0;
|
||||
(void)rv_mesh_decode(buf, n, &hdr, &p, &plen);
|
||||
}
|
||||
clock_gettime(CLOCK_MONOTONIC, &b);
|
||||
double ns = ((b.tv_sec - a.tv_sec) * 1e9 +
|
||||
(b.tv_nsec - a.tv_nsec)) / (double)N;
|
||||
printf(" %d roundtrips, %.1f ns/call\n", N, ns);
|
||||
CHECK(ns < 20000.0, "encode+decode must be under 20us/roundtrip");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("=== rv_mesh encode/decode host tests ===\n\n");
|
||||
test_header_size();
|
||||
test_encode_health_roundtrip();
|
||||
test_encode_anomaly_roundtrip();
|
||||
test_encode_feature_delta_wraps_feature_state();
|
||||
test_decode_rejects_bad_magic();
|
||||
test_decode_rejects_truncated();
|
||||
test_decode_rejects_bad_crc();
|
||||
test_encode_rejects_oversize_payload();
|
||||
test_encode_rejects_small_buf();
|
||||
benchmark_encode();
|
||||
printf("\n=== result: %d pass, %d fail ===\n", g_pass, g_fail);
|
||||
return g_fail > 0 ? 1 : 0;
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
0.6.2
|
||||
0.6.1
|
||||
|
||||
+7
-7
@@ -25,7 +25,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
RUST_DIR="${SCRIPT_DIR}/v2"
|
||||
RUST_DIR="${SCRIPT_DIR}/rust-port/wifi-densepose-rs"
|
||||
|
||||
# ─── Colors ───────────────────────────────────────────────────────────
|
||||
if [ -t 1 ]; then
|
||||
@@ -955,7 +955,7 @@ post_install() {
|
||||
;;
|
||||
rust)
|
||||
echo " # Run benchmarks:"
|
||||
echo " cd v2"
|
||||
echo " cd rust-port/wifi-densepose-rs"
|
||||
echo " cargo bench --package wifi-densepose-signal"
|
||||
echo ""
|
||||
echo " # Start Rust API server:"
|
||||
@@ -963,7 +963,7 @@ post_install() {
|
||||
;;
|
||||
browser)
|
||||
echo " # WASM package is at:"
|
||||
echo " # v2/crates/wifi-densepose-wasm/pkg/"
|
||||
echo " # rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm/pkg/"
|
||||
echo ""
|
||||
echo " # Open the 3D visualization:"
|
||||
echo " python3 -m http.server 3000 --directory ui"
|
||||
@@ -999,17 +999,17 @@ post_install() {
|
||||
echo " # WiFi-Mat disaster response module built."
|
||||
echo ""
|
||||
echo " # Run WiFi-Mat tests:"
|
||||
echo " cd v2"
|
||||
echo " cd rust-port/wifi-densepose-rs"
|
||||
echo " cargo test --package wifi-densepose-mat"
|
||||
echo ""
|
||||
echo " # Field deployment WASM package at:"
|
||||
echo " # v2/crates/wifi-densepose-wasm/pkg/"
|
||||
echo " # rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm/pkg/"
|
||||
;;
|
||||
full)
|
||||
echo " # Verification: ./verify"
|
||||
echo " # Python API: uvicorn v1.src.api.main:app --host 0.0.0.0 --port 8000"
|
||||
echo " # Rust API: cd v2 && cargo run --release --package wifi-densepose-api"
|
||||
echo " # Benchmarks: cd v2 && cargo bench"
|
||||
echo " # Rust API: cd rust-port/wifi-densepose-rs && cargo run --release --package wifi-densepose-api"
|
||||
echo " # Benchmarks: cd rust-port/wifi-densepose-rs && cargo bench"
|
||||
echo " # Visualization: python3 -m http.server 3000 --directory ui"
|
||||
echo " # Docker: docker compose up"
|
||||
;;
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
# Development and testing dependencies
|
||||
# Install with: pip install -r requirements.txt -r requirements-dev.txt
|
||||
|
||||
# Testing
|
||||
pytest>=7.0.0
|
||||
pytest-asyncio>=0.21.0
|
||||
pytest-mock>=3.10.0
|
||||
pytest-benchmark>=4.0.0
|
||||
|
||||
# Linting and formatting
|
||||
black>=23.0.0
|
||||
flake8>=6.0.0
|
||||
mypy>=1.0.0
|
||||
+13
-2
@@ -4,6 +4,14 @@ scipy>=1.7.0
|
||||
torch>=1.12.0
|
||||
torchvision>=0.13.0
|
||||
|
||||
# Testing dependencies
|
||||
pytest>=7.0.0
|
||||
pytest-asyncio>=0.21.0
|
||||
pytest-mock>=3.10.0
|
||||
pytest-benchmark>=4.0.0
|
||||
httpx>=0.24.0
|
||||
pydantic-settings>=2.0.0
|
||||
|
||||
# API dependencies
|
||||
fastapi>=0.95.0
|
||||
uvicorn>=0.20.0
|
||||
@@ -12,8 +20,6 @@ pydantic>=1.10.0
|
||||
python-jose[cryptography]>=3.3.0
|
||||
python-multipart>=0.0.6
|
||||
passlib[bcrypt]>=1.7.4
|
||||
httpx>=0.24.0
|
||||
pydantic-settings>=2.0.0
|
||||
|
||||
# Database dependencies
|
||||
sqlalchemy>=2.0.0
|
||||
@@ -36,3 +42,8 @@ scikit-learn>=1.2.0
|
||||
|
||||
# Monitoring dependencies
|
||||
prometheus-client>=0.16.0
|
||||
|
||||
# Development dependencies
|
||||
black>=23.0.0
|
||||
flake8>=6.0.0
|
||||
mypy>=1.0.0
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user