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
102 changes: 102 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,108 @@ asyncio.run(stream())

</details>

<details>
<summary><strong>Running on macOS</strong> — Live WiFi sensing with CoreWLAN (no ESP32 needed)</summary>

macOS can capture real RSSI, noise floor, and TX rate from your Mac's WiFi hardware via CoreWLAN. A Swift helper + Python bridge repackages these readings as ESP32-format CSI frames, feeding them to the sensing server over UDP.

### Prerequisites

- **macOS** 10.15+ (Catalina or later)
- **Rust** 1.70+ (`curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`)
- **Python 3** (pre-installed on macOS)
- **Xcode Command Line Tools** (`xcode-select --install`)
- **WiFi Location Services** must be enabled (System Settings → Privacy & Security → Location Services → enable for Terminal)

### Step 1: Build the Rust sensing server

```bash
cd rust-port/wifi-densepose-rs
cargo build --release -p wifi-densepose-sensing-server
```

### Step 2: Compile the macOS WiFi helper

```bash
swiftc -O -framework CoreWLAN -framework Foundation \
v1/src/sensing/mac_wifi.swift -o mac_wifi
```

This produces a `mac_wifi` binary that reads RSSI, noise, and TX rate from your Mac's WiFi interface at ~100 Hz and outputs JSON lines.

### Step 3: Start the sensing server

```bash
cd rust-port/wifi-densepose-rs
cargo run --release -p wifi-densepose-sensing-server
```

The server starts in **auto** mode: it listens on UDP port 5005 for live data and falls back to simulation if nothing arrives within 2 seconds. Once running you'll see:

```
HTTP server listening on 0.0.0.0:8080
WebSocket server listening on 0.0.0.0:8765
UDP listening on 0.0.0.0:5005 for ESP32 CSI frames
```

### Step 4: Start the WiFi bridge (separate terminal)

```bash
python3 scripts/macos_wifi_bridge.py --mac-wifi ./mac_wifi --port 5005
```

The bridge captures live WiFi readings and sends them to the sensing server. You should see:

```
[bridge] Starting mac_wifi helper: ./mac_wifi
[bridge] Sending ESP32 frames to 127.0.0.1:5005
[bridge] # 1 RSSI= -30 dBm noise= -72 dBm tx_rate= 144.0 Mbps frame=132 bytes
```

The sensing server **hot-plugs** automatically — it switches from simulation to live data as soon as UDP frames arrive.

### Step 5: Open the UI

Open http://localhost:8080/ui/index.html in your browser.

### One-command startup

To start everything at once from the repo root:

```bash
bash run_all.sh &
sleep 10 # wait for build + server startup
python3 scripts/macos_wifi_bridge.py --mac-wifi ./mac_wifi --port 5005
```

### What macOS WiFi provides vs ESP32

| Capability | macOS CoreWLAN | ESP32-S3 CSI |
|------------|---------------|--------------|
| RSSI (signal strength) | Yes | Yes |
| Noise floor | Yes | Yes |
| TX rate | Yes | Yes |
| Per-subcarrier amplitude | No | Yes (56-192 subcarriers) |
| Per-subcarrier phase | No | Yes |
| Pose estimation | No (RSSI-only) | Yes (full CSI) |
| Breathing detection | Coarse (RSSI variance) | Yes (sub-Hz phase) |
| Heart rate | No | Yes (micro-Doppler) |
| Presence / motion | Yes | Yes |

macOS provides RSSI-level sensing — good for presence detection, coarse motion, and environment monitoring. For full pose estimation and vital signs, ESP32-S3 hardware is required.

### Troubleshooting

| Issue | Fix |
|-------|-----|
| `No WiFi interface found` | Enable WiFi; grant Location Services permission to Terminal |
| Server stays in simulation mode | Start the bridge *after* the server is listening on port 5005 |
| `Permission denied` on `mac_wifi` | Run `chmod +x mac_wifi` |
| Bridge shows no output | Ensure `mac_wifi` binary exists and is compiled for your architecture (`file mac_wifi`) |
| Port 8080 already in use | Kill previous server: `lsof -ti:8080 \| xargs kill` |

</details>

---

## 📋 Table of Contents
Expand Down
Binary file added mac_wifi
Binary file not shown.
16 changes: 16 additions & 0 deletions run_all.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -e

# Build the sensing server (wifi-densepose-api is a lib stub, no binary)
cd rust-port/wifi-densepose-rs
cargo build --release -p wifi-densepose-sensing-server

# Start sensing server (serves REST + WebSocket + UI)
cargo run --release -p wifi-densepose-sensing-server &
SENSE_PID=$!

echo "Sensing server PID=$SENSE_PID"
echo "Open: http://localhost:8080/ui/index.html"

# Keep shell alive
wait $SENSE_PID
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,9 @@ struct AppStateInner {
training_status: String,
/// Training configuration, if any.
training_config: Option<serde_json::Value>,
/// Set to true when real ESP32/bridge frames arrive on UDP — simulation task
/// will stop generating data so the real source takes over.
real_data_active: bool,
}

/// Number of frames retained in `frame_history` for temporal analysis.
Expand Down Expand Up @@ -2482,6 +2485,10 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
frame.node_id, frame.n_subcarriers, frame.sequence);

let mut s = state.write().await;
if !s.real_data_active {
info!("Real ESP32/bridge data detected — overriding simulation");
s.real_data_active = true;
}
s.source = "esp32".to_string();

// Append current amplitudes to history before extracting features so
Expand Down Expand Up @@ -2582,6 +2589,14 @@ async fn simulated_data_task(state: SharedState, tick_ms: u64) {
loop {
interval.tick().await;

// Yield to real data when available
{
let s = state.read().await;
if s.real_data_active {
continue;
}
}

let mut s = state.write().await;
s.tick += 1;
let tick = s.tick;
Expand Down Expand Up @@ -3278,6 +3293,7 @@ async fn main() {
// Training
training_status: "idle".to_string(),
training_config: None,
real_data_active: false,
}));

