Compare commits

...

3 Commits

Author SHA1 Message Date
ruv cb1fcbad85 fix: add wifi_densepose package so from wifi_densepose import WiFiDensePose works (#314)
The README Quick Start tells users to `pip install wifi-densepose` and then
`from wifi_densepose import WiFiDensePose`, but no `wifi_densepose` Python
package existed — only `v1/src`. This adds a top-level `wifi_densepose/`
package with a WiFiDensePose facade class matching the documented API, and
updates pyproject.toml to include it in the distribution.

Closes #314

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-27 16:57:20 -04:00
rUv e6068c5efe Enhance README with Cognitum.One reference
Updated project description to include Cognitum.One.
2026-03-25 21:21:58 -04:00
rUv 7a13877fa3 fix(sensing-server): detect ESP32 offline after 5s frame timeout (#300)
The source field was set to "esp32" on the first UDP frame but never
reverted when frames stopped arriving. This caused the UI to show
"Real hardware connected" indefinitely after powering off all nodes.

Changes:
- Add last_esp32_frame timestamp to AppStateInner
- Add effective_source() method with 5-second timeout
- Source becomes "esp32:offline" when no frames received within 5s
- Health endpoint shows "degraded" instead of "healthy" when offline
- All 6 status/health/info API endpoints use effective_source()

Fixes #297

Co-authored-by: Reuven <cohen@ruv-mac-mini.local>
2026-03-24 08:00:18 -04:00
4 changed files with 174 additions and 11 deletions
+1 -1
View File
@@ -14,7 +14,7 @@
Instead of relying on cameras or cloud models, it observes whatever signals exist in a space such as WiFi, radio waves across the spectrum, motion patterns, vibration, sound, or other sensory inputs and builds an understanding of what is happening locally.
Built on top of [RuVector](https://github.com/ruvnet/ruvector/), the project became widely known for its implementation of WiFi DensePose — a sensing technique first explored in academic research such as Carnegie Mellon University's *DensePose From WiFi* work. That research demonstrated that WiFi signals can be used to reconstruct human pose.
Built on top of [RuVector](https://github.com/ruvnet/ruvector/) Self Learning Vector Memory system and [Cognitum.One](https://Cognitum.One) , the project became widely known for its implementation of WiFi DensePose — a sensing technique first explored in academic research such as Carnegie Mellon University's *DensePose From WiFi* work. That research demonstrated that WiFi signals can be used to reconstruct human pose.
RuView extends that concept into a practical edge system. By analyzing Channel State Information (CSI) disturbances caused by human movement, RuView reconstructs body position, breathing rate, heart rate, and presence in real time using physics-based signal processing and machine learning.
+1 -1
View File
@@ -185,7 +185,7 @@ package-dir = {"" = "."}
[tool.setuptools.packages.find]
where = ["."]
include = ["src*"]
include = ["wifi_densepose*", "src*"]
exclude = ["tests*", "docs*", "scripts*"]
[tool.setuptools.package-data]
@@ -285,6 +285,8 @@ struct AppStateInner {
frame_history: VecDeque<Vec<f64>>,
tick: u64,
source: String,
/// Instant of the last ESP32 UDP frame received (for offline detection).
last_esp32_frame: Option<std::time::Instant>,
tx: broadcast::Sender<String>,
total_detections: u64,
start_time: std::time::Instant,
@@ -364,6 +366,25 @@ struct AppStateInner {
adaptive_model: Option<adaptive_classifier::AdaptiveModel>,
}
/// If no ESP32 frame arrives within this duration, source reverts to offline.
const ESP32_OFFLINE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
impl AppStateInner {
/// Return the effective data source, accounting for ESP32 frame timeout.
/// If the source is "esp32" but no frame has arrived in 5 seconds, returns
/// "esp32:offline" so the UI can distinguish active vs stale connections.
fn effective_source(&self) -> String {
if self.source == "esp32" {
if let Some(last) = self.last_esp32_frame {
if last.elapsed() > ESP32_OFFLINE_TIMEOUT {
return "esp32:offline".to_string();
}
}
}
self.source.clone()
}
}
/// Number of frames retained in `frame_history` for temporal analysis.
/// At 500 ms ticks this covers ~50 seconds; at 100 ms ticks ~10 seconds.
const FRAME_HISTORY_CAPACITY: usize = 100;
@@ -1669,7 +1690,7 @@ async fn health(State(state): State<SharedState>) -> Json<serde_json::Value> {
let s = state.read().await;
Json(serde_json::json!({
"status": "ok",
"source": s.source,
"source": s.effective_source(),
"tick": s.tick,
"clients": s.tx.receiver_count(),
}))
@@ -1977,7 +1998,7 @@ async fn health_ready(State(state): State<SharedState>) -> Json<serde_json::Valu
let s = state.read().await;
Json(serde_json::json!({
"status": "ready",
"source": s.source,
"source": s.effective_source(),
}))
}
@@ -1988,7 +2009,10 @@ async fn health_system(State(state): State<SharedState>) -> Json<serde_json::Val
"status": "healthy",
"components": {
"api": { "status": "healthy", "message": "Rust Axum server" },
"hardware": { "status": "healthy", "message": format!("Source: {}", s.source) },
"hardware": {
"status": if s.effective_source().ends_with(":offline") { "degraded" } else { "healthy" },
"message": format!("Source: {}", s.effective_source())
},
"pose": { "status": "healthy", "message": "WiFi-derived pose estimation" },
"stream": { "status": if s.tx.receiver_count() > 0 { "healthy" } else { "idle" },
"message": format!("{} client(s)", s.tx.receiver_count()) },
@@ -2028,7 +2052,7 @@ async fn api_info(State(state): State<SharedState>) -> Json<serde_json::Value> {
"version": env!("CARGO_PKG_VERSION"),
"environment": "production",
"backend": "rust",
"source": s.source,
"source": s.effective_source(),
"features": {
"wifi_sensing": true,
"pose_estimation": true,
@@ -2049,7 +2073,7 @@ async fn pose_current(State(state): State<SharedState>) -> Json<serde_json::Valu
"timestamp": chrono::Utc::now().timestamp_millis() as f64 / 1000.0,
"persons": persons,
"total_persons": persons.len(),
"source": s.source,
"source": s.effective_source(),
}))
}
@@ -2059,7 +2083,7 @@ async fn pose_stats(State(state): State<SharedState>) -> Json<serde_json::Value>
"total_detections": s.total_detections,
"average_confidence": 0.87,
"frames_processed": s.tick,
"source": s.source,
"source": s.effective_source(),
}))
}
@@ -2083,7 +2107,7 @@ async fn stream_status(State(state): State<SharedState>) -> Json<serde_json::Val
"active": true,
"clients": s.tx.receiver_count(),
"fps": if s.tick > 1 { 10u64 } else { 0u64 },
"source": s.source,
"source": s.effective_source(),
}))
}
@@ -2619,7 +2643,7 @@ async fn vital_signs_endpoint(State(state): State<SharedState>) -> Json<serde_js
"heartbeat_samples": hb_len,
"heartbeat_capacity": hb_cap,
},
"source": s.source,
"source": s.effective_source(),
"tick": s.tick,
}))
}
@@ -2825,6 +2849,7 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
let mut s = state.write().await;
s.source = "esp32".to_string();
s.last_esp32_frame = Some(std::time::Instant::now());
// Append current amplitudes to history before extracting features so
// that temporal analysis includes the most recent frame.
@@ -3607,6 +3632,7 @@ async fn main() {
frame_history: VecDeque::new(),
tick: 0,
source: source.into(),
last_esp32_frame: None,
tx,
total_detections: 0,
start_time: std::time::Instant::now(),
@@ -3781,7 +3807,7 @@ async fn main() {
"WiFi DensePose sensing model state",
);
builder.add_metadata(&serde_json::json!({
"source": s.source,
"source": s.effective_source(),
"total_ticks": s.tick,
"total_detections": s.total_detections,
"uptime_secs": s.start_time.elapsed().as_secs(),
+137
View File
@@ -0,0 +1,137 @@
"""
WiFi-DensePose — WiFi-based human pose estimation using CSI data.
Usage:
from wifi_densepose import WiFiDensePose
system = WiFiDensePose()
system.start()
poses = system.get_latest_poses()
system.stop()
"""
__version__ = "1.2.0"
import sys
import os
import logging
logger = logging.getLogger(__name__)
# Allow importing the v1 src package when installed from the repo
_v1_src = os.path.join(os.path.dirname(os.path.dirname(__file__)), "v1")
if os.path.isdir(_v1_src) and _v1_src not in sys.path:
sys.path.insert(0, _v1_src)
class WiFiDensePose:
"""High-level facade for the WiFi-DensePose sensing system.
This is the primary entry point documented in the README Quick Start.
It wraps the underlying ServiceOrchestrator and exposes a simple
start / get_latest_poses / stop interface.
"""
def __init__(self, host: str = "0.0.0.0", port: int = 3000, **kwargs):
self.host = host
self.port = port
self._config = kwargs
self._orchestrator = None
self._server_task = None
self._poses = []
self._running = False
# ------------------------------------------------------------------
# Public API (matches README Quick Start)
# ------------------------------------------------------------------
def start(self):
"""Start the sensing system (blocking until ready)."""
import asyncio
loop = _get_or_create_event_loop()
loop.run_until_complete(self._async_start())
async def _async_start(self):
try:
from src.config.settings import get_settings
from src.services.orchestrator import ServiceOrchestrator
settings = get_settings()
self._orchestrator = ServiceOrchestrator(settings)
await self._orchestrator.initialize()
await self._orchestrator.start()
self._running = True
logger.info("WiFiDensePose system started on %s:%s", self.host, self.port)
except ImportError:
raise ImportError(
"Core dependencies not found. Make sure you installed "
"from the repository root:\n"
" cd wifi-densepose && pip install -e .\n"
"Or install the v1 package:\n"
" cd wifi-densepose/v1 && pip install -e ."
)
def stop(self):
"""Stop the sensing system."""
import asyncio
if self._orchestrator is not None:
loop = _get_or_create_event_loop()
loop.run_until_complete(self._orchestrator.shutdown())
self._running = False
logger.info("WiFiDensePose system stopped")
def get_latest_poses(self):
"""Return the most recent list of detected pose dicts."""
if self._orchestrator is None:
return []
try:
import asyncio
loop = _get_or_create_event_loop()
return loop.run_until_complete(self._fetch_poses())
except Exception:
return []
async def _fetch_poses(self):
try:
pose_svc = self._orchestrator.pose_service
if pose_svc and hasattr(pose_svc, "get_latest"):
return await pose_svc.get_latest()
except Exception:
pass
return []
# ------------------------------------------------------------------
# Context-manager support
# ------------------------------------------------------------------
def __enter__(self):
self.start()
return self
def __exit__(self, *exc):
self.stop()
# ------------------------------------------------------------------
# Convenience re-exports
# ------------------------------------------------------------------
@staticmethod
def version():
return __version__
def _get_or_create_event_loop():
import asyncio
try:
return asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop
__all__ = ["WiFiDensePose", "__version__"]