Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
394 changes: 394 additions & 0 deletions docs/adr/ADR-341-authenticated-ble-and-channel-sounding-fusion.md

Large diffs are not rendered by default.

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

### ADR-018 Binary Frame Format

Expand Down Expand Up @@ -238,6 +241,92 @@ Offset Size Field
28 4 Reserved
```

### Optional BLE and Bluetooth 6 companion path (ADR-341)

Both paths are disabled by default. ESP32-S3 can scan ordinary BLE advertising
metadata and RSSI, but this firmware does **not** claim that the S3 exposes raw
CTE IQ or native Bluetooth 6 Channel Sounding.

The BLE path accepts only the RuView vendor service token authenticated with a
provisioned 32-byte HMAC key. It discards the advertiser address and raw packet,
then forwards a rotating eight-byte pseudonym with an explicit TTL. Ordinary
iPhone background advertisements do not satisfy this contract and are not an
identity source.

The Channel Sounding path uses a separate capable radio on UART2. Its fixed v1
frame carries sample age, timing uncertainty, phase, RTT, frequency offset,
quality, source session, procedure metadata, sequence, a domain-separated
128-bit HMAC tag and CRC32. The S3 validates these primitives. Both radio paths
then enter a bounded queue and a second HMAC-protected gateway envelope carrying
the node, random boot nonce, gateway sequence and receive time. Respiration
inference and motion abstention happen on the host. See
[`ADR-341`](../../docs/adr/ADR-341-authenticated-ble-and-channel-sounding-fusion.md)
for exact layouts and the rvCSI mapping.

To compile the BLE scanner, first enable ESP-IDF Bluetooth, NimBLE, the observer
role, `CONFIG_BT_NIMBLE_EXT_SCAN=y`, `CONFIG_BT_NIMBLE_EXT_ADV=y`, and
`CONFIG_BT_NIMBLE_TRANSPORT_EVT_SIZE=257`, then set
`CONFIG_BLE_IDENTITY_SCAN_ENABLE=y`. The 50-byte token requires extended
advertising and cannot fit in a legacy advertisement or scan response. The
advertiser should keep its complete advertising data at or below 200 bytes;
incomplete or truncated reports are rejected rather than authenticating a
fragment. The
default scan window is 50 ms per 1000 ms, or 5 percent duty. Firmware refuses
settings above 25 percent. To compile the companion ingress on ESP32-S3, set
`CONFIG_CHANNEL_SOUNDING_INGRESS_ENABLE=y` and verify the UART and GPIO choices
against the specific board.

Runtime activation requires separate secrets and remains fail closed:

```bash
python firmware/esp32-csi-node/provision.py --port COM7 \
--ssid "YourSSID" --password "YourPass" --target-ip 192.168.1.20 \
--ble-identity-enable 1 --ble-key-id 7 --ble-secret-file ble-key.bin \
--cs-ingress-enable 1 --cs-key-id 9 --cs-source-id 270544960 \
--cs-secret-file cs-key.bin --radio-envelope-key-id 12 \
--radio-envelope-secret-file gateway-key.bin
```

Each of the three independent key files contains exactly 32 raw bytes or 64
hexadecimal characters. Key
contents are written to NVS but are never printed or persisted in the local
additive provisioning-state JSON. Production devices also require secure boot,
flash encryption and NVS encryption. Re-supply the secret files on every later
provisioning run while either feature remains enabled. Provisioning fails closed
instead of writing a fallback CSV when any secret is present.

The sensing server requires a fourth independent 32-byte host pseudonym key.
On the first boot only, explicitly create the replay snapshot:

```bash
RUVIEW_API_TOKEN="replace-with-a-long-local-token" \
cargo run -p wifi-densepose-sensing-server -- --source auto \
--radio-gateway-node-id 7 --radio-gateway-key-id 12 \
--radio-gateway-secret-file gateway-key.bin \
--radio-host-pseudonym-secret-file host-pseudonym-key.bin \
--radio-replay-state data/radio-replay-v2.json \
--radio-initialize-replay-state \
--radio-cs-key-id 9 --radio-cs-source-id 270544960 \
--radio-cs-secret-file cs-key.bin
```

Omit `--radio-initialize-replay-state` on every subsequent boot. If an
established replay snapshot is lost, rotate all gateway, advertiser, and
companion keys before creating a replacement. Add independent gateways with a
repeatable `--radio-gateway NODE,KEY,SECRET_PATH` argument. P4 respiration and
P5 pseudonymous anchor WebSocket exports remain closed by default. Their local
overrides require loopback binding, configured bearer or OAuth authentication,
and a private audit log. The override is deployment authorization, not a
subject consent receipt.

The gateway envelope authenticates integrity and source but does not encrypt
UDP. Use WireGuard, DTLS, or an equivalent confidential transport if observers
on the LAN must not see rotating pseudonyms or Channel Sounding primitives.

The included C and Rust replays are **SYNTHETIC**. They are not evidence that a
specific board, companion, room, respiration rate or identity-association
accuracy has been validated.

---

## Building
Expand Down
11 changes: 11 additions & 0 deletions firmware/esp32-csi-node/main/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ set(SRCS
"edge_processing.c" "ota_update.c" "power_mgmt.c"
"wasm_runtime.c" "wasm_upload.c" "rvf_parser.c"
"mmwave_sensor.c"
# ADR-341 — privacy-minimized BLE anchors + external BT6 CS ingress
"ble_identity_protocol.c"
"ble_identity.c"
"channel_sounding_protocol.c"
"channel_sounding_ingress.c"
"radio_gateway_protocol.c"
"radio_gateway_sender.c"
"swarm_bridge.c"
# ADR-081 — adaptive CSI mesh firmware kernel
"rv_radio_ops_esp32.c"
Expand Down Expand Up @@ -38,6 +45,10 @@ set(REQUIRES
driver
lwip
mbedtls
# The BLE translation unit compiles to stubs when disabled. Keep the
# component dependency explicit so a feature-enabled clean configure gets
# the public NimBLE headers during component discovery.
bt
)

# ADR-110: C6-only components — pulled in when building for esp32c6.
Expand Down
153 changes: 153 additions & 0 deletions firmware/esp32-csi-node/main/Kconfig.projbuild
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,159 @@ menu "Edge Intelligence (ADR-039)"

endmenu

menu "BLE identity anchors and external Channel Sounding (ADR-341)"

config RADIO_GATEWAY_QUEUE_DEPTH
int "Authenticated radio-envelope queue depth"
default 16
range 4 64
help
Fixed nonblocking queue between radio callbacks and HMAC plus UDP
egress. A full queue drops evidence instead of blocking NimBLE or
UART ingest. Runtime radio_key_id and an exact 32-byte
radio_secret are required whenever either radio path is enabled.

config BLE_IDENTITY_SCAN_ENABLE
bool "Enable authenticated RuView BLE identity-token scanning"
default n
depends on BT_ENABLED && BT_NIMBLE_ENABLED && BT_NIMBLE_ROLE_OBSERVER
depends on BT_NIMBLE_EXT_SCAN
depends on BT_NIMBLE_EXT_ADV
depends on BT_NIMBLE_TRANSPORT_EVT_SIZE >= 257
help
Passive extended scanning for the RuView 128-bit service-data
token. The 50-byte token cannot fit in a legacy advertising or
scan-response payload. This does not inspect arbitrary phone
advertisements, expose BLE MAC addresses, acquire CTE IQ, or infer
vital signs. Runtime NVS key ble_enable and a 32-byte ble_secret
are also required, so a binary compiled with this option still
fails closed until provisioned. A 257-byte NimBLE transport event
buffer is required so the 50-byte service record plus the extended
report header arrives without controller truncation. ESP-IDF 5.4
also gates the extended-report event structure behind
BT_NIMBLE_EXT_ADV, so both extended scan and advertising support
must be compiled even though this node never advertises.

config BLE_IDENTITY_SCAN_INTERVAL_MS
int "BLE scan interval (ms)"
default 1000
range 100 10000
depends on BLE_IDENTITY_SCAN_ENABLE
help
Controller scan interval. The scan window below must remain at or
below one quarter of this value to bound WiFi/BLE coexistence cost.

config BLE_IDENTITY_SCAN_WINDOW_MS
int "BLE scan window (ms)"
default 50
range 5 250
depends on BLE_IDENTITY_SCAN_ENABLE
help
Passive scan window. Firmware rejects configurations above a 25
percent duty ceiling. Default is 5 percent.

config BLE_IDENTITY_TTL_MS
int "Forwarded BLE anchor TTL (ms)"
default 3000
range 250 5000
depends on BLE_IDENTITY_SCAN_ENABLE
help
Maximum host association lifetime. Expired anchors must abstain.

config BLE_IDENTITY_TOKEN_SKEW_MIN
int "Authenticated token clock-skew allowance (minutes)"
default 2
range 0 10
depends on BLE_IDENTITY_SCAN_ENABLE

config BLE_IDENTITY_MIN_CSI_PPS
int "Warn below this CSI callback rate when BLE starts"
default 5
range 0 100
depends on BLE_IDENTITY_SCAN_ENABLE
help
Coexistence diagnostic only. Operators should disable BLE if the
deployment's measured CSI yield regresses.

config BLE_IDENTITY_MAX_REPORTS_PER_SEC
int "Maximum BLE token reports admitted per second"
default 40
range 1 200
depends on BLE_IDENTITY_SCAN_ENABLE
help
Global bound before token HMAC and enqueue. Excess reports are
dropped and counted, limiting valid-token or replay floods.

config CHANNEL_SOUNDING_INGRESS_ENABLE
bool "Enable external Bluetooth 6 Channel Sounding UART ingress"
default n
depends on IDF_TARGET_ESP32S3
help
Accept calibrated phase and timing primitives from a separate
Channel Sounding-capable radio. ESP32-S3 does not acquire these
primitives itself. The gateway validates framing, CRC, bounds,
age, quality, session, procedure and sequence, then places the
exact primitive in an authenticated gateway envelope.

config CHANNEL_SOUNDING_UART_NUM
int "Companion UART controller"
default 2
range 1 2
depends on CHANNEL_SOUNDING_INGRESS_ENABLE
help
UART1 is used by the optional mmWave probe. UART2 is the default on
ESP32-S3. Confirm the board pinout before enabling.

config CHANNEL_SOUNDING_UART_BAUD
int "Companion UART baud"
default 921600
range 115200 2000000
depends on CHANNEL_SOUNDING_INGRESS_ENABLE

config CHANNEL_SOUNDING_UART_TX_GPIO
int "Companion UART TX GPIO"
default 15
range 0 48
depends on CHANNEL_SOUNDING_INGRESS_ENABLE

config CHANNEL_SOUNDING_UART_RX_GPIO
int "Companion UART RX GPIO"
default 16
range 0 48
depends on CHANNEL_SOUNDING_INGRESS_ENABLE

config CHANNEL_SOUNDING_MAX_AGE_MS
int "Maximum companion measurement age (ms)"
default 2000
range 50 10000
depends on CHANNEL_SOUNDING_INGRESS_ENABLE

config CHANNEL_SOUNDING_MIN_QUALITY_PERMILLE
int "Minimum companion quality (per mille)"
default 600
range 1 1000
depends on CHANNEL_SOUNDING_INGRESS_ENABLE

config CHANNEL_SOUNDING_MAX_FRAMES_PER_SEC
int "Maximum companion frames admitted per second"
default 100
range 1 500
depends on CHANNEL_SOUNDING_INGRESS_ENABLE

config CHANNEL_SOUNDING_SESSION_RETIRE_MS
int "Retire oldest companion replay session after inactivity (ms)"
default 600000
range 10000 86400000
depends on CHANNEL_SOUNDING_INGRESS_ENABLE
help
The gateway keeps eight recent authenticated companion sessions.
When the table is full, only a sequence-one frame may replace the
oldest session after this inactivity horizon. The host remains the
durable replay authority and must checkpoint all retired session
high-water marks.

endmenu

menu "Adaptive Controller (ADR-081)"

config ADAPTIVE_FAST_LOOP_MS
Expand Down
Loading
Loading