mirror of
https://github.com/ruvnet/RuView
synced 2026-07-31 18:51:42 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b2aacd923 | |||
| 1d4af7c757 | |||
| 523be943b0 | |||
| a467dfed9f | |||
| d793c1f49f | |||
| 3457610c9f | |||
| e9d5ea3ad3 | |||
| 9cefb32815 | |||
| a7c74e0c57 | |||
| 98a2b0462c | |||
| e5e3d42ca2 | |||
| 7c1351fd5d | |||
| 6e03a47867 | |||
| 9d1140de2d | |||
| 952f27a1ce | |||
| f7d043d727 | |||
| ff91d4e8cf |
@@ -1 +0,0 @@
|
|||||||
31273
|
|
||||||
@@ -0,0 +1,370 @@
|
|||||||
|
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
|
||||||
+15
-1
@@ -226,4 +226,18 @@ v1/src/sensing/mac_wifi
|
|||||||
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||||
# refer to https://docs.cursor.com/context/ignore-files
|
# refer to https://docs.cursor.com/context/ignore-files
|
||||||
.cursorignore
|
.cursorignore
|
||||||
.cursorindexingignore
|
.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
|
||||||
Binary file not shown.
Vendored
+49
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -5,9 +5,49 @@ 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/),
|
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).
|
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]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### 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
|
- **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`
|
- 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`
|
- CSI recording: `GET /api/v1/recording/list`, `POST /api/v1/recording/start`, `POST /api/v1/recording/stop`, `DELETE /api/v1/recording/:id`
|
||||||
|
|||||||
@@ -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 |
|
| [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) |
|
| [Build Guide](docs/build-guide.md) | Building from source (Rust and Python) |
|
||||||
| [Architecture Decisions](docs/adr/README.md) | 48 ADRs — why each technical choice was made, organized by domain (hardware, signal processing, ML, platform, infrastructure) |
|
| [Architecture Decisions](docs/adr/README.md) | 62 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 |
|
| [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 |
|
| [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,10 +87,14 @@ docker run -p 3000:3000 ruvnet/wifi-densepose:latest
|
|||||||
</a>
|
</a>
|
||||||
<br>
|
<br>
|
||||||
<em>Real-time pose skeleton from WiFi CSI signals — no cameras, no wearables</em>
|
<em>Real-time pose skeleton from WiFi CSI signals — no cameras, no wearables</em>
|
||||||
<br>
|
<br><br>
|
||||||
<a href="https://ruvnet.github.io/RuView/"><strong>▶ Live Observatory Demo</strong></a>
|
<a href="https://ruvnet.github.io/RuView/"><strong>▶ Live Observatory Demo</strong></a>
|
||||||
|
|
|
||||||
|
<a href="https://ruvnet.github.io/RuView/pose-fusion.html"><strong>▶ Dual-Modal Pose Fusion Demo</strong></a>
|
||||||
|
|
||||||
> The [server](#-quick-start) is optional for visualization and aggregation — the ESP32 [runs independently](#esp32-s3-hardware-pipeline) for presence detection, vital signs, and fall alerts.
|
> 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
|
## 🚀 Key Features
|
||||||
@@ -1043,14 +1047,23 @@ Download a pre-built binary — no build toolchain needed:
|
|||||||
|
|
||||||
| Release | What's included | Tag |
|
| Release | What's included | Tag |
|
||||||
|---------|-----------------|-----|
|
|---------|-----------------|-----|
|
||||||
| [v0.2.0](https://github.com/ruvnet/RuView/releases/tag/v0.2.0-esp32) | Stable — raw CSI streaming, multi-node TDM, channel hopping | `v0.2.0-esp32` |
|
| [v0.4.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.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.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
|
```bash
|
||||||
# 1. Flash the firmware to your ESP32-S3
|
# 1. Flash the firmware to your ESP32-S3 (8MB flash — most boards)
|
||||||
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
|
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
|
||||||
write_flash --flash_mode dio --flash_size 8MB \
|
write_flash --flash-mode dio --flash-size 8MB --flash-freq 80m \
|
||||||
0x0 bootloader.bin 0x8000 partition-table.bin 0x10000 esp32-csi-node.bin
|
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
|
||||||
|
|
||||||
# 2. Set WiFi credentials and server address (stored in flash, survives reboots)
|
# 2. Set WiFi credentials and server address (stored in flash, survives reboots)
|
||||||
python firmware/esp32-csi-node/provision.py --port COM7 \
|
python firmware/esp32-csi-node/provision.py --port COM7 \
|
||||||
@@ -1098,9 +1111,9 @@ python firmware/esp32-csi-node/provision.py --port COM7 \
|
|||||||
--ssid "YourWiFi" --password "secret" --target-ip 192.168.1.20 \
|
--ssid "YourWiFi" --password "secret" --target-ip 192.168.1.20 \
|
||||||
--edge-tier 2
|
--edge-tier 2
|
||||||
|
|
||||||
# Fine-tune detection thresholds
|
# Fine-tune detection thresholds (fall-thresh in milli-units: 15000 = 15.0 rad/s²)
|
||||||
python firmware/esp32-csi-node/provision.py --port COM7 \
|
python firmware/esp32-csi-node/provision.py --port COM7 \
|
||||||
--edge-tier 2 --vital-int 500 --fall-thresh 5000 --subk-count 16
|
--edge-tier 2 --vital-int 500 --fall-thresh 15000 --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.
|
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.
|
||||||
@@ -1690,6 +1703,82 @@ WebSocket: `ws://localhost:3001/ws/sensing` (real-time sensing + vital signs)
|
|||||||
|
|
||||||
</details>
|
</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>
|
<details>
|
||||||
<summary><strong>Python Legacy CLI</strong> — v1 API server commands</summary>
|
<summary><strong>Python Legacy CLI</strong> — v1 API server commands</summary>
|
||||||
|
|
||||||
@@ -1709,7 +1798,9 @@ wifi-densepose tasks list # List background tasks
|
|||||||
<details>
|
<details>
|
||||||
<summary><strong>Documentation Links</strong></summary>
|
<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)
|
- [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)
|
- [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>
|
</details>
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
# ADR-057: Firmware CSI Build Guard and sdkconfig.defaults
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------------|---------------------------------------------|
|
||||||
|
| **Status** | Accepted |
|
||||||
|
| **Date** | 2026-03-12 |
|
||||||
|
| **Authors** | ruv |
|
||||||
|
| **Issues** | #223, #238, #234, #210, #190 |
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Multiple GitHub issues (#223, #238, #234, #210, #190) report firmware problems
|
||||||
|
that fall into two categories:
|
||||||
|
|
||||||
|
1. **CSI not enabled at runtime** — The committed `sdkconfig` had
|
||||||
|
`# CONFIG_ESP_WIFI_CSI_ENABLED is not set` (line 1135), meaning users who
|
||||||
|
built from source or used pre-built binaries got the runtime error:
|
||||||
|
`E (6700) wifi:CSI not enabled in menuconfig!`
|
||||||
|
|
||||||
|
Root cause: `sdkconfig.defaults.template` existed with the correct setting
|
||||||
|
(`CONFIG_ESP_WIFI_CSI_ENABLED=y`) but ESP-IDF only reads
|
||||||
|
`sdkconfig.defaults` — not `.template` suffixed files. No `sdkconfig.defaults`
|
||||||
|
file was committed.
|
||||||
|
|
||||||
|
2. **Unsupported ESP32 variants** — Users attempting to use original ESP32
|
||||||
|
(D0WD) and ESP32-C3 boards. The firmware targets ESP32-S3 only
|
||||||
|
(`CONFIG_IDF_TARGET="esp32s3"`, Xtensa architecture) and this was not
|
||||||
|
surfaced clearly enough in documentation or build errors.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### Fix 1: Commit `sdkconfig.defaults` (not just template)
|
||||||
|
|
||||||
|
Copy `sdkconfig.defaults.template` → `sdkconfig.defaults` so that ESP-IDF
|
||||||
|
applies the correct defaults (including `CONFIG_ESP_WIFI_CSI_ENABLED=y`)
|
||||||
|
automatically when `sdkconfig` is regenerated.
|
||||||
|
|
||||||
|
### Fix 2: `#error` compile-time guard in `csi_collector.c`
|
||||||
|
|
||||||
|
Add a preprocessor guard:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#ifndef CONFIG_ESP_WIFI_CSI_ENABLED
|
||||||
|
#error "CONFIG_ESP_WIFI_CSI_ENABLED must be set in sdkconfig."
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
This turns a confusing runtime crash into a clear compile-time error with
|
||||||
|
instructions on how to fix it.
|
||||||
|
|
||||||
|
### Fix 3: Fix committed `sdkconfig`
|
||||||
|
|
||||||
|
Change line 1135 from `# CONFIG_ESP_WIFI_CSI_ENABLED is not set` to
|
||||||
|
`CONFIG_ESP_WIFI_CSI_ENABLED=y`.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Positive**: New builds will always have CSI enabled. Users building from
|
||||||
|
source will get a clear compile error if CSI is somehow disabled.
|
||||||
|
- **Positive**: Pre-built release binaries will include CSI support.
|
||||||
|
- **Neutral**: Original ESP32 and ESP32-C3 remain unsupported. This is by
|
||||||
|
design — only ESP32-S3 has the CSI API surface we depend on. Future ADRs
|
||||||
|
may address multi-target support if demand warrants it.
|
||||||
|
- **Negative**: None identified.
|
||||||
|
|
||||||
|
## Hardware Support Matrix
|
||||||
|
|
||||||
|
| Variant | CSI Support | Firmware Target | Status |
|
||||||
|
|--------------|-------------|-----------------|---------------|
|
||||||
|
| ESP32-S3 | Yes | Yes | Supported |
|
||||||
|
| ESP32 (orig) | Partial | No | Unsupported |
|
||||||
|
| ESP32-C3 | Yes (IDF 5.1+) | No | Unsupported |
|
||||||
|
| ESP32-C6 | Yes | No | Unsupported |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- ESP32-C3 and C6 use RISC-V architecture; a separate build target
|
||||||
|
(`idf.py set-target esp32c3`) would be needed.
|
||||||
|
- Original ESP32 has limited CSI (no STBC HT-LTF2, fewer subcarriers).
|
||||||
|
- Users on unsupported hardware can still write custom firmware using the
|
||||||
|
ADR-018 binary frame format (magic `0xC5110001`) for interop with the
|
||||||
|
Rust aggregator.
|
||||||
@@ -0,0 +1,392 @@
|
|||||||
|
# ADR-058: Dual-Modal WASM Browser Pose Estimation — Live Video + WiFi CSI Fusion
|
||||||
|
|
||||||
|
- **Status**: Proposed
|
||||||
|
- **Date**: 2026-03-12
|
||||||
|
- **Deciders**: ruv
|
||||||
|
- **Tags**: wasm, browser, cnn, pose-estimation, ruvector, video, multimodal, fusion
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
WiFi-DensePose estimates human poses from WiFi CSI (Channel State Information).
|
||||||
|
The `ruvector-cnn` crate provides a pure Rust CNN (MobileNet-V3) with WASM bindings.
|
||||||
|
Both modalities exist independently — what's missing is **fusing live webcam video
|
||||||
|
with WiFi CSI** in a single browser demo to achieve robust pose estimation that
|
||||||
|
works even when one modality degrades (occlusion, signal noise, poor lighting).
|
||||||
|
|
||||||
|
Existing assets:
|
||||||
|
|
||||||
|
1. **`wifi-densepose-wasm`** — CSI signal processing compiled to WASM
|
||||||
|
2. **`wifi-densepose-sensing-server`** — Axum server streaming live CSI via WebSocket
|
||||||
|
3. **`ruvector-cnn`** — Pure Rust CNN with MobileNet-V3 backbones, SIMD, contrastive learning
|
||||||
|
4. **`ruvector-cnn-wasm`** — wasm-bindgen bindings: `WasmCnnEmbedder`, `SimdOps`, `LayerOps`, contrastive losses
|
||||||
|
5. **`vendor/ruvector/examples/wasm-vanilla/`** — Reference vanilla JS WASM example
|
||||||
|
|
||||||
|
Research shows multi-modal fusion (camera + WiFi) significantly outperforms either alone:
|
||||||
|
- Camera fails under occlusion, poor lighting, privacy constraints
|
||||||
|
- WiFi CSI fails with signal noise, multipath, low spatial resolution
|
||||||
|
- Fusion compensates: WiFi provides through-wall coverage, camera provides fine-grained detail
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Build a **dual-modal browser demo** at `examples/wasm-browser-pose/` that:
|
||||||
|
|
||||||
|
1. Captures **live webcam video** via `getUserMedia` API
|
||||||
|
2. Receives **live WiFi CSI** via WebSocket from the sensing server
|
||||||
|
3. Processes **both streams** through separate CNN pipelines in `ruvector-cnn-wasm`
|
||||||
|
4. **Fuses embeddings** with learned attention weights for combined pose estimation
|
||||||
|
5. Renders **video overlay** with skeleton + WiFi confidence heatmap on Canvas
|
||||||
|
6. Runs entirely in the browser — all inference client-side via WASM
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Browser │
|
||||||
|
│ │
|
||||||
|
│ ┌────────────┐ ┌────────────────┐ ┌───────────────────┐ │
|
||||||
|
│ │ getUserMedia│───▶│ Video Frame │───▶│ CNN WASM │ │
|
||||||
|
│ │ (Webcam) │ │ Capture │ │ (Visual Embedder) │ │
|
||||||
|
│ └────────────┘ │ 224×224 RGB │ │ → 512-dim │ │
|
||||||
|
│ └────────────────┘ └────────┬──────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ visual_embedding │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────▼──────┐ │
|
||||||
|
│ ┌────────────┐ ┌────────────────┐ │ │ │
|
||||||
|
│ │ WebSocket │───▶│ CSI WASM │ │ Attention │ │
|
||||||
|
│ │ Client │ │ (densepose- │ │ Fusion │ │
|
||||||
|
│ │ │ │ wasm) │ │ Module │ │
|
||||||
|
│ └────────────┘ └───────┬────────┘ │ │ │
|
||||||
|
│ │ └──────┬──────┘ │
|
||||||
|
│ ┌───────▼────────┐ │ │
|
||||||
|
│ │ CNN WASM │ fused_embedding │
|
||||||
|
│ │ (CSI Embedder) │ │ │
|
||||||
|
│ │ → 512-dim │ ┌──────▼──────┐ │
|
||||||
|
│ └───────┬────────┘ │ Pose │ │
|
||||||
|
│ │ │ Decoder │ │
|
||||||
|
│ csi_embedding │ → 17 kpts │ │
|
||||||
|
│ │ └──────┬──────┘ │
|
||||||
|
│ └──────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────────────┐ ┌─────▼──────┐ │
|
||||||
|
│ │ Video Canvas │◀────────│ Overlay │ │
|
||||||
|
│ │ + Skeleton │ │ Renderer │ │
|
||||||
|
│ │ + Heatmap │ └────────────┘ │
|
||||||
|
│ └──────────────┘ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
▲ ▲
|
||||||
|
│ getUserMedia │ WebSocket
|
||||||
|
│ (camera) │ (ws://host:3030/ws/csi)
|
||||||
|
│ │
|
||||||
|
┌────┴────┐ ┌───────┴─────────┐
|
||||||
|
│ Webcam │ │ Sensing Server │
|
||||||
|
└─────────┘ └─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dual Pipeline Design
|
||||||
|
|
||||||
|
Two parallel CNN pipelines run on each frame tick (~30 FPS):
|
||||||
|
|
||||||
|
| Pipeline | Input | Preprocessing | CNN Config | Output |
|
||||||
|
|----------|-------|---------------|------------|--------|
|
||||||
|
| **Visual** | Webcam frame (640×480) | Resize to 224×224 RGB, ImageNet normalize | MobileNet-V3 Small, 512-dim | Visual embedding |
|
||||||
|
| **CSI** | CSI frame (ADR-018 binary) | Amplitude/phase/delta → 224×224 pseudo-RGB | MobileNet-V3 Small, 512-dim | CSI embedding |
|
||||||
|
|
||||||
|
Both use the same `WasmCnnEmbedder` but with separate instances and weight sets.
|
||||||
|
|
||||||
|
### Fusion Strategy
|
||||||
|
|
||||||
|
**Learned attention-weighted fusion** combines the two 512-dim embeddings:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Attention fusion: learn which modality to trust per-dimension
|
||||||
|
// α ∈ [0,1]^512 — attention weights (shipped as JSON, trained offline)
|
||||||
|
// visual_emb, csi_emb ∈ R^512
|
||||||
|
|
||||||
|
function fuseEmbeddings(visual_emb, csi_emb, attention_weights) {
|
||||||
|
const fused = new Float32Array(512);
|
||||||
|
for (let i = 0; i < 512; i++) {
|
||||||
|
const α = attention_weights[i];
|
||||||
|
fused[i] = α * visual_emb[i] + (1 - α) * csi_emb[i];
|
||||||
|
}
|
||||||
|
return fused;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dynamic confidence gating** adjusts fusion based on signal quality:
|
||||||
|
|
||||||
|
| Condition | Behavior |
|
||||||
|
|-----------|----------|
|
||||||
|
| Good video + good CSI | Balanced fusion (α ≈ 0.5) |
|
||||||
|
| Poor lighting / occlusion | CSI-dominant (α → 0, WiFi takes over) |
|
||||||
|
| CSI noise / no ESP32 | Video-dominant (α → 1, camera only) |
|
||||||
|
| Video-only mode (no WiFi) | α = 1.0, pure visual CNN pose estimation |
|
||||||
|
| CSI-only mode (no camera) | α = 0.0, pure WiFi pose estimation |
|
||||||
|
|
||||||
|
Quality detection:
|
||||||
|
- **Video quality**: Frame brightness variance (dark = low quality), motion blur score
|
||||||
|
- **CSI quality**: Signal-to-noise ratio from `wifi-densepose-wasm`, coherence gate output
|
||||||
|
|
||||||
|
### CSI-to-Image Encoding
|
||||||
|
|
||||||
|
CSI data encoded as 3-channel pseudo-image for the CSI CNN pipeline:
|
||||||
|
|
||||||
|
| Channel | Data | Normalization |
|
||||||
|
|---------|------|---------------|
|
||||||
|
| R | CSI amplitude (subcarrier × time window) | Min-max to [0, 255] |
|
||||||
|
| G | CSI phase (unwrapped, subcarrier × time window) | Min-max to [0, 255] |
|
||||||
|
| B | Temporal difference (frame-to-frame Δ amplitude) | Abs, min-max to [0, 255] |
|
||||||
|
|
||||||
|
### Video Processing
|
||||||
|
|
||||||
|
Webcam frames processed through standard ImageNet pipeline:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Capture frame from video element
|
||||||
|
const frame = captureVideoFrame(videoElement, 224, 224); // Returns Uint8Array RGB
|
||||||
|
|
||||||
|
// ImageNet normalization happens inside WasmCnnEmbedder.extract()
|
||||||
|
const visual_embedding = visual_embedder.extract(frame, 224, 224);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pose Keypoint Mapping
|
||||||
|
|
||||||
|
17 COCO-format keypoints decoded from the fused 512-dim embedding:
|
||||||
|
|
||||||
|
```
|
||||||
|
0: nose 1: left_eye 2: right_eye
|
||||||
|
3: left_ear 4: right_ear 5: left_shoulder
|
||||||
|
6: right_shoulder 7: left_elbow 8: right_elbow
|
||||||
|
9: left_wrist 10: right_wrist 11: left_hip
|
||||||
|
12: right_hip 13: left_knee 14: right_knee
|
||||||
|
15: left_ankle 16: right_ankle
|
||||||
|
```
|
||||||
|
|
||||||
|
Each keypoint decoded as (x, y, confidence) = 51 values from the 512-dim embedding
|
||||||
|
via a learned linear projection.
|
||||||
|
|
||||||
|
### Operating Modes
|
||||||
|
|
||||||
|
The demo supports three modes, selectable in the UI:
|
||||||
|
|
||||||
|
| Mode | Video | CSI | Fusion | Use Case |
|
||||||
|
|------|-------|-----|--------|----------|
|
||||||
|
| **Dual (default)** | ✅ | ✅ | Attention-weighted | Best accuracy, full demo |
|
||||||
|
| **Video Only** | ✅ | ❌ | α = 1.0 | No ESP32 available, quick demo |
|
||||||
|
| **CSI Only** | ❌ | ✅ | α = 0.0 | Privacy mode, through-wall sensing |
|
||||||
|
|
||||||
|
**Video Only mode works without any hardware** — just a webcam — making the demo
|
||||||
|
instantly accessible for anyone wanting to try it.
|
||||||
|
|
||||||
|
### File Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
examples/wasm-browser-pose/
|
||||||
|
├── index.html # Single-page app (vanilla JS, no bundler)
|
||||||
|
├── js/
|
||||||
|
│ ├── app.js # Main entry, mode selection, orchestration
|
||||||
|
│ ├── video-capture.js # getUserMedia, frame extraction, quality detection
|
||||||
|
│ ├── csi-processor.js # WebSocket CSI client, frame parsing, pseudo-image encoding
|
||||||
|
│ ├── fusion.js # Attention-weighted embedding fusion, confidence gating
|
||||||
|
│ ├── pose-decoder.js # Fused embedding → 17 keypoints
|
||||||
|
│ └── canvas-renderer.js # Video overlay, skeleton, CSI heatmap, confidence bars
|
||||||
|
├── data/
|
||||||
|
│ ├── visual-weights.json # Visual CNN → embedding projection (placeholder until trained)
|
||||||
|
│ ├── csi-weights.json # CSI CNN → embedding projection (placeholder until trained)
|
||||||
|
│ ├── fusion-weights.json # Attention fusion α weights (512 values)
|
||||||
|
│ └── pose-weights.json # Fused embedding → keypoint projection
|
||||||
|
├── css/
|
||||||
|
│ └── style.css # Dark theme UI styling
|
||||||
|
├── pkg/ # Built WASM packages (gitignored, built by script)
|
||||||
|
│ ├── wifi_densepose_wasm/
|
||||||
|
│ └── ruvector_cnn_wasm/
|
||||||
|
├── build.sh # wasm-pack build script for both packages
|
||||||
|
└── README.md # Setup and usage instructions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Pipeline
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# build.sh — builds both WASM packages into pkg/
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Build wifi-densepose-wasm (CSI processing)
|
||||||
|
wasm-pack build ../../rust-port/wifi-densepose-rs/crates/wifi-densepose-wasm \
|
||||||
|
--target web --out-dir "$(pwd)/pkg/wifi_densepose_wasm" --no-typescript
|
||||||
|
|
||||||
|
# Build ruvector-cnn-wasm (CNN inference for both video and CSI)
|
||||||
|
wasm-pack build ../../vendor/ruvector/crates/ruvector-cnn-wasm \
|
||||||
|
--target web --out-dir "$(pwd)/pkg/ruvector_cnn_wasm" --no-typescript
|
||||||
|
|
||||||
|
echo "Build complete. Serve with: python3 -m http.server 8080"
|
||||||
|
```
|
||||||
|
|
||||||
|
### UI Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ WiFi-DensePose — Live Dual-Modal Pose Estimation │
|
||||||
|
│ [Dual Mode ▼] [⚙ Settings] FPS: 28 ◉ Live │
|
||||||
|
├───────────────────────────┬─────────────────────────────┤
|
||||||
|
│ │ │
|
||||||
|
│ ┌───────────────────┐ │ ┌───────────────────┐ │
|
||||||
|
│ │ │ │ │ │ │
|
||||||
|
│ │ Video + Skeleton │ │ │ CSI Heatmap │ │
|
||||||
|
│ │ Overlay │ │ │ (amplitude × │ │
|
||||||
|
│ │ (main canvas) │ │ │ subcarrier) │ │
|
||||||
|
│ │ │ │ │ │ │
|
||||||
|
│ └───────────────────┘ │ └───────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
├───────────────────────────┴─────────────────────────────┤
|
||||||
|
│ Fusion Confidence: ████████░░ 78% │
|
||||||
|
│ Video: ██████████ 95% │ CSI: ██████░░░░ 61% │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ ┌─────────────────────────────────────────────────┐ │
|
||||||
|
│ │ Embedding Space (2D projection) │ │
|
||||||
|
│ │ · · · │ │
|
||||||
|
│ │ · · · · · · (color = pose cluster) │ │
|
||||||
|
│ │ · · · · │ │
|
||||||
|
│ └─────────────────────────────────────────────────┘ │
|
||||||
|
├─────────────────────────────────────────────────────────┤
|
||||||
|
│ Latency: Video 12ms │ CSI 8ms │ Fusion 1ms │ Total 21ms│
|
||||||
|
│ [▶ Record] [📷 Snapshot] [Confidence: ████ 0.6] │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### WASM Module Structure
|
||||||
|
|
||||||
|
| Package | Source Crate | Provides | Size (est.) |
|
||||||
|
|---------|-------------|----------|-------------|
|
||||||
|
| `wifi_densepose_wasm` | `wifi-densepose-wasm` | CSI frame parsing, signal processing, feature extraction | ~200KB |
|
||||||
|
| `ruvector_cnn_wasm` | `ruvector-cnn-wasm` | `WasmCnnEmbedder` (×2 instances), `SimdOps`, `LayerOps`, contrastive losses | ~150KB |
|
||||||
|
|
||||||
|
Two `WasmCnnEmbedder` instances are created — one for video frames, one for CSI pseudo-images.
|
||||||
|
They share the same WASM module but have independent state.
|
||||||
|
|
||||||
|
### Browser API Requirements
|
||||||
|
|
||||||
|
| API | Purpose | Required | Fallback |
|
||||||
|
|-----|---------|----------|----------|
|
||||||
|
| `getUserMedia` | Webcam capture | For video mode | CSI-only mode |
|
||||||
|
| WebAssembly | CNN inference | Yes | None (hard requirement) |
|
||||||
|
| WASM SIMD128 | Accelerated inference | No | Scalar fallback (~2× slower) |
|
||||||
|
| WebSocket | CSI data stream | For CSI mode | Video-only mode |
|
||||||
|
| Canvas 2D | Rendering | Yes | None |
|
||||||
|
| `requestAnimationFrame` | Render loop | Yes | `setTimeout` fallback |
|
||||||
|
| ES Modules | Code organization | Yes | None |
|
||||||
|
|
||||||
|
Target: Chrome 89+, Firefox 89+, Safari 15+, Edge 89+
|
||||||
|
|
||||||
|
### Performance Budget
|
||||||
|
|
||||||
|
| Stage | Target Latency | Notes |
|
||||||
|
|-------|---------------|-------|
|
||||||
|
| Video frame capture + resize | <3ms | `drawImage` to offscreen canvas |
|
||||||
|
| Video CNN embedding | <15ms | 224×224 RGB → 512-dim |
|
||||||
|
| CSI receive + parse | <2ms | Binary WebSocket message |
|
||||||
|
| CSI pseudo-image encoding | <3ms | Amplitude/phase/delta channels |
|
||||||
|
| CSI CNN embedding | <15ms | 224×224 pseudo-RGB → 512-dim |
|
||||||
|
| Attention fusion | <1ms | Element-wise weighted sum |
|
||||||
|
| Pose decoding | <1ms | Linear projection |
|
||||||
|
| Canvas overlay render | <3ms | Video + skeleton + heatmap |
|
||||||
|
| **Total (dual mode)** | **<33ms** | **30 FPS capable** |
|
||||||
|
| **Total (video only)** | **<22ms** | **45 FPS capable** |
|
||||||
|
|
||||||
|
Note: Video and CSI CNN pipelines can run in parallel using Web Workers,
|
||||||
|
reducing dual-mode latency to ~max(15, 15) + 5 = ~20ms (50 FPS).
|
||||||
|
|
||||||
|
### Contrastive Learning Integration
|
||||||
|
|
||||||
|
The demo optionally shows real-time contrastive learning in the browser:
|
||||||
|
|
||||||
|
- **InfoNCE loss** (`WasmInfoNCELoss`): Compare video vs CSI embeddings for the same pose — trains cross-modal alignment
|
||||||
|
- **Triplet loss** (`WasmTripletLoss`): Push apart different poses, pull together same pose across modalities
|
||||||
|
- **SimdOps**: Accelerated dot products for real-time similarity computation
|
||||||
|
- **Embedding space panel**: Live 2D projection shows video and CSI embeddings converging when viewing the same person
|
||||||
|
|
||||||
|
### Relationship to Existing Crates
|
||||||
|
|
||||||
|
| Existing Crate | Role in This Demo |
|
||||||
|
|---------------|-------------------|
|
||||||
|
| `ruvector-cnn-wasm` | CNN inference for **both** video frames and CSI pseudo-images |
|
||||||
|
| `wifi-densepose-wasm` | CSI frame parsing and signal processing |
|
||||||
|
| `wifi-densepose-sensing-server` | WebSocket CSI data source |
|
||||||
|
| `wifi-densepose-core` | ADR-018 frame format definitions |
|
||||||
|
| `ruvector-cnn` | Underlying MobileNet-V3, layers, contrastive learning |
|
||||||
|
|
||||||
|
No new Rust crates are needed. The example is pure HTML/JS consuming existing WASM packages.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
|
||||||
|
- **Instant demo**: Video-only mode works with just a webcam — no ESP32 needed
|
||||||
|
- **Multi-modal showcase**: Demonstrates camera + WiFi fusion, the core innovation of the project
|
||||||
|
- **Graceful degradation**: Works with video-only, CSI-only, or both
|
||||||
|
- **Through-wall capability**: CSI mode shows pose estimation where cameras cannot reach
|
||||||
|
- **Zero-install**: Anyone with a browser can try it
|
||||||
|
- **Training data collection**: Can record paired (video, CSI) data for offline model training
|
||||||
|
- **Reusable**: JS modules embed directly in the Tauri desktop app's webview
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
|
||||||
|
- **Model weights**: Requires offline-trained weights for visual CNN, CSI CNN, fusion, and pose decoder (~200KB total JSON)
|
||||||
|
- **WASM size**: Two WASM modules total ~350KB (acceptable)
|
||||||
|
- **No GPU**: CPU-only WASM inference; adequate at 224×224 but limits resolution scaling
|
||||||
|
- **Camera privacy**: Video mode requires camera permission (mitigated: CSI-only mode available)
|
||||||
|
- **Two CNN instances**: Memory footprint doubles vs single-modal (~10MB total, acceptable for desktop browsers)
|
||||||
|
|
||||||
|
### Risks
|
||||||
|
|
||||||
|
- **Cross-modal alignment**: Video and CSI embeddings must be trained jointly for fusion to work;
|
||||||
|
without proper training, fusion may be worse than either modality alone
|
||||||
|
- **Latency on mobile**: Dual CNN on mobile browsers may exceed 33ms; implement automatic quality reduction
|
||||||
|
- **WebSocket drops**: Network jitter → CSI frame gaps; buffer last 3 frames, interpolate missing data
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
1. **Phase 1 — Scaffold**: File layout, build.sh, index.html shell, mode selector UI
|
||||||
|
2. **Phase 2 — Video pipeline**: getUserMedia → frame capture → CNN embedding → basic pose display
|
||||||
|
3. **Phase 3 — CSI pipeline**: WebSocket client → CSI parsing → pseudo-image → CNN embedding
|
||||||
|
4. **Phase 4 — Fusion**: Attention-weighted combination, confidence gating, mode switching
|
||||||
|
5. **Phase 5 — Pose decoder**: Linear projection with placeholder weights → 17 keypoints
|
||||||
|
6. **Phase 6 — Overlay renderer**: Video canvas with skeleton overlay, CSI heatmap panel
|
||||||
|
7. **Phase 7 — Training**: Use `wifi-densepose-train` to generate real weights for both CNNs + fusion + decoder
|
||||||
|
8. **Phase 8 — Contrastive demo**: Embedding space visualization, cross-modal similarity display
|
||||||
|
9. **Phase 9 — Web Workers**: Move CNN inference to workers for parallel video + CSI processing
|
||||||
|
10. **Phase 10 — Polish**: Recording, snapshots, adaptive quality, mobile optimization
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### 1. CSI-Only (No Video)
|
||||||
|
Rejected: Misses the opportunity to show multi-modal fusion and makes the demo less
|
||||||
|
accessible (requires ESP32 hardware). Video-only mode as a fallback is strictly better.
|
||||||
|
|
||||||
|
### 2. Server-Side Video Inference
|
||||||
|
Rejected: Adds latency, requires webcam stream upload (privacy concern), and defeats
|
||||||
|
the WASM-first architecture. All inference must be client-side.
|
||||||
|
|
||||||
|
### 3. TensorFlow.js for Video, ruvector-cnn-wasm for CSI
|
||||||
|
Rejected: Would require two different ML frameworks. Using `ruvector-cnn-wasm` for both
|
||||||
|
keeps a single WASM module, unified embedding space, and simpler fusion.
|
||||||
|
|
||||||
|
### 4. Pre-recorded Video Demo
|
||||||
|
Rejected: Live webcam input is far more compelling for demonstrations.
|
||||||
|
Pre-recorded mode can be added as a secondary option.
|
||||||
|
|
||||||
|
### 5. React/Vue Framework
|
||||||
|
Rejected: Adds build tooling. Vanilla JS + ES modules keeps the demo self-contained.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [ADR-018: Binary CSI Frame Format](ADR-018-binary-csi-frame-format.md)
|
||||||
|
- [ADR-024: Contrastive CSI Embedding / AETHER](ADR-024-contrastive-csi-embedding.md)
|
||||||
|
- [ADR-055: Integrated Sensing Server](ADR-055-integrated-sensing-server.md)
|
||||||
|
- `vendor/ruvector/crates/ruvector-cnn/src/lib.rs` — CNN embedder implementation
|
||||||
|
- `vendor/ruvector/crates/ruvector-cnn-wasm/src/lib.rs` — WASM bindings
|
||||||
|
- `vendor/ruvector/examples/wasm-vanilla/index.html` — Reference vanilla JS WASM pattern
|
||||||
|
- Person-in-WiFi: Fine-grained Person Perception using WiFi (ICCV 2019) — camera+WiFi fusion precedent
|
||||||
|
- WiPose: Multi-Person WiFi Pose Estimation (TMC 2022) — cross-modal embedding approach
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# ADR-059: Live ESP32 CSI Pipeline Integration
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Date
|
||||||
|
|
||||||
|
2026-03-12
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
ADR-058 established a dual-modal browser demo combining webcam video and WiFi CSI for pose estimation. However, it used simulated CSI data. To demonstrate real-world capability, we need an end-to-end pipeline from physical ESP32 hardware through to the browser visualization.
|
||||||
|
|
||||||
|
The ESP32-S3 firmware (`firmware/esp32-csi-node/`) already supports CSI collection and UDP streaming (ADR-018). The sensing server (`wifi-densepose-sensing-server`) already supports UDP ingestion and WebSocket bridging. The missing piece was connecting these components and enabling the browser demo to consume live data.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Implement a complete live CSI pipeline:
|
||||||
|
|
||||||
|
```
|
||||||
|
ESP32-S3 (CSI capture) → UDP:5005 → sensing-server (Rust/Axum) → WS:8765 → browser demo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
1. **ESP32 Firmware** — Rebuilt with native Windows ESP-IDF v5.4.0 toolchain (no Docker). Configured for target network and PC IP via `sdkconfig`. Helper scripts added:
|
||||||
|
- `build_firmware.ps1` — Sets up IDF environment, cleans, builds, and flashes
|
||||||
|
- `read_serial.ps1` — Serial monitor with DTR/RTS reset capability
|
||||||
|
|
||||||
|
2. **Sensing Server** — `wifi-densepose-sensing-server` started with:
|
||||||
|
- `--source esp32` — Expect real ESP32 UDP frames
|
||||||
|
- `--bind-addr 0.0.0.0` — Accept connections from any interface
|
||||||
|
- `--ui-path <path>` — Serve the demo UI via HTTP
|
||||||
|
|
||||||
|
3. **Browser Demo** — `main.js` updated to auto-connect to `ws://localhost:8765/ws/sensing` on page load. Falls back to simulated CSI if the WebSocket is unavailable (GitHub Pages).
|
||||||
|
|
||||||
|
### Network Configuration
|
||||||
|
|
||||||
|
The ESP32 sends UDP packets to a configured target IP. If the PC's IP doesn't match the firmware's compiled target, a secondary IP alias can be added:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# PowerShell (Admin)
|
||||||
|
New-NetIPAddress -IPAddress 192.168.1.100 -PrefixLength 24 -InterfaceAlias "Wi-Fi"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
| Stage | Protocol | Format | Rate |
|
||||||
|
|-------|----------|--------|------|
|
||||||
|
| ESP32 → Server | UDP | ADR-018 binary frame (magic `0xC5110001`, I/Q pairs) | ~100 Hz |
|
||||||
|
| Server → Browser | WebSocket | ADR-018 binary frame (forwarded) | ~10 Hz (tick-ms=100) |
|
||||||
|
| Browser decode | JavaScript | Float32 amplitude/phase arrays | Per frame |
|
||||||
|
|
||||||
|
### Build Environment (Windows)
|
||||||
|
|
||||||
|
ESP-IDF v5.4.0 on Windows requires:
|
||||||
|
- IDF_PATH pointing to the ESP-IDF framework
|
||||||
|
- IDF_TOOLS_PATH pointing to toolchain binaries
|
||||||
|
- MSYS/MinGW environment variables removed (ESP-IDF rejects them)
|
||||||
|
- Python venv from ESP-IDF tools for `idf.py` execution
|
||||||
|
|
||||||
|
The `build_firmware.ps1` script handles all of this automatically.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- First end-to-end demonstration of real WiFi CSI → pose estimation in a browser
|
||||||
|
- No Docker required for firmware builds on Windows
|
||||||
|
- Demo gracefully degrades to simulated CSI when no server is available
|
||||||
|
- Same demo works on GitHub Pages (simulated) and locally (live ESP32)
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
- ESP32 target IP is compiled into firmware; changing it requires a rebuild or NVS override
|
||||||
|
- Windows firewall may block UDP:5005; user must allow it
|
||||||
|
- Mixed content restrictions prevent HTTPS pages from connecting to ws:// (local only)
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [ADR-018](ADR-018-esp32-dev-implementation.md) — ESP32 CSI frame format and UDP streaming
|
||||||
|
- [ADR-058](ADR-058-ruvector-wasm-browser-pose-example.md) — Dual-modal WASM browser pose demo
|
||||||
|
- [ADR-039](ADR-039-edge-intelligence-framework.md) — Edge intelligence on ESP32
|
||||||
|
- Issue [#245](https://github.com/ruvnet/RuView/issues/245) — Tracking issue
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# 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
@@ -0,0 +1,199 @@
|
|||||||
|
# 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)
|
||||||
+377
-14
@@ -38,8 +38,17 @@ WiFi DensePose turns commodity WiFi signals into real-time human pose estimation
|
|||||||
- [ESP32-S3 Mesh](#esp32-s3-mesh)
|
- [ESP32-S3 Mesh](#esp32-s3-mesh)
|
||||||
- [Intel 5300 / Atheros NIC](#intel-5300--atheros-nic)
|
- [Intel 5300 / Atheros NIC](#intel-5300--atheros-nic)
|
||||||
15. [Docker Compose (Multi-Service)](#docker-compose-multi-service)
|
15. [Docker Compose (Multi-Service)](#docker-compose-multi-service)
|
||||||
16. [Troubleshooting](#troubleshooting)
|
16. [Testing Firmware Without Hardware (QEMU)](#testing-firmware-without-hardware-qemu)
|
||||||
17. [FAQ](#faq)
|
- [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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -78,6 +87,17 @@ docker pull ruvnet/wifi-densepose:latest
|
|||||||
|
|
||||||
Multi-architecture image (amd64 + arm64). Works on Intel/AMD and Apple Silicon Macs. Contains the Rust sensing server, Three.js UI, and all signal processing.
|
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)
|
### From Source (Rust)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -267,8 +287,8 @@ Real Channel State Information at 20 Hz with 56-192 subcarriers. Required for po
|
|||||||
# From source
|
# From source
|
||||||
./target/release/sensing-server --source esp32 --udp-port 5005 --http-port 3000 --ws-port 3001
|
./target/release/sensing-server --source esp32 --udp-port 5005 --http-port 3000 --ws-port 3001
|
||||||
|
|
||||||
# Docker
|
# Docker (use CSI_SOURCE environment variable)
|
||||||
docker run -p 3000:3000 -p 3001:3001 -p 5005:5005/udp ruvnet/wifi-densepose:latest --source esp32
|
docker run -p 3000:3000 -p 3001:3001 -p 5005:5005/udp -e CSI_SOURCE=esp32 ruvnet/wifi-densepose:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
The ESP32 nodes stream binary CSI frames over UDP to port 5005. See [Hardware Setup](#esp32-s3-mesh) for flashing instructions.
|
The ESP32 nodes stream binary CSI frames over UDP to port 5005. See [Hardware Setup](#esp32-s3-mesh) for flashing instructions.
|
||||||
@@ -679,9 +699,11 @@ Download the dataset files and place them in a `data/` directory.
|
|||||||
./target/release/sensing-server --train --dataset data/ --dataset-type mmfi --epochs 100 --save-rvf model.rvf
|
./target/release/sensing-server --train --dataset data/ --dataset-type mmfi --epochs 100 --save-rvf model.rvf
|
||||||
|
|
||||||
# Via Docker (mount your data directory)
|
# Via Docker (mount your data directory)
|
||||||
|
# Note: Training mode requires overriding the default entrypoint
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v $(pwd)/data:/data \
|
-v $(pwd)/data:/data \
|
||||||
-v $(pwd)/output:/output \
|
-v $(pwd)/output:/output \
|
||||||
|
--entrypoint /app/sensing-server \
|
||||||
ruvnet/wifi-densepose:latest \
|
ruvnet/wifi-densepose:latest \
|
||||||
--train --dataset /data --epochs 100 --export-rvf /output/model.rvf
|
--train --dataset /data --epochs 100 --export-rvf /output/model.rvf
|
||||||
```
|
```
|
||||||
@@ -797,14 +819,27 @@ Pre-built binaries are available at [Releases](https://github.com/ruvnet/RuView/
|
|||||||
|
|
||||||
| Release | What It Includes | Tag |
|
| Release | What It Includes | Tag |
|
||||||
|---------|-----------------|-----|
|
|---------|-----------------|-----|
|
||||||
| [v0.2.0](https://github.com/ruvnet/RuView/releases/tag/v0.2.0-esp32) | Stable — raw CSI streaming, TDM, channel hopping, QUIC mesh | `v0.2.0-esp32` |
|
| [v0.4.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.1-esp32) | **Stable** — CSI build fix, compile guard, AMOLED display, edge intelligence ([ADR-057](../docs/adr/ADR-057-firmware-csi-build-guard.md)) | `v0.4.1-esp32` |
|
||||||
| [v0.3.0-alpha](https://github.com/ruvnet/RuView/releases/tag/v0.3.0-alpha-esp32) | Alpha — adds on-device edge intelligence (ADR-039) | `v0.3.0-alpha-esp32` |
|
| [v0.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
|
```bash
|
||||||
# Flash an ESP32-S3 (requires esptool: pip install esptool)
|
# Flash an ESP32-S3 with 8MB flash (most boards)
|
||||||
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
|
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
|
||||||
write-flash --flash-mode dio --flash-size 4MB \
|
write-flash --flash-mode dio --flash-size 8MB --flash-freq 80m \
|
||||||
0x0 bootloader.bin 0x8000 partition-table.bin 0x10000 esp32-csi-node.bin
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
**Provisioning:**
|
**Provisioning:**
|
||||||
@@ -885,8 +920,8 @@ Binary size: 777 KB (24% free in the 1 MB app partition).
|
|||||||
# From source
|
# From source
|
||||||
./target/release/sensing-server --source esp32 --udp-port 5005 --http-port 3000 --ws-port 3001
|
./target/release/sensing-server --source esp32 --udp-port 5005 --http-port 3000 --ws-port 3001
|
||||||
|
|
||||||
# Docker
|
# Docker (use CSI_SOURCE environment variable)
|
||||||
docker run -p 3000:3000 -p 3001:3001 -p 5005:5005/udp ruvnet/wifi-densepose:latest --source esp32
|
docker run -p 3000:3000 -p 3001:3001 -p 5005:5005/udp -e CSI_SOURCE=esp32 ruvnet/wifi-densepose:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
See [ADR-018](../docs/adr/ADR-018-esp32-dev-implementation.md), [ADR-029](../docs/adr/ADR-029-ruvsense-multistatic-sensing-mode.md), and [Tutorial #34](https://github.com/ruvnet/RuView/issues/34).
|
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).
|
||||||
@@ -919,6 +954,288 @@ 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
|
## Troubleshooting
|
||||||
|
|
||||||
### Docker: "no matching manifest for linux/arm64" on macOS
|
### Docker: "no matching manifest for linux/arm64" on macOS
|
||||||
@@ -953,12 +1270,17 @@ Add the WebSocket port mapping:
|
|||||||
docker run -p 3000:3000 -p 3001:3001 ruvnet/wifi-densepose:latest
|
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
|
### ESP32: No data arriving
|
||||||
|
|
||||||
1. Verify the ESP32 is connected to the same WiFi network
|
1. Verify firmware is v0.4.1+ (older versions had CSI disabled — see above)
|
||||||
2. Check the target IP matches the sensing server machine: `python firmware/esp32-csi-node/provision.py --port COM7 --target-ip <YOUR_IP>`
|
2. Verify the ESP32 is connected to the same WiFi network
|
||||||
3. Verify UDP port 5005 is not blocked by firewall
|
3. Check the target IP matches the sensing server machine: `python firmware/esp32-csi-node/provision.py --port COM7 --target-ip <YOUR_IP>`
|
||||||
4. Test with: `nc -lu 5005` (Linux) or similar UDP listener
|
4. Verify UDP port 5005 is not blocked by firewall
|
||||||
|
5. Test with: `nc -lu 5005` (Linux) or similar UDP listener
|
||||||
|
|
||||||
### Build: Rust compilation errors
|
### Build: Rust compilation errors
|
||||||
|
|
||||||
@@ -993,6 +1315,47 @@ The server applies a 3-stage smoothing pipeline (ADR-048). If readings are still
|
|||||||
- Hard refresh with Ctrl+Shift+R to clear cached settings
|
- Hard refresh with Ctrl+Shift+R to clear cached settings
|
||||||
- The auto-detect probes `/health` on the same origin — cross-origin won't work
|
- 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
|
## FAQ
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{"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}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -523,6 +523,231 @@ 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
|
## Troubleshooting
|
||||||
|
|
||||||
| Symptom | Cause | Fix |
|
| Symptom | Cause | Fix |
|
||||||
@@ -556,6 +781,9 @@ 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-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-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-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 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Remove MSYS environment variables that trigger ESP-IDF's MinGW rejection
|
||||||
|
Remove-Item env:MSYSTEM -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item env:MSYSTEM_CARCH -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item env:MSYSTEM_CHOST -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item env:MSYSTEM_PREFIX -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item env:MINGW_CHOST -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item env:MINGW_PACKAGE_PREFIX -ErrorAction SilentlyContinue
|
||||||
|
Remove-Item env:MINGW_PREFIX -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
$env:IDF_PATH = "C:\Users\ruv\esp\v5.4\esp-idf"
|
||||||
|
$env:IDF_TOOLS_PATH = "C:\Espressif\tools"
|
||||||
|
$env:IDF_PYTHON_ENV_PATH = "C:\Espressif\tools\python\v5.4\venv"
|
||||||
|
$env:PATH = "C:\Espressif\tools\xtensa-esp-elf\esp-14.2.0_20241119\xtensa-esp-elf\bin;C:\Espressif\tools\cmake\3.30.2\cmake-3.30.2-windows-x86_64\bin;C:\Espressif\tools\ninja\1.12.1;C:\Espressif\tools\ccache\4.10.2\ccache-4.10.2-windows-x86_64;C:\Espressif\tools\idf-exe\1.0.3;C:\Espressif\tools\python\v5.4\venv\Scripts;$env:PATH"
|
||||||
|
|
||||||
|
Set-Location "C:\Users\ruv\Projects\wifi-densepose\firmware\esp32-csi-node"
|
||||||
|
|
||||||
|
$python = "$env:IDF_PYTHON_ENV_PATH\Scripts\python.exe"
|
||||||
|
$idf = "$env:IDF_PATH\tools\idf.py"
|
||||||
|
|
||||||
|
Write-Host "=== Cleaning stale build cache ==="
|
||||||
|
& $python $idf fullclean
|
||||||
|
|
||||||
|
Write-Host "=== Building firmware (SSID=ruv.net, target=192.168.1.20:5005) ==="
|
||||||
|
& $python $idf build
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host "=== Build succeeded! Flashing to COM7 ==="
|
||||||
|
& $python $idf -p COM7 flash
|
||||||
|
} else {
|
||||||
|
Write-Host "=== Build failed with exit code $LASTEXITCODE ==="
|
||||||
|
}
|
||||||
@@ -6,6 +6,11 @@ set(SRCS
|
|||||||
|
|
||||||
set(REQUIRES "")
|
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)
|
# ADR-045: AMOLED display support (compile-time optional)
|
||||||
if(CONFIG_DISPLAY_ENABLE)
|
if(CONFIG_DISPLAY_ENABLE)
|
||||||
list(APPEND SRCS "display_hal.c" "display_ui.c" "display_task.c")
|
list(APPEND SRCS "display_hal.c" "display_ui.c" "display_task.c")
|
||||||
|
|||||||
@@ -68,10 +68,13 @@ menu "Edge Intelligence (ADR-039)"
|
|||||||
|
|
||||||
config EDGE_FALL_THRESH
|
config EDGE_FALL_THRESH
|
||||||
int "Fall detection threshold (x1000)"
|
int "Fall detection threshold (x1000)"
|
||||||
default 2000
|
default 15000
|
||||||
range 100 50000
|
range 100 50000
|
||||||
help
|
help
|
||||||
Phase acceleration threshold for fall detection.
|
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.
|
Stored as integer; divided by 1000 at runtime.
|
||||||
Default 2000 = 2.0 rad/s^2.
|
Default 2000 = 2.0 rad/s^2.
|
||||||
|
|
||||||
@@ -201,3 +204,40 @@ menu "WASM Programmable Sensing (ADR-040)"
|
|||||||
Default 1000 ms = 1 Hz.
|
Default 1000 ms = 1 Hz.
|
||||||
|
|
||||||
endmenu
|
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
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "csi_collector.h"
|
#include "csi_collector.h"
|
||||||
|
#include "nvs_config.h"
|
||||||
#include "stream_sender.h"
|
#include "stream_sender.h"
|
||||||
#include "edge_processing.h"
|
#include "edge_processing.h"
|
||||||
|
|
||||||
@@ -21,6 +22,19 @@
|
|||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
#include "sdkconfig.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 const char *TAG = "csi_collector";
|
||||||
|
|
||||||
static uint32_t s_sequence = 0;
|
static uint32_t s_sequence = 0;
|
||||||
@@ -141,6 +155,14 @@ 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)
|
static void wifi_csi_callback(void *ctx, wifi_csi_info_t *info)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(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++;
|
s_cb_count++;
|
||||||
|
|
||||||
if (s_cb_count <= 3 || (s_cb_count % 100) == 0) {
|
if (s_cb_count <= 3 || (s_cb_count % 100) == 0) {
|
||||||
@@ -193,6 +215,29 @@ static void wifi_promiscuous_cb(void *buf, wifi_promiscuous_pkt_type_t type)
|
|||||||
|
|
||||||
void csi_collector_init(void)
|
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.
|
/* Enable promiscuous mode — required for reliable CSI callbacks.
|
||||||
* Without this, CSI only fires on frames destined to this station,
|
* Without this, CSI only fires on frames destined to this station,
|
||||||
* which may be very infrequent on a quiet network. */
|
* which may be very infrequent on a quiet network. */
|
||||||
@@ -220,8 +265,15 @@ 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_rx_cb(wifi_csi_callback, NULL));
|
||||||
ESP_ERROR_CHECK(esp_wifi_set_csi(true));
|
ESP_ERROR_CHECK(esp_wifi_set_csi(true));
|
||||||
|
|
||||||
ESP_LOGI(TAG, "CSI collection initialized (node_id=%d, channel=%d)",
|
if (g_nvs_config.filter_mac_set) {
|
||||||
CONFIG_CSI_NODE_ID, CONFIG_CSI_WIFI_CHANNEL);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- ADR-029: Channel hopping ---- */
|
/* ---- ADR-029: Channel hopping ---- */
|
||||||
|
|||||||
@@ -244,6 +244,10 @@ static uint32_t s_frame_count;
|
|||||||
/** Previous phase velocity for fall detection (acceleration). */
|
/** Previous phase velocity for fall detection (acceleration). */
|
||||||
static float s_prev_phase_velocity;
|
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. */
|
/** Adaptive calibration state. */
|
||||||
static bool s_calibrated;
|
static bool s_calibrated;
|
||||||
static float s_calib_sum;
|
static float s_calib_sum;
|
||||||
@@ -689,7 +693,7 @@ static void process_frame(const edge_ring_slot_t *slot)
|
|||||||
}
|
}
|
||||||
s_presence_detected = (s_presence_score > threshold);
|
s_presence_detected = (s_presence_score > threshold);
|
||||||
|
|
||||||
/* --- Step 10: Fall detection (phase acceleration) --- */
|
/* --- Step 10: Fall detection (phase acceleration + debounce, issue #263) --- */
|
||||||
if (s_history_len >= 3) {
|
if (s_history_len >= 3) {
|
||||||
uint16_t i0 = (s_history_idx + EDGE_PHASE_HISTORY_LEN - 1) % EDGE_PHASE_HISTORY_LEN;
|
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;
|
uint16_t i1 = (s_history_idx + EDGE_PHASE_HISTORY_LEN - 2) % EDGE_PHASE_HISTORY_LEN;
|
||||||
@@ -697,10 +701,26 @@ static void process_frame(const edge_ring_slot_t *slot)
|
|||||||
float accel = fabsf(velocity - s_prev_phase_velocity);
|
float accel = fabsf(velocity - s_prev_phase_velocity);
|
||||||
s_prev_phase_velocity = velocity;
|
s_prev_phase_velocity = velocity;
|
||||||
|
|
||||||
s_fall_detected = (accel > s_cfg.fall_thresh);
|
if (accel > s_cfg.fall_thresh) {
|
||||||
if (s_fall_detected) {
|
s_fall_consec_count++;
|
||||||
ESP_LOGW(TAG, "Fall detected! accel=%.4f > thresh=%.4f",
|
} else {
|
||||||
accel, s_cfg.fall_thresh);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -850,6 +870,8 @@ esp_err_t edge_processing_init(const edge_config_t *cfg)
|
|||||||
s_latest_rssi = 0;
|
s_latest_rssi = 0;
|
||||||
s_frame_count = 0;
|
s_frame_count = 0;
|
||||||
s_prev_phase_velocity = 0.0f;
|
s_prev_phase_velocity = 0.0f;
|
||||||
|
s_fall_consec_count = 0;
|
||||||
|
s_fall_last_alert_us = 0;
|
||||||
s_last_vitals_send_us = 0;
|
s_last_vitals_send_us = 0;
|
||||||
s_has_prev_iq = false;
|
s_has_prev_iq = false;
|
||||||
s_prev_iq_len = 0;
|
s_prev_iq_len = 0;
|
||||||
|
|||||||
@@ -42,6 +42,10 @@
|
|||||||
#define EDGE_CALIB_FRAMES 1200 /**< Frames for adaptive calibration (~60s at 20 Hz). */
|
#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. */
|
#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 ---- */
|
/* ---- SPSC ring buffer slot ---- */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t iq_data[EDGE_MAX_IQ_BYTES]; /**< Raw I/Q bytes from CSI callback. */
|
uint8_t iq_data[EDGE_MAX_IQ_BYTES]; /**< Raw I/Q bytes from CSI callback. */
|
||||||
|
|||||||
@@ -27,6 +27,9 @@
|
|||||||
#include "wasm_runtime.h"
|
#include "wasm_runtime.h"
|
||||||
#include "wasm_upload.h"
|
#include "wasm_upload.h"
|
||||||
#include "display_task.h"
|
#include "display_task.h"
|
||||||
|
#ifdef CONFIG_CSI_MOCK_ENABLED
|
||||||
|
#include "mock_csi.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "esp_timer.h"
|
#include "esp_timer.h"
|
||||||
|
|
||||||
@@ -134,17 +137,35 @@ void app_main(void)
|
|||||||
|
|
||||||
ESP_LOGI(TAG, "ESP32-S3 CSI Node (ADR-018) — Node ID: %d", g_nvs_config.node_id);
|
ESP_LOGI(TAG, "ESP32-S3 CSI Node (ADR-018) — Node ID: %d", g_nvs_config.node_id);
|
||||||
|
|
||||||
/* Initialize WiFi STA */
|
/* Initialize WiFi STA (skip entirely under QEMU mock — no RF hardware) */
|
||||||
|
#ifndef CONFIG_CSI_MOCK_SKIP_WIFI_CONNECT
|
||||||
wifi_init_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 */
|
/* 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) {
|
if (stream_sender_init_with(g_nvs_config.target_ip, g_nvs_config.target_port) != 0) {
|
||||||
ESP_LOGE(TAG, "Failed to initialize UDP sender");
|
ESP_LOGE(TAG, "Failed to initialize UDP sender");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
/* Initialize CSI collection */
|
/* 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();
|
csi_collector_init();
|
||||||
|
#endif
|
||||||
|
|
||||||
/* ADR-039: Initialize edge processing pipeline. */
|
/* ADR-039: Initialize edge processing pipeline. */
|
||||||
edge_config_t edge_cfg = {
|
edge_config_t edge_cfg = {
|
||||||
@@ -162,12 +183,17 @@ void app_main(void)
|
|||||||
esp_err_to_name(edge_ret));
|
esp_err_to_name(edge_ret));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Initialize OTA update HTTP server. */
|
/* Initialize OTA update HTTP server (requires network). */
|
||||||
httpd_handle_t ota_server = NULL;
|
httpd_handle_t ota_server = NULL;
|
||||||
|
#ifndef CONFIG_CSI_MOCK_SKIP_WIFI_CONNECT
|
||||||
esp_err_t ota_ret = ota_update_init_ex(&ota_server);
|
esp_err_t ota_ret = ota_update_init_ex(&ota_server);
|
||||||
if (ota_ret != ESP_OK) {
|
if (ota_ret != ESP_OK) {
|
||||||
ESP_LOGW(TAG, "OTA server init failed: %s", esp_err_to_name(ota_ret));
|
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. */
|
/* ADR-040: Initialize WASM programmable sensing runtime. */
|
||||||
esp_err_t wasm_ret = wasm_runtime_init();
|
esp_err_t wasm_ret = wasm_runtime_init();
|
||||||
@@ -205,10 +231,12 @@ void app_main(void)
|
|||||||
power_mgmt_init(g_nvs_config.power_duty);
|
power_mgmt_init(g_nvs_config.power_duty);
|
||||||
|
|
||||||
/* ADR-045: Start AMOLED display task (gracefully skips if no display). */
|
/* ADR-045: Start AMOLED display task (gracefully skips if no display). */
|
||||||
|
#ifdef CONFIG_DISPLAY_ENABLE
|
||||||
esp_err_t disp_ret = display_task_start();
|
esp_err_t disp_ret = display_task_start();
|
||||||
if (disp_ret != ESP_OK) {
|
if (disp_ret != ESP_OK) {
|
||||||
ESP_LOGW(TAG, "Display init returned: %s", esp_err_to_name(disp_ret));
|
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)",
|
ESP_LOGI(TAG, "CSI streaming active → %s:%d (edge_tier=%u, OTA=%s, WASM=%s)",
|
||||||
g_nvs_config.target_ip, g_nvs_config.target_port,
|
g_nvs_config.target_ip, g_nvs_config.target_port,
|
||||||
|
|||||||
@@ -0,0 +1,696 @@
|
|||||||
|
/**
|
||||||
|
* @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 */
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* @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 */
|
||||||
@@ -61,7 +61,7 @@ void nvs_config_load(nvs_config_t *cfg)
|
|||||||
#ifdef CONFIG_EDGE_FALL_THRESH
|
#ifdef CONFIG_EDGE_FALL_THRESH
|
||||||
cfg->fall_thresh = (float)CONFIG_EDGE_FALL_THRESH / 1000.0f;
|
cfg->fall_thresh = (float)CONFIG_EDGE_FALL_THRESH / 1000.0f;
|
||||||
#else
|
#else
|
||||||
cfg->fall_thresh = 2.0f;
|
cfg->fall_thresh = 15.0f; /* Default raised from 2.0 — see issue #263. */
|
||||||
#endif
|
#endif
|
||||||
cfg->vital_window = 256;
|
cfg->vital_window = 256;
|
||||||
#ifdef CONFIG_EDGE_VITAL_INTERVAL_MS
|
#ifdef CONFIG_EDGE_VITAL_INTERVAL_MS
|
||||||
@@ -91,6 +91,11 @@ void nvs_config_load(nvs_config_t *cfg)
|
|||||||
cfg->wasm_verify = 0; /* Kconfig disabled signature verification. */
|
cfg->wasm_verify = 0; /* Kconfig disabled signature verification. */
|
||||||
#endif
|
#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 */
|
/* Try to override from NVS */
|
||||||
nvs_handle_t handle;
|
nvs_handle_t handle;
|
||||||
esp_err_t err = nvs_open("csi_cfg", NVS_READONLY, &handle);
|
esp_err_t err = nvs_open("csi_cfg", NVS_READONLY, &handle);
|
||||||
@@ -277,6 +282,26 @@ void nvs_config_load(nvs_config_t *cfg)
|
|||||||
ESP_LOGW(TAG, "wasm_verify=1 but no wasm_pubkey in NVS — uploads will be rejected");
|
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 */
|
/* Validate tdm_slot_index < tdm_node_count */
|
||||||
if (cfg->tdm_slot_index >= cfg->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",
|
ESP_LOGW(TAG, "tdm_slot_index=%u >= tdm_node_count=%u, clamping to 0",
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ typedef struct {
|
|||||||
uint8_t wasm_verify; /**< Require Ed25519 signature for uploads. */
|
uint8_t wasm_verify; /**< Require Ed25519 signature for uploads. */
|
||||||
uint8_t wasm_pubkey[32]; /**< Ed25519 public key for WASM signature. */
|
uint8_t wasm_pubkey[32]; /**< Ed25519 public key for WASM signature. */
|
||||||
uint8_t wasm_pubkey_valid; /**< 1 if pubkey was loaded from NVS. */
|
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;
|
} nvs_config_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# 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.
|
@@ -64,6 +64,13 @@ def build_nvs_csv(args):
|
|||||||
writer.writerow(["vital_int", "data", "u16", str(args.vital_int)])
|
writer.writerow(["vital_int", "data", "u16", str(args.vital_int)])
|
||||||
if args.subk_count is not None:
|
if args.subk_count is not None:
|
||||||
writer.writerow(["subk_count", "data", "u8", str(args.subk_count)])
|
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()
|
return buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
@@ -76,25 +83,20 @@ def generate_nvs_binary(csv_content, size):
|
|||||||
bin_path = csv_path.replace(".csv", ".bin")
|
bin_path = csv_path.replace(".csv", ".bin")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Try the pip-installed version first (esp_idf_nvs_partition_gen package)
|
# Method 1: subprocess invocation (most reliable across package versions)
|
||||||
try:
|
for module_name in ["esp_idf_nvs_partition_gen", "nvs_partition_gen"]:
|
||||||
from esp_idf_nvs_partition_gen import nvs_partition_gen
|
try:
|
||||||
nvs_partition_gen.generate(csv_path, bin_path, size)
|
subprocess.check_call(
|
||||||
with open(bin_path, "rb") as f:
|
[sys.executable, "-m", module_name, "generate",
|
||||||
return f.read()
|
csv_path, bin_path, hex(size)],
|
||||||
except ImportError:
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||||
pass
|
)
|
||||||
|
with open(bin_path, "rb") as f:
|
||||||
|
return f.read()
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
|
continue
|
||||||
|
|
||||||
# Try legacy import name (older versions)
|
# Method 2: ESP-IDF bundled script
|
||||||
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", "")
|
idf_path = os.environ.get("IDF_PATH", "")
|
||||||
gen_script = os.path.join(idf_path, "components", "nvs_flash",
|
gen_script = os.path.join(idf_path, "components", "nvs_flash",
|
||||||
"nvs_partition_generator", "nvs_partition_gen.py")
|
"nvs_partition_generator", "nvs_partition_gen.py")
|
||||||
@@ -106,13 +108,10 @@ def generate_nvs_binary(csv_content, size):
|
|||||||
with open(bin_path, "rb") as f:
|
with open(bin_path, "rb") as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
# Last resort: try as a module
|
raise RuntimeError(
|
||||||
subprocess.check_call([
|
"NVS partition generator not available. "
|
||||||
sys.executable, "-m", "nvs_partition_gen", "generate",
|
"Install: pip install esp-idf-nvs-partition-gen"
|
||||||
csv_path, bin_path, hex(size)
|
)
|
||||||
])
|
|
||||||
with open(bin_path, "rb") as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
for p in (csv_path, bin_path):
|
for p in (csv_path, bin_path):
|
||||||
@@ -161,10 +160,16 @@ def main():
|
|||||||
parser.add_argument("--edge-tier", type=int, choices=[0, 1, 2],
|
parser.add_argument("--edge-tier", type=int, choices=[0, 1, 2],
|
||||||
help="Edge processing tier: 0=off, 1=stats, 2=vitals")
|
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("--pres-thresh", type=int, help="Presence detection threshold (default: 50)")
|
||||||
parser.add_argument("--fall-thresh", type=int, help="Fall detection threshold (default: 500)")
|
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("--vital-win", type=int, help="Phase history window in frames (default: 300)")
|
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("--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)")
|
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")
|
parser.add_argument("--dry-run", action="store_true", help="Generate NVS binary but don't flash")
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@@ -176,6 +181,7 @@ def main():
|
|||||||
args.edge_tier is not None, args.pres_thresh is not None,
|
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.fall_thresh is not None, args.vital_win is not None,
|
||||||
args.vital_int is not None, args.subk_count 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:
|
if not has_value:
|
||||||
parser.error("At least one config value must be specified")
|
parser.error("At least one config value must be specified")
|
||||||
@@ -186,6 +192,22 @@ def main():
|
|||||||
if args.tdm_slot is not None and args.tdm_slot >= args.tdm_total:
|
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})")
|
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:")
|
print("Building NVS configuration:")
|
||||||
if args.ssid:
|
if args.ssid:
|
||||||
print(f" WiFi SSID: {args.ssid}")
|
print(f" WiFi SSID: {args.ssid}")
|
||||||
@@ -212,6 +234,10 @@ def main():
|
|||||||
print(f" Vital Interval:{args.vital_int} ms")
|
print(f" Vital Interval:{args.vital_int} ms")
|
||||||
if args.subk_count is not None:
|
if args.subk_count is not None:
|
||||||
print(f" Top-K Subcarr: {args.subk_count}")
|
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)
|
csv_content = build_nvs_csv(args)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
$p = New-Object System.IO.Ports.SerialPort('COM7', 115200)
|
||||||
|
$p.ReadTimeout = 5000
|
||||||
|
$p.Open()
|
||||||
|
Start-Sleep -Milliseconds 200
|
||||||
|
|
||||||
|
for ($i = 0; $i -lt 60; $i++) {
|
||||||
|
try {
|
||||||
|
$line = $p.ReadLine()
|
||||||
|
Write-Host $line
|
||||||
|
} catch {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$p.Close()
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# ESP32-S3 CSI Node — Default SDK Configuration
|
||||||
|
# This file is applied automatically by idf.py when no sdkconfig exists.
|
||||||
|
|
||||||
|
# Target: ESP32-S3
|
||||||
|
CONFIG_IDF_TARGET="esp32s3"
|
||||||
|
|
||||||
|
# Use custom partition table (8MB flash with OTA — ADR-045)
|
||||||
|
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||||
|
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_display.csv"
|
||||||
|
|
||||||
|
# Flash configuration: 8MB (Quad SPI)
|
||||||
|
CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
|
||||||
|
CONFIG_ESPTOOLPY_FLASHSIZE="8MB"
|
||||||
|
|
||||||
|
# Compiler optimization: optimize for size to reduce binary
|
||||||
|
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
|
||||||
|
|
||||||
|
# Enable CSI (Channel State Information) in WiFi driver
|
||||||
|
CONFIG_ESP_WIFI_CSI_ENABLED=y
|
||||||
|
|
||||||
|
# NVS encryption disabled by default (requires eFuse provisioning).
|
||||||
|
# Enable only after burning HMAC key to eFuse block.
|
||||||
|
# CONFIG_NVS_ENCRYPTION is not set
|
||||||
|
|
||||||
|
# Disable unused features to reduce binary size
|
||||||
|
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
|
||||||
|
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
|
||||||
|
|
||||||
|
# LWIP: enable extended socket options for UDP multicast
|
||||||
|
CONFIG_LWIP_SO_RCVBUF=y
|
||||||
|
|
||||||
|
# FreeRTOS: increase task stack for CSI processing
|
||||||
|
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# 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,8 +18,9 @@ CONFIG_COMPILER_OPTIMIZATION_SIZE=y
|
|||||||
# Enable CSI (Channel State Information) in WiFi driver
|
# Enable CSI (Channel State Information) in WiFi driver
|
||||||
CONFIG_ESP_WIFI_CSI_ENABLED=y
|
CONFIG_ESP_WIFI_CSI_ENABLED=y
|
||||||
|
|
||||||
# Enable NVS encryption for secure credential storage
|
# NVS encryption disabled by default (requires eFuse provisioning).
|
||||||
CONFIG_NVS_ENCRYPTION=y
|
# Enable only after burning HMAC key to eFuse block.
|
||||||
|
# CONFIG_NVS_ENCRYPTION is not set
|
||||||
|
|
||||||
# Disable unused features to reduce binary size
|
# Disable unused features to reduce binary size
|
||||||
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
|
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# 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.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,203 @@
|
|||||||
|
/**
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
/**
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
/**
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/* Stub: redirect to unified stubs header. */
|
||||||
|
#ifndef ESP_ERR_H_STUB
|
||||||
|
#define ESP_ERR_H_STUB
|
||||||
|
#include "esp_stubs.h"
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/* Stub: redirect to unified stubs header. */
|
||||||
|
#ifndef ESP_LOG_H_STUB
|
||||||
|
#define ESP_LOG_H_STUB
|
||||||
|
#include "esp_stubs.h"
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* @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; }
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
/**
|
||||||
|
* @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 */
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/* Stub: redirect to unified stubs header. */
|
||||||
|
#ifndef ESP_TIMER_H_STUB
|
||||||
|
#define ESP_TIMER_H_STUB
|
||||||
|
#include "esp_stubs.h"
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/* Stub: redirect to unified stubs header. */
|
||||||
|
#ifndef ESP_WIFI_H_STUB
|
||||||
|
#define ESP_WIFI_H_STUB
|
||||||
|
#include "esp_stubs.h"
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/* Stub: redirect to unified stubs header. */
|
||||||
|
#ifndef ESP_WIFI_TYPES_H_STUB
|
||||||
|
#define ESP_WIFI_TYPES_H_STUB
|
||||||
|
#include "esp_stubs.h"
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/* Stub: redirect to unified stubs header. */
|
||||||
|
#ifndef FREERTOS_H_STUB
|
||||||
|
#define FREERTOS_H_STUB
|
||||||
|
#include "esp_stubs.h"
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/* Stub: redirect to unified stubs header. */
|
||||||
|
#ifndef FREERTOS_TASK_H_STUB
|
||||||
|
#define FREERTOS_TASK_H_STUB
|
||||||
|
#include "esp_stubs.h"
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/* Stub: redirect to unified stubs header. */
|
||||||
|
#ifndef NVS_H_STUB
|
||||||
|
#define NVS_H_STUB
|
||||||
|
#include "esp_stubs.h"
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/* Stub: redirect to unified stubs header. */
|
||||||
|
#ifndef NVS_FLASH_H_STUB
|
||||||
|
#define NVS_FLASH_H_STUB
|
||||||
|
#include "esp_stubs.h"
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/* Stub: sdkconfig.h — all CONFIG_ macros provided by esp_stubs.h. */
|
||||||
|
#ifndef SDKCONFIG_H_STUB
|
||||||
|
#define SDKCONFIG_H_STUB
|
||||||
|
#include "esp_stubs.h"
|
||||||
|
#endif
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{"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}
|
|
||||||
@@ -30,9 +30,6 @@
|
|||||||
"icons/128x128@2x.png",
|
"icons/128x128@2x.png",
|
||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
]
|
||||||
"resources": {
|
|
||||||
"../../target/release/sensing-server": "bin/sensing-server"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
Vendored
-16
@@ -1,16 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-17
@@ -1,17 +0,0 @@
|
|||||||
@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" %*
|
|
||||||
Generated
Vendored
-28
@@ -1,28 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-16
@@ -1,16 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-17
@@ -1,17 +0,0 @@
|
|||||||
@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" %*
|
|
||||||
Generated
Vendored
-28
@@ -1,28 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-16
@@ -1,16 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-17
@@ -1,17 +0,0 @@
|
|||||||
@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" %*
|
|
||||||
Generated
Vendored
-28
@@ -1,28 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-16
@@ -1,16 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-17
@@ -1,17 +0,0 @@
|
|||||||
@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" %*
|
|
||||||
Generated
Vendored
-28
@@ -1,28 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-16
@@ -1,16 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-17
@@ -1,17 +0,0 @@
|
|||||||
@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" %*
|
|
||||||
Generated
Vendored
-28
@@ -1,28 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-16
@@ -1,16 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-17
@@ -1,17 +0,0 @@
|
|||||||
@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" %*
|
|
||||||
Generated
Vendored
-28
@@ -1,28 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-16
@@ -1,16 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-17
@@ -1,17 +0,0 @@
|
|||||||
@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" %*
|
|
||||||
Generated
Vendored
-28
@@ -1,28 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-16
@@ -1,16 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-17
@@ -1,17 +0,0 @@
|
|||||||
@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" %*
|
|
||||||
Generated
Vendored
-28
@@ -1,28 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-16
@@ -1,16 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-17
@@ -1,17 +0,0 @@
|
|||||||
@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" %*
|
|
||||||
Generated
Vendored
-28
@@ -1,28 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-16
@@ -1,16 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-17
@@ -1,17 +0,0 @@
|
|||||||
@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" %*
|
|
||||||
Generated
Vendored
-28
@@ -1,28 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-16
@@ -1,16 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-17
@@ -1,17 +0,0 @@
|
|||||||
@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" %*
|
|
||||||
Generated
Vendored
-28
@@ -1,28 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Generated
Vendored
-16
@@ -1,16 +0,0 @@
|
|||||||
#!/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/tsserver" "$@"
|
|
||||||
else
|
|
||||||
exec node "$basedir/../typescript/bin/tsserver" "$@"
|
|
||||||
fi
|
|
||||||
Generated
Vendored
-17
@@ -1,17 +0,0 @@
|
|||||||
@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\tsserver" %*
|
|
||||||
Generated
Vendored
-28
@@ -1,28 +0,0 @@
|
|||||||
#!/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/tsserver" $args
|
|
||||||
} else {
|
|
||||||
& "$basedir/node$exe" "$basedir/../typescript/bin/tsserver" $args
|
|
||||||
}
|
|
||||||
$ret=$LASTEXITCODE
|
|
||||||
} else {
|
|
||||||
# Support pipeline input
|
|
||||||
if ($MyInvocation.ExpectingInput) {
|
|
||||||
$input | & "node$exe" "$basedir/../typescript/bin/tsserver" $args
|
|
||||||
} else {
|
|
||||||
& "node$exe" "$basedir/../typescript/bin/tsserver" $args
|
|
||||||
}
|
|
||||||
$ret=$LASTEXITCODE
|
|
||||||
}
|
|
||||||
exit $ret
|
|
||||||
Generated
Vendored
-16
@@ -1,16 +0,0 @@
|
|||||||
#!/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/../update-browserslist-db/cli.js" "$@"
|
|
||||||
else
|
|
||||||
exec node "$basedir/../update-browserslist-db/cli.js" "$@"
|
|
||||||
fi
|
|
||||||
Generated
Vendored
-17
@@ -1,17 +0,0 @@
|
|||||||
@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%\..\update-browserslist-db\cli.js" %*
|
|
||||||
Generated
Vendored
-28
@@ -1,28 +0,0 @@
|
|||||||
#!/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/../update-browserslist-db/cli.js" $args
|
|
||||||
} else {
|
|
||||||
& "$basedir/node$exe" "$basedir/../update-browserslist-db/cli.js" $args
|
|
||||||
}
|
|
||||||
$ret=$LASTEXITCODE
|
|
||||||
} else {
|
|
||||||
# Support pipeline input
|
|
||||||
if ($MyInvocation.ExpectingInput) {
|
|
||||||
$input | & "node$exe" "$basedir/../update-browserslist-db/cli.js" $args
|
|
||||||
} else {
|
|
||||||
& "node$exe" "$basedir/../update-browserslist-db/cli.js" $args
|
|
||||||
}
|
|
||||||
$ret=$LASTEXITCODE
|
|
||||||
}
|
|
||||||
exit $ret
|
|
||||||
Generated
Vendored
-16
@@ -1,16 +0,0 @@
|
|||||||
#!/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/../vite/bin/vite.js" "$@"
|
|
||||||
else
|
|
||||||
exec node "$basedir/../vite/bin/vite.js" "$@"
|
|
||||||
fi
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user