Add brain bridge — sparse spatial observation sync every 60s

Stores room scan summaries, motion events, and vital signs
in the ruOS brain as memories. Only syncs every 120 frames
(~60 seconds) to keep the brain sparse and optimized.

Categories: spatial-observation, spatial-motion, spatial-vitals.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv 2026-04-19 20:38:17 -04:00
parent 898d90f689
commit ae792aad0d
3 changed files with 106 additions and 1 deletions

View File

@ -0,0 +1,90 @@
//! 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.
use crate::csi_pipeline::PipelineOutput;
use anyhow::Result;
const BRAIN_URL: &str = "http://127.0.0.1:9876";
/// 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!("{BRAIN_URL}/memories"))
.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;
}
}
/// Check if brain is reachable.
pub async fn brain_available() -> bool {
reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(2))
.build()
.ok()
.and_then(|c| {
tokio::runtime::Handle::current().block_on(async {
c.get(format!("{BRAIN_URL}/health")).send().await.ok()
})
})
.is_some()
}

View File

@ -10,6 +10,7 @@
//! ruview-pointcloud train # calibration training
//! ruview-pointcloud csi-test # send test CSI frames
mod brain_bridge;
mod camera;
mod csi;
mod csi_pipeline;

View File

@ -1,5 +1,6 @@
//! HTTP server — live camera + ESP32 CSI + fusion → real-time point cloud.
use crate::brain_bridge;
use crate::camera;
use crate::csi_pipeline;
use crate::depth;
@ -59,7 +60,9 @@ pub async fn serve(host: &str, port: u16, _wifi_source: Option<&str>) -> anyhow:
let frame_num = *bg.frame_count.lock().unwrap();
skip_depth = !out.motion_detected && frame_num % 5 != 0;
}
let pipeline_clone = pipeline_out.clone();
*bg.latest_pipeline.lock().unwrap() = pipeline_out;
let pipeline_out = pipeline_clone;
let interval = if skip_depth { 1000 } else { 500 }; // slower when no motion
tokio::time::sleep(std::time::Duration::from_millis(interval)).await;
@ -74,7 +77,18 @@ pub async fn serve(host: &str, port: u16, _wifi_source: Option<&str>) -> anyhow:
let splats = pointcloud::to_gaussian_splats(&cloud);
*bg.latest_cloud.lock().unwrap() = cloud;
*bg.latest_splats.lock().unwrap() = splats;
*bg.frame_count.lock().unwrap() += 1;
let frame_num = {
let mut fc = bg.frame_count.lock().unwrap();
*fc += 1;
*fc
};
// Brain sync — sparse, every 120 frames (~60 seconds)
if frame_num % 120 == 0 {
if let Some(ref out) = pipeline_out {
brain_bridge::sync_to_brain(out, frame_num).await;
}
}
}
});