mirror of
https://github.com/ruvnet/RuView.git
synced 2026-04-28 05:59:32 +00:00
feat: QEMU ESP32-S3 testing platform + swarm configurator (ADR-061/062) (#260)
9-layer QEMU testing platform (ADR-061) and YAML-driven swarm configurator (ADR-062) for ESP32-S3 firmware testing without hardware. 12 commits, 56 files, +9,500 lines. Tested on Windows with Espressif QEMU 9.0.0 — firmware boots, mock CSI generates frames, 14/16 validation checks pass. 39 bugs found and fixed across 2 deep code reviews. Closes #259 Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
a467dfed9f
commit
523be943b0
57 changed files with 9532 additions and 8 deletions
|
|
@ -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
|
||||
|
||||
| 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-039](../../docs/adr/ADR-039-esp32-edge-intelligence.md) | Edge intelligence tiers 0-2 | Accepted |
|
||||
| [ADR-040](../../docs/adr/) | WASM programmable sensing (Tier 3) with RVF container format | Alpha |
|
||||
| [ADR-057](../../docs/adr/ADR-057-build-time-csi-guard.md) | Build-time CSI guard (`CONFIG_ESP_WIFI_CSI_ENABLED`) | Accepted |
|
||||
| [ADR-060](../../docs/adr/ADR-060-channel-mac-filter.md) | Channel override and MAC address filter | Accepted |
|
||||
| [ADR-061](../../docs/adr/ADR-061-qemu-esp32s3-firmware-testing.md) | QEMU ESP32-S3 emulation for firmware testing | Proposed |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ set(SRCS
|
|||
|
||||
set(REQUIRES "")
|
||||
|
||||
# ADR-061: Mock CSI generator for QEMU testing
|
||||
if(CONFIG_CSI_MOCK_ENABLED)
|
||||
list(APPEND SRCS "mock_csi.c")
|
||||
endif()
|
||||
|
||||
# ADR-045: AMOLED display support (compile-time optional)
|
||||
if(CONFIG_DISPLAY_ENABLE)
|
||||
list(APPEND SRCS "display_hal.c" "display_ui.c" "display_task.c")
|
||||
|
|
|
|||
|
|
@ -201,3 +201,40 @@ menu "WASM Programmable Sensing (ADR-040)"
|
|||
Default 1000 ms = 1 Hz.
|
||||
|
||||
endmenu
|
||||
|
||||
menu "Mock CSI (QEMU Testing)"
|
||||
config CSI_MOCK_ENABLED
|
||||
bool "Enable mock CSI generator (for QEMU testing)"
|
||||
default n
|
||||
help
|
||||
Replace real WiFi CSI with synthetic frame generator.
|
||||
Use with QEMU emulation for automated testing.
|
||||
|
||||
config CSI_MOCK_SKIP_WIFI_CONNECT
|
||||
bool "Skip WiFi STA connection"
|
||||
depends on CSI_MOCK_ENABLED
|
||||
default y
|
||||
help
|
||||
Skip WiFi initialization when using mock CSI.
|
||||
|
||||
config CSI_MOCK_SCENARIO
|
||||
int "Mock scenario (0-9, 255=all)"
|
||||
depends on CSI_MOCK_ENABLED
|
||||
default 255
|
||||
range 0 255
|
||||
help
|
||||
0=empty, 1=static, 2=walking, 3=fall, 4=multi-person,
|
||||
5=channel-sweep, 6=mac-filter, 7=ring-overflow,
|
||||
8=boundary-rssi, 9=zero-length, 255=run all.
|
||||
|
||||
config CSI_MOCK_SCENARIO_DURATION_MS
|
||||
int "Scenario duration (ms)"
|
||||
depends on CSI_MOCK_ENABLED
|
||||
default 5000
|
||||
range 1000 60000
|
||||
|
||||
config CSI_MOCK_LOG_FRAMES
|
||||
bool "Log every mock frame (verbose)"
|
||||
depends on CSI_MOCK_ENABLED
|
||||
default n
|
||||
endmenu
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@
|
|||
#include "wasm_runtime.h"
|
||||
#include "wasm_upload.h"
|
||||
#include "display_task.h"
|
||||
#ifdef CONFIG_CSI_MOCK_ENABLED
|
||||
#include "mock_csi.h"
|
||||
#endif
|
||||
|
||||
#include "esp_timer.h"
|
||||
|
||||
|
|
@ -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);
|
||||
|
||||
/* Initialize WiFi STA */
|
||||
/* Initialize WiFi STA (skip entirely under QEMU mock — no RF hardware) */
|
||||
#ifndef CONFIG_CSI_MOCK_SKIP_WIFI_CONNECT
|
||||
wifi_init_sta();
|
||||
#else
|
||||
ESP_LOGI(TAG, "Mock CSI mode: skipping WiFi init (CONFIG_CSI_MOCK_SKIP_WIFI_CONNECT)");
|
||||
#endif
|
||||
|
||||
/* Initialize UDP sender with runtime target */
|
||||
#ifdef CONFIG_CSI_MOCK_SKIP_WIFI_CONNECT
|
||||
ESP_LOGI(TAG, "Mock CSI mode: skipping UDP sender init (no network)");
|
||||
#else
|
||||
if (stream_sender_init_with(g_nvs_config.target_ip, g_nvs_config.target_port) != 0) {
|
||||
ESP_LOGE(TAG, "Failed to initialize UDP sender");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Initialize CSI collection */
|
||||
#ifdef CONFIG_CSI_MOCK_ENABLED
|
||||
/* ADR-061: Start mock CSI generator (replaces real WiFi CSI in QEMU) */
|
||||
esp_err_t mock_ret = mock_csi_init(CONFIG_CSI_MOCK_SCENARIO);
|
||||
if (mock_ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Mock CSI init failed: %s", esp_err_to_name(mock_ret));
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Mock CSI active (scenario=%d)", CONFIG_CSI_MOCK_SCENARIO);
|
||||
}
|
||||
#else
|
||||
csi_collector_init();
|
||||
#endif
|
||||
|
||||
/* ADR-039: Initialize edge processing pipeline. */
|
||||
edge_config_t edge_cfg = {
|
||||
|
|
@ -162,12 +183,17 @@ void app_main(void)
|
|||
esp_err_to_name(edge_ret));
|
||||
}
|
||||
|
||||
/* Initialize OTA update HTTP server. */
|
||||
/* Initialize OTA update HTTP server (requires network). */
|
||||
httpd_handle_t ota_server = NULL;
|
||||
#ifndef CONFIG_CSI_MOCK_SKIP_WIFI_CONNECT
|
||||
esp_err_t ota_ret = ota_update_init_ex(&ota_server);
|
||||
if (ota_ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "OTA server init failed: %s", esp_err_to_name(ota_ret));
|
||||
}
|
||||
#else
|
||||
esp_err_t ota_ret = ESP_ERR_NOT_SUPPORTED;
|
||||
ESP_LOGI(TAG, "Mock CSI mode: skipping OTA server (no network)");
|
||||
#endif
|
||||
|
||||
/* ADR-040: Initialize WASM programmable sensing runtime. */
|
||||
esp_err_t wasm_ret = wasm_runtime_init();
|
||||
|
|
@ -205,10 +231,12 @@ void app_main(void)
|
|||
power_mgmt_init(g_nvs_config.power_duty);
|
||||
|
||||
/* ADR-045: Start AMOLED display task (gracefully skips if no display). */
|
||||
#ifdef CONFIG_DISPLAY_ENABLE
|
||||
esp_err_t disp_ret = display_task_start();
|
||||
if (disp_ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Display init returned: %s", esp_err_to_name(disp_ret));
|
||||
}
|
||||
#endif
|
||||
|
||||
ESP_LOGI(TAG, "CSI streaming active → %s:%d (edge_tier=%u, OTA=%s, WASM=%s)",
|
||||
g_nvs_config.target_ip, g_nvs_config.target_port,
|
||||
|
|
|
|||
696
firmware/esp32-csi-node/main/mock_csi.c
Normal file
696
firmware/esp32-csi-node/main/mock_csi.c
Normal file
|
|
@ -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 */
|
||||
107
firmware/esp32-csi-node/main/mock_csi.h
Normal file
107
firmware/esp32-csi-node/main/mock_csi.h
Normal file
|
|
@ -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 */
|
||||
54
firmware/esp32-csi-node/sdkconfig.coverage
Normal file
54
firmware/esp32-csi-node/sdkconfig.coverage
Normal file
|
|
@ -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
|
||||
27
firmware/esp32-csi-node/sdkconfig.qemu
Normal file
27
firmware/esp32-csi-node/sdkconfig.qemu
Normal file
|
|
@ -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
|
||||
79
firmware/esp32-csi-node/test/Makefile
Normal file
79
firmware/esp32-csi-node/test/Makefile
Normal file
|
|
@ -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/
|
||||
BIN
firmware/esp32-csi-node/test/corpus/seed_edge_normal.bin
Normal file
BIN
firmware/esp32-csi-node/test/corpus/seed_edge_normal.bin
Normal file
Binary file not shown.
BIN
firmware/esp32-csi-node/test/corpus/seed_edge_overflow.bin
Normal file
BIN
firmware/esp32-csi-node/test/corpus/seed_edge_overflow.bin
Normal file
Binary file not shown.
BIN
firmware/esp32-csi-node/test/corpus/seed_empty.bin
Normal file
BIN
firmware/esp32-csi-node/test/corpus/seed_empty.bin
Normal file
Binary file not shown.
BIN
firmware/esp32-csi-node/test/corpus/seed_large.bin
Normal file
BIN
firmware/esp32-csi-node/test/corpus/seed_large.bin
Normal file
Binary file not shown.
BIN
firmware/esp32-csi-node/test/corpus/seed_normal.bin
Normal file
BIN
firmware/esp32-csi-node/test/corpus/seed_normal.bin
Normal file
Binary file not shown.
BIN
firmware/esp32-csi-node/test/corpus/seed_nvs.bin
Normal file
BIN
firmware/esp32-csi-node/test/corpus/seed_nvs.bin
Normal file
Binary file not shown.
203
firmware/esp32-csi-node/test/fuzz_csi_serialize.c
Normal file
203
firmware/esp32-csi-node/test/fuzz_csi_serialize.c
Normal file
|
|
@ -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;
|
||||
}
|
||||
217
firmware/esp32-csi-node/test/fuzz_edge_enqueue.c
Normal file
217
firmware/esp32-csi-node/test/fuzz_edge_enqueue.c
Normal file
|
|
@ -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;
|
||||
}
|
||||
286
firmware/esp32-csi-node/test/fuzz_nvs_config.c
Normal file
286
firmware/esp32-csi-node/test/fuzz_nvs_config.c
Normal file
|
|
@ -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;
|
||||
}
|
||||
5
firmware/esp32-csi-node/test/stubs/esp_err.h
Normal file
5
firmware/esp32-csi-node/test/stubs/esp_err.h
Normal file
|
|
@ -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
|
||||
5
firmware/esp32-csi-node/test/stubs/esp_log.h
Normal file
5
firmware/esp32-csi-node/test/stubs/esp_log.h
Normal file
|
|
@ -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
|
||||
65
firmware/esp32-csi-node/test/stubs/esp_stubs.c
Normal file
65
firmware/esp32-csi-node/test/stubs/esp_stubs.c
Normal file
|
|
@ -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; }
|
||||
169
firmware/esp32-csi-node/test/stubs/esp_stubs.h
Normal file
169
firmware/esp32-csi-node/test/stubs/esp_stubs.h
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
/**
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* Stub: returns a monotonically increasing microsecond counter.
|
||||
* Declared here, defined in esp_stubs.c.
|
||||
*/
|
||||
int64_t esp_timer_get_time(void);
|
||||
|
||||
/* ---- 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 */
|
||||
5
firmware/esp32-csi-node/test/stubs/esp_timer.h
Normal file
5
firmware/esp32-csi-node/test/stubs/esp_timer.h
Normal file
|
|
@ -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
|
||||
5
firmware/esp32-csi-node/test/stubs/esp_wifi.h
Normal file
5
firmware/esp32-csi-node/test/stubs/esp_wifi.h
Normal file
|
|
@ -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
|
||||
5
firmware/esp32-csi-node/test/stubs/esp_wifi_types.h
Normal file
5
firmware/esp32-csi-node/test/stubs/esp_wifi_types.h
Normal file
|
|
@ -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
|
||||
5
firmware/esp32-csi-node/test/stubs/freertos/FreeRTOS.h
Normal file
5
firmware/esp32-csi-node/test/stubs/freertos/FreeRTOS.h
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/* Stub: redirect to unified stubs header. */
|
||||
#ifndef FREERTOS_H_STUB
|
||||
#define FREERTOS_H_STUB
|
||||
#include "esp_stubs.h"
|
||||
#endif
|
||||
5
firmware/esp32-csi-node/test/stubs/freertos/task.h
Normal file
5
firmware/esp32-csi-node/test/stubs/freertos/task.h
Normal file
|
|
@ -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
|
||||
5
firmware/esp32-csi-node/test/stubs/nvs.h
Normal file
5
firmware/esp32-csi-node/test/stubs/nvs.h
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/* Stub: redirect to unified stubs header. */
|
||||
#ifndef NVS_H_STUB
|
||||
#define NVS_H_STUB
|
||||
#include "esp_stubs.h"
|
||||
#endif
|
||||
5
firmware/esp32-csi-node/test/stubs/nvs_flash.h
Normal file
5
firmware/esp32-csi-node/test/stubs/nvs_flash.h
Normal file
|
|
@ -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
|
||||
5
firmware/esp32-csi-node/test/stubs/sdkconfig.h
Normal file
5
firmware/esp32-csi-node/test/stubs/sdkconfig.h
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue