Skip to content

Commit 87ce7bd

Browse files
committed
feat: add authenticated ESP32 BLE fusion path
1 parent 0df48df commit 87ce7bd

34 files changed

Lines changed: 8318 additions & 32 deletions

docs/adr/ADR-341-authenticated-ble-and-channel-sounding-fusion.md

Lines changed: 394 additions & 0 deletions
Large diffs are not rendered by default.

firmware/esp32-csi-node/README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,9 @@ All packets are sent over UDP to the configured aggregator. The magic number in
203203
| `0xC5110001` | CSI Frame (ADR-018) | ~20 Hz | Variable | Raw I/Q per subcarrier per antenna |
204204
| `0xC5110002` | Vitals Packet | 1 Hz | 32 bytes | Presence, breathing BPM, heart rate, fall flag, occupancy |
205205
| `0xC5110004` | WASM Output | Event-driven | Variable | Custom events from WASM modules (u8 type + f32 value) |
206+
| `RVAE` (`0x45415652`) | Authenticated radio envelope v1 | Bounded worker | 92 or 128 bytes | Gateway-authenticated wrapper sent over UDP |
207+
| `0xC51100B1` | BLE Identity Evidence v1 | Bounded passive scan | 36 bytes inner payload | Rotating pseudonym, RSSI, TTL and evidence quality; never sent bare |
208+
| `RVCS` (`0x53435652`) | External Channel Sounding v1 | Companion-defined | 72 bytes inner payload | HMAC-authenticated phase and RTT primitives; never sent bare |
206209

207210
### ADR-018 Binary Frame Format
208211

