mirror of
https://github.com/ruvnet/RuView.git
synced 2026-04-28 05:59:32 +00:00
docs: clarify macOS live sensing setup
This commit is contained in:
parent
2a05378bd2
commit
95f2bd881e
4 changed files with 92 additions and 14 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -170,6 +170,7 @@ celerybeat.pid
|
|||
# Environments
|
||||
.env
|
||||
.venv
|
||||
.venv-*/
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
|
|
|
|||
|
|
@ -92,6 +92,8 @@ node scripts/mincut-person-counter.js --port 5006 # Correct person counting
|
|||
> | **Research NIC** | Intel 5300 / Atheros AR9580 | ~$50-100 | Yes | Full CSI with 3x3 MIMO |
|
||||
> | **Any WiFi** | Windows, macOS, or Linux laptop | $0 | No | RSSI-only: coarse presence and motion |
|
||||
>
|
||||
> **macOS host Wi-Fi note:** live macOS RSSI sensing currently uses the legacy CoreWLAN/WebSocket path (`python -m v1.src.sensing.ws_server`). The main Rust auto-detect server still falls back to simulation on macOS. See the [User Guide macOS section](docs/user-guide.md#macos-wifi-rssi-only).
|
||||
>
|
||||
> No hardware? Verify the signal processing pipeline with the deterministic reference signal: `python v1/data/proof/verify.py`
|
||||
>
|
||||
---
|
||||
|
|
|
|||
|
|
@ -261,16 +261,41 @@ docker run --network host ruvnet/wifi-densepose:latest --source wifi --tick-ms 5
|
|||
|
||||
### macOS WiFi (RSSI Only)
|
||||
|
||||
Uses CoreWLAN via a Swift helper binary. macOS Sonoma 14.4+ redacts real BSSIDs; the adapter generates deterministic synthetic MACs so the multi-BSSID pipeline still works.
|
||||
Uses CoreWLAN via the legacy `v1` Swift helper. macOS Sonoma 14.4+ redacts real BSSIDs; the adapter generates deterministic synthetic MACs so the multi-BSSID pipeline still works.
|
||||
|
||||
> **Current status:** the main Rust `sensing-server --source auto` path still falls back to simulation on macOS. For live host Wi-Fi sensing on macOS, use the legacy Python/WebSocket path below until the ADR-025 integration lands fully.
|
||||
|
||||
> **Homebrew Python note:** if `pip` reports an `externally-managed-environment` error (PEP 668), create a virtual environment first instead of installing packages into the system interpreter.
|
||||
|
||||
```bash
|
||||
# Compile the Swift helper (once)
|
||||
swiftc -O v1/src/sensing/mac_wifi.swift -o mac_wifi
|
||||
# Create a lightweight virtual environment that can still see the
|
||||
# Homebrew-installed numpy/scipy already present on the machine.
|
||||
python3 -m venv .venv-macos-sensing --system-site-packages
|
||||
source .venv-macos-sensing/bin/activate
|
||||
|
||||
# Run natively
|
||||
./target/release/sensing-server --source macos --http-port 3000 --ws-port 3001 --tick-ms 500
|
||||
# Install the only missing runtime dependency for the legacy server.
|
||||
python -m pip install websockets
|
||||
|
||||
# Compile the CoreWLAN helper (once). The explicit xcrun + module-cache path
|
||||
# avoids toolchain/module-cache issues seen on recent macOS + CLT setups.
|
||||
mkdir -p /tmp/swift-module-cache
|
||||
CLANG_MODULE_CACHE_PATH=/tmp/swift-module-cache \
|
||||
xcrun --sdk macosx swiftc -O -o v1/src/sensing/mac_wifi v1/src/sensing/mac_wifi.swift
|
||||
|
||||
# Optional sanity check: this should print JSON lines with rssi/noise/tx_rate.
|
||||
./v1/src/sensing/mac_wifi | head -n 3
|
||||
|
||||
# Start the live sensing WebSocket server.
|
||||
python -m v1.src.sensing.ws_server
|
||||
|
||||
# In another terminal, serve the UI static files.
|
||||
python3 -m http.server 3000 --directory ui
|
||||
```
|
||||
|
||||
Open `http://localhost:3000/observatory.html`, switch **Data Source** to **Live WebSocket**, and set **WS URL** to `ws://localhost:8765/ws/sensing`.
|
||||
|
||||
If the helper prints `{"error": "No WiFi interface found"}`, make sure the Mac is connected to Wi-Fi on `en0` and try again.
|
||||
|
||||
See [ADR-025](adr/ADR-025-macos-corewlan-wifi-sensing.md) for details.
|
||||
|
||||
### Linux WiFi (RSSI Only)
|
||||
|
|
@ -465,7 +490,9 @@ Real-time sensing data is available via WebSocket.
|
|||
|
||||
**URL:** `ws://localhost:3000/ws/sensing` (same port as HTTP — recommended) or `ws://localhost:3001/ws/sensing` (dedicated WS port).
|
||||
|
||||
> **Note:** The `/ws/sensing` WebSocket endpoint is available on both the HTTP port (3000) and the dedicated WebSocket port (3001/8765). The web UI uses the HTTP port so only one port needs to be exposed. The dedicated WS port remains available for backward compatibility.
|
||||
> **Legacy Python/macOS path:** when using `python -m v1.src.sensing.ws_server`, the WebSocket endpoint is `ws://localhost:8765/ws/sensing` and the UI is typically served separately on `http://localhost:3000/`.
|
||||
|
||||
> **Note:** The main Rust server exposes `/ws/sensing` on both the HTTP port (3000) and the dedicated WebSocket port (3001/8765). The web UI prefers the HTTP port when available and falls back to the legacy `:8765` endpoint for local macOS RSSI sensing.
|
||||
|
||||
### Python Example
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,32 @@
|
|||
/**
|
||||
* Sensing WebSocket Service
|
||||
*
|
||||
* Manages the connection to the Python sensing WebSocket server
|
||||
* (ws://localhost:8765) and provides a callback-based API for the UI.
|
||||
* Manages the connection to the sensing WebSocket server and provides a
|
||||
* callback-based API for the UI.
|
||||
*
|
||||
* Connection strategy:
|
||||
* 1. Same-origin /ws/sensing (Rust server / reverse-proxy path)
|
||||
* 2. localhost:8765/ws/sensing (legacy Python/macOS path)
|
||||
*
|
||||
* Falls back to simulated data only after MAX_RECONNECT_ATTEMPTS exhausted.
|
||||
* While reconnecting the service stays in "reconnecting" state and does NOT
|
||||
* emit simulated frames so the UI can clearly distinguish live vs. fallback data.
|
||||
*/
|
||||
|
||||
// Derive WebSocket URL from the page origin so it works on any port.
|
||||
// The /ws/sensing endpoint is available on the same HTTP port (3000).
|
||||
// Derive candidate WebSocket URLs from the current origin.
|
||||
const _wsProto = (typeof window !== 'undefined' && window.location.protocol === 'https:') ? 'wss:' : 'ws:';
|
||||
const _wsHost = (typeof window !== 'undefined' && window.location.host) ? window.location.host : 'localhost:3000';
|
||||
const SENSING_WS_URL = `${_wsProto}//${_wsHost}/ws/sensing`;
|
||||
const _wsHostname = (typeof window !== 'undefined' && window.location.hostname) ? window.location.hostname : 'localhost';
|
||||
|
||||
function buildSensingWsUrls() {
|
||||
const candidates = [
|
||||
`${_wsProto}//${_wsHost}/ws/sensing`,
|
||||
`${_wsProto}//${_wsHostname}:8765/ws/sensing`,
|
||||
];
|
||||
return [...new Set(candidates)];
|
||||
}
|
||||
|
||||
const SENSING_WS_URLS = buildSensingWsUrls();
|
||||
const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000];
|
||||
const MAX_RECONNECT_ATTEMPTS = 20;
|
||||
// Number of failed attempts that must occur before simulation starts.
|
||||
|
|
@ -41,6 +54,10 @@ class SensingService {
|
|||
// The raw source string from the server (e.g. "esp32", "simulated", "simulate")
|
||||
this._serverSource = null;
|
||||
this._lastMessage = null;
|
||||
this._wsCandidates = SENSING_WS_URLS;
|
||||
this._candidateIndex = 0;
|
||||
this._successfulWsUrl = null;
|
||||
this._activeWsUrl = null;
|
||||
|
||||
// Ring buffer of recent RSSI values for sparkline
|
||||
this._rssiHistory = [];
|
||||
|
|
@ -110,9 +127,11 @@ class SensingService {
|
|||
if (this._ws && this._ws.readyState <= WebSocket.OPEN) return;
|
||||
|
||||
this._setState('connecting');
|
||||
const wsUrl = this._successfulWsUrl || this._wsCandidates[this._candidateIndex] || `${_wsProto}//${_wsHost}/ws/sensing`;
|
||||
this._activeWsUrl = wsUrl;
|
||||
|
||||
try {
|
||||
this._ws = new WebSocket(SENSING_WS_URL);
|
||||
this._ws = new WebSocket(wsUrl);
|
||||
} catch (err) {
|
||||
console.warn('[Sensing] WebSocket constructor failed:', err.message);
|
||||
this._fallbackToSimulation();
|
||||
|
|
@ -120,8 +139,10 @@ class SensingService {
|
|||
}
|
||||
|
||||
this._ws.onopen = () => {
|
||||
console.info('[Sensing] Connected to', SENSING_WS_URL);
|
||||
console.info('[Sensing] Connected to', this._activeWsUrl);
|
||||
this._reconnectAttempt = 0;
|
||||
this._successfulWsUrl = this._activeWsUrl;
|
||||
this._candidateIndex = Math.max(0, this._wsCandidates.indexOf(this._successfulWsUrl));
|
||||
this._stopSimulation();
|
||||
this._setState('connected');
|
||||
// Don't assume "live" yet — wait for first frame's source field.
|
||||
|
|
@ -146,6 +167,9 @@ class SensingService {
|
|||
console.info('[Sensing] Connection closed (code=%d)', evt.code);
|
||||
this._ws = null;
|
||||
if (evt.code !== 1000) {
|
||||
if (!this._successfulWsUrl && this._wsCandidates.length > 1) {
|
||||
this._candidateIndex = (this._candidateIndex + 1) % this._wsCandidates.length;
|
||||
}
|
||||
this._scheduleReconnect();
|
||||
} else {
|
||||
this._setState('disconnected');
|
||||
|
|
@ -276,8 +300,14 @@ class SensingService {
|
|||
* hardware or simulation. Called once on WebSocket open.
|
||||
*/
|
||||
async _detectServerSource() {
|
||||
const statusUrl = this._statusUrlForActiveSocket();
|
||||
if (!statusUrl) {
|
||||
this._setDataSource('live');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/v1/status');
|
||||
const resp = await fetch(statusUrl);
|
||||
if (resp.ok) {
|
||||
const json = await resp.json();
|
||||
this._applyServerSource(json.source);
|
||||
|
|
@ -290,6 +320,24 @@ class SensingService {
|
|||
}
|
||||
}
|
||||
|
||||
_statusUrlForActiveSocket() {
|
||||
if (!this._activeWsUrl || typeof window === 'undefined') {
|
||||
return '/api/v1/status';
|
||||
}
|
||||
|
||||
try {
|
||||
const wsUrl = new URL(this._activeWsUrl, window.location.href);
|
||||
if (wsUrl.port === '8765') {
|
||||
// The legacy Python/macOS sensing path exposes only WebSocket frames.
|
||||
return null;
|
||||
}
|
||||
const httpProto = wsUrl.protocol === 'wss:' ? 'https:' : 'http:';
|
||||
return `${httpProto}//${wsUrl.host}/api/v1/status`;
|
||||
} catch {
|
||||
return '/api/v1/status';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a raw server source string to the UI data-source label.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue