Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions contrib/hw/30c9-0120.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[device]
vendor_id = 0x30C9
product_id = 0x0120
name = "HP OmniBook X Flip IR Camera (Luxvisions 30c9:0120)"

[emitter]
# Discovered via UVC XU probe: IR function extension unit 14,
# GUID 0f3f95dc-2632-4c4e-92c9-a04782f43bc8 (Windows Hello IR control).
# selector 6, len 9. cur/def=[1,3,1] (off), max=[1,3,3] (on).
unit = 14
selector = 6
control_bytes = [1, 3, 3, 0, 0, 0, 0, 0, 0]
# This camera rejects an all-zero "off" payload (ERANGE) and would stay lit;
# its real off/default value is [1,3,1,...].
off_bytes = [1, 3, 1, 0, 0, 0, 0, 0, 0]
# This camera resets the XU control the instant the controlling fd closes and
# only re-illuminates on a fresh open->set edge, so the emitter holds one fd
# open for the duration of each capture (open in activate, close in deactivate).
reset_on_close = true
2 changes: 2 additions & 0 deletions contrib/hw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ control_bytes = [1, 3, 3, 0, 0, 0, 0, 0, 0]
| `[emitter]` | `unit` | u8 | UVC extension unit ID |
| `[emitter]` | `selector` | u8 | UVC control selector |
| `[emitter]` | `control_bytes` | byte array | Payload to activate the emitter. Zeros of the same length deactivate it. |
| `[emitter]` | `off_bytes` | byte array | Optional. Explicit payload to deactivate the emitter. Needed for cameras that reject an all-zero "off" payload (e.g. with `ERANGE`). Defaults to zeros of `control_bytes` length when omitted. |
| `[emitter]` | `reset_on_close` | bool | Optional. Set `true` for cameras that reset the control when the controlling fd closes and only re-illuminate on a fresh open→set edge; the emitter then holds one fd open for the duration of each capture. Defaults to `false`. |

The `control_bytes` values are found via `linux-enable-ir-emitter configure` or UVC descriptor analysis.

Expand Down
66 changes: 59 additions & 7 deletions crates/visage-hw/src/ir_emitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
//! `linux-enable-ir-emitter` dependency.

use crate::quirks::{get_usb_ids, lookup_quirk, CameraQuirk};
use std::cell::RefCell;
use std::fs::{File, OpenOptions};
use std::os::unix::io::AsRawFd;
use thiserror::Error;

