Compare commits

..

3 Commits

Author SHA1 Message Date
Reuven 377413e6a8 feat(desktop): v0.5.0 - Training backend with 16 Tauri commands
Implements full Rust backend for Training page (ADR-057):

Training Domain Types (domain/training.rs):
- GpuInfo, GpuBackend (Cpu, Cuda, Metal)
- DatasetInfo, DatasetFormat (MmFi, WiPose, Wiar, Custom)
- ModelInfo, ModelType (Encoder, Decoder, Embedding, Adaptor)
- CheckpointInfo, TrainingJob, TrainingConfig, TrainingProgress
- RuVectorConfig with MinCut, Attention, Temporal, Solver params
- EvaluationMetrics, JointAccuracy, EpochMetrics

Training Commands (commands/training.rs):
- detect_gpu - Auto-detect CUDA/Metal/CPU with caching
- list_datasets, get_datasets, download_dataset
- list_models, list_checkpoints, export_model (ONNX/TorchScript)
- start_training, stop_training, training_progress
- get_ruvector_config, set_ruvector_config, test_ruvector_live
- get_training_history, get_evaluation_metrics, get_joint_accuracies

State Management (state.rs):
- Added TrainingState to AppState
- GPU info caching, datasets, checkpoints, current job
- RuVector config persistence

Tests: 48 passed (27 unit + 21 integration)

Ref: ADR-057

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-10 11:57:57 -04:00
Reuven b9e36a8be0 feat(desktop): add Training page with 5 tabs (ADR-057)
Implements the Training & Models page with tabbed navigation:
- Datasets tab: Download/import datasets, preview samples
- Models tab: Browse architectures, manage checkpoints, export ONNX
- Training tab: Configure training, GPU detection, live progress
- RuVector tab: Module config (MinCut, Attention, Temporal, Solver)
- Metrics tab: Loss curves, evaluation metrics, per-joint accuracy

Features:
- GPU detection status display (CUDA/Metal)
- Live training progress with Tauri events
- RuVector module enable/disable and parameter tuning
- Training presets (Low Latency, High Accuracy, Balanced)
- Export metrics to CSV/JSON/TensorBoard
- Mock data for demonstration when backend not implemented

Ref: ADR-057

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-10 11:50:05 -04:00
Reuven 9e860c3a7a docs(adr): ADR-057 Desktop Training & RuVector Integration
Proposes a new Training page in the desktop app with tabs:
- Datasets: Download/manage training datasets (MM-Fi, Wi-Pose)
- Models: Browse architectures, load checkpoints, export ONNX
- Training: Configure and run training jobs with GPU support
- RuVector: Configure signal processing modules, live testing
- Metrics: View loss curves, evaluation results

