1use v4l::Device;
18use v4l::control::{Control, Description, Flags, Value};
19
20use crate::controls::{
21 AutoState, AutoToggle, CameraControl, CameraState, ControlError, ControlRange,
22};
23use crate::linux;
24
25const CID_BRIGHTNESS: u32 = 0x0098_0900;
27const CID_CONTRAST: u32 = 0x0098_0901;
28const CID_SATURATION: u32 = 0x0098_0902;
29const CID_AUTO_WHITE_BALANCE: u32 = 0x0098_090c;
30const CID_WHITE_BALANCE_TEMPERATURE: u32 = 0x0098_091a;
31const CID_SHARPNESS: u32 = 0x0098_091b;
32
33const CID_EXPOSURE_AUTO: u32 = 0x009a_0901;
35const CID_EXPOSURE_ABSOLUTE: u32 = 0x009a_0902;
36const CID_FOCUS_ABSOLUTE: u32 = 0x009a_090a;
37const CID_FOCUS_AUTO: u32 = 0x009a_090c;
38const CID_ZOOM_ABSOLUTE: u32 = 0x009a_090d;
39
40const EXPOSURE_AUTO: i64 = 0;
42const EXPOSURE_MANUAL: i64 = 1;
43const EXPOSURE_SHUTTER_PRIORITY: i64 = 2;
44const EXPOSURE_APERTURE_PRIORITY: i64 = 3;
45
46fn control_id(control: CameraControl) -> Option<u32> {
53 Some(match control {
54 CameraControl::Zoom => CID_ZOOM_ABSOLUTE,
55 CameraControl::Focus => CID_FOCUS_ABSOLUTE,
56 CameraControl::Exposure => CID_EXPOSURE_ABSOLUTE,
57 CameraControl::Brightness => CID_BRIGHTNESS,
58 CameraControl::Contrast => CID_CONTRAST,
59 CameraControl::Saturation => CID_SATURATION,
60 CameraControl::Sharpness => CID_SHARPNESS,
61 CameraControl::WhiteBalance => CID_WHITE_BALANCE_TEMPERATURE,
62 CameraControl::Tint => return None,
63 })
64}
65
66fn auto_id(toggle: AutoToggle) -> u32 {
68 match toggle {
69 AutoToggle::Focus => CID_FOCUS_AUTO,
70 AutoToggle::Exposure => CID_EXPOSURE_AUTO,
71 AutoToggle::WhiteBalance => CID_AUTO_WHITE_BALANCE,
72 }
73}
74
75fn open(unique_id: &str) -> Result<Device, ControlError> {
77 let path = linux::node_for_unique_id(unique_id).ok_or(ControlError::NotFound)?;
78 Device::with_path(&path).map_err(|error| ControlError::Io(error.to_string()))
79}
80
81pub fn control_range(
86 unique_id: &str,
87 control: CameraControl,
88) -> Result<ControlRange, ControlError> {
89 let device = open(unique_id)?;
90 let id = control_id(control).ok_or(ControlError::Unsupported)?;
91 let description = describe(&device, id).ok_or(ControlError::Unsupported)?;
92 range_of(&device, &description).ok_or(ControlError::Unsupported)
93}
94
95pub fn control_ranges(unique_id: &str) -> Result<Vec<(CameraControl, ControlRange)>, ControlError> {
100 let device = open(unique_id)?;
101 let descriptions = query(&device)?;
102
103 Ok(CameraControl::ALL
104 .into_iter()
105 .filter_map(|control| {
106 let id = control_id(control)?;
107 let description = descriptions.iter().find(|d| d.id == id)?;
108 Some((control, range_of(&device, description)?))
109 })
110 .collect())
111}
112
113pub fn read_camera_state(unique_id: &str) -> Result<CameraState, ControlError> {
118 let device = open(unique_id)?;
119 let descriptions = query(&device)?;
120
121 let controls = CameraControl::ALL
122 .into_iter()
123 .filter_map(|control| {
124 let id = control_id(control)?;
125 let description = descriptions.iter().find(|d| d.id == id)?;
126 Some((control, range_of(&device, description)?))
127 })
128 .collect();
129
130 let autos = AutoToggle::ALL
131 .into_iter()
132 .filter_map(|toggle| {
133 let id = auto_id(toggle);
134 let description = descriptions.iter().find(|d| d.id == id)?;
135 let current = read_auto(&device, toggle)?;
136 let default = if toggle == AutoToggle::Exposure {
137 is_auto_mode(description.default)
138 } else {
139 description.default != 0
140 };
141 Some((toggle, AutoState { current, default }))
142 })
143 .collect();
144
145 Ok(CameraState { controls, autos })
146}
147
148pub fn set_control(
154 unique_id: &str,
155 control: CameraControl,
156 value: i32,
157) -> Result<(), ControlError> {
158 let device = open(unique_id)?;
159 let id = control_id(control).ok_or(ControlError::Unsupported)?;
160 write_value(&device, id, i64::from(value))
161}
162
163pub fn set_auto(unique_id: &str, toggle: AutoToggle, on: bool) -> Result<(), ControlError> {
168 let device = open(unique_id)?;
169 write_auto(&device, toggle, on)
170}
171
172pub fn apply_settings(
186 unique_id: &str,
187 autos: &[(AutoToggle, bool)],
188 values: &[(CameraControl, i32)],
189) -> Result<(), ControlError> {
190 let device = open(unique_id)?;
191 let supported = query(&device)?;
192 let has = |id: u32| supported.iter().any(|d| d.id == id);
193
194 for &(toggle, on) in autos {
195 if has(auto_id(toggle)) {
196 write_auto(&device, toggle, on)?;
197 }
198 }
199
200 let writable: Vec<(u32, i64)> = values
201 .iter()
202 .filter(|&&(control, _)| !gated_by_enabled_auto(control, autos))
203 .filter_map(|&(control, value)| {
204 let id = control_id(control)?;
205 has(id).then_some((id, i64::from(value)))
206 })
207 .collect();
208
209 for class in [CLASS_USER, CLASS_CAMERA] {
210 let in_class = || {
211 writable
212 .iter()
213 .filter(move |(id, _)| id & CLASS_MASK == class)
214 };
215 let batch: Vec<Control> = in_class()
216 .map(|&(id, value)| Control {
217 id,
218 value: Value::Integer(value),
219 })
220 .collect();
221 if batch.is_empty() {
222 continue;
223 }
224 if device.set_controls(batch).is_err() {
229 for &(id, value) in in_class() {
230 match write_value(&device, id, value) {
231 Ok(()) | Err(ControlError::Unsupported) => {}
232 Err(error) => return Err(error),
233 }
234 }
235 }
236 }
237
238 Ok(())
239}
240
241fn gated_by_enabled_auto(control: CameraControl, autos: &[(AutoToggle, bool)]) -> bool {
248 control
249 .auto_toggle()
250 .is_some_and(|gate| autos.iter().any(|&(toggle, on)| toggle == gate && on))
251}
252
253const CLASS_MASK: u32 = 0xFFFF_0000;
255const CLASS_USER: u32 = 0x0098_0000;
256const CLASS_CAMERA: u32 = 0x009a_0000;
257
258fn query(device: &Device) -> Result<Vec<Description>, ControlError> {
260 device
261 .query_controls()
262 .map_err(|error| ControlError::Io(error.to_string()))
263}
264
265fn describe(device: &Device, id: u32) -> Option<Description> {
267 device
268 .query_controls()
269 .ok()?
270 .into_iter()
271 .find(|description| description.id == id)
272}
273
274fn range_of(device: &Device, description: &Description) -> Option<ControlRange> {
282 if description.flags.contains(Flags::DISABLED) {
283 return None;
284 }
285 let current = read_int(device, description.id).unwrap_or(description.default);
286 Some(ControlRange {
287 min: clamp_i32(description.minimum),
288 max: clamp_i32(description.maximum),
289 default: clamp_i32(description.default),
290 current: clamp_i32(current),
291 })
292}
293
294fn read_int(device: &Device, id: u32) -> Option<i64> {
296 match device.control(id).ok()?.value {
297 Value::Integer(value) => Some(value),
298 Value::Boolean(value) => Some(i64::from(value)),
299 _ => None,
300 }
301}
302
303fn read_auto(device: &Device, toggle: AutoToggle) -> Option<bool> {
305 let raw = read_int(device, auto_id(toggle))?;
306 Some(if toggle == AutoToggle::Exposure {
307 is_auto_mode(raw)
308 } else {
309 raw != 0
310 })
311}
312
313fn is_auto_mode(value: i64) -> bool {
318 value == EXPOSURE_AUTO || value == EXPOSURE_APERTURE_PRIORITY
319}
320
321fn write_auto(device: &Device, toggle: AutoToggle, on: bool) -> Result<(), ControlError> {
323 if toggle == AutoToggle::Exposure {
324 let mode = exposure_mode(device, on).ok_or(ControlError::Unsupported)?;
325 return write_value(device, CID_EXPOSURE_AUTO, mode);
326 }
327 let control = Control {
328 id: auto_id(toggle),
329 value: Value::Boolean(on),
330 };
331 device
332 .set_control(control)
333 .map_err(|error| ControlError::Io(error.to_string()))
334}
335
336fn exposure_mode(device: &Device, on: bool) -> Option<i64> {
343 let description = describe(device, CID_EXPOSURE_AUTO)?;
344 let offered = |value: i64| -> bool {
345 description.items.as_ref().map_or(
348 value >= description.minimum && value <= description.maximum,
349 |items| items.iter().any(|(index, _)| i64::from(*index) == value),
350 )
351 };
352
353 let preferences: [i64; 2] = if on {
354 [EXPOSURE_APERTURE_PRIORITY, EXPOSURE_AUTO]
355 } else {
356 [EXPOSURE_MANUAL, EXPOSURE_SHUTTER_PRIORITY]
357 };
358 preferences.into_iter().find(|&value| offered(value))
359}
360
361const REJECTED: [i32; 4] = [
365 22, 34, 13, 16, ];
370
371fn write_value(device: &Device, id: u32, value: i64) -> Result<(), ControlError> {
373 let control = Control {
374 id,
375 value: Value::Integer(value),
376 };
377 device.set_control(control).map_err(|error| {
378 if error
379 .raw_os_error()
380 .is_some_and(|no| REJECTED.contains(&no))
381 {
382 ControlError::Unsupported
383 } else {
384 ControlError::Io(error.to_string())
385 }
386 })
387}
388
389fn clamp_i32(value: i64) -> i32 {
394 i32::try_from(value).unwrap_or(if value.is_negative() {
395 i32::MIN
396 } else {
397 i32::MAX
398 })
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
406 fn exposure_auto_maps_only_two_menu_values_to_automatic() {
407 assert!(is_auto_mode(EXPOSURE_AUTO));
408 assert!(is_auto_mode(EXPOSURE_APERTURE_PRIORITY));
409 assert!(!is_auto_mode(EXPOSURE_MANUAL));
410 assert!(!is_auto_mode(EXPOSURE_SHUTTER_PRIORITY));
411 }
412
413 #[test]
414 fn a_control_handed_to_auto_is_skipped() {
415 let autos = [(AutoToggle::Exposure, true)];
416 assert!(gated_by_enabled_auto(CameraControl::Exposure, &autos));
418 assert!(!gated_by_enabled_auto(CameraControl::Zoom, &autos));
421 assert!(!gated_by_enabled_auto(CameraControl::Focus, &autos));
422 }
423
424 #[test]
425 fn a_control_taken_off_auto_still_applies() {
426 let autos = [(AutoToggle::Focus, false)];
428 assert!(!gated_by_enabled_auto(CameraControl::Focus, &autos));
429 }
430
431 #[test]
432 fn an_unmentioned_toggle_leaves_its_control_writable() {
433 assert!(!gated_by_enabled_auto(CameraControl::WhiteBalance, &[]));
434 }
435
436 #[test]
437 fn every_supported_control_has_a_known_class() {
438 for control in CameraControl::ALL {
441 let Some(id) = control_id(control) else {
442 continue; };
444 let class = id & CLASS_MASK;
445 assert!(
446 class == CLASS_USER || class == CLASS_CAMERA,
447 "{} has class {class:#x}",
448 control.name()
449 );
450 }
451 }
452}