//! Brain bridge — sends spatial observations to the ruOS brain. //! //! Periodically summarizes the sensor pipeline state and stores it //! as brain memories for the agent to reason about. //! //! The brain URL is read from the `RUVIEW_BRAIN_URL` env var on first use, //! defaulting to `http://127.0.0.1:9876`. use crate::csi_pipeline::PipelineOutput; use anyhow::Result; use std::sync::OnceLock; /// Default brain URL if `RUVIEW_BRAIN_URL` is not set. const DEFAULT_BRAIN_URL: &str = "http://127.0.0.1:9876"; fn brain_url() -> &'static str { static BRAIN_URL: OnceLock = OnceLock::new(); BRAIN_URL.get_or_init(|| { let url = std::env::var("RUVIEW_BRAIN_URL").unwrap_or_else(|_| DEFAULT_BRAIN_URL.to_string()); eprintln!(" brain_bridge: using brain URL {url}"); url }) } /// Store a spatial observation in the brain. async fn store_memory(category: &str, content: &str) -> Result<()> { let client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(5)) .build()?; let body = serde_json::json!({ "category": category, "content": content, }); client .post(format!("{}/memories", brain_url())) .json(&body) .send() .await?; Ok(()) } /// Summarize pipeline state and store in brain (called every 60 seconds). pub async fn sync_to_brain(pipeline: &PipelineOutput, camera_frames: u64) { // Only store if there's meaningful data if pipeline.total_frames < 10 && camera_frames < 5 { return; } // Store spatial summary let motion_str = if pipeline.motion_detected { "detected" } else { "absent" }; let skeleton_str = if let Some(ref sk) = pipeline.skeleton { format!( "{} keypoints ({:.0}% conf)", sk.keypoints.len(), sk.confidence * 100.0 ) } else { "inactive".to_string() }; let summary = format!( "Room scan: {} camera frames, {} CSI frames from {} nodes. \ Motion {} ({:.0}%). Breathing {:.0} BPM. Skeleton: {}. \ Occupancy grid {}x{}x{} with {} occupied voxels.", camera_frames, pipeline.total_frames, pipeline.num_nodes, motion_str, pipeline.vitals.motion_score * 100.0, pipeline.vitals.breathing_rate, skeleton_str, pipeline.occupancy_dims.0, pipeline.occupancy_dims.1, pipeline.occupancy_dims.2, pipeline.occupancy.iter().filter(|&&d| d > 0.3).count(), ); let _ = store_memory("spatial-observation", &summary).await; // Store motion events if pipeline.motion_detected && pipeline.vitals.motion_score > 0.3 { let _ = store_memory( "spatial-motion", &format!( "Strong motion detected: {:.0}% score, {} CSI frames", pipeline.vitals.motion_score * 100.0, pipeline.total_frames ), ) .await; } // Store vital signs if available if pipeline.vitals.breathing_rate > 5.0 && pipeline.vitals.breathing_rate < 35.0 { let _ = store_memory( "spatial-vitals", &format!( "Vital signs: breathing {:.0} BPM, motion {:.0}%", pipeline.vitals.breathing_rate, pipeline.vitals.motion_score * 100.0 ), ) .await; } }