Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(CompositeDeviceConfig): add udev-based source device matching #232

Merged
merged 3 commits into from
Nov 21, 2024
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
27 changes: 18 additions & 9 deletions rootfs/usr/share/inputplumber/devices/60-xbox_one_bt_gamepad.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,31 @@ name: Microsoft X-Box One pad
# /sys/class/dmi/id/product_name
matches: []

# Only allow a single source device per composite device of this type.
single_source: false
# Only allow a CompositeDevice to manage at most the given number of
# source devices. When this limit is reached, a new CompositeDevice will be
# created for any new matching devices.
maximum_sources: 2

# One or more source devices to combine into a single virtual device. The events
# from these devices will be watched and translated according to the key map.
source_devices:
- group: gamepad
blocked: true
evdev:
vendor_id: "045e"
product_id: "0b13"
handler: event*
udev:
attributes:
- name: name
value: Xbox Wireless Controller
properties:
- name: ID_BUS
value: bluetooth
driver: microsoft
sys_name: "event*"
subsystem: input
- group: gamepad
hidraw:
vendor_id: 0x045e
product_id: 0x0b13
udev:
# NOTE: This might also capture other non-xbox microsoft devices :(
driver: microsoft
subsystem: hidraw

# The target input device(s) to emulate by default
target_devices:
Expand Down
74 changes: 73 additions & 1 deletion rootfs/usr/share/inputplumber/schema/composite_device_v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@
"type": "string"
},
"single_source": {
"description": "If true, this composite device should only use one source device. Defaults to false.",
"description": "DEPRECATED: use 'maximum_sources' instead. If true, this composite device should only use one source device. Defaults to false.",
"type": "boolean",
"default": false
},
"maximum_sources": {
"description": "Maximum number of source devices that this composite device can manage. When this composite device reaches this maximum and a new matching source device is detected, a new composite device will be created instead of adding the source device to the existing one. Any value less than 1 indicates no maximum. Defaults to 0 (unlimited).",
"type": "integer",
"default": 0
},
"matches": {
"description": "Only use this profile if *any* of the given DMI system matches match. If this list is empty, then the source devices will *always* be checked.",
"type": "array",
Expand Down Expand Up @@ -168,6 +173,9 @@
"type": "boolean",
"default": false
},
"udev": {
"$ref": "#/definitions/Udev"
},
"evdev": {
"$ref": "#/definitions/Evdev"
},
Expand All @@ -187,6 +195,70 @@
],
"title": "SourceDevice"
},
"Udev": {
"description": "Source device to manage. Properties support globbing patterns.",
"type": "object",
"additionalProperties": false,
"properties": {
"attributes": {
"description": "Device attributes to match. Attributes can be found by running `udevadm info --attribute-walk /path/to/device` and looking at fields that look like: `ATTR{name}==\"value\"`.",
"type": "array",
"items": {
"$ref": "#/definitions/UdevKeyValue"
}
},
"dev_node": {
"description": "Full device node path to match. E.g. '/dev/hidraw3', '/dev/input/event*'",
"type": "string"
},
"dev_path": {
"description": "Full kernel device path to match. The path does not contain the sys mount point, but does start with a `/`. For example, the dev_path for `hidraw3` could be `/devices/pci0000:00/0000:00:08.1/.../hidraw/hidraw3`.",
"type": "string"
},
"driver": {
"description": "Driver being used by the device (or parent devices) to match. E.g. `playstation`, `microsoft`",
"type": "string"
},
"properties": {
"description": "Device properties to match. Properties can be found by running `udevadm info -q property /path/to/device`.",
"type": "array",
"items": {
"$ref": "#/definitions/UdevKeyValue"
}
},
"subsystem": {
"description": "Subsystem to match. E.g. `input`, `hidraw`, `iio`",
"type": "string"
},
"sys_name": {
"description": "Sysname to match. The sysname is typically the last part of the path to the device. E.g. `hidraw3`, `event6`",
"type": "string"
},
"sys_path": {
"description": "Syspath to match. The syspath is an absolute path and includes the sys mount point. For example, the syspath for `hidraw3` could be `/sys/devices/pci0000:00/0000:00:08.1/.../hidraw/hidraw3`, which includes the sys mount point `/sys`.",
"type": "string"
}
},
"required": [],
"title": "Udev"
},
"UdevKeyValue": {
"description": "Udev attribute or property key/value pair"
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"description": "Name of the property or attribute to match. Does NOT support globbing patterns.",
"type": "string"
},
"value": {
"description": "Value of the property or attribute to match. Supports globbing patterns.",
"type": "string"
},
},
"required": ["name"],
"title": "UdevKeyValue"
},
"Evdev": {
"description": "Source device to manage. Properties support globbing patterns.",
"type": "object",
Expand Down
146 changes: 145 additions & 1 deletion src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ pub struct SourceDevice {
pub evdev: Option<Evdev>,
pub hidraw: Option<Hidraw>,
pub iio: Option<IIO>,
pub udev: Option<Udev>,
pub unique: Option<bool>,
pub blocked: Option<bool>,
pub ignore: Option<bool>,
Expand All @@ -332,6 +333,26 @@ pub struct Hidraw {
pub name: Option<String>,
}

#[derive(Debug, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub struct Udev {
pub attributes: Option<Vec<UdevAttribute>>,
pub dev_node: Option<String>,
pub dev_path: Option<String>,
pub driver: Option<String>,
pub properties: Option<Vec<UdevAttribute>>,
pub subsystem: Option<String>,
pub sys_name: Option<String>,
pub sys_path: Option<String>,
}

#[derive(Debug, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub struct UdevAttribute {
pub name: String,
pub value: Option<String>,
}