@@ -238,6 +241,92 @@ Offset Size Field
238241
28 4 Reserved
239242
```
240243

244+
### Optional BLE and Bluetooth 6 companion path (ADR-341)
245+
246+
Both paths are disabled by default. ESP32-S3 can scan ordinary BLE advertising
247+
metadata and RSSI, but this firmware does **not** claim that the S3 exposes raw
248+
CTE IQ or native Bluetooth 6 Channel Sounding.
249+
250+
The BLE path accepts only the RuView vendor service token authenticated with a
251+
provisioned 32-byte HMAC key. It discards the advertiser address and raw packet,
252+
then forwards a rotating eight-byte pseudonym with an explicit TTL. Ordinary
253+
iPhone background advertisements do not satisfy this contract and are not an
254+
identity source.
255+
256+
The Channel Sounding path uses a separate capable radio on UART2. Its fixed v1
257+
frame carries sample age, timing uncertainty, phase, RTT, frequency offset,
258+
quality, source session, procedure metadata, sequence, a domain-separated
259+
128-bit HMAC tag and CRC32. The S3 validates these primitives. Both radio paths
260+
then enter a bounded queue and a second HMAC-protected gateway envelope carrying
261+
the node, random boot nonce, gateway sequence and receive time. Respiration
262+
inference and motion abstention happen on the host. See
263+
[`ADR-341`](../../docs/adr/ADR-341-authenticated-ble-and-channel-sounding-fusion.md)
264+
for exact layouts and the rvCSI mapping.
265+
266+
To compile the BLE scanner, first enable ESP-IDF Bluetooth, NimBLE, the observer
267+
role, `CONFIG_BT_NIMBLE_EXT_SCAN=y`, `CONFIG_BT_NIMBLE_EXT_ADV=y`, and
268+
`CONFIG_BT_NIMBLE_TRANSPORT_EVT_SIZE=257`, then set
269+
`CONFIG_BLE_IDENTITY_SCAN_ENABLE=y`. The 50-byte token requires extended
270+
advertising and cannot fit in a legacy advertisement or scan response. The
271+
advertiser should keep its complete advertising data at or below 200 bytes;
272+
incomplete or truncated reports are rejected rather than authenticating a
273+
fragment. The
274+
default scan window is 50 ms per 1000 ms, or 5 percent duty. Firmware refuses
275+
settings above 25 percent. To compile the companion ingress on ESP32-S3, set
276+
`CONFIG_CHANNEL_SOUNDING_INGRESS_ENABLE=y` and verify the UART and GPIO choices
277+
against the specific board.
278+
279+
Runtime activation requires separate secrets and remains fail closed:
280+
281+
```bash
282+
python firmware/esp32-csi-node/provision.py --port COM7 \
283+
--ssid "YourSSID" --password "YourPass" --target-ip 192.168.1.20 \
284+
--ble-identity-enable 1 --ble-key-id 7 --ble-secret-file ble-key.bin \
285+
--cs-ingress-enable 1 --cs-key-id 9 --cs-source-id 270544960 \
286+
--cs-secret-file cs-key.bin --radio-envelope-key-id 12 \
287+
--radio-envelope-secret-file gateway-key.bin
288+
```
289+
290+
Each of the three independent key files contains exactly 32 raw bytes or 64
291+
hexadecimal characters. Key
292+
contents are written to NVS but are never printed or persisted in the local
293+
additive provisioning-state JSON. Production devices also require secure boot,
294+
flash encryption and NVS encryption. Re-supply the secret files on every later
295+
provisioning run while either feature remains enabled. Provisioning fails closed
296+
instead of writing a fallback CSV when any secret is present.
297+
298+
The sensing server requires a fourth independent 32-byte host pseudonym key.
299+
On the first boot only, explicitly create the replay snapshot:
300+
301+
```bash
302+
RUVIEW_API_TOKEN="replace-with-a-long-local-token" \
303+
cargo run -p wifi-densepose-sensing-server -- --source auto \
304+
--radio-gateway-node-id 7 --radio-gateway-key-id 12 \
305+
--radio-gateway-secret-file gateway-key.bin \
306+
--radio-host-pseudonym-secret-file host-pseudonym-key.bin \
307+
--radio-replay-state data/radio-replay-v2.json \
308+
--radio-initialize-replay-state \
309+
--radio-cs-key-id 9 --radio-cs-source-id 270544960 \
310+
--radio-cs-secret-file cs-key.bin
311+
```
312+
313+
Omit `--radio-initialize-replay-state` on every subsequent boot. If an
314+
established replay snapshot is lost, rotate all gateway, advertiser, and
315+
companion keys before creating a replacement. Add independent gateways with a
316+
repeatable `--radio-gateway NODE,KEY,SECRET_PATH` argument. P4 respiration and
317+
P5 pseudonymous anchor WebSocket exports remain closed by default. Their local
318+
overrides require loopback binding, configured bearer or OAuth authentication,
319+
and a private audit log. The override is deployment authorization, not a
320+
subject consent receipt.
321+
322+
The gateway envelope authenticates integrity and source but does not encrypt
323+
UDP. Use WireGuard, DTLS, or an equivalent confidential transport if observers
324+
on the LAN must not see rotating pseudonyms or Channel Sounding primitives.
325+
326+
The included C and Rust replays are **SYNTHETIC**. They are not evidence that a
327+
specific board, companion, room, respiration rate or identity-association
328+
accuracy has been validated.
329+
241330
---
242331

243332
## Building

firmware/esp32-csi-node/main/CMakeLists.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ set(SRCS
33
"edge_processing.c" "ota_update.c" "power_mgmt.c"
44
"wasm_runtime.c" "wasm_upload.c" "rvf_parser.c"
55
"mmwave_sensor.c"
6+
# ADR-341 — privacy-minimized BLE anchors + external BT6 CS ingress
7+
"ble_identity_protocol.c"
8+
"ble_identity.c"
9+
"channel_sounding_protocol.c"
10+
"channel_sounding_ingress.c"
11+
"radio_gateway_protocol.c"
12+
"radio_gateway_sender.c"
613
"swarm_bridge.c"
714
# ADR-081 — adaptive CSI mesh firmware kernel
815
"rv_radio_ops_esp32.c"
@@ -38,6 +45,10 @@ set(REQUIRES
3845
driver
3946
lwip
4047
mbedtls
48+
# The BLE translation unit compiles to stubs when disabled. Keep the
49+
# component dependency explicit so a feature-enabled clean configure gets
50+
# the public NimBLE headers during component discovery.
51+
bt
4152
)
4253

4354
# ADR-110: C6-only components — pulled in when building for esp32c6.

firmware/esp32-csi-node/main/Kconfig.projbuild

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,159 @@ menu "Edge Intelligence (ADR-039)"
8787

8888
endmenu
8989

90+
menu "BLE identity anchors and external Channel Sounding (ADR-341)"
91+
92+
config RADIO_GATEWAY_QUEUE_DEPTH
93+
int "Authenticated radio-envelope queue depth"
94+
default 16
95+
range 4 64
96+
help
97+
Fixed nonblocking queue between radio callbacks and HMAC plus UDP
98+
egress. A full queue drops evidence instead of blocking NimBLE or
99+
UART ingest. Runtime radio_key_id and an exact 32-byte
100+
radio_secret are required whenever either radio path is enabled.
101+
102+
config BLE_IDENTITY_SCAN_ENABLE
103+
bool "Enable authenticated RuView BLE identity-token scanning"
104+
default n
105+
depends on BT_ENABLED && BT_NIMBLE_ENABLED && BT_NIMBLE_ROLE_OBSERVER
106+
depends on BT_NIMBLE_EXT_SCAN
107+
depends on BT_NIMBLE_EXT_ADV
108+
depends on BT_NIMBLE_TRANSPORT_EVT_SIZE >= 257
109+
help
110+
Passive extended scanning for the RuView 128-bit service-data
111+
token. The 50-byte token cannot fit in a legacy advertising or
112+
scan-response payload. This does not inspect arbitrary phone
113+
advertisements, expose BLE MAC addresses, acquire CTE IQ, or infer
114+
vital signs. Runtime NVS key ble_enable and a 32-byte ble_secret
115+
are also required, so a binary compiled with this option still
116+
fails closed until provisioned. A 257-byte NimBLE transport event
117+
buffer is required so the 50-byte service record plus the extended
118+
report header arrives without controller truncation. ESP-IDF 5.4
119+
also gates the extended-report event structure behind
120+
BT_NIMBLE_EXT_ADV, so both extended scan and advertising support
121+
must be compiled even though this node never advertises.
122+
123+
config BLE_IDENTITY_SCAN_INTERVAL_MS
124+
int "BLE scan interval (ms)"
125+
default 1000
126+
range 100 10000
127+
depends on BLE_IDENTITY_SCAN_ENABLE
128+
help
129+
Controller scan interval. The scan window below must remain at or
130+
below one quarter of this value to bound WiFi/BLE coexistence cost.
131+
132+
config BLE_IDENTITY_SCAN_WINDOW_MS
133+
int "BLE scan window (ms)"
134+
default 50
135+
range 5 250
136+
depends on BLE_IDENTITY_SCAN_ENABLE
137+
help
138+
Passive scan window. Firmware rejects configurations above a 25
139+
percent duty ceiling. Default is 5 percent.
140+
141+
config BLE_IDENTITY_TTL_MS
142+
int "Forwarded BLE anchor TTL (ms)"
143+
default 3000
144+
range 250 5000
145+
depends on BLE_IDENTITY_SCAN_ENABLE
146+
help
147+
Maximum host association lifetime. Expired anchors must abstain.
148+
149+
config BLE_IDENTITY_TOKEN_SKEW_MIN
150+
int "Authenticated token clock-skew allowance (minutes)"
151+
default 2
152+
range 0 10
153+
depends on BLE_IDENTITY_SCAN_ENABLE
154+
155+
config BLE_IDENTITY_MIN_CSI_PPS
156+
int "Warn below this CSI callback rate when BLE starts"
157+
default 5
158+
range 0 100
159+
depends on BLE_IDENTITY_SCAN_ENABLE
160+
help
161+
Coexistence diagnostic only. Operators should disable BLE if the
162+
deployment's measured CSI yield regresses.
163+
164+
config BLE_IDENTITY_MAX_REPORTS_PER_SEC
165+
int "Maximum BLE token reports admitted per second"
166+
default 40
167+
range 1 200
168+
depends on BLE_IDENTITY_SCAN_ENABLE
169+
help
170+
Global bound before token HMAC and enqueue. Excess reports are
171+
dropped and counted, limiting valid-token or replay floods.
172+
173+
config CHANNEL_SOUNDING_INGRESS_ENABLE
174+
bool "Enable external Bluetooth 6 Channel Sounding UART ingress"
175+
default n
176+
depends on IDF_TARGET_ESP32S3
177+
help
178+
Accept calibrated phase and timing primitives from a separate
179+
Channel Sounding-capable radio. ESP32-S3 does not acquire these
180+
primitives itself. The gateway validates framing, CRC, bounds,
181+
age, quality, session, procedure and sequence, then places the
182+
exact primitive in an authenticated gateway envelope.
183+
184+
config CHANNEL_SOUNDING_UART_NUM
185+
int "Companion UART controller"
186+
default 2
187+
range 1 2
188+
depends on CHANNEL_SOUNDING_INGRESS_ENABLE
189+
help
190+
UART1 is used by the optional mmWave probe. UART2 is the default on
191+
ESP32-S3. Confirm the board pinout before enabling.
192+
193+
config CHANNEL_SOUNDING_UART_BAUD
194+
int "Companion UART baud"
195+
default 921600
196+
range 115200 2000000
197+
depends on CHANNEL_SOUNDING_INGRESS_ENABLE
198+
199+
config CHANNEL_SOUNDING_UART_TX_GPIO
200+
int "Companion UART TX GPIO"
201+
default 15
202+
range 0 48
203+
depends on CHANNEL_SOUNDING_INGRESS_ENABLE
204+
205+
config CHANNEL_SOUNDING_UART_RX_GPIO
206+
int "Companion UART RX GPIO"
207+
default 16
208+
range 0 48
209+
depends on CHANNEL_SOUNDING_INGRESS_ENABLE
210+
211+
config CHANNEL_SOUNDING_MAX_AGE_MS
212+
int "Maximum companion measurement age (ms)"
213+
default 2000
214+
range 50 10000
215+
depends on CHANNEL_SOUNDING_INGRESS_ENABLE
216+
217+
config CHANNEL_SOUNDING_MIN_QUALITY_PERMILLE
218+
int "Minimum companion quality (per mille)"
219+
default 600
220+
range 1 1000
221+
depends on CHANNEL_SOUNDING_INGRESS_ENABLE
222+
223+
config CHANNEL_SOUNDING_MAX_FRAMES_PER_SEC
224+
int "Maximum companion frames admitted per second"
225+
default 100
226+
range 1 500
227+
depends on CHANNEL_SOUNDING_INGRESS_ENABLE
228+
229+
config CHANNEL_SOUNDING_SESSION_RETIRE_MS
230+
int "Retire oldest companion replay session after inactivity (ms)"
231+
default 600000
232+
range 10000 86400000
233+
depends on CHANNEL_SOUNDING_INGRESS_ENABLE
234+
help
235+
The gateway keeps eight recent authenticated companion sessions.
236+
When the table is full, only a sequence-one frame may replace the
237+
oldest session after this inactivity horizon. The host remains the
238+
durable replay authority and must checkpoint all retired session
239+
high-water marks.
240+
241+
endmenu
242+
90243
menu "Adaptive Controller (ADR-081)"
91244

92245
config ADAPTIVE_FAST_LOOP_MS

0 commit comments

Comments
 (0)