wifi-densepose/scripts/macos-rssi-bridge/README.md

6.3 KiB
Raw Blame History

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 515 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 (line ~134, "Any WiFi: RSSI-only") and ADR-022.

Build

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:

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):

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:

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):

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)
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 (~35 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 (35 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
  • Makefilebuild, 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)