Skip to main content

yscv_eval/
camera_diagnostics.rs

1use std::fs;
2use std::path::Path;
3
4use serde::Deserialize;
5
6use crate::EvalError;
7
8#[derive(Debug, Clone, PartialEq, Deserialize)]
9pub struct CameraDiagnosticsReport {
10    pub tool: String,
11    pub mode: String,
12    pub status: String,
13    pub requested: CameraDiagnosticsRequested,
14    #[serde(default)]
15    pub discovered_devices: Vec<CameraDiagnosticsDevice>,
16    pub selected_device: Option<CameraDiagnosticsDevice>,
17    pub capture: Option<CameraDiagnosticsCapture>,
18}
19
20#[derive(Debug, Clone, PartialEq, Deserialize)]
21pub struct CameraDiagnosticsRequested {
22    pub device_index: u32,
23    pub width: u32,
24    pub height: u32,
25    pub fps: u32,
26    pub diagnose_frames: usize,
27    pub device_name_query: Option<String>,
28}
29
30#[derive(Debug, Clone, PartialEq, Deserialize)]
31pub struct CameraDiagnosticsDevice {
32    pub index: u32,
33    pub label: String,
34}
35
36#[derive(Debug, Clone, PartialEq, Deserialize)]
37pub struct CameraDiagnosticsCapture {
38    pub requested_frames: usize,
39    pub collected_frames: usize,
40    pub wall_ms: f64,
41    pub first_frame: Option<CameraDiagnosticsFirstFrame>,
42    pub timing: Option<CameraDiagnosticsTiming>,
43}
44
45#[derive(Debug, Clone, PartialEq, Deserialize)]
46pub struct CameraDiagnosticsFirstFrame {
47    pub index: u64,
48    pub timestamp_us: u64,
49    pub shape: [usize; 3],
50}
51
52#[derive(Debug, Clone, PartialEq, Deserialize)]
53pub struct CameraDiagnosticsTiming {
54    pub target_fps: f64,
55    pub wall_fps: f64,
56    pub sensor_fps: f64,
57    pub wall_drift_pct: f64,
58    pub sensor_drift_pct: f64,
59    pub mean_gap_us: f64,
60    pub min_gap_us: u64,
61    pub max_gap_us: u64,
62    pub dropped_frames: u64,
63    pub drift_warning: bool,
64    pub dropped_frames_warning: bool,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq)]
68pub struct CameraDiagnosticsThresholds {
69    pub min_collected_frames: usize,
70    pub max_abs_wall_drift_pct: f64,
71    pub max_abs_sensor_drift_pct: f64,
72    pub max_dropped_frames: u64,
73}
74
75impl Default for CameraDiagnosticsThresholds {
76    fn default() -> Self {
77        Self {
78            min_collected_frames: 2,
79            max_abs_wall_drift_pct: 25.0,
80            max_abs_sensor_drift_pct: 25.0,
81            max_dropped_frames: 0,
82        }
83    }
84}
85
86#[derive(Debug, Clone, PartialEq)]
87pub struct CameraDiagnosticsViolation {
88    pub field: &'static str,
89    pub message: String,
90}
91
92pub fn parse_camera_diagnostics_report_json(
93    text: &str,
94) -> Result<CameraDiagnosticsReport, EvalError> {
95    serde_json::from_str(text).map_err(|err| EvalError::InvalidDiagnosticsReport {
96        message: err.to_string(),
97    })
98}
99
100pub fn load_camera_diagnostics_report_json_file(
101    path: &Path,
102) -> Result<CameraDiagnosticsReport, EvalError> {
103    let body = fs::read_to_string(path).map_err(|err| EvalError::DiagnosticsReportIo {
104        path: path.display().to_string(),
105        message: err.to_string(),
106    })?;
107    parse_camera_diagnostics_report_json(&body)
108}
109
110pub fn validate_camera_diagnostics_report(
111    report: &CameraDiagnosticsReport,
112    thresholds: CameraDiagnosticsThresholds,
113) -> Vec<CameraDiagnosticsViolation> {
114    let mut violations = Vec::new();
115    if report.mode != "diagnostics" {
116        violations.push(CameraDiagnosticsViolation {
117            field: "mode",
118            message: format!("expected `diagnostics`, got `{}`", report.mode),
119        });
120    }
121    if report.status != "ok" {
122        violations.push(CameraDiagnosticsViolation {
123            field: "status",
124            message: format!("expected `ok`, got `{}`", report.status),
125        });
126    }
127
128    let Some(capture) = report.capture.as_ref() else {
129        violations.push(CameraDiagnosticsViolation {
130            field: "capture",
131            message: "capture section is missing".to_string(),
132        });
133        return violations;
134    };
135
136    if capture.collected_frames < thresholds.min_collected_frames {
137        violations.push(CameraDiagnosticsViolation {
138            field: "capture.collected_frames",
139            message: format!(
140                "expected >= {}, got {}",
141                thresholds.min_collected_frames, capture.collected_frames
142            ),
143        });
144    }
145    if !capture.wall_ms.is_finite() || capture.wall_ms < 0.0 {
146        violations.push(CameraDiagnosticsViolation {
147            field: "capture.wall_ms",
148            message: format!(
149                "expected finite non-negative value, got {}",
150                capture.wall_ms
151            ),
152        });
153    }
154
155    let Some(timing) = capture.timing.as_ref() else {
156        violations.push(CameraDiagnosticsViolation {
157            field: "capture.timing",
158            message: "timing section is missing".to_string(),
159        });
160        return violations;
161    };
162
163    for (field, value) in [
164        ("capture.timing.target_fps", timing.target_fps),
165        ("capture.timing.wall_fps", timing.wall_fps),
166        ("capture.timing.sensor_fps", timing.sensor_fps),
167        ("capture.timing.mean_gap_us", timing.mean_gap_us),
168    ] {
169        if !value.is_finite() {
170            violations.push(CameraDiagnosticsViolation {
171                field,
172                message: format!("expected finite value, got {value}"),
173            });
174        }
175    }
176
177    if timing.wall_drift_pct.abs() > thresholds.max_abs_wall_drift_pct {
178        violations.push(CameraDiagnosticsViolation {
179            field: "capture.timing.wall_drift_pct",
180            message: format!(
181                "abs drift {} exceeds threshold {}",
182                timing.wall_drift_pct.abs(),
183                thresholds.max_abs_wall_drift_pct
184            ),
185        });
186    }
187    if timing.sensor_drift_pct.abs() > thresholds.max_abs_sensor_drift_pct {
188        violations.push(CameraDiagnosticsViolation {
189            field: "capture.timing.sensor_drift_pct",
190            message: format!(
191                "abs drift {} exceeds threshold {}",
192                timing.sensor_drift_pct.abs(),
193                thresholds.max_abs_sensor_drift_pct
194            ),
195        });
196    }
197    if timing.dropped_frames > thresholds.max_dropped_frames {
198        violations.push(CameraDiagnosticsViolation {
199            field: "capture.timing.dropped_frames",
200            message: format!(
201                "dropped frames {} exceeds threshold {}",
202                timing.dropped_frames, thresholds.max_dropped_frames
203            ),
204        });
205    }
206    violations
207}