160 lines
5.9 KiB
Markdown
160 lines
5.9 KiB
Markdown
# ADR-121 — HLK-LD2402 24 GHz mmWave Radar (auxiliary modality)
|
|
|
|
**Status**: Accepted (single-modality readout). Fusion deferred.
|
|
**Date**: 2026-05-18
|
|
**Scope**: `v2/crates/wifi-densepose-sensing-server/src/mmwave.rs` (new),
|
|
`Cargo.toml` (serialport dep), `main.rs` (CLI flags `--mmwave-port` /
|
|
`--mmwave-baud`, spawn reader, `mmwave_latest` REST handler, route),
|
|
`ui/components/SensingTab.js` (new card, poll integration).
|
|
|
|
## Context
|
|
|
|
The operator has an HLK-LD2402 24 GHz mmWave radar module attached via
|
|
a CP2102 USB-to-UART bridge. Factory firmware emits ASCII
|
|
`distance:<cm>\r\n` lines at 115200 8N1, ~6 Hz, in Normal Mode.
|
|
|
|
This module is a useful **auxiliary modality**: sub-mm range to a
|
|
moving target, very different physical principle than WiFi CSI, runs
|
|
fully independent. Concrete uses:
|
|
|
|
1. **Live readout in the UI** — easiest. Operator sees the radar
|
|
distance alongside the WiFi sensing.
|
|
2. **Vitals ground-truth** — at 6 Hz the data is too slow for HR but
|
|
captures breathing rate (0.2-0.4 Hz). Compare against the WiFi-CSI
|
|
vitals detector (ADR-021) for calibration.
|
|
3. **Multi-modal fusion** — feed the mmWave distance + WiFi features
|
|
into a future classifier. Different physics, very different
|
|
confusion set — high-value addition.
|
|
|
|
This ADR ships #1 only. #2 and #3 are follow-ups.
|
|
|
|
## Decisions
|
|
|
|
### D1 — Dedicated blocking reader thread, not async
|
|
|
|
`serialport` is a sync API. Wrapping it with `tokio::spawn_blocking`
|
|
adds overhead for a single-port reader running indefinitely. A
|
|
plain `std::thread` named `mmwave-reader` reads the port, parses
|
|
lines, and writes the latest reading into a global
|
|
`OnceLock<Mutex<Option<MmwaveReading>>>`.
|
|
|
|
### D2 — Graceful absence
|
|
|
|
`--mmwave-port` is **optional**. When unset, the server runs as
|
|
before. When set but the port can't be opened, the reader thread
|
|
logs a single warning and exits — server keeps running with WiFi
|
|
sensing only. No retries, no panics. (Operator can hot-plug; if
|
|
auto-reconnect is wanted we add it later.)
|
|
|
|
### D3 — Stale-after policy
|
|
|
|
`mmwave::current(staleness)` returns `None` if the most recent
|
|
reading is older than `staleness`. The REST endpoint uses 2 seconds
|
|
— at the module's 6 Hz cadence, 2 s = ~12 missed frames, plenty of
|
|
slack for a brief USB hiccup but tight enough to flag a dead module.
|
|
|
|
### D4 — Single new REST endpoint, no SensingUpdate change
|
|
|
|
`GET /api/v1/mmwave/latest` returns:
|
|
|
|
```json
|
|
{ "available": true, "distance_cm": 152, "age_ms": 90 }
|
|
```
|
|
|
|
or
|
|
|
|
```json
|
|
{ "available": false }
|
|
```
|
|
|
|
Not embedded in `SensingUpdate` because:
|
|
|
|
* The WS stream is already busy with per-tick CSI broadcasts; a
|
|
separate poll lets the UI throttle the mmWave refresh
|
|
independently (saves bandwidth if many clients connect).
|
|
* Keeps the SensingUpdate schema stable — older WS consumers don't
|
|
need a migration.
|
|
|
|
UI polls the endpoint once per visible WS tick. ~5-10 Hz refresh.
|
|
|
|
### D5 — UI badge in `SensingTab`, hidden when unavailable
|
|
|
|
New card "mmWave Radar (24 GHz)" with a blue badge showing
|
|
`<distance> cm` and an age bar (100 % at 0 ms → 0 % at 2 s). The
|
|
whole card hides via `display: none` when the endpoint reports
|
|
`available: false`, so deployments without the radar see no
|
|
clutter.
|
|
|
|
### D6 — Parse only the `distance:<n>` Normal Mode format
|
|
|
|
HLK-LD2402 also has an "Engineering Mode" emitting binary frames
|
|
with per-range-gate energy. Out of scope for v1 — Normal Mode
|
|
covers the live-readout use case. Engineering Mode parsing is a
|
|
separate ADR if/when we need per-gate data for vitals fusion.
|
|
|
|
## Files Touched
|
|
|
|
```
|
|
v2/crates/wifi-densepose-sensing-server/Cargo.toml
|
|
+ serialport.workspace = true
|
|
v2/crates/wifi-densepose-sensing-server/src/mmwave.rs (new, ~130 LoC)
|
|
+ pub struct MmwaveReading { distance_cm: u32, at: Instant }
|
|
+ static LATEST: OnceLock<Mutex<Option<MmwaveReading>>>
|
|
+ pub fn current(staleness) -> Option<MmwaveReading>
|
|
+ pub fn spawn_reader(port, baud)
|
|
+ fn parse_distance(line: &str) -> Option<u32>
|
|
+ 1 unit test
|
|
v2/crates/wifi-densepose-sensing-server/src/lib.rs
|
|
+ pub mod mmwave;
|
|
v2/crates/wifi-densepose-sensing-server/src/main.rs
|
|
+ Args { mmwave_port, mmwave_baud }
|
|
+ spawn_reader call in main()
|
|
+ async fn mmwave_latest
|
|
+ route /api/v1/mmwave/latest
|
|
ui/components/SensingTab.js
|
|
+ #mmwaveCard hidden-by-default card with #mmwaveLabel + age bar
|
|
+ fetch /api/v1/mmwave/latest each visible tick, show/hide card
|
|
docs/adr/ADR-121-mmwave-hlk-ld2402.md (this)
|
|
```
|
|
|
|
## Verified Acceptance
|
|
|
|
Live with the module attached:
|
|
|
|
```
|
|
$ ./target/release/sensing-server --mmwave-port /dev/cu.usbserial-1140 …
|
|
ADR-121 mmWave reader: opened /dev/cu.usbserial-1140 @ 115200
|
|
|
|
$ curl :8080/api/v1/mmwave/latest
|
|
{"age_ms":55,"available":true,"distance_cm":149}
|
|
{"age_ms":90,"available":true,"distance_cm":152}
|
|
{"age_ms":127,"available":true,"distance_cm":153}
|
|
```
|
|
|
|
Live without module attached (port arg omitted): server starts cleanly,
|
|
endpoint returns `{"available": false}`, Sensing tab card hidden.
|
|
|
|
## Out of Scope / Follow-ups
|
|
|
|
* **Engineering Mode binary parser** — needed if we want per-gate
|
|
energy for vitals (breathing band) or person-counting from
|
|
per-gate occupancy.
|
|
* **Vitals fusion (ADR-021 cross-check)** — log mmWave breathing
|
|
rate side-by-side with WiFi-CSI vitals for 5 min, compute
|
|
Pearson correlation, decide whether to weight one over the other
|
|
in the final vitals output.
|
|
* **W-MLP feature input** — once vitals fusion proves out, expose
|
|
mmWave distance as a 23rd feature in the W-MLP and retrain.
|
|
Would warrant ADR-122.
|
|
* **Auto-reconnect** — current behaviour: open fails or read errors
|
|
exit the reader thread. Add a retry loop with 2-second backoff
|
|
if the operator wants USB hot-plug recovery.
|
|
|
|
## References
|
|
|
|
* ADR-021 — WiFi-CSI vitals detector (the candidate cross-check
|
|
partner for HLK-LD2402 breathing-rate output).
|
|
* `assets/sensors/sensor_03.jpeg` / `_04.jpeg` / `_05.jpeg` —
|
|
hardware photos and inventory entry for the module + CP2102
|
|
bridge.
|