diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-pointcloud/src/stream.rs b/rust-port/wifi-densepose-rs/crates/wifi-densepose-pointcloud/src/stream.rs index 1a64d044..83f988e2 100644 --- a/rust-port/wifi-densepose-rs/crates/wifi-densepose-pointcloud/src/stream.rs +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-pointcloud/src/stream.rs @@ -1,5 +1,4 @@ //! HTTP server — live camera + ESP32 CSI + fusion → real-time point cloud. -#![allow(dead_code)] use crate::brain_bridge; use crate::camera; @@ -21,13 +20,19 @@ struct AppState { latest_pipeline: Mutex>, frame_count: Mutex, use_camera: bool, - csi_pipeline: Option>>, } -pub async fn serve(host: &str, port: u16, _wifi_source: Option<&str>) -> anyhow::Result<()> { +/// Start the HTTP/viewer server bound to `bind` (e.g. +/// `"127.0.0.1:9880"` — the safe default — or `"0.0.0.0:9880"` to expose +/// the viewer to the LAN). +/// +/// **Security**: the viewer streams live camera/CSI/vitals data. Bind to +/// `127.0.0.1` unless you intentionally want remote viewers. +pub async fn serve(bind: &str, _brain: Option<&str>) -> anyhow::Result<()> { let has_camera = camera::camera_available(); - // Start CSI pipeline — listens for UDP CSI data from ESP32 nodes + // Start CSI pipeline — listens for UDP CSI data from ESP32 nodes. + // Kept on 0.0.0.0 because ESP32 nodes are remote devices on the LAN. let csi_pipeline_state = csi_pipeline::start_pipeline("0.0.0.0:3333"); eprintln!(" CSI pipeline: UDP port 3333 (ADR-018 binary frames)"); @@ -44,18 +49,17 @@ pub async fn serve(host: &str, port: u16, _wifi_source: Option<&str>) -> anyhow: latest_pipeline: Mutex::new(None), frame_count: Mutex::new(0), use_camera: has_camera, - csi_pipeline: Some(csi_pipeline_state.clone()), }); // Background: capture + fuse every 500ms (motion-adaptive) let bg = state.clone(); - let bg_csi = Some(csi_pipeline_state.clone()); + let bg_csi = csi_pipeline_state.clone(); let bg_cam = has_camera; tokio::spawn(async move { let mut skip_depth = false; loop { // Motion-adaptive: check CSI motion score - let pipeline_out = bg_csi.as_ref().map(|c| csi_pipeline::get_pipeline_output(c)); + let pipeline_out = Some(csi_pipeline::get_pipeline_output(&bg_csi)); if let Some(ref out) = pipeline_out { // Only run expensive depth when motion detected or every 5th frame let frame_num = *bg.frame_count.lock().unwrap(); @@ -68,13 +72,21 @@ pub async fn serve(host: &str, port: u16, _wifi_source: Option<&str>) -> anyhow: let interval = if skip_depth { 1000 } else { 500 }; // slower when no motion tokio::time::sleep(std::time::Duration::from_millis(interval)).await; - let cloud = if bg_cam && !skip_depth { - tokio::task::spawn_blocking(capture_camera_cloud) - .await.unwrap_or_else(|_| demo_cloud()) + let (cloud, luminance) = if bg_cam && !skip_depth { + tokio::task::spawn_blocking(capture_camera_cloud_with_luminance) + .await.unwrap_or_else(|_| (demo_cloud(), None)) } else { // Reuse previous cloud when no motion - bg.latest_cloud.lock().unwrap().clone() + (bg.latest_cloud.lock().unwrap().clone(), None) }; + // Feed luminance into the CSI pipeline so is_dark toggles for the + // viewer. The lock is held briefly here — the UDP thread never + // touches it (messages go through the mpsc channel). + if let Some(lum) = luminance { + if let Ok(mut st) = bg_csi.lock() { + st.set_light_level(lum); + } + } let splats = pointcloud::to_gaussian_splats(&cloud); *bg.latest_cloud.lock().unwrap() = cloud; *bg.latest_splats.lock().unwrap() = splats; @@ -104,30 +116,54 @@ pub async fn serve(host: &str, port: u16, _wifi_source: Option<&str>) -> anyhow: .route("/health", get(api_health)) .with_state(state); - let addr = format!("{host}:{port}"); println!("╔══════════════════════════════════════════════╗"); println!("║ RuView Dense Point Cloud — ALL SENSORS ║"); println!("╚══════════════════════════════════════════════╝"); - println!(" Viewer: http://{addr}/"); + println!(" Viewer: http://{bind}/"); + if bind.starts_with("0.0.0.0") || bind.starts_with("::") { + eprintln!( + " WARNING: bound to {bind} — camera/CSI/vitals are exposed \ + to the network. Use --bind 127.0.0.1:9880 to restrict to loopback." + ); + } - let listener = tokio::net::TcpListener::bind(&addr).await?; + let listener = tokio::net::TcpListener::bind(bind).await?; axum::serve(listener, app).await?; Ok(()) } fn capture_camera_cloud() -> pointcloud::PointCloud { + capture_camera_cloud_with_luminance().0 +} + +/// Grab one camera frame, backproject it to a point cloud, and return the +/// mean luminance alongside (used to drive `set_light_level` for night mode). +fn capture_camera_cloud_with_luminance() -> (pointcloud::PointCloud, Option) { let config = camera::CameraConfig::default(); match camera::capture_frame(&config) { Ok(frame) => { - match depth::estimate_depth(&frame.rgb, frame.width, frame.height) { + // Mean luminance across the RGB frame (BT.601 coefficients). + let pixels = (frame.width as usize) * (frame.height as usize); + let mut sum = 0.0f64; + let mut n = 0usize; + for chunk in frame.rgb.chunks_exact(3).take(pixels) { + sum += 0.299 * chunk[0] as f64 + + 0.587 * chunk[1] as f64 + + 0.114 * chunk[2] as f64; + n += 1; + } + let lum = if n > 0 { Some((sum / n as f64) as f32) } else { None }; + + let cloud = match depth::estimate_depth(&frame.rgb, frame.width, frame.height) { Ok(dm) => { let intr = depth::CameraIntrinsics::default(); depth::backproject_depth(&dm, &intr, Some(&frame.rgb), 2) } Err(_) => depth::demo_depth_cloud(), - } + }; + (cloud, lum) } - Err(_) => depth::demo_depth_cloud(), + Err(_) => (depth::demo_depth_cloud(), None), } } @@ -185,234 +221,12 @@ async fn api_health() -> Json { Json(serde_json::json!({"status": "ok"})) } -async fn index() -> Html { - Html(r#" - - - RuView — Camera + WiFi CSI Point Cloud - - - - - -
-

RuView Point Cloud

-
Loading...
-
- - -"#.to_string()) +async fn index() -> Html<&'static str> { + Html(VIEWER_HTML) } + diff --git a/rust-port/wifi-densepose-rs/crates/wifi-densepose-pointcloud/src/viewer.html b/rust-port/wifi-densepose-rs/crates/wifi-densepose-pointcloud/src/viewer.html new file mode 100644 index 00000000..342735d7 --- /dev/null +++ b/rust-port/wifi-densepose-rs/crates/wifi-densepose-pointcloud/src/viewer.html @@ -0,0 +1,229 @@ + + + + RuView — Camera + WiFi CSI Point Cloud + + + + + +
+

RuView Point Cloud

+
Loading...
+
+ + +