#[derive(Debug, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
#[allow(clippy::upper_case_acronyms)]
Expand All @@ -358,7 +379,8 @@ pub struct CompositeDeviceConfig {
pub kind: String,
pub name: String,
pub matches: Vec<Match>,
pub single_source: Option<bool>,
pub single_source: Option<bool>, // DEPRECATED; use 'maximum_sources' instead
pub maximum_sources: Option<i32>,
pub capability_map_id: Option<String>,
pub source_devices: Vec<SourceDevice>,
pub target_devices: Option<Vec<String>>,
Expand Down Expand Up @@ -397,6 +419,18 @@ impl CompositeDeviceConfig {

/// Returns a [SourceDevice] if it matches the given [UdevDevice].
pub fn get_matching_device(&self, udevice: &UdevDevice) -> Option<SourceDevice> {
// Check udev matches first
for config in self.source_devices.iter() {
let Some(udev_config) = config.udev.as_ref() else {
continue;
};

if self.has_matching_udev(udevice, udev_config) {
return Some(config.clone());
}
}

// Deprecated method for device matching based on subsystem
let subsystem = udevice.subsystem();
match subsystem.as_str() {
"input" => {
Expand Down Expand Up @@ -431,6 +465,116 @@ impl CompositeDeviceConfig {
None
}

/// Returns true if a given device matches the given udev config
pub fn has_matching_udev(&self, device: &UdevDevice, udev_config: &Udev) -> bool {
log::trace!("Checking udev config '{:?}'", udev_config);

if let Some(attributes) = udev_config.attributes.as_ref() {
let device_attributes = device.get_attributes();

for attribute in attributes {
let Some(device_attr_value) = device_attributes.get(&attribute.name) else {
// If the device does not have this attribute, return false
return false;
};

// If no value was specified in the config, then only match on
// the presence of the attribute and not the value.
let Some(attr_value) = attribute.value.as_ref() else {
continue;
};

// Glob match on the attribute value
log::trace!("Checking attribute: {attr_value} against {device_attr_value}");
if !glob_match(attr_value.as_str(), device_attr_value.as_str()) {
return false;
}
}
}

if let Some(dev_node) = udev_config.dev_node.as_ref() {
let device_dev_node = device.devnode();
log::trace!("Checking dev_node: {dev_node} against {device_dev_node}");
if !glob_match(dev_node.as_str(), device_dev_node.as_str()) {
return false;
}
}

if let Some(dev_path) = udev_config.dev_path.as_ref() {
let device_dev_path = device.devpath();
log::trace!("Checking dev_path: {dev_path} against {device_dev_path}");
if !glob_match(dev_path.as_str(), device_dev_path.as_str()) {
return false;
}
}

if let Some(driver) = udev_config.driver.as_ref() {
let all_drivers = device.drivers();
let mut has_matches = false;

for device_driver in all_drivers {
log::trace!("Checking driver: {driver} against {device_driver}");
if glob_match(driver.as_str(), device_driver.as_str()) {
has_matches = true;
break;
}
}

if !has_matches {
return false;
}
}

if let Some(properties) = udev_config.properties.as_ref() {
let device_properties = device.get_properties();

for property in properties {
let Some(device_prop_value) = device_properties.get(&property.name) else {
// If the device does not have this property, return false
return false;
};

// If no value was specified in the config, then only match on
// the presence of the property and not the value.
let Some(prop_value) = property.value.as_ref() else {
continue;
};

// Glob match on the property value
log::trace!("Checking property: {prop_value} against {device_prop_value}");
if !glob_match(prop_value.as_str(), device_prop_value.as_str()) {
return false;
}
}
}

if let Some(subsystem) = udev_config.subsystem.as_ref() {
let device_subsystem = device.subsystem();
log::trace!("Checking subsystem: {subsystem} against {device_subsystem}");
if !glob_match(subsystem.as_str(), device_subsystem.as_str()) {
return false;
}
}

if let Some(sys_name) = udev_config.sys_name.as_ref() {
let device_sys_name = device.sysname();
log::trace!("Checking sys_name: {sys_name} against {device_sys_name}");
if !glob_match(sys_name.as_str(), device_sys_name.as_str()) {
return false;
}
}

if let Some(sys_path) = udev_config.sys_path.as_ref() {
let device_sys_path = device.syspath();
log::trace!("Checking sys_path: {sys_path} against {device_sys_path}");
if !glob_match(sys_path.as_str(), device_sys_path.as_str()) {
return false;
}
}

true
}

/// Returns true if a given hidraw device is within a list of hidraw configs.
pub fn has_matching_hidraw(&self, device: &UdevDevice, hidraw_config: &Hidraw) -> bool {
log::trace!("Checking hidraw config '{:?}'", hidraw_config,);
Expand Down
33 changes: 24 additions & 9 deletions src/drivers/xpad_uhid/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,9 @@ use super::{
hid_report::{XpadUhidOutputData, XpadUhidOutputReport},
};

// Hardware ID's
pub const VIDS: [u16; 1] = [0x045e];
pub const PIDS: [u16; 1] = [0x0b13];

// Report ID
pub const DATA: u8 = 0x01;
pub const GUIDE: u8 = 0x02;

// Input report size
const PACKET_SIZE: usize = 17;
Expand Down Expand Up @@ -51,13 +48,19 @@ pub struct Driver {
impl Driver {
pub fn new(udevice: UdevDevice) -> Result<Self, Box<dyn Error + Send + Sync>> {
let path = udevice.devnode();
let driver = udevice.drivers();
if !driver.contains(&"microsoft".to_string()) {
return Err(format!("Device '{path}' is not using the hid-microsoft driver").into());
}
let syspath = udevice.syspath();
if !syspath.contains("uhid") {
return Err(format!("Device '{path}' is not a uhid virtual device").into());
}

let cs_path = CString::new(path.clone())?;
let api = hidapi::HidApi::new()?;
let device = api.open_path(&cs_path)?;
let info = device.get_device_info()?;
if !VIDS.contains(&info.vendor_id()) || !PIDS.contains(&info.product_id()) {
return Err(format!("Device '{path}' is not an xpad_uhid controller").into());
}

Ok(Self {
device,
state: None,
Expand Down Expand Up @@ -104,13 +107,25 @@ impl Driver {
DATA => {
log::trace!("Got input data.");
if bytes_read != PACKET_SIZE {
return Err("Invalid packet size for Keyboard or Touchpad Data.".into());
return Err("Invalid packet size for input data.".into());
}
// Handle the incoming input report
let sized_buf = slice.try_into()?;

self.handle_input_report(sized_buf)?
}
// XBox One gamepads have a separate report for guide button presses
// for some reason.
GUIDE => {
log::trace!("Got guide input data.");
// This report is only 2 bytes, with the first byte representing
// the report id and the second byte being the guide button state
let value = buf[1];
let event = Event::Button(ButtonEvent::Guide(BinaryInput {
pressed: value == 1,
}));
vec![event]
}
_ => {
//log::debug!("Invalid Report ID.");
let events = vec![];
Expand Down
Loading
Loading