#![allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
reason = "UVC payloads are bounded 16-bit values copied verbatim"
)]
mod iokit;
use std::collections::HashMap;
use std::ffi::c_void;
use objc2_core_foundation::{CFNumber, CFString};
use objc2_io_kit::IOUSBDevRequest;
use iokit::{IoObject, SeizedDevice, UsbInterface};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Unit {
CameraTerminal,
Processing,
}
pub use crate::controls::{
AutoState, AutoToggle, CameraControl, CameraState, ControlError, ControlRange,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Payload {
U16,
I16,
U32,
}
impl Payload {
const fn len(self) -> usize {
match self {
Self::U16 | Self::I16 => 2,
Self::U32 => 4,
}
}
}
struct ControlSpec {
unit: Unit,
selector: u16,
payload: Payload,
}
impl CameraControl {
const fn spec(self) -> ControlSpec {
use Payload::{I16, U16, U32};
use Unit::{CameraTerminal, Processing};
let (unit, selector, payload) = match self {
Self::Zoom => (CameraTerminal, 0x0B, U16), Self::Focus => (CameraTerminal, 0x06, U16), Self::Exposure => (CameraTerminal, 0x04, U32), Self::Brightness => (Processing, 0x02, I16), Self::Contrast => (Processing, 0x03, U16), Self::Saturation => (Processing, 0x07, U16), Self::Sharpness => (Processing, 0x08, U16), Self::WhiteBalance => (Processing, 0x0A, U16), Self::Tint => (Processing, 0x06, I16), };
ControlSpec {
unit,
selector,
payload,
}
}
}
struct ToggleSpec {
unit: Unit,
selector: u16,
}
impl AutoToggle {
const fn spec(self) -> ToggleSpec {
use Unit::{CameraTerminal, Processing};
let (unit, selector) = match self {
Self::Focus => (CameraTerminal, 0x08), Self::Exposure => (CameraTerminal, 0x02), Self::WhiteBalance => (Processing, 0x0B), };
ToggleSpec { unit, selector }
}
}
const UVC_SET_CUR: u8 = 0x01;
const UVC_GET_CUR: u8 = 0x81;
const UVC_GET_MIN: u8 = 0x82;
const UVC_GET_MAX: u8 = 0x83;
const UVC_GET_DEF: u8 = 0x87;
const RT_GET: u8 = 0xA1; const RT_SET: u8 = 0x21;
const CC_VIDEO: u8 = 0x0E;
const SC_VIDEOCONTROL: u8 = 0x01;
const DESC_INTERFACE: u8 = 0x04;
const DESC_CS_INTERFACE: u8 = 0x24;
const VC_INPUT_TERMINAL: u8 = 0x02;
const VC_PROCESSING_UNIT: u8 = 0x05;
const ITT_CAMERA: u16 = 0x0201;
const AE_MANUAL: u8 = 0x01;
const AE_AUTO_MODES: [u8; 3] = [0x02, 0x08, 0x04];
fn quiesce() -> std::sync::MutexGuard<'static, ()> {
crate::USB_QUIESCE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub fn control_range(
unique_id: &str,
control: CameraControl,
) -> Result<ControlRange, ControlError> {
let _quiesce = quiesce();
let dev = UsbDevice::open_for(unique_id)?;
let min = dev.get(control, UVC_GET_MIN)?;
let max = dev.get(control, UVC_GET_MAX)?;
let default = dev.get(control, UVC_GET_DEF)?;
let current = dev.get(control, UVC_GET_CUR).unwrap_or(default);
Ok(ControlRange {
min,
max,
default,
current,
})
}
pub fn control_ranges(unique_id: &str) -> Result<Vec<(CameraControl, ControlRange)>, ControlError> {
Ok(read_camera_state(unique_id)?.controls)
}
pub fn read_camera_state(unique_id: &str) -> Result<CameraState, ControlError> {
let _quiesce = quiesce();
let dev = UsbDevice::open_for(unique_id)?;
let mut state = CameraState::default();
for control in CameraControl::ALL {
if let (Ok(min), Ok(max), Ok(default)) = (
dev.get(control, UVC_GET_MIN),
dev.get(control, UVC_GET_MAX),
dev.get(control, UVC_GET_DEF),
) {
let current = dev.get(control, UVC_GET_CUR).unwrap_or(default);
state.controls.push((
control,
ControlRange {
min,
max,
default,
current,
},
));
}
}
for toggle in AutoToggle::ALL {
if let (Ok(current), Ok(default)) = (
dev.get_auto(toggle, UVC_GET_CUR),
dev.get_auto(toggle, UVC_GET_DEF),
) {
state.autos.push((toggle, AutoState { current, default }));
}
}
Ok(state)
}
pub fn set_control(
unique_id: &str,
control: CameraControl,
value: i32,
) -> Result<(), ControlError> {
let _quiesce = quiesce();
let dev = UsbDevice::open_for(unique_id)?;
dev.set(control, value)
}
pub fn set_auto(unique_id: &str, toggle: AutoToggle, on: bool) -> Result<(), ControlError> {
let _quiesce = quiesce();
let dev = UsbDevice::open_for(unique_id)?;
dev.set_auto(toggle, on)
}
pub fn apply_settings(
unique_id: &str,
autos: &[(AutoToggle, bool)],
values: &[(CameraControl, i32)],
) -> Result<(), ControlError> {
let _quiesce = quiesce();
let dev = UsbDevice::open_for(unique_id)?;
let mut first_err = None;
for (toggle, on) in autos {
if let Err(e) = dev.set_auto(*toggle, *on) {
first_err.get_or_insert(e);
}
}
for (control, value) in values {
if let Err(e) = dev.set(*control, *value) {
first_err.get_or_insert(e);
}
}
first_err.map_or(Ok(()), Err)
}
pub(crate) fn location_hint(unique_id: &str) -> Option<u32> {
let hex = unique_id.strip_prefix("0x").unwrap_or(unique_id);
let location = hex.get(..hex.len().checked_sub(8)?)?;
if location.is_empty() {
return None;
}
u32::from_str_radix(location, 16).ok()
}
pub(crate) fn usb_serials_by_location() -> HashMap<u32, String> {
let serial_key = CFString::from_static_str("USB Serial Number");
let location_key = CFString::from_static_str("locationID");
iokit::usb_devices()
.into_iter()
.flatten()
.filter_map(|service| registry_location_and_serial(&service, &serial_key, &location_key))
.fold(HashMap::new(), |mut serials, (location, serial)| {
serials.entry(location).or_insert(serial);
serials
})
}
fn registry_location_and_serial(
service: &IoObject,
serial_key: &CFString,
location_key: &CFString,
) -> Option<(u32, String)> {
let location = UsbInterface::open(service)
.and_then(|interface| interface.location_id())
.or_else(|| {
iokit::registry_property(service, location_key)?
.downcast::<CFNumber>()
.ok()?
.as_i32()
.map(i32::cast_unsigned)
})?;
let serial = iokit::registry_property(service, serial_key)?
.downcast::<CFString>()
.ok()?
.to_string();
(!serial.is_empty()).then_some((location, serial))
}
struct UsbDevice {
device: SeizedDevice,
vc_interface: u8,
unit_id: u8,
terminal_id: Option<u8>,
}
impl UsbDevice {
fn open_for(unique_id: &str) -> Result<Self, ControlError> {
let want_vid = crate::LOGITECH_VID;
let want_location = location_hint(unique_id);
let services = iokit::usb_devices().map_err(|call| ControlError::Io(call.to_string()))?;
let mut chosen: Option<Opened> = None;
let mut vendor_candidates = 0usize;
for service in services {
let Some(found) = Self::try_open(&service, want_vid) else {
continue;
};
if want_location.is_some_and(|want| found.matched_location == Some(want)) {
chosen = Some(found);
break;
}
if want_location.is_none() {
vendor_candidates += 1;
if chosen.is_none() {
chosen = Some(found);
}
}
}
if want_location.is_none() && vendor_candidates > 1 {
return Err(ControlError::Ambiguous);
}
chosen
.map(Opened::into_device)
.ok_or(ControlError::NotFound)
}
fn try_open(service: &IoObject, want_vid: u16) -> Option<Opened> {
let interface = UsbInterface::open(service)?;
if interface.vendor_id()? != want_vid {
return None;
}
let matched_location = interface.location_id();
let device = interface.seize()?;
let topology = video_control_topology(&device)?;
Some(Opened {
device: Self {
device,
vc_interface: topology.vc_interface,
unit_id: topology.processing_unit,
terminal_id: topology.camera_terminal,
},
matched_location,
})
}
fn entity(&self, unit: Unit) -> Result<u8, ControlError> {
match unit {
Unit::Processing => Ok(self.unit_id),
Unit::CameraTerminal => self.terminal_id.ok_or(ControlError::Unsupported),
}
}
fn get(&self, control: CameraControl, req: u8) -> Result<i32, ControlError> {
let ControlSpec {
unit,
selector,
payload,
} = control.spec();
let entity = self.entity(unit)?;
let mut buf = [0u8; 4];
self.transfer(RT_GET, req, selector, entity, &mut buf[..payload.len()])?;
Ok(match payload {
Payload::U32 => i32::try_from(u32::from_le_bytes(buf)).unwrap_or(i32::MAX),
Payload::I16 => i32::from(i16::from_le_bytes([buf[0], buf[1]])),
Payload::U16 => i32::from(u16::from_le_bytes([buf[0], buf[1]])),
})
}
fn set(&self, control: CameraControl, value: i32) -> Result<(), ControlError> {
let ControlSpec {
unit,
selector,
payload,
} = control.spec();
let entity = self.entity(unit)?;
let mut buf = (value as u32).to_le_bytes();
self.transfer(
RT_SET,
UVC_SET_CUR,
selector,
entity,
&mut buf[..payload.len()],
)
}
fn get_auto(&self, toggle: AutoToggle, req: u8) -> Result<bool, ControlError> {
let ToggleSpec { unit, selector } = toggle.spec();
let entity = self.entity(unit)?;
let mut buf = [0u8; 1];
self.transfer(RT_GET, req, selector, entity, &mut buf)?;
Ok(match toggle {
AutoToggle::Exposure => buf[0] != AE_MANUAL,
_ => buf[0] != 0,
})
}
fn set_auto(&self, toggle: AutoToggle, on: bool) -> Result<(), ControlError> {
let ToggleSpec { unit, selector } = toggle.spec();
let entity = self.entity(unit)?;
let candidates: &[u8] = match (toggle, on) {
(AutoToggle::Exposure, true) => &AE_AUTO_MODES,
(AutoToggle::Exposure, false) => &[AE_MANUAL],
(_, true) => &[1],
(_, false) => &[0],
};
let mut last = ControlError::Unsupported;
for &mode in candidates {
match self.transfer(RT_SET, UVC_SET_CUR, selector, entity, &mut [mode]) {
Ok(()) => return Ok(()),
Err(e) => last = e,
}
}
Err(last)
}
fn transfer(
&self,
request_type: u8,
request: u8,
selector: u16,
entity: u8,
data: &mut [u8],
) -> Result<(), ControlError> {
let mut req = IOUSBDevRequest {
bmRequestType: request_type,
bRequest: request,
wValue: selector << 8,
wIndex: (u16::from(entity) << 8) | u16::from(self.vc_interface),
wLength: data.len() as u16,
pData: data.as_mut_ptr().cast::<c_void>(),
wLenDone: 0,
};
if self.device.control_request(&mut req) {
Ok(())
} else {
Err(ControlError::Unsupported)
}
}
}
struct Opened {
device: UsbDevice,
matched_location: Option<u32>,
}
impl Opened {
fn into_device(self) -> UsbDevice {
self.device
}
}
#[derive(Debug, PartialEq, Eq)]
struct VcTopology {
vc_interface: u8,
processing_unit: u8,
camera_terminal: Option<u8>,
}
fn video_control_topology(device: &SeizedDevice) -> Option<VcTopology> {
(0..device.configuration_count()?)
.filter_map(|index| device.configuration_descriptor(index))
.find_map(scan_descriptors)
}
struct VcBlock {
interface: u8,
camera_terminal: Option<u8>,
}
fn scan_descriptors(blob: &[u8]) -> Option<VcTopology> {
let mut rest = blob;
let mut block: Option<VcBlock> = None;
while rest.len() >= 2 {
let len = usize::from(rest[0]);
let dtype = rest[1];
if len < 2 || len > rest.len() {
break;
}
let (descriptor, tail) = rest.split_at(len);
rest = tail;
if dtype == DESC_INTERFACE {
block = match (descriptor.get(2), descriptor.get(5), descriptor.get(6)) {
(Some(&interface), Some(&class), Some(&subclass))
if class == CC_VIDEO && subclass == SC_VIDEOCONTROL =>
{
Some(VcBlock {
interface,
camera_terminal: None,
})
}
_ => None,
};
} else if dtype == DESC_CS_INTERFACE
&& let Some(block) = block.as_mut()
&& let (Some(&subtype), Some(&entity)) = (descriptor.get(2), descriptor.get(3))
{
if subtype == VC_INPUT_TERMINAL && descriptor.len() >= 8 {
let terminal_type = u16::from(descriptor[4]) | (u16::from(descriptor[5]) << 8);
if terminal_type == ITT_CAMERA && block.camera_terminal.is_none() {
block.camera_terminal = Some(entity);
}
} else if subtype == VC_PROCESSING_UNIT {
return Some(VcTopology {
vc_interface: block.interface,
processing_unit: entity,
camera_terminal: block.camera_terminal,
});
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::{ITT_CAMERA, VcTopology, location_hint, scan_descriptors};
#[test]
fn unpadded_location_parses() {
assert_eq!(location_hint("0x1123000046d0893"), Some(0x0112_3000));
}
#[test]
fn padded_location_parses() {
assert_eq!(location_hint("0x14110000046d082d"), Some(0x1411_0000));
}
#[test]
fn too_short_ids_yield_no_hint() {
assert_eq!(location_hint("0x46d0893"), None);
assert_eq!(location_hint("46d0893"), None);
assert_eq!(location_hint(""), None);
}
fn interface(number: u8, class: u8, subclass: u8) -> Vec<u8> {
vec![9, 0x04, number, 0, 0, class, subclass, 0, 0]
}
fn input_terminal(entity: u8, terminal_type: u16) -> Vec<u8> {
vec![
8,
0x24,
0x02,
entity,
terminal_type as u8,
(terminal_type >> 8) as u8,
0,
0,
]
}
fn processing_unit(entity: u8) -> Vec<u8> {
vec![4, 0x24, 0x05, entity]
}
#[test]
fn finds_the_processing_unit_behind_a_videocontrol_interface() {
let blob: Vec<u8> = [
vec![9, 0x02, 0, 0, 0, 0, 0, 0, 0], interface(3, 0x0E, 0x01), input_terminal(1, ITT_CAMERA),
processing_unit(2),
]
.concat();
assert_eq!(
scan_descriptors(&blob),
Some(VcTopology {
vc_interface: 3,
processing_unit: 2,
camera_terminal: Some(1),
})
);
}
#[test]
fn a_non_camera_input_terminal_leaves_lens_controls_unsupported() {
let blob: Vec<u8> = [
interface(0, 0x0E, 0x01),
input_terminal(1, 0x0401), processing_unit(5),
]
.concat();
assert_eq!(
scan_descriptors(&blob),
Some(VcTopology {
vc_interface: 0,
processing_unit: 5,
camera_terminal: None,
})
);
}
#[test]
fn class_descriptors_outside_a_videocontrol_interface_are_ignored() {
let blob: Vec<u8> = [
interface(0, 0x01, 0x01), processing_unit(9),
]
.concat();
assert_eq!(scan_descriptors(&blob), None);
}
#[test]
fn a_videostreaming_frame_descriptor_is_not_a_processing_unit() {
let mut vs_frame = vec![0u8; 30];
vs_frame[0] = 30;
vs_frame[1] = 0x24;
vs_frame[2] = 0x05;
vs_frame[3] = 1;
let blob: Vec<u8> = [
interface(0, 0x0E, 0x01), input_terminal(1, ITT_CAMERA),
interface(1, 0x0E, 0x02), vs_frame,
]
.concat();
assert_eq!(scan_descriptors(&blob), None);
}
#[test]
fn malformed_lengths_stop_the_walk() {
let overrun: Vec<u8> = [interface(0, 0x0E, 0x01), vec![64, 0x24, 0x05, 7]].concat();
assert_eq!(scan_descriptors(&overrun), None);
let zero_length: Vec<u8> = [interface(0, 0x0E, 0x01), vec![0, 0x24]].concat();
assert_eq!(scan_descriptors(&zero_length), None);
}
}