Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot] ba40b4a42a chore: update vendor submodules to latest main 2026-03-15 00:41:48 +00:00
16 changed files with 89 additions and 245 deletions
+7 -22
View File
@@ -38,7 +38,7 @@ jobs:
with:
path: /opt/qemu-esp32
# Include date component so cache refreshes monthly when branch updates
key: qemu-esp32s3-${{ env.QEMU_BRANCH }}-v5
key: qemu-esp32s3-${{ env.QEMU_BRANCH }}-v4
restore-keys: |
qemu-esp32s3-${{ env.QEMU_BRANCH }}-
@@ -49,7 +49,6 @@ jobs:
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
@@ -114,9 +113,7 @@ jobs:
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
run: pip install esptool esp-idf-nvs-partition-gen
- name: Set target ESP32-S3
working-directory: firmware/esp32-csi-node
@@ -134,7 +131,6 @@ jobs:
- 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 }}
@@ -153,7 +149,6 @@ jobs:
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 \
@@ -322,15 +317,13 @@ jobs:
uses: actions/download-artifact@v4
with:
name: qemu-esp32
path: /opt/qemu-esp32
path: ${{ github.workspace }}/qemu-build
- name: Make QEMU executable
run: chmod +x /opt/qemu-esp32/bin/qemu-system-xtensa
run: chmod +x ${{ github.workspace }}/qemu-build/bin/qemu-system-xtensa
- name: Install Python dependencies
run: |
. $IDF_PATH/export.sh
pip install pyyaml esptool esp-idf-nvs-partition-gen
run: pip install pyyaml esptool esp-idf-nvs-partition-gen
- name: Build firmware for swarm
working-directory: firmware/esp32-csi-node
@@ -341,23 +334,15 @@ jobs:
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
--qemu-path ${{ github.workspace }}/qemu-build/bin/qemu-system-xtensa \
--output-dir build/swarm-results
timeout-minutes: 10
- name: Upload swarm results
-12
View File
@@ -5,18 +5,6 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [v0.4.3-esp32] — 2026-03-15
### Fixed
- **Fall detection false positives (#263)** — Default threshold raised from 2.0 to 15.0 rad/s²; normal walking (2-5 rad/s²) no longer triggers alerts. Added 3-consecutive-frame debounce and 5-second cooldown between alerts. Verified on real ESP32-S3 hardware: 0 false alerts in 60s / 1,300+ live WiFi CSI frames.
- **Kconfig default mismatch** — `CONFIG_EDGE_FALL_THRESH` Kconfig default was still 2000 (=2.0) while `nvs_config.c` fallback was updated to 15.0. Fixed Kconfig to 15000. Caught by real hardware testing — mock data did not reproduce.
- **provision.py NVS generator API change** — `esp_idf_nvs_partition_gen` package changed its `generate()` signature; switched to subprocess-first invocation for cross-version compatibility.
- **QEMU CI pipeline (11 jobs)** — Fixed all failures: fuzz test `esp_timer` stubs, QEMU `libgcrypt` dependency, NVS matrix generator, IDF container `pip` path, flash image padding, validation WARN handling, swarm `ip`/`cargo` missing.
### Added
- **4MB flash support (#265)** — `partitions_4mb.csv` and `sdkconfig.defaults.4mb` for ESP32-S3 boards with 4MB flash (e.g. SuperMini). Dual OTA slots, 1.856 MB each. Thanks to @sebbu for the community workaround that confirmed feasibility.
- **`--strict` flag** for `validate_qemu_output.py` — WARNs now pass by default in CI (no real WiFi in QEMU); use `--strict` to fail on warnings.
## [Unreleased]
### Added
+4 -11
View File
@@ -1047,24 +1047,17 @@ Download a pre-built binary — no build toolchain needed:
| Release | What's included | Tag |
|---------|-----------------|-----|
| [v0.4.3](https://github.com/ruvnet/RuView/releases/tag/v0.4.3-esp32) | **Stable**Fall detection fix ([#263](https://github.com/ruvnet/RuView/issues/263)), 4MB flash support ([#265](https://github.com/ruvnet/RuView/issues/265)), QEMU CI green | `v0.4.3-esp32` |
| [v0.4.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.1-esp32) | CSI build fix, compile guard, AMOLED display, edge intelligence ([ADR-057](docs/adr/ADR-057-firmware-csi-build-guard.md)) | `v0.4.1-esp32` |
| [v0.4.1](https://github.com/ruvnet/RuView/releases/tag/v0.4.1-esp32) | **Stable**CSI build fix, compile guard, AMOLED display, edge intelligence ([ADR-057](docs/adr/ADR-057-firmware-csi-build-guard.md)) | `v0.4.1-esp32` |
| [v0.3.0-alpha](https://github.com/ruvnet/RuView/releases/tag/v0.3.0-alpha-esp32) | Alpha — adds on-device edge intelligence and WASM modules ([ADR-039](docs/adr/ADR-039-esp32-edge-intelligence.md), [ADR-040](docs/adr/ADR-040-wasm-programmable-sensing.md)) | `v0.3.0-alpha-esp32` |
| [v0.2.0](https://github.com/ruvnet/RuView/releases/tag/v0.2.0-esp32) | Raw CSI streaming, multi-node TDM, channel hopping | `v0.2.0-esp32` |
```bash
# 1. Flash the firmware to your ESP32-S3 (8MB flash — most boards)
# 1. Flash the firmware to your ESP32-S3
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
write_flash --flash-mode dio --flash-size 8MB --flash-freq 80m \
0x0 bootloader.bin 0x8000 partition-table.bin \
0xf000 ota_data_initial.bin 0x20000 esp32-csi-node.bin
# 1b. For 4MB flash boards (e.g. ESP32-S3 SuperMini 4MB) — use the 4MB binaries:
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
write_flash --flash-mode dio --flash-size 4MB --flash-freq 80m \
0x0 bootloader.bin 0x8000 partition-table-4mb.bin \
0xF000 ota_data_initial.bin 0x20000 esp32-csi-node-4mb.bin
# 2. Set WiFi credentials and server address (stored in flash, survives reboots)
python firmware/esp32-csi-node/provision.py --port COM7 \
--ssid "YourWiFi" --password "secret" --target-ip 192.168.1.20
@@ -1111,9 +1104,9 @@ python firmware/esp32-csi-node/provision.py --port COM7 \
--ssid "YourWiFi" --password "secret" --target-ip 192.168.1.20 \
--edge-tier 2
# Fine-tune detection thresholds (fall-thresh in milli-units: 15000 = 15.0 rad/s²)
# Fine-tune detection thresholds
python firmware/esp32-csi-node/provision.py --port COM7 \
--edge-tier 2 --vital-int 500 --fall-thresh 15000 --subk-count 16
--edge-tier 2 --vital-int 500 --fall-thresh 5000 --subk-count 16
```
When Tier 2 is active, the node sends a 32-byte vitals packet once per second containing: presence, motion level, breathing BPM, heart rate BPM, confidence scores, fall alert flag, and occupancy count.
+1 -10
View File
@@ -826,22 +826,13 @@ Pre-built binaries are available at [Releases](https://github.com/ruvnet/RuView/
> **Important:** Firmware versions prior to v0.4.1 had CSI **disabled** in the build config, causing a runtime error (`E wifi:CSI not enabled in menuconfig!`). Always use v0.4.1 or later.
```bash
# Flash an ESP32-S3 with 8MB flash (most boards)
# Flash an ESP32-S3 (requires esptool: pip install esptool)
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
write-flash --flash-mode dio --flash-size 8MB --flash-freq 80m \
0x0 bootloader.bin 0x8000 partition-table.bin \
0xf000 ota_data_initial.bin 0x20000 esp32-csi-node.bin
```
**4MB flash boards** (e.g. ESP32-S3 SuperMini 4MB): download the 4MB binaries from the [v0.4.3 release](https://github.com/ruvnet/RuView/releases/tag/v0.4.3-esp32) and use `--flash-size 4MB`:
```bash
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
write-flash --flash-mode dio --flash-size 4MB --flash-freq 80m \
0x0 bootloader.bin 0x8000 partition-table-4mb.bin \
0xF000 ota_data_initial.bin 0x20000 esp32-csi-node-4mb.bin
```
**Provisioning:**
```bash
@@ -68,13 +68,10 @@ menu "Edge Intelligence (ADR-039)"
config EDGE_FALL_THRESH
int "Fall detection threshold (x1000)"
default 15000
default 2000
range 100 50000
help
Phase acceleration threshold for fall detection.
Value is divided by 1000 to get rad/s². Default 15000 = 15.0 rad/s².
Raise to reduce false positives in high-traffic environments.
Normal walking produces accelerations of 2-5 rad/s².
Stored as integer; divided by 1000 at runtime.
Default 2000 = 2.0 rad/s^2.
+5 -33
View File
@@ -244,10 +244,6 @@ static uint32_t s_frame_count;
/** Previous phase velocity for fall detection (acceleration). */
static float s_prev_phase_velocity;
/** Fall detection debounce state (issue #263). */
static uint8_t s_fall_consec_count; /**< Consecutive frames above threshold. */
static int64_t s_fall_last_alert_us; /**< Timestamp of last fall alert (debounce). */
/** Adaptive calibration state. */
static bool s_calibrated;
static float s_calib_sum;
@@ -693,7 +689,7 @@ static void process_frame(const edge_ring_slot_t *slot)
}
s_presence_detected = (s_presence_score > threshold);
/* --- Step 10: Fall detection (phase acceleration + debounce, issue #263) --- */
/* --- Step 10: Fall detection (phase acceleration) --- */
if (s_history_len >= 3) {
uint16_t i0 = (s_history_idx + EDGE_PHASE_HISTORY_LEN - 1) % EDGE_PHASE_HISTORY_LEN;
uint16_t i1 = (s_history_idx + EDGE_PHASE_HISTORY_LEN - 2) % EDGE_PHASE_HISTORY_LEN;
@@ -701,26 +697,10 @@ static void process_frame(const edge_ring_slot_t *slot)
float accel = fabsf(velocity - s_prev_phase_velocity);
s_prev_phase_velocity = velocity;
if (accel > s_cfg.fall_thresh) {
s_fall_consec_count++;
} else {
s_fall_consec_count = 0;
}
/* Require EDGE_FALL_CONSEC_MIN consecutive frames above threshold,
* plus a cooldown period to prevent alert storms. */
int64_t now_us = esp_timer_get_time();
int64_t cooldown_us = (int64_t)EDGE_FALL_COOLDOWN_MS * 1000;
if (s_fall_consec_count >= EDGE_FALL_CONSEC_MIN
&& (now_us - s_fall_last_alert_us) >= cooldown_us)
{
s_fall_detected = true;
s_fall_last_alert_us = now_us;
s_fall_consec_count = 0;
ESP_LOGW(TAG, "Fall detected! accel=%.4f > thresh=%.4f (consec=%u)",
accel, s_cfg.fall_thresh, EDGE_FALL_CONSEC_MIN);
} else if (s_fall_consec_count == 0) {
s_fall_detected = false;
s_fall_detected = (accel > s_cfg.fall_thresh);
if (s_fall_detected) {
ESP_LOGW(TAG, "Fall detected! accel=%.4f > thresh=%.4f",
accel, s_cfg.fall_thresh);
}
}
@@ -787,12 +767,6 @@ static void edge_task(void *arg)
while (1) {
if (ring_pop(&slot)) {
process_frame(&slot);
/* Yield after every frame to feed the Core 1 watchdog.
* process_frame() is CPU-intensive (biquad filters, Welford stats,
* BPM estimation, multi-person vitals) and can take several ms.
* Without this yield, edge_dsp at priority 5 starves IDLE1 at
* priority 0, triggering the task watchdog. See issue #266. */
vTaskDelay(1);
} else {
/* No frames available — yield briefly. */
vTaskDelay(pdMS_TO_TICKS(1));
@@ -876,8 +850,6 @@ esp_err_t edge_processing_init(const edge_config_t *cfg)
s_latest_rssi = 0;
s_frame_count = 0;
s_prev_phase_velocity = 0.0f;
s_fall_consec_count = 0;
s_fall_last_alert_us = 0;
s_last_vitals_send_us = 0;
s_has_prev_iq = false;
s_prev_iq_len = 0;
@@ -42,10 +42,6 @@
#define EDGE_CALIB_FRAMES 1200 /**< Frames for adaptive calibration (~60s at 20 Hz). */
#define EDGE_CALIB_SIGMA_MULT 3.0f /**< Threshold = mean + 3*sigma of ambient. */
/* ---- Fall detection ---- */
#define EDGE_FALL_COOLDOWN_MS 5000 /**< Minimum ms between fall alerts (debounce). */
#define EDGE_FALL_CONSEC_MIN 3 /**< Consecutive frames above threshold to trigger. */
/* ---- SPSC ring buffer slot ---- */
typedef struct {
uint8_t iq_data[EDGE_MAX_IQ_BYTES]; /**< Raw I/Q bytes from CSI callback. */
+1 -1
View File
@@ -61,7 +61,7 @@ void nvs_config_load(nvs_config_t *cfg)
#ifdef CONFIG_EDGE_FALL_THRESH
cfg->fall_thresh = (float)CONFIG_EDGE_FALL_THRESH / 1000.0f;
#else
cfg->fall_thresh = 15.0f; /* Default raised from 2.0 — see issue #263. */
cfg->fall_thresh = 2.0f;
#endif
cfg->vital_window = 256;
#ifdef CONFIG_EDGE_VITAL_INTERVAL_MS
@@ -1,15 +0,0 @@
# ESP32-S3 CSI Node — 4MB flash partition table (issue #265)
# For boards with 4MB flash (e.g. ESP32-S3 SuperMini 4MB).
# Binary is ~978KB so each OTA slot is 1.875MB — plenty of room.
#
# Usage: copy to partitions_display.csv OR set in sdkconfig:
# CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_4mb.csv"
# CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
# CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
#
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
otadata, data, ota, 0xF000, 0x2000,
phy_init, data, phy, 0x11000, 0x1000,
ota_0, app, ota_0, 0x20000, 0x1D0000,
ota_1, app, ota_1, 0x1F0000, 0x1D0000,
Can't render this file because it contains an unexpected character in line 6 and column 44.
+26 -20
View File
@@ -83,20 +83,25 @@ def generate_nvs_binary(csv_content, size):
bin_path = csv_path.replace(".csv", ".bin")
try:
# Method 1: subprocess invocation (most reliable across package versions)
for module_name in ["esp_idf_nvs_partition_gen", "nvs_partition_gen"]:
try:
subprocess.check_call(
[sys.executable, "-m", module_name, "generate",
csv_path, bin_path, hex(size)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
with open(bin_path, "rb") as f:
return f.read()
except (subprocess.CalledProcessError, FileNotFoundError):
continue
# Try the pip-installed version first (esp_idf_nvs_partition_gen package)
try:
from esp_idf_nvs_partition_gen import nvs_partition_gen
nvs_partition_gen.generate(csv_path, bin_path, size)
with open(bin_path, "rb") as f:
return f.read()
except ImportError:
pass
# Method 2: ESP-IDF bundled script
# Try legacy import name (older versions)
try:
import nvs_partition_gen
nvs_partition_gen.generate(csv_path, bin_path, size)
with open(bin_path, "rb") as f:
return f.read()
except ImportError:
pass
# Fall back to calling the ESP-IDF script directly
idf_path = os.environ.get("IDF_PATH", "")
gen_script = os.path.join(idf_path, "components", "nvs_flash",
"nvs_partition_generator", "nvs_partition_gen.py")
@@ -108,10 +113,13 @@ def generate_nvs_binary(csv_content, size):
with open(bin_path, "rb") as f:
return f.read()
raise RuntimeError(
"NVS partition generator not available. "
"Install: pip install esp-idf-nvs-partition-gen"
)
# Last resort: try as a module
subprocess.check_call([
sys.executable, "-m", "nvs_partition_gen", "generate",
csv_path, bin_path, hex(size)
])
with open(bin_path, "rb") as f:
return f.read()
finally:
for p in (csv_path, bin_path):
@@ -160,9 +168,7 @@ def main():
parser.add_argument("--edge-tier", type=int, choices=[0, 1, 2],
help="Edge processing tier: 0=off, 1=stats, 2=vitals")
parser.add_argument("--pres-thresh", type=int, help="Presence detection threshold (default: 50)")
parser.add_argument("--fall-thresh", type=int, help="Fall detection threshold in milli-units "
"(value/1000 = rad/s²). Default: 15000 → 15.0 rad/s². "
"Raise to reduce false positives in high-traffic areas.")
parser.add_argument("--fall-thresh", type=int, help="Fall detection threshold (default: 500)")
parser.add_argument("--vital-win", type=int, help="Phase history window in frames (default: 300)")
parser.add_argument("--vital-int", type=int, help="Vitals packet interval in ms (default: 1000)")
parser.add_argument("--subk-count", type=int, help="Top-K subcarrier count (default: 32)")
@@ -1,29 +0,0 @@
# ESP32-S3 CSI Node — 4MB Flash SDK Configuration (issue #265)
# For boards with 4MB flash (e.g. ESP32-S3 SuperMini 4MB).
#
# Build: cp sdkconfig.defaults.4mb sdkconfig.defaults && idf.py set-target esp32s3 && idf.py build
# Or: idf.py -D SDKCONFIG_DEFAULTS="sdkconfig.defaults.4mb" set-target esp32s3 && idf.py build
CONFIG_IDF_TARGET="esp32s3"
# 4MB flash partition table
CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_4mb.csv"
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_ESPTOOLPY_FLASHSIZE="4MB"
# Compiler: optimize for size (critical for 4MB)
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
# CSI support
CONFIG_ESP_WIFI_CSI_ENABLED=y
# Disable display support to save flash (ADR-045 display requires 8MB)
# CONFIG_DISPLAY_ENABLE is not set
# Reduce logging to save flash
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
CONFIG_LWIP_SO_RCVBUF=y
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
@@ -33,32 +33,12 @@ typedef int esp_err_t;
/* ---- 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. */
+38 -40
View File
@@ -266,10 +266,10 @@ def generate_nvs_binary(csv_content: str, size: int) -> bytes:
"""Generate an NVS partition binary from CSV content.
Tries multiple methods to find nvs_partition_gen:
1. Subprocess invocation (most reliable across package versions)
2. esp_idf_nvs_partition_gen pip package (direct import)
3. Legacy nvs_partition_gen pip package
4. ESP-IDF bundled script (via IDF_PATH)
1. esp_idf_nvs_partition_gen pip package
2. Legacy nvs_partition_gen pip package
3. ESP-IDF bundled script (via IDF_PATH)
4. Module invocation
"""
import subprocess
import tempfile
@@ -281,36 +281,25 @@ def generate_nvs_binary(csv_content: str, size: int) -> bytes:
bin_path = csv_path.replace(".csv", ".bin")
try:
# Method 1: subprocess invocation (most reliable — avoids API changes)
for module_name in ["esp_idf_nvs_partition_gen", "nvs_partition_gen"]:
try:
subprocess.check_call(
[sys.executable, "-m", module_name, "generate",
csv_path, bin_path, hex(size)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
with open(bin_path, "rb") as f:
return f.read()
except (subprocess.CalledProcessError, FileNotFoundError):
continue
# Try pip-installed version first
try:
from esp_idf_nvs_partition_gen import nvs_partition_gen
nvs_partition_gen.generate(csv_path, bin_path, size)
with open(bin_path, "rb") as f:
return f.read()
except ImportError:
pass
# Method 2: direct import (handles older API where generate() takes int)
for module_name in ["esp_idf_nvs_partition_gen.nvs_partition_gen",
"nvs_partition_gen"]:
try:
mod = __import__(module_name, fromlist=["generate"])
# Try int size first, then hex string (API varies by version)
for size_arg in [size, hex(size)]:
try:
mod.generate(csv_path, bin_path, size_arg)
with open(bin_path, "rb") as f:
return f.read()
except (TypeError, AttributeError):
continue
except ImportError:
continue
# Try legacy import
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
# Method 3: ESP-IDF bundled script
# Try ESP-IDF bundled script
idf_path = os.environ.get("IDF_PATH", "")
gen_script = os.path.join(
idf_path, "components", "nvs_flash",
@@ -324,16 +313,25 @@ def generate_nvs_binary(csv_content: str, size: int) -> bytes:
with open(bin_path, "rb") as f:
return f.read()
print("ERROR: NVS partition generator tool not found.", file=sys.stderr)
print("Install: pip install esp-idf-nvs-partition-gen", file=sys.stderr)
print("Or set IDF_PATH to your ESP-IDF installation", file=sys.stderr)
raise RuntimeError(
"NVS partition generator not available. "
"Install: pip install esp-idf-nvs-partition-gen"
)
# Last resort: try as a module
try:
subprocess.check_call([
sys.executable, "-m", "nvs_partition_gen", "generate",
csv_path, bin_path, hex(size)
])
with open(bin_path, "rb") as f:
return f.read()
except (subprocess.CalledProcessError, FileNotFoundError):
print("ERROR: NVS partition generator tool not found.", file=sys.stderr)
print("Install: pip install esp-idf-nvs-partition-gen", file=sys.stderr)
print("Or set IDF_PATH to your ESP-IDF installation", file=sys.stderr)
raise RuntimeError(
"NVS partition generator not available. "
"Install: pip install esp-idf-nvs-partition-gen"
)
finally:
for p in set((csv_path, bin_path)):
for p in set((csv_path, bin_path)): # deduplicate in case paths are identical
if os.path.isfile(p):
os.unlink(p)
+3 -14
View File
@@ -326,12 +326,7 @@ class NetworkState:
def _run_ip(args: List[str], check: bool = False) -> subprocess.CompletedProcess:
try:
return subprocess.run(["ip"] + args, capture_output=True, text=True, check=check)
except FileNotFoundError:
# 'ip' command not installed (e.g. minimal container image)
return subprocess.CompletedProcess(args=["ip"] + args, returncode=127,
stdout="", stderr="ip: command not found")
return subprocess.run(["ip"] + args, capture_output=True, text=True, check=check)
def setup_network(cfg: SwarmConfig, net: NetworkState) -> Dict[int, List[str]]:
@@ -343,10 +338,8 @@ def setup_network(cfg: SwarmConfig, net: NetworkState) -> Dict[int, List[str]]:
node_net_args: Dict[int, List[str]] = {}
n = len(cfg.nodes)
# Check if we can use TAP/bridge (requires root on Linux + ip command)
import shutil
can_tap = (IS_LINUX and hasattr(os, 'geteuid') and os.geteuid() == 0
and shutil.which("ip") is not None)
# Check if we can use TAP/bridge (requires root on Linux)
can_tap = IS_LINUX and hasattr(os, 'geteuid') and os.geteuid() == 0
if not can_tap:
if IS_LINUX:
@@ -502,14 +495,10 @@ def start_aggregator(
port: int, n_nodes: int, output_file: Path, log_file: Path
) -> Optional[subprocess.Popen]:
"""Start the Rust aggregator binary. Returns Popen or None on failure."""
import shutil
cargo_toml = RUST_DIR / "Cargo.toml"
if not cargo_toml.exists():
warn(f"Rust workspace not found at {RUST_DIR}; skipping aggregator.")
return None
if shutil.which("cargo") is None:
warn("cargo not found; skipping aggregator (Rust not installed).")
return None
args = [
"cargo", "run",
+2 -9
View File
@@ -375,10 +375,6 @@ def main():
"log_file",
help="Path to QEMU UART log file",
)
parser.add_argument(
"--strict", action="store_true",
help="Exit non-zero on warnings (default: only fail on errors/fatals)",
)
args = parser.parse_args()
log_path = Path(args.log_file)
@@ -396,15 +392,12 @@ def main():
report = validate_log(log_text)
report.print_report()
# Map max severity to exit code.
# WARNs are expected in QEMU without real WiFi hardware (no CSI data
# flowing), so they exit 0 to avoid failing CI. Use --strict to
# fail on warnings (useful for mock-CSI scenarios where data IS expected).
# Map max severity to exit code
max_sev = report.max_severity
if max_sev <= Severity.SKIP:
sys.exit(0)
elif max_sev == Severity.WARN:
sys.exit(1 if args.strict else 0)
sys.exit(1)
elif max_sev == Severity.ERROR:
sys.exit(2)
else: