mirror of
https://github.com/ruvnet/RuView
synced 2026-07-03 14:13:17 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb1fcbad85 | |||
| e6068c5efe | |||
| 7a13877fa3 |
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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__"]
|
||||
Reference in New Issue
Block a user