1use anyhow::{Result, ensure};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::{
5 collections::BTreeMap,
6 sync::{Arc, Mutex},
7};
8
9#[derive(Debug)]
10pub enum Failure {
11 Invalid(String),
12 Cancelled,
13 Worker {
14 message: String,
15 code: Option<i32>,
16 },
17 UncertainControl {
20 message: String,
21 code: Option<i32>,
22 },
23}
24impl std::fmt::Display for Failure {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 match self {
27 Self::Invalid(m)
28 | Self::Worker { message: m, .. }
29 | Self::UncertainControl { message: m, .. } => f.write_str(m),
30 Self::Cancelled => f.write_str("Exposure aborted"),
31 }
32 }
33}
34impl std::error::Error for Failure {}
35pub fn invalid(message: impl Into<String>) -> anyhow::Error {
36 Failure::Invalid(message.into()).into()
37}
38pub fn retryable(error: &anyhow::Error) -> bool {
39 match error.downcast_ref::<Failure>() {
40 Some(Failure::Invalid(_) | Failure::Cancelled | Failure::UncertainControl { .. }) => false,
41 Some(Failure::Worker { code: Some(c), .. }) => {
42 matches!(c, 1 | 2 | 4 | 5 | 11 | 12 | 15 | 16)
43 }
44 _ => true,
45 }
46}
47#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
48pub struct Exposure {
49 pub width: u32,
50 pub height: u32,
51 pub bin: u32,
52 pub x: u32,
53 pub y: u32,
54 pub microseconds: u64,
55 pub dark: bool,
56}
57impl Exposure {
58 pub fn bytes(&self) -> Result<usize> {
59 let count = u64::from(self.width)
60 .checked_mul(u64::from(self.height))
61 .and_then(|pixels| pixels.checked_mul(2))
62 .ok_or_else(|| invalid("Invalid frame size"))?;
63 ensure!(
64 count > 0 && count <= 512 * 1024 * 1024,
65 Failure::Invalid("Invalid frame size".into())
66 );
67 Ok(count as usize)
68 }
69}
70#[derive(Clone, Debug, Serialize, Deserialize)]
71pub struct Control {
72 #[serde(rename = "type")]
73 pub kind: i32,
74 pub min: i64,
75 pub max: i64,
76 pub value: i64,
77 pub writable: bool,
78}
79pub use crate::recovery::RecoveryOptions;
80#[derive(Clone, Debug, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub struct Selection {
83 pub name: String,
84 pub serial: Option<String>,
85 pub direct: bool,
86 pub sdk_fallback: bool,
87 pub recovery: RecoveryOptions,
88}
89#[derive(Clone, Debug, Default, Serialize)]
90#[serde(rename_all = "camelCase")]
91pub struct Status {
92 pub info: Value,
93 pub serial: Option<String>,
94 pub sdk_version: String,
95 pub backend: String,
96 pub sdk_fallback: bool,
97 pub phase: String,
98 pub error: Option<String>,
99 pub retry: RetryStatus,
100 pub controls: BTreeMap<i32, Control>,
101 pub values: BTreeMap<i32, i64>,
102 #[serde(skip)]
105 pub observations: BTreeMap<i32, ControlObservation>,
106 pub connected: bool,
107 pub control_connection_available: bool,
108 pub usb_peripheral_recovery_id: Option<u64>,
111 pub sdk_exposure_state: Option<i64>,
112 pub sdk_error_code: Option<i32>,
113 pub process_id: Option<u32>,
114 pub white_balance_capabilities: Value,
115 pub white_balance: Option<crate::white_balance::Settings>,
116}
117#[derive(Clone, Debug, Default, Serialize)]
120#[serde(rename_all = "camelCase")]
121pub struct RetryStatus {
122 pub recaptures: u32,
123 pub downloads: u32,
124 pub usb_reads: u32,
125 pub last_failure: Option<String>,
126}
127impl Status {
128 pub fn recovery_info(&self) -> String {
129 let retry = &self.retry;
130 let failure = retry.last_failure.as_deref().unwrap_or("none");
131 let failure = failure.split_whitespace().collect::<Vec<_>>().join(" ");
132 format!(
133 "state: {}; retries: {} (recaptures: {}, downloads: {}, USB reads: {}); last failure: {}",
134 if self.phase.is_empty() {
135 "Disconnected"
136 } else {
137 &self.phase
138 },
139 u64::from(retry.recaptures) + u64::from(retry.downloads) + u64::from(retry.usb_reads),
140 retry.recaptures,
141 retry.downloads,
142 retry.usb_reads,
143 failure
144 )
145 }
146}
147pub type SharedStatus = Arc<Mutex<Status>>;
148pub type Diagnostic = Arc<dyn Fn(&str, &str, &str) + Send + Sync>;
151#[derive(Clone, Copy, Debug, PartialEq, Eq)]
152pub struct ControlObservation {
153 pub value: i64,
154 pub observed_at: tokio::time::Instant,
155}
156#[derive(Clone, Debug, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase", deny_unknown_fields)]
160pub struct ControlObservationReply {
161 pub value: i64,
162 pub age_seconds: f64,
163}
164impl ControlObservationReply {
165 pub fn normalize(&self, request_started: tokio::time::Instant) -> Result<ControlObservation> {
166 Ok(ControlObservation {
167 value: self.value,
168 observed_at: self.observed_at(request_started)?,
169 })
170 }
171 pub fn observed_at(
175 &self,
176 request_started: tokio::time::Instant,
177 ) -> Result<tokio::time::Instant> {
178 let age = std::time::Duration::try_from_secs_f64(self.age_seconds)
179 .map_err(|_| invalid("Invalid control observation age"))?;
180 request_started
181 .checked_sub(age)
182 .ok_or_else(|| invalid("Unrepresentable control observation time"))
183 }
184}
185#[derive(Clone)]
186pub struct Frame {
187 pub exposure: Exposure,
188 pub pixels: Arc<[u8]>,
189 pub metadata: Value,
190}
191
192pub fn validate_exposure(
193 info: &Value,
194 controls: &BTreeMap<i32, Control>,
195 e: &Exposure,
196) -> Result<()> {
197 e.bytes()?;
198 let bins = info["bins"]
199 .as_array()
200 .ok_or_else(|| invalid("Camera binning is unavailable"))?;
201 let w = info["width"].as_u64().unwrap_or(0);
202 let h = info["height"].as_u64().unwrap_or(0);
203 let exp = controls
204 .get(&1)
205 .ok_or_else(|| invalid("Exposure control unavailable"))?;
206 let alignment = info["originAlignment"]
207 .as_u64()
208 .or_else(|| info["originAlignmentX"].as_u64())
209 .unwrap_or(1)
210 .max(1);
211 let align_y = info["originAlignmentY"]
212 .as_u64()
213 .unwrap_or(if info["originAlignment"].is_number() {
214 2
215 } else {
216 1
217 })
218 .max(1);
219 ensure!(e.bin>0 && bins.iter().any(|b|b.as_u64()==Some(e.bin as u64)) && e.width.is_multiple_of(8) && e.height.is_multiple_of(2) &&
220 (u64::from(e.x)+u64::from(e.width))*u64::from(e.bin)<=w && (u64::from(e.y)+u64::from(e.height))*u64::from(e.bin)<=h &&
221 u64::from(e.width)*u64::from(e.bin)>=info["minimumWidth"].as_u64().unwrap_or(8) && u64::from(e.height)*u64::from(e.bin)>=info["minimumHeight"].as_u64().unwrap_or(2) &&
222 u64::from(e.x)*u64::from(e.bin)%alignment==0 && u64::from(e.y)*u64::from(e.bin)%align_y==0 &&
223 e.microseconds>0 && e.microseconds>=exp.min.max(0) as u64 && e.microseconds<=exp.max.max(0) as u64,
224 Failure::Invalid("Exposure or ROI is outside camera capabilities (width multiple of 8, height multiple of 2; see camera alignment)".into()));
225 Ok(())
226}
227
228pub fn validate_capture(
231 info: &Value,
232 controls: &BTreeMap<i32, Control>,
233 e: &Exposure,
234 sdk_fallback: bool,
235) -> Result<()> {
236 if !sdk_fallback {
237 return validate_exposure(info, controls, e);
238 }
239 let mut info = info.clone();
240 info["minimumWidth"] = serde_json::json!(8);
241 info["minimumHeight"] = serde_json::json!(2);
242 info["originAlignment"] = serde_json::json!(1);
243 info["originAlignmentX"] = serde_json::json!(1);
244 info["originAlignmentY"] = serde_json::json!(1);
245 validate_exposure(&info, controls, e)
246}
247
248#[cfg(test)]
249mod observation_tests {
250 use super::*;
251 use std::time::Duration;
252 use tokio::time::Instant;
253
254 #[test]
255 fn worker_age_normalization_includes_transit_and_rejects_invalid_times() {
256 let request = Instant::now();
257 let reply = ControlObservationReply {
258 value: -100,
259 age_seconds: 20.25,
260 };
261 let observed = reply.observed_at(request).unwrap();
262 let received = request + Duration::from_secs(5);
263 assert_eq!(
264 received.duration_since(observed),
265 Duration::from_secs_f64(25.25)
266 );
267 for invalid_age in [-1.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY, f64::MAX] {
268 assert!(
269 ControlObservationReply {
270 value: 0,
271 age_seconds: invalid_age
272 }
273 .observed_at(request)
274 .is_err()
275 );
276 }
277 assert_eq!(
278 ControlObservationReply {
279 value: 0,
280 age_seconds: 0.0
281 }
282 .observed_at(request)
283 .unwrap(),
284 request
285 );
286 for invalid in [
287 serde_json::json!({"value":true,"ageSeconds":0}),
288 serde_json::json!({"value":0,"ageSeconds":"0"}),
289 serde_json::json!({"value":0}),
290 serde_json::json!({"value":0,"ageSeconds":0,"timestamp":"private clock epoch"}),
291 ] {
292 assert!(serde_json::from_value::<ControlObservationReply>(invalid).is_err());
293 }
294 }
295}