156 lines
6.3 KiB
Markdown
156 lines
6.3 KiB
Markdown
# macos-rssi-bridge
|
||
|
||
Mac WiFi card → RuView sensing-server bridge. Lets you do RSSI-based motion
|
||
sensing on a stock Mac with **no CSI hardware** while you wait for ESP32-S3
|
||
boards to arrive (or as a permanent extra "node" once they do).
|
||
|
||
## What it does
|
||
|
||
1. A small Swift helper (`mac_wifi`) wraps Apple's CoreWLAN framework and
|
||
prints one JSON line per visible BSSID, ~once per second. It replaces the
|
||
single-AP v1 helper at `archive/v1/src/sensing/mac_wifi.swift` with the
|
||
multi-BSSID format that `v2/.../macos_scanner.rs` expects.
|
||
2. A Rust binary (`macos-rssi-bridge`) spawns the helper, maintains a rolling
|
||
per-AP RSSI history, and packs each scan into an ESP32 CSI frame
|
||
(`magic = 0xC511_0001`) emitted over UDP — the same wire format
|
||
`wifi-densepose-sensing-server` expects from real ESP32 nodes.
|
||
|
||
The sensing-server pipeline runs unmodified. RSSI per AP becomes a
|
||
pseudo-subcarrier amplitude; per-AP variance becomes the Q channel.
|
||
|
||
## Why it works
|
||
|
||
Every AP in range beacons every ~100 ms. When a person moves, multipath
|
||
reflections shift and RSSI fluctuates. With 5–15 visible APs (your router +
|
||
neighbors'), running variance and cross-AP correlation gives a real motion
|
||
signal — coarser than CSI but useful for through-wall presence and
|
||
room-level activity. See [`README.md`](../../README.md) (line ~134, "Any WiFi:
|
||
RSSI-only") and [ADR-022](../../docs/adr/ADR-022-multi-bssid-scanning.md).
|
||
|
||
## Build
|
||
|
||
```bash
|
||
make build # compiles mac_wifi (Swift) + macos-rssi-bridge (Rust)
|
||
make scan # smoke test: print JSON for one scan
|
||
make test # unit tests (frame builder, variance, RSSI mapping)
|
||
```
|
||
|
||
Toolchain: `swiftc` 6.0+ and Cargo 1.80+. Tested on macOS 26 (Tahoe) arm64.
|
||
|
||
## Run
|
||
|
||
One command — builds everything, launches the full three-process pipeline,
|
||
opens both UIs, and cleans up on Ctrl-C:
|
||
|
||
```bash
|
||
make start # → sensing-server (:8080) + bridge (:9090/dashboard) + presence injector
|
||
make stop # kill any leftover process from the trio
|
||
```
|
||
|
||
The pipeline is three processes:
|
||
|
||
1. **sensing-server** — UDP receiver, motion stats, WebSocket stream, UI
|
||
(pinned to `--source esp32` so it binds UDP and consumes the bridge's
|
||
frames instead of falling back to its simulation generator).
|
||
2. **macos-rssi-bridge** — Swift scan → Rust UDP emitter → ESP32 frame format.
|
||
3. **presence_to_pose.py** — polls the bridge's `/aps` endpoint and POSTs a
|
||
single honest pose to `/api/v1/pose/external` so the Observatory renders
|
||
one figure modulated by RSSI variance instead of the placeholder
|
||
five-skeleton fallback.
|
||
|
||
Or run each manually if you want logs side-by-side. In one terminal, the
|
||
sensing-server (UI + UDP receiver):
|
||
|
||
```bash
|
||
cd ../../v2 && cargo run --release -p wifi-densepose-sensing-server --no-default-features -- --source esp32
|
||
# UI: http://localhost:8080
|
||
# WS: ws://localhost:8765/ws/sensing
|
||
```
|
||
|
||
In another, the bridge:
|
||
|
||
```bash
|
||
make run # default: target 127.0.0.1:5005, 1.5 s scan interval
|
||
# or: make run-verbose for per-frame stats on stderr
|
||
```
|
||
|
||
In a third, the presence injector (only needed for the 3D Observatory pose):
|
||
|
||
```bash
|
||
make run-presence # polls :9090/aps, posts a pose to :8080/api/v1/pose/external
|
||
```
|
||
|
||
Open http://localhost:8080 and walk around. Motion should register within a
|
||
few seconds (the pipeline needs ~5 frames of baseline before it'll classify).
|
||
|
||
## Tuning
|
||
|
||
| Variable | Default | Notes |
|
||
|---------------|--------------|-------------------------------------------------------|
|
||
| `TARGET_HOST` | `127.0.0.1` | Where the sensing-server is listening |
|
||
| `TARGET_PORT` | `5005` | UDP port — matches `--udp-port` on sensing-server |
|
||
| `INTERVAL` | `1.5` | Seconds between active scans (helper enforces 0.5 floor) |
|
||
|
||
```bash
|
||
TARGET_HOST=192.168.1.50 TARGET_PORT=5005 make run
|
||
```
|
||
|
||
## macOS Location Services note
|
||
|
||
macOS Sonoma 14.4+ redacts BSSIDs to `00:00:00:00:00:00` and SSIDs to empty
|
||
strings unless the calling process holds the `com.apple.wifi.scan` entitlement
|
||
or has been granted Location Services authorization. The bridge handles the
|
||
redacted case automatically — the Swift helper synthesizes stable per-AP
|
||
identifiers from `(channel, RSSI bucket, ordinal)` so downstream code still
|
||
sees distinct virtual APs.
|
||
|
||
To get **real** SSIDs and BSSIDs (better tracking quality across scans):
|
||
|
||
1. **System Settings → Privacy & Security → Location Services → On**
|
||
2. Scroll to your terminal app (Terminal.app, iTerm2, Cursor, etc.) and toggle it on.
|
||
3. Re-run `make scan` — SSIDs and BSSIDs should now be populated.
|
||
|
||
This is optional. Sensing works without it; it's just less stable across scans
|
||
because the synthetic ordinal can shuffle.
|
||
|
||
## Limits vs. real CSI
|
||
|
||
| Capability | RSSI bridge | ESP32-S3 CSI |
|
||
|-------------------------------|-------------|--------------|
|
||
| Coarse presence / motion | Yes (~3–5 m) | Yes (~5 m) |
|
||
| Through-wall detection | Limited | Yes |
|
||
| Breathing rate | No (~0.5 Hz frame rate is too slow) | Yes (real-time) |
|
||
| Heart rate | No | Yes |
|
||
| 17-keypoint pose | No | Yes (with trained model) |
|
||
| Fall detection | Coarse | Yes (<200 ms) |
|
||
| Multi-person counting | No | Yes (3–5 per AP) |
|
||
|
||
Treat the bridge as a "node 0" you keep around for environmental fingerprinting
|
||
and motion baseline, alongside the real ESP32 mesh.
|
||
|
||
## Files
|
||
|
||
- `mac_wifi.swift` — CoreWLAN multi-BSSID scanner, JSON-lines output
|
||
- `src/main.rs` — Rust UDP emitter (ESP32 frame format `0xC511_0001`)
|
||
- `Cargo.toml` — standalone Cargo project; not a workspace member, so it
|
||
doesn't perturb the v2 build
|
||
- `Makefile` — `build`, `scan`, `run`, `listen`, `test`, `clean`
|
||
|
||
## Wire format
|
||
|
||
Reproduced from `v2/crates/wifi-densepose-sensing-server/src/csi.rs`:
|
||
|
||
```
|
||
bytes 0..4 u32 magic = 0xC511_0001 (LE)
|
||
byte 4 u8 node_id
|
||
byte 5 u8 n_antennas = 1
|
||
byte 6 u8 n_subcarriers = 56
|
||
byte 7 _ skipped
|
||
bytes 8..10 u16 freq_mhz (LE)
|
||
bytes 10..14 u32 sequence (LE)
|
||
byte 14 i8 rssi (strongest AP)
|
||
byte 15 i8 noise_floor (mean across APs)
|
||
bytes 16..20 _ header padding (iq_start = 20)
|
||
bytes 20.. i8 I/Q pairs (n_antennas * n_subcarriers, I=amp, Q=variance)
|
||
```
|