Integrates wifi-densepose-train crate and 5 RuVector crates
into the Tauri desktop application.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-10 11:42:59 -04:00
1644 changed files with 261535 additions and 18345 deletions
+1
View File
@@ -0,0 +1 @@
31273
-370
View File
@@ -1,370 +0,0 @@
name: Firmware QEMU Tests (ADR-061)
on:
push:
paths:
- 'firmware/**'
- 'scripts/qemu-esp32s3-test.sh'
- 'scripts/validate_qemu_output.py'
- 'scripts/generate_nvs_matrix.py'
- 'scripts/qemu_swarm.py'
- 'scripts/swarm_health.py'
- 'scripts/swarm_presets/**'
- '.github/workflows/firmware-qemu.yml'
pull_request:
paths:
- 'firmware/**'
- 'scripts/qemu-esp32s3-test.sh'
- 'scripts/validate_qemu_output.py'
- 'scripts/generate_nvs_matrix.py'
- 'scripts/qemu_swarm.py'
- 'scripts/swarm_health.py'
- 'scripts/swarm_presets/**'
- '.github/workflows/firmware-qemu.yml'
env:
IDF_VERSION: "v5.4"
QEMU_REPO: "https://github.com/espressif/qemu.git"
QEMU_BRANCH: "esp-develop"
jobs:
build-qemu:
name: Build Espressif QEMU
runs-on: ubuntu-latest
steps:
- name: Cache QEMU build
id: cache-qemu
uses: actions/cache@v4
with:
path: /opt/qemu-esp32
# Include date component so cache refreshes monthly when branch updates
key: qemu-esp32s3-${{ env.QEMU_BRANCH }}-v5
restore-keys: |
qemu-esp32s3-${{ env.QEMU_BRANCH }}-
- name: Install QEMU build dependencies
if: steps.cache-qemu.outputs.cache-hit != 'true'
run: |
sudo apt-get update
sudo apt-get install -y \
git build-essential ninja-build pkg-config \
libglib2.0-dev libpixman-1-dev libslirp-dev \
libgcrypt20-dev \
python3 python3-venv
- name: Clone and build Espressif QEMU
if: steps.cache-qemu.outputs.cache-hit != 'true'
run: |
git clone --depth 1 -b "$QEMU_BRANCH" "$QEMU_REPO" /tmp/qemu-esp
cd /tmp/qemu-esp
mkdir build && cd build
../configure \
--target-list=xtensa-softmmu \
--prefix=/opt/qemu-esp32 \
--enable-slirp \
--disable-werror
ninja -j$(nproc)
ninja install
- name: Verify QEMU binary
run: |
file_size() { stat -c%s "$1" 2>/dev/null || stat -f%z "$1" 2>/dev/null || wc -c < "$1"; }
/opt/qemu-esp32/bin/qemu-system-xtensa --version
echo "QEMU binary size: $(file_size /opt/qemu-esp32/bin/qemu-system-xtensa) bytes"
- name: Upload QEMU artifact
uses: actions/upload-artifact@v4
with:
name: qemu-esp32
path: /opt/qemu-esp32/
retention-days: 7
qemu-test:
name: QEMU Test (${{ matrix.nvs_config }})
needs: build-qemu
runs-on: ubuntu-latest
container:
image: espressif/idf:v5.4
strategy:
fail-fast: false
matrix:
nvs_config:
- default
- full-adr060
- edge-tier0
- edge-tier1
- tdm-3node
- boundary-max
- boundary-min
steps:
- uses: actions/checkout@v4
- name: Download QEMU artifact
uses: actions/download-artifact@v4
with:
name: qemu-esp32
path: /opt/qemu-esp32
- name: Make QEMU executable
run: chmod +x /opt/qemu-esp32/bin/qemu-system-xtensa
- name: Verify QEMU works
run: /opt/qemu-esp32/bin/qemu-system-xtensa --version
- name: Install Python dependencies
run: |
. $IDF_PATH/export.sh
pip install esptool esp-idf-nvs-partition-gen
- name: Set target ESP32-S3
working-directory: firmware/esp32-csi-node
run: |
. $IDF_PATH/export.sh
idf.py set-target esp32s3
- name: Build firmware (mock CSI mode)
working-directory: firmware/esp32-csi-node
run: |
. $IDF_PATH/export.sh
idf.py \
-D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.qemu" \
build
- name: Generate NVS matrix
run: |
. $IDF_PATH/export.sh
python3 scripts/generate_nvs_matrix.py \
--output-dir firmware/esp32-csi-node/build/nvs_matrix \
--only ${{ matrix.nvs_config }}
- name: Create merged flash image
working-directory: firmware/esp32-csi-node
run: |
. $IDF_PATH/export.sh
# Determine merge_bin arguments
OTA_ARGS=""
if [ -f build/ota_data_initial.bin ]; then
OTA_ARGS="0xf000 build/ota_data_initial.bin"
fi
python3 -m esptool --chip esp32s3 merge_bin \
-o build/qemu_flash.bin \
--flash_mode dio --flash_freq 80m --flash_size 8MB \
--fill-flash-size 8MB \
0x0 build/bootloader/bootloader.bin \
0x8000 build/partition_table/partition-table.bin \
$OTA_ARGS \
0x20000 build/esp32-csi-node.bin
file_size() { stat -c%s "$1" 2>/dev/null || stat -f%z "$1" 2>/dev/null || wc -c < "$1"; }
echo "Flash image size: $(file_size build/qemu_flash.bin) bytes"
- name: Inject NVS partition
if: matrix.nvs_config != 'default'
working-directory: firmware/esp32-csi-node
run: |
NVS_BIN="build/nvs_matrix/nvs_${{ matrix.nvs_config }}.bin"
if [ -f "$NVS_BIN" ]; then
file_size() { stat -c%s "$1" 2>/dev/null || stat -f%z "$1" 2>/dev/null || wc -c < "$1"; }
echo "Injecting NVS: $NVS_BIN ($(file_size "$NVS_BIN") bytes)"
dd if="$NVS_BIN" of=build/qemu_flash.bin \
bs=1 seek=$((0x9000)) conv=notrunc 2>/dev/null
else
echo "WARNING: NVS binary not found: $NVS_BIN"
fi
- name: Run QEMU smoke test
env:
QEMU_PATH: /opt/qemu-esp32/bin/qemu-system-xtensa
QEMU_TIMEOUT: "90"
run: |
echo "Starting QEMU (timeout: ${QEMU_TIMEOUT}s)..."
timeout "$QEMU_TIMEOUT" "$QEMU_PATH" \
-machine esp32s3 \
-nographic \
-drive file=firmware/esp32-csi-node/build/qemu_flash.bin,if=mtd,format=raw \
-serial mon:stdio \
-nic user,model=open_eth,net=10.0.2.0/24 \
-no-reboot \
2>&1 | tee firmware/esp32-csi-node/build/qemu_output.log || true
echo "QEMU finished. Log size: $(wc -l < firmware/esp32-csi-node/build/qemu_output.log) lines"
- name: Validate QEMU output
run: |
python3 scripts/validate_qemu_output.py \
firmware/esp32-csi-node/build/qemu_output.log
- name: Upload test logs
if: always()
uses: actions/upload-artifact@v4
with:
name: qemu-logs-${{ matrix.nvs_config }}
path: |
firmware/esp32-csi-node/build/qemu_output.log
firmware/esp32-csi-node/build/nvs_matrix/
retention-days: 14
fuzz-test:
name: Fuzz Testing (ADR-061 Layer 6)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install clang
run: |
sudo apt-get update
sudo apt-get install -y clang
- name: Build fuzz targets
working-directory: firmware/esp32-csi-node/test
run: make all CC=clang
- name: Run serialize fuzzer (60s)
working-directory: firmware/esp32-csi-node/test
run: make run_serialize FUZZ_DURATION=60 || echo "FUZZER_CRASH=serialize" >> "$GITHUB_ENV"
- name: Run edge enqueue fuzzer (60s)
working-directory: firmware/esp32-csi-node/test
run: make run_edge FUZZ_DURATION=60 || echo "FUZZER_CRASH=edge" >> "$GITHUB_ENV"
- name: Run NVS config fuzzer (60s)
working-directory: firmware/esp32-csi-node/test
run: make run_nvs FUZZ_DURATION=60 || echo "FUZZER_CRASH=nvs" >> "$GITHUB_ENV"
- name: Check for crashes
working-directory: firmware/esp32-csi-node/test
run: |
CRASHES=$(find . -type f \( -name "crash-*" -o -name "oom-*" -o -name "timeout-*" \) 2>/dev/null | wc -l)
echo "Crash artifacts found: $CRASHES"
if [ "$CRASHES" -gt 0 ] || [ -n "${FUZZER_CRASH:-}" ]; then
echo "::error::Fuzzer found $CRASHES crash/oom/timeout artifacts. FUZZER_CRASH=${FUZZER_CRASH:-none}"
ls -la crash-* oom-* timeout-* 2>/dev/null
exit 1
fi
- name: Upload fuzz artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: fuzz-crashes
path: |
firmware/esp32-csi-node/test/crash-*
firmware/esp32-csi-node/test/oom-*
firmware/esp32-csi-node/test/timeout-*
retention-days: 30
nvs-matrix-validate:
name: NVS Matrix Generation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install NVS generator
run: pip install esp-idf-nvs-partition-gen
- name: Generate all 14 NVS configs
run: |
python3 scripts/generate_nvs_matrix.py \
--output-dir build/nvs_matrix
- name: Verify all binaries generated
run: |
EXPECTED=14
ACTUAL=$(find build/nvs_matrix -type f -name "nvs_*.bin" 2>/dev/null | wc -l)
echo "Generated $ACTUAL / $EXPECTED NVS binaries"
ls -la build/nvs_matrix/
if [ "$ACTUAL" -lt "$EXPECTED" ]; then
echo "::error::Only $ACTUAL of $EXPECTED NVS binaries generated"
exit 1
fi
- name: Verify binary sizes
run: |
file_size() { stat -c%s "$1" 2>/dev/null || stat -f%z "$1" 2>/dev/null || wc -c < "$1"; }
for f in build/nvs_matrix/nvs_*.bin; do
SIZE=$(file_size "$f")
if [ "$SIZE" -ne 24576 ]; then
echo "::error::$f has unexpected size $SIZE (expected 24576)"
exit 1
fi
echo " OK: $(basename $f) ($SIZE bytes)"
done
# ---------------------------------------------------------------------------
# ADR-062: QEMU Swarm Configurator Test
#
# Runs a lightweight 3-node swarm (ci_matrix preset) under QEMU to validate
# multi-node orchestration, TDM slot coordination, and swarm-level health
# assertions. Uses the pre-built QEMU binary from the build-qemu job and the
# firmware built by qemu-test.
#
# The CI runner is non-root, so TAP bridge networking is unavailable.
# The orchestrator (qemu_swarm.py) detects this and falls back to SLIRP
# user-mode networking, which is sufficient for the ci_matrix preset.
# ---------------------------------------------------------------------------
swarm-test:
name: Swarm Test (ADR-062)
needs: [build-qemu]
runs-on: ubuntu-latest
container:
image: espressif/idf:v5.4
steps:
- uses: actions/checkout@v4
- name: Download QEMU artifact
uses: actions/download-artifact@v4
with:
name: qemu-esp32
path: /opt/qemu-esp32
- name: Make QEMU executable
run: chmod +x /opt/qemu-esp32/bin/qemu-system-xtensa
- name: Install Python dependencies
run: |
. $IDF_PATH/export.sh
pip install pyyaml esptool esp-idf-nvs-partition-gen
- name: Build firmware for swarm
working-directory: firmware/esp32-csi-node
run: |
. $IDF_PATH/export.sh
idf.py set-target esp32s3
idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.qemu" build
python3 -m esptool --chip esp32s3 merge_bin \
-o build/qemu_flash.bin \
--flash_mode dio --flash_freq 80m --flash_size 8MB \
--fill-flash-size 8MB \
0x0 build/bootloader/bootloader.bin \
0x8000 build/partition_table/partition-table.bin \
0x20000 build/esp32-csi-node.bin
- name: Run swarm smoke test
run: |
. $IDF_PATH/export.sh
EXIT_CODE=0
python3 scripts/qemu_swarm.py --preset ci_matrix \
--qemu-path /opt/qemu-esp32/bin/qemu-system-xtensa \
--output-dir build/swarm-results || EXIT_CODE=$?
# Exit 0=PASS, 1=WARN (acceptable in CI without real hardware)
if [ "$EXIT_CODE" -gt 1 ]; then
echo "Swarm test failed with exit code $EXIT_CODE"
exit "$EXIT_CODE"
fi
timeout-minutes: 10
- name: Upload swarm results
if: always()
uses: actions/upload-artifact@v4
with:
name: swarm-results
path: |
build/swarm-results/
retention-days: 14
+1 -15
View File
@@ -226,18 +226,4 @@ v1/src/sensing/mac_wifi
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
# Claude Flow runtime artifacts (auto-generated, machine-specific)
**/daemon.pid
**/pending-insights.jsonl
**/vectors.db
**/memory.db
**/.claude-flow/sessions/session-*.json
**/.claude-flow/sessions/current.json
# Node modules (should use npm ci, not committed)
**/node_modules/
# Local build scripts
firmware/esp32-csi-node/build_firmware.bat
.cursorindexingignore
BIN
View File
Binary file not shown.
-49
View File
@@ -1,49 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "QEMU ESP32-S3 Debug",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/firmware/esp32-csi-node/build/esp32-csi-node.elf",
"cwd": "${workspaceFolder}/firmware/esp32-csi-node",
"MIMode": "gdb",
"miDebuggerPath": "xtensa-esp-elf-gdb",
"miDebuggerServerAddress": "localhost:1234",
"setupCommands": [
{
"description": "Set remote hardware breakpoint limit (ESP32-S3 has 2)",
"text": "set remote hardware-breakpoint-limit 2",
"ignoreFailures": false
},
{
"description": "Set remote hardware watchpoint limit (ESP32-S3 has 2)",
"text": "set remote hardware-watchpoint-limit 2",
"ignoreFailures": false
}
]
},
{
"name": "QEMU ESP32-S3 Debug (attach)",
"type": "cppdbg",
"request": "attach",
"program": "${workspaceFolder}/firmware/esp32-csi-node/build/esp32-csi-node.elf",
"cwd": "${workspaceFolder}/firmware/esp32-csi-node",
"MIMode": "gdb",
"miDebuggerPath": "xtensa-esp-elf-gdb",
"miDebuggerServerAddress": "localhost:1234",
"setupCommands": [
{
"description": "Set remote hardware breakpoint limit (ESP32-S3 has 2)",
"text": "set remote hardware-breakpoint-limit 2",
"ignoreFailures": false
},
{
"description": "Set remote hardware watchpoint limit (ESP32-S3 has 2)",
"text": "set remote hardware-watchpoint-limit 2",
"ignoreFailures": false
}
]
}
]
}
-40
View File
@@ -5,49 +5,9 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v0.4.3-esp32] — 2026-03-15
### Fixed
- **Fall detection false positives (#263)** — Default threshold raised from 2.0 to 15.0 rad/s²; normal walking (2-5 rad/s²) no longer triggers alerts. Added 3-consecutive-frame debounce and 5-second cooldown between alerts. Verified on real ESP32-S3 hardware: 0 false alerts in 60s / 1,300+ live WiFi CSI frames.
- **Kconfig default mismatch** — `CONFIG_EDGE_FALL_THRESH` Kconfig default was still 2000 (=2.0) while `nvs_config.c` fallback was updated to 15.0. Fixed Kconfig to 15000. Caught by real hardware testing — mock data did not reproduce.
- **provision.py NVS generator API change** — `esp_idf_nvs_partition_gen` package changed its `generate()` signature; switched to subprocess-first invocation for cross-version compatibility.
- **QEMU CI pipeline (11 jobs)** — Fixed all failures: fuzz test `esp_timer` stubs, QEMU `libgcrypt` dependency, NVS matrix generator, IDF container `pip` path, flash image padding, validation WARN handling, swarm `ip`/`cargo` missing.
### Added
- **4MB flash support (#265)** — `partitions_4mb.csv` and `sdkconfig.defaults.4mb` for ESP32-S3 boards with 4MB flash (e.g. SuperMini). Dual OTA slots, 1.856 MB each. Thanks to @sebbu for the community workaround that confirmed feasibility.
- **`--strict` flag** for `validate_qemu_output.py` — WARNs now pass by default in CI (no real WiFi in QEMU); use `--strict` to fail on warnings.
## [Unreleased]
### Added
- **QEMU ESP32-S3 testing platform (ADR-061)** — 9-layer firmware testing without hardware
- Mock CSI generator with 10 physics-based scenarios (empty room, walking, fall, multi-person, etc.)
- Single-node QEMU runner with 16-check UART validation
- Multi-node TDM mesh simulation (TAP networking, 2-6 nodes)
- GDB remote debugging with VS Code integration
- Code coverage via gcov/lcov + apptrace
- Fuzz testing (3 libFuzzer targets + ASAN/UBSAN)
- NVS provisioning matrix (14 configs)
- Snapshot-based regression testing (sub-second VM restore)
- Chaos testing with fault injection + health monitoring
- **QEMU Swarm Configurator (ADR-062)** — YAML-driven multi-ESP32 test orchestration
- 4 topologies: star, mesh, line, ring
- 3 node roles: sensor, coordinator, gateway
- 9 swarm-level assertions (boot, crashes, TDM, frame rate, fall detection, etc.)
- 7 presets: smoke (2n/15s), standard (3n/60s), ci-matrix, large-mesh, line-relay, ring-fault, heterogeneous
- Health oracle with cross-node validation
- **QEMU installer** (`install-qemu.sh`) — auto-detects OS, installs deps, builds Espressif QEMU fork
- **Unified QEMU CLI** (`qemu-cli.sh`) — single entry point for all 11 QEMU test commands
- CI: `firmware-qemu.yml` workflow with QEMU test matrix, fuzz testing, NVS validation, and swarm test jobs
- User guide: QEMU testing and swarm configurator section with plain-language walkthrough
### Fixed
- Firmware now boots in QEMU: WiFi/UDP/OTA/display guards for mock CSI mode
- 9 bugs in mock_csi.c (LFSR bias, MAC filter init, scenario loop, overflow burst timing)
- 23 bugs from ADR-061 deep review (inject_fault.py writes, CI cache, snapshot log corruption, etc.)
- 16 bugs from ADR-062 deep review (log filename mismatch, SLIRP port collision, heap false positives, etc.)
- All scripts: `--help` flags, prerequisite checks with install hints, standardized exit codes
- **Sensing server UI API completion (ADR-043)** — 14 fully-functional REST endpoints for model management, CSI recording, and training control
- Model CRUD: `GET /api/v1/models`, `GET /api/v1/models/active`, `POST /api/v1/models/load`, `POST /api/v1/models/unload`, `DELETE /api/v1/models/:id`, `GET /api/v1/models/lora/profiles`, `POST /api/v1/models/lora/activate`
- CSI recording: `GET /api/v1/recording/list`, `POST /api/v1/recording/start`, `POST /api/v1/recording/stop`, `DELETE /api/v1/recording/:id`
+8 -99
View File
@@ -75,7 +75,7 @@ docker run -p 3000:3000 ruvnet/wifi-densepose:latest
|----------|-------------|
| [User Guide](docs/user-guide.md) | Step-by-step guide: installation, first run, API usage, hardware setup, training |
| [Build Guide](docs/build-guide.md) | Building from source (Rust and Python) |
| [Architecture Decisions](docs/adr/README.md) | 62 ADRs — why each technical choice was made, organized by domain (hardware, signal processing, ML, platform, infrastructure) |
| [Architecture Decisions](docs/adr/README.md) | 48 ADRs — why each technical choice was made, organized by domain (hardware, signal processing, ML, platform, infrastructure) |
| [Domain Models](docs/ddd/README.md) | 7 DDD models (RuvSense, Signal Processing, Training Pipeline, Hardware Platform, Sensing Server, WiFi-Mat, CHCI) — bounded contexts, aggregates, domain events, and ubiquitous language |
| [Desktop App](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 |
@@ -87,14 +87,10 @@ docker run -p 3000:3000 ruvnet/wifi-densepose:latest
</a>
<br>
<em>Real-time pose skeleton from WiFi CSI signals — no cameras, no wearables</em>
<br><br>
<br>
<a href="https://ruvnet.github.io/RuView/"><strong>▶ Live Observatory Demo</strong></a>
&nbsp;|&nbsp;
<a href="https://ruvnet.github.io/RuView/pose-fusion.html"><strong>▶ Dual-Modal Pose Fusion Demo</strong></a>
> The [server](#-quick-start) is optional for visualization and aggregation — the ESP32 [runs independently](#esp32-s3-hardware-pipeline) for presence detection, vital signs, and fall alerts.
>
> **Live ESP32 pipeline**: Connect an ESP32-S3 node → run the [sensing server](#sensing-server) → open the [pose fusion demo](https://ruvnet.github.io/RuView/pose-fusion.html) for real-time dual-modal pose estimation (webcam + WiFi CSI). See [ADR-059](docs/adr/ADR-059-live-esp32-csi-pipeline.md).
## 🚀 Key Features
@@ -1047,23 +1043,14 @@ Download a pre-built binary — no build toolchain needed:
| Release | What's included | Tag |
|---------|-----------------|-----|
| [v0.4.3](https://github.com/ruvnet/RuView/releases/tag/v0.4.3-esp32) | **Stable** — Fall detection fix ([#263](https://github.com/ruvnet/RuView/issues/263)), 4MB flash support ([#265](https://github.com/ruvnet/RuView/issues/265)), QEMU CI green | `v0.4.3-esp32` |
| [v0.4.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.1-esp32) | CSI build fix, compile guard, AMOLED display, edge intelligence ([ADR-057](docs/adr/ADR-057-firmware-csi-build-guard.md)) | `v0.4.1-esp32` |
| [v0.2.0](https://github.com/ruvnet/RuView/releases/tag/v0.2.0-esp32) | Stable — raw CSI streaming, multi-node TDM, channel hopping | `v0.2.0-esp32` |
| [v0.3.0-alpha](https://github.com/ruvnet/RuView/releases/tag/v0.3.0-alpha-esp32) | Alpha — adds on-device edge intelligence and WASM modules ([ADR-039](docs/adr/ADR-039-esp32-edge-intelligence.md), [ADR-040](docs/adr/ADR-040-wasm-programmable-sensing.md)) | `v0.3.0-alpha-esp32` |
| [v0.2.0](https://github.com/ruvnet/RuView/releases/tag/v0.2.0-esp32) | Raw CSI streaming, multi-node TDM, channel hopping | `v0.2.0-esp32` |
```bash
# 1. Flash the firmware to your ESP32-S3 (8MB flash — most boards)
# 1. Flash the firmware to your ESP32-S3
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
write_flash --flash-mode dio --flash-size 8MB --flash-freq 80m \
0x0 bootloader.bin 0x8000 partition-table.bin \
0xf000 ota_data_initial.bin 0x20000 esp32-csi-node.bin
# 1b. For 4MB flash boards (e.g. ESP32-S3 SuperMini 4MB) — use the 4MB binaries:
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
write_flash --flash-mode dio --flash-size 4MB --flash-freq 80m \
0x0 bootloader.bin 0x8000 partition-table-4mb.bin \
0xF000 ota_data_initial.bin 0x20000 esp32-csi-node-4mb.bin
write_flash --flash_mode dio --flash_size 8MB \
0x0 bootloader.bin 0x8000 partition-table.bin 0x10000 esp32-csi-node.bin
# 2. Set WiFi credentials and server address (stored in flash, survives reboots)
python firmware/esp32-csi-node/provision.py --port COM7 \
@@ -1111,9 +1098,9 @@ python firmware/esp32-csi-node/provision.py --port COM7 \
--ssid "YourWiFi" --password "secret" --target-ip 192.168.1.20 \
--edge-tier 2
# Fine-tune detection thresholds (fall-thresh in milli-units: 15000 = 15.0 rad/s²)
# Fine-tune detection thresholds
python firmware/esp32-csi-node/provision.py --port COM7 \
--edge-tier 2 --vital-int 500 --fall-thresh 15000 --subk-count 16
--edge-tier 2 --vital-int 500 --fall-thresh 5000 --subk-count 16
```
When Tier 2 is active, the node sends a 32-byte vitals packet once per second containing: presence, motion level, breathing BPM, heart rate BPM, confidence scores, fall alert flag, and occupancy count.
@@ -1703,82 +1690,6 @@ WebSocket: `ws://localhost:3001/ws/sensing` (real-time sensing + vital signs)
</details>
<details>
<summary><strong>QEMU Firmware Testing (ADR-061) — 9-Layer Platform</strong></summary>
Test ESP32-S3 firmware without physical hardware using Espressif's QEMU fork. The platform provides 9 layers of testing capability:
| Layer | Capability | Script / Config |
|-------|-----------|-----------------|
| 1 | Mock CSI generator (10 physics-based scenarios) | `firmware/esp32-csi-node/main/mock_csi.c` |
| 2 | Single-node QEMU runner + UART validation (16 checks) | `scripts/qemu-esp32s3-test.sh`, `scripts/validate_qemu_output.py` |
| 3 | Multi-node TDM mesh simulation (TAP networking) | `scripts/qemu-mesh-test.sh`, `scripts/validate_mesh_test.py` |
| 4 | GDB remote debugging (VS Code integration) | `.vscode/launch.json` |
| 5 | Code coverage (gcov/lcov via apptrace) | `firmware/esp32-csi-node/sdkconfig.coverage` |
| 6 | Fuzz testing (libFuzzer + ASAN/UBSAN) | `firmware/esp32-csi-node/test/fuzz_*.c` |
| 7 | NVS provisioning matrix (14 configs) | `scripts/generate_nvs_matrix.py` |
| 8 | Snapshot regression (sub-second VM restore) | `scripts/qemu-snapshot-test.sh` |
| 9 | Chaos testing (fault injection + health monitoring) | `scripts/qemu-chaos-test.sh`, `scripts/inject_fault.py`, `scripts/check_health.py` |
```bash
# Quick start: build + run + validate
cd firmware/esp32-csi-node
idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.qemu" build
# Single-node test (builds, merges flash, runs QEMU, validates output)
bash scripts/qemu-esp32s3-test.sh
# Multi-node mesh test (3 QEMU instances with TDM)
sudo bash scripts/qemu-mesh-test.sh 3
# Fuzz testing (60 seconds per target)
cd firmware/esp32-csi-node/test && make all CC=clang && make run_serialize FUZZ_DURATION=60
# Chaos testing (fault injection resilience)
bash scripts/qemu-chaos-test.sh --faults all --duration 120
```
**10 test scenarios**: empty room, static person, walking, fall, multi-person, channel sweep, MAC filter, ring overflow, boundary RSSI, zero-length frames.
**14 NVS configs**: default, WiFi-only, full ADR-060, edge tiers 0/1/2, TDM mesh, WASM signed/unsigned, 5GHz, boundary max/min, power-save, empty-strings.
**CI**: GitHub Actions workflow runs 7 NVS matrix configs, 3 fuzz targets, and NVS binary validation on every push to `firmware/`.
See [ADR-061](docs/adr/ADR-061-qemu-esp32s3-firmware-testing.md) for the full architecture.
</details>
<details>
<summary><strong>QEMU Swarm Configurator (ADR-062)</strong></summary>
Test multiple ESP32-S3 nodes simultaneously using a YAML-driven orchestrator. Define node roles, network topologies, and validation assertions in a config file.
```bash
# Quick smoke test (2 nodes, 15 seconds)
python3 scripts/qemu_swarm.py --preset smoke
# Standard 3-node test (coordinator + 2 sensors)
python3 scripts/qemu_swarm.py --preset standard
# See all presets
python3 scripts/qemu_swarm.py --list-presets
# Preview without running
python3 scripts/qemu_swarm.py --preset standard --dry-run
```
**Topologies**: star (sensors → coordinator), mesh (fully connected), line (relay chain), ring (circular).
**Node roles**: sensor (generates CSI), coordinator (aggregates), gateway (bridges to host).
**7 presets**: smoke, standard, ci-matrix, large-mesh, line-relay, ring-fault, heterogeneous.
**9 swarm assertions**: boot check, crash detection, TDM collision, frame production, coordinator reception, fall detection, frame rate, boot time, heap health.
See [ADR-062](docs/adr/ADR-062-qemu-swarm-configurator.md) and the [User Guide](docs/user-guide.md#testing-firmware-without-hardware-qemu) for step-by-step instructions.
</details>
<details>
<summary><strong>Python Legacy CLI</strong> — v1 API server commands</summary>
@@ -1798,9 +1709,7 @@ wifi-densepose tasks list # List background tasks
<details>
<summary><strong>Documentation Links</strong></summary>
- [User Guide](docs/user-guide.md) — installation, first run, API, hardware setup, QEMU testing
- [WiFi-Mat User Guide](docs/wifi-mat-user-guide.md) | [Domain Model](docs/ddd/wifi-mat-domain-model.md)
- [ADR-061](docs/adr/ADR-061-qemu-esp32s3-firmware-testing.md) QEMU platform | [ADR-062](docs/adr/ADR-062-qemu-swarm-configurator.md) Swarm configurator
- [ADR-021](docs/adr/ADR-021-vital-sign-detection-rvdna-pipeline.md) | [ADR-022](docs/adr/ADR-022-windows-wifi-enhanced-fidelity-ruvector.md) | [ADR-023](docs/adr/ADR-023-trained-densepose-model-ruvector-pipeline.md)
</details>
@@ -0,0 +1,240 @@
# ADR-057: Desktop App Training & RuVector Integration
| Field | Value |
|-------|-------|
| Status | Proposed |
| Date | 2026-03-10 |
| Authors | RuView Team |
| Reviewers | - |
| Related | ADR-016, ADR-017, ADR-024, ADR-027 |
## Context
The RuView desktop application currently provides device discovery, firmware flashing, OTA updates, and real-time sensing visualization. However, users cannot train models or configure RuVector signal processing modules directly from the desktop app.
The following crates exist in the workspace but are not exposed in the desktop UI:
### Training Crate (`wifi-densepose-train`)
- Dataset management (MM-Fi, Wi-Pose formats)
- Model architectures (CSI encoder, pose decoder)
- Training loops with metrics tracking
- Checkpoint save/load
- ruview_metrics integration
### RuVector Crates (5 modules)
1. **ruvector-mincut** - Graph-based person segmentation, DynamicPersonMatcher
2. **ruvector-attn-mincut** - Attention-weighted antenna selection
3. **ruvector-temporal-tensor** - Temporal CSI compression, breathing detection
4. **ruvector-solver** - Sparse interpolation, triangulation
5. **ruvector-attention** - Spatial attention, BVP extraction
## Decision
Add a new **"Training"** page to the desktop application with tabbed navigation:
### Tab Structure
```
┌─────────────────────────────────────────────────────────────┐
│ Training & Models │
├──────────┬──────────┬──────────┬──────────┬────────────────┤
│ Datasets │ Models │ Training │ RuVector │ Metrics │
└──────────┴──────────┴──────────┴──────────┴────────────────┘
```
### Tab 1: Datasets
- **Download** standard datasets (MM-Fi, Wi-Pose)
- **Import** custom CSI recordings
- **Preview** dataset samples (CSI heatmaps, labels)
- **Split** into train/val/test sets
- **Statistics** - sample counts, class distribution
### Tab 2: Models
- **Browse** available architectures:
- CSI Encoder (CNN, Transformer)
- Pose Decoder (LSTM, GRU)
- AETHER embedding network (ADR-024)
- MERIDIAN domain adaptor (ADR-027)
- **Load** checkpoints from disk
- **View** model summary (params, layers, memory)
- **Export** to ONNX/TorchScript
### Tab 3: Training
- **Configure** training:
- Learning rate, batch size, epochs
- Optimizer (Adam, SGD, AdamW)
- Loss function selection
- Data augmentation toggles
- **GPU Detection** - CUDA/Metal availability
- **Start/Stop** training jobs
- **Progress** - live loss curves, ETA
- **Checkpointing** - auto-save best model
### Tab 4: RuVector
- **Module Configuration**:
- MinCut graph parameters
- Attention weights
- Temporal compression ratio
- Solver interpolation settings
- **Live Testing** - apply to real-time CSI stream
- **Comparison** - A/B test configurations
- **Export** - save optimal config
### Tab 5: Metrics
- **Loss Curves** - training/validation over epochs
- **Evaluation** - PCK, mAP, IoU scores
- **Confusion Matrix** - per-joint accuracy
- **Export** - CSV, JSON, TensorBoard format
## Architecture
### Backend (Rust/Tauri)
```
wifi-densepose-desktop/
├── src/
│ ├── commands/
│ │ ├── training.rs # NEW: Training job management
│ │ ├── datasets.rs # NEW: Dataset download/import
│ │ ├── models.rs # NEW: Model loading/export
│ │ ├── ruvector.rs # NEW: RuVector config
│ │ └── metrics.rs # NEW: Metrics retrieval
│ └── domain/
│ ├── training.rs # Training state machine
│ └── ruvector.rs # RuVector config types
```
### Frontend (React/TypeScript)
```
ui/src/pages/
├── Training/
│ ├── index.tsx # Tab container
│ ├── DatasetsTab.tsx # Dataset management
│ ├── ModelsTab.tsx # Model browser
│ ├── TrainingTab.tsx # Training control
│ ├── RuVectorTab.tsx # Signal processing config
│ └── MetricsTab.tsx # Visualization
```
### Tauri Commands
| Command | Description |
|---------|-------------|
| `list_datasets` | Get available datasets |
| `download_dataset` | Download standard dataset |
| `import_dataset` | Import custom recordings |
| `list_models` | Get model architectures |
| `load_checkpoint` | Load model weights |
| `export_model` | Export to ONNX |
| `detect_gpu` | Check CUDA/Metal |
| `start_training` | Begin training job |
| `stop_training` | Cancel training |
| `training_progress` | Get current status |
| `get_ruvector_config` | Load RuVector settings |
| `set_ruvector_config` | Update settings |
| `test_ruvector_live` | Apply to live CSI |
| `get_metrics` | Retrieve training metrics |
### Event System
Training progress updates via Tauri events:
```rust
#[derive(Serialize, Clone)]
pub struct TrainingProgress {
pub epoch: u32,
pub total_epochs: u32,
pub batch: u32,
pub total_batches: u32,
pub train_loss: f32,
pub val_loss: Option<f32>,
pub learning_rate: f32,
pub eta_secs: u64,
pub gpu_memory_mb: Option<u64>,
}
// Emit every batch
app.emit("training:progress", progress)?;
// Emit on completion
app.emit("training:complete", result)?;
```
## Implementation Plan
### Phase 1: Foundation (Week 1-2)
1. Create `Training` page skeleton with tabs
2. Implement `detect_gpu` command
3. Add dataset listing/download commands
4. Design TypeScript types for all entities
### Phase 2: Dataset Management (Week 3)
1. MM-Fi dataset downloader
2. Wi-Pose dataset downloader
3. Custom dataset import (CSV/NPZ)
4. Dataset preview component
### Phase 3: Model Management (Week 4)
1. Model architecture browser
2. Checkpoint loading
3. Model summary display
4. ONNX export
### Phase 4: Training Loop (Week 5-6)
1. Training configuration UI
2. Background training thread
3. Progress event emission
4. Checkpoint auto-save
5. Training history persistence
### Phase 5: RuVector Integration (Week 7)
1. RuVector config UI
2. Live CSI testing
3. A/B comparison mode
4. Config export/import
### Phase 6: Metrics & Polish (Week 8)
1. Loss curve visualization (Chart.js/Recharts)
2. Evaluation metrics display
3. Export functionality
4. Error handling & edge cases
## Risks & Mitigations
| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|------------|
| No GPU available | Medium | High | CPU fallback with warning |
| Large dataset downloads | High | Medium | Resume support, progress UI |
| Training crashes | Medium | High | Checkpoint recovery, error reporting |
| Memory exhaustion | Low | High | Batch size auto-tuning |
| UI blocking | Medium | High | All training in background thread |
## Success Criteria
1. User can download MM-Fi dataset from UI
2. User can start training with GPU detection
3. Live progress updates without UI freeze
4. Training can be paused/resumed
5. RuVector config changes apply to live CSI
6. Metrics display updates in real-time
7. Models can be exported to ONNX
## Alternatives Considered
### 1. Separate Training App
- **Rejected**: Fragments user experience, duplicates code
### 2. Web-based Training Dashboard
- **Rejected**: Requires server, no offline support
### 3. CLI-only Training
- **Rejected**: Poor UX for non-technical users
## References
- ADR-016: RuVector Training Pipeline Integration
- ADR-017: RuVector Signal + MAT Integration
- ADR-024: AETHER Contrastive CSI Embedding
- ADR-027: MERIDIAN Domain Generalization
- Tauri v2 Events: https://v2.tauri.app/develop/calling-rust/#events
@@ -1,82 +0,0 @@
# ADR-057: Firmware CSI Build Guard and sdkconfig.defaults
| Field | Value |
|-------------|---------------------------------------------|
| **Status** | Accepted |
| **Date** | 2026-03-12 |
| **Authors** | ruv |
| **Issues** | #223, #238, #234, #210, #190 |
## Context
Multiple GitHub issues (#223, #238, #234, #210, #190) report firmware problems
that fall into two categories:
1. **CSI not enabled at runtime** — The committed `sdkconfig` had
`# CONFIG_ESP_WIFI_CSI_ENABLED is not set` (line 1135), meaning users who
built from source or used pre-built binaries got the runtime error:
`E (6700) wifi:CSI not enabled in menuconfig!`
Root cause: `sdkconfig.defaults.template` existed with the correct setting
(`CONFIG_ESP_WIFI_CSI_ENABLED=y`) but ESP-IDF only reads
`sdkconfig.defaults` — not `.template` suffixed files. No `sdkconfig.defaults`
file was committed.
2. **Unsupported ESP32 variants** — Users attempting to use original ESP32
(D0WD) and ESP32-C3 boards. The firmware targets ESP32-S3 only
(`CONFIG_IDF_TARGET="esp32s3"`, Xtensa architecture) and this was not
surfaced clearly enough in documentation or build errors.
## Decision
### Fix 1: Commit `sdkconfig.defaults` (not just template)
Copy `sdkconfig.defaults.template``sdkconfig.defaults` so that ESP-IDF
applies the correct defaults (including `CONFIG_ESP_WIFI_CSI_ENABLED=y`)
automatically when `sdkconfig` is regenerated.
### Fix 2: `#error` compile-time guard in `csi_collector.c`
Add a preprocessor guard:
```c
#ifndef CONFIG_ESP_WIFI_CSI_ENABLED
#error "CONFIG_ESP_WIFI_CSI_ENABLED must be set in sdkconfig."
#endif
```
This turns a confusing runtime crash into a clear compile-time error with
instructions on how to fix it.
### Fix 3: Fix committed `sdkconfig`
Change line 1135 from `# CONFIG_ESP_WIFI_CSI_ENABLED is not set` to
`CONFIG_ESP_WIFI_CSI_ENABLED=y`.
## Consequences
- **Positive**: New builds will always have CSI enabled. Users building from
source will get a clear compile error if CSI is somehow disabled.
- **Positive**: Pre-built release binaries will include CSI support.
- **Neutral**: Original ESP32 and ESP32-C3 remain unsupported. This is by
design — only ESP32-S3 has the CSI API surface we depend on. Future ADRs
may address multi-target support if demand warrants it.
- **Negative**: None identified.
## Hardware Support Matrix
| Variant | CSI Support | Firmware Target | Status |
|--------------|-------------|-----------------|---------------|
| ESP32-S3 | Yes | Yes | Supported |
| ESP32 (orig) | Partial | No | Unsupported |
| ESP32-C3 | Yes (IDF 5.1+) | No | Unsupported |
| ESP32-C6 | Yes | No | Unsupported |
## Notes
- ESP32-C3 and C6 use RISC-V architecture; a separate build target
(`idf.py set-target esp32c3`) would be needed.
- Original ESP32 has limited CSI (no STBC HT-LTF2, fewer subcarriers).
- Users on unsupported hardware can still write custom firmware using the
ADR-018 binary frame format (magic `0xC5110001`) for interop with the
Rust aggregator.
@@ -1,392 +0,0 @@
# ADR-058: Dual-Modal WASM Browser Pose Estimation — Live Video + WiFi CSI Fusion
- **Status**: Proposed
- **Date**: 2026-03-12
- **Deciders**: ruv
- **Tags**: wasm, browser, cnn, pose-estimation, ruvector, video, multimodal, fusion
## Context
WiFi-DensePose estimates human poses from WiFi CSI (Channel State Information).
The `ruvector-cnn` crate provides a pure Rust CNN (MobileNet-V3) with WASM bindings.
Both modalities exist independently — what's missing is **fusing live webcam video
with WiFi CSI** in a single browser demo to achieve robust pose estimation that
works even when one modality degrades (occlusion, signal noise, poor lighting).
Existing assets:
1. **`wifi-densepose-wasm`** — CSI signal processing compiled to WASM
2. **`wifi-densepose-sensing-server`** — Axum server streaming live CSI via WebSocket
3. **`ruvector-cnn`** — Pure Rust CNN with MobileNet-V3 backbones, SIMD, contrastive learning
4. **`ruvector-cnn-wasm`** — wasm-bindgen bindings: `WasmCnnEmbedder`, `SimdOps`, `LayerOps`, contrastive losses
5. **`vendor/ruvector/examples/wasm-vanilla/`** — Reference vanilla JS WASM example
Research shows multi-modal fusion (camera + WiFi) significantly outperforms either alone:
- Camera fails under occlusion, poor lighting, privacy constraints
- WiFi CSI fails with signal noise, multipath, low spatial resolution
- Fusion compensates: WiFi provides through-wall coverage, camera provides fine-grained detail
## Decision
Build a **dual-modal browser demo** at `examples/wasm-browser-pose/` that:
1. Captures **live webcam video** via `getUserMedia` API
2. Receives **live WiFi CSI** via WebSocket from the sensing server
3. Processes **both streams** through separate CNN pipelines in `ruvector-cnn-wasm`
4. **Fuses embeddings** with learned attention weights for combined pose estimation
5. Renders **video overlay** with skeleton + WiFi confidence heatmap on Canvas
6. Runs entirely in the browser — all inference client-side via WASM
### Architecture
```
┌──────────────────────────────────────────────────────────────────┐
│ Browser │
│ │
│ ┌────────────┐ ┌────────────────┐ ┌───────────────────┐ │
│ │ getUserMedia│───▶│ Video Frame │───▶│ CNN WASM │ │
│ │ (Webcam) │ │ Capture │ │ (Visual Embedder) │ │
│ └────────────┘ │ 224×224 RGB │ │ → 512-dim │ │
│ └────────────────┘ └────────┬──────────┘ │
│ │ │
│ visual_embedding │
│ │ │
│ ┌──────▼──────┐ │
│ ┌────────────┐ ┌────────────────┐ │ │ │
│ │ WebSocket │───▶│ CSI WASM │ │ Attention │ │
│ │ Client │ │ (densepose- │ │ Fusion │ │
│ │ │ │ wasm) │ │ Module │ │
│ └────────────┘ └───────┬────────┘ │ │ │
│ │ └──────┬──────┘ │
│ ┌───────▼────────┐ │ │
│ │ CNN WASM │ fused_embedding │
│ │ (CSI Embedder) │ │ │
│ │ → 512-dim │ ┌──────▼──────┐ │
│ └───────┬────────┘ │ Pose │ │
│ │ │ Decoder │ │
│ csi_embedding │ → 17 kpts │ │
│ │ └──────┬──────┘ │
│ └──────────────────────┘ │
│ │ │
│ ┌──────────────┐ ┌─────▼──────┐ │
│ │ Video Canvas │◀────────│ Overlay │ │
│ │ + Skeleton │ │ Renderer │ │
│ │ + Heatmap │ └────────────┘ │
│ └──────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
▲ ▲
│ getUserMedia │ WebSocket
│ (camera) │ (ws://host:3030/ws/csi)
│ │
┌────┴────┐ ┌───────┴─────────┐
│ Webcam │ │ Sensing Server │
└─────────┘ └─────────────────┘
```
### Dual Pipeline Design
Two parallel CNN pipelines run on each frame tick (~30 FPS):
| Pipeline | Input | Preprocessing | CNN Config | Output |
|----------|-------|---------------|------------|--------|
| **Visual** | Webcam frame (640×480) | Resize to 224×224 RGB, ImageNet normalize | MobileNet-V3 Small, 512-dim | Visual embedding |
| **CSI** | CSI frame (ADR-018 binary) | Amplitude/phase/delta → 224×224 pseudo-RGB | MobileNet-V3 Small, 512-dim | CSI embedding |
Both use the same `WasmCnnEmbedder` but with separate instances and weight sets.
### Fusion Strategy
**Learned attention-weighted fusion** combines the two 512-dim embeddings:
```javascript
// Attention fusion: learn which modality to trust per-dimension
// α ∈ [0,1]^512 — attention weights (shipped as JSON, trained offline)
// visual_emb, csi_emb ∈ R^512
function fuseEmbeddings(visual_emb, csi_emb, attention_weights) {
const fused = new Float32Array(512);
for (let i = 0; i < 512; i++) {
const α = attention_weights[i];
fused[i] = α * visual_emb[i] + (1 - α) * csi_emb[i];
}
return fused;
}
```
**Dynamic confidence gating** adjusts fusion based on signal quality:
| Condition | Behavior |
|-----------|----------|
| Good video + good CSI | Balanced fusion (α ≈ 0.5) |
| Poor lighting / occlusion | CSI-dominant (α → 0, WiFi takes over) |
| CSI noise / no ESP32 | Video-dominant (α → 1, camera only) |
| Video-only mode (no WiFi) | α = 1.0, pure visual CNN pose estimation |
| CSI-only mode (no camera) | α = 0.0, pure WiFi pose estimation |
Quality detection:
- **Video quality**: Frame brightness variance (dark = low quality), motion blur score
- **CSI quality**: Signal-to-noise ratio from `wifi-densepose-wasm`, coherence gate output
### CSI-to-Image Encoding
CSI data encoded as 3-channel pseudo-image for the CSI CNN pipeline:
| Channel | Data | Normalization |
|---------|------|---------------|
| R | CSI amplitude (subcarrier × time window) | Min-max to [0, 255] |
| G | CSI phase (unwrapped, subcarrier × time window) | Min-max to [0, 255] |
| B | Temporal difference (frame-to-frame Δ amplitude) | Abs, min-max to [0, 255] |
### Video Processing
Webcam frames processed through standard ImageNet pipeline:
```javascript
// Capture frame from video element
const frame = captureVideoFrame(videoElement, 224, 224); // Returns Uint8Array RGB
// ImageNet normalization happens inside WasmCnnEmbedder.extract()
const visual_embedding = visual_embedder.extract(frame, 224, 224);
```
### Pose Keypoint Mapping
17 COCO-format keypoints decoded from the fused 512-dim embedding:
```
0: nose 1: left_eye 2: right_eye
3: left_ear 4: right_ear 5: left_shoulder
6: right_shoulder 7: left_elbow 8: right_elbow
9: left_wrist 10: right_wrist 11: left_hip
12: right_hip 13: left_knee 14: right_knee
15: left_ankle 16: right_ankle
```
Each keypoint decoded as (x, y, confidence) = 51 values from the 512-dim embedding
via a learned linear projection.
### Operating Modes
The demo supports three modes, selectable in the UI:
| Mode | Video | CSI | Fusion | Use Case |
|------|-------|-----|--------|----------|
| **Dual (default)** | ✅ | ✅ | Attention-weighted | Best accuracy, full demo |
| **Video Only** | ✅ | ❌ | α = 1.0 | No ESP32 available, quick demo |
| **CSI Only** | ❌ | ✅ | α = 0.0 | Privacy mode, through-wall sensing |
**Video Only mode works without any hardware** — just a webcam — making the demo
instantly accessible for anyone wanting to try it.
### File Layout
```
examples/wasm-browser-pose/
├── index.html # Single-page app (vanilla JS, no bundler)
├── js/
│ ├── app.js # Main entry, mode selection, orchestration
│ ├── video-capture.js # getUserMedia, frame extraction, quality detection
│ ├── csi-processor.js # WebSocket CSI client, frame parsing, pseudo-image encoding
│ ├── fusion.js # Attention-weighted embedding fusion, confidence gating
│ ├── pose-decoder.js # Fused embedding → 17 keypoints
│ └── canvas-renderer.js # Video overlay, skeleton, CSI heatmap, confidence bars
├── data/
│ ├── visual-weights.json # Visual CNN → embedding projection (placeholder until trained)
│ ├── csi-weights.json # CSI CNN → embedding projection (placeholder until trained)
│ ├── fusion-weights.json # Attention fusion α weights (512 values)
│ └── pose-weights.json # Fused embedding → keypoint projection
├── css/
│ └── style.css # Dark theme UI styling
├── pkg/ # Built WASM packages (gitignored, built by script)
│ ├── wifi_densepose_wasm/
│ └── ruvector_cnn_wasm/
├── build.sh # wasm-pack build script for both packages
└── README.md # Setup and usage instructions
```
### Build Pipeline
```bash
#!/bin/bash
# build.sh — builds both WASM packages into pkg/
set -e
# Build wifi-densepose-wasm (CSI processing)
wasm-pack build ../../rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm \
--target web --out-dir "$(pwd)/pkg/wifi_densepose_wasm" --no-typescript
# Build ruvector-cnn-wasm (CNN inference for both video and CSI)
wasm-pack build ../../vendor/ruvector/crates/ruvector-cnn-wasm \
--target web --out-dir "$(pwd)/pkg/ruvector_cnn_wasm" --no-typescript
echo "Build complete. Serve with: python3 -m http.server 8080"
```
### UI Layout
```
┌─────────────────────────────────────────────────────────┐
│ WiFi-DensePose — Live Dual-Modal Pose Estimation │
│ [Dual Mode ▼] [⚙ Settings] FPS: 28 ◉ Live │
├───────────────────────────┬─────────────────────────────┤
│ │ │
│ ┌───────────────────┐ │ ┌───────────────────┐ │
│ │ │ │ │ │ │
│ │ Video + Skeleton │ │ │ CSI Heatmap │ │
│ │ Overlay │ │ │ (amplitude × │ │
│ │ (main canvas) │ │ │ subcarrier) │ │
│ │ │ │ │ │ │
│ └───────────────────┘ │ └───────────────────┘ │
│ │ │
├───────────────────────────┴─────────────────────────────┤
│ Fusion Confidence: ████████░░ 78% │
│ Video: ██████████ 95% │ CSI: ██████░░░░ 61% │
├─────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────┐ │
│ │ Embedding Space (2D projection) │ │
│ │ · · · │ │
│ │ · · · · · · (color = pose cluster) │ │
│ │ · · · · │ │
│ └─────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ Latency: Video 12ms │ CSI 8ms │ Fusion 1ms │ Total 21ms│
│ [▶ Record] [📷 Snapshot] [Confidence: ████ 0.6] │
└─────────────────────────────────────────────────────────┘
```
### WASM Module Structure
| Package | Source Crate | Provides | Size (est.) |
|---------|-------------|----------|-------------|
| `wifi_densepose_wasm` | `wifi-densepose-wasm` | CSI frame parsing, signal processing, feature extraction | ~200KB |
| `ruvector_cnn_wasm` | `ruvector-cnn-wasm` | `WasmCnnEmbedder` (×2 instances), `SimdOps`, `LayerOps`, contrastive losses | ~150KB |
Two `WasmCnnEmbedder` instances are created — one for video frames, one for CSI pseudo-images.
They share the same WASM module but have independent state.
### Browser API Requirements
| API | Purpose | Required | Fallback |
|-----|---------|----------|----------|
| `getUserMedia` | Webcam capture | For video mode | CSI-only mode |
| WebAssembly | CNN inference | Yes | None (hard requirement) |
| WASM SIMD128 | Accelerated inference | No | Scalar fallback (~2× slower) |
| WebSocket | CSI data stream | For CSI mode | Video-only mode |
| Canvas 2D | Rendering | Yes | None |
| `requestAnimationFrame` | Render loop | Yes | `setTimeout` fallback |
| ES Modules | Code organization | Yes | None |
Target: Chrome 89+, Firefox 89+, Safari 15+, Edge 89+
### Performance Budget
| Stage | Target Latency | Notes |
|-------|---------------|-------|
| Video frame capture + resize | <3ms | `drawImage` to offscreen canvas |
| Video CNN embedding | <15ms | 224×224 RGB → 512-dim |
| CSI receive + parse | <2ms | Binary WebSocket message |
| CSI pseudo-image encoding | <3ms | Amplitude/phase/delta channels |
| CSI CNN embedding | <15ms | 224×224 pseudo-RGB → 512-dim |
| Attention fusion | <1ms | Element-wise weighted sum |
| Pose decoding | <1ms | Linear projection |
| Canvas overlay render | <3ms | Video + skeleton + heatmap |
| **Total (dual mode)** | **<33ms** | **30 FPS capable** |
| **Total (video only)** | **<22ms** | **45 FPS capable** |
Note: Video and CSI CNN pipelines can run in parallel using Web Workers,
reducing dual-mode latency to ~max(15, 15) + 5 = ~20ms (50 FPS).
### Contrastive Learning Integration
The demo optionally shows real-time contrastive learning in the browser:
- **InfoNCE loss** (`WasmInfoNCELoss`): Compare video vs CSI embeddings for the same pose — trains cross-modal alignment
- **Triplet loss** (`WasmTripletLoss`): Push apart different poses, pull together same pose across modalities
- **SimdOps**: Accelerated dot products for real-time similarity computation
- **Embedding space panel**: Live 2D projection shows video and CSI embeddings converging when viewing the same person
### Relationship to Existing Crates
| Existing Crate | Role in This Demo |
|---------------|-------------------|
| `ruvector-cnn-wasm` | CNN inference for **both** video frames and CSI pseudo-images |
| `wifi-densepose-wasm` | CSI frame parsing and signal processing |
| `wifi-densepose-sensing-server` | WebSocket CSI data source |
| `wifi-densepose-core` | ADR-018 frame format definitions |
| `ruvector-cnn` | Underlying MobileNet-V3, layers, contrastive learning |
No new Rust crates are needed. The example is pure HTML/JS consuming existing WASM packages.
## Consequences
### Positive
- **Instant demo**: Video-only mode works with just a webcam — no ESP32 needed
- **Multi-modal showcase**: Demonstrates camera + WiFi fusion, the core innovation of the project
- **Graceful degradation**: Works with video-only, CSI-only, or both
- **Through-wall capability**: CSI mode shows pose estimation where cameras cannot reach
- **Zero-install**: Anyone with a browser can try it
- **Training data collection**: Can record paired (video, CSI) data for offline model training
- **Reusable**: JS modules embed directly in the Tauri desktop app's webview
### Negative
- **Model weights**: Requires offline-trained weights for visual CNN, CSI CNN, fusion, and pose decoder (~200KB total JSON)
- **WASM size**: Two WASM modules total ~350KB (acceptable)
- **No GPU**: CPU-only WASM inference; adequate at 224×224 but limits resolution scaling
- **Camera privacy**: Video mode requires camera permission (mitigated: CSI-only mode available)
- **Two CNN instances**: Memory footprint doubles vs single-modal (~10MB total, acceptable for desktop browsers)
### Risks
- **Cross-modal alignment**: Video and CSI embeddings must be trained jointly for fusion to work;
without proper training, fusion may be worse than either modality alone
- **Latency on mobile**: Dual CNN on mobile browsers may exceed 33ms; implement automatic quality reduction
- **WebSocket drops**: Network jitter → CSI frame gaps; buffer last 3 frames, interpolate missing data
## Implementation Plan
1. **Phase 1 — Scaffold**: File layout, build.sh, index.html shell, mode selector UI
2. **Phase 2 — Video pipeline**: getUserMedia → frame capture → CNN embedding → basic pose display
3. **Phase 3 — CSI pipeline**: WebSocket client → CSI parsing → pseudo-image → CNN embedding
4. **Phase 4 — Fusion**: Attention-weighted combination, confidence gating, mode switching
5. **Phase 5 — Pose decoder**: Linear projection with placeholder weights → 17 keypoints
6. **Phase 6 — Overlay renderer**: Video canvas with skeleton overlay, CSI heatmap panel
7. **Phase 7 — Training**: Use `wifi-densepose-train` to generate real weights for both CNNs + fusion + decoder
8. **Phase 8 — Contrastive demo**: Embedding space visualization, cross-modal similarity display
9. **Phase 9 — Web Workers**: Move CNN inference to workers for parallel video + CSI processing
10. **Phase 10 — Polish**: Recording, snapshots, adaptive quality, mobile optimization
## Alternatives Considered
### 1. CSI-Only (No Video)
Rejected: Misses the opportunity to show multi-modal fusion and makes the demo less
accessible (requires ESP32 hardware). Video-only mode as a fallback is strictly better.
### 2. Server-Side Video Inference
Rejected: Adds latency, requires webcam stream upload (privacy concern), and defeats
the WASM-first architecture. All inference must be client-side.
### 3. TensorFlow.js for Video, ruvector-cnn-wasm for CSI
Rejected: Would require two different ML frameworks. Using `ruvector-cnn-wasm` for both
keeps a single WASM module, unified embedding space, and simpler fusion.
### 4. Pre-recorded Video Demo
Rejected: Live webcam input is far more compelling for demonstrations.
Pre-recorded mode can be added as a secondary option.
### 5. React/Vue Framework
Rejected: Adds build tooling. Vanilla JS + ES modules keeps the demo self-contained.
## References
- [ADR-018: Binary CSI Frame Format](ADR-018-binary-csi-frame-format.md)
- [ADR-024: Contrastive CSI Embedding / AETHER](ADR-024-contrastive-csi-embedding.md)
- [ADR-055: Integrated Sensing Server](ADR-055-integrated-sensing-server.md)
- `vendor/ruvector/crates/ruvector-cnn/src/lib.rs` — CNN embedder implementation
- `vendor/ruvector/crates/ruvector-cnn-wasm/src/lib.rs` — WASM bindings
- `vendor/ruvector/examples/wasm-vanilla/index.html` — Reference vanilla JS WASM pattern
- Person-in-WiFi: Fine-grained Person Perception using WiFi (ICCV 2019) — camera+WiFi fusion precedent
- WiPose: Multi-Person WiFi Pose Estimation (TMC 2022) — cross-modal embedding approach
@@ -1,83 +0,0 @@
# ADR-059: Live ESP32 CSI Pipeline Integration
## Status
Accepted
## Date
2026-03-12
## Context
ADR-058 established a dual-modal browser demo combining webcam video and WiFi CSI for pose estimation. However, it used simulated CSI data. To demonstrate real-world capability, we need an end-to-end pipeline from physical ESP32 hardware through to the browser visualization.
The ESP32-S3 firmware (`firmware/esp32-csi-node/`) already supports CSI collection and UDP streaming (ADR-018). The sensing server (`wifi-densepose-sensing-server`) already supports UDP ingestion and WebSocket bridging. The missing piece was connecting these components and enabling the browser demo to consume live data.
## Decision
Implement a complete live CSI pipeline:
```
ESP32-S3 (CSI capture) → UDP:5005 → sensing-server (Rust/Axum) → WS:8765 → browser demo
```
### Components
1. **ESP32 Firmware** — Rebuilt with native Windows ESP-IDF v5.4.0 toolchain (no Docker). Configured for target network and PC IP via `sdkconfig`. Helper scripts added:
- `build_firmware.ps1` — Sets up IDF environment, cleans, builds, and flashes
- `read_serial.ps1` — Serial monitor with DTR/RTS reset capability
2. **Sensing Server**`wifi-densepose-sensing-server` started with:
- `--source esp32` — Expect real ESP32 UDP frames
- `--bind-addr 0.0.0.0` — Accept connections from any interface
- `--ui-path <path>` — Serve the demo UI via HTTP
3. **Browser Demo**`main.js` updated to auto-connect to `ws://localhost:8765/ws/sensing` on page load. Falls back to simulated CSI if the WebSocket is unavailable (GitHub Pages).
### Network Configuration
The ESP32 sends UDP packets to a configured target IP. If the PC's IP doesn't match the firmware's compiled target, a secondary IP alias can be added:
```powershell
# PowerShell (Admin)
New-NetIPAddress -IPAddress 192.168.1.100 -PrefixLength 24 -InterfaceAlias "Wi-Fi"
```
### Data Flow
| Stage | Protocol | Format | Rate |
|-------|----------|--------|------|
| ESP32 → Server | UDP | ADR-018 binary frame (magic `0xC5110001`, I/Q pairs) | ~100 Hz |
| Server → Browser | WebSocket | ADR-018 binary frame (forwarded) | ~10 Hz (tick-ms=100) |
| Browser decode | JavaScript | Float32 amplitude/phase arrays | Per frame |
### Build Environment (Windows)
ESP-IDF v5.4.0 on Windows requires:
- IDF_PATH pointing to the ESP-IDF framework
- IDF_TOOLS_PATH pointing to toolchain binaries
- MSYS/MinGW environment variables removed (ESP-IDF rejects them)
- Python venv from ESP-IDF tools for `idf.py` execution
The `build_firmware.ps1` script handles all of this automatically.
## Consequences
### Positive
- First end-to-end demonstration of real WiFi CSI → pose estimation in a browser
- No Docker required for firmware builds on Windows
- Demo gracefully degrades to simulated CSI when no server is available
- Same demo works on GitHub Pages (simulated) and locally (live ESP32)
### Negative
- ESP32 target IP is compiled into firmware; changing it requires a rebuild or NVS override
- Windows firewall may block UDP:5005; user must allow it
- Mixed content restrictions prevent HTTPS pages from connecting to ws:// (local only)
## Related
- [ADR-018](ADR-018-esp32-dev-implementation.md) — ESP32 CSI frame format and UDP streaming
- [ADR-058](ADR-058-ruvector-wasm-browser-pose-example.md) — Dual-modal WASM browser pose demo
- [ADR-039](ADR-039-edge-intelligence-framework.md) — Edge intelligence on ESP32
- Issue [#245](https://github.com/ruvnet/RuView/issues/245) — Tracking issue
@@ -1,59 +0,0 @@
# ADR-060: Provision Channel Override and MAC Address Filtering
- **Status:** Accepted
- **Date:** 2026-03-12
- **Issues:** [#247](https://github.com/ruvnet/RuView/issues/247), [#229](https://github.com/ruvnet/RuView/issues/229)
## Context
Two related provisioning gaps were reported by users:
1. **Channel mismatch (Issue #247):** The CSI collector initializes on the
Kconfig default channel (typically 6), even when the ESP32 connects to an AP
on a different channel (e.g. 11). On managed networks where the user cannot
change the router channel, this makes nodes undiscoverable. The
`provision.py` script has no `--channel` argument.
2. **Missing MAC filter (Issue #229):** The v0.2.0 release notes documented a
`--filter-mac` argument for `provision.py`, but it was never implemented.
The firmware's CSI callback accepts frames from all sources, causing signal
mixing in multi-AP environments.
## Decision
### Channel configuration
- Add `--channel` argument to `provision.py` that writes a `csi_channel` key
(u8) to NVS.
- In `nvs_config.c`, read the `csi_channel` key and override
`channel_list[0]` when present.
- In `csi_collector_init()`, after WiFi connects, auto-detect the AP channel
via `esp_wifi_sta_get_ap_info()` and use it as the default CSI channel when
no NVS override is set. This ensures the CSI collector always matches the
connected AP's channel without requiring manual provisioning.
### MAC address filtering
- Add `--filter-mac` argument to `provision.py` that writes a `filter_mac`
key (6-byte blob) to NVS.
- In `nvs_config.h`, add a `filter_mac[6]` field and `filter_mac_set` flag.
- In `nvs_config.c`, read the `filter_mac` blob from NVS.
- In the CSI callback (`wifi_csi_callback`), if `filter_mac_set` is true,
compare the source MAC from the received frame against the configured MAC
and drop non-matching frames.
### Provisioning flow
```
python provision.py --port COM7 --channel 11
python provision.py --port COM7 --filter-mac "AA:BB:CC:DD:EE:FF"
python provision.py --port COM7 --channel 11 --filter-mac "AA:BB:CC:DD:EE:FF"
```
## Consequences
- Users on managed networks can force the CSI channel to match their AP
- Multi-AP environments can filter CSI to a single source
- Auto-channel detection eliminates the most common misconfiguration
- Backward compatible: existing provisioned nodes without these keys behave
as before (use Kconfig default channel, accept all MACs)
File diff suppressed because it is too large Load Diff
-199
View File
@@ -1,199 +0,0 @@
# ADR-062: QEMU ESP32-S3 Swarm Configurator
| Field | Value |
|-------------|------------------------------------------------|
| **Status** | Accepted |
| **Date** | 2026-03-14 |
| **Authors** | RuView Team |
| **Relates** | ADR-061 (QEMU testing platform), ADR-060 (channel/MAC filter), ADR-018 (binary frame), ADR-039 (edge intel) |
## Glossary
| Term | Definition |
|------|-----------|
| Swarm | A group of N QEMU ESP32-S3 instances running simultaneously |
| Topology | How nodes are connected: star, mesh, line, ring |
| Role | Node function: `sensor` (collects CSI), `coordinator` (aggregates + forwards), `gateway` (bridges to host) |
| Scenario matrix | Cross-product of topology × node count × NVS config × mock scenario |
| Health oracle | Python process that monitors all node UART logs and declares swarm health |
## Context
ADR-061 Layer 3 provides a basic multi-node mesh test: N identical nodes with sequential TDM slots connected via a Linux bridge. This is useful but limited:
1. **All nodes are identical** — real deployments have heterogeneous roles (sensor, coordinator, gateway)
2. **Single topology** — only fully-connected bridge; no star, line, or ring topologies
3. **No scenario variation per node** — all nodes run the same mock CSI scenario
4. **Manual configuration** — each test requires hand-editing env vars and arguments
5. **No swarm-level health monitoring** — validation checks individual nodes, not collective behavior
6. **No cross-node timing validation** — TDM slot ordering and inter-frame gaps aren't verified
Real WiFi-DensePose deployments use 3-8 ESP32-S3 nodes in various topologies. A single coordinator aggregates CSI from multiple sensors. The firmware must handle TDM conflicts, missing nodes, role-based behavior differences, and network partitions — none of which ADR-061 Layer 3 tests.
## Decision
Build a **QEMU Swarm Configurator** — a YAML-driven tool that defines multi-node test scenarios declaratively and orchestrates them under QEMU with swarm-level validation.
### Architecture
```
┌─────────────────────────────────────────────────────┐
│ swarm_config.yaml │
│ nodes: [{role: sensor, scenario: 2, channel: 6}] │
│ topology: star │
│ duration: 60s │
│ assertions: [all_nodes_boot, tdm_no_collision, ...] │
└──────────────────────┬──────────────────────────────┘
┌────────────▼────────────┐
│ qemu_swarm.py │
│ (orchestrator) │
└───┬────┬────┬───┬──────┘
│ │ │ │
┌────▼┐ ┌▼──┐ ▼ ┌▼────┐
│Node0│ │N1 │... │N(n-1)│ QEMU instances
│sens │ │sen│ │coord │
└──┬──┘ └─┬─┘ └──┬───┘
│ │ │
┌──▼──────▼─────────▼──┐
│ Virtual Network │ TAP bridge / SLIRP
│ (topology-shaped) │
└──────────┬───────────┘
┌──────────▼───────────┐
│ Aggregator (Rust) │ Collects frames
└──────────┬───────────┘
┌──────────▼───────────┐
│ Health Oracle │ Swarm-level assertions
│ (swarm_health.py) │
└──────────────────────┘
```
### YAML Configuration Schema
```yaml
# swarm_config.yaml
swarm:
name: "3-sensor-star"
duration_s: 60
topology: star # star | mesh | line | ring
aggregator_port: 5005
nodes:
- role: coordinator
node_id: 0
scenario: 0 # empty room (baseline)
channel: 6
edge_tier: 2
is_gateway: true # receives aggregated frames
- role: sensor
node_id: 1
scenario: 2 # walking person
channel: 6
tdm_slot: 1 # TDM slot index (auto-assigned from node position if omitted)
- role: sensor
node_id: 2
scenario: 3 # fall event
channel: 6
tdm_slot: 2
assertions:
- all_nodes_boot
- no_crashes
- tdm_no_collision
- all_nodes_produce_frames
- coordinator_receives_from_all
- fall_detected_by_node_2
- frame_rate_above: 15 # Hz minimum per node
- max_boot_time_s: 10
```
### Topologies
| Topology | Network | Description |
|----------|---------|-------------|
| `star` | All sensors connect to coordinator; coordinator has TAP to each sensor | Hub-and-spoke, most common |
| `mesh` | All nodes on same bridge (existing Layer 3 behavior) | Every node sees every other |
| `line` | Node 0 ↔ Node 1 ↔ Node 2 ↔ ... | Linear chain, tests multi-hop |
| `ring` | Like line but last connects to first | Circular, tests routing |
### Node Roles
| Role | Behavior | NVS Keys |
|------|----------|----------|
| `sensor` | Runs mock CSI, sends frames to coordinator | `node_id`, `tdm_slot`, `target_ip` |
| `coordinator` | Receives frames from sensors, runs edge aggregation | `node_id`, `tdm_slot=0`, `edge_tier=2` |
| `gateway` | Like coordinator but also bridges to host UDP | `node_id`, `target_ip=host`, `is_gateway=1` |
### Assertions (Swarm-Level)
| Assertion | What It Checks |
|-----------|---------------|
| `all_nodes_boot` | Every node's UART log shows boot indicators within timeout |
| `no_crashes` | No Guru Meditation, assert, panic in any log |
| `tdm_no_collision` | No two nodes transmit in the same TDM slot |
| `all_nodes_produce_frames` | Every sensor node's log contains CSI frame output |
| `coordinator_receives_from_all` | Coordinator log shows frames from each sensor's node_id |
| `fall_detected_by_node_N` | Node N's log reports a fall detection event |
| `frame_rate_above` | Each node produces at least N frames/second |
| `max_boot_time_s` | All nodes boot within N seconds |
| `no_heap_errors` | No OOM or heap corruption in any log |
| `network_partitioned_recovery` | After deliberate partition, nodes resume communication (future) |
### Preset Configurations
| Preset | Nodes | Topology | Purpose |
|--------|-------|----------|---------|
| `smoke` | 2 | star | Quick CI smoke test (15s) |
| `standard` | 3 | star | Default 3-node (sensor + sensor + coordinator) |
| `large-mesh` | 6 | mesh | Scale test with 6 fully-connected nodes |
| `line-relay` | 4 | line | Multi-hop relay chain |
| `ring-fault` | 4 | ring | Ring with fault injection mid-test |
| `heterogeneous` | 5 | star | Mixed scenarios: walk, fall, static, channel-sweep, empty |
| `ci-matrix` | 3 | star | CI-optimized preset (30s, minimal assertions) |
## File Layout
```
scripts/
├── qemu_swarm.py # Main orchestrator (CLI entry point)
├── swarm_health.py # Swarm-level health oracle
└── swarm_presets/
├── smoke.yaml
├── standard.yaml
├── large_mesh.yaml
├── line_relay.yaml
├── ring_fault.yaml
├── heterogeneous.yaml
└── ci_matrix.yaml
.github/workflows/
└── firmware-qemu.yml # MODIFIED: add swarm test job
```
## Consequences
### Benefits
1. **Declarative testing** — define swarm topology in YAML, not shell scripts
2. **Role-based nodes** — test coordinator/sensor/gateway interactions
3. **Topology variety** — star/mesh/line/ring match real deployment patterns
4. **Swarm-level assertions** — validate collective behavior, not just individual nodes
5. **Preset library** — quick CI smoke tests and thorough manual validation
6. **Reproducible** — YAML configs are version-controlled and shareable
### Limitations
1. **Still requires root** for TAP bridge topologies (star, line, ring); mesh can use SLIRP
2. **QEMU resource usage** — 6+ QEMU instances use ~2GB RAM, may slow CI runners
3. **No real RF** — inter-node communication is IP-based, not WiFi CSI multipath
## References
- ADR-061: QEMU ESP32-S3 firmware testing platform (Layers 1-9)
- ADR-060: Channel override and MAC address filter provisioning
- ADR-018: Binary CSI frame format (magic `0xC5110001`)
- ADR-039: Edge intelligence pipeline (biquad, vitals, fall detection)
+14 -377
View File
@@ -38,17 +38,8 @@ WiFi DensePose turns commodity WiFi signals into real-time human pose estimation
- [ESP32-S3 Mesh](#esp32-s3-mesh)
- [Intel 5300 / Atheros NIC](#intel-5300--atheros-nic)
15. [Docker Compose (Multi-Service)](#docker-compose-multi-service)
16. [Testing Firmware Without Hardware (QEMU)](#testing-firmware-without-hardware-qemu)
- [What You Need](#what-you-need)
- [Your First Test Run](#your-first-test-run)
- [Understanding the Test Output](#understanding-the-test-output)
- [Testing Multiple Nodes at Once (Swarm)](#testing-multiple-nodes-at-once-swarm)
- [Swarm Presets](#swarm-presets)
- [Writing Your Own Swarm Config](#writing-your-own-swarm-config)
- [Debugging Firmware in QEMU](#debugging-firmware-in-qemu)
- [Running the Full Test Suite](#running-the-full-test-suite)
17. [Troubleshooting](#troubleshooting)
18. [FAQ](#faq)
16. [Troubleshooting](#troubleshooting)
17. [FAQ](#faq)
---
@@ -87,17 +78,6 @@ docker pull ruvnet/wifi-densepose:latest
Multi-architecture image (amd64 + arm64). Works on Intel/AMD and Apple Silicon Macs. Contains the Rust sensing server, Three.js UI, and all signal processing.
**Data source selection:** Use the `CSI_SOURCE` environment variable to select the sensing mode:
| Value | Description |
|-------|-------------|
| `auto` | (default) Probe for ESP32 on UDP 5005, fall back to simulation |
| `esp32` | Receive real CSI frames from ESP32 devices over UDP |
| `simulated` | Generate synthetic CSI frames (no hardware required) |
| `wifi` | Host Wi-Fi RSSI (not available inside containers) |
Example: `docker run -e CSI_SOURCE=esp32 -p 3000:3000 -p 5005:5005/udp ruvnet/wifi-densepose:latest`
### From Source (Rust)
```bash
@@ -287,8 +267,8 @@ Real Channel State Information at 20 Hz with 56-192 subcarriers. Required for po
# From source
./target/release/sensing-server --source esp32 --udp-port 5005 --http-port 3000 --ws-port 3001
# Docker (use CSI_SOURCE environment variable)
docker run -p 3000:3000 -p 3001:3001 -p 5005:5005/udp -e CSI_SOURCE=esp32 ruvnet/wifi-densepose:latest
# Docker
docker run -p 3000:3000 -p 3001:3001 -p 5005:5005/udp ruvnet/wifi-densepose:latest --source esp32
```
The ESP32 nodes stream binary CSI frames over UDP to port 5005. See [Hardware Setup](#esp32-s3-mesh) for flashing instructions.
@@ -699,11 +679,9 @@ Download the dataset files and place them in a `data/` directory.
./target/release/sensing-server --train --dataset data/ --dataset-type mmfi --epochs 100 --save-rvf model.rvf
# Via Docker (mount your data directory)
# Note: Training mode requires overriding the default entrypoint
docker run --rm \
-v $(pwd)/data:/data \
-v $(pwd)/output:/output \
--entrypoint /app/sensing-server \
ruvnet/wifi-densepose:latest \
--train --dataset /data --epochs 100 --export-rvf /output/model.rvf
```
@@ -819,27 +797,14 @@ Pre-built binaries are available at [Releases](https://github.com/ruvnet/RuView/
| Release | What It Includes | Tag |
|---------|-----------------|-----|
| [v0.4.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.1-esp32) | **Stable** — CSI build fix, compile guard, AMOLED display, edge intelligence ([ADR-057](../docs/adr/ADR-057-firmware-csi-build-guard.md)) | `v0.4.1-esp32` |
| [v0.2.0](https://github.com/ruvnet/RuView/releases/tag/v0.2.0-esp32) | Stable — raw CSI streaming, TDM, channel hopping, QUIC mesh | `v0.2.0-esp32` |
| [v0.3.0-alpha](https://github.com/ruvnet/RuView/releases/tag/v0.3.0-alpha-esp32) | Alpha — adds on-device edge intelligence (ADR-039) | `v0.3.0-alpha-esp32` |
| [v0.2.0](https://github.com/ruvnet/RuView/releases/tag/v0.2.0-esp32) | Raw CSI streaming, TDM, channel hopping, QUIC mesh | `v0.2.0-esp32` |
> **Important:** Firmware versions prior to v0.4.1 had CSI **disabled** in the build config, causing a runtime error (`E wifi:CSI not enabled in menuconfig!`). Always use v0.4.1 or later.
```bash
# Flash an ESP32-S3 with 8MB flash (most boards)
# Flash an ESP32-S3 (requires esptool: pip install esptool)
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
write-flash --flash-mode dio --flash-size 8MB --flash-freq 80m \
0x0 bootloader.bin 0x8000 partition-table.bin \
0xf000 ota_data_initial.bin 0x20000 esp32-csi-node.bin
```
**4MB flash boards** (e.g. ESP32-S3 SuperMini 4MB): download the 4MB binaries from the [v0.4.3 release](https://github.com/ruvnet/RuView/releases/tag/v0.4.3-esp32) and use `--flash-size 4MB`:
```bash
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
write-flash --flash-mode dio --flash-size 4MB --flash-freq 80m \
0x0 bootloader.bin 0x8000 partition-table-4mb.bin \
0xF000 ota_data_initial.bin 0x20000 esp32-csi-node-4mb.bin
write-flash --flash-mode dio --flash-size 4MB \
0x0 bootloader.bin 0x8000 partition-table.bin 0x10000 esp32-csi-node.bin
```
**Provisioning:**
@@ -920,8 +885,8 @@ Binary size: 777 KB (24% free in the 1 MB app partition).
# From source
./target/release/sensing-server --source esp32 --udp-port 5005 --http-port 3000 --ws-port 3001
# Docker (use CSI_SOURCE environment variable)
docker run -p 3000:3000 -p 3001:3001 -p 5005:5005/udp -e CSI_SOURCE=esp32 ruvnet/wifi-densepose:latest
# Docker
docker run -p 3000:3000 -p 3001:3001 -p 5005:5005/udp ruvnet/wifi-densepose:latest --source esp32
```
See [ADR-018](../docs/adr/ADR-018-esp32-dev-implementation.md), [ADR-029](../docs/adr/ADR-029-ruvsense-multistatic-sensing-mode.md), and [Tutorial #34](https://github.com/ruvnet/RuView/issues/34).
@@ -954,288 +919,6 @@ This starts:
---
## Testing Firmware Without Hardware (QEMU)
You can test the ESP32-S3 firmware on your computer without any physical hardware. The project uses **QEMU** — an emulator that pretends to be an ESP32-S3 chip, running the real firmware code inside a virtual machine on your PC.
This is useful when:
- You don't have an ESP32-S3 board yet
- You want to test firmware changes before flashing to real hardware
- You're running automated tests in CI/CD
- You want to simulate multiple ESP32 nodes talking to each other
### What You Need
**Required:**
- Python 3.8+ (you probably already have this)
- QEMU with ESP32-S3 support (Espressif's fork)
**Install QEMU (one-time setup):**
```bash
# Easiest: use the automated installer (installs QEMU + Python tools)
bash scripts/install-qemu.sh
# Or check what's already installed:
bash scripts/install-qemu.sh --check
```
The installer detects your OS (Ubuntu, Fedora, macOS, etc.), installs build dependencies, clones Espressif's QEMU fork, builds it, and adds it to your PATH. It also installs the Python tools (`esptool`, `pyyaml`, `esp-idf-nvs-partition-gen`).
<details>
<summary>Manual installation (if you prefer)</summary>
```bash
# Build from source
git clone https://github.com/espressif/qemu.git
cd qemu
./configure --target-list=xtensa-softmmu --enable-slirp
make -j$(nproc)
export QEMU_PATH=$(pwd)/build/qemu-system-xtensa
# Install Python tools
pip install esptool pyyaml esp-idf-nvs-partition-gen
```
</details>
**For multi-node testing (optional):**
```bash
# Linux only — needed for virtual network bridges
sudo apt install socat bridge-utils iproute2
```
### The `qemu-cli.sh` Command
All QEMU testing is available through a single command:
```bash
bash scripts/qemu-cli.sh <command>
```
| Command | What it does |
|---------|-------------|
| `install` | Install QEMU (runs the installer above) |
| `test` | Run single-node firmware test |
| `swarm --preset smoke` | Quick 2-node swarm test |
| `swarm --preset standard` | Standard 3-node test |
| `mesh 3` | Multi-node mesh test |
| `chaos` | Fault injection resilience test |
| `fuzz --duration 60` | Run fuzz testing |
| `status` | Show what's installed and ready |
| `help` | Show all commands |
### Your First Test Run
The simplest way to test the firmware:
```bash
# Using the CLI:
bash scripts/qemu-cli.sh test
# Or directly:
bash scripts/qemu-esp32s3-test.sh
```
**What happens behind the scenes:**
1. The firmware is compiled with a "mock CSI" mode — instead of reading real WiFi signals, it generates synthetic test data that mimics real people walking, falling, or breathing
2. The compiled firmware is loaded into QEMU, which boots it like a real ESP32-S3
3. The emulator's serial output (what you'd see on a USB cable) is captured
4. A validation script checks the output for expected behavior and errors
If you already built the firmware and want to skip rebuilding:
```bash
SKIP_BUILD=1 bash scripts/qemu-esp32s3-test.sh
```
To give it more time (useful on slower machines):
```bash
QEMU_TIMEOUT=120 bash scripts/qemu-esp32s3-test.sh
```
### Understanding the Test Output
The test runs 16 checks on the firmware's output. Here's what a successful run looks like:
```
=== QEMU ESP32-S3 Firmware Test (ADR-061) ===
[PASS] Boot: Firmware booted successfully
[PASS] NVS config: Configuration loaded from flash
[PASS] Mock CSI: Synthetic WiFi data generator started
[PASS] Edge processing: Signal analysis pipeline running
[PASS] Frame serialization: Data packets formatted correctly
[PASS] No crashes: No error conditions detected
...
16/16 checks passed
=== Test Complete (exit code: 0) ===
```
**Exit codes explained:**
| Code | Meaning | What to do |
|------|---------|-----------|
| 0 | **PASS** — everything works | Nothing, you're good! |
| 1 | **WARN** — minor issues | Review the output; usually safe to continue |
| 2 | **FAIL** — something broke | Check the `[FAIL]` lines for what went wrong |
| 3 | **FATAL** — can't even start | Usually a missing tool or build failure; check error messages |
### Testing Multiple Nodes at Once (Swarm)
Real deployments use 3-8 ESP32 nodes. The **swarm configurator** lets you simulate multiple nodes on your computer, each with a different role:
- **Sensor nodes** — generate WiFi signal data (like ESP32s placed around a room)
- **Coordinator node** — collects data from all sensors and runs analysis
- **Gateway node** — bridges data to your computer
```bash
# Quick 2-node smoke test (15 seconds)
python3 scripts/qemu_swarm.py --preset smoke
# Standard 3-node test: 2 sensors + 1 coordinator (60 seconds)
python3 scripts/qemu_swarm.py --preset standard
# See what's available
python3 scripts/qemu_swarm.py --list-presets
# Preview what would run (without actually running)
python3 scripts/qemu_swarm.py --preset standard --dry-run
```
**Note:** Multi-node testing with virtual bridges requires Linux and `sudo`. On other systems, nodes use a simpler networking mode where each node can reach the coordinator but not each other.
### Swarm Presets
| Preset | Nodes | Duration | Best for |
|--------|-------|----------|----------|
| `smoke` | 2 | 15s | Quick check that things work |
| `standard` | 3 | 60s | Normal development testing |
| `ci_matrix` | 3 | 30s | CI/CD pipelines |
| `large_mesh` | 6 | 90s | Testing at scale |
| `line_relay` | 4 | 60s | Multi-hop relay testing |
| `ring_fault` | 4 | 75s | Fault tolerance testing |
| `heterogeneous` | 5 | 90s | Mixed scenario testing |
### Writing Your Own Swarm Config
Create a YAML file describing your test scenario:
```yaml
# my_test.yaml
swarm:
name: my-custom-test
duration_s: 45
topology: star # star, mesh, line, or ring
aggregator_port: 5005
nodes:
- role: coordinator
node_id: 0
scenario: 0 # 0=empty room (baseline)
channel: 6
edge_tier: 2
- role: sensor
node_id: 1
scenario: 2 # 2=walking person
channel: 6
tdm_slot: 1
- role: sensor
node_id: 2
scenario: 3 # 3=fall event
channel: 6
tdm_slot: 2
assertions:
- all_nodes_boot # Did every node start up?
- no_crashes # Any error/panic?
- all_nodes_produce_frames # Is each sensor generating data?
- fall_detected_by_node_2 # Did node 2 detect the fall?
```
**Available scenarios** (what kind of fake WiFi data to generate):
| # | Scenario | Description |
|---|----------|-------------|
| 0 | Empty room | Baseline with just noise |
| 1 | Static person | Someone standing still |
| 2 | Walking | Someone walking across the room |
| 3 | Fall | Someone falling down |
| 4 | Multiple people | Two people in the room |
| 5 | Channel sweep | Cycling through WiFi channels |
| 6 | MAC filter | Testing device filtering |
| 7 | Ring overflow | Stress test with burst of data |
| 8 | RSSI sweep | Signal strength from weak to strong |
| 9 | Zero-length | Edge case: empty data packet |
**Topology options:**
| Topology | Shape | When to use |
|----------|-------|-------------|
| `star` | All sensors connect to one coordinator | Most common setup |
| `mesh` | Every node can talk to every other | Testing fully connected networks |
| `line` | Nodes in a chain (A → B → C → D) | Testing relay/forwarding |
| `ring` | Chain with ends connected | Testing circular routing |
Run your custom config:
```bash
python3 scripts/qemu_swarm.py --config my_test.yaml
```
### Debugging Firmware in QEMU
If something goes wrong, you can attach a debugger to the emulated ESP32:
```bash
# Terminal 1: Start QEMU with debug support (paused at boot)
qemu-system-xtensa -machine esp32s3 -nographic \
-drive file=firmware/esp32-csi-node/build/qemu_flash.bin,if=mtd,format=raw \
-s -S
# Terminal 2: Connect the debugger
xtensa-esp-elf-gdb firmware/esp32-csi-node/build/esp32-csi-node.elf \
-ex "target remote :1234" \
-ex "break app_main" \
-ex "continue"
```
Or use VS Code: open the project, press **F5**, and select **"QEMU ESP32-S3 Debug"**.
### Running the Full Test Suite
For thorough validation before submitting a pull request:
```bash
# 1. Single-node test (2 minutes)
bash scripts/qemu-esp32s3-test.sh
# 2. Multi-node swarm test (1 minute)
python3 scripts/qemu_swarm.py --preset standard
# 3. Fuzz testing — finds edge-case crashes (1-5 minutes)
cd firmware/esp32-csi-node/test
make all CC=clang
make run_serialize FUZZ_DURATION=60
make run_edge FUZZ_DURATION=60
make run_nvs FUZZ_DURATION=60
# 4. NVS configuration matrix — tests 14 config combinations
python3 scripts/generate_nvs_matrix.py --output-dir build/nvs_matrix
# 5. Chaos testing — injects faults to test resilience (2 minutes)
bash scripts/qemu-chaos-test.sh
```
All of these also run automatically in CI when you push changes to `firmware/`.
---
## Troubleshooting
### Docker: "no matching manifest for linux/arm64" on macOS
@@ -1270,17 +953,12 @@ Add the WebSocket port mapping:
docker run -p 3000:3000 -p 3001:3001 ruvnet/wifi-densepose:latest
```
### ESP32: "CSI not enabled in menuconfig"
Firmware versions prior to v0.4.1 had `CONFIG_ESP_WIFI_CSI_ENABLED` disabled in the build config. Upgrade to [v0.4.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.1-esp32) or later. If building from source, ensure `sdkconfig.defaults` exists (not just `sdkconfig.defaults.template`). See [ADR-057](../docs/adr/ADR-057-firmware-csi-build-guard.md).
### ESP32: No data arriving
1. Verify firmware is v0.4.1+ (older versions had CSI disabled — see above)
2. Verify the ESP32 is connected to the same WiFi network
3. Check the target IP matches the sensing server machine: `python firmware/esp32-csi-node/provision.py --port COM7 --target-ip <YOUR_IP>`
4. Verify UDP port 5005 is not blocked by firewall
5. Test with: `nc -lu 5005` (Linux) or similar UDP listener
1. Verify the ESP32 is connected to the same WiFi network
2. Check the target IP matches the sensing server machine: `python firmware/esp32-csi-node/provision.py --port COM7 --target-ip <YOUR_IP>`
3. Verify UDP port 5005 is not blocked by firewall
4. Test with: `nc -lu 5005` (Linux) or similar UDP listener
### Build: Rust compilation errors
@@ -1315,47 +993,6 @@ The server applies a 3-stage smoothing pipeline (ADR-048). If readings are still
- Hard refresh with Ctrl+Shift+R to clear cached settings
- The auto-detect probes `/health` on the same origin — cross-origin won't work
### QEMU: "qemu-system-xtensa: command not found"
QEMU for ESP32-S3 must be built from Espressif's fork — it is not in standard package managers:
```bash
git clone https://github.com/espressif/qemu.git
cd qemu && ./configure --target-list=xtensa-softmmu && make -j$(nproc)
export QEMU_PATH=$(pwd)/build/qemu-system-xtensa
```
Or point to an existing build: `QEMU_PATH=/path/to/qemu-system-xtensa bash scripts/qemu-esp32s3-test.sh`
### QEMU: Test times out with no output
The emulator is slower than real hardware. Increase the timeout:
```bash
QEMU_TIMEOUT=120 bash scripts/qemu-esp32s3-test.sh
```
If there's truly no output at all, the firmware build may have failed. Rebuild without `SKIP_BUILD`:
```bash
bash scripts/qemu-esp32s3-test.sh # without SKIP_BUILD
```
### QEMU: "esptool not found"
Install it with pip: `pip install esptool`
### QEMU Swarm: "Must be run as root"
Multi-node swarm tests with virtual network bridges require root on Linux. Two options:
1. Run with sudo: `sudo python3 scripts/qemu_swarm.py --preset standard`
2. Skip bridges (nodes use simpler networking): the tool automatically falls back on non-root systems, but nodes can't communicate with each other (only with the aggregator)
### QEMU Swarm: "yaml module not found"
Install PyYAML: `pip install pyyaml`
---
## FAQ
@@ -0,0 +1,7 @@
{"type":"edit","file":"unknown","timestamp":1773152422749,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773152444021,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773152460956,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773152493971,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773152501432,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773152510853,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773152596890,"sessionId":null}
@@ -0,0 +1,12 @@
{
"id": "session-1773152560779",
"startedAt": "2026-03-10T14:22:40.779Z",
"cwd": "/Users/cohen/GitHub/ruvnet/RuView/firmware/esp32-csi-node",
"context": {},
"metrics": {
"edits": 1,
"commands": 0,
"tasks": 0,
"errors": 0
}
}
-228
View File
@@ -523,231 +523,6 @@ The firmware is continuously verified by [`.github/workflows/firmware-ci.yml`](.
---
## QEMU Testing (ADR-061)
Test the firmware without physical hardware using Espressif's QEMU fork. A compile-time mock CSI generator (`CONFIG_CSI_MOCK_ENABLED=y`) replaces the real WiFi CSI callback with a timer-driven synthetic frame injector that exercises the full edge processing pipeline -- biquad filtering, Welford stats, top-K selection, presence/fall detection, and vitals extraction.
### Prerequisites
- **ESP-IDF v5.4** -- [installation guide](https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32s3/get-started/)
- **Espressif QEMU fork** -- must be built from source (not in Ubuntu packages):
```bash
git clone --depth 1 https://github.com/espressif/qemu.git /tmp/qemu
cd /tmp/qemu
./configure --target-list=xtensa-softmmu --enable-slirp
make -j$(nproc)
sudo cp build/qemu-system-xtensa /usr/local/bin/
```
### Quick Start
Three commands to go from source to running firmware in QEMU:
```bash
cd firmware/esp32-csi-node
# 1. Build with mock CSI enabled (replaces real WiFi CSI with synthetic frames)
idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.qemu" build
# 2. Create merged flash image
esptool.py --chip esp32s3 merge_bin -o build/qemu_flash.bin \
--flash_mode dio --flash_freq 80m --flash_size 8MB \
0x0 build/bootloader/bootloader.bin \
0x8000 build/partition_table/partition-table.bin \
0x20000 build/esp32-csi-node.bin
# 3. Run in QEMU
qemu-system-xtensa -machine esp32s3 -nographic \
-drive file=build/qemu_flash.bin,if=mtd,format=raw \
-serial mon:stdio -no-reboot
```
The firmware boots FreeRTOS, loads NVS config, starts the mock CSI generator at 20 Hz, and runs all edge processing. UART output shows log lines that can be validated automatically.
### Mock CSI Scenarios
The mock generator cycles through 10 scenarios that exercise every edge processing path:
| ID | Scenario | Duration | Expected Output |
|----|----------|----------|-----------------|
| 0 | Empty room | 10 s | `presence=0`, `motion_energy < thresh` |
| 1 | Static person | 10 s | `presence=1`, `breathing_rate` in [10, 25], `fall=0` |
| 2 | Walking person | 10 s | `presence=1`, `motion_energy > 0.5`, `fall=0` |
| 3 | Fall event | 5 s | `fall=1` flag set, `motion_energy` spike |
| 4 | Multi-person | 15 s | `n_persons=2`, independent breathing rates |
| 5 | Channel sweep | 5 s | Frames on channels 1, 6, 11 in sequence |
| 6 | MAC filter test | 5 s | Frames with wrong MAC dropped (counter check) |
| 7 | Ring buffer overflow | 3 s | 1000 frames in 100 ms burst, graceful drop |
| 8 | Boundary RSSI | 5 s | RSSI sweeps -127 to 0, no crash |
| 9 | Zero-length frame | 2 s | `iq_len=0` frames, serialize returns 0 |
### NVS Provisioning Matrix
14 NVS configurations are tested in CI to ensure all config paths work correctly:
| Config | NVS Values | Validates |
|--------|-----------|-----------|
| `default` | (empty NVS) | Kconfig fallback paths |
| `wifi-only` | ssid, password | Basic provisioning |
| `full-adr060` | channel=6, filter_mac=AA:BB:CC:DD:EE:FF | Channel override + MAC filter |
| `edge-tier0` | edge_tier=0 | Raw CSI passthrough (no DSP) |
| `edge-tier1` | edge_tier=1, pres_thresh=100, fall_thresh=2000 | Stats-only mode |
| `edge-tier2-custom` | edge_tier=2, vital_win=128, vital_int=500, subk_count=16 | Full vitals with custom params |
| `tdm-3node` | tdm_slot=1, tdm_nodes=3, node_id=1 | TDM mesh timing |
| `wasm-signed` | wasm_max=4, wasm_verify=1, wasm_pubkey=<32B> | WASM with Ed25519 verification |
| `wasm-unsigned` | wasm_max=2, wasm_verify=0 | WASM without signature check |
| `5ghz-channel` | channel=36, filter_mac=... | 5 GHz CSI collection |
| `boundary-max` | target_port=65535, node_id=255, top_k=32, vital_win=256 | Max-range values |
| `boundary-min` | target_port=1, node_id=0, top_k=1, vital_win=32 | Min-range values |
| `power-save` | power_duty=10, edge_tier=0 | Low-power mode |
| `corrupt-nvs` | (partial/corrupt partition) | Graceful fallback to defaults |
Generate all configs for CI testing:
```bash
python scripts/generate_nvs_matrix.py
```
### Validation Checks
The output validation script (`scripts/validate_qemu_output.py`) parses UART logs and checks:
| Check | Pass Criteria | Severity |
|-------|---------------|----------|
| Boot | `app_main()` called, no panic/assert | FATAL |
| NVS load | `nvs_config:` log line present | FATAL |
| Mock CSI init | `mock_csi: Starting mock CSI generator` | FATAL |
| Frame generation | `mock_csi: Generated N frames` where N > 0 | ERROR |
| Edge pipeline | `edge_processing: DSP task started on Core 1` | ERROR |
| Vitals output | At least one `vitals:` log line with valid BPM | ERROR |
| Presence detection | `presence=1` during person scenarios | WARN |
| Fall detection | `fall=1` during fall scenario | WARN |
| MAC filter | `csi_collector: MAC filter dropped N frames` where N > 0 | WARN |
| ADR-018 serialize | `csi_collector: Serialized N frames` where N > 0 | ERROR |
| No crash | No `Guru Meditation Error`, no `assert failed`, no `abort()` | FATAL |
| Clean exit | Firmware reaches end of scenario sequence | ERROR |
| Heap OK | No `HEAP_ERROR` or `out of memory` | FATAL |
| Stack OK | No `Stack overflow` detected | FATAL |
Exit codes: `0` = all pass, `1` = WARN only, `2` = ERROR, `3` = FATAL.
### GDB Debugging
QEMU provides a built-in GDB stub for zero-cost breakpoint debugging without JTAG hardware:
```bash
# Launch QEMU paused, with GDB stub on port 1234
qemu-system-xtensa \
-machine esp32s3 -nographic \
-drive file=build/qemu_flash.bin,if=mtd,format=raw \
-serial mon:stdio \
-s -S
# In another terminal, attach GDB
xtensa-esp-elf-gdb build/esp32-csi-node.elf \
-ex "target remote :1234" \
-ex "b edge_processing.c:dsp_task" \
-ex "b csi_collector.c:csi_serialize_frame" \
-ex "b mock_csi.c:mock_generate_csi_frame" \
-ex "watch g_nvs_config.csi_channel" \
-ex "continue"
```
Key breakpoints:
| Location | Purpose |
|----------|---------|
| `edge_processing.c:dsp_task` | DSP consumer loop entry |
| `edge_processing.c:presence_detect` | Threshold comparison |
| `edge_processing.c:fall_detect` | Phase acceleration check |
| `csi_collector.c:csi_serialize_frame` | ADR-018 serialization |
| `nvs_config.c:nvs_config_load` | NVS parse logic |
| `wasm_runtime.c:wasm_on_csi` | WASM module dispatch |
| `mock_csi.c:mock_generate_csi_frame` | Synthetic frame generation |
VS Code integration -- add to `.vscode/launch.json`:
```json
{
"name": "QEMU ESP32-S3 Debug",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/firmware/esp32-csi-node/build/esp32-csi-node.elf",
"miDebuggerPath": "xtensa-esp-elf-gdb",
"miDebuggerServerAddress": "localhost:1234",
"setupCommands": [
{ "text": "set remote hardware-breakpoint-limit 2" },
{ "text": "set remote hardware-watchpoint-limit 2" }
]
}
```
### Code Coverage
Build with gcov enabled and collect coverage after a QEMU run:
```bash
# Build with coverage overlay
idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.qemu;sdkconfig.coverage" build
# After QEMU run, generate HTML report
lcov --capture --directory build --output-file coverage.info
lcov --remove coverage.info '*/esp-idf/*' '*/test/*' --output-file coverage_filtered.info
genhtml coverage_filtered.info --output-directory build/coverage_report
```
Coverage targets:
| Module | Target |
|--------|--------|
| `edge_processing.c` | >= 80% |
| `csi_collector.c` | >= 90% |
| `nvs_config.c` | >= 95% |
| `mock_csi.c` | >= 95% |
| `stream_sender.c` | >= 80% |
| `wasm_runtime.c` | >= 70% |
### Fuzz Testing
Host-native fuzz targets compiled with libFuzzer + AddressSanitizer (no QEMU needed):
```bash
cd firmware/esp32-csi-node/test
# Build fuzz target
clang -fsanitize=fuzzer,address -I../main \
fuzz_csi_serialize.c ../main/csi_collector.c \
-o fuzz_serialize
# Run for 5 minutes
timeout 300 ./fuzz_serialize corpus/ || true
```
Fuzz targets:
| Target | Input | Looking For |
|--------|-------|-------------|
| `csi_serialize_frame()` | Random `wifi_csi_info_t` | Buffer overflow, NULL deref |
| `nvs_config_load()` | Crafted NVS partition binary | No crash, fallback to defaults |
| `edge_enqueue_csi()` | Rapid-fire 10,000 frames | Ring overflow, no data corruption |
| `rvf_parser.c` | Malformed RVF packets | Parse rejection, no crash |
| `wasm_upload.c` | Corrupt WASM blobs | Rejection without crash |
### QEMU CI Workflow
The GitHub Actions workflow (`.github/workflows/firmware-qemu.yml`) runs on every push or PR touching `firmware/**`:
1. Uses the `espressif/idf:v5.4` container image
2. Builds Espressif's QEMU fork from source
3. Runs a CI matrix across NVS configurations: `default`, `nvs-full`, `nvs-edge-tier0`, `nvs-tdm-3node`
4. For each config: provisions NVS, builds with mock CSI, runs in QEMU with timeout, validates UART output
5. Uploads QEMU logs as build artifacts for debugging failures
No physical ESP32 hardware is needed in CI.
---
## Troubleshooting
| Symptom | Cause | Fix |
@@ -781,9 +556,6 @@ This firmware implements or references the following ADRs:
| [ADR-029](../../docs/adr/ADR-029-ruvsense-multistatic-sensing-mode.md) | Channel hopping and TDM protocol | Accepted |
| [ADR-039](../../docs/adr/ADR-039-esp32-edge-intelligence.md) | Edge intelligence tiers 0-2 | Accepted |
| [ADR-040](../../docs/adr/) | WASM programmable sensing (Tier 3) with RVF container format | Alpha |
| [ADR-057](../../docs/adr/ADR-057-build-time-csi-guard.md) | Build-time CSI guard (`CONFIG_ESP_WIFI_CSI_ENABLED`) | Accepted |
| [ADR-060](../../docs/adr/ADR-060-channel-mac-filter.md) | Channel override and MAC address filter | Accepted |
| [ADR-061](../../docs/adr/ADR-061-qemu-esp32s3-firmware-testing.md) | QEMU ESP32-S3 emulation for firmware testing | Proposed |
---
@@ -1,31 +0,0 @@
# Remove MSYS environment variables that trigger ESP-IDF's MinGW rejection
Remove-Item env:MSYSTEM -ErrorAction SilentlyContinue
Remove-Item env:MSYSTEM_CARCH -ErrorAction SilentlyContinue
Remove-Item env:MSYSTEM_CHOST -ErrorAction SilentlyContinue
Remove-Item env:MSYSTEM_PREFIX -ErrorAction SilentlyContinue
Remove-Item env:MINGW_CHOST -ErrorAction SilentlyContinue
Remove-Item env:MINGW_PACKAGE_PREFIX -ErrorAction SilentlyContinue
Remove-Item env:MINGW_PREFIX -ErrorAction SilentlyContinue
$env:IDF_PATH = "C:\Users\ruv\esp\v5.4\esp-idf"
$env:IDF_TOOLS_PATH = "C:\Espressif\tools"
$env:IDF_PYTHON_ENV_PATH = "C:\Espressif\tools\python\v5.4\venv"
$env:PATH = "C:\Espressif\tools\xtensa-esp-elf\esp-14.2.0_20241119\xtensa-esp-elf\bin;C:\Espressif\tools\cmake\3.30.2\cmake-3.30.2-windows-x86_64\bin;C:\Espressif\tools\ninja\1.12.1;C:\Espressif\tools\ccache\4.10.2\ccache-4.10.2-windows-x86_64;C:\Espressif\tools\idf-exe\1.0.3;C:\Espressif\tools\python\v5.4\venv\Scripts;$env:PATH"
Set-Location "C:\Users\ruv\Projects\wifi-densepose\firmware\esp32-csi-node"
$python = "$env:IDF_PYTHON_ENV_PATH\Scripts\python.exe"
$idf = "$env:IDF_PATH\tools\idf.py"
Write-Host "=== Cleaning stale build cache ==="
& $python $idf fullclean
Write-Host "=== Building firmware (SSID=ruv.net, target=192.168.1.20:5005) ==="
& $python $idf build
if ($LASTEXITCODE -eq 0) {
Write-Host "=== Build succeeded! Flashing to COM7 ==="
& $python $idf -p COM7 flash
} else {
Write-Host "=== Build failed with exit code $LASTEXITCODE ==="
}
@@ -6,11 +6,6 @@ set(SRCS
set(REQUIRES "")
# ADR-061: Mock CSI generator for QEMU testing
if(CONFIG_CSI_MOCK_ENABLED)
list(APPEND SRCS "mock_csi.c")
endif()
# ADR-045: AMOLED display support (compile-time optional)
if(CONFIG_DISPLAY_ENABLE)
list(APPEND SRCS "display_hal.c" "display_ui.c" "display_task.c")
+1 -41
View File
@@ -68,13 +68,10 @@ menu "Edge Intelligence (ADR-039)"
config EDGE_FALL_THRESH
int "Fall detection threshold (x1000)"
default 15000
default 2000
range 100 50000
help
Phase acceleration threshold for fall detection.
Value is divided by 1000 to get rad/s². Default 15000 = 15.0 rad/s².
Raise to reduce false positives in high-traffic environments.
Normal walking produces accelerations of 2-5 rad/s².
Stored as integer; divided by 1000 at runtime.
Default 2000 = 2.0 rad/s^2.
@@ -204,40 +201,3 @@ menu "WASM Programmable Sensing (ADR-040)"
Default 1000 ms = 1 Hz.
endmenu
menu "Mock CSI (QEMU Testing)"
config CSI_MOCK_ENABLED
bool "Enable mock CSI generator (for QEMU testing)"
default n
help
Replace real WiFi CSI with synthetic frame generator.
Use with QEMU emulation for automated testing.
config CSI_MOCK_SKIP_WIFI_CONNECT
bool "Skip WiFi STA connection"
depends on CSI_MOCK_ENABLED
default y
help
Skip WiFi initialization when using mock CSI.
config CSI_MOCK_SCENARIO
int "Mock scenario (0-9, 255=all)"
depends on CSI_MOCK_ENABLED
default 255
range 0 255
help
0=empty, 1=static, 2=walking, 3=fall, 4=multi-person,
5=channel-sweep, 6=mac-filter, 7=ring-overflow,
8=boundary-rssi, 9=zero-length, 255=run all.
config CSI_MOCK_SCENARIO_DURATION_MS
int "Scenario duration (ms)"
depends on CSI_MOCK_ENABLED
default 5000
range 1000 60000
config CSI_MOCK_LOG_FRAMES
bool "Log every mock frame (verbose)"
depends on CSI_MOCK_ENABLED
default n
endmenu
+2 -54
View File
@@ -12,7 +12,6 @@
*/
#include "csi_collector.h"
#include "nvs_config.h"
#include "stream_sender.h"
#include "edge_processing.h"
@@ -22,19 +21,6 @@
#include "esp_timer.h"
#include "sdkconfig.h"
/* ADR-060: Access the global NVS config for MAC filter and channel override. */
extern nvs_config_t g_nvs_config;
/* ADR-057: Build-time guard — fail early if CSI is not enabled in sdkconfig.
* Without this, the firmware compiles but crashes at runtime with:
* "E (xxxx) wifi:CSI not enabled in menuconfig!"
* which is confusing for users flashing pre-built binaries. */
#ifndef CONFIG_ESP_WIFI_CSI_ENABLED
#error "CONFIG_ESP_WIFI_CSI_ENABLED must be set in sdkconfig. " \
"Run: idf.py menuconfig -> Component config -> Wi-Fi -> Enable WiFi CSI, " \
"or copy sdkconfig.defaults.template to sdkconfig.defaults before building."
#endif
static const char *TAG = "csi_collector";
static uint32_t s_sequence = 0;
@@ -155,14 +141,6 @@ size_t csi_serialize_frame(const wifi_csi_info_t *info, uint8_t *buf, size_t buf
static void wifi_csi_callback(void *ctx, wifi_csi_info_t *info)
{
(void)ctx;
/* ADR-060: MAC address filtering — drop frames from non-matching sources. */
if (g_nvs_config.filter_mac_set) {
if (memcmp(info->mac, g_nvs_config.filter_mac, 6) != 0) {
return; /* Source MAC doesn't match filter — skip frame. */
}
}
s_cb_count++;
if (s_cb_count <= 3 || (s_cb_count % 100) == 0) {
@@ -215,29 +193,6 @@ static void wifi_promiscuous_cb(void *buf, wifi_promiscuous_pkt_type_t type)
void csi_collector_init(void)
{
/* ADR-060: Determine the CSI channel.
* Priority: 1) NVS override (--channel), 2) connected AP channel, 3) Kconfig default. */
uint8_t csi_channel = (uint8_t)CONFIG_CSI_WIFI_CHANNEL;
if (g_nvs_config.csi_channel > 0) {
/* Explicit NVS override via provision.py --channel */
csi_channel = g_nvs_config.csi_channel;
ESP_LOGI(TAG, "Using NVS channel override: %u", (unsigned)csi_channel);
} else {
/* Auto-detect from connected AP */
wifi_ap_record_t ap_info;
if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK && ap_info.primary > 0) {
csi_channel = ap_info.primary;
ESP_LOGI(TAG, "Auto-detected AP channel: %u", (unsigned)csi_channel);
} else {
ESP_LOGW(TAG, "Could not detect AP channel, using Kconfig default: %u",
(unsigned)csi_channel);
}
}
/* Update the hop table's first channel to match. */
s_hop_channels[0] = csi_channel;
/* Enable promiscuous mode — required for reliable CSI callbacks.
* Without this, CSI only fires on frames destined to this station,
* which may be very infrequent on a quiet network. */
@@ -265,15 +220,8 @@ void csi_collector_init(void)
ESP_ERROR_CHECK(esp_wifi_set_csi_rx_cb(wifi_csi_callback, NULL));
ESP_ERROR_CHECK(esp_wifi_set_csi(true));
if (g_nvs_config.filter_mac_set) {
ESP_LOGI(TAG, "MAC filter active: %02x:%02x:%02x:%02x:%02x:%02x",
g_nvs_config.filter_mac[0], g_nvs_config.filter_mac[1],
g_nvs_config.filter_mac[2], g_nvs_config.filter_mac[3],
g_nvs_config.filter_mac[4], g_nvs_config.filter_mac[5]);
}
ESP_LOGI(TAG, "CSI collection initialized (node_id=%d, channel=%u)",
CONFIG_CSI_NODE_ID, (unsigned)csi_channel);
ESP_LOGI(TAG, "CSI collection initialized (node_id=%d, channel=%d)",
CONFIG_CSI_NODE_ID, CONFIG_CSI_WIFI_CHANNEL);
}
/* ---- ADR-029: Channel hopping ---- */
+5 -27
View File
@@ -244,10 +244,6 @@ static uint32_t s_frame_count;
/** Previous phase velocity for fall detection (acceleration). */
static float s_prev_phase_velocity;
/** Fall detection debounce state (issue #263). */
static uint8_t s_fall_consec_count; /**< Consecutive frames above threshold. */
static int64_t s_fall_last_alert_us; /**< Timestamp of last fall alert (debounce). */
/** Adaptive calibration state. */
static bool s_calibrated;
static float s_calib_sum;
@@ -693,7 +689,7 @@ static void process_frame(const edge_ring_slot_t *slot)
}
s_presence_detected = (s_presence_score > threshold);
/* --- Step 10: Fall detection (phase acceleration + debounce, issue #263) --- */
/* --- Step 10: Fall detection (phase acceleration) --- */
if (s_history_len >= 3) {
uint16_t i0 = (s_history_idx + EDGE_PHASE_HISTORY_LEN - 1) % EDGE_PHASE_HISTORY_LEN;
uint16_t i1 = (s_history_idx + EDGE_PHASE_HISTORY_LEN - 2) % EDGE_PHASE_HISTORY_LEN;
@@ -701,26 +697,10 @@ static void process_frame(const edge_ring_slot_t *slot)
float accel = fabsf(velocity - s_prev_phase_velocity);
s_prev_phase_velocity = velocity;
if (accel > s_cfg.fall_thresh) {
s_fall_consec_count++;
} else {
s_fall_consec_count = 0;
}
/* Require EDGE_FALL_CONSEC_MIN consecutive frames above threshold,
* plus a cooldown period to prevent alert storms. */
int64_t now_us = esp_timer_get_time();
int64_t cooldown_us = (int64_t)EDGE_FALL_COOLDOWN_MS * 1000;
if (s_fall_consec_count >= EDGE_FALL_CONSEC_MIN
&& (now_us - s_fall_last_alert_us) >= cooldown_us)
{
s_fall_detected = true;
s_fall_last_alert_us = now_us;
s_fall_consec_count = 0;
ESP_LOGW(TAG, "Fall detected! accel=%.4f > thresh=%.4f (consec=%u)",
accel, s_cfg.fall_thresh, EDGE_FALL_CONSEC_MIN);
} else if (s_fall_consec_count == 0) {
s_fall_detected = false;
s_fall_detected = (accel > s_cfg.fall_thresh);
if (s_fall_detected) {
ESP_LOGW(TAG, "Fall detected! accel=%.4f > thresh=%.4f",
accel, s_cfg.fall_thresh);
}
}
@@ -870,8 +850,6 @@ esp_err_t edge_processing_init(const edge_config_t *cfg)
s_latest_rssi = 0;
s_frame_count = 0;
s_prev_phase_velocity = 0.0f;
s_fall_consec_count = 0;
s_fall_last_alert_us = 0;
s_last_vitals_send_us = 0;
s_has_prev_iq = false;
s_prev_iq_len = 0;
@@ -42,10 +42,6 @@
#define EDGE_CALIB_FRAMES 1200 /**< Frames for adaptive calibration (~60s at 20 Hz). */
#define EDGE_CALIB_SIGMA_MULT 3.0f /**< Threshold = mean + 3*sigma of ambient. */
/* ---- Fall detection ---- */
#define EDGE_FALL_COOLDOWN_MS 5000 /**< Minimum ms between fall alerts (debounce). */
#define EDGE_FALL_CONSEC_MIN 3 /**< Consecutive frames above threshold to trigger. */
/* ---- SPSC ring buffer slot ---- */
typedef struct {
uint8_t iq_data[EDGE_MAX_IQ_BYTES]; /**< Raw I/Q bytes from CSI callback. */
+2 -30
View File
@@ -27,9 +27,6 @@
#include "wasm_runtime.h"
#include "wasm_upload.h"
#include "display_task.h"
#ifdef CONFIG_CSI_MOCK_ENABLED
#include "mock_csi.h"
#endif
#include "esp_timer.h"
@@ -137,35 +134,17 @@ void app_main(void)
ESP_LOGI(TAG, "ESP32-S3 CSI Node (ADR-018) — Node ID: %d", g_nvs_config.node_id);
/* Initialize WiFi STA (skip entirely under QEMU mock — no RF hardware) */
#ifndef CONFIG_CSI_MOCK_SKIP_WIFI_CONNECT
/* Initialize WiFi STA */
wifi_init_sta();
#else
ESP_LOGI(TAG, "Mock CSI mode: skipping WiFi init (CONFIG_CSI_MOCK_SKIP_WIFI_CONNECT)");
#endif
/* Initialize UDP sender with runtime target */
#ifdef CONFIG_CSI_MOCK_SKIP_WIFI_CONNECT
ESP_LOGI(TAG, "Mock CSI mode: skipping UDP sender init (no network)");
#else
if (stream_sender_init_with(g_nvs_config.target_ip, g_nvs_config.target_port) != 0) {
ESP_LOGE(TAG, "Failed to initialize UDP sender");
return;
}
#endif
/* Initialize CSI collection */
#ifdef CONFIG_CSI_MOCK_ENABLED
/* ADR-061: Start mock CSI generator (replaces real WiFi CSI in QEMU) */
esp_err_t mock_ret = mock_csi_init(CONFIG_CSI_MOCK_SCENARIO);
if (mock_ret != ESP_OK) {
ESP_LOGE(TAG, "Mock CSI init failed: %s", esp_err_to_name(mock_ret));
} else {
ESP_LOGI(TAG, "Mock CSI active (scenario=%d)", CONFIG_CSI_MOCK_SCENARIO);
}
#else
csi_collector_init();
#endif
/* ADR-039: Initialize edge processing pipeline. */
edge_config_t edge_cfg = {
@@ -183,17 +162,12 @@ void app_main(void)
esp_err_to_name(edge_ret));
}
/* Initialize OTA update HTTP server (requires network). */
/* Initialize OTA update HTTP server. */
httpd_handle_t ota_server = NULL;
#ifndef CONFIG_CSI_MOCK_SKIP_WIFI_CONNECT
esp_err_t ota_ret = ota_update_init_ex(&ota_server);
if (ota_ret != ESP_OK) {
ESP_LOGW(TAG, "OTA server init failed: %s", esp_err_to_name(ota_ret));
}
#else
esp_err_t ota_ret = ESP_ERR_NOT_SUPPORTED;
ESP_LOGI(TAG, "Mock CSI mode: skipping OTA server (no network)");
#endif
/* ADR-040: Initialize WASM programmable sensing runtime. */
esp_err_t wasm_ret = wasm_runtime_init();
@@ -231,12 +205,10 @@ void app_main(void)
power_mgmt_init(g_nvs_config.power_duty);
/* ADR-045: Start AMOLED display task (gracefully skips if no display). */
#ifdef CONFIG_DISPLAY_ENABLE
esp_err_t disp_ret = display_task_start();
if (disp_ret != ESP_OK) {
ESP_LOGW(TAG, "Display init returned: %s", esp_err_to_name(disp_ret));
}
#endif
ESP_LOGI(TAG, "CSI streaming active → %s:%d (edge_tier=%u, OTA=%s, WASM=%s)",
g_nvs_config.target_ip, g_nvs_config.target_port,
-696
View File
@@ -1,696 +0,0 @@
/**
* @file mock_csi.c
* @brief ADR-061 Mock CSI generator for ESP32-S3 QEMU testing.
*
* Generates synthetic CSI frames at 20 Hz using an esp_timer callback,
* injecting them directly into the edge processing pipeline. This allows
* full-stack testing of the CSI signal processing, vitals extraction,
* and presence detection pipeline under QEMU without WiFi hardware.
*
* Signal model per subcarrier k at time t:
* A_k(t) = A_base + A_person * exp(-d_k^2 / sigma^2) + noise
* phi_k(t) = phi_base + (2*pi*d / lambda) + breathing_mod(t) + noise
*
* The entire file is guarded by CONFIG_CSI_MOCK_ENABLED so it compiles
* to nothing on production builds.
*/
#include "sdkconfig.h"
#ifdef CONFIG_CSI_MOCK_ENABLED
#include "mock_csi.h"
#include "edge_processing.h"
#include "nvs_config.h"
#include <string.h>
#include <math.h>
#include "esp_log.h"
#include "esp_timer.h"
#include "sdkconfig.h"
static const char *TAG = "mock_csi";
/* ---- Configuration defaults ---- */
/** Scenario duration in ms. Kconfig-overridable. */
#ifndef CONFIG_CSI_MOCK_SCENARIO_DURATION_MS
#define CONFIG_CSI_MOCK_SCENARIO_DURATION_MS 5000
#endif
/* ---- Physical constants ---- */
#define SPEED_OF_LIGHT_MHZ 300.0f /**< c in m * MHz (simplified). */
#define FREQ_CH6_MHZ 2437.0f /**< Center frequency of WiFi channel 6. */
#define LAMBDA_CH6 (SPEED_OF_LIGHT_MHZ / FREQ_CH6_MHZ) /**< ~0.123 m */
/** Breathing rate: ~15 breaths/min = 0.25 Hz. */
#define BREATHING_FREQ_HZ 0.25f
/** Breathing modulation amplitude in radians. */
#define BREATHING_AMP_RAD 0.3f
/** Walking speed in m/s. */
#define WALK_SPEED_MS 1.0f
/** Room width for position wrapping (meters). */
#define ROOM_WIDTH_M 6.0f
/** Gaussian sigma for person influence on subcarriers. */
#define PERSON_SIGMA 8.0f
/** Base amplitude for all subcarriers. */
#define A_BASE 80.0f
/** Person-induced amplitude perturbation. */
#define A_PERSON 40.0f
/** Noise amplitude (peak). */
#define NOISE_AMP 3.0f
/** Phase noise amplitude (radians). */
#define PHASE_NOISE_AMP 0.05f
/** Number of frames in the ring overflow burst (scenario 7). */
#define OVERFLOW_BURST_COUNT 1000
/** Fall detection: number of frames with abrupt phase jump. */
#define FALL_FRAME_COUNT 5
/** Fall phase acceleration magnitude (radians). */
#define FALL_PHASE_JUMP 3.14f
/** Pi constant. */
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
/* ---- Channel sweep table ---- */
static const uint8_t s_sweep_channels[] = {1, 6, 11, 36};
#define SWEEP_CHANNEL_COUNT (sizeof(s_sweep_channels) / sizeof(s_sweep_channels[0]))
/* ---- MAC addresses for filter test ---- */
/** "Correct" MAC that matches a typical filter_mac. */
static const uint8_t s_good_mac[6] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
/** "Wrong" MAC that should be rejected by the filter. */
static const uint8_t s_bad_mac[6] __attribute__((unused)) = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
/* ---- LFSR pseudo-random number generator ---- */
/**
* 32-bit Galois LFSR for deterministic pseudo-random noise.
* Avoids stdlib rand() which may not be available on ESP32 bare-metal.
* Taps: bits 32, 31, 29, 1 (Galois LFSR polynomial 0xD0000001).
*/
static uint32_t s_lfsr = 0xDEADBEEF;
static uint32_t lfsr_next(void)
{
uint32_t lsb = s_lfsr & 1u;
s_lfsr >>= 1;
if (lsb) {
s_lfsr ^= 0xD0000001u; /* x^32 + x^31 + x^29 + x^1 */
}
return s_lfsr;
}
/**
* Return a pseudo-random float in [-1.0, +1.0].
*/
static float lfsr_float(void)
{
uint32_t r = lfsr_next();
/* Map [0, 65535] to [-1.0, +1.0] using 65535/2 = 32767.5 */
return ((float)(r & 0xFFFF) / 32768.0f) - 1.0f;
}
/* ---- Module state ---- */
static mock_state_t s_state;
static esp_timer_handle_t s_timer = NULL;
/** Tracks whether the MAC filter has been set up in gen_mac_filter. */
static bool s_mac_filter_initialized = false;
/** Tracks whether the overflow burst has fired in gen_ring_overflow. */
static bool s_overflow_burst_done = false;
/* External NVS config (for MAC filter scenario). */
extern nvs_config_t g_nvs_config;
/* ---- Helper: compute channel frequency ---- */
static uint32_t channel_to_freq_mhz(uint8_t channel)
{
if (channel >= 1 && channel <= 13) {
return 2412 + (channel - 1) * 5;
} else if (channel == 14) {
return 2484;
} else if (channel >= 36 && channel <= 177) {
return 5000 + channel * 5;
}
return 2437; /* Default to ch 6. */
}
/* ---- Helper: compute wavelength for a channel ---- */
static float channel_to_lambda(uint8_t channel)
{
float freq = (float)channel_to_freq_mhz(channel);
return SPEED_OF_LIGHT_MHZ / freq;
}
/* ---- Helper: elapsed ms since scenario start ---- */
static int64_t scenario_elapsed_ms(void)
{
int64_t now = esp_timer_get_time() / 1000;
return now - s_state.scenario_start_ms;
}
/* ---- Helper: clamp int8 ---- */
static int8_t clamp_i8(int32_t val)
{
if (val < -128) return -128;
if (val > 127) return 127;
return (int8_t)val;
}
/* ---- Core signal generation ---- */
/**
* Generate one I/Q frame for a single person at position person_x.
*
* @param iq_buf Output buffer (MOCK_IQ_LEN bytes).
* @param person_x Person X position in meters.
* @param breathing Breathing phase in radians.
* @param has_person Whether a person is present.
* @param lambda Wavelength in meters.
*/
static void generate_person_iq(uint8_t *iq_buf, float person_x,
float breathing, bool has_person,
float lambda)
{
for (int k = 0; k < MOCK_N_SUBCARRIERS; k++) {
/* Distance of subcarrier k's spatial sample from person. */
float d_k = (float)k - person_x * (MOCK_N_SUBCARRIERS / ROOM_WIDTH_M);
/* Amplitude model. */
float amp = A_BASE;
if (has_person) {
float gauss = expf(-(d_k * d_k) / (2.0f * PERSON_SIGMA * PERSON_SIGMA));
amp += A_PERSON * gauss;
}
amp += NOISE_AMP * lfsr_float();
/* Phase model. */
float phase = (float)k * 0.1f; /* Base phase gradient. */
if (has_person) {
float d_meters = fabsf(d_k) * (ROOM_WIDTH_M / MOCK_N_SUBCARRIERS);
phase += (2.0f * M_PI * d_meters) / lambda;
phase += BREATHING_AMP_RAD * sinf(breathing);
}
phase += PHASE_NOISE_AMP * lfsr_float();
/* Convert to I/Q (int8). */
float i_f = amp * cosf(phase);
float q_f = amp * sinf(phase);
iq_buf[k * 2] = (uint8_t)clamp_i8((int32_t)i_f);
iq_buf[k * 2 + 1] = (uint8_t)clamp_i8((int32_t)q_f);
}
}
/* ---- Scenario generators ---- */
/**
* Scenario 0: Empty room.
* Low-amplitude noise on all subcarriers, no person present.
*/
static void gen_empty(uint8_t *iq_buf, uint8_t *channel, int8_t *rssi)
{
generate_person_iq(iq_buf, 0.0f, 0.0f, false, LAMBDA_CH6);
*channel = 6;
*rssi = -60;
}
/**
* Scenario 1: Static person.
* Person at fixed position with breathing modulation.
*/
static void gen_static_person(uint8_t *iq_buf, uint8_t *channel, int8_t *rssi)
{
s_state.breathing_phase += 2.0f * M_PI * BREATHING_FREQ_HZ
* (MOCK_CSI_INTERVAL_MS / 1000.0f);
if (s_state.breathing_phase > 2.0f * M_PI) {
s_state.breathing_phase -= 2.0f * M_PI;
}
generate_person_iq(iq_buf, 3.0f, s_state.breathing_phase, true, LAMBDA_CH6);
*channel = 6;
*rssi = -45;
}
/**
* Scenario 2: Walking person.
* Person moves across the room and wraps around.
*/
static void gen_walking(uint8_t *iq_buf, uint8_t *channel, int8_t *rssi)
{
s_state.breathing_phase += 2.0f * M_PI * BREATHING_FREQ_HZ
* (MOCK_CSI_INTERVAL_MS / 1000.0f);
if (s_state.breathing_phase > 2.0f * M_PI) {
s_state.breathing_phase -= 2.0f * M_PI;
}
s_state.person_x += s_state.person_speed * (MOCK_CSI_INTERVAL_MS / 1000.0f);
if (s_state.person_x > ROOM_WIDTH_M) {
s_state.person_x -= ROOM_WIDTH_M;
}
generate_person_iq(iq_buf, s_state.person_x, s_state.breathing_phase,
true, LAMBDA_CH6);
*channel = 6;
*rssi = -40;
}
/**
* Scenario 3: Fall event.
* Normal walking for most frames, then an abrupt phase discontinuity
* simulating a fall (rapid vertical displacement).
*/
static void gen_fall(uint8_t *iq_buf, uint8_t *channel, int8_t *rssi)
{
int64_t elapsed = scenario_elapsed_ms();
uint32_t duration = CONFIG_CSI_MOCK_SCENARIO_DURATION_MS;
/* Fall occurs at 70% of scenario duration. */
uint32_t fall_start = (duration * 70) / 100;
uint32_t fall_end = fall_start + (FALL_FRAME_COUNT * MOCK_CSI_INTERVAL_MS);
s_state.breathing_phase += 2.0f * M_PI * BREATHING_FREQ_HZ
* (MOCK_CSI_INTERVAL_MS / 1000.0f);
s_state.person_x += 0.5f * (MOCK_CSI_INTERVAL_MS / 1000.0f);
if (s_state.person_x > ROOM_WIDTH_M) {
s_state.person_x = ROOM_WIDTH_M;
}
float extra_phase = 0.0f;
if (elapsed >= fall_start && elapsed < fall_end) {
/* Abrupt phase jump simulating rapid downward motion. */
extra_phase = FALL_PHASE_JUMP;
}
/* Build I/Q with fall perturbation. */
float lambda = LAMBDA_CH6;
for (int k = 0; k < MOCK_N_SUBCARRIERS; k++) {
float d_k = (float)k - s_state.person_x * (MOCK_N_SUBCARRIERS / ROOM_WIDTH_M);
float gauss = expf(-(d_k * d_k) / (2.0f * PERSON_SIGMA * PERSON_SIGMA));
float amp = A_BASE + A_PERSON * gauss + NOISE_AMP * lfsr_float();
float d_meters = fabsf(d_k) * (ROOM_WIDTH_M / MOCK_N_SUBCARRIERS);
float phase = (float)k * 0.1f
+ (2.0f * M_PI * d_meters) / lambda
+ BREATHING_AMP_RAD * sinf(s_state.breathing_phase)
+ extra_phase * gauss /* Fall affects nearby subcarriers. */
+ PHASE_NOISE_AMP * lfsr_float();
iq_buf[k * 2] = (uint8_t)clamp_i8((int32_t)(amp * cosf(phase)));
iq_buf[k * 2 + 1] = (uint8_t)clamp_i8((int32_t)(amp * sinf(phase)));
}
*channel = 6;
*rssi = -42;
}
/**
* Scenario 4: Multiple people.
* Two people at different positions with independent breathing.
*/
static void gen_multi_person(uint8_t *iq_buf, uint8_t *channel, int8_t *rssi)
{
float dt = MOCK_CSI_INTERVAL_MS / 1000.0f;
s_state.breathing_phase += 2.0f * M_PI * BREATHING_FREQ_HZ * dt;
float breathing2 = s_state.breathing_phase * 1.3f; /* Slightly different rate. */
s_state.person_x += s_state.person_speed * dt;
s_state.person2_x += s_state.person2_speed * dt;
/* Wrap positions. */
if (s_state.person_x > ROOM_WIDTH_M) s_state.person_x -= ROOM_WIDTH_M;
if (s_state.person2_x > ROOM_WIDTH_M) s_state.person2_x -= ROOM_WIDTH_M;
float lambda = LAMBDA_CH6;
for (int k = 0; k < MOCK_N_SUBCARRIERS; k++) {
/* Superpose contributions from both people. */
float d1 = (float)k - s_state.person_x * (MOCK_N_SUBCARRIERS / ROOM_WIDTH_M);
float d2 = (float)k - s_state.person2_x * (MOCK_N_SUBCARRIERS / ROOM_WIDTH_M);
float g1 = expf(-(d1 * d1) / (2.0f * PERSON_SIGMA * PERSON_SIGMA));
float g2 = expf(-(d2 * d2) / (2.0f * PERSON_SIGMA * PERSON_SIGMA));
float amp = A_BASE + A_PERSON * g1 + (A_PERSON * 0.7f) * g2
+ NOISE_AMP * lfsr_float();
float dm1 = fabsf(d1) * (ROOM_WIDTH_M / MOCK_N_SUBCARRIERS);
float dm2 = fabsf(d2) * (ROOM_WIDTH_M / MOCK_N_SUBCARRIERS);
float phase = (float)k * 0.1f
+ (2.0f * M_PI * dm1) / lambda * g1
+ (2.0f * M_PI * dm2) / lambda * g2
+ BREATHING_AMP_RAD * sinf(s_state.breathing_phase) * g1
+ BREATHING_AMP_RAD * sinf(breathing2) * g2
+ PHASE_NOISE_AMP * lfsr_float();
iq_buf[k * 2] = (uint8_t)clamp_i8((int32_t)(amp * cosf(phase)));
iq_buf[k * 2 + 1] = (uint8_t)clamp_i8((int32_t)(amp * sinf(phase)));
}
*channel = 6;
*rssi = -38;
}
/**
* Scenario 5: Channel sweep.
* Cycles through channels 1, 6, 11, 36 every 20 frames.
*/
static void gen_channel_sweep(uint8_t *iq_buf, uint8_t *channel, int8_t *rssi)
{
/* Switch channel every 20 frames (1 second at 20 Hz). */
if ((s_state.frame_count % 20) == 0 && s_state.frame_count > 0) {
s_state.channel_idx = (s_state.channel_idx + 1) % SWEEP_CHANNEL_COUNT;
}
uint8_t ch = s_sweep_channels[s_state.channel_idx];
float lambda = channel_to_lambda(ch);
generate_person_iq(iq_buf, 3.0f, 0.0f, true, lambda);
*channel = ch;
*rssi = -50;
}
/**
* Scenario 6: MAC filter test.
* Alternates between a "good" MAC (should pass filter) and a "bad" MAC
* (should be rejected). Even frames use good MAC, odd frames use bad MAC.
*
* Note: Since we inject via edge_enqueue_csi() which bypasses the MAC
* filter (that happens in wifi_csi_callback), this scenario instead
* sets/clears the NVS filter_mac and logs which frames would pass.
* The test harness can verify frame_count vs expected.
*/
static void gen_mac_filter(uint8_t *iq_buf, uint8_t *channel, int8_t *rssi,
bool *skip_inject)
{
/* Set up the filter MAC to match s_good_mac on first frame of this scenario. */
if (!s_mac_filter_initialized) {
memcpy(g_nvs_config.filter_mac, s_good_mac, 6);
g_nvs_config.filter_mac_set = 1;
s_mac_filter_initialized = true;
ESP_LOGI(TAG, "MAC filter scenario: filter set to %02X:%02X:%02X:%02X:%02X:%02X",
s_good_mac[0], s_good_mac[1], s_good_mac[2],
s_good_mac[3], s_good_mac[4], s_good_mac[5]);
}
generate_person_iq(iq_buf, 3.0f, 0.0f, true, LAMBDA_CH6);
*channel = 6;
*rssi = -50;
/* Odd frames: simulate "wrong" MAC by skipping injection. */
if ((s_state.frame_count & 1) != 0) {
*skip_inject = true;
ESP_LOGD(TAG, "MAC filter: frame %lu skipped (bad MAC)",
(unsigned long)s_state.frame_count);
} else {
*skip_inject = false;
}
}
/**
* Scenario 7: Ring buffer overflow.
* Burst OVERFLOW_BURST_COUNT frames as fast as possible to test
* the SPSC ring buffer's overflow handling.
*/
static void gen_ring_overflow(uint8_t *iq_buf, uint8_t *channel, int8_t *rssi,
uint16_t *burst_count)
{
generate_person_iq(iq_buf, 3.0f, 0.0f, true, LAMBDA_CH6);
*channel = 6;
*rssi = -50;
/* Burst once on the first timer tick of this scenario. */
if (!s_overflow_burst_done) {
*burst_count = OVERFLOW_BURST_COUNT;
s_overflow_burst_done = true;
} else {
*burst_count = 1;
}
}
/**
* Scenario 8: Boundary RSSI sweep.
* Sweeps RSSI from -90 dBm to -10 dBm linearly over the scenario duration.
*/
static void gen_boundary_rssi(uint8_t *iq_buf, uint8_t *channel, int8_t *rssi)
{
int64_t elapsed = scenario_elapsed_ms();
uint32_t duration = CONFIG_CSI_MOCK_SCENARIO_DURATION_MS;
/* Linear sweep: -90 to -10 dBm. */
float frac = (float)elapsed / (float)duration;
if (frac > 1.0f) frac = 1.0f;
int8_t sweep_rssi = (int8_t)(-90.0f + 80.0f * frac);
generate_person_iq(iq_buf, 3.0f, 0.0f, true, LAMBDA_CH6);
*channel = 6;
*rssi = sweep_rssi;
}
/**
* Scenario 9: Zero-length I/Q.
* Injects a frame with iq_len = 0 to test error handling.
*/
/* Handled inline in the timer callback. */
/* ---- Scenario transition ---- */
/**
* Advance to the next scenario when running SCENARIO_ALL.
*/
/** Flag: set when all scenarios are done so timer callback exits early. */
static bool s_all_done = false;
static void advance_scenario(void)
{
s_state.all_idx++;
if (s_state.all_idx >= MOCK_SCENARIO_COUNT) {
ESP_LOGI(TAG, "All %d scenarios complete (%lu total frames)",
MOCK_SCENARIO_COUNT, (unsigned long)s_state.frame_count);
s_all_done = true;
return; /* Stop generating — timer callback will check s_all_done. */
}
s_state.scenario = s_state.all_idx;
s_state.scenario_start_ms = esp_timer_get_time() / 1000;
/* Reset per-scenario state. */
s_state.person_x = 1.0f;
s_state.person_speed = WALK_SPEED_MS;
s_state.person2_x = 4.0f;
s_state.person2_speed = WALK_SPEED_MS * 0.6f;
s_state.breathing_phase = 0.0f;
s_state.channel_idx = 0;
s_state.rssi_sweep = -90;
ESP_LOGI(TAG, "=== Scenario %u started ===", (unsigned)s_state.scenario);
}
/* ---- Timer callback ---- */
static void mock_timer_cb(void *arg)
{
(void)arg;
/* All scenarios finished — stop generating. */
if (s_all_done) {
return;
}
/* Check for scenario timeout in SCENARIO_ALL mode. */
if (s_state.scenario == MOCK_SCENARIO_ALL ||
(s_state.all_idx > 0 && s_state.all_idx < MOCK_SCENARIO_COUNT)) {
/* We're running in sequential mode. */
int64_t elapsed = scenario_elapsed_ms();
if (elapsed >= CONFIG_CSI_MOCK_SCENARIO_DURATION_MS) {
advance_scenario();
}
}
uint8_t iq_buf[MOCK_IQ_LEN];
uint8_t channel = 6;
int8_t rssi = -50;
uint16_t iq_len = MOCK_IQ_LEN;
uint16_t burst = 1;
bool skip = false;
uint8_t active_scenario = s_state.scenario;
switch (active_scenario) {
case MOCK_SCENARIO_EMPTY:
gen_empty(iq_buf, &channel, &rssi);
break;
case MOCK_SCENARIO_STATIC_PERSON:
gen_static_person(iq_buf, &channel, &rssi);
break;
case MOCK_SCENARIO_WALKING:
gen_walking(iq_buf, &channel, &rssi);
break;
case MOCK_SCENARIO_FALL:
gen_fall(iq_buf, &channel, &rssi);
break;
case MOCK_SCENARIO_MULTI_PERSON:
gen_multi_person(iq_buf, &channel, &rssi);
break;
case MOCK_SCENARIO_CHANNEL_SWEEP:
gen_channel_sweep(iq_buf, &channel, &rssi);
break;
case MOCK_SCENARIO_MAC_FILTER:
gen_mac_filter(iq_buf, &channel, &rssi, &skip);
break;
case MOCK_SCENARIO_RING_OVERFLOW:
gen_ring_overflow(iq_buf, &channel, &rssi, &burst);
break;
case MOCK_SCENARIO_BOUNDARY_RSSI:
gen_boundary_rssi(iq_buf, &channel, &rssi);
break;
case MOCK_SCENARIO_ZERO_LENGTH:
/* Deliberately inject zero-length data to test error path. */
iq_len = 0;
memset(iq_buf, 0, sizeof(iq_buf));
break;
default:
ESP_LOGW(TAG, "Unknown scenario %u, defaulting to empty", active_scenario);
gen_empty(iq_buf, &channel, &rssi);
break;
}
/* Inject frame(s) into the edge processing pipeline. */
if (!skip) {
for (uint16_t i = 0; i < burst; i++) {
edge_enqueue_csi(iq_buf, iq_len, rssi, channel);
s_state.frame_count++;
}
} else {
/* Count skipped frames for MAC filter validation. */
s_state.frame_count++;
}
/* Periodic logging (every 20 frames = 1 second). */
if ((s_state.frame_count % 20) == 0) {
ESP_LOGI(TAG, "scenario=%u frames=%lu ch=%u rssi=%d",
active_scenario, (unsigned long)s_state.frame_count,
(unsigned)channel, (int)rssi);
}
}
/* ---- Public API ---- */
esp_err_t mock_csi_init(uint8_t scenario)
{
if (s_timer != NULL) {
ESP_LOGW(TAG, "Mock CSI already running");
return ESP_ERR_INVALID_STATE;
}
/* Initialize state. */
memset(&s_state, 0, sizeof(s_state));
s_state.person_x = 1.0f;
s_state.person_speed = WALK_SPEED_MS;
s_state.person2_x = 4.0f;
s_state.person2_speed = WALK_SPEED_MS * 0.6f;
s_state.scenario_start_ms = esp_timer_get_time() / 1000;
s_all_done = false;
s_mac_filter_initialized = false;
s_overflow_burst_done = false;
/* Reset LFSR to deterministic seed. */
s_lfsr = 0xDEADBEEF;
if (scenario == MOCK_SCENARIO_ALL) {
s_state.scenario = 0;
s_state.all_idx = 0;
ESP_LOGI(TAG, "Mock CSI: running ALL %d scenarios sequentially (%u ms each)",
MOCK_SCENARIO_COUNT, CONFIG_CSI_MOCK_SCENARIO_DURATION_MS);
} else {
s_state.scenario = scenario;
s_state.all_idx = 0;
ESP_LOGI(TAG, "Mock CSI: scenario=%u, interval=%u ms, duration=%u ms",
(unsigned)scenario, MOCK_CSI_INTERVAL_MS,
CONFIG_CSI_MOCK_SCENARIO_DURATION_MS);
}
/* Create periodic timer. */
esp_timer_create_args_t timer_args = {
.callback = mock_timer_cb,
.arg = NULL,
.name = "mock_csi",
};
esp_err_t err = esp_timer_create(&timer_args, &s_timer);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to create mock CSI timer: %s", esp_err_to_name(err));
return err;
}
uint64_t period_us = (uint64_t)MOCK_CSI_INTERVAL_MS * 1000;
err = esp_timer_start_periodic(s_timer, period_us);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to start mock CSI timer: %s", esp_err_to_name(err));
esp_timer_delete(s_timer);
s_timer = NULL;
return err;
}
ESP_LOGI(TAG, "Mock CSI generator started (20 Hz, %u subcarriers, %u bytes/frame)",
MOCK_N_SUBCARRIERS, MOCK_IQ_LEN);
return ESP_OK;
}
void mock_csi_stop(void)
{
if (s_timer == NULL) {
return;
}
esp_timer_stop(s_timer);
esp_timer_delete(s_timer);
s_timer = NULL;
ESP_LOGI(TAG, "Mock CSI stopped after %lu frames",
(unsigned long)s_state.frame_count);
}
uint32_t mock_csi_get_frame_count(void)
{
return s_state.frame_count;
}
#endif /* CONFIG_CSI_MOCK_ENABLED */
-107
View File
@@ -1,107 +0,0 @@
/**
* @file mock_csi.h
* @brief ADR-061 Mock CSI generator for ESP32-S3 QEMU testing.
*
* Generates synthetic CSI frames at 20 Hz using an esp_timer, injecting
* them directly into the edge processing pipeline via edge_enqueue_csi().
* Ten scenarios exercise the full signal processing and edge intelligence
* pipeline without requiring real WiFi hardware.
*
* Signal model per subcarrier k at time t:
* A_k(t) = A_base + A_person * exp(-d_k^2 / sigma^2) + noise
* phi_k(t) = phi_base + (2*pi*d / lambda) + breathing_mod(t) + noise
*
* Enable via: idf.py menuconfig -> CSI Mock Generator -> Enable
* Or add CONFIG_CSI_MOCK_ENABLED=y to sdkconfig.defaults.
*/
#ifndef MOCK_CSI_H
#define MOCK_CSI_H
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/* ---- Timing ---- */
/** Mock CSI frame interval in milliseconds (20 Hz). */
#define MOCK_CSI_INTERVAL_MS 50
/* ---- HT20 subcarrier geometry ---- */
/** Number of OFDM subcarriers for HT20 (802.11n). */
#define MOCK_N_SUBCARRIERS 52
/** I/Q data length in bytes: 52 subcarriers * 2 bytes (I + Q). */
#define MOCK_IQ_LEN (MOCK_N_SUBCARRIERS * 2)
/* ---- Scenarios ---- */
/** Scenario identifiers for mock CSI generation. */
typedef enum {
MOCK_SCENARIO_EMPTY = 0, /**< Empty room: low-noise baseline. */
MOCK_SCENARIO_STATIC_PERSON = 1, /**< Static person: amplitude dip, no motion. */
MOCK_SCENARIO_WALKING = 2, /**< Walking person: moving reflector. */
MOCK_SCENARIO_FALL = 3, /**< Fall event: abrupt phase acceleration. */
MOCK_SCENARIO_MULTI_PERSON = 4, /**< Multiple people at different positions. */
MOCK_SCENARIO_CHANNEL_SWEEP = 5, /**< Sweep through channels 1, 6, 11, 36. */
MOCK_SCENARIO_MAC_FILTER = 6, /**< Alternate correct/wrong MAC for filter test. */
MOCK_SCENARIO_RING_OVERFLOW = 7, /**< Burst 1000 frames rapidly to overflow ring. */
MOCK_SCENARIO_BOUNDARY_RSSI = 8, /**< Sweep RSSI from -90 to -10 dBm. */
MOCK_SCENARIO_ZERO_LENGTH = 9, /**< Zero-length I/Q payload (error case). */
MOCK_SCENARIO_COUNT = 10, /**< Total number of individual scenarios. */
MOCK_SCENARIO_ALL = 255 /**< Meta: run all scenarios sequentially. */
} mock_scenario_t;
/* ---- State ---- */
/** Internal state for the mock CSI generator. */
typedef struct {
uint8_t scenario; /**< Current active scenario. */
uint32_t frame_count; /**< Total frames emitted since init. */
float person_x; /**< Person X position in meters (walking). */
float person_speed; /**< Person movement speed in m/s. */
float breathing_phase; /**< Breathing oscillator phase in radians. */
float person2_x; /**< Second person X position (multi-person). */
float person2_speed; /**< Second person movement speed. */
uint8_t channel_idx; /**< Index into channel sweep table. */
int8_t rssi_sweep; /**< Current RSSI for boundary sweep. */
int64_t scenario_start_ms; /**< Timestamp when current scenario started. */
uint8_t all_idx; /**< Current scenario index in SCENARIO_ALL mode. */
} mock_state_t;
/**
* Initialize and start the mock CSI generator.
*
* Creates a periodic esp_timer that fires every MOCK_CSI_INTERVAL_MS
* and injects synthetic CSI frames into edge_enqueue_csi().
*
* @param scenario Scenario to run (0-9), or MOCK_SCENARIO_ALL (255)
* to run all scenarios sequentially.
* @return ESP_OK on success, ESP_ERR_INVALID_STATE if already running.
*/
esp_err_t mock_csi_init(uint8_t scenario);
/**
* Stop and destroy the mock CSI timer.
*
* Safe to call even if the timer is not running.
*/
void mock_csi_stop(void);
/**
* Get the total number of mock frames emitted since init.
*
* @return Frame count (useful for test validation).
*/
uint32_t mock_csi_get_frame_count(void);
#ifdef __cplusplus
}
#endif
#endif /* MOCK_CSI_H */
+1 -26
View File
@@ -61,7 +61,7 @@ void nvs_config_load(nvs_config_t *cfg)
#ifdef CONFIG_EDGE_FALL_THRESH
cfg->fall_thresh = (float)CONFIG_EDGE_FALL_THRESH / 1000.0f;
#else
cfg->fall_thresh = 15.0f; /* Default raised from 2.0 — see issue #263. */
cfg->fall_thresh = 2.0f;
#endif
cfg->vital_window = 256;
#ifdef CONFIG_EDGE_VITAL_INTERVAL_MS
@@ -91,11 +91,6 @@ void nvs_config_load(nvs_config_t *cfg)
cfg->wasm_verify = 0; /* Kconfig disabled signature verification. */
#endif
/* ADR-060: Channel override and MAC filter defaults. */
cfg->csi_channel = 0; /* 0 = auto-detect from connected AP. */
cfg->filter_mac_set = 0;
memset(cfg->filter_mac, 0, 6);
/* Try to override from NVS */
nvs_handle_t handle;
esp_err_t err = nvs_open("csi_cfg", NVS_READONLY, &handle);
@@ -282,26 +277,6 @@ void nvs_config_load(nvs_config_t *cfg)
ESP_LOGW(TAG, "wasm_verify=1 but no wasm_pubkey in NVS — uploads will be rejected");
}
/* ADR-060: CSI channel override. */
uint8_t csi_ch_val;
if (nvs_get_u8(handle, "csi_channel", &csi_ch_val) == ESP_OK) {
if ((csi_ch_val >= 1 && csi_ch_val <= 14) || (csi_ch_val >= 36 && csi_ch_val <= 177)) {
cfg->csi_channel = csi_ch_val;
ESP_LOGI(TAG, "NVS override: csi_channel=%u", (unsigned)cfg->csi_channel);
} else {
ESP_LOGW(TAG, "NVS csi_channel=%u invalid, ignored", (unsigned)csi_ch_val);
}
}
/* ADR-060: MAC address filter (6-byte blob). */
size_t mac_len = 6;
if (nvs_get_blob(handle, "filter_mac", cfg->filter_mac, &mac_len) == ESP_OK && mac_len == 6) {
cfg->filter_mac_set = 1;
ESP_LOGI(TAG, "NVS override: filter_mac=%02x:%02x:%02x:%02x:%02x:%02x",
cfg->filter_mac[0], cfg->filter_mac[1], cfg->filter_mac[2],
cfg->filter_mac[3], cfg->filter_mac[4], cfg->filter_mac[5]);
}
/* Validate tdm_slot_index < tdm_node_count */
if (cfg->tdm_slot_index >= cfg->tdm_node_count) {
ESP_LOGW(TAG, "tdm_slot_index=%u >= tdm_node_count=%u, clamping to 0",
@@ -50,11 +50,6 @@ typedef struct {
uint8_t wasm_verify; /**< Require Ed25519 signature for uploads. */
uint8_t wasm_pubkey[32]; /**< Ed25519 public key for WASM signature. */
uint8_t wasm_pubkey_valid; /**< 1 if pubkey was loaded from NVS. */
/* ADR-060: Channel override and MAC address filtering */
uint8_t csi_channel; /**< Explicit CSI channel override (0 = auto-detect). */
uint8_t filter_mac[6]; /**< MAC address to filter CSI frames. */
uint8_t filter_mac_set; /**< 1 if filter_mac was loaded from NVS. */
} nvs_config_t;
/**
@@ -1,15 +0,0 @@
# ESP32-S3 CSI Node — 4MB flash partition table (issue #265)
# For boards with 4MB flash (e.g. ESP32-S3 SuperMini 4MB).
# Binary is ~978KB so each OTA slot is 1.875MB — plenty of room.
#
# Usage: copy to partitions_display.csv OR set in sdkconfig:
# CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_4mb.csv"
# CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
# CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
#
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
otadata, data, ota, 0xF000, 0x2000,
phy_init, data, phy, 0x11000, 0x1000,
ota_0, app, ota_0, 0x20000, 0x1D0000,
ota_1, app, ota_1, 0x1F0000, 0x1D0000,
Can't render this file because it contains an unexpected character in line 6 and column 44.
+26 -52
View File
@@ -64,13 +64,6 @@ def build_nvs_csv(args):
writer.writerow(["vital_int", "data", "u16", str(args.vital_int)])
if args.subk_count is not None:
writer.writerow(["subk_count", "data", "u8", str(args.subk_count)])
# ADR-060: Channel override and MAC filter
if args.channel is not None:
writer.writerow(["csi_channel", "data", "u8", str(args.channel)])
if args.filter_mac is not None:
mac_bytes = bytes(int(b, 16) for b in args.filter_mac.split(":"))
# NVS blob: write as hex-encoded string for CSV compatibility
writer.writerow(["filter_mac", "data", "hex2bin", mac_bytes.hex()])
return buf.getvalue()
@@ -83,20 +76,25 @@ def generate_nvs_binary(csv_content, size):
bin_path = csv_path.replace(".csv", ".bin")
try:
# Method 1: subprocess invocation (most reliable across package versions)
for module_name in ["esp_idf_nvs_partition_gen", "nvs_partition_gen"]:
try:
subprocess.check_call(
[sys.executable, "-m", module_name, "generate",
csv_path, bin_path, hex(size)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
with open(bin_path, "rb") as f:
return f.read()
except (subprocess.CalledProcessError, FileNotFoundError):
continue
# Try the pip-installed version first (esp_idf_nvs_partition_gen package)
try:
from esp_idf_nvs_partition_gen import nvs_partition_gen
nvs_partition_gen.generate(csv_path, bin_path, size)
with open(bin_path, "rb") as f:
return f.read()
except ImportError:
pass
# Method 2: ESP-IDF bundled script
# Try legacy import name (older versions)
try:
import nvs_partition_gen
nvs_partition_gen.generate(csv_path, bin_path, size)
with open(bin_path, "rb") as f:
return f.read()
except ImportError:
pass
# Fall back to calling the ESP-IDF script directly
idf_path = os.environ.get("IDF_PATH", "")
gen_script = os.path.join(idf_path, "components", "nvs_flash",
"nvs_partition_generator", "nvs_partition_gen.py")
@@ -108,10 +106,13 @@ def generate_nvs_binary(csv_content, size):
with open(bin_path, "rb") as f:
return f.read()
raise RuntimeError(
"NVS partition generator not available. "
"Install: pip install esp-idf-nvs-partition-gen"
)
# Last resort: try as a module
subprocess.check_call([
sys.executable, "-m", "nvs_partition_gen", "generate",
csv_path, bin_path, hex(size)
])
with open(bin_path, "rb") as f:
return f.read()
finally:
for p in (csv_path, bin_path):
@@ -160,16 +161,10 @@ def main():
parser.add_argument("--edge-tier", type=int, choices=[0, 1, 2],
help="Edge processing tier: 0=off, 1=stats, 2=vitals")
parser.add_argument("--pres-thresh", type=int, help="Presence detection threshold (default: 50)")
parser.add_argument("--fall-thresh", type=int, help="Fall detection threshold in milli-units "
"(value/1000 = rad/s²). Default: 15000 → 15.0 rad/s². "
"Raise to reduce false positives in high-traffic areas.")
parser.add_argument("--fall-thresh", type=int, help="Fall detection threshold (default: 500)")
parser.add_argument("--vital-win", type=int, help="Phase history window in frames (default: 300)")
parser.add_argument("--vital-int", type=int, help="Vitals packet interval in ms (default: 1000)")
parser.add_argument("--subk-count", type=int, help="Top-K subcarrier count (default: 32)")
# ADR-060: Channel override and MAC filter
parser.add_argument("--channel", type=int, help="CSI channel (1-14 for 2.4GHz, 36-177 for 5GHz). "
"Overrides auto-detection from connected AP.")
parser.add_argument("--filter-mac", type=str, help="MAC address to filter CSI frames (AA:BB:CC:DD:EE:FF)")
parser.add_argument("--dry-run", action="store_true", help="Generate NVS binary but don't flash")
args = parser.parse_args()
@@ -181,7 +176,6 @@ def main():
args.edge_tier is not None, args.pres_thresh is not None,
args.fall_thresh is not None, args.vital_win is not None,
args.vital_int is not None, args.subk_count is not None,
args.channel is not None, args.filter_mac is not None,
])
if not has_value:
parser.error("At least one config value must be specified")
@@ -192,22 +186,6 @@ def main():
if args.tdm_slot is not None and args.tdm_slot >= args.tdm_total:
parser.error(f"--tdm-slot ({args.tdm_slot}) must be less than --tdm-total ({args.tdm_total})")
# ADR-060: Validate channel and MAC filter
if args.channel is not None:
if not ((1 <= args.channel <= 14) or (36 <= args.channel <= 177)):
parser.error(f"--channel must be 1-14 (2.4GHz) or 36-177 (5GHz), got {args.channel}")
if args.filter_mac is not None:
parts = args.filter_mac.split(":")
if len(parts) != 6:
parser.error(f"--filter-mac must be in AA:BB:CC:DD:EE:FF format, got '{args.filter_mac}'")
try:
for p in parts:
val = int(p, 16)
if val < 0 or val > 255:
raise ValueError
except ValueError:
parser.error(f"--filter-mac contains invalid hex bytes: '{args.filter_mac}'")
print("Building NVS configuration:")
if args.ssid:
print(f" WiFi SSID: {args.ssid}")
@@ -234,10 +212,6 @@ def main():
print(f" Vital Interval:{args.vital_int} ms")
if args.subk_count is not None:
print(f" Top-K Subcarr: {args.subk_count}")
if args.channel is not None:
print(f" CSI Channel: {args.channel}")
if args.filter_mac is not None:
print(f" Filter MAC: {args.filter_mac}")
csv_content = build_nvs_csv(args)
-14
View File
@@ -1,14 +0,0 @@
$p = New-Object System.IO.Ports.SerialPort('COM7', 115200)
$p.ReadTimeout = 5000
$p.Open()
Start-Sleep -Milliseconds 200
for ($i = 0; $i -lt 60; $i++) {
try {
$line = $p.ReadLine()
Write-Host $line
} catch {
break
}
}
$p.Close()
@@ -1,54 +0,0 @@
# sdkconfig.coverage -- ESP-IDF sdkconfig overlay for gcov/lcov code coverage
#
# This overlay enables GCC code coverage instrumentation (gcov) and the
# application-level trace (apptrace) channel required to extract .gcda
# files from the target via JTAG/QEMU GDB.
#
# Usage (combine with sdkconfig.defaults as the base):
#
# idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.coverage" build
#
# After running the firmware under QEMU, dump coverage data through GDB:
#
# (gdb) mon gcov dump
#
# Then process the .gcda files on the host with lcov/genhtml:
#
# lcov --capture --directory build --output-file coverage.info \
# --gcov-tool xtensa-esp-elf-gcov
# genhtml coverage.info --output-directory coverage_html
# ---------------------------------------------------------------------------
# Compiler: disable optimizations so every source line maps 1:1 to object code
# ---------------------------------------------------------------------------
CONFIG_COMPILER_OPTIMIZATION_NONE=y
# ---------------------------------------------------------------------------
# Application-level trace: enables the gcov data channel over JTAG
# ---------------------------------------------------------------------------
CONFIG_APPTRACE_ENABLE=y
CONFIG_APPTRACE_DEST_JTAG=y
# ---------------------------------------------------------------------------
# CSI mock mode: identical to sdkconfig.qemu so coverage runs use the same
# deterministic mock data path (no real WiFi hardware needed)
# ---------------------------------------------------------------------------
CONFIG_CSI_MOCK_ENABLED=y
CONFIG_CSI_MOCK_SKIP_WIFI_CONNECT=y
CONFIG_CSI_MOCK_SCENARIO=255
CONFIG_CSI_TARGET_IP="10.0.2.2"
CONFIG_CSI_MOCK_SCENARIO_DURATION_MS=5000
CONFIG_CSI_MOCK_LOG_FRAMES=y
# ---------------------------------------------------------------------------
# FreeRTOS and watchdog: match sdkconfig.qemu for QEMU timing tolerance
# ---------------------------------------------------------------------------
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096
CONFIG_ESP_TASK_WDT_TIMEOUT_S=30
CONFIG_ESP_INT_WDT_TIMEOUT_MS=800
# ---------------------------------------------------------------------------
# Logging and display
# ---------------------------------------------------------------------------
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
CONFIG_DISPLAY_ENABLE=n
@@ -1,33 +0,0 @@
# ESP32-S3 CSI Node — Default SDK Configuration
# This file is applied automatically by idf.py when no sdkconfig exists.
# Target: ESP32-S3
CONFIG_IDF_TARGET="esp32s3"
# Use custom partition table (8MB flash with OTA — ADR-045)
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_display.csv"
# Flash configuration: 8MB (Quad SPI)
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="8MB"
# Compiler optimization: optimize for size to reduce binary
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
# Enable CSI (Channel State Information) in WiFi driver
CONFIG_ESP_WIFI_CSI_ENABLED=y
# NVS encryption disabled by default (requires eFuse provisioning).
# Enable only after burning HMAC key to eFuse block.
# CONFIG_NVS_ENCRYPTION is not set
# Disable unused features to reduce binary size
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
# LWIP: enable extended socket options for UDP multicast
CONFIG_LWIP_SO_RCVBUF=y
# FreeRTOS: increase task stack for CSI processing
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
@@ -1,29 +0,0 @@
# ESP32-S3 CSI Node — 4MB Flash SDK Configuration (issue #265)
# For boards with 4MB flash (e.g. ESP32-S3 SuperMini 4MB).
#
# Build: cp sdkconfig.defaults.4mb sdkconfig.defaults && idf.py set-target esp32s3 && idf.py build
# Or: idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults.4mb" set-target esp32s3 && idf.py build
CONFIG_IDF_TARGET="esp32s3"
# 4MB flash partition table
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_4mb.csv"
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
# Compiler: optimize for size (critical for 4MB)
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
# CSI support
CONFIG_ESP_WIFI_CSI_ENABLED=y
# Disable display support to save flash (ADR-045 display requires 8MB)
# CONFIG_DISPLAY_ENABLE is not set
# Reduce logging to save flash
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
CONFIG_LWIP_SO_RCVBUF=y
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
@@ -18,9 +18,8 @@ CONFIG_COMPILER_OPTIMIZATION_SIZE=y
# Enable CSI (Channel State Information) in WiFi driver
CONFIG_ESP_WIFI_CSI_ENABLED=y
# NVS encryption disabled by default (requires eFuse provisioning).
# Enable only after burning HMAC key to eFuse block.
# CONFIG_NVS_ENCRYPTION is not set
# Enable NVS encryption for secure credential storage
CONFIG_NVS_ENCRYPTION=y
# Disable unused features to reduce binary size
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
-27
View File
@@ -1,27 +0,0 @@
# QEMU ESP32-S3 sdkconfig overlay (ADR-061)
#
# Merge with: idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.qemu" build
# ---- Mock CSI generator (replaces real WiFi CSI) ----
CONFIG_CSI_MOCK_ENABLED=y
CONFIG_CSI_MOCK_SKIP_WIFI_CONNECT=y
CONFIG_CSI_MOCK_SCENARIO=255
CONFIG_CSI_MOCK_SCENARIO_DURATION_MS=5000
CONFIG_CSI_MOCK_LOG_FRAMES=y
# ---- Network (QEMU SLIRP provides 10.0.2.x) ----
CONFIG_CSI_TARGET_IP="10.0.2.2"
# ---- Logging (verbose for validation) ----
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
# ---- FreeRTOS tuning for QEMU ----
# Increase timer task stack to prevent overflow from mock_csi timer callback
CONFIG_FREERTOS_TIMER_TASK_STACK_DEPTH=4096
# ---- Watchdog (relaxed for emulation — QEMU timing is not cycle-accurate) ----
CONFIG_ESP_TASK_WDT_TIMEOUT_S=30
CONFIG_ESP_INT_WDT_TIMEOUT_MS=800
# ---- Disable hardware-dependent features ----
CONFIG_DISPLAY_ENABLE=n
-79
View File
@@ -1,79 +0,0 @@
# Makefile for ESP32 CSI firmware fuzz testing targets (ADR-061 Layer 6).
#
# Requirements:
# - clang with libFuzzer support (clang 6.0+)
# - Linux or macOS (host-based fuzzing, no ESP-IDF needed)
#
# Usage:
# make all # Build all fuzz targets
# make fuzz_serialize # Build serialize target only
# make fuzz_edge # Build edge enqueue target only
# make fuzz_nvs # Build NVS config target only
# make run_serialize # Build and run serialize fuzzer (30s)
# make run_edge # Build and run edge fuzzer (30s)
# make run_nvs # Build and run NVS fuzzer (30s)
# make run_all # Run all fuzzers (30s each)
# make clean # Remove build artifacts
#
# Environment variables:
# FUZZ_DURATION=60 # Override fuzz duration in seconds
# FUZZ_JOBS=4 # Parallel fuzzing jobs
CC = clang
CFLAGS = -fsanitize=fuzzer,address,undefined -g -O1 \
-Istubs -I../main \
-DCONFIG_CSI_NODE_ID=1 \
-DCONFIG_CSI_WIFI_CHANNEL=6 \
-DCONFIG_CSI_WIFI_SSID=\"test\" \
-DCONFIG_CSI_TARGET_IP=\"192.168.1.1\" \
-DCONFIG_CSI_TARGET_PORT=5500 \
-DCONFIG_ESP_WIFI_CSI_ENABLED=1 \
-Wno-unused-function
STUBS_SRC = stubs/esp_stubs.c
MAIN_DIR = ../main
# Default fuzz duration (seconds) and jobs
FUZZ_DURATION ?= 30
FUZZ_JOBS ?= 1
.PHONY: all clean run_serialize run_edge run_nvs run_all
all: fuzz_serialize fuzz_edge fuzz_nvs
# --- Serialize fuzzer ---
# Tests csi_serialize_frame() with random wifi_csi_info_t inputs.
# Links against the real csi_collector.c (with stubs for ESP-IDF).
fuzz_serialize: fuzz_csi_serialize.c $(MAIN_DIR)/csi_collector.c $(STUBS_SRC)
$(CC) $(CFLAGS) $^ -o $@ -lm
# --- Edge enqueue fuzzer ---
# Tests the SPSC ring buffer push/pop logic with rapid-fire enqueues.
# Self-contained: reproduces ring buffer logic from edge_processing.c.
fuzz_edge: fuzz_edge_enqueue.c $(STUBS_SRC)
$(CC) $(CFLAGS) $^ -o $@ -lm
# --- NVS config validation fuzzer ---
# Tests all NVS config validation ranges with random values.
# Self-contained: reproduces validation logic from nvs_config.c.
fuzz_nvs: fuzz_nvs_config.c $(STUBS_SRC)
$(CC) $(CFLAGS) $^ -o $@ -lm
# --- Run targets ---
run_serialize: fuzz_serialize
@mkdir -p corpus_serialize
./fuzz_serialize corpus_serialize/ -max_total_time=$(FUZZ_DURATION) -max_len=2048 -jobs=$(FUZZ_JOBS)
run_edge: fuzz_edge
@mkdir -p corpus_edge
./fuzz_edge corpus_edge/ -max_total_time=$(FUZZ_DURATION) -max_len=4096 -jobs=$(FUZZ_JOBS)
run_nvs: fuzz_nvs
@mkdir -p corpus_nvs
./fuzz_nvs corpus_nvs/ -max_total_time=$(FUZZ_DURATION) -max_len=256 -jobs=$(FUZZ_JOBS)
run_all: run_serialize run_edge run_nvs
clean:
rm -f fuzz_serialize fuzz_edge fuzz_nvs
rm -rf corpus_serialize/ corpus_edge/ corpus_nvs/
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,203 +0,0 @@
/**
* @file fuzz_csi_serialize.c
* @brief libFuzzer target for csi_serialize_frame() (ADR-061 Layer 6).
*
* Takes fuzz input and constructs wifi_csi_info_t structs with random
* field values including extreme boundaries. Verifies that
* csi_serialize_frame() never crashes, triggers ASAN, or causes UBSAN.
*
* Build (Linux/macOS with clang):
* make fuzz_serialize
*
* Run:
* ./fuzz_serialize corpus/ -max_len=2048
*/
#include "esp_stubs.h"
/* Provide the globals that csi_collector.c references. */
#include "nvs_config.h"
nvs_config_t g_nvs_config;
/* Pull in the serialization function. */
#include "csi_collector.h"
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <stdlib.h>
/**
* Helper: read a value from the fuzz data, advancing the cursor.
* Returns 0 if insufficient data remains.
*/
static size_t fuzz_read(const uint8_t **data, size_t *size,
void *out, size_t n)
{
if (*size < n) {
memset(out, 0, n);
return 0;
}
memcpy(out, *data, n);
*data += n;
*size -= n;
return n;
}
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
if (size < 8) {
return 0; /* Need at least a few control bytes. */
}
const uint8_t *cursor = data;
size_t remaining = size;
/* Parse control bytes from fuzz input. */
uint8_t test_case;
int16_t iq_len_raw;
int8_t rssi;
uint8_t channel;
int8_t noise_floor;
uint8_t out_buf_scale; /* Controls output buffer size: 0-255. */
fuzz_read(&cursor, &remaining, &test_case, 1);
fuzz_read(&cursor, &remaining, &iq_len_raw, 2);
fuzz_read(&cursor, &remaining, &rssi, 1);
fuzz_read(&cursor, &remaining, &channel, 1);
fuzz_read(&cursor, &remaining, &noise_floor, 1);
fuzz_read(&cursor, &remaining, &out_buf_scale, 1);
/* --- Test case 0: Normal operation with fuzz-controlled values --- */
wifi_csi_info_t info;
memset(&info, 0, sizeof(info));
info.rx_ctrl.rssi = rssi;
info.rx_ctrl.channel = channel & 0x0F; /* 4-bit field */
info.rx_ctrl.noise_floor = noise_floor;
/* Use remaining fuzz data as I/Q buffer content. */
uint16_t iq_len;
if (iq_len_raw < 0) {
iq_len = 0;
} else if (iq_len_raw > (int16_t)remaining) {
iq_len = (uint16_t)remaining;
} else {
iq_len = (uint16_t)iq_len_raw;
}
int8_t iq_buf[CSI_MAX_FRAME_SIZE];
if (iq_len > 0 && remaining > 0) {
uint16_t copy = (iq_len > remaining) ? (uint16_t)remaining : iq_len;
memcpy(iq_buf, cursor, copy);
/* Zero-fill the rest if iq_len > available data. */
if (copy < iq_len) {
memset(iq_buf + copy, 0, iq_len - copy);
}
info.buf = iq_buf;
} else {
info.buf = iq_buf;
memset(iq_buf, 0, sizeof(iq_buf));
}
info.len = (int16_t)iq_len;
/* Output buffer: scale from tiny (1 byte) to full size. */
uint8_t out_buf[CSI_MAX_FRAME_SIZE + 64];
size_t out_len;
if (out_buf_scale == 0) {
out_len = 0;
} else if (out_buf_scale < 20) {
/* Small buffer: test buffer-too-small path. */
out_len = (size_t)out_buf_scale;
} else {
/* Normal/large buffer. */
out_len = sizeof(out_buf);
}
/* Call the function under test. Must not crash. */
size_t result = csi_serialize_frame(&info, out_buf, out_len);
/* Basic sanity: result must be 0 (error) or <= out_len. */
if (result > out_len) {
__builtin_trap(); /* Buffer overflow detected. */
}
/* --- Test case 1: NULL info pointer --- */
if (test_case & 0x01) {
result = csi_serialize_frame(NULL, out_buf, sizeof(out_buf));
if (result != 0) {
__builtin_trap(); /* NULL info should return 0. */
}
}
/* --- Test case 2: NULL output buffer --- */
if (test_case & 0x02) {
result = csi_serialize_frame(&info, NULL, sizeof(out_buf));
if (result != 0) {
__builtin_trap(); /* NULL buf should return 0. */
}
}
/* --- Test case 3: NULL I/Q buffer in info --- */
if (test_case & 0x04) {
wifi_csi_info_t null_iq_info = info;
null_iq_info.buf = NULL;
result = csi_serialize_frame(&null_iq_info, out_buf, sizeof(out_buf));
if (result != 0) {
__builtin_trap(); /* NULL info->buf should return 0. */
}
}
/* --- Test case 4: Extreme channel values --- */
if (test_case & 0x08) {
wifi_csi_info_t extreme_info = info;
extreme_info.buf = iq_buf;
/* Channel 0 (invalid). */
extreme_info.rx_ctrl.channel = 0;
csi_serialize_frame(&extreme_info, out_buf, sizeof(out_buf));
/* Channel 15 (max 4-bit value, invalid for WiFi). */
extreme_info.rx_ctrl.channel = 15;
csi_serialize_frame(&extreme_info, out_buf, sizeof(out_buf));
}
/* --- Test case 5: Extreme RSSI values --- */
if (test_case & 0x10) {
wifi_csi_info_t rssi_info = info;
rssi_info.buf = iq_buf;
rssi_info.rx_ctrl.rssi = -128;
csi_serialize_frame(&rssi_info, out_buf, sizeof(out_buf));
rssi_info.rx_ctrl.rssi = 127;
csi_serialize_frame(&rssi_info, out_buf, sizeof(out_buf));
}
/* --- Test case 6: Zero-length I/Q --- */
if (test_case & 0x20) {
wifi_csi_info_t zero_info = info;
zero_info.buf = iq_buf;
zero_info.len = 0;
result = csi_serialize_frame(&zero_info, out_buf, sizeof(out_buf));
/* len=0 means frame_size = CSI_HEADER_SIZE + 0 = 20 bytes. */
if (result != 0 && result != CSI_HEADER_SIZE) {
/* Either 0 (rejected) or exactly the header size is acceptable. */
}
}
/* --- Test case 7: Output buffer exactly header size --- */
if (test_case & 0x40) {
wifi_csi_info_t hdr_info = info;
hdr_info.buf = iq_buf;
hdr_info.len = 4; /* Small I/Q. */
/* Buffer exactly header_size + iq_len = 24 bytes. */
uint8_t tight_buf[CSI_HEADER_SIZE + 4];
result = csi_serialize_frame(&hdr_info, tight_buf, sizeof(tight_buf));
if (result > sizeof(tight_buf)) {
__builtin_trap();
}
}
return 0;
}
@@ -1,217 +0,0 @@
/**
* @file fuzz_edge_enqueue.c
* @brief libFuzzer target for edge_enqueue_csi() (ADR-061 Layer 6).
*
* Rapid-fire enqueues with varying iq_len from 0 to beyond
* EDGE_MAX_IQ_BYTES, testing the SPSC ring buffer overflow behavior
* and verifying no out-of-bounds writes occur.
*
* Build (Linux/macOS with clang):
* make fuzz_edge
*
* Run:
* ./fuzz_edge corpus/ -max_len=4096
*/
#include "esp_stubs.h"
/*
* We cannot include edge_processing.c directly because it references
* FreeRTOS task creation and other ESP-IDF APIs in edge_processing_init().
* Instead, we re-implement the SPSC ring buffer and edge_enqueue_csi()
* logic identically to the production code, testing the same algorithm.
*/
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <stdlib.h>
/* ---- Reproduce the ring buffer from edge_processing.h ---- */
#define EDGE_RING_SLOTS 16
#define EDGE_MAX_IQ_BYTES 1024
#define EDGE_MAX_SUBCARRIERS 128
typedef struct {
uint8_t iq_data[EDGE_MAX_IQ_BYTES];
uint16_t iq_len;
int8_t rssi;
uint8_t channel;
uint32_t timestamp_us;
} fuzz_ring_slot_t;
typedef struct {
fuzz_ring_slot_t slots[EDGE_RING_SLOTS];
volatile uint32_t head;
volatile uint32_t tail;
} fuzz_ring_buf_t;
static fuzz_ring_buf_t s_ring;
/**
* ring_push: identical logic to edge_processing.c::ring_push().
* This is the code path exercised by edge_enqueue_csi().
*/
static bool ring_push(const uint8_t *iq, uint16_t len,
int8_t rssi, uint8_t channel)
{
uint32_t next = (s_ring.head + 1) % EDGE_RING_SLOTS;
if (next == s_ring.tail) {
return false; /* Full. */
}
fuzz_ring_slot_t *slot = &s_ring.slots[s_ring.head];
uint16_t copy_len = (len > EDGE_MAX_IQ_BYTES) ? EDGE_MAX_IQ_BYTES : len;
memcpy(slot->iq_data, iq, copy_len);
slot->iq_len = copy_len;
slot->rssi = rssi;
slot->channel = channel;
slot->timestamp_us = (uint32_t)(esp_timer_get_time() & 0xFFFFFFFF);
__sync_synchronize();
s_ring.head = next;
return true;
}
/**
* ring_pop: identical logic to edge_processing.c::ring_pop().
*/
static bool ring_pop(fuzz_ring_slot_t *out)
{
if (s_ring.tail == s_ring.head) {
return false;
}
memcpy(out, &s_ring.slots[s_ring.tail], sizeof(fuzz_ring_slot_t));
__sync_synchronize();
s_ring.tail = (s_ring.tail + 1) % EDGE_RING_SLOTS;
return true;
}
/**
* Canary pattern: write to a buffer zone after ring memory to detect
* out-of-bounds writes. If the canary is overwritten, we trap.
*/
#define CANARY_SIZE 64
#define CANARY_BYTE 0xCD
static uint8_t s_canary_before[CANARY_SIZE];
/* s_ring is between the canaries (static allocation order not guaranteed,
* but ASAN will catch OOB writes regardless). */
static uint8_t s_canary_after[CANARY_SIZE];
static void init_canaries(void)
{
memset(s_canary_before, CANARY_BYTE, CANARY_SIZE);
memset(s_canary_after, CANARY_BYTE, CANARY_SIZE);
}
static void check_canaries(void)
{
for (int i = 0; i < CANARY_SIZE; i++) {
if (s_canary_before[i] != CANARY_BYTE) __builtin_trap();
if (s_canary_after[i] != CANARY_BYTE) __builtin_trap();
}
}
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
if (size < 4) return 0;
/* Reset ring buffer state for each fuzz iteration. */
memset(&s_ring, 0, sizeof(s_ring));
init_canaries();
const uint8_t *cursor = data;
size_t remaining = size;
/*
* Protocol: each "enqueue command" is:
* [0..1] iq_len (LE u16)
* [2] rssi (i8)
* [3] channel (u8)
* [4..] iq_data (up to iq_len bytes, zero-padded if short)
*
* We consume commands until data is exhausted.
*/
uint32_t enqueue_count = 0;
uint32_t full_count = 0;
uint32_t pop_count = 0;
while (remaining >= 4) {
uint16_t iq_len = (uint16_t)cursor[0] | ((uint16_t)cursor[1] << 8);
int8_t rssi = (int8_t)cursor[2];
uint8_t channel = cursor[3];
cursor += 4;
remaining -= 4;
/* Prepare I/Q data buffer.
* Even if iq_len > EDGE_MAX_IQ_BYTES, we pass it to ring_push
* which must clamp it internally. We need a source buffer that
* is at least iq_len bytes to avoid reading OOB. */
uint8_t iq_buf[EDGE_MAX_IQ_BYTES + 128];
memset(iq_buf, 0, sizeof(iq_buf));
/* Copy available fuzz data into iq_buf. */
uint16_t avail = (remaining > sizeof(iq_buf))
? (uint16_t)sizeof(iq_buf)
: (uint16_t)remaining;
if (avail > 0) {
memcpy(iq_buf, cursor, avail);
}
/* Advance cursor past the I/Q data portion.
* We consume min(iq_len, remaining) bytes. */
uint16_t consume = (iq_len > remaining) ? (uint16_t)remaining : iq_len;
cursor += consume;
remaining -= consume;
/* The key test: iq_len can be 0, normal, EDGE_MAX_IQ_BYTES,
* or larger (up to 65535). ring_push must clamp to EDGE_MAX_IQ_BYTES. */
bool ok = ring_push(iq_buf, iq_len, rssi, channel);
if (ok) {
enqueue_count++;
} else {
full_count++;
/* When ring is full, drain one slot to make room.
* This tests the interleaved push/pop pattern. */
fuzz_ring_slot_t popped;
if (ring_pop(&popped)) {
pop_count++;
/* Verify popped data is sane. */
if (popped.iq_len > EDGE_MAX_IQ_BYTES) {
__builtin_trap(); /* Clamping failed. */
}
}
/* Retry the enqueue after popping. */
ring_push(iq_buf, iq_len, rssi, channel);
}
/* Periodically check canaries. */
if ((enqueue_count + full_count) % 8 == 0) {
check_canaries();
}
}
/* Drain remaining items and verify each. */
fuzz_ring_slot_t popped;
while (ring_pop(&popped)) {
pop_count++;
if (popped.iq_len > EDGE_MAX_IQ_BYTES) {
__builtin_trap();
}
}
/* Final canary check. */
check_canaries();
/* Verify ring is now empty. */
if (s_ring.head != s_ring.tail) {
__builtin_trap();
}
return 0;
}
@@ -1,286 +0,0 @@
/**
* @file fuzz_nvs_config.c
* @brief libFuzzer target for NVS config validation logic (ADR-061 Layer 6).
*
* Since we cannot easily mock the full ESP-IDF NVS API under libFuzzer,
* this target extracts and tests the validation ranges used by
* nvs_config_load() when processing NVS values. Each validation check
* from nvs_config.c is reproduced here with fuzz-driven inputs.
*
* Build (Linux/macOS with clang):
* clang -fsanitize=fuzzer,address -g -I stubs fuzz_nvs_config.c \
* stubs/esp_stubs.c -o fuzz_nvs_config -lm
*
* Run:
* ./fuzz_nvs_config corpus/ -max_len=256
*/
#include "esp_stubs.h"
#include "nvs_config.h"
#include <stdint.h>
#include <stddef.h>
#include <string.h>
/**
* Validate a hop_count value using the same logic as nvs_config_load().
* Returns the validated value (0 = rejected).
*/
static uint8_t validate_hop_count(uint8_t val)
{
if (val >= 1 && val <= NVS_CFG_HOP_MAX) return val;
return 0;
}
/**
* Validate dwell_ms using the same logic as nvs_config_load().
* Returns the validated value (0 = rejected).
*/
static uint32_t validate_dwell_ms(uint32_t val)
{
if (val >= 10) return val;
return 0;
}
/**
* Validate TDM node count.
*/
static uint8_t validate_tdm_node_count(uint8_t val)
{
if (val >= 1) return val;
return 0;
}
/**
* Validate edge_tier (0-2).
*/
static uint8_t validate_edge_tier(uint8_t val)
{
if (val <= 2) return val;
return 0xFF; /* Invalid. */
}
/**
* Validate vital_window (32-256).
*/
static uint16_t validate_vital_window(uint16_t val)
{
if (val >= 32 && val <= 256) return val;
return 0;
}
/**
* Validate vital_interval_ms (>= 100).
*/
static uint16_t validate_vital_interval(uint16_t val)
{
if (val >= 100) return val;
return 0;
}
/**
* Validate top_k_count (1-32).
*/
static uint8_t validate_top_k(uint8_t val)
{
if (val >= 1 && val <= 32) return val;
return 0;
}
/**
* Validate power_duty (10-100).
*/
static uint8_t validate_power_duty(uint8_t val)
{
if (val >= 10 && val <= 100) return val;
return 0;
}
/**
* Validate wasm_max_modules (1-8).
*/
static uint8_t validate_wasm_max(uint8_t val)
{
if (val >= 1 && val <= 8) return val;
return 0;
}
/**
* Validate CSI channel: 1-14 (2.4 GHz) or 36-177 (5 GHz).
*/
static uint8_t validate_csi_channel(uint8_t val)
{
if ((val >= 1 && val <= 14) || (val >= 36 && val <= 177)) return val;
return 0;
}
/**
* Validate tdm_slot_index < tdm_node_count (clamp to 0 on violation).
*/
static uint8_t validate_tdm_slot(uint8_t slot, uint8_t node_count)
{
if (slot >= node_count) return 0;
return slot;
}
/**
* Test string field handling: ensure NVS_CFG_SSID_MAX length is respected.
*/
static void test_string_bounds(const uint8_t *data, size_t len)
{
char ssid[NVS_CFG_SSID_MAX];
char password[NVS_CFG_PASS_MAX];
char ip[NVS_CFG_IP_MAX];
/* Simulate strncpy with NVS_CFG_*_MAX bounds. */
size_t ssid_len = (len > NVS_CFG_SSID_MAX - 1) ? NVS_CFG_SSID_MAX - 1 : len;
memcpy(ssid, data, ssid_len);
ssid[ssid_len] = '\0';
size_t pass_len = (len > NVS_CFG_PASS_MAX - 1) ? NVS_CFG_PASS_MAX - 1 : len;
memcpy(password, data, pass_len);
password[pass_len] = '\0';
size_t ip_len = (len > NVS_CFG_IP_MAX - 1) ? NVS_CFG_IP_MAX - 1 : len;
memcpy(ip, data, ip_len);
ip[ip_len] = '\0';
/* Ensure null termination holds. */
if (ssid[NVS_CFG_SSID_MAX - 1] != '\0' && ssid_len == NVS_CFG_SSID_MAX - 1) {
/* OK: we set terminator above. */
}
}
/**
* Test presence_thresh and fall_thresh fixed-point conversion.
* nvs_config.c stores as u16 with value * 1000.
*/
static void test_thresh_conversion(uint16_t pres_raw, uint16_t fall_raw)
{
float pres = (float)pres_raw / 1000.0f;
float fall = (float)fall_raw / 1000.0f;
/* Ensure no NaN or Inf from valid integer inputs. */
if (pres != pres) __builtin_trap(); /* NaN check. */
if (fall != fall) __builtin_trap(); /* NaN check. */
/* Range: 0.0 to 65.535 for u16/1000. Both should be finite. */
if (pres < 0.0f || pres > 65.536f) __builtin_trap();
if (fall < 0.0f || fall > 65.536f) __builtin_trap();
}
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
if (size < 32) return 0;
const uint8_t *p = data;
/* Extract fuzz-driven config field values. */
uint8_t hop_count = p[0];
uint32_t dwell_ms = (uint32_t)p[1] | ((uint32_t)p[2] << 8)
| ((uint32_t)p[3] << 16) | ((uint32_t)p[4] << 24);
uint8_t tdm_slot = p[5];
uint8_t tdm_nodes = p[6];
uint8_t edge_tier = p[7];
uint16_t vital_win = (uint16_t)p[8] | ((uint16_t)p[9] << 8);
uint16_t vital_int = (uint16_t)p[10] | ((uint16_t)p[11] << 8);
uint8_t top_k = p[12];
uint8_t power_duty = p[13];
uint8_t wasm_max = p[14];
uint8_t csi_channel = p[15];
uint16_t pres_thresh = (uint16_t)p[16] | ((uint16_t)p[17] << 8);
uint16_t fall_thresh = (uint16_t)p[18] | ((uint16_t)p[19] << 8);
uint8_t node_id = p[20];
uint16_t target_port = (uint16_t)p[21] | ((uint16_t)p[22] << 8);
uint8_t wasm_verify = p[23];
/* Run all validators. These must not crash regardless of input. */
(void)validate_hop_count(hop_count);
(void)validate_dwell_ms(dwell_ms);
(void)validate_tdm_node_count(tdm_nodes);
(void)validate_edge_tier(edge_tier);
(void)validate_vital_window(vital_win);
(void)validate_vital_interval(vital_int);
(void)validate_top_k(top_k);
(void)validate_power_duty(power_duty);
(void)validate_wasm_max(wasm_max);
(void)validate_csi_channel(csi_channel);
/* Validate TDM slot with validated node count. */
uint8_t valid_nodes = validate_tdm_node_count(tdm_nodes);
if (valid_nodes > 0) {
(void)validate_tdm_slot(tdm_slot, valid_nodes);
}
/* Test threshold conversions. */
test_thresh_conversion(pres_thresh, fall_thresh);
/* Test string field bounds with remaining data. */
if (size > 24) {
test_string_bounds(data + 24, size - 24);
}
/* Construct a full nvs_config_t and verify field assignments don't overflow. */
nvs_config_t cfg;
memset(&cfg, 0, sizeof(cfg));
cfg.target_port = target_port;
cfg.node_id = node_id;
uint8_t valid_hop = validate_hop_count(hop_count);
cfg.channel_hop_count = valid_hop ? valid_hop : 1;
/* Fill channel list from fuzz data. */
for (uint8_t i = 0; i < NVS_CFG_HOP_MAX && (24 + i) < size; i++) {
cfg.channel_list[i] = data[24 + i];
}
cfg.dwell_ms = validate_dwell_ms(dwell_ms) ? dwell_ms : 50;
cfg.tdm_slot_index = 0;
cfg.tdm_node_count = valid_nodes ? valid_nodes : 1;
if (cfg.tdm_slot_index >= cfg.tdm_node_count) {
cfg.tdm_slot_index = 0;
}
uint8_t valid_tier = validate_edge_tier(edge_tier);
cfg.edge_tier = (valid_tier != 0xFF) ? valid_tier : 2;
cfg.presence_thresh = (float)pres_thresh / 1000.0f;
cfg.fall_thresh = (float)fall_thresh / 1000.0f;
uint16_t valid_win = validate_vital_window(vital_win);
cfg.vital_window = valid_win ? valid_win : 256;
uint16_t valid_int = validate_vital_interval(vital_int);
cfg.vital_interval_ms = valid_int ? valid_int : 1000;
uint8_t valid_topk = validate_top_k(top_k);
cfg.top_k_count = valid_topk ? valid_topk : 8;
uint8_t valid_duty = validate_power_duty(power_duty);
cfg.power_duty = valid_duty ? valid_duty : 100;
uint8_t valid_wasm = validate_wasm_max(wasm_max);
cfg.wasm_max_modules = valid_wasm ? valid_wasm : 4;
cfg.wasm_verify = wasm_verify ? 1 : 0;
uint8_t valid_ch = validate_csi_channel(csi_channel);
cfg.csi_channel = valid_ch;
/* MAC filter: use 6 bytes from fuzz data if available. */
if (size >= 32) {
memcpy(cfg.filter_mac, data + 24, 6);
cfg.filter_mac_set = (data[30] & 0x01) ? 1 : 0;
}
/* Verify struct is self-consistent — no field should be in an impossible state. */
if (cfg.channel_hop_count > NVS_CFG_HOP_MAX) __builtin_trap();
if (cfg.tdm_slot_index >= cfg.tdm_node_count) __builtin_trap();
if (cfg.edge_tier > 2) __builtin_trap();
if (cfg.wasm_max_modules > 8 || cfg.wasm_max_modules < 1) __builtin_trap();
if (cfg.top_k_count > 32 || cfg.top_k_count < 1) __builtin_trap();
if (cfg.power_duty > 100 || cfg.power_duty < 10) __builtin_trap();
return 0;
}
@@ -1,5 +0,0 @@
/* Stub: redirect to unified stubs header. */
#ifndef ESP_ERR_H_STUB
#define ESP_ERR_H_STUB
#include "esp_stubs.h"
#endif
@@ -1,5 +0,0 @@
/* Stub: redirect to unified stubs header. */
#ifndef ESP_LOG_H_STUB
#define ESP_LOG_H_STUB
#include "esp_stubs.h"
#endif
@@ -1,65 +0,0 @@
/**
* @file esp_stubs.c
* @brief Implementation of ESP-IDF stubs for host-based fuzz testing.
*
* Must be compiled with: -Istubs -I../main
* so that ESP-IDF headers resolve to stubs/ and firmware headers
* resolve to ../main/.
*/
#include "esp_stubs.h"
#include "edge_processing.h"
#include "wasm_runtime.h"
#include <stdint.h>
/** Monotonically increasing microsecond counter for esp_timer_get_time(). */
static int64_t s_fake_time_us = 0;
int64_t esp_timer_get_time(void)
{
/* Advance by 50ms each call (~20 Hz CSI rate simulation). */
s_fake_time_us += 50000;
return s_fake_time_us;
}
/* ---- stream_sender stubs ---- */
int stream_sender_send(const uint8_t *data, size_t len)
{
(void)data;
return (int)len;
}
int stream_sender_init(void)
{
return 0;
}
int stream_sender_init_with(const char *ip, uint16_t port)
{
(void)ip; (void)port;
return 0;
}
void stream_sender_deinit(void)
{
}
/* ---- wasm_runtime stubs ---- */
void wasm_runtime_on_frame(const float *phases, const float *amplitudes,
const float *variances, uint16_t n_sc,
const edge_vitals_pkt_t *vitals)
{
(void)phases; (void)amplitudes; (void)variances;
(void)n_sc; (void)vitals;
}
esp_err_t wasm_runtime_init(void) { return ESP_OK; }
esp_err_t wasm_runtime_load(const uint8_t *d, uint32_t l, uint8_t *id) { (void)d; (void)l; (void)id; return ESP_OK; }
esp_err_t wasm_runtime_start(uint8_t id) { (void)id; return ESP_OK; }
esp_err_t wasm_runtime_stop(uint8_t id) { (void)id; return ESP_OK; }
esp_err_t wasm_runtime_unload(uint8_t id) { (void)id; return ESP_OK; }
void wasm_runtime_on_timer(void) {}
void wasm_runtime_get_info(wasm_module_info_t *info, uint8_t *count) { (void)info; if(count) *count = 0; }
esp_err_t wasm_runtime_set_manifest(uint8_t id, const char *n, uint32_t c, uint32_t m) { (void)id; (void)n; (void)c; (void)m; return ESP_OK; }
@@ -1,189 +0,0 @@
/**
* @file esp_stubs.h
* @brief Minimal ESP-IDF type stubs for host-based fuzz testing.
*
* Provides just enough type definitions and macros to compile
* csi_collector.c and edge_processing.c on a Linux/macOS host
* without the full ESP-IDF SDK.
*/
#ifndef ESP_STUBS_H
#define ESP_STUBS_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
/* ---- esp_err.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
/* ---- esp_log.h ---- */
#define ESP_LOGI(tag, fmt, ...) ((void)0)
#define ESP_LOGW(tag, fmt, ...) ((void)0)
#define ESP_LOGE(tag, fmt, ...) ((void)0)
#define ESP_LOGD(tag, fmt, ...) ((void)0)
#define ESP_ERROR_CHECK(x) ((void)(x))
/* ---- esp_timer.h ---- */
typedef void *esp_timer_handle_t;
/** Timer callback type (matches ESP-IDF signature). */
typedef void (*esp_timer_cb_t)(void *arg);
/** Timer creation arguments (matches ESP-IDF esp_timer_create_args_t). */
typedef struct {
esp_timer_cb_t callback;
void *arg;
const char *name;
} esp_timer_create_args_t;
/**
* Stub: returns a monotonically increasing microsecond counter.
* Declared here, defined in esp_stubs.c.
*/
int64_t esp_timer_get_time(void);
/** Stub: timer lifecycle (no-ops for fuzz testing). */
static inline esp_err_t esp_timer_create(const esp_timer_create_args_t *args, esp_timer_handle_t *h) {
(void)args; if (h) *h = (void *)1; return ESP_OK;
}
static inline esp_err_t esp_timer_start_periodic(esp_timer_handle_t h, uint64_t period) {
(void)h; (void)period; return ESP_OK;
}
static inline esp_err_t esp_timer_stop(esp_timer_handle_t h) { (void)h; return ESP_OK; }
static inline esp_err_t esp_timer_delete(esp_timer_handle_t h) { (void)h; return ESP_OK; }
/* ---- esp_wifi_types.h ---- */
/** Minimal rx_ctrl fields needed by csi_serialize_frame. */
typedef struct {
signed rssi : 8;
unsigned channel : 4;
unsigned noise_floor : 8;
unsigned rx_ant : 2;
/* Padding to fill out the struct so it compiles. */
unsigned _pad : 10;
} wifi_pkt_rx_ctrl_t;
/** Minimal wifi_csi_info_t needed by csi_serialize_frame. */
typedef struct {
wifi_pkt_rx_ctrl_t rx_ctrl;
uint8_t mac[6];
int16_t len; /**< Length of the I/Q buffer in bytes. */
int8_t *buf; /**< Pointer to I/Q data. */
} wifi_csi_info_t;
/* ---- Kconfig defaults ---- */
#ifndef CONFIG_CSI_NODE_ID
#define CONFIG_CSI_NODE_ID 1
#endif
#ifndef CONFIG_CSI_WIFI_CHANNEL
#define CONFIG_CSI_WIFI_CHANNEL 6
#endif
#ifndef CONFIG_CSI_WIFI_SSID
#define CONFIG_CSI_WIFI_SSID "test_ssid"
#endif
#ifndef CONFIG_CSI_TARGET_IP
#define CONFIG_CSI_TARGET_IP "192.168.1.1"
#endif
#ifndef CONFIG_CSI_TARGET_PORT
#define CONFIG_CSI_TARGET_PORT 5500
#endif
/* Suppress the build-time guard in csi_collector.c */
#ifndef CONFIG_ESP_WIFI_CSI_ENABLED
#define CONFIG_ESP_WIFI_CSI_ENABLED 1
#endif
/* ---- sdkconfig.h stub ---- */
/* (empty — all needed CONFIG_ macros are above) */
/* ---- FreeRTOS stubs ---- */
#define pdMS_TO_TICKS(x) ((x))
#define pdPASS 1
typedef int BaseType_t;
static inline int xPortGetCoreID(void) { return 0; }
static inline void vTaskDelay(uint32_t ticks) { (void)ticks; }
static inline BaseType_t xTaskCreatePinnedToCore(
void (*fn)(void *), const char *name, uint32_t stack,
void *arg, int prio, void *handle, int core)
{
(void)fn; (void)name; (void)stack; (void)arg;
(void)prio; (void)handle; (void)core;
return pdPASS;
}
/* ---- WiFi API stubs (no-ops) ---- */
typedef int wifi_interface_t;
typedef int wifi_second_chan_t;
#define WIFI_IF_STA 0
#define WIFI_SECOND_CHAN_NONE 0
typedef struct {
unsigned filter_mask;
} wifi_promiscuous_filter_t;
typedef int wifi_promiscuous_pkt_type_t;
#define WIFI_PROMIS_FILTER_MASK_MGMT 1
#define WIFI_PROMIS_FILTER_MASK_DATA 2
typedef struct {
int lltf_en;
int htltf_en;
int stbc_htltf2_en;
int ltf_merge_en;
int channel_filter_en;
int manu_scale;
int shift;
} wifi_csi_config_t;
typedef struct {
uint8_t primary;
} wifi_ap_record_t;
static inline esp_err_t esp_wifi_set_promiscuous(bool en) { (void)en; return ESP_OK; }
static inline esp_err_t esp_wifi_set_promiscuous_rx_cb(void *cb) { (void)cb; return ESP_OK; }
static inline esp_err_t esp_wifi_set_promiscuous_filter(wifi_promiscuous_filter_t *f) { (void)f; return ESP_OK; }
static inline esp_err_t esp_wifi_set_csi_config(wifi_csi_config_t *c) { (void)c; return ESP_OK; }
static inline esp_err_t esp_wifi_set_csi_rx_cb(void *cb, void *ctx) { (void)cb; (void)ctx; return ESP_OK; }
static inline esp_err_t esp_wifi_set_csi(bool en) { (void)en; return ESP_OK; }
static inline esp_err_t esp_wifi_set_channel(uint8_t ch, wifi_second_chan_t sc) { (void)ch; (void)sc; return ESP_OK; }
static inline esp_err_t esp_wifi_80211_tx(wifi_interface_t ifx, const void *b, int len, bool en) { (void)ifx; (void)b; (void)len; (void)en; return ESP_OK; }
static inline esp_err_t esp_wifi_sta_get_ap_info(wifi_ap_record_t *ap) { (void)ap; return ESP_FAIL; }
static inline const char *esp_err_to_name(esp_err_t code) { (void)code; return "STUB"; }
/* ---- NVS stubs ---- */
typedef uint32_t nvs_handle_t;
#define NVS_READONLY 0
static inline esp_err_t nvs_open(const char *ns, int mode, nvs_handle_t *h) { (void)ns; (void)mode; (void)h; return ESP_FAIL; }
static inline void nvs_close(nvs_handle_t h) { (void)h; }
static inline esp_err_t nvs_get_str(nvs_handle_t h, const char *k, char *v, size_t *l) { (void)h; (void)k; (void)v; (void)l; return ESP_FAIL; }
static inline esp_err_t nvs_get_u8(nvs_handle_t h, const char *k, uint8_t *v) { (void)h; (void)k; (void)v; return ESP_FAIL; }
static inline esp_err_t nvs_get_u16(nvs_handle_t h, const char *k, uint16_t *v) { (void)h; (void)k; (void)v; return ESP_FAIL; }
static inline esp_err_t nvs_get_u32(nvs_handle_t h, const char *k, uint32_t *v) { (void)h; (void)k; (void)v; return ESP_FAIL; }
static inline esp_err_t nvs_get_blob(nvs_handle_t h, const char *k, void *v, size_t *l) { (void)h; (void)k; (void)v; (void)l; return ESP_FAIL; }
/* ---- stream_sender stubs (defined in esp_stubs.c) ---- */
int stream_sender_send(const uint8_t *data, size_t len);
int stream_sender_init(void);
int stream_sender_init_with(const char *ip, uint16_t port);
void stream_sender_deinit(void);
/*
* wasm_runtime stubs: defined in esp_stubs.c.
* The actual prototype comes from ../main/wasm_runtime.h (via csi_collector.c).
* We just need the definition in esp_stubs.c to link.
*/
#endif /* ESP_STUBS_H */
@@ -1,5 +0,0 @@
/* Stub: redirect to unified stubs header. */
#ifndef ESP_TIMER_H_STUB
#define ESP_TIMER_H_STUB
#include "esp_stubs.h"
#endif
@@ -1,5 +0,0 @@
/* Stub: redirect to unified stubs header. */
#ifndef ESP_WIFI_H_STUB
#define ESP_WIFI_H_STUB
#include "esp_stubs.h"
#endif
@@ -1,5 +0,0 @@
/* Stub: redirect to unified stubs header. */
#ifndef ESP_WIFI_TYPES_H_STUB
#define ESP_WIFI_TYPES_H_STUB
#include "esp_stubs.h"
#endif
@@ -1,5 +0,0 @@
/* Stub: redirect to unified stubs header. */
#ifndef FREERTOS_H_STUB
#define FREERTOS_H_STUB
#include "esp_stubs.h"
#endif
@@ -1,5 +0,0 @@
/* Stub: redirect to unified stubs header. */
#ifndef FREERTOS_TASK_H_STUB
#define FREERTOS_TASK_H_STUB
#include "esp_stubs.h"
#endif
-5
View File
@@ -1,5 +0,0 @@
/* Stub: redirect to unified stubs header. */
#ifndef NVS_H_STUB
#define NVS_H_STUB
#include "esp_stubs.h"
#endif
@@ -1,5 +0,0 @@
/* Stub: redirect to unified stubs header. */
#ifndef NVS_FLASH_H_STUB
#define NVS_FLASH_H_STUB
#include "esp_stubs.h"
#endif
@@ -1,5 +0,0 @@
/* Stub: sdkconfig.h — all CONFIG_ macros provided by esp_stubs.h. */
#ifndef SDKCONFIG_H_STUB
#define SDKCONFIG_H_STUB
#include "esp_stubs.h"
#endif
@@ -0,0 +1,10 @@
{"type":"edit","file":"unknown","timestamp":1772820418129,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1772820462588,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1772820472219,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1772832571444,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1772832585997,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773099593107,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773115162931,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773115172336,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773147087836,"sessionId":null}
{"type":"edit","file":"unknown","timestamp":1773149448951,"sessionId":null}
@@ -4,4 +4,5 @@ pub mod ota;
pub mod provision;
pub mod server;
pub mod settings;
pub mod training;
pub mod wasm;
@@ -0,0 +1,482 @@
//! Training commands for the desktop application.
//!
//! Provides Tauri commands for:
//! - GPU detection
//! - Dataset management
//! - Model/checkpoint operations
//! - Training job control
//! - RuVector configuration
//! - Metrics retrieval
use crate::domain::training::{
CheckpointInfo, DatasetFormat, DatasetInfo, EpochMetrics, EvaluationMetrics,
GpuBackend, GpuInfo, JointAccuracy, LiveTestMetrics,
ModelInfo, ModelType, RuVectorConfig, TrainingConfig, TrainingJob,
TrainingProgress, TrainingStatus,
};
use crate::state::AppState;
use tauri::State;
// ============================================================================
// Standard Datasets (built-in)
// ============================================================================
fn get_standard_datasets() -> Vec<DatasetInfo> {
vec![
DatasetInfo {
id: "mmfi".into(),
name: "MM-Fi Dataset".into(),
description: "Multi-modal WiFi sensing dataset with 40 subjects, 27 activities".into(),
format: DatasetFormat::MmFi,
size_mb: 2400.0,
samples: 320000,
downloaded: false,
path: None,
url: Some("https://ntu-aiot-lab.github.io/mm-fi".into()),
},
DatasetInfo {
id: "wipose".into(),
name: "Wi-Pose Dataset".into(),
description: "WiFi-based pose estimation with 3D skeleton annotations".into(),
format: DatasetFormat::WiPose,
size_mb: 1800.0,
samples: 150000,
downloaded: false,
path: None,
url: Some("https://github.com/Wi-Pose".into()),
},
DatasetInfo {
id: "wiar".into(),
name: "WiAR Dataset".into(),
description: "WiFi activity recognition with CSI data".into(),
format: DatasetFormat::Wiar,
size_mb: 500.0,
samples: 45000,
downloaded: false,
path: None,
url: Some("https://github.com/WiAR".into()),
},
]
}
// ============================================================================
// Standard Model Architectures
// ============================================================================
fn get_standard_models() -> Vec<ModelInfo> {
vec![
ModelInfo {
id: "csi-encoder-cnn".into(),
name: "CSI Encoder (CNN)".into(),
model_type: ModelType::Encoder,
description: "Convolutional encoder for CSI amplitude/phase features".into(),
params_m: 2.3,
memory_mb: 128,
paper: None,
},
ModelInfo {
id: "csi-encoder-transformer".into(),
name: "CSI Encoder (Transformer)".into(),
model_type: ModelType::Encoder,
description: "Self-attention based CSI feature extraction".into(),
params_m: 8.5,
memory_mb: 384,
paper: Some("WiFi-ViT 2024".into()),
},
ModelInfo {
id: "pose-decoder-lstm".into(),
name: "Pose Decoder (LSTM)".into(),
model_type: ModelType::Decoder,
description: "Recurrent decoder for temporal pose estimation".into(),
params_m: 1.8,
memory_mb: 96,
paper: None,
},
ModelInfo {
id: "pose-decoder-gru".into(),
name: "Pose Decoder (GRU)".into(),
model_type: ModelType::Decoder,
description: "Gated recurrent unit pose decoder (faster)".into(),
params_m: 1.2,
memory_mb: 64,
paper: None,
},
ModelInfo {
id: "aether-embedding".into(),
name: "AETHER Embedding".into(),
model_type: ModelType::Embedding,
description: "Contrastive CSI embedding for person re-identification (ADR-024)".into(),
params_m: 4.2,
memory_mb: 192,
paper: Some("AETHER 2025".into()),
},
ModelInfo {
id: "meridian-adaptor".into(),
name: "MERIDIAN Adaptor".into(),
model_type: ModelType::Adaptor,
description: "Cross-environment domain generalization module (ADR-027)".into(),
params_m: 3.1,
memory_mb: 144,
paper: Some("MERIDIAN 2025".into()),
},
]
}
// ============================================================================
// GPU Detection Commands
// ============================================================================
/// Detect available GPU(s) and return information.
#[tauri::command]
pub async fn detect_gpu(state: State<'_, AppState>) -> Result<GpuInfo, String> {
// Check for cached GPU info
if let Ok(training) = state.training.lock() {
if let Some(ref info) = training.gpu_info {
return Ok(info.clone());
}
}
// Detect GPU
let info = detect_gpu_internal();
// Cache the result
if let Ok(mut training) = state.training.lock() {
training.gpu_info = Some(info.clone());
}
Ok(info)
}
fn detect_gpu_internal() -> GpuInfo {
// Check for Metal on macOS
#[cfg(target_os = "macos")]
{
// Check if system has Apple Silicon or discrete GPU
let has_metal = std::process::Command::new("system_profiler")
.args(["SPDisplaysDataType", "-json"])
.output()
.map(|o| {
let output = String::from_utf8_lossy(&o.stdout);
output.contains("Metal") || output.contains("Apple M")
})
.unwrap_or(false);
if has_metal {
// Try to get GPU name
let name = std::process::Command::new("system_profiler")
.args(["SPDisplaysDataType"])
.output()
.ok()
.and_then(|o| {
let output = String::from_utf8_lossy(&o.stdout);
// Parse chipset name
for line in output.lines() {
if line.contains("Chipset Model:") {
return line.split(':').nth(1).map(|s| s.trim().to_string());
}
}
None
});
return GpuInfo {
available: true,
backend: GpuBackend::Metal,
name,
memory_mb: None, // Metal doesn't easily expose this
cuda_version: None,
metal_supported: true,
};
}
}
// Check for CUDA on Linux/Windows
#[cfg(any(target_os = "linux", target_os = "windows"))]
{
// Try nvidia-smi for CUDA detection
if let Ok(output) = std::process::Command::new("nvidia-smi")
.args(["--query-gpu=name,memory.total", "--format=csv,noheader,nounits"])
.output()
{
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let parts: Vec<&str> = stdout.trim().split(',').collect();
let name = parts.first().map(|s| s.trim().to_string());
let memory_mb = parts.get(1)
.and_then(|s| s.trim().parse::<u64>().ok());
// Get CUDA version
let cuda_version = std::process::Command::new("nvidia-smi")
.output()
.ok()
.and_then(|o| {
let output = String::from_utf8_lossy(&o.stdout);
for line in output.lines() {
if line.contains("CUDA Version:") {
return line.split("CUDA Version:")
.nth(1)
.map(|s| s.split_whitespace().next().unwrap_or("").to_string());
}
}
None
});
return GpuInfo {
available: true,
backend: GpuBackend::Cuda,
name,
memory_mb,
cuda_version,
metal_supported: false,
};
}
}
}
// Fall back to CPU
GpuInfo {
available: false,
backend: GpuBackend::Cpu,
name: None,
memory_mb: None,
cuda_version: None,
metal_supported: false,
}
}
// ============================================================================
// Dataset Commands
// ============================================================================
/// List available datasets (both standard and downloaded).
#[tauri::command]
pub async fn list_datasets(state: State<'_, AppState>) -> Result<Vec<String>, String> {
let training = state.training.lock().map_err(|e| e.to_string())?;
// Return IDs of downloaded datasets
Ok(training.datasets.iter()
.filter(|d| d.downloaded)
.map(|d| d.id.clone())
.collect())
}
/// Get full dataset information.
#[tauri::command]
pub async fn get_datasets(state: State<'_, AppState>) -> Result<Vec<DatasetInfo>, String> {
let mut training = state.training.lock().map_err(|e| e.to_string())?;
// Initialize with standard datasets if empty
if training.datasets.is_empty() {
training.datasets = get_standard_datasets();
}
Ok(training.datasets.clone())
}
/// Download a dataset (placeholder - actual download would need async HTTP).
#[tauri::command]
pub async fn download_dataset(
dataset_id: String,
state: State<'_, AppState>,
) -> Result<DatasetInfo, String> {
let mut training = state.training.lock().map_err(|e| e.to_string())?;
// Find the dataset
let dataset = training.datasets.iter_mut()
.find(|d| d.id == dataset_id)
.ok_or_else(|| format!("Dataset not found: {}", dataset_id))?;
// Simulate download completion
dataset.downloaded = true;
dataset.path = Some(format!("~/.ruview/datasets/{}", dataset_id));
Ok(dataset.clone())
}
// ============================================================================
// Model/Checkpoint Commands
// ============================================================================
/// List available model architectures.
#[tauri::command]
pub async fn list_models() -> Result<Vec<ModelInfo>, String> {
Ok(get_standard_models())
}
/// List saved checkpoints.
#[tauri::command]
pub async fn list_checkpoints(state: State<'_, AppState>) -> Result<Vec<CheckpointInfo>, String> {
let training = state.training.lock().map_err(|e| e.to_string())?;
Ok(training.checkpoints.clone())
}
/// Export a model checkpoint to ONNX or TorchScript.
#[tauri::command]
pub async fn export_model(
checkpoint_id: String,
format: String,
state: State<'_, AppState>,
) -> Result<String, String> {
let training = state.training.lock().map_err(|e| e.to_string())?;
let checkpoint = training.checkpoints.iter()
.find(|c| c.id == checkpoint_id)
.ok_or_else(|| format!("Checkpoint not found: {}", checkpoint_id))?;
let output_path = match format.as_str() {
"onnx" => format!("{}.onnx", checkpoint.path.trim_end_matches(".pt")),
"torchscript" => format!("{}.ts", checkpoint.path.trim_end_matches(".pt")),
_ => return Err(format!("Unsupported format: {}", format)),
};
// In a real implementation, this would call the actual export logic
Ok(output_path)
}
// ============================================================================
// Training Job Commands
// ============================================================================
/// Start a training job.
#[tauri::command]
pub async fn start_training(
config: TrainingConfig,
state: State<'_, AppState>,
) -> Result<String, String> {
let mut training = state.training.lock().map_err(|e| e.to_string())?;
// Create a new job
let job_id = uuid::Uuid::new_v4().to_string();
let job = TrainingJob {
id: job_id.clone(),
config,
status: TrainingStatus::Running,
started_at: Some(chrono::Utc::now().to_rfc3339()),
progress: TrainingProgress::default(),
loss_history: Vec::new(),
};
training.current_job = Some(job);
// In a real implementation, this would spawn a background training thread
// and emit progress events via Tauri's event system
Ok(job_id)
}
/// Stop the current training job.
#[tauri::command]
pub async fn stop_training(state: State<'_, AppState>) -> Result<(), String> {
let mut training = state.training.lock().map_err(|e| e.to_string())?;
if let Some(ref mut job) = training.current_job {
job.status = TrainingStatus::Paused;
}
Ok(())
}
/// Get current training progress.
#[tauri::command]
pub async fn training_progress(state: State<'_, AppState>) -> Result<Option<TrainingProgress>, String> {
let training = state.training.lock().map_err(|e| e.to_string())?;
Ok(training.current_job.as_ref().map(|j| j.progress.clone()))
}
// ============================================================================
// RuVector Configuration Commands
// ============================================================================
/// Get current RuVector configuration.
#[tauri::command]
pub async fn get_ruvector_config(state: State<'_, AppState>) -> Result<RuVectorConfig, String> {
let training = state.training.lock().map_err(|e| e.to_string())?;
Ok(training.ruvector_config.clone())
}
/// Set RuVector configuration.
#[tauri::command]
pub async fn set_ruvector_config(
config: RuVectorConfig,
state: State<'_, AppState>,
) -> Result<(), String> {
let mut training = state.training.lock().map_err(|e| e.to_string())?;
training.ruvector_config = config;
Ok(())
}
/// Test RuVector modules on live CSI data.
#[tauri::command]
pub async fn test_ruvector_live(
_state: State<'_, AppState>,
) -> Result<LiveTestMetrics, String> {
// In a real implementation, this would process live CSI data
// through the RuVector pipeline and return metrics
Ok(LiveTestMetrics {
fps: 30.0,
latency_ms: 15.0,
persons_detected: 1,
})
}
// ============================================================================
// Metrics Commands
// ============================================================================
/// Get training history (loss/accuracy per epoch).
#[tauri::command]
pub async fn get_training_history(state: State<'_, AppState>) -> Result<Vec<EpochMetrics>, String> {
let training = state.training.lock().map_err(|e| e.to_string())?;
Ok(training.training_history.clone())
}
/// Get evaluation metrics.
#[tauri::command]
pub async fn get_evaluation_metrics(state: State<'_, AppState>) -> Result<Option<EvaluationMetrics>, String> {
let training = state.training.lock().map_err(|e| e.to_string())?;
Ok(training.evaluation_metrics.clone())
}
/// Get per-joint accuracy metrics.
#[tauri::command]
pub async fn get_joint_accuracies(state: State<'_, AppState>) -> Result<Vec<JointAccuracy>, String> {
let training = state.training.lock().map_err(|e| e.to_string())?;
Ok(training.joint_accuracies.clone())
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_standard_datasets() {
let datasets = get_standard_datasets();
assert_eq!(datasets.len(), 3);
assert!(datasets.iter().any(|d| d.id == "mmfi"));
}
#[test]
fn test_standard_models() {
let models = get_standard_models();
assert_eq!(models.len(), 6);
assert!(models.iter().any(|m| m.id == "csi-encoder-cnn"));
}
#[test]
fn test_detect_gpu_internal() {
let info = detect_gpu_internal();
// Just verify it returns valid data
assert!(matches!(info.backend, GpuBackend::Cpu | GpuBackend::Cuda | GpuBackend::Metal));
}
#[test]
fn test_ruvector_config_default() {
let config = RuVectorConfig::default();
assert!(config.mincut_enabled);
assert_eq!(config.attention_heads, 4);
}
}
@@ -1,3 +1,4 @@
pub mod config;
pub mod firmware;
pub mod node;
pub mod training;
@@ -0,0 +1,312 @@
//! Training domain types for the desktop application.
use serde::{Deserialize, Serialize};
/// GPU backend type.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum GpuBackend {
Cuda,
Metal,
#[default]
Cpu,
}
/// GPU information.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GpuInfo {
pub available: bool,
pub backend: GpuBackend,
pub name: Option<String>,
pub memory_mb: Option<u64>,
pub cuda_version: Option<String>,
pub metal_supported: bool,
}
/// Dataset format type.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum DatasetFormat {
#[default]
MmFi,
WiPose,
Wiar,
Custom,
}
/// Dataset information.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatasetInfo {
pub id: String,
pub name: String,
pub description: String,
pub format: DatasetFormat,
pub size_mb: f64,
pub samples: u64,
pub downloaded: bool,
pub path: Option<String>,
pub url: Option<String>,
}
/// Model architecture type.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum ModelType {
#[default]
Encoder,
Decoder,
Embedding,
Adaptor,
}
/// Model architecture information.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelInfo {
pub id: String,
pub name: String,
pub model_type: ModelType,
pub description: String,
pub params_m: f64,
pub memory_mb: u64,
pub paper: Option<String>,
}
/// Checkpoint information.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CheckpointInfo {
pub id: String,
pub model_id: String,
pub name: String,
pub epoch: u32,
pub val_loss: f64,
pub created_at: String,
pub path: String,
pub size_mb: f64,
}
/// Training configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrainingConfig {
pub dataset_id: String,
pub model_id: String,
pub epochs: u32,
pub batch_size: u32,
pub learning_rate: f64,
pub optimizer: OptimizerType,
pub weight_decay: f64,
pub use_augmentation: bool,
pub checkpoint_every: u32,
}
impl Default for TrainingConfig {
fn default() -> Self {
Self {
dataset_id: "mmfi".into(),
model_id: "csi-encoder-cnn".into(),
epochs: 100,
batch_size: 32,
learning_rate: 0.001,
optimizer: OptimizerType::Adam,
weight_decay: 0.0001,
use_augmentation: true,
checkpoint_every: 10,
}
}
}
/// Optimizer type.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum OptimizerType {
#[default]
Adam,
AdamW,
Sgd,
}
/// Training job status.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum TrainingStatus {
#[default]
Pending,
Running,
Paused,
Completed,
Failed,
}
/// Training progress.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TrainingProgress {
pub epoch: u32,
pub total_epochs: u32,
pub batch: u32,
pub total_batches: u32,
pub train_loss: f64,
pub val_loss: Option<f64>,
pub learning_rate: f64,
pub eta_secs: u64,
pub gpu_memory_mb: Option<u64>,
}
/// Training job.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrainingJob {
pub id: String,
pub config: TrainingConfig,
pub status: TrainingStatus,
pub started_at: Option<String>,
pub progress: TrainingProgress,
pub loss_history: Vec<EpochMetrics>,
}
/// Metrics for a single epoch.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EpochMetrics {
pub epoch: u32,
pub train_loss: f64,
pub val_loss: f64,
pub train_acc: f64,
pub val_acc: f64,
pub learning_rate: f64,
pub timestamp: String,
}
/// Evaluation metrics.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EvaluationMetrics {
pub pck_05: f64,
pub pck_10: f64,
pub pck_20: f64,
pub map_50: f64,
pub map_75: f64,
pub iou: f64,
}
/// Per-joint accuracy.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JointAccuracy {
pub joint: String,
pub accuracy: f64,
}
/// RuVector interpolation mode.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
pub enum InterpolationMode {
Linear,
Cubic,
#[default]
Sparse,
}
/// RuVector module configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RuVectorConfig {
// MinCut parameters
pub mincut_enabled: bool,
pub mincut_threshold: f64,
pub mincut_max_persons: u32,
// Attention parameters
pub attention_enabled: bool,
pub attention_heads: u32,
pub attention_dropout: f64,
// Temporal parameters
pub temporal_enabled: bool,
pub temporal_window_ms: u32,
pub temporal_compression_ratio: u32,
// Solver parameters
pub solver_enabled: bool,
pub solver_interpolation: InterpolationMode,
pub solver_subcarrier_count: u32,
// BVP parameters
pub bvp_enabled: bool,
pub bvp_filter_hz: (f64, f64),
}
impl Default for RuVectorConfig {
fn default() -> Self {
Self {
mincut_enabled: true,
mincut_threshold: 0.5,
mincut_max_persons: 5,
attention_enabled: true,
attention_heads: 4,
attention_dropout: 0.1,
temporal_enabled: true,
temporal_window_ms: 500,
temporal_compression_ratio: 4,
solver_enabled: true,
solver_interpolation: InterpolationMode::Sparse,
solver_subcarrier_count: 56,
bvp_enabled: false,
bvp_filter_hz: (0.7, 4.0),
}
}
}
/// Live test metrics from RuVector processing.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LiveTestMetrics {
pub fps: f64,
pub latency_ms: f64,
pub persons_detected: u32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gpu_info_default() {
let info = GpuInfo::default();
assert!(!info.available);
assert_eq!(info.backend, GpuBackend::Cpu);
}
#[test]
fn test_training_config_default() {
let config = TrainingConfig::default();
assert_eq!(config.epochs, 100);
assert_eq!(config.batch_size, 32);
assert_eq!(config.optimizer, OptimizerType::Adam);
}
#[test]
fn test_ruvector_config_default() {
let config = RuVectorConfig::default();
assert!(config.mincut_enabled);
assert_eq!(config.mincut_threshold, 0.5);
assert_eq!(config.attention_heads, 4);
}
#[test]
fn test_serialization() {
let config = TrainingConfig::default();
let json = serde_json::to_string(&config).unwrap();
let parsed: TrainingConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.epochs, config.epochs);
}
#[test]
fn test_dataset_info() {
let dataset = DatasetInfo {
id: "mmfi".into(),
name: "MM-Fi Dataset".into(),
description: "Multi-modal WiFi sensing".into(),
format: DatasetFormat::MmFi,
size_mb: 2400.0,
samples: 320000,
downloaded: false,
path: None,
url: Some("https://example.com/mmfi.zip".into()),
};
assert_eq!(dataset.id, "mmfi");
assert!(!dataset.downloaded);
}
}
@@ -2,7 +2,7 @@ pub mod commands;
pub mod domain;
pub mod state;
use commands::{discovery, flash, ota, provision, server, settings, wasm};
use commands::{discovery, flash, ota, provision, server, settings, training, wasm};
pub fn run() {
tauri::Builder::default()
@@ -46,6 +46,23 @@ pub fn run() {
// Settings
settings::get_settings,
settings::save_settings,
// Training
training::detect_gpu,
training::list_datasets,
training::get_datasets,
training::download_dataset,
training::list_models,
training::list_checkpoints,
training::export_model,
training::start_training,
training::stop_training,
training::training_progress,
training::get_ruvector_config,
training::set_ruvector_config,
training::test_ruvector_live,
training::get_training_history,
training::get_evaluation_metrics,
training::get_joint_accuracies,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
@@ -3,6 +3,10 @@ use std::sync::Mutex;
use std::time::Instant;
use crate::domain::node::DiscoveredNode;
use crate::domain::training::{
CheckpointInfo, DatasetInfo, EpochMetrics, EvaluationMetrics,
GpuInfo, JointAccuracy, RuVectorConfig, TrainingJob,
};
/// Sub-state for discovered nodes.
#[derive(Default)]
@@ -87,6 +91,33 @@ impl Default for SettingsState {
}
}
/// Sub-state for training operations.
pub struct TrainingState {
pub gpu_info: Option<GpuInfo>,
pub datasets: Vec<DatasetInfo>,
pub checkpoints: Vec<CheckpointInfo>,
pub current_job: Option<TrainingJob>,
pub ruvector_config: RuVectorConfig,
pub training_history: Vec<EpochMetrics>,
pub evaluation_metrics: Option<EvaluationMetrics>,
pub joint_accuracies: Vec<JointAccuracy>,
}
impl Default for TrainingState {
fn default() -> Self {
Self {
gpu_info: None,
datasets: Vec::new(),
checkpoints: Vec::new(),
current_job: None,
ruvector_config: RuVectorConfig::default(),
training_history: Vec::new(),
evaluation_metrics: None,
joint_accuracies: Vec::new(),
}
}
}
/// Top-level application state managed by Tauri.
pub struct AppState {
pub discovery: Mutex<DiscoveryState>,
@@ -94,6 +125,7 @@ pub struct AppState {
pub flash: Mutex<FlashState>,
pub ota: Mutex<OtaState>,
pub settings: Mutex<SettingsState>,
pub training: Mutex<TrainingState>,
}
impl Default for AppState {
@@ -104,6 +136,7 @@ impl Default for AppState {
flash: Mutex::new(FlashState::default()),
ota: Mutex::new(OtaState::default()),
settings: Mutex::new(SettingsState::default()),
training: Mutex::new(TrainingState::default()),
}
}
}
@@ -135,6 +168,9 @@ impl AppState {
if let Ok(mut settings) = self.settings.lock() {
*settings = SettingsState::default();
}
if let Ok(mut training) = self.training.lock() {
*training = TrainingState::default();
}
}
}
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
"productName": "RuView Desktop",
"version": "0.4.4",
"version": "0.5.0",
"identifier": "net.ruv.ruview",
"build": {
"frontendDist": "ui/dist",
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../baseline-browser-mapping/dist/cli.cjs" "$@"
else
exec node "$basedir/../baseline-browser-mapping/dist/cli.cjs" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\baseline-browser-mapping\dist\cli.cjs" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../baseline-browser-mapping/dist/cli.cjs" $args
} else {
& "$basedir/node$exe" "$basedir/../baseline-browser-mapping/dist/cli.cjs" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../baseline-browser-mapping/dist/cli.cjs" $args
} else {
& "node$exe" "$basedir/../baseline-browser-mapping/dist/cli.cjs" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../browserslist/cli.js" "$@"
else
exec node "$basedir/../browserslist/cli.js" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\browserslist\cli.js" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../browserslist/cli.js" $args
} else {
& "$basedir/node$exe" "$basedir/../browserslist/cli.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../browserslist/cli.js" $args
} else {
& "node$exe" "$basedir/../browserslist/cli.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../esbuild/bin/esbuild" "$@"
else
exec node "$basedir/../esbuild/bin/esbuild" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\esbuild\bin\esbuild" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../esbuild/bin/esbuild" $args
} else {
& "$basedir/node$exe" "$basedir/../esbuild/bin/esbuild" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../esbuild/bin/esbuild" $args
} else {
& "node$exe" "$basedir/../esbuild/bin/esbuild" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../jsesc/bin/jsesc" "$@"
else
exec node "$basedir/../jsesc/bin/jsesc" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\jsesc\bin\jsesc" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../jsesc/bin/jsesc" $args
} else {
& "$basedir/node$exe" "$basedir/../jsesc/bin/jsesc" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../jsesc/bin/jsesc" $args
} else {
& "node$exe" "$basedir/../jsesc/bin/jsesc" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../json5/lib/cli.js" "$@"
else
exec node "$basedir/../json5/lib/cli.js" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\json5\lib\cli.js" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../json5/lib/cli.js" $args
} else {
& "$basedir/node$exe" "$basedir/../json5/lib/cli.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../json5/lib/cli.js" $args
} else {
& "node$exe" "$basedir/../json5/lib/cli.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../loose-envify/cli.js" "$@"
else
exec node "$basedir/../loose-envify/cli.js" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\loose-envify\cli.js" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../loose-envify/cli.js" $args
} else {
& "$basedir/node$exe" "$basedir/../loose-envify/cli.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../loose-envify/cli.js" $args
} else {
& "node$exe" "$basedir/../loose-envify/cli.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../nanoid/bin/nanoid.cjs" "$@"
else
exec node "$basedir/../nanoid/bin/nanoid.cjs" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\nanoid\bin\nanoid.cjs" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
} else {
& "$basedir/node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
} else {
& "node$exe" "$basedir/../nanoid/bin/nanoid.cjs" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../@babel/parser/bin/babel-parser.js" "$@"
else
exec node "$basedir/../@babel/parser/bin/babel-parser.js" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\@babel\parser\bin\babel-parser.js" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../@babel/parser/bin/babel-parser.js" $args
} else {
& "$basedir/node$exe" "$basedir/../@babel/parser/bin/babel-parser.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../@babel/parser/bin/babel-parser.js" $args
} else {
& "node$exe" "$basedir/../@babel/parser/bin/babel-parser.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../rollup/dist/bin/rollup" "$@"
else
exec node "$basedir/../rollup/dist/bin/rollup" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\rollup\dist\bin\rollup" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../rollup/dist/bin/rollup" $args
} else {
& "$basedir/node$exe" "$basedir/../rollup/dist/bin/rollup" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../rollup/dist/bin/rollup" $args
} else {
& "node$exe" "$basedir/../rollup/dist/bin/rollup" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../semver/bin/semver.js" "$@"
else
exec node "$basedir/../semver/bin/semver.js" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\semver\bin\semver.js" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../semver/bin/semver.js" $args
} else {
& "$basedir/node$exe" "$basedir/../semver/bin/semver.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../semver/bin/semver.js" $args
} else {
& "node$exe" "$basedir/../semver/bin/semver.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
else
exec node "$basedir/../typescript/bin/tsc" "$@"
fi
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\typescript\bin\tsc" %*
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../typescript/bin/tsc" $args
} else {
& "$basedir/node$exe" "$basedir/../typescript/bin/tsc" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../typescript/bin/tsc" $args
} else {
& "node$exe" "$basedir/../typescript/bin/tsc" $args
}
$ret=$LASTEXITCODE
}
exit $ret

Some files were not shown because too many files have changed in this diff Show More