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