337 lines
11 KiB
Rust
337 lines
11 KiB
Rust
use anyhow::{Result, Context};
|
|
use std::collections::VecDeque;
|
|
use tokio::time::{sleep, Duration};
|
|
|
|
use crate::events::{EventBus, SystemEvent, FrameCapturedEvent, MeteorDetectedEvent};
|
|
|
|
/// Configuration for the detection controller
|
|
#[derive(Debug, Clone)]
|
|
pub struct DetectionConfig {
|
|
pub algorithm_name: String,
|
|
pub brightness_threshold: f32,
|
|
pub buffer_capacity: usize,
|
|
pub min_event_frames: usize,
|
|
pub max_event_gap_frames: usize,
|
|
}
|
|
|
|
impl Default for DetectionConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
algorithm_name: "brightness_diff".to_string(),
|
|
brightness_threshold: 0.3,
|
|
buffer_capacity: 150,
|
|
min_event_frames: 3,
|
|
max_event_gap_frames: 10,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Available detection algorithms
|
|
#[derive(Debug, Clone)]
|
|
pub enum DetectionAlgorithm {
|
|
BrightnessDiff,
|
|
// Future algorithms can be added here
|
|
}
|
|
|
|
/// Stored frame information for analysis
|
|
#[derive(Debug, Clone)]
|
|
struct StoredFrame {
|
|
frame_id: u64,
|
|
timestamp: chrono::DateTime<chrono::Utc>,
|
|
width: u32,
|
|
height: u32,
|
|
brightness_score: f64, // Simplified representation for analysis
|
|
}
|
|
|
|
/// Detection controller that analyzes frames for meteors
|
|
pub struct DetectionController {
|
|
config: DetectionConfig,
|
|
event_bus: EventBus,
|
|
frame_buffer: VecDeque<StoredFrame>,
|
|
last_processed_frame_id: u64,
|
|
}
|
|
|
|
impl DetectionController {
|
|
/// Create a new detection controller
|
|
pub fn new(config: DetectionConfig, event_bus: EventBus) -> Self {
|
|
let buffer_capacity = config.buffer_capacity;
|
|
Self {
|
|
config,
|
|
event_bus,
|
|
frame_buffer: VecDeque::with_capacity(buffer_capacity),
|
|
last_processed_frame_id: 0,
|
|
}
|
|
}
|
|
|
|
/// Start the detection loop
|
|
pub async fn run(&mut self) -> Result<()> {
|
|
println!("🔍 Starting meteor detection controller...");
|
|
println!(" Buffer size: {} frames", self.config.buffer_capacity);
|
|
println!(" Algorithm: {}", self.config.algorithm_name);
|
|
println!(" Brightness threshold: {}", self.config.brightness_threshold);
|
|
println!(" Min event frames: {}", self.config.min_event_frames);
|
|
|
|
let mut event_receiver = self.event_bus.subscribe();
|
|
let check_interval = Duration::from_millis(100); // Fixed 100ms check interval
|
|
|
|
println!("✅ Detection controller initialized, starting analysis loop...");
|
|
|
|
loop {
|
|
tokio::select! {
|
|
// Handle incoming events
|
|
event_result = event_receiver.recv() => {
|
|
match event_result {
|
|
Ok(event) => {
|
|
if let Err(e) = self.handle_event(event.as_ref()).await {
|
|
eprintln!("❌ Error handling event: {}", e);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
eprintln!("❌ Error receiving event: {}", e);
|
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Periodic analysis check
|
|
_ = sleep(check_interval) => {
|
|
if let Err(e) = self.run_detection_analysis().await {
|
|
eprintln!("❌ Error in detection analysis: {}", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Handle incoming events from the event bus
|
|
async fn handle_event(&mut self, event: &SystemEvent) -> Result<()> {
|
|
match event {
|
|
SystemEvent::FrameCaptured(frame_event) => {
|
|
self.process_frame_event(frame_event.clone()).await?;
|
|
}
|
|
SystemEvent::SystemStarted(_) => {
|
|
println!("🔍 Detection controller received system started event");
|
|
}
|
|
SystemEvent::MeteorDetected(_) => {
|
|
// We don't need to process our own detections
|
|
}
|
|
SystemEvent::EventPackageArchived(_) => {
|
|
// Detection controller doesn't need to handle archived events
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Process a captured frame event and add to buffer
|
|
async fn process_frame_event(&mut self, frame_event: FrameCapturedEvent) -> Result<()> {
|
|
// Calculate brightness score (simplified analysis)
|
|
let brightness_score = self.calculate_brightness_score(&frame_event)?;
|
|
|
|
let stored_frame = StoredFrame {
|
|
frame_id: frame_event.frame_id,
|
|
timestamp: frame_event.timestamp,
|
|
width: frame_event.frame_data.width,
|
|
height: frame_event.frame_data.height,
|
|
brightness_score,
|
|
};
|
|
|
|
// Add to circular buffer
|
|
self.frame_buffer.push_back(stored_frame);
|
|
|
|
// Maintain buffer size
|
|
while self.frame_buffer.len() > self.config.buffer_capacity {
|
|
self.frame_buffer.pop_front();
|
|
}
|
|
|
|
// Update last processed frame ID
|
|
self.last_processed_frame_id = frame_event.frame_id;
|
|
|
|
if frame_event.frame_id % 50 == 0 {
|
|
println!("🔍 Processed {} frames, buffer size: {}",
|
|
frame_event.frame_id,
|
|
self.frame_buffer.len()
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Calculate brightness score from frame data (simplified)
|
|
fn calculate_brightness_score(&self, frame_event: &FrameCapturedEvent) -> Result<f64> {
|
|
// Simplified brightness calculation based on frame data content
|
|
// In our synthetic JPEG format, the brightness is encoded in the pixel values
|
|
|
|
if frame_event.frame_data.len() < 8 { // Need at least header + some data
|
|
return Ok(0.0);
|
|
}
|
|
|
|
// Skip the fake JPEG header (first 4 bytes) and footer (last 2 bytes)
|
|
let data_start = 4;
|
|
let frame_data_slice = frame_event.frame_data.as_slice();
|
|
let data_end = frame_data_slice.len().saturating_sub(2);
|
|
|
|
if data_start >= data_end {
|
|
return Ok(0.0);
|
|
}
|
|
|
|
// Calculate average pixel value (brightness) from the data section
|
|
let pixel_data = &frame_data_slice[data_start..data_end];
|
|
let average_brightness = pixel_data.iter()
|
|
.map(|&b| b as f64)
|
|
.sum::<f64>() / pixel_data.len() as f64;
|
|
|
|
// Normalize to 0.0-1.0 range (assuming 255 is max brightness)
|
|
let score = (average_brightness / 255.0).min(1.0).max(0.0);
|
|
|
|
Ok(score)
|
|
}
|
|
|
|
/// Run detection analysis on the frame buffer
|
|
async fn run_detection_analysis(&mut self) -> Result<()> {
|
|
if self.frame_buffer.len() < 10 { // Need at least 10 frames for analysis
|
|
return Ok(());
|
|
}
|
|
|
|
match self.config.algorithm_name.as_str() {
|
|
"brightness_diff" => {
|
|
self.run_brightness_diff_detection().await?;
|
|
}
|
|
_ => {
|
|
eprintln!("Unknown detection algorithm: {}", self.config.algorithm_name);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Run brightness difference detection algorithm
|
|
async fn run_brightness_diff_detection(&mut self) -> Result<()> {
|
|
let frames: Vec<&StoredFrame> = self.frame_buffer.iter().collect();
|
|
|
|
// Need at least 10 frames for reliable detection
|
|
if frames.len() < 10 {
|
|
return Ok(());
|
|
}
|
|
|
|
// Calculate average brightness of historical frames (excluding recent ones)
|
|
let history_end = frames.len().saturating_sub(3); // Exclude last 3 frames
|
|
if history_end < 5 {
|
|
return Ok(());
|
|
}
|
|
|
|
let historical_avg = frames[..history_end]
|
|
.iter()
|
|
.map(|f| f.brightness_score)
|
|
.sum::<f64>() / history_end as f64;
|
|
|
|
// Check recent frames for significant brightness increase
|
|
for recent_frame in &frames[history_end..] {
|
|
let brightness_diff = recent_frame.brightness_score - historical_avg;
|
|
let relative_increase = if historical_avg > 0.0 {
|
|
brightness_diff / historical_avg
|
|
} else {
|
|
brightness_diff
|
|
};
|
|
|
|
// Use relative increase as confidence
|
|
let confidence = relative_increase.max(0.0).min(1.0);
|
|
|
|
// Debug output
|
|
if frames.len() >= 15 {
|
|
println!("🔍 DEBUG: Frame #{}, Historical avg: {:.3}, Current: {:.3}, Diff: {:.3}, Confidence: {:.3}",
|
|
recent_frame.frame_id,
|
|
historical_avg,
|
|
recent_frame.brightness_score,
|
|
brightness_diff,
|
|
confidence
|
|
);
|
|
}
|
|
|
|
if confidence >= self.config.brightness_threshold as f64 {
|
|
// Potential meteor detected!
|
|
let detection_event = MeteorDetectedEvent::new(
|
|
recent_frame.frame_id,
|
|
recent_frame.timestamp,
|
|
confidence,
|
|
"brightness_diff_v1".to_string(),
|
|
);
|
|
|
|
println!("🌟 METEOR DETECTED! Frame #{}, Confidence: {:.2}",
|
|
recent_frame.frame_id,
|
|
confidence
|
|
);
|
|
|
|
self.event_bus.publish_meteor_detected(detection_event)
|
|
.context("Failed to publish meteor detection event")?;
|
|
|
|
// Prevent duplicate detections for a short period
|
|
// by clearing recent frames from analysis
|
|
break;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get current buffer statistics
|
|
pub fn get_stats(&self) -> DetectionStats {
|
|
DetectionStats {
|
|
buffer_size: self.frame_buffer.len(),
|
|
buffer_capacity: self.config.buffer_capacity,
|
|
last_processed_frame_id: self.last_processed_frame_id,
|
|
avg_brightness: if !self.frame_buffer.is_empty() {
|
|
self.frame_buffer.iter().map(|f| f.brightness_score).sum::<f64>()
|
|
/ self.frame_buffer.len() as f64
|
|
} else {
|
|
0.0
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Statistics about the detection system
|
|
#[derive(Debug, Clone)]
|
|
pub struct DetectionStats {
|
|
pub buffer_size: usize,
|
|
pub buffer_capacity: usize,
|
|
pub last_processed_frame_id: u64,
|
|
pub avg_brightness: f64,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::events::EventBus;
|
|
|
|
#[test]
|
|
fn test_detection_config_default() {
|
|
let config = DetectionConfig::default();
|
|
assert_eq!(config.buffer_capacity, 150);
|
|
assert_eq!(config.brightness_threshold, 0.3);
|
|
assert_eq!(config.min_event_frames, 3);
|
|
assert_eq!(config.algorithm_name, "brightness_diff");
|
|
}
|
|
|
|
#[test]
|
|
fn test_detection_controller_creation() {
|
|
let config = DetectionConfig::default();
|
|
let event_bus = EventBus::new(100);
|
|
let controller = DetectionController::new(config, event_bus);
|
|
|
|
assert_eq!(controller.frame_buffer.len(), 0);
|
|
assert_eq!(controller.last_processed_frame_id, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_detection_stats() {
|
|
let config = DetectionConfig::default();
|
|
let event_bus = EventBus::new(100);
|
|
let controller = DetectionController::new(config, event_bus);
|
|
|
|
let stats = controller.get_stats();
|
|
assert_eq!(stats.buffer_size, 0);
|
|
assert_eq!(stats.buffer_capacity, 100);
|
|
assert_eq!(stats.last_processed_frame_id, 0);
|
|
assert_eq!(stats.avg_brightness, 0.0);
|
|
}
|
|
} |