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 { message: String, code: Option<i32> },
14}
15impl std::fmt::Display for Failure {
16 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 match self {
18 Self::Invalid(m) | Self::Worker { message: m, .. } => f.write_str(m),
19 Self::Cancelled => f.write_str("Exposure aborted"),
20 }
21 }
22}
23impl std::error::Error for Failure {}
24pub fn invalid(message: impl Into<String>) -> anyhow::Error {
25 Failure::Invalid(message.into()).into()
26}
27pub fn retryable(error: &anyhow::Error) -> bool {
28 match error.downcast_ref::<Failure>() {
29 Some(Failure::Invalid(_) | Failure::Cancelled) => false,
30 Some(Failure::Worker { code: Some(c), .. }) => {
31 matches!(c, 1 | 2 | 4 | 5 | 11 | 12 | 15 | 16)
32 }
33 _ => true,
34 }
35}
36#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
37pub struct Exposure {
38 pub width: u32,
39 pub height: u32,
40 pub bin: u32,
41 pub x: u32,
42 pub y: u32,
43 pub microseconds: u64,
44 pub dark: bool,
45}
46impl Exposure {
47 pub fn bytes(&self) -> Result<usize> {
48 let count = u64::from(self.width) * u64::from(self.height) * 2;
49 ensure!(
50 count > 0 && count <= 512 * 1024 * 1024,
51 Failure::Invalid("Invalid frame size".into())
52 );
53 Ok(count as usize)
54 }
55}
56#[derive(Clone, Debug, Serialize, Deserialize)]
57pub struct Control {
58 #[serde(rename = "type")]
59 pub kind: i32,
60 pub min: i64,
61 pub max: i64,
62 pub value: i64,
63 pub writable: bool,
64}
65#[derive(Clone, Debug, Serialize, Deserialize)]
66#[serde(default, rename_all = "camelCase")]
67pub struct RecoveryOptions {
68 pub max_retries: u32,
69 pub maximum_retry_exposure_seconds: f64,
70 pub reconnect_delay_seconds: f64,
71 pub command_timeout_seconds: f64,
72 pub download_timeout_seconds: f64,
73 pub exposure_grace_seconds: f64,
74 pub cooling_timeout_seconds: f64,
75 pub temperature_tolerance_c: f64,
76 pub cooling_stable_samples: u32,
77 pub cooling_sample_seconds: f64,
78 pub ready_frame_download_retries: u32,
79 pub direct_read_retries: u32,
80 #[serde(rename = "directReadChunkKiB")]
82 pub direct_read_chunk_kib: u32,
83 pub usb_reset_after_failures: u32,
85 pub usb_port_cycle: bool,
87}
88impl Default for RecoveryOptions {
89 fn default() -> Self {
90 Self {
91 max_retries: 3,
92 maximum_retry_exposure_seconds: 30.,
93 reconnect_delay_seconds: 5.,
94 command_timeout_seconds: 15.,
95 download_timeout_seconds: 60.,
96 exposure_grace_seconds: 30.,
97 cooling_timeout_seconds: 300.,
98 temperature_tolerance_c: 2.,
99 cooling_stable_samples: 3,
100 cooling_sample_seconds: 2.,
101 ready_frame_download_retries: 2,
102 direct_read_retries: 2,
103 direct_read_chunk_kib: 1024,
104 usb_reset_after_failures: 0,
105 usb_port_cycle: false,
106 }
107 }
108}
109impl RecoveryOptions {
110 pub fn validate(&self) -> Result<()> {
111 ensure!(
112 (1..=1024).contains(&self.direct_read_chunk_kib)
113 && self.direct_read_chunk_kib.is_power_of_two(),
114 Failure::Invalid(
115 "Direct USB read size must be a power of two from 1 to 1024 KiB".into()
116 )
117 );
118 ensure!(
119 self.max_retries <= 20
120 && self.ready_frame_download_retries <= 5
121 && self.direct_read_retries <= 5
122 && self.usb_reset_after_failures <= 20
123 && (1..=60).contains(&self.cooling_stable_samples),
124 Failure::Invalid("Invalid retry limits".into())
125 );
126 ensure!(
127 self.maximum_retry_exposure_seconds.is_finite()
128 && (0.0..=86400.).contains(&self.maximum_retry_exposure_seconds),
129 Failure::Invalid("Invalid replacement exposure limit".into())
130 );
131 for v in [
132 self.reconnect_delay_seconds,
133 self.command_timeout_seconds,
134 self.download_timeout_seconds,
135 self.exposure_grace_seconds,
136 self.cooling_timeout_seconds,
137 self.temperature_tolerance_c,
138 self.cooling_sample_seconds,
139 ] {
140 ensure!(
141 v.is_finite() && v > 0. && v <= 3600.,
142 Failure::Invalid("Invalid recovery timeout or tolerance".into())
143 );
144 }
145 Ok(())
146 }
147}
148#[derive(Clone, Debug, Serialize, Deserialize)]
149#[serde(rename_all = "camelCase")]
150pub struct Selection {
151 pub name: String,
152 pub serial: Option<String>,
153 pub direct: bool,
154 pub sdk_fallback: bool,
155 pub recovery: RecoveryOptions,
156}
157#[derive(Clone, Debug, Default, Serialize)]
158#[serde(rename_all = "camelCase")]
159pub struct Status {
160 pub info: Value,
161 pub serial: Option<String>,
162 pub sdk_version: String,
163 pub backend: String,
164 pub sdk_fallback: bool,
165 pub phase: String,
166 pub error: Option<String>,
167 pub retry: RetryStatus,
168 pub controls: BTreeMap<i32, Control>,
169 pub values: BTreeMap<i32, i64>,
170 pub connected: bool,
171 pub control_connection_available: bool,
172 pub sdk_exposure_state: Option<i64>,
173 pub sdk_error_code: Option<i32>,
174 pub process_id: Option<u32>,
175 pub white_balance_capabilities: Value,
176 pub white_balance: Option<crate::white_balance::Settings>,
177}
178#[derive(Clone, Debug, Default, Serialize)]
181#[serde(rename_all = "camelCase")]
182pub struct RetryStatus {
183 pub recaptures: u32,
184 pub downloads: u32,
185 pub usb_reads: u32,
186 pub last_failure: Option<String>,
187}
188impl Status {
189 pub fn recovery_info(&self) -> String {
190 let retry = &self.retry;
191 let failure = retry.last_failure.as_deref().unwrap_or("none");
192 let failure = failure.split_whitespace().collect::<Vec<_>>().join(" ");
193 format!(
194 "state: {}; retries: {} (recaptures: {}, downloads: {}, USB reads: {}); last failure: {}",
195 if self.phase.is_empty() {
196 "Disconnected"
197 } else {
198 &self.phase
199 },
200 u64::from(retry.recaptures) + u64::from(retry.downloads) + u64::from(retry.usb_reads),
201 retry.recaptures,
202 retry.downloads,
203 retry.usb_reads,
204 failure
205 )
206 }
207}
208pub type SharedStatus = Arc<Mutex<Status>>;
209pub type Diagnostic = Arc<dyn Fn(&str, &str, &str) + Send + Sync>;
210#[derive(Clone)]
211pub struct Frame {
212 pub exposure: Exposure,
213 pub pixels: Arc<[u8]>,
214 pub metadata: Value,
215}
216
217pub fn validate_exposure(
218 info: &Value,
219 controls: &BTreeMap<i32, Control>,
220 e: &Exposure,
221) -> Result<()> {
222 e.bytes()?;
223 let bins = info["bins"]
224 .as_array()
225 .ok_or_else(|| invalid("Camera binning is unavailable"))?;
226 let w = info["width"].as_u64().unwrap_or(0);
227 let h = info["height"].as_u64().unwrap_or(0);
228 let exp = controls
229 .get(&1)
230 .ok_or_else(|| invalid("Exposure control unavailable"))?;
231 let alignment = info["originAlignment"]
232 .as_u64()
233 .or_else(|| info["originAlignmentX"].as_u64())
234 .unwrap_or(1)
235 .max(1);
236 let align_y = info["originAlignmentY"]
237 .as_u64()
238 .unwrap_or(if info["originAlignment"].is_number() {
239 2
240 } else {
241 1
242 })
243 .max(1);
244 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) &&
245 (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 &&
246 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) &&
247 u64::from(e.x)*u64::from(e.bin)%alignment==0 && u64::from(e.y)*u64::from(e.bin)%align_y==0 &&
248 e.microseconds>0 && e.microseconds>=exp.min.max(0) as u64 && e.microseconds<=exp.max.max(0) as u64,
249 Failure::Invalid("Exposure or ROI is outside camera capabilities (width multiple of 8, height multiple of 2; see camera alignment)".into()));
250 Ok(())
251}
252
253pub fn validate_capture(
256 info: &Value,
257 controls: &BTreeMap<i32, Control>,
258 e: &Exposure,
259 sdk_fallback: bool,
260) -> Result<()> {
261 if !sdk_fallback {
262 return validate_exposure(info, controls, e);
263 }
264 let mut info = info.clone();
265 info["minimumWidth"] = serde_json::json!(8);
266 info["minimumHeight"] = serde_json::json!(2);
267 info["originAlignment"] = serde_json::json!(1);
268 info["originAlignmentX"] = serde_json::json!(1);
269 info["originAlignmentY"] = serde_json::json!(1);
270 validate_exposure(&info, controls, e)
271}