Skip to main content

openlogi_camera/
controls.rs

1//! Platform-independent control vocabulary shared by every UVC backend
2//! (IOKit on macOS, DirectShow on Windows, stubs elsewhere).
3
4use thiserror::Error;
5
6/// One adjustable camera control, mapped to a UVC selector by each backend.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum CameraControl {
9    Zoom,
10    Focus,
11    Exposure,
12    PowerLineFrequency,
13    LowLightCompensation,
14    Brightness,
15    Contrast,
16    Saturation,
17    Sharpness,
18    WhiteBalance,
19    Tint,
20}
21
22impl CameraControl {
23    /// Every control, in the order the UI lists them (lens first, then image).
24    pub const ALL: [Self; 11] = [
25        Self::Zoom,
26        Self::Focus,
27        Self::Exposure,
28        Self::PowerLineFrequency,
29        Self::LowLightCompensation,
30        Self::Brightness,
31        Self::Contrast,
32        Self::Saturation,
33        Self::Sharpness,
34        Self::WhiteBalance,
35        Self::Tint,
36    ];
37
38    /// Stable snake_case identifier used for config persistence and the CLI.
39    #[must_use]
40    pub fn name(self) -> &'static str {
41        match self {
42            Self::Zoom => "zoom",
43            Self::Focus => "focus",
44            Self::Exposure => "exposure",
45            Self::PowerLineFrequency => "power_line_frequency",
46            Self::LowLightCompensation => "low_light_compensation",
47            Self::Brightness => "brightness",
48            Self::Contrast => "contrast",
49            Self::Saturation => "saturation",
50            Self::Sharpness => "sharpness",
51            Self::WhiteBalance => "white_balance",
52            Self::Tint => "tint",
53        }
54    }
55
56    /// The auto-mode toggle that gates this control, if the device has one.
57    #[must_use]
58    pub fn auto_toggle(self) -> Option<AutoToggle> {
59        match self {
60            Self::Focus => Some(AutoToggle::Focus),
61            Self::Exposure => Some(AutoToggle::Exposure),
62            Self::WhiteBalance => Some(AutoToggle::WhiteBalance),
63            _ => None,
64        }
65    }
66}
67
68/// An auto-mode toggle paired with a manual control (focus / exposure / white
69/// balance).
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum AutoToggle {
72    Focus,
73    Exposure,
74    WhiteBalance,
75}
76
77impl AutoToggle {
78    /// Every toggle, matching [`CameraControl::auto_toggle`] pairs.
79    pub const ALL: [Self; 3] = [Self::Focus, Self::Exposure, Self::WhiteBalance];
80
81    /// Stable snake_case identifier used for config persistence and the CLI.
82    #[must_use]
83    pub fn name(self) -> &'static str {
84        match self {
85            Self::Focus => "focus_auto",
86            Self::Exposure => "exposure_auto",
87            Self::WhiteBalance => "white_balance_auto",
88        }
89    }
90}
91
92/// One auto toggle's live and default state, read from the device.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub struct AutoState {
95    pub current: bool,
96    pub default: bool,
97}
98
99/// Everything the controls UI needs, read in a single device-open: each
100/// supported control's range and each supported auto toggle's state.
101#[derive(Debug, Clone, Default)]
102pub struct CameraState {
103    pub controls: Vec<(CameraControl, ControlRange)>,
104    pub autos: Vec<(AutoToggle, AutoState)>,
105}
106
107/// The device's reported range and current value for a control.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub struct ControlRange {
110    pub min: i32,
111    pub max: i32,
112    pub default: i32,
113    pub current: i32,
114    /// Bit `n` is set when discrete value `n` is supported. `None` means every
115    /// value in the range is available.
116    pub value_mask: Option<u32>,
117}
118
119impl ControlRange {
120    /// Whether the device reports `value` as supported.
121    #[must_use]
122    pub fn supports(self, value: i32) -> bool {
123        if !(self.min..=self.max).contains(&value) {
124            return false;
125        }
126        self.value_mask.is_none_or(|mask| {
127            u32::try_from(value)
128                .ok()
129                .filter(|value| *value < u32::BITS)
130                .is_some_and(|value| mask & (1 << value) != 0)
131        })
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::ControlRange;
138
139    #[test]
140    fn discrete_range_rejects_missing_values() {
141        let range = ControlRange {
142            min: 0,
143            max: 3,
144            default: 1,
145            current: 1,
146            value_mask: Some((1 << 1) | (1 << 3)),
147        };
148
149        assert!(range.supports(1));
150        assert!(!range.supports(2));
151        assert!(range.supports(3));
152    }
153}
154
155/// Why a UVC control operation failed.
156#[derive(Debug, Clone, Error)]
157pub enum ControlError {
158    /// No matching camera device (or it exposes no controllable unit).
159    #[error("no matching UVC device")]
160    NotFound,
161    /// The selected camera can't be uniquely identified: its unique id didn't
162    /// resolve to a USB location and more than one Logitech camera is attached,
163    /// so a write could hit the wrong device. Fails closed instead of guessing.
164    #[error("camera could not be uniquely identified")]
165    Ambiguous,
166    /// The camera rejected or didn't support the control — or the platform
167    /// has no UVC control backend at all.
168    #[error("camera does not support that control")]
169    Unsupported,
170    /// A platform API call failed (open, bind, or the control transfer).
171    #[error("platform error: {0}")]
172    Io(String),
173}