// Start background tasks based on source
Expand All @@ -3290,7 +3306,11 @@ async fn main() {
tokio::spawn(windows_wifi_task(state.clone(), args.tick_ms));
}
_ => {
// Run simulation as fallback, but ALSO listen on UDP so that if
// real ESP32/bridge frames arrive later they override simulated data.
tokio::spawn(simulated_data_task(state.clone(), args.tick_ms));
tokio::spawn(udp_receiver_task(state.clone(), args.udp_port));
info!("UDP listener also started — real frames will override simulation");
}
}

Expand Down
148 changes: 148 additions & 0 deletions scripts/macos_wifi_bridge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""
macOS WiFi → UDP bridge for the WiFi-DensePose sensing server.
Reads real RSSI/noise/tx_rate from the compiled mac_wifi Swift helper and
packs each reading into the ESP32 binary frame format expected by the
sensing server's UDP listener on port 5005.
The server auto-detects these frames and switches from simulation to live
WiFi data (hot-plug).
Usage:
python3 scripts/macos_wifi_bridge.py [--mac-wifi ./mac_wifi] [--port 5005]
"""

import argparse
import json
import math
import socket
import struct
import subprocess
import sys
import time


MAGIC = 0xC511_0001
NODE_ID = 1
N_ANTENNAS = 1
N_SUBCARRIERS = 56 # match simulated frame size

UDP_HOST = "127.0.0.1"


def build_esp32_frame(seq: int, rssi: float, noise: float, tx_rate: float) -> bytes:
"""Pack a WiFi reading into the binary ESP32 frame format.
Layout (little-endian):
[0:4] u32 magic 0xC5110001
[4] u8 node_id
[5] u8 n_antennas
[6] u8 n_subcarriers
[7] u8 (reserved)
[8:10] u16 freq_mhz
[10:14] u32 sequence
[14] i8 rssi
[15] i8 noise_floor
[16:20] (reserved / padding)
[20..] i8 pairs (I, Q) × n_antennas × n_subcarriers
We synthesize per-subcarrier I/Q values from the RSSI + noise so the
server's feature extractor has plausible amplitude/phase distributions.
"""
rssi_i8 = max(-128, min(127, int(rssi)))
noise_i8 = max(-128, min(127, int(noise)))

# Derive a base amplitude from RSSI (higher RSSI → larger amplitude)
snr = max(rssi - noise, 1.0)
base_amp = snr / 2.0 # scale into a reasonable I/Q range

# 20-byte header matching parse_esp32_frame() layout exactly:
# [0:4] u32 LE magic, [4] node_id, [5] n_antennas, [6] n_subcarriers,
# [7] reserved, [8:10] u16 LE freq_mhz, [10:14] u32 LE sequence,
# [14] i8 rssi, [15] i8 noise_floor, [16:20] reserved
header = struct.pack(
"<IBBBBHIbb4x",
MAGIC,
NODE_ID,
N_ANTENNAS,
N_SUBCARRIERS,
0, # reserved
2437, # freq_mhz (channel 6, 2.4 GHz)
seq,
rssi_i8,
noise_i8,
)

t = time.time()
rate_factor = tx_rate / 400.0 if tx_rate > 0 else 0.5

iq_data = bytearray()
for i in range(N_SUBCARRIERS):
phase = math.sin(i * 0.2 + t * 0.5) * math.pi
amp = base_amp * (0.8 + 0.4 * math.sin(i * 0.15 + t * 0.3)) * rate_factor
amp = max(1.0, min(127.0, amp))
i_val = int(amp * math.cos(phase))
q_val = int(amp * math.sin(phase))
i_val = max(-128, min(127, i_val))
q_val = max(-128, min(127, q_val))
iq_data.append(i_val & 0xFF)
iq_data.append(q_val & 0xFF)

return header + bytes(iq_data)


def main():
parser = argparse.ArgumentParser(description="macOS WiFi → sensing server bridge")
parser.add_argument("--mac-wifi", default="./mac_wifi", help="Path to mac_wifi binary")
parser.add_argument("--port", type=int, default=5005, help="UDP port for sensing server")
args = parser.parse_args()

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
print(f"[bridge] Starting mac_wifi helper: {args.mac_wifi}")
print(f"[bridge] Sending ESP32 frames to {UDP_HOST}:{args.port}")

proc = subprocess.Popen(
[args.mac_wifi],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
env={**__import__("os").environ, "NSUnbufferedIO": "YES"},
)

sys.stdout.reconfigure(line_buffering=True) if hasattr(sys.stdout, "reconfigure") else None

seq = 0
try:
for line in proc.stdout:
line = line.strip()
if not line or not line.startswith("{"):
continue
try:
data = json.loads(line)
except json.JSONDecodeError:
continue

rssi = data.get("rssi", -70)
noise = data.get("noise", -90)
tx_rate = data.get("tx_rate", 0.0)
seq += 1

frame = build_esp32_frame(seq, rssi, noise, tx_rate)
sock.sendto(frame, (UDP_HOST, args.port))

if seq % 10 == 1:
print(f"[bridge] #{seq:>5d} RSSI={rssi:>4d} dBm noise={noise:>4d} dBm "
f"tx_rate={tx_rate:>6.1f} Mbps frame={len(frame)} bytes")

except KeyboardInterrupt:
print("\n[bridge] Stopped.")
finally:
proc.terminate()
proc.wait()
sock.close()


if __name__ == "__main__":
main()
Loading