Skip to content

Commit c602dd4

Browse files
authored
Support for Omnibook X Flip (#47)
* Adding changes for Omnibook X Flip * Adding changes for Omnibook X Flip * Fixes typo on quirks.rs
1 parent 6d7604a commit c602dd4

4 files changed

Lines changed: 96 additions & 8 deletions

File tree

contrib/hw/30c9-0120.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[device]
2+
vendor_id = 0x30C9
3+
product_id = 0x0120
4+
name = "HP OmniBook X Flip IR Camera (Luxvisions 30c9:0120)"
5+
6+
[emitter]
7+
# Discovered via UVC XU probe: IR function extension unit 14,
8+
# GUID 0f3f95dc-2632-4c4e-92c9-a04782f43bc8 (Windows Hello IR control).
9+
# selector 6, len 9. cur/def=[1,3,1] (off), max=[1,3,3] (on).
10+
unit = 14
11+
selector = 6
12+
control_bytes = [1, 3, 3, 0, 0, 0, 0, 0, 0]
13+
# This camera rejects an all-zero "off" payload (ERANGE) and would stay lit;
14+
# its real off/default value is [1,3,1,...].
15+
off_bytes = [1, 3, 1, 0, 0, 0, 0, 0, 0]
16+
# This camera resets the XU control the instant the controlling fd closes and
17+
# only re-illuminates on a fresh open->set edge, so the emitter holds one fd
18+
# open for the duration of each capture (open in activate, close in deactivate).
19+
reset_on_close = true

contrib/hw/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ control_bytes = [1, 3, 3, 0, 0, 0, 0, 0, 0]
2828
| `[emitter]` | `unit` | u8 | UVC extension unit ID |
2929
| `[emitter]` | `selector` | u8 | UVC control selector |
3030
| `[emitter]` | `control_bytes` | byte array | Payload to activate the emitter. Zeros of the same length deactivate it. |
31+
| `[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. |
32+
| `[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`. |
3133

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

crates/visage-hw/src/ir_emitter.rs

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
//! `linux-enable-ir-emitter` dependency.
66
77
use crate::quirks::{get_usb_ids, lookup_quirk, CameraQuirk};
8+
use std::cell::RefCell;
9+
use std::fs::{File, OpenOptions};
810
use std::os::unix::io::AsRawFd;
911
use thiserror::Error;
1012

@@ -40,6 +42,9 @@ const _SIZE_ASSERT: () = assert!(
4042
pub struct IrEmitter {
4143
device_path: String,
4244
quirk: &'static CameraQuirk,
45+
46+
/// Additional options for cameras with special file descriptor (fd) rules
47+
active_fd: RefCell<Option<File>>,
4348
}
4449

4550
#[derive(Debug, Error)]
@@ -62,20 +67,50 @@ impl IrEmitter {
6267
Some(Self {
6368
device_path: device_path.to_string(),
6469
quirk,
70+
active_fd: RefCell::new(None),
6571
})
6672
}
6773

6874
/// Activate the IR emitter by sending the quirk's control bytes.
6975
pub fn activate(&self) -> Result<(), EmitterError> {
7076
tracing::debug!(device = %self.device_path, "activating IR emitter");
7177
let mut payload = self.quirk.emitter.control_bytes.clone();
78+
79+
// reset_on_close devices forget the control the moment the fd closes,
80+
// so open a fresh fd, set it, and hold it open until deactivate().
81+
if self.quirk.emitter.reset_on_close {
82+
self.active_fd.borrow_mut().take(); // drop any stale fd first
83+
let file = OpenOptions::new()
84+
.read(true)
85+
.write(true)
86+
.open(&self.device_path)
87+
.map_err(EmitterError::Open)?;
88+
let result = Self::send_via_fd(&file, self.quirk, &mut payload);
89+
*self.active_fd.borrow_mut() = Some(file);
90+
return result;
91+
}
92+
93+
// Default: open, set the control, close.
7294
self.send_uvc_control(&mut payload)
7395
}
7496

75-
/// Deactivate the IR emitter by sending zeros of the same length.
97+
/// Deactivate the IR emitter after a capture.
7698
pub fn deactivate(&self) -> Result<(), EmitterError> {
7799
tracing::debug!(device = %self.device_path, "deactivating IR emitter");
78-
let mut payload = vec![0u8; self.quirk.emitter.control_bytes.len()];
100+
let mut payload = self.off_payload();
101+
102+
// reset_on_close devices reset the control when the fd closes, so send
103+
// "off" through the held fd, then close it to return control to default.
104+
if self.quirk.emitter.reset_on_close {
105+
let result = match self.active_fd.borrow().as_ref() {
106+
Some(file) => Self::send_via_fd(file, self.quirk, &mut payload),
107+
None => Ok(()),
108+
};
109+
self.active_fd.borrow_mut().take();
110+
return result;
111+
}
112+
113+
// Default: open, send "off", close.
79114
self.send_uvc_control(&mut payload)
80115
}
81116

@@ -89,18 +124,35 @@ impl IrEmitter {
89124
&self.quirk.device.name
90125
}
91126

127+
/// Deactivate IR emitter by sending zeros of `control_bytes` length or
128+
/// send explicit `off_bytes` when provided for cameras that require them.
129+
fn off_payload(&self) -> Vec<u8> {
130+
match &self.quirk.emitter.off_bytes {
131+
Some(off) if !off.is_empty() => off.clone(),
132+
_ => vec![0u8; self.quirk.emitter.control_bytes.len()],
133+
}
134+
}
135+
136+
/// Open a second fd here rather than requiring `AsRawFd` on `Camera`.
137+
/// Open with read+write, send one control, close (default)
92138
fn send_uvc_control(&self, payload: &mut [u8]) -> Result<(), EmitterError> {
93-
// Open the device with read+write access — needed for UVC ioctls.
94-
// We open a second fd here rather than requiring AsRawFd on Camera.
95-
let file = std::fs::OpenOptions::new()
139+
let file = OpenOptions::new()
96140
.read(true)
97141
.write(true)
98142
.open(&self.device_path)
99143
.map_err(EmitterError::Open)?;
144+
Self::send_via_fd(&file, self.quirk, payload)
145+
}
100146

147+
/// Send one UVC `SET_CUR` control over an already-open fd.
148+
fn send_via_fd(
149+
file: &File,
150+
quirk: &CameraQuirk,
151+
payload: &mut [u8],
152+
) -> Result<(), EmitterError> {
101153
let mut query = UvcXuControlQuery {
102-
unit: self.quirk.emitter.unit,
103-
selector: self.quirk.emitter.selector,
154+
unit: quirk.emitter.unit,
155+
selector: quirk.emitter.selector,
104156
query: UVC_SET_CUR,
105157
_pad0: 0,
106158
size: payload.len() as u16,

crates/visage-hw/src/quirks.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ const QUIRK_04F2_B6D9: &str = include_str!("../../../contrib/hw/04f2-b6d9.toml")
1313
const QUIRK_174F_2454: &str = include_str!("../../../contrib/hw/174f-2454.toml");
1414
/// Compile-time embedded quirk for the Lenovo ThinkBook 14 MP2PQAZG IR camera.
1515
const QUIRK_30C9_00C2: &str = include_str!("../../../contrib/hw/30c9-00c2.toml");
16+
/// Compile-time embedded quirk for the HP OmniBook X Flip IR camera (Luxvisions 30c9:0120).
17+
const QUIRK_30C9_0120: &str = include_str!("../../../contrib/hw/30c9-0120.toml");
1618

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

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

4454
/// Public alias used by `IrEmitter`.
@@ -47,7 +57,12 @@ pub type CameraQuirk = QuirkFile;
4757
fn quirk_db() -> &'static Vec<QuirkFile> {
4858
QUIRK_DB.get_or_init(|| {
4959
let mut db = Vec::new();
50-
for src in [QUIRK_04F2_B6D9, QUIRK_174F_2454, QUIRK_30C9_00C2] {
60+
for src in [
61+
QUIRK_04F2_B6D9,
62+
QUIRK_174F_2454,
63+
QUIRK_30C9_00C2,
64+
QUIRK_30C9_0120,
65+
] {
5166
match toml::from_str::<QuirkFile>(src) {
5267
Ok(q) => db.push(q),
5368
Err(e) => eprintln!("visage-hw: bad quirk TOML: {e}"),

0 commit comments

Comments
 (0)