1use crate::{RecoveryOptions, Selection, invalid};
6use anyhow::{Result, ensure};
7use std::time::Duration;
8
9pub(crate) const PERSISTENT_CONTROLS: [i32; 18] = [
10 0, 2, 3, 4, 5, 6, 7, 9, 13, 14, 16, 17, 18, 19, 20, 21, 22, 23,
11];
12pub(crate) const ACKNOWLEDGED_CONTROLS: [i32; 4] = [0, 5, 16, 17];
13pub(crate) const SIMULATION_SETUP_SECONDS: f64 = 5.;
14pub(crate) const USB_BIND_SECONDS: u64 = 30;
15pub(crate) const USB_RESET_SECONDS: u64 = 80;
16pub(crate) const USB_REBIND_PAUSE_SECONDS: f64 = 0.5;
17pub(crate) const CLOSE_SECONDS: f64 = 2.;
18pub(crate) const MAX_READ_RETRY_OVERHEAD_SECONDS: f64 = 15.;
19const SUPERVISOR_MARGIN_SECONDS: f64 = 5.;
20
21#[derive(Clone, Debug)]
22pub struct NativeCameraTiming {
23 options: RecoveryOptions,
24 direct: bool,
25 sdk_fallback: bool,
26}
27impl NativeCameraTiming {
28 pub fn new(selection: &Selection) -> Result<Self> {
31 selection.recovery.validate()?;
32 Ok(Self {
33 options: selection.recovery.clone(),
34 direct: selection.direct,
35 sdk_fallback: selection.direct && selection.sdk_fallback,
36 })
37 }
38 fn command(&self) -> f64 {
39 self.options.command_timeout_seconds
40 }
41 fn open_seconds(&self) -> f64 {
42 SIMULATION_SETUP_SECONDS + 3. * self.command()
45 }
46 fn restore_seconds(&self) -> f64 {
47 (2 * PERSISTENT_CONTROLS.len() - ACKNOWLEDGED_CONTROLS.len()) as f64 * self.command()
50 }
51 fn close_seconds(&self) -> f64 {
52 CLOSE_SECONDS
53 + if self.direct {
54 self.options.reconnect_delay_seconds
58 + self.open_seconds()
59 + self.command()
60 + CLOSE_SECONDS
61 } else {
62 0.
63 }
64 }
65 fn usb_reset_seconds(&self) -> f64 {
66 USB_RESET_SECONDS as f64 + 2. * USB_BIND_SECONDS as f64 + USB_REBIND_PAUSE_SECONDS
69 }
70 fn cleanup_seconds(&self) -> f64 {
71 4. * self.close_seconds()
75 + self.command()
76 + if self.options.usb_reset_after_failures > 0 {
77 USB_RESET_SECONDS as f64
78 } else {
79 0.
80 }
81 + SUPERVISOR_MARGIN_SECONDS
82 }
83 pub fn cleanup_allowance(&self) -> Duration {
84 duration(self.cleanup_seconds()).expect("validated native cleanup timing")
85 }
86 pub fn connection_allowance(&self) -> Duration {
87 let usb = self.options.usb_reset_after_failures > 0;
88 let opens = 1 + usize::from(self.sdk_fallback) + usize::from(usb);
89 duration(
93 self.cleanup_seconds()
94 + 3. * self.close_seconds()
95 + opens as f64 * self.open_seconds()
96 + if self.sdk_fallback {
97 self.options.reconnect_delay_seconds
98 } else {
99 0.
100 }
101 + if usb { USB_BIND_SECONDS as f64 } else { 0. }
102 + self.restore_seconds()
103 + 2. * self.command()
104 + SUPERVISOR_MARGIN_SECONDS,
105 )
106 .expect("validated native connection timing")
107 }
108 pub fn control_allowance(&self) -> Duration {
109 duration(
113 self.options.reconnect_delay_seconds
114 + self.open_seconds()
115 + self.restore_seconds()
116 + 5. * self.command()
117 + SUPERVISOR_MARGIN_SECONDS,
118 )
119 .expect("validated native control timing")
120 }
121 pub fn capture_allowance(&self, microseconds: u64) -> Result<Duration> {
122 ensure!(
123 microseconds > 0,
124 invalid("Invalid native exposure duration")
125 );
126 let o = &self.options;
127 let replacements = o.replacement_exposures(microseconds);
128 let settle = o.cooling_timeout_seconds + 3. * self.command() + o.cooling_sample_seconds;
129 let ready = microseconds as f64 / 1e6
130 + o.exposure_grace_seconds
131 + if self.direct {
132 (1 + o.direct_read_retries) as f64 * o.download_timeout_seconds
135 + o.direct_read_retries as f64 * MAX_READ_RETRY_OVERHEAD_SECONDS
136 } else {
137 0.
138 };
139 let reads = 1 + o.ready_frame_download_retries;
140 let download = reads as f64 * (o.download_timeout_seconds + self.command())
141 + o.ready_frame_download_retries as f64 * o.reconnect_delay_seconds;
142 let fallback = if self.sdk_fallback {
143 self.command()
146 + o.reconnect_delay_seconds
147 + self.open_seconds()
148 + self.restore_seconds()
149 + settle
150 } else {
151 0.
152 };
153 let attempt = o.reconnect_delay_seconds
154 + self.open_seconds()
155 + self.restore_seconds()
156 + settle
157 + fallback
158 + ready
159 + download
160 + 10. * self.command()
163 + 0.025;
164 duration(
165 (1 + replacements) as f64 * attempt
166 + if o.usb_reset_after_failures > 0 && o.usb_reset_after_failures <= replacements {
167 self.usb_reset_seconds()
168 } else {
169 0.
170 }
171 + SUPERVISOR_MARGIN_SECONDS,
172 )
173 }
174}
175fn duration(seconds: f64) -> Result<Duration> {
176 Duration::try_from_secs_f64(seconds).map_err(|_| invalid("Native timing is not representable"))
177}
178
179impl RecoveryOptions {
180 pub fn replacement_exposures(&self, microseconds: u64) -> u32 {
183 if microseconds as f64 / 1e6 <= self.maximum_retry_exposure_seconds {
184 self.max_retries
185 } else {
186 0
187 }
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 fn selection(direct: bool) -> Selection {
195 Selection {
196 name: "Timing-only fixture".into(),
197 serial: None,
198 direct,
199 sdk_fallback: false,
200 recovery: RecoveryOptions::default(),
201 }
202 }
203 #[test]
204 fn replacement_threshold_is_inclusive_and_long_captures_get_no_replacements() {
205 let mut selection = selection(true);
206 assert_eq!(selection.recovery.replacement_exposures(30_000_000), 3);
207 assert_eq!(selection.recovery.replacement_exposures(30_000_001), 0);
208 let timing = NativeCameraTiming::new(&selection).unwrap();
209 assert!(
210 timing.capture_allowance(30_000_000).unwrap()
211 > timing.capture_allowance(30_000_001).unwrap()
212 );
213 selection.recovery.max_retries = 20;
214 assert_eq!(
215 NativeCameraTiming::new(&selection)
216 .unwrap()
217 .capture_allowance(600_000_000)
218 .unwrap(),
219 timing.capture_allowance(600_000_000).unwrap()
220 );
221 selection.recovery.direct_read_retries = 5;
222 assert!(
223 NativeCameraTiming::new(&selection)
224 .unwrap()
225 .capture_allowance(600_000_000)
226 .unwrap()
227 > timing.capture_allowance(600_000_000).unwrap(),
228 "retained rereads outlive the replacement threshold"
229 );
230 }
231 #[test]
232 fn usb_capture_allowance_requires_a_reachable_replacement_and_counts_one_reset() {
233 let mut selection = selection(true);
234 let base = NativeCameraTiming::new(&selection).unwrap();
235 selection.recovery.usb_reset_after_failures = 4;
236 assert_eq!(
237 NativeCameraTiming::new(&selection)
238 .unwrap()
239 .capture_allowance(1_000_000)
240 .unwrap(),
241 base.capture_allowance(1_000_000).unwrap()
242 );
243 selection.recovery.usb_reset_after_failures = 1;
244 let reset = NativeCameraTiming::new(&selection).unwrap();
245 assert_eq!(
246 reset.capture_allowance(1_000_000).unwrap()
247 - base.capture_allowance(1_000_000).unwrap(),
248 Duration::from_millis(140_500)
249 );
250 assert_eq!(
251 reset.capture_allowance(600_000_000).unwrap(),
252 base.capture_allowance(600_000_000).unwrap()
253 );
254 }
255 #[test]
256 fn sdk_fallback_and_late_control_restoration_are_included_only_for_native_paths() {
257 let mut selection = selection(true);
258 let direct = NativeCameraTiming::new(&selection).unwrap();
259 selection.sdk_fallback = true;
260 let fallback = NativeCameraTiming::new(&selection).unwrap();
261 assert!(fallback.connection_allowance() > direct.connection_allowance());
262 assert!(
263 fallback.capture_allowance(1_000_000).unwrap()
264 > direct.capture_allowance(1_000_000).unwrap()
265 );
266 selection.direct = false;
267 let sdk = NativeCameraTiming::new(&selection).unwrap();
268 selection.sdk_fallback = false;
269 let no_fallback = NativeCameraTiming::new(&selection).unwrap();
270 assert_eq!(
271 sdk.connection_allowance(),
272 no_fallback.connection_allowance()
273 );
274 assert_eq!(
275 sdk.capture_allowance(1_000_000).unwrap(),
276 no_fallback.capture_allowance(1_000_000).unwrap()
277 );
278 assert!(sdk.control_allowance() > Duration::from_secs(60));
279 assert!(sdk.connection_allowance() > Duration::from_secs(300));
280 assert!(sdk.capture_allowance(1_000_000).unwrap() > Duration::from_secs(300));
281 }
282 #[test]
283 fn all_accepted_policy_maxima_produce_representable_positive_allowances() {
284 use crate::recovery::RecoveryType;
285 let mut values = serde_json::Map::new();
286 for field in RecoveryOptions::fields() {
287 values.insert(
288 field.key.into(),
289 match field.value_type {
290 RecoveryType::Integer(_, max) | RecoveryType::PowerOfTwo(_, max) => {
291 serde_json::json!(max)
292 }
293 RecoveryType::Number { maximum, .. } => serde_json::json!(maximum),
294 RecoveryType::Boolean => serde_json::json!(true),
295 },
296 );
297 }
298 for direct in [false, true] {
299 let mut selection = selection(direct);
300 selection.recovery = serde_json::from_value(values.clone().into()).unwrap();
301 selection.sdk_fallback = true;
302 let timing = NativeCameraTiming::new(&selection).unwrap();
303 for duration in [
304 timing.connection_allowance(),
305 timing.control_allowance(),
306 timing.cleanup_allowance(),
307 timing.capture_allowance(86_400_000_000).unwrap(),
308 ] {
309 assert!(!duration.is_zero());
310 assert!(std::time::Instant::now().checked_add(duration).is_some());
311 }
312 assert!(timing.capture_allowance(u64::MAX).is_ok());
313 assert!(timing.capture_allowance(0).is_err());
314 }
315 }
316 #[test]
317 fn invalid_recovery_policy_never_reaches_duration_conversion() {
318 let mut selection = selection(false);
319 selection.recovery.command_timeout_seconds = f64::NAN;
320 assert!(NativeCameraTiming::new(&selection).is_err());
321 selection.recovery.command_timeout_seconds = 3600.01;
322 assert!(NativeCameraTiming::new(&selection).is_err());
323 }
324}