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}