use crate::quality::{QualityReport, QualityScore, QualityValidator};
use crate::types::CameraFrame;
use std::collections::VecDeque;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct TriggerConfig {
pub min_quality_score: f32,
pub min_stability_duration: Duration,
pub timeout: Option<Duration>,
pub required_consecutive_good_frames: usize,
}
impl Default for TriggerConfig {
fn default() -> Self {
Self {
min_quality_score: 0.75,
min_stability_duration: Duration::from_millis(200),
timeout: Some(Duration::from_secs(5)),
required_consecutive_good_frames: 3,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum TriggerStatus {
Thinking(String),
Ready,
Timeout,
Captured,
}
pub struct SmartTrigger {
validator: QualityValidator,
config: TriggerConfig,
start_time: Instant,
good_frame_streak: usize,
last_good_frame_time: Option<Instant>,
best_frame_so_far: Option<(CameraFrame, QualityScore)>,
score_history: VecDeque<f32>,
}
impl SmartTrigger {
pub fn new(config: TriggerConfig) -> Self {
Self {
validator: QualityValidator::default(),
config,
start_time: Instant::now(),
good_frame_streak: 0,
last_good_frame_time: None,
best_frame_so_far: None,
score_history: VecDeque::with_capacity(10),
}
}
pub fn with_validator(mut self, validator: QualityValidator) -> Self {
self.validator = validator;
self
}
pub fn reset(&mut self) {
self.start_time = Instant::now();
self.good_frame_streak = 0;
self.last_good_frame_time = None;
self.best_frame_so_far = None;
self.score_history.clear();
}
pub fn process_frame(&mut self, frame: &CameraFrame) -> (TriggerStatus, QualityReport) {
#[cfg(debug_assertions)]
let _perf_guard = {
use crate::invariant_ppt::{PerfSnapshot, assert_performance_invariant};
struct Guard(Instant);
impl Drop for Guard {
fn drop(&mut self) {
let elapsed = self.0.elapsed().as_secs_f64() * 1000.0;
assert_performance_invariant(
&PerfSnapshot {
label: "smart_trigger_analysis".into(),
latency_ms: elapsed,
throughput_ops: 0.0,
memory_delta_kb: 0,
},
200.0, 1.0
);
}
}
Guard(Instant::now())
};
let report = self.validator.validate_frame(frame);
let score = report.score.overall;
if self.score_history.len() >= 10 {
self.score_history.pop_front();
}
self.score_history.push_back(score);
if let Some((_, best_score)) = &self.best_frame_so_far {
if score > best_score.overall {
self.best_frame_so_far = Some((frame.clone(), report.score.clone()));
}
} else {
self.best_frame_so_far = Some((frame.clone(), report.score.clone()));
}
if let Some(timeout) = self.config.timeout {
if self.start_time.elapsed() > timeout {
return (TriggerStatus::Timeout, report);
}
}
if score >= self.config.min_quality_score {
self.good_frame_streak += 1;
if self.last_good_frame_time.is_none() {
self.last_good_frame_time = Some(Instant::now());
}
let stability_duration = self.last_good_frame_time
.map(|t| t.elapsed())
.unwrap_or(Duration::ZERO);
if self.good_frame_streak >= self.config.required_consecutive_good_frames
&& stability_duration >= self.config.min_stability_duration
{
return (TriggerStatus::Ready, report);
}
} else {
self.good_frame_streak = 0;
self.last_good_frame_time = None;
}
let status_msg = if score < self.config.min_quality_score {
"Improving quality...".to_string()
} else {
format!("Stabilizing ({}/{})",
self.good_frame_streak,
self.config.required_consecutive_good_frames)
};
(TriggerStatus::Thinking(status_msg), report)
}
pub fn get_best_frame(&self) -> Option<CameraFrame> {
self.best_frame_so_far.as_ref().map(|(f, _)| f.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_frame(brightness: u8) -> CameraFrame {
let width = 640;
let height = 480;
let data = vec![brightness; (width * height * 3) as usize];
CameraFrame::new(data, width, height, "test".into())
}
#[test]
fn test_smart_trigger_flow() {
crate::invariant_ppt::clear_invariant_log();
let config = TriggerConfig {
min_quality_score: 0.5,
min_stability_duration: Duration::ZERO,
required_consecutive_good_frames: 2,
timeout: None,
};
let mut trigger = SmartTrigger::new(config);
let frame_bad = create_test_frame(0);
let (status, _) = trigger.process_frame(&frame_bad);
match status {
TriggerStatus::Thinking(msg) => assert!(msg.contains("Improving")),
_ => panic!("Should be thinking on bad frame"),
}
let frame_good = create_test_frame(128);
let (status, _) = trigger.process_frame(&frame_good);
match status {
TriggerStatus::Thinking(msg) => assert!(msg.contains("Stabilizing")),
_ => panic!("Should be stabilizing on first good frame"),
}
let (status, _) = trigger.process_frame(&frame_good);
assert_eq!(status, TriggerStatus::Ready);
}
#[test]
fn test_smart_trigger_timeout() {
crate::invariant_ppt::clear_invariant_log();
let config = TriggerConfig {
min_quality_score: 0.99, min_stability_duration: Duration::ZERO,
required_consecutive_good_frames: 1,
timeout: Some(Duration::from_millis(1)), };
let mut trigger = SmartTrigger::new(config);
std::thread::sleep(Duration::from_millis(10));
let frame = create_test_frame(128);
let (status, _) = trigger.process_frame(&frame);
assert_eq!(status, TriggerStatus::Timeout);
assert!(trigger.get_best_frame().is_some());
}
}