Expand Down Expand Up @@ -40,6 +42,9 @@ const _SIZE_ASSERT: () = assert!(
pub struct IrEmitter {
device_path: String,
quirk: &'static CameraQuirk,

/// Additional options for cameras with special file descriptor (fd) rules
active_fd: RefCell<Option<File>>,
}

#[derive(Debug, Error)]
Expand All @@ -62,20 +67,50 @@ impl IrEmitter {
Some(Self {
device_path: device_path.to_string(),
quirk,
active_fd: RefCell::new(None),
})
}

/// Activate the IR emitter by sending the quirk's control bytes.
pub fn activate(&self) -> Result<(), EmitterError> {
tracing::debug!(device = %self.device_path, "activating IR emitter");
let mut payload = self.quirk.emitter.control_bytes.clone();

// reset_on_close devices forget the control the moment the fd closes,
// so open a fresh fd, set it, and hold it open until deactivate().
if self.quirk.emitter.reset_on_close {
self.active_fd.borrow_mut().take(); // drop any stale fd first
let file = OpenOptions::new()
.read(true)
.write(true)
.open(&self.device_path)
.map_err(EmitterError::Open)?;
let result = Self::send_via_fd(&file, self.quirk, &mut payload);
*self.active_fd.borrow_mut() = Some(file);
return result;
}

// Default: open, set the control, close.
self.send_uvc_control(&mut payload)
}

/// Deactivate the IR emitter by sending zeros of the same length.
/// Deactivate the IR emitter after a capture.
pub fn deactivate(&self) -> Result<(), EmitterError> {
tracing::debug!(device = %self.device_path, "deactivating IR emitter");
let mut payload = vec![0u8; self.quirk.emitter.control_bytes.len()];
let mut payload = self.off_payload();

// reset_on_close devices reset the control when the fd closes, so send
// "off" through the held fd, then close it to return control to default.
if self.quirk.emitter.reset_on_close {
let result = match self.active_fd.borrow().as_ref() {
Some(file) => Self::send_via_fd(file, self.quirk, &mut payload),
None => Ok(()),
};
self.active_fd.borrow_mut().take();
return result;
}

// Default: open, send "off", close.
self.send_uvc_control(&mut payload)
}

Expand All @@ -89,18 +124,35 @@ impl IrEmitter {
&self.quirk.device.name
}

/// Deactivate IR emitter by sending zeros of `control_bytes` length or
/// send explicit `off_bytes` when provided for cameras that require them.
fn off_payload(&self) -> Vec<u8> {
match &self.quirk.emitter.off_bytes {
Some(off) if !off.is_empty() => off.clone(),
_ => vec![0u8; self.quirk.emitter.control_bytes.len()],
}
}

/// Open a second fd here rather than requiring `AsRawFd` on `Camera`.
/// Open with read+write, send one control, close (default)
fn send_uvc_control(&self, payload: &mut [u8]) -> Result<(), EmitterError> {
// Open the device with read+write access — needed for UVC ioctls.
// We open a second fd here rather than requiring AsRawFd on Camera.
let file = std::fs::OpenOptions::new()
let file = OpenOptions::new()
.read(true)
.write(true)
.open(&self.device_path)
.map_err(EmitterError::Open)?;
Self::send_via_fd(&file, self.quirk, payload)
}

/// Send one UVC `SET_CUR` control over an already-open fd.
fn send_via_fd(
file: &File,
quirk: &CameraQuirk,
payload: &mut [u8],
) -> Result<(), EmitterError> {
let mut query = UvcXuControlQuery {
unit: self.quirk.emitter.unit,
selector: self.quirk.emitter.selector,
unit: quirk.emitter.unit,
selector: quirk.emitter.selector,
query: UVC_SET_CUR,
_pad0: 0,
size: payload.len() as u16,
Expand Down
17 changes: 16 additions & 1 deletion crates/visage-hw/src/quirks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const QUIRK_04F2_B6D9: &str = include_str!("../../../contrib/hw/04f2-b6d9.toml")
const QUIRK_174F_2454: &str = include_str!("../../../contrib/hw/174f-2454.toml");
/// Compile-time embedded quirk for the Lenovo ThinkBook 14 MP2PQAZG IR camera.
const QUIRK_30C9_00C2: &str = include_str!("../../../contrib/hw/30c9-00c2.toml");
/// Compile-time embedded quirk for the HP OmniBook X Flip IR camera (Luxvisions 30c9:0120).
const QUIRK_30C9_0120: &str = include_str!("../../../contrib/hw/30c9-0120.toml");

static QUIRK_DB: OnceLock<Vec<QuirkFile>> = OnceLock::new();

Expand All @@ -39,6 +41,14 @@ pub struct EmitterInfo {
/// Payload bytes sent to activate the emitter.
/// Zeros of the same length deactivate it.
pub control_bytes: Vec<u8>,
/// Payload bytes sent to deactivate the emitter.
/// Defaults to zeros of `control_bytes` length.
#[serde(default)]
pub off_bytes: Option<Vec<u8>>,
/// Flag `true` for cameras that reset the XU control when the controlling fd closes,
/// making `IrEmitter` hold an fd open for the duration of each capture.
#[serde(default)]
pub reset_on_close: bool,
}

/// Public alias used by `IrEmitter`.
Expand All @@ -47,7 +57,12 @@ pub type CameraQuirk = QuirkFile;
fn quirk_db() -> &'static Vec<QuirkFile> {
QUIRK_DB.get_or_init(|| {
let mut db = Vec::new();
for src in [QUIRK_04F2_B6D9, QUIRK_174F_2454, QUIRK_30C9_00C2] {
for src in [
QUIRK_04F2_B6D9,
QUIRK_174F_2454,
QUIRK_30C9_00C2,
QUIRK_30C9_0120,
] {
match toml::from_str::<QuirkFile>(src) {
Ok(q) => db.push(q),
Err(e) => eprintln!("visage-hw: bad quirk TOML: {e}"),
Expand Down