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_POWER_LINE_FREQUENCY: u32 = 0x0098_0918;
31const CID_WHITE_BALANCE_TEMPERATURE: u32 = 0x0098_091a;
32const CID_SHARPNESS: u32 = 0x0098_091b;
33
34const CID_EXPOSURE_AUTO: u32 = 0x009a_0901;
36const CID_EXPOSURE_ABSOLUTE: u32 = 0x009a_0902;
37const CID_EXPOSURE_AUTO_PRIORITY: u32 = 0x009a_0903;
38const CID_FOCUS_ABSOLUTE: u32 = 0x009a_090a;
39const CID_FOCUS_AUTO: u32 = 0x009a_090c;
40const CID_ZOOM_ABSOLUTE: u32 = 0x009a_090d;
41
42const EXPOSURE_AUTO: i64 = 0;
44const EXPOSURE_MANUAL: i64 = 1;
45const EXPOSURE_SHUTTER_PRIORITY: i64 = 2;
46const EXPOSURE_APERTURE_PRIORITY: i64 = 3;
47
48fn control_id(control: CameraControl) -> Option<u32> {
55 Some(match control {
56 CameraControl::Zoom => CID_ZOOM_ABSOLUTE,
57 CameraControl::Focus => CID_FOCUS_ABSOLUTE,
58 CameraControl::Exposure => CID_EXPOSURE_ABSOLUTE,
59 CameraControl::PowerLineFrequency => CID_POWER_LINE_FREQUENCY,
60 CameraControl::LowLightCompensation => CID_EXPOSURE_AUTO_PRIORITY,
61 CameraControl::Brightness => CID_BRIGHTNESS,
62 CameraControl::Contrast => CID_CONTRAST,
63 CameraControl::Saturation => CID_SATURATION,
64 CameraControl::Sharpness => CID_SHARPNESS,
65 CameraControl::WhiteBalance => CID_WHITE_BALANCE_TEMPERATURE,
66 CameraControl::Tint => return None,
67 })
68}
69
70fn auto_id(toggle: AutoToggle) -> u32 {
72 match toggle {
73 AutoToggle::Focus => CID_FOCUS_AUTO,
74 AutoToggle::Exposure => CID_EXPOSURE_AUTO,
75 AutoToggle::WhiteBalance => CID_AUTO_WHITE_BALANCE,
76 }
77}
78
79fn open(unique_id: &str) -> Result<Device, ControlError> {
81 let path = linux::node_for_unique_id(unique_id).ok_or(ControlError::NotFound)?;
82 Device::with_path(&path).map_err(|error| ControlError::Io(error.to_string()))
83}
84
85pub fn control_range(
90 unique_id: &str,
91 control: CameraControl,
92) -> Result<ControlRange, ControlError> {
93 let device = open(unique_id)?;
94 let id = control_id(control).ok_or(ControlError::Unsupported)?;
95 let description = describe(&device, id).ok_or(ControlError::Unsupported)?;
96 range_of(&device, &description).ok_or(ControlError::Unsupported)
97}
98
99pub fn control_ranges(unique_id: &str) -> Result<Vec<(CameraControl, ControlRange)>, ControlError> {
104 let device = open(unique_id)?;
105 let descriptions = query(&device)?;
106
107 Ok(CameraControl::ALL
108 .into_iter()
109 .filter_map(|control| {
110 let id = control_id(control)?;
111 let description = descriptions.iter().find(|d| d.id == id)?;
112 Some((control, range_of(&device, description)?))
113 })
114 .collect())
115}
116
117pub fn read_camera_state(unique_id: &str) -> Result<CameraState, ControlError> {
122 let device = open(unique_id)?;
123 let descriptions = query(&device)?;
124
125 let controls = CameraControl::ALL
126 .into_iter()
127 .filter_map(|control| {
128 let id = control_id(control)?;
129 let description = descriptions.iter().find(|d| d.id == id)?;
130 Some((control, range_of(&device, description)?))
131 })
132 .collect();
133
134 let autos = AutoToggle::ALL
135 .into_iter()
136 .filter_map(|toggle| {
137 let id = auto_id(toggle);
138 let description = descriptions.iter().find(|d| d.id == id)?;
139 let current = read_auto(&device, toggle)?;
140 let default = if toggle == AutoToggle::Exposure {
141 is_auto_mode(description.default)
142 } else {
143 description.default != 0
144 };
145 Some((toggle, AutoState { current, default }))
146 })
147 .collect();
148
149 Ok(CameraState { controls, autos })
150}
151
152pub fn set_control(
158 unique_id: &str,
159 control: CameraControl,
160 value: i32,
161) -> Result<(), ControlError> {
162 let device = open(unique_id)?;
163 let id = control_id(control).ok_or(ControlError::Unsupported)?;
164 write_value(&device, id, i64::from(value))
165}
166
167pub fn set_auto(unique_id: &str, toggle: AutoToggle, on: bool) -> Result<(), ControlError> {
172 let device = open(unique_id)?;
173 write_auto(&device, toggle, on)
174}
175
176pub fn apply_settings(
190 unique_id: &str,
191 autos: &[(AutoToggle, bool)],
192 values: &[(CameraControl, i32)],
193) -> Result<(), ControlError> {
194 let device = open(unique_id)?;
195 let supported = query(&device)?;
196 let has = |id: u32| supported.iter().any(|d| d.id == id);
197
198 for &(toggle, on) in autos {
199 if has(auto_id(toggle)) {
200 write_auto(&device, toggle, on)?;
201 }
202 }
203
204 let writable: Vec<(u32, i64)> = values
205 .iter()
206 .filter(|&&(control, _)| !gated_by_enabled_auto(control, autos))
207 .filter_map(|&(control, value)| {
208 let id = control_id(control)?;
209 has(id).then_some((id, i64::from(value)))
210 })
211 .collect();
212
213 for class in [CLASS_USER, CLASS_CAMERA] {
214 let in_class = || {
215 writable
216 .iter()
217 .filter(move |(id, _)| id & CLASS_MASK == class)
218 };
219 let batch: Vec<Control> = in_class()
220 .map(|&(id, value)| Control {
221 id,
222 value: Value::Integer(value),
223 })
224 .collect();
225 if batch.is_empty() {
226 continue;
227 }
228 if device.set_controls(batch).is_err() {
233 for &(id, value) in in_class() {
234 match write_value(&device, id, value) {
235 Ok(()) | Err(ControlError::Unsupported) => {}
236 Err(error) => return Err(error),
237 }
238 }
239 }
240 }
241
242 Ok(())
243}
244
245fn gated_by_enabled_auto(control: CameraControl, autos: &[(AutoToggle, bool)]) -> bool {
252 control
253 .auto_toggle()
254 .is_some_and(|gate| autos.iter().any(|&(toggle, on)| toggle == gate && on))
255}
256
257const CLASS_MASK: u32 = 0xFFFF_0000;
259const CLASS_USER: u32 = 0x0098_0000;
260const CLASS_CAMERA: u32 = 0x009a_0000;
261
262fn query(device: &Device) -> Result<Vec<Description>, ControlError> {
264 device
265 .query_controls()
266 .map_err(|error| ControlError::Io(error.to_string()))
267}
268
269fn describe(device: &Device, id: u32) -> Option<Description> {
271 device
272 .query_controls()
273 .ok()?
274 .into_iter()
275 .find(|description| description.id == id)
276}
277
278fn range_of(device: &Device, description: &Description) -> Option<ControlRange> {
286 if description.flags.contains(Flags::DISABLED) {
287 return None;
288 }
289 let current = read_int(device, description.id).unwrap_or(description.default);
290 Some(ControlRange {
291 min: clamp_i32(description.minimum),
292 max: clamp_i32(description.maximum),
293 default: clamp_i32(description.default),
294 current: clamp_i32(current),
295 value_mask: description.items.as_ref().and_then(|items| {
296 items.iter().try_fold(0u32, |mask, (value, _)| {
297 (*value < u32::BITS).then_some(mask | (1u32 << *value))
298 })
299 }),
300 })
301}
302
303fn read_int(device: &Device, id: u32) -> Option<i64> {
305 match device.control(id).ok()?.value {
306 Value::Integer(value) => Some(value),
307 Value::Boolean(value) => Some(i64::from(value)),
308 _ => None,
309 }
310}
311
312fn read_auto(device: &Device, toggle: AutoToggle) -> Option<bool> {
314 let raw = read_int(device, auto_id(toggle))?;
315 Some(if toggle == AutoToggle::Exposure {
316 is_auto_mode(raw)
317 } else {
318 raw != 0
319 })
320}
321
322fn is_auto_mode(value: i64) -> bool {
327 value == EXPOSURE_AUTO || value == EXPOSURE_APERTURE_PRIORITY
328}
329
330fn write_auto(device: &Device, toggle: AutoToggle, on: bool) -> Result<(), ControlError> {
332 if toggle == AutoToggle::Exposure {
333 let mode = exposure_mode(device, on).ok_or(ControlError::Unsupported)?;
334 return write_value(device, CID_EXPOSURE_AUTO, mode);
335 }
336 let control = Control {
337 id: auto_id(toggle),
338 value: Value::Boolean(on),
339 };
340 device
341 .set_control(control)
342 .map_err(|error| ControlError::Io(error.to_string()))
343}
344
345fn exposure_mode(device: &Device, on: bool) -> Option<i64> {
352 let description = describe(device, CID_EXPOSURE_AUTO)?;
353 let offered = |value: i64| -> bool {
354 description.items.as_ref().map_or(
357 value >= description.minimum && value <= description.maximum,
358 |items| items.iter().any(|(index, _)| i64::from(*index) == value),
359 )
360 };
361
362 let preferences: [i64; 2] = if on {
363 [EXPOSURE_APERTURE_PRIORITY, EXPOSURE_AUTO]
364 } else {
365 [EXPOSURE_MANUAL, EXPOSURE_SHUTTER_PRIORITY]
366 };
367 preferences.into_iter().find(|&value| offered(value))
368}
369
370const REJECTED: [i32; 4] = [
374 22, 34, 13, 16, ];
379
380fn write_value(device: &Device, id: u32, value: i64) -> Result<(), ControlError> {
382 let control = Control {
383 id,
384 value: Value::Integer(value),
385 };
386 device.set_control(control).map_err(|error| {
387 if error
388 .raw_os_error()
389 .is_some_and(|no| REJECTED.contains(&no))
390 {
391 ControlError::Unsupported
392 } else {
393 ControlError::Io(error.to_string())
394 }
395 })
396}
397
398fn clamp_i32(value: i64) -> i32 {
403 i32::try_from(value).unwrap_or_else(|_| {
404 if value.is_negative() {
405 i32::MIN
406 } else {
407 i32::MAX
408 }
409 })
410}
411
412#[cfg(test)]
413mod tests {
414 use super::*;
415
416 #[test]
417 fn exposure_auto_maps_only_two_menu_values_to_automatic() {
418 assert!(is_auto_mode(EXPOSURE_AUTO));
419 assert!(is_auto_mode(EXPOSURE_APERTURE_PRIORITY));
420 assert!(!is_auto_mode(EXPOSURE_MANUAL));
421 assert!(!is_auto_mode(EXPOSURE_SHUTTER_PRIORITY));
422 }
423
424 #[test]
425 fn a_control_handed_to_auto_is_skipped() {
426 let autos = [(AutoToggle::Exposure, true)];
427 assert!(gated_by_enabled_auto(CameraControl::Exposure, &autos));
429 assert!(!gated_by_enabled_auto(CameraControl::Zoom, &autos));
432 assert!(!gated_by_enabled_auto(CameraControl::Focus, &autos));
433 }
434
435 #[test]
436 fn a_control_taken_off_auto_still_applies() {
437 let autos = [(AutoToggle::Focus, false)];
439 assert!(!gated_by_enabled_auto(CameraControl::Focus, &autos));
440 }
441
442 #[test]
443 fn an_unmentioned_toggle_leaves_its_control_writable() {
444 assert!(!gated_by_enabled_auto(CameraControl::WhiteBalance, &[]));
445 }
446
447 #[test]
448 fn every_supported_control_has_a_known_class() {
449 for control in CameraControl::ALL {
452 let Some(id) = control_id(control) else {
453 continue; };
455 let class = id & CLASS_MASK;
456 assert!(
457 class == CLASS_USER || class == CLASS_CAMERA,
458 "{} has class {class:#x}",
459 control.name()
460 );
461 }
462 }
463}