mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-18 21:00:22 -05:00
Integrate reviewed source PRs #2201-#2223, #2225-#2231, #2235-#2240, #2242 and #2243 (firmware README hunks remain in #2247). Regenerate one package manifest after integration, validate native Windows test execution, bound MTK listener startup, support official C6 release bundles, and retain stronger release metadata gates.
182 lines
7.1 KiB
Markdown
182 lines
7.1 KiB
Markdown
# wifi-densepose
|
||
|
||
[](https://pypi.org/project/wifi-densepose/)
|
||
[](https://pypi.org/project/wifi-densepose/)
|
||
[](https://opensource.org/licenses/MIT)
|
||
|
||
**Detect human presence, count people, read breathing and heart rate, and
|
||
estimate skeletal pose — using only the WiFi signal already in your home.**
|
||
|
||
No cameras. No wearables. Works through walls and in the dark.
|
||
|
||
`wifi-densepose` is the Python binding for the [RuView](https://github.com/ruvnet/RuView)
|
||
sensing stack: a Rust core that turns the Channel State Information (CSI)
|
||
emitted by ordinary WiFi chips into ambient-intelligence signals. The wheel
|
||
ships compiled DSP for fast offline analysis, plus an opt-in Python client
|
||
for talking to a live RuView sensing-server over WebSocket or MQTT.
|
||
|
||
## Features
|
||
|
||
- **17-keypoint pose** — full-body skeletal estimate from WiFi CSI, no camera
|
||
- **Vital signs** — respiratory rate (6–30 BPM) and heart rate (40–120 BPM)
|
||
with a confidence score and clinical-grade / degraded / unreliable status
|
||
- **Presence, person count, fall detection, motion** — fused outputs from
|
||
the same CSI stream
|
||
- **10 semantic primitives** (HA-MIND) — someone-sleeping, possible-distress,
|
||
room-active, bathroom-occupied, fall-risk-elevated, bed-exit, … — ready
|
||
to wire into Home Assistant or Apple Home automations
|
||
- **Beamforming Feedback (BFLD) support** — 802.11ac/ax/be compressed feedback
|
||
matrices on top of the receiver-side CSI path
|
||
- **GIL-releasing DSP** — extract loops run with the GIL released, so a
|
||
tokio-backed web server can call into the pipeline without stalling its
|
||
event loop
|
||
- **Tiny wheel** — ~240 KB compiled (one binary per OS/arch covers Python
|
||
3.10+ via the stable ABI)
|
||
|
||
## Install
|
||
|
||
```bash
|
||
pip install wifi-densepose # core DSP only
|
||
pip install "wifi-densepose[client]" # + WebSocket/MQTT clients
|
||
```
|
||
|
||
Wheels are published for Linux (x86_64, aarch64), macOS (x86_64, arm64), and
|
||
Windows (amd64).
|
||
|
||
### SOTA extras (ADR-185)
|
||
|
||
Three optional subsystems bind the Rust SOTA modules as compiled-feature
|
||
wheels. Each raises a clear `ImportError` if you import it without the extra:
|
||
|
||
| Extra | Module | What it adds |
|
||
|-------|--------|--------------|
|
||
| `[aether]` | `wifi_densepose.aether` | Contrastive CSI embeddings / re-identification (ADR-024) — `EmbeddingExtractor`, `cosine_similarity`, `info_nce_loss` |
|
||
| `[meridian]` | `wifi_densepose.meridian` | Cross-environment domain generalization (ADR-027) — `HardwareNormalizer`, `GeometryEncoder`, `RapidAdaptation`, `CrossDomainEvaluator` |
|
||
| `[mat]` | `wifi_densepose.mat` | Mass-Casualty Assessment disaster-survivor detection + START triage — `DisasterResponse`, `Survivor`, `TriageStatus` |
|
||
| `[sota]` | all three | Convenience superset |
|
||
|
||
```bash
|
||
pip install "wifi-densepose[aether]" # re-identification embeddings
|
||
pip install "wifi-densepose[meridian]" # cross-room calibration
|
||
pip install "wifi-densepose[mat]" # disaster triage
|
||
pip install "wifi-densepose[sota]" # all three
|
||
```
|
||
|
||
Runnable examples: [`examples/reid_from_csi.py`](examples/reid_from_csi.py),
|
||
[`examples/cross_room_calibrate.py`](examples/cross_room_calibrate.py),
|
||
[`examples/mat_triage.py`](examples/mat_triage.py).
|
||
|
||
## Usage
|
||
|
||
### Extract breathing rate from a CSI stream
|
||
|
||
```python
|
||
from wifi_densepose import BreathingExtractor
|
||
|
||
br = BreathingExtractor.esp32_default() # 56 subcarriers @ 100 Hz, 30s window
|
||
|
||
for residuals, weights in your_csi_source: # one frame at a time
|
||
est = br.extract(residuals=residuals, weights=weights)
|
||
if est is not None:
|
||
print(f"{est.value_bpm:.1f} BPM (confidence={est.confidence:.2f})")
|
||
```
|
||
|
||
For heart rate, use `HeartRateExtractor.esp32_default()` with a 0.8–2.0 Hz
|
||
bandpass and a 15-second window. Its second argument is `phases`, not the
|
||
breathing extractor's `weights`:
|
||
|
||
```python
|
||
hr = HeartRateExtractor.esp32_default()
|
||
est = hr.extract(residuals=residuals, phases=phases)
|
||
# If phase measurements are unavailable, phases=[] uses equal weighting.
|
||
```
|
||
|
||
The current binding rejects the old `weights` keyword with `TypeError`.
|
||
Heart-rate estimation requires a local autocorrelation peak; a declining
|
||
breathing-only correlation is not treated as a pulse at the band edge.
|
||
Additional filtering reduces respiratory leakage, while confidence remains
|
||
capped by evidence from the original filter stage. Synthetic regression tests
|
||
cover these cases; they do not establish accuracy on real CSI or medical use.
|
||
|
||
### Subscribe to a live sensing-server
|
||
|
||
```python
|
||
import asyncio
|
||
from wifi_densepose.client import SensingClient, EdgeVitalsMessage
|
||
|
||
async def main():
|
||
async with SensingClient("ws://your-ruview-node:8765/ws/sensing") as c:
|
||
async for msg in c.stream():
|
||
if isinstance(msg, EdgeVitalsMessage):
|
||
print(msg.presence, msg.breathing_rate_bpm, msg.heartrate_bpm)
|
||
|
||
asyncio.run(main())
|
||
```
|
||
|
||
### React to Home Assistant semantic primitives
|
||
|
||
```python
|
||
from wifi_densepose.client import (
|
||
RuViewMqttClient, SemanticPrimitive, SemanticPrimitiveListener,
|
||
)
|
||
|
||
listener = SemanticPrimitiveListener()
|
||
listener.on(SemanticPrimitive.BedExit, lambda e: print("bed exit:", e.node_id))
|
||
listener.on(SemanticPrimitive.PossibleDistress, lambda e: alert(e))
|
||
|
||
client = RuViewMqttClient(broker_host="homeassistant.local")
|
||
client.on_message(
|
||
# MQTT `+` must fill a whole topic level; the listener skips non-RuView topics.
|
||
"homeassistant/+/+/+/state",
|
||
listener.handle_mqtt_message,
|
||
)
|
||
client.start()
|
||
client.wait_connected()
|
||
```
|
||
|
||
### Decode 802.11ax beamforming feedback
|
||
|
||
```python
|
||
import numpy as np
|
||
from wifi_densepose import BfldFrame, BfldKind
|
||
|
||
# Parse compressed BFR from a Wireshark capture into a complex128 ndarray ...
|
||
fb = np.zeros((2, 1, 996), dtype=np.complex128) # Nr=2 Nc=1 Nsc=996 for HE80
|
||
|
||
frame = BfldFrame.from_compressed_feedback(
|
||
timestamp_ms=ts,
|
||
sounding_index=seq,
|
||
sta_mac="aa:bb:cc:dd:ee:ff",
|
||
kind=BfldKind.CompressedHE80,
|
||
feedback_matrix=fb,
|
||
)
|
||
print(frame.n_subcarriers, frame.mean_amplitude)
|
||
```
|
||
|
||
## Hardware
|
||
|
||
Works with any WiFi chip that exposes CSI. Reference setups (ESP-IDF firmware,
|
||
build scripts, witness-verified test bundles) are in the
|
||
[RuView repo](https://github.com/ruvnet/RuView):
|
||
|
||
| Device | Cost | Role |
|
||
|---|---|---|
|
||
| ESP32-S3 (8MB flash) | ~$9 | WiFi CSI sensing node |
|
||
| ESP32-S3 SuperMini (4MB) | ~$6 | WiFi CSI (compact) |
|
||
| ESP32-C6 + Seeed MR60BHA2 | ~$15 | mmWave HR/BR/presence add-on |
|
||
|
||
The legacy v1 line (Wi-Pose-style FastAPI server) is end-of-life;
|
||
`wifi-densepose==1.99.0` is a tombstone that raises `ImportError` pointing
|
||
to v2 with a migration URL.
|
||
|
||
## Links
|
||
|
||
- **Repository** — https://github.com/ruvnet/RuView
|
||
- **Modernization plan** — [ADR-117](https://github.com/ruvnet/RuView/blob/main/docs/adr/ADR-117-pip-wifi-densepose-modernization.md)
|
||
- **Home Assistant integration** — [ADR-115](https://github.com/ruvnet/RuView/blob/main/docs/adr/ADR-115-home-assistant-integration.md)
|
||
- **Issues** — https://github.com/ruvnet/RuView/issues
|
||
|
||
## License
|
||
|
||
MIT.
|