|
| 1 | +# ADR-044: Provisioning Tool Enhancements |
| 2 | + |
| 3 | +**Status**: Proposed |
| 4 | +**Date**: 2026-03-03 |
| 5 | +**Deciders**: @ruvnet |
| 6 | +**Supersedes**: None |
| 7 | +**Related**: ADR-029, ADR-032, ADR-039, ADR-040 |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## Context |
| 12 | + |
| 13 | +The ESP32-S3 CSI node provisioning script (`firmware/esp32-csi-node/provision.py`) is the primary tool for configuring pre-built firmware binaries without recompiling. It writes NVS key-value pairs that the firmware reads at boot. |
| 14 | + |
| 15 | +After #131 added TDM and edge intelligence flags, the script now covers the most-requested NVS keys. However, there remain gaps between what the firmware reads from NVS (`nvs_config.c`, 20 keys) and what the provisioning script can write (13 keys). Additionally, the script lacks usability features that would help field operators deploying multi-node meshes. |
| 16 | + |
| 17 | +### Gap 1: Missing NVS Keys (7 keys) |
| 18 | + |
| 19 | +The firmware reads these NVS keys at boot but the provisioning script has no corresponding CLI flags: |
| 20 | + |
| 21 | +| NVS Key | Type | Firmware Default | Purpose | |
| 22 | +|---------|------|-----------------|---------| |
| 23 | +| `hop_count` | u8 | 1 (no hop) | Number of channels to hop through | |
| 24 | +| `chan_list` | blob (u8[6]) | {1,6,11} | Channel numbers for hopping sequence | |
| 25 | +| `dwell_ms` | u32 | 100 | Time to dwell on each channel before hopping (ms) | |
| 26 | +| `power_duty` | u8 | 100 | Power duty cycle percentage (10-100%) for battery life | |
| 27 | +| `wasm_max` | u8 | 4 | Max concurrent WASM modules (ADR-040) | |
| 28 | +| `wasm_verify` | u8 | 0 | Require Ed25519 signature for WASM uploads (0/1) | |
| 29 | +| `wasm_pubkey` | blob (32B) | zeros | Ed25519 public key for WASM signature verification | |
| 30 | + |
| 31 | +### Gap 2: No Read-Back |
| 32 | + |
| 33 | +There is no way to read the current NVS configuration from a device. Field operators must remember what was provisioned or reflash everything. This is especially problematic for multi-node meshes where each node has different TDM slots. |
| 34 | + |
| 35 | +### Gap 3: No Verification |
| 36 | + |
| 37 | +After flashing, there is no automated check that the device booted successfully with the new configuration. Operators must manually run a serial monitor and inspect logs. |
| 38 | + |
| 39 | +### Gap 4: No Config File Support |
| 40 | + |
| 41 | +Provisioning a 6-node mesh requires running the script 6 times with largely overlapping flags (same SSID, password, target IP) and only TDM slot varying. There is no way to define a mesh configuration in a file. |
| 42 | + |
| 43 | +### Gap 5: No Presets |
| 44 | + |
| 45 | +Common deployment scenarios (single-node basic, 3-node mesh, 6-node mesh with vitals) require operators to know which flags to combine. Named presets would lower the barrier to entry. |
| 46 | + |
| 47 | +### Gap 6: No Auto-Detect |
| 48 | + |
| 49 | +The `--port` flag is required even though the script could auto-detect connected ESP32-S3 devices via `esptool.py`. |
| 50 | + |
| 51 | +--- |
| 52 | + |
| 53 | +## Decision |
| 54 | + |
| 55 | +Enhance `provision.py` with the following capabilities, implemented incrementally. |
| 56 | + |
| 57 | +### Phase 1: Complete NVS Coverage |
| 58 | + |
| 59 | +Add flags for all remaining firmware NVS keys: |
| 60 | + |
| 61 | +``` |
| 62 | +--hop-count N Channel hop count (1=no hop, default: 1) |
| 63 | +--channels 1,6,11 Comma-separated channel list for hopping |
| 64 | +--dwell-ms N Dwell time per channel in ms (default: 100) |
| 65 | +--power-duty N Power duty cycle 10-100% (default: 100) |
| 66 | +--wasm-max N Max concurrent WASM modules 1-8 (default: 4) |
| 67 | +--wasm-verify Require Ed25519 signature for WASM uploads |
| 68 | +--wasm-pubkey FILE Path to Ed25519 public key file (32 bytes raw or PEM) |
| 69 | +``` |
| 70 | + |
| 71 | +Validation: |
| 72 | +- `--channels` length must match `--hop-count` |
| 73 | +- `--power-duty` clamped to 10-100 |
| 74 | +- `--wasm-pubkey` implies `--wasm-verify` |
| 75 | + |
| 76 | +### Phase 2: Config File and Mesh Provisioning |
| 77 | + |
| 78 | +Add `--config FILE` to load settings from a JSON or TOML file: |
| 79 | + |
| 80 | +```json |
| 81 | +{ |
| 82 | + "common": { |
| 83 | + "ssid": "SensorNet", |
| 84 | + "password": "secret", |
| 85 | + "target_ip": "192.168.1.20", |
| 86 | + "target_port": 5005, |
| 87 | + "edge_tier": 2 |
| 88 | + }, |
| 89 | + "nodes": [ |
| 90 | + { "port": "COM7", "node_id": 0, "tdm_slot": 0 }, |
| 91 | + { "port": "COM8", "node_id": 1, "tdm_slot": 1 }, |
| 92 | + { "port": "COM9", "node_id": 2, "tdm_slot": 2 } |
| 93 | + ] |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +`--config mesh.json` provisions all listed nodes in sequence, computing `tdm_total` automatically from the `nodes` array length. |
| 98 | + |
| 99 | +### Phase 3: Presets |
| 100 | + |
| 101 | +Add `--preset NAME` for common deployment profiles: |
| 102 | + |
| 103 | +| Preset | What It Sets | |
| 104 | +|--------|-------------| |
| 105 | +| `basic` | Single node, edge_tier=0, no TDM, no hopping | |
| 106 | +| `vitals` | Single node, edge_tier=2, vital_int=1000, subk_count=32 | |
| 107 | +| `mesh-3` | 3-node TDM, edge_tier=1, hop_count=3, channels=1,6,11 | |
| 108 | +| `mesh-6-vitals` | 6-node TDM, edge_tier=2, hop_count=3, channels=1,6,11, vital_int=500 | |
| 109 | + |
| 110 | +Presets set defaults that can be overridden by explicit flags. |
| 111 | + |
| 112 | +### Phase 4: Read-Back and Verify |
| 113 | + |
| 114 | +Add `--read` to dump the current NVS configuration from a connected device: |
| 115 | + |
| 116 | +```bash |
| 117 | +python provision.py --port COM7 --read |
| 118 | +# Output: |
| 119 | +# ssid: SensorNet |
| 120 | +# target_ip: 192.168.1.20 |
| 121 | +# tdm_slot: 0 |
| 122 | +# tdm_nodes: 3 |
| 123 | +# edge_tier: 2 |
| 124 | +# ... |
| 125 | +``` |
| 126 | + |
| 127 | +Implementation: use `esptool.py read_flash` to read the NVS partition, then parse the NVS binary format to extract key-value pairs. |
| 128 | + |
| 129 | +Add `--verify` to provision and then confirm the device booted: |
| 130 | + |
| 131 | +```bash |
| 132 | +python provision.py --port COM7 --ssid "Net" --password "pass" --target-ip 192.168.1.20 --verify |
| 133 | +# After flash, opens serial monitor for 5 seconds |
| 134 | +# Checks for "CSI streaming active" log line |
| 135 | +# Reports PASS or FAIL |
| 136 | +``` |
| 137 | + |
| 138 | +### Phase 5: Auto-Detect Port |
| 139 | + |
| 140 | +When `--port` is omitted, scan for connected ESP32-S3 devices: |
| 141 | + |
| 142 | +```bash |
| 143 | +python provision.py --ssid "Net" --password "pass" --target-ip 192.168.1.20 |
| 144 | +# Auto-detected ESP32-S3 on COM7 (Silicon Labs CP210x) |
| 145 | +# Proceed? [Y/n] |
| 146 | +``` |
| 147 | + |
| 148 | +Implementation: use `esptool.py` or `serial.tools.list_ports` to enumerate ports. |
| 149 | + |
| 150 | +--- |
| 151 | + |
| 152 | +## Rationale |
| 153 | + |
| 154 | +### Why incremental phases? |
| 155 | + |
| 156 | +Phase 1 is a small diff that closes the NVS coverage gap immediately. Phases 2-5 add progressively more UX polish. Each phase is independently useful and can be shipped separately. |
| 157 | + |
| 158 | +### Why JSON config over YAML/TOML? |
| 159 | + |
| 160 | +JSON requires no additional Python dependencies (stdlib `json` module). TOML requires `tomllib` (Python 3.11+) or `tomli`. JSON is sufficient for this use case. |
| 161 | + |
| 162 | +### Why not a GUI? |
| 163 | + |
| 164 | +The target users are embedded developers and field operators who are already running `esptool` from the command line. A TUI/GUI would add dependencies and complexity for minimal benefit. |
| 165 | + |
| 166 | +--- |
| 167 | + |
| 168 | +## Consequences |
| 169 | + |
| 170 | +### Positive |
| 171 | + |
| 172 | +- **Complete NVS coverage**: Every firmware-readable key can be set from the provisioning tool |
| 173 | +- **Mesh provisioning in one command**: `--config mesh.json` replaces 6 separate invocations |
| 174 | +- **Lower barrier to entry**: Presets eliminate the need to know which flags to combine |
| 175 | +- **Auditability**: `--read` lets operators inspect and verify deployed configurations |
| 176 | +- **Fewer mis-provisions**: `--verify` catches flashing failures before the operator walks away |
| 177 | + |
| 178 | +### Negative |
| 179 | + |
| 180 | +- **NVS binary parsing** (Phase 4) requires understanding the ESP-IDF NVS binary format, which is not officially documented as a stable API |
| 181 | +- **Auto-detect** (Phase 5) may produce false positives if other ESP32 variants are connected |
| 182 | + |
| 183 | +### Risks |
| 184 | + |
| 185 | +| Risk | Likelihood | Impact | Mitigation | |
| 186 | +|------|-----------|--------|------------| |
| 187 | +| NVS binary format changes in ESP-IDF v6 | Low | Medium | Pin to known ESP-IDF NVS page format; add format version check | |
| 188 | +| `--verify` serial parsing is fragile | Medium | Low | Match on stable log tag `[CSI_MAIN]`; timeout after 10s | |
| 189 | +| Config file credentials in plaintext | Medium | Medium | Document that config files should not be committed; add `.gitignore` pattern | |
| 190 | + |
| 191 | +--- |
| 192 | + |
| 193 | +## Implementation Priority |
| 194 | + |
| 195 | +| Phase | Effort | Impact | Priority | |
| 196 | +|-------|--------|--------|----------| |
| 197 | +| Phase 1: Complete NVS coverage | Small (1 file, ~50 lines) | High — closes feature gap | P0 | |
| 198 | +| Phase 2: Config file + mesh | Medium (~100 lines) | High — biggest UX win | P1 | |
| 199 | +| Phase 3: Presets | Small (~40 lines) | Medium — convenience | P2 | |
| 200 | +| Phase 4: Read-back + verify | Medium (~150 lines) | Medium — debugging aid | P2 | |
| 201 | +| Phase 5: Auto-detect | Small (~30 lines) | Low — minor convenience | P3 | |
| 202 | + |
| 203 | +--- |
| 204 | + |
| 205 | +## References |
| 206 | + |
| 207 | +- `firmware/esp32-csi-node/main/nvs_config.h` — NVS config struct (20 fields) |
| 208 | +- `firmware/esp32-csi-node/main/nvs_config.c` — NVS read logic (20 keys) |
| 209 | +- `firmware/esp32-csi-node/provision.py` — Current provisioning script (13 of 20 keys) |
| 210 | +- ADR-029: RuvSense multistatic sensing mode (TDM, channel hopping) |
| 211 | +- ADR-032: Multistatic mesh security hardening (mesh keys) |
| 212 | +- ADR-039: ESP32-S3 edge intelligence (edge tiers, vitals) |
| 213 | +- ADR-040: WASM programmable sensing (WASM modules, signature verification) |
| 214 | +- Issue #130: Provisioning script doesn't support TDM |
0 commit comments