use v4l::Device;
use v4l::control::{Control, Description, Flags, Value};
use crate::controls::{
AutoState, AutoToggle, CameraControl, CameraState, ControlError, ControlRange,
};
use crate::linux;
const CID_BRIGHTNESS: u32 = 0x0098_0900;
const CID_CONTRAST: u32 = 0x0098_0901;
const CID_SATURATION: u32 = 0x0098_0902;
const CID_AUTO_WHITE_BALANCE: u32 = 0x0098_090c;
const CID_WHITE_BALANCE_TEMPERATURE: u32 = 0x0098_091a;
const CID_SHARPNESS: u32 = 0x0098_091b;
const CID_EXPOSURE_AUTO: u32 = 0x009a_0901;
const CID_EXPOSURE_ABSOLUTE: u32 = 0x009a_0902;
const CID_FOCUS_ABSOLUTE: u32 = 0x009a_090a;
const CID_FOCUS_AUTO: u32 = 0x009a_090c;
const CID_ZOOM_ABSOLUTE: u32 = 0x009a_090d;
const EXPOSURE_AUTO: i64 = 0;
const EXPOSURE_MANUAL: i64 = 1;
const EXPOSURE_SHUTTER_PRIORITY: i64 = 2;
const EXPOSURE_APERTURE_PRIORITY: i64 = 3;
fn control_id(control: CameraControl) -> Option<u32> {
Some(match control {
CameraControl::Zoom => CID_ZOOM_ABSOLUTE,
CameraControl::Focus => CID_FOCUS_ABSOLUTE,
CameraControl::Exposure => CID_EXPOSURE_ABSOLUTE,
CameraControl::Brightness => CID_BRIGHTNESS,
CameraControl::Contrast => CID_CONTRAST,
CameraControl::Saturation => CID_SATURATION,
CameraControl::Sharpness => CID_SHARPNESS,
CameraControl::WhiteBalance => CID_WHITE_BALANCE_TEMPERATURE,
CameraControl::Tint => return None,
})
}
fn auto_id(toggle: AutoToggle) -> u32 {
match toggle {
AutoToggle::Focus => CID_FOCUS_AUTO,
AutoToggle::Exposure => CID_EXPOSURE_AUTO,
AutoToggle::WhiteBalance => CID_AUTO_WHITE_BALANCE,
}
}
fn open(unique_id: &str) -> Result<Device, ControlError> {
let path = linux::node_for_unique_id(unique_id).ok_or(ControlError::NotFound)?;
Device::with_path(&path).map_err(|error| ControlError::Io(error.to_string()))
}
pub fn control_range(
unique_id: &str,
control: CameraControl,
) -> Result<ControlRange, ControlError> {
let device = open(unique_id)?;
let id = control_id(control).ok_or(ControlError::Unsupported)?;
let description = describe(&device, id).ok_or(ControlError::Unsupported)?;
range_of(&device, &description).ok_or(ControlError::Unsupported)
}
pub fn control_ranges(unique_id: &str) -> Result<Vec<(CameraControl, ControlRange)>, ControlError> {
let device = open(unique_id)?;
let descriptions = query(&device)?;
Ok(CameraControl::ALL
.into_iter()
.filter_map(|control| {
let id = control_id(control)?;
let description = descriptions.iter().find(|d| d.id == id)?;
Some((control, range_of(&device, description)?))
})
.collect())
}
pub fn read_camera_state(unique_id: &str) -> Result<CameraState, ControlError> {
let device = open(unique_id)?;
let descriptions = query(&device)?;
let controls = CameraControl::ALL
.into_iter()
.filter_map(|control| {
let id = control_id(control)?;
let description = descriptions.iter().find(|d| d.id == id)?;
Some((control, range_of(&device, description)?))
})
.collect();
let autos = AutoToggle::ALL
.into_iter()
.filter_map(|toggle| {
let id = auto_id(toggle);
let description = descriptions.iter().find(|d| d.id == id)?;
let current = read_auto(&device, toggle)?;
let default = if toggle == AutoToggle::Exposure {
is_auto_mode(description.default)
} else {
description.default != 0
};
Some((toggle, AutoState { current, default }))
})
.collect();
Ok(CameraState { controls, autos })
}
pub fn set_control(
unique_id: &str,
control: CameraControl,
value: i32,
) -> Result<(), ControlError> {
let device = open(unique_id)?;
let id = control_id(control).ok_or(ControlError::Unsupported)?;
write_value(&device, id, i64::from(value))
}
pub fn set_auto(unique_id: &str, toggle: AutoToggle, on: bool) -> Result<(), ControlError> {
let device = open(unique_id)?;
write_auto(&device, toggle, on)
}
pub fn apply_settings(
unique_id: &str,
autos: &[(AutoToggle, bool)],
values: &[(CameraControl, i32)],
) -> Result<(), ControlError> {
let device = open(unique_id)?;
let supported = query(&device)?;
let has = |id: u32| supported.iter().any(|d| d.id == id);
for &(toggle, on) in autos {
if has(auto_id(toggle)) {
write_auto(&device, toggle, on)?;
}
}
let writable: Vec<(u32, i64)> = values
.iter()
.filter(|&&(control, _)| !gated_by_enabled_auto(control, autos))
.filter_map(|&(control, value)| {
let id = control_id(control)?;
has(id).then_some((id, i64::from(value)))
})
.collect();
for class in [CLASS_USER, CLASS_CAMERA] {
let in_class = || {
writable
.iter()
.filter(move |(id, _)| id & CLASS_MASK == class)
};
let batch: Vec<Control> = in_class()
.map(|&(id, value)| Control {
id,
value: Value::Integer(value),
})
.collect();
if batch.is_empty() {
continue;
}
if device.set_controls(batch).is_err() {
for &(id, value) in in_class() {
match write_value(&device, id, value) {
Ok(()) | Err(ControlError::Unsupported) => {}
Err(error) => return Err(error),
}
}
}
}
Ok(())
}
fn gated_by_enabled_auto(control: CameraControl, autos: &[(AutoToggle, bool)]) -> bool {
control
.auto_toggle()
.is_some_and(|gate| autos.iter().any(|&(toggle, on)| toggle == gate && on))
}
const CLASS_MASK: u32 = 0xFFFF_0000;
const CLASS_USER: u32 = 0x0098_0000;
const CLASS_CAMERA: u32 = 0x009a_0000;
fn query(device: &Device) -> Result<Vec<Description>, ControlError> {
device
.query_controls()
.map_err(|error| ControlError::Io(error.to_string()))
}
fn describe(device: &Device, id: u32) -> Option<Description> {
device
.query_controls()
.ok()?
.into_iter()
.find(|description| description.id == id)
}
fn range_of(device: &Device, description: &Description) -> Option<ControlRange> {
if description.flags.contains(Flags::DISABLED) {
return None;
}
let current = read_int(device, description.id).unwrap_or(description.default);
Some(ControlRange {
min: clamp_i32(description.minimum),
max: clamp_i32(description.maximum),
default: clamp_i32(description.default),
current: clamp_i32(current),
})
}
fn read_int(device: &Device, id: u32) -> Option<i64> {
match device.control(id).ok()?.value {
Value::Integer(value) => Some(value),
Value::Boolean(value) => Some(i64::from(value)),
_ => None,
}
}
fn read_auto(device: &Device, toggle: AutoToggle) -> Option<bool> {
let raw = read_int(device, auto_id(toggle))?;
Some(if toggle == AutoToggle::Exposure {
is_auto_mode(raw)
} else {
raw != 0
})
}
fn is_auto_mode(value: i64) -> bool {
value == EXPOSURE_AUTO || value == EXPOSURE_APERTURE_PRIORITY
}
fn write_auto(device: &Device, toggle: AutoToggle, on: bool) -> Result<(), ControlError> {
if toggle == AutoToggle::Exposure {
let mode = exposure_mode(device, on).ok_or(ControlError::Unsupported)?;
return write_value(device, CID_EXPOSURE_AUTO, mode);
}
let control = Control {
id: auto_id(toggle),
value: Value::Boolean(on),
};
device
.set_control(control)
.map_err(|error| ControlError::Io(error.to_string()))
}
fn exposure_mode(device: &Device, on: bool) -> Option<i64> {
let description = describe(device, CID_EXPOSURE_AUTO)?;
let offered = |value: i64| -> bool {
description.items.as_ref().map_or(
value >= description.minimum && value <= description.maximum,
|items| items.iter().any(|(index, _)| i64::from(*index) == value),
)
};
let preferences: [i64; 2] = if on {
[EXPOSURE_APERTURE_PRIORITY, EXPOSURE_AUTO]
} else {
[EXPOSURE_MANUAL, EXPOSURE_SHUTTER_PRIORITY]
};
preferences.into_iter().find(|&value| offered(value))
}
const REJECTED: [i32; 4] = [
22, 34, 13, 16, ];
fn write_value(device: &Device, id: u32, value: i64) -> Result<(), ControlError> {
let control = Control {
id,
value: Value::Integer(value),
};
device.set_control(control).map_err(|error| {
if error
.raw_os_error()
.is_some_and(|no| REJECTED.contains(&no))
{
ControlError::Unsupported
} else {
ControlError::Io(error.to_string())
}
})
}
fn clamp_i32(value: i64) -> i32 {
i32::try_from(value).unwrap_or(if value.is_negative() {
i32::MIN
} else {
i32::MAX
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exposure_auto_maps_only_two_menu_values_to_automatic() {
assert!(is_auto_mode(EXPOSURE_AUTO));
assert!(is_auto_mode(EXPOSURE_APERTURE_PRIORITY));
assert!(!is_auto_mode(EXPOSURE_MANUAL));
assert!(!is_auto_mode(EXPOSURE_SHUTTER_PRIORITY));
}
#[test]
fn a_control_handed_to_auto_is_skipped() {
let autos = [(AutoToggle::Exposure, true)];
assert!(gated_by_enabled_auto(CameraControl::Exposure, &autos));
assert!(!gated_by_enabled_auto(CameraControl::Zoom, &autos));
assert!(!gated_by_enabled_auto(CameraControl::Focus, &autos));
}
#[test]
fn a_control_taken_off_auto_still_applies() {
let autos = [(AutoToggle::Focus, false)];
assert!(!gated_by_enabled_auto(CameraControl::Focus, &autos));
}
#[test]
fn an_unmentioned_toggle_leaves_its_control_writable() {
assert!(!gated_by_enabled_auto(CameraControl::WhiteBalance, &[]));
}
#[test]
fn every_supported_control_has_a_known_class() {
for control in CameraControl::ALL {
let Some(id) = control_id(control) else {
continue; };
let class = id & CLASS_MASK;
assert!(
class == CLASS_USER || class == CLASS_CAMERA,
"{} has class {class:#x}",
control.name()
);
}
}
}