feat: add provision.py to repo, fix user guide paths

- Move provision.py from release-only asset into firmware/esp32-csi-node/
- Fix user guide references from scripts/provision.py to correct path
- Update release link to v0.2.0-esp32

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv 2026-03-02 19:07:32 -05:00
parent 915943cef4
commit e0fe10b3dc
2 changed files with 188 additions and 7 deletions

View File

@ -612,7 +612,7 @@ A 3-6 node ESP32-S3 mesh provides full CSI at 20 Hz. Total cost: ~$54 for a 3-no
**Flashing firmware:** **Flashing firmware:**
Pre-built binaries are available at [Releases](https://github.com/ruvnet/wifi-densepose/releases/tag/v0.1.0-esp32). Pre-built binaries are available at [Releases](https://github.com/ruvnet/wifi-densepose/releases/tag/v0.2.0-esp32).
```bash ```bash
# Flash an ESP32-S3 (requires esptool: pip install esptool) # Flash an ESP32-S3 (requires esptool: pip install esptool)
@ -624,7 +624,7 @@ python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
**Provisioning:** **Provisioning:**
```bash ```bash
python scripts/provision.py --port COM7 \ python firmware/esp32-csi-node/provision.py --port COM7 \
--ssid "YourWiFi" --password "YourPassword" --target-ip 192.168.1.20 --ssid "YourWiFi" --password "YourPassword" --target-ip 192.168.1.20
``` ```
@ -635,7 +635,7 @@ Replace `192.168.1.20` with the IP of the machine running the sensing server.
For multistatic mesh deployments with authenticated beacons (ADR-032), provision a shared mesh key: For multistatic mesh deployments with authenticated beacons (ADR-032), provision a shared mesh key:
```bash ```bash
python scripts/provision.py --port COM7 \ python firmware/esp32-csi-node/provision.py --port COM7 \
--ssid "YourWiFi" --password "YourPassword" --target-ip 192.168.1.20 \ --ssid "YourWiFi" --password "YourPassword" --target-ip 192.168.1.20 \
--mesh-key "$(openssl rand -hex 32)" --mesh-key "$(openssl rand -hex 32)"
``` ```
@ -648,13 +648,13 @@ Each node in a multistatic mesh needs a unique TDM slot ID (0-based):
```bash ```bash
# Node 0 (slot 0) — first transmitter # Node 0 (slot 0) — first transmitter
python scripts/provision.py --port COM7 --tdm-slot 0 --tdm-total 3 python firmware/esp32-csi-node/provision.py --port COM7 --tdm-slot 0 --tdm-total 3
# Node 1 (slot 1) # Node 1 (slot 1)
python scripts/provision.py --port COM8 --tdm-slot 1 --tdm-total 3 python firmware/esp32-csi-node/provision.py --port COM8 --tdm-slot 1 --tdm-total 3
# Node 2 (slot 2) # Node 2 (slot 2)
python scripts/provision.py --port COM9 --tdm-slot 2 --tdm-total 3 python firmware/esp32-csi-node/provision.py --port COM9 --tdm-slot 2 --tdm-total 3
``` ```
**Start the aggregator:** **Start the aggregator:**
@ -720,7 +720,7 @@ docker run -p 3000:3000 -p 3001:3001 ruvnet/wifi-densepose:latest
### ESP32: No data arriving ### ESP32: No data arriving
1. Verify the ESP32 is connected to the same WiFi network 1. Verify the ESP32 is connected to the same WiFi network
2. Check the target IP matches the sensing server machine: `python scripts/provision.py --port COM7 --target-ip <YOUR_IP>` 2. Check the target IP matches the sensing server machine: `python firmware/esp32-csi-node/provision.py --port COM7 --target-ip <YOUR_IP>`
3. Verify UDP port 5005 is not blocked by firewall 3. Verify UDP port 5005 is not blocked by firewall
4. Test with: `nc -lu 5005` (Linux) or similar UDP listener 4. Test with: `nc -lu 5005` (Linux) or similar UDP listener

View File

@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""
ESP32-S3 CSI Node Provisioning Script
Writes WiFi credentials and aggregator target to the ESP32's NVS partition
so users can configure a pre-built firmware binary without recompiling.
Usage:
python provision.py --port COM7 --ssid "MyWiFi" --password "secret" --target-ip 192.168.1.20
Requirements:
pip install esptool nvs-partition-gen
(or use the nvs_partition_gen.py bundled with ESP-IDF)
"""
import argparse
import csv
import io
import os
import struct
import subprocess
import sys
import tempfile
# NVS partition table offset — default for ESP-IDF 4MB flash with standard
# partition scheme. The "nvs" partition starts at 0x9000 (36864) and is
# 0x6000 (24576) bytes.
NVS_PARTITION_OFFSET = 0x9000
NVS_PARTITION_SIZE = 0x6000 # 24 KiB
def build_nvs_csv(ssid, password, target_ip, target_port, node_id):
"""Build an NVS CSV string for the csi_cfg namespace."""
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(["key", "type", "encoding", "value"])
writer.writerow(["csi_cfg", "namespace", "", ""])
if ssid:
writer.writerow(["ssid", "data", "string", ssid])
if password is not None:
writer.writerow(["password", "data", "string", password])
if target_ip:
writer.writerow(["target_ip", "data", "string", target_ip])
if target_port is not None:
writer.writerow(["target_port", "data", "u16", str(target_port)])
if node_id is not None:
writer.writerow(["node_id", "data", "u8", str(node_id)])
return buf.getvalue()
def generate_nvs_binary(csv_content, size):
"""Generate an NVS partition binary from CSV using nvs_partition_gen.py."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as f_csv:
f_csv.write(csv_content)
csv_path = f_csv.name
bin_path = csv_path.replace(".csv", ".bin")
try:
# Try the pip-installed version first
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")
if os.path.isfile(gen_script):
subprocess.check_call([
sys.executable, gen_script, "generate",
csv_path, bin_path, hex(size)
])
with open(bin_path, "rb") as f:
return f.read()
# 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):
if os.path.isfile(p):
os.unlink(p)
def flash_nvs(port, baud, nvs_bin):
"""Flash the NVS partition binary to the ESP32."""
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
f.write(nvs_bin)
bin_path = f.name
try:
cmd = [
sys.executable, "-m", "esptool",
"--chip", "esp32s3",
"--port", port,
"--baud", str(baud),
"write_flash",
hex(NVS_PARTITION_OFFSET), bin_path,
]
print(f"Flashing NVS partition ({len(nvs_bin)} bytes) to {port}...")
subprocess.check_call(cmd)
print("NVS provisioning complete!")
finally:
os.unlink(bin_path)
def main():
parser = argparse.ArgumentParser(
description="Provision ESP32-S3 CSI Node with WiFi and aggregator settings",
epilog="Example: python provision.py --port COM7 --ssid MyWiFi --password secret --target-ip 192.168.1.20",
)
parser.add_argument("--port", required=True, help="Serial port (e.g. COM7, /dev/ttyUSB0)")
parser.add_argument("--baud", type=int, default=460800, help="Flash baud rate (default: 460800)")
parser.add_argument("--ssid", help="WiFi SSID")
parser.add_argument("--password", help="WiFi password")
parser.add_argument("--target-ip", help="Aggregator host IP (e.g. 192.168.1.20)")
parser.add_argument("--target-port", type=int, help="Aggregator UDP port (default: 5005)")
parser.add_argument("--node-id", type=int, help="Node ID 0-255 (default: 1)")
parser.add_argument("--dry-run", action="store_true", help="Generate NVS binary but don't flash")
args = parser.parse_args()
if not any([args.ssid, args.password is not None, args.target_ip,
args.target_port, args.node_id is not None]):
parser.error("At least one config value must be specified "
"(--ssid, --password, --target-ip, --target-port, --node-id)")
print("Building NVS configuration:")
if args.ssid:
print(f" WiFi SSID: {args.ssid}")
if args.password is not None:
print(f" WiFi Password: {'*' * len(args.password)}")
if args.target_ip:
print(f" Target IP: {args.target_ip}")
if args.target_port:
print(f" Target Port: {args.target_port}")
if args.node_id is not None:
print(f" Node ID: {args.node_id}")
csv_content = build_nvs_csv(args.ssid, args.password, args.target_ip,
args.target_port, args.node_id)
try:
nvs_bin = generate_nvs_binary(csv_content, NVS_PARTITION_SIZE)
except Exception as e:
print(f"\nError generating NVS binary: {e}", file=sys.stderr)
print("\nFallback: save CSV and flash manually with ESP-IDF tools.", file=sys.stderr)
fallback_path = "nvs_config.csv"
with open(fallback_path, "w") as f:
f.write(csv_content)
print(f"Saved NVS CSV to {fallback_path}", file=sys.stderr)
print(f"Flash with: python $IDF_PATH/components/nvs_flash/"
f"nvs_partition_generator/nvs_partition_gen.py generate "
f"{fallback_path} nvs.bin 0x6000", file=sys.stderr)
sys.exit(1)
if args.dry_run:
out = "nvs_provision.bin"
with open(out, "wb") as f:
f.write(nvs_bin)
print(f"NVS binary saved to {out} ({len(nvs_bin)} bytes)")
print(f"Flash manually: python -m esptool --chip esp32s3 --port {args.port} "
f"write_flash 0x9000 {out}")
return
flash_nvs(args.port, args.baud, nvs_bin)
if __name__ == "__main__":
main()