ADR-185 §13 flagged the `[aether]` wheel as linking the whole
`wifi-densepose-sensing-server` Axum/tokio/worldgraph/ruvector tree via the
non-optional deps of that crate. Hoist the pure-compute AETHER stack out so the
binding depends only on pure Rust.
New crate `wifi-densepose-aether` (std-only, ZERO external deps) holds the four
self-contained modules the AETHER surface needs — `embedding`, `graph_transformer`,
`sona`, `sparse_inference` (verified: no serde/tokio/axum/crate:: refs outside the
set; the four form a closed dependency graph). `wifi-densepose-sensing-server`
now `pub use`s them from the leaf crate, so its own code (`crate::embedding`, …)
and public API (`wifi_densepose_sensing_server::embedding`, …) are unchanged —
`main.rs`/`trainer.rs` untouched. The Python binding + parity test + `[aether]`
feature repoint at the leaf crate; the sensing-server dep is dropped from python.
MEASURED wheel size (maturin build --release --features aether --strip; each
wheel install-verified to actually contain `AetherConfig`):
BEFORE (aether -> sensing-server): 369,782 B (~361 KB)
AFTER (aether -> leaf crate): 319,719 B (~312 KB)
HONEST CORRECTION to the §9/§13 premise: the pre-hoist wheel was NOT over the
ADR-117 §5.4 5 MB budget — it was ~361 KB, ~14x under. Linker dead-code
elimination (--gc-sections on the pyo3 cdylib) already strips the unreached
server tree because the binding only reaches pure-compute symbols. So this is
"blows the budget" CLAIMED, not MEASURED. The hoist is still worthwhile and
lands the real, measured wins:
- `[aether]` build time 71s -> 12s (no server tree to compile),
- python/Cargo.lock -1238 lines (server tree no longer resolved),
- ~50 KB smaller wheel,
- honest, minimal declared dependency surface + removes the latent risk that a
future change makes some server code reachable and *then* bloats the wheel.
Verified this session (real counts):
- cargo test --features aether --test aether_parity: 2 passed / 0 failed
- pytest tests/test_aether.py (maturin develop --features aether): 9 passed
- cargo test -p wifi-densepose-sensing-server --no-default-features: 217 bin +
388 lib, 0 failed (no regression)
- cargo test -p wifi-densepose-aether: 96 passed (the relocated unit tests)
The §9/§13 wheel-size narrative in ADR-185 (owned by the ADR author) should be
corrected to reflect the measured reality — flagged to the team lead.
wifi-densepose
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
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
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 |
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/cross_room_calibrate.py,
examples/mat_triage.py.
Usage
Extract breathing rate from a CSI stream
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})")
Heart rate is the same shape — HeartRateExtractor.esp32_default() with a
0.8–2.0 Hz band-pass and a 15-second window.
Subscribe to a live sensing-server
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
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(
"homeassistant/+/wifi_densepose_+/+/state",
listener.handle_mqtt_message,
)
client.start()
client.wait_connected()
Decode 802.11ax beamforming feedback
import numpy as np
from wifi_densepose import BfldFrame, BfldKind
# Parse compressed BFR from a Wireshark capture into a Complex64 ndarray ...
fb = np.zeros((2, 1, 996), dtype=np.complex64) # 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:
| 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
- Home Assistant integration — ADR-115
- Issues — https://github.com/ruvnet/RuView/issues
License
MIT.