diff --git a/rootfs/usr/share/inputplumber/devices/60-xbox_one_bt_gamepad.yaml b/rootfs/usr/share/inputplumber/devices/60-xbox_one_bt_gamepad.yaml index 17b7155..7d4ff97 100644 --- a/rootfs/usr/share/inputplumber/devices/60-xbox_one_bt_gamepad.yaml +++ b/rootfs/usr/share/inputplumber/devices/60-xbox_one_bt_gamepad.yaml @@ -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: diff --git a/rootfs/usr/share/inputplumber/schema/composite_device_v1.json b/rootfs/usr/share/inputplumber/schema/composite_device_v1.json index dd5f08f..41c5ba0 100644 --- a/rootfs/usr/share/inputplumber/schema/composite_device_v1.json +++ b/rootfs/usr/share/inputplumber/schema/composite_device_v1.json @@ -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", @@ -168,6 +173,9 @@ "type": "boolean", "default": false }, + "udev": { + "$ref": "#/definitions/Udev" + }, "evdev": { "$ref": "#/definitions/Evdev" }, @@ -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", diff --git a/src/config/mod.rs b/src/config/mod.rs index 6b0c7b5..e9ad6be 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -307,6 +307,7 @@ pub struct SourceDevice { pub evdev: Option, pub hidraw: Option, pub iio: Option, + pub udev: Option, pub unique: Option, pub blocked: Option, pub ignore: Option, @@ -332,6 +333,26 @@ pub struct Hidraw { pub name: Option, } +#[derive(Debug, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct Udev { + pub attributes: Option>, + pub dev_node: Option, + pub dev_path: Option, + pub driver: Option, + pub properties: Option>, + pub subsystem: Option, + pub sys_name: Option, + pub sys_path: Option, +} + +#[derive(Debug, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct UdevAttribute { + pub name: String, + pub value: Option, +} + #[derive(Debug, Deserialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] #[allow(clippy::upper_case_acronyms)] @@ -358,7 +379,8 @@ pub struct CompositeDeviceConfig { pub kind: String, pub name: String, pub matches: Vec, - pub single_source: Option, + pub single_source: Option, // DEPRECATED; use 'maximum_sources' instead + pub maximum_sources: Option, pub capability_map_id: Option, pub source_devices: Vec, pub target_devices: Option>, @@ -397,6 +419,18 @@ impl CompositeDeviceConfig { /// Returns a [SourceDevice] if it matches the given [UdevDevice]. pub fn get_matching_device(&self, udevice: &UdevDevice) -> Option { + // 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" => { @@ -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,); diff --git a/src/drivers/xpad_uhid/driver.rs b/src/drivers/xpad_uhid/driver.rs index e73dc71..6d6ce42 100644 --- a/src/drivers/xpad_uhid/driver.rs +++ b/src/drivers/xpad_uhid/driver.rs @@ -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; @@ -51,13 +48,19 @@ pub struct Driver { impl Driver { pub fn new(udevice: UdevDevice) -> Result> { 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, @@ -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![]; diff --git a/src/input/manager.rs b/src/input/manager.rs index 649558f..35c0ba1 100644 --- a/src/input/manager.rs +++ b/src/input/manager.rs @@ -699,82 +699,183 @@ impl Manager { continue; }; log::debug!("Checking if existing composite device {composite_device:?} with config {:?} is missing device: {id:?}", config.name); - let source_devices = config.source_devices.clone(); + + // If the CompositeDevice only allows a single source device, skip its + // consideration. + if config.single_source.unwrap_or(false) { + log::trace!("{:?} is a single source device. Skipping.", config.name); + continue; + } + if config.maximum_sources.unwrap_or(0) == 1 { + log::trace!("{:?} is a single source device. Skipping.", config.name); + continue; + } + log::trace!( + "Composite device has {} source devices defined", + config.source_devices.len() + ); + + // If the CompositeDevice only allows a maximum number of source devices, + // check to see if that limit has been reached. If that limit is reached, + // then a new CompositeDevice will be created for the source device. + if let Some(max_sources) = config.maximum_sources { + // If maximum_sources is less than 1 (e.g. 0, -1) then consider + // the maximum to be 'unlimited'. + if max_sources > 0 { + // Check to see how many source devices this composite device is + // currently managing. + if let Some(sources) = self.composite_device_sources.get(composite_device) { + let sources_count = sources.len() as i32; + if sources_count >= max_sources { + log::trace!( + "{composite_device:?} maximum source devices reached: {max_sources}. Skipping." + ); + continue; + } + } + } + } + + // Check if this device matches any source udev configs of the running + // CompositeDevice. + for source_device in config.source_devices.iter() { + log::trace!("Checking if existing composite device is missing udev device {id}"); + let Some(udev_config) = source_device.udev.as_ref() else { + continue; + }; + if !config.has_matching_udev(&device, udev_config) { + continue; + } + + // Check if the device has already been used in this config or not, + // stop here if the device must be unique. + if let Some(sources) = self.composite_device_sources.get(composite_device) { + for source in sources { + if source != source_device { + continue; + } + if let Some(ignored) = source_device.ignore { + if ignored { + log::debug!( + "Ignoring device {:?}, not adding to composite device: {}", + source_device, + composite_device + ); + break 'start; + } + } + if let Some(unique) = source_device.clone().unique { + if unique { + log::trace!( + "Found unique device {:?}, not adding to composite device {}", + source_device, + composite_device + ); + break 'start; + } + // Default to being unique + } else { + log::trace!( + "Found unique device {:?}, not adding to composite device {}", + source_device, + composite_device + ); + break 'start; + } + } + } + + log::info!("Found missing device, adding source device {id:?} to existing composite device: {composite_device:?}"); + let client = self.composite_devices.get(composite_device.as_str()); + if client.is_none() { + log::error!("No existing composite device found for key {composite_device:?}"); + continue; + } + self.add_device_to_composite_device(device, client.unwrap()) + .await?; + self.source_devices_used + .insert(id.clone(), composite_device.clone()); + let composite_id = composite_device.clone(); + if !self.composite_device_sources.contains_key(&composite_id) { + self.composite_device_sources + .insert(composite_id.clone(), Vec::new()); + } + let sources = self + .composite_device_sources + .get_mut(&composite_id) + .unwrap(); + sources.push(source_device.clone()); + self.source_devices.insert(id, source_device.clone()); + + return Ok(()); + } + // TODO: Consolidate these match device.subsystem().as_str() { "input" => { log::trace!( - "Checking if existing composite device is missing event device {id}" - ); - - if config.single_source.unwrap_or(false) { - log::trace!("{:?} is a single source device. Skipping.", config.name); - continue; - } - log::trace!( - "Composite device has {} source devices defined", - source_devices.len() + "Checking if existing composite device is missing evdev device: {:?}", + device.name() ); - for source_device in source_devices { - if source_device.evdev.is_none() { + for source_device in config.source_devices.iter() { + let Some(evdev_config) = source_device.evdev.as_ref() else { log::trace!("Evdev section is empty"); continue; + }; + if !config.has_matching_evdev(&device, evdev_config) { + continue; } - if config.has_matching_evdev(&device, &source_device.clone().evdev.unwrap()) - { - // Check if the device has already been used in this config or not, stop here if the device must be unique. - if let Some(sources) = - self.composite_device_sources.get(composite_device) - { - for source in sources { - if source != &source_device { - continue; - } - if let Some(ignored) = source_device.ignore { - if ignored { - log::debug!("Ignoring device {:?}, not adding to composite device: {}", source_device, composite_device); - break 'start; - } + + // Check if the device has already been used in this config or not, stop here if the device must be unique. + if let Some(sources) = self.composite_device_sources.get(composite_device) { + for source in sources { + if source != source_device { + continue; + } + if let Some(ignored) = source_device.ignore { + if ignored { + log::debug!("Ignoring device {:?}, not adding to composite device: {}", source_device, composite_device); + break 'start; } - if let Some(unique) = source_device.clone().unique { - if unique { - log::trace!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); - break 'start; - } - // Default to being unique - } else { + } + if let Some(unique) = source_device.clone().unique { + if unique { log::trace!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); break 'start; } + // Default to being unique + } else { + log::trace!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); + break 'start; } } + } - log::info!("Found missing device, adding source device {id:?} to existing composite device: {composite_device:?}"); - let client = self.composite_devices.get(composite_device.as_str()); - if client.is_none() { - log::error!( - "No existing composite device found for key {composite_device:?}" - ); - continue; - } - self.add_device_to_composite_device(device, client.unwrap()) - .await?; - self.source_devices_used - .insert(id.clone(), composite_device.clone()); - let composite_id = composite_device.clone(); - if !self.composite_device_sources.contains_key(&composite_id) { - self.composite_device_sources - .insert(composite_id.clone(), Vec::new()); - } - let sources = self - .composite_device_sources - .get_mut(&composite_id) - .unwrap(); - sources.push(source_device.clone()); - self.source_devices.insert(id, source_device.clone()); - - return Ok(()); + log::info!("Found missing device, adding source device {id:?} to existing composite device: {composite_device:?}"); + let client = self.composite_devices.get(composite_device.as_str()); + if client.is_none() { + log::error!( + "No existing composite device found for key {composite_device:?}" + ); + continue; + } + self.add_device_to_composite_device(device, client.unwrap()) + .await?; + self.source_devices_used + .insert(id.clone(), composite_device.clone()); + let composite_id = composite_device.clone(); + if !self.composite_device_sources.contains_key(&composite_id) { + self.composite_device_sources + .insert(composite_id.clone(), Vec::new()); } + let sources = self + .composite_device_sources + .get_mut(&composite_id) + .unwrap(); + sources.push(source_device.clone()); + self.source_devices.insert(id, source_device.clone()); + + return Ok(()); } } "hidraw" => { @@ -782,128 +883,126 @@ impl Manager { "Checking if existing composite device is missing hidraw device: {:?}", device.name() ); - for source_device in source_devices { - if source_device.hidraw.is_none() { + for source_device in config.source_devices.iter() { + let Some(hidraw_config) = source_device.hidraw.as_ref() else { + continue; + }; + if !config.has_matching_hidraw(&device, hidraw_config) { continue; } - if config - .has_matching_hidraw(&device, &source_device.clone().hidraw.unwrap()) - { - // Check if the device has already been used in this config or not, stop here if the device must be unique. - if let Some(sources) = - self.composite_device_sources.get(composite_device) - { - for source in sources { - if source != &source_device { - continue; - } - if let Some(ignored) = source_device.ignore { - if ignored { - log::debug!("Ignoring device {:?}, not adding to composite device: {}", source_device, composite_device); - break 'start; - } + + // Check if the device has already been used in this config or not, stop here if the device must be unique. + if let Some(sources) = self.composite_device_sources.get(composite_device) { + for source in sources { + if source != source_device { + continue; + } + if let Some(ignored) = source_device.ignore { + if ignored { + log::debug!("Ignoring device {:?}, not adding to composite device: {}", source_device, composite_device); + break 'start; } - if let Some(unique) = source_device.clone().unique { - if unique { - log::trace!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); - break 'start; - } - } else { + } + if let Some(unique) = source_device.clone().unique { + if unique { log::trace!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); break 'start; } + } else { + log::trace!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); + break 'start; } } + } - log::info!("Found missing device, adding source device {id} to existing composite device: {composite_device}"); - let handle = self.composite_devices.get(composite_device.as_str()); - if handle.is_none() { - log::error!( - "No existing composite device found for key {}", - composite_device.as_str() - ); - continue; - } - self.add_device_to_composite_device(device, handle.unwrap()) - .await?; - self.source_devices_used - .insert(id.clone(), composite_device.clone()); - let composite_id = composite_device.clone(); - if !self.composite_device_sources.contains_key(&composite_id) { - self.composite_device_sources - .insert(composite_id.clone(), Vec::new()); - } - let sources = self - .composite_device_sources - .get_mut(&composite_id) - .unwrap(); - sources.push(source_device.clone()); - - self.source_devices.insert(id, source_device.clone()); - return Ok(()); + log::info!("Found missing device, adding source device {id} to existing composite device: {composite_device}"); + let handle = self.composite_devices.get(composite_device.as_str()); + if handle.is_none() { + log::error!( + "No existing composite device found for key {}", + composite_device.as_str() + ); + continue; } + self.add_device_to_composite_device(device, handle.unwrap()) + .await?; + self.source_devices_used + .insert(id.clone(), composite_device.clone()); + let composite_id = composite_device.clone(); + if !self.composite_device_sources.contains_key(&composite_id) { + self.composite_device_sources + .insert(composite_id.clone(), Vec::new()); + } + let sources = self + .composite_device_sources + .get_mut(&composite_id) + .unwrap(); + sources.push(source_device.clone()); + + self.source_devices.insert(id, source_device.clone()); + return Ok(()); } } "iio" => { - log::trace!("Checking if existing composite device is missing hidraw device"); - for source_device in source_devices { - if source_device.iio.is_none() { + log::trace!("Checking if existing composite device is missing iio device"); + for source_device in config.source_devices.iter() { + let Some(iio_config) = source_device.iio.as_ref() else { + continue; + }; + if !config.has_matching_iio(&device, iio_config) { continue; } - if config.has_matching_iio(&device, &source_device.clone().iio.unwrap()) { - // Check if the device has already been used in this config or not, stop here if the device must be unique. - if let Some(sources) = - self.composite_device_sources.get(composite_device) - { - for source in sources { - if source != &source_device { + + // Check if the device has already been used in this config or not, stop here if the device must be unique. + if let Some(sources) = self.composite_device_sources.get(composite_device) { + for source in sources { + if source != source_device { + continue; + } + if let Some(ignored) = source_device.ignore { + if ignored { + log::debug!("Ignoring device {:?}, not adding to composite device: {}", source_device, composite_device); continue; } - if let Some(ignored) = source_device.ignore { - if ignored { - log::debug!("Ignoring device {:?}, not adding to composite device: {}", source_device, composite_device); - continue; - } - } - if let Some(unique) = source_device.clone().unique { - if unique { - log::trace!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); - break 'start; - } - } else { + } + if let Some(unique) = source_device.clone().unique { + if unique { log::trace!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); break 'start; } + } else { + log::trace!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); + break 'start; } } + } - log::info!("Found missing device, adding source device {id} to existing composite device: {composite_device}"); - let handle = self.composite_devices.get(composite_device.as_str()); - if handle.is_none() { - log::error!( - "No existing composite device found for key {}", - composite_device.as_str() - ); - continue; - } - self.add_device_to_composite_device(device, handle.unwrap()) - .await?; - self.source_devices_used - .insert(id.clone(), composite_device.clone()); - let composite_id = composite_device.clone(); - if !self.composite_device_sources.contains_key(&composite_id) { - self.composite_device_sources - .insert(composite_id.clone(), Vec::new()); - } - let sources = self - .composite_device_sources - .get_mut(&composite_id) - .unwrap(); - sources.push(source_device.clone()); - - self.source_devices.insert(id, source_device.clone()); - return Ok(()); + log::info!("Found missing device, adding source device {id} to existing composite device: {composite_device}"); + let handle = self.composite_devices.get(composite_device.as_str()); + if handle.is_none() { + log::error!( + "No existing composite device found for key {}", + composite_device.as_str() + ); + continue; } + self.add_device_to_composite_device(device, handle.unwrap()) + .await?; + self.source_devices_used + .insert(id.clone(), composite_device.clone()); + let composite_id = composite_device.clone(); + if !self.composite_device_sources.contains_key(&composite_id) { + self.composite_device_sources + .insert(composite_id.clone(), Vec::new()); + } + let sources = self + .composite_device_sources + .get_mut(&composite_id) + .unwrap(); + sources.push(source_device.clone()); + + self.source_devices.insert(id, source_device.clone()); + return Ok(()); } } _ => (), @@ -944,6 +1043,41 @@ impl Manager { continue; } + // Check if this device matches any source udev configs + for source_device in config.source_devices.iter() { + let Some(udev_config) = source_device.udev.as_ref() else { + continue; + }; + if !config.has_matching_udev(&device, udev_config) { + continue; + } + + if let Some(ignored) = source_device.ignore { + if ignored { + log::trace!("Event device configured to ignore: {:?}", device); + return Ok(()); + } + } + log::info!("Found a matching udev device {id}, creating CompositeDevice"); + let dev = self + .create_composite_device_from_config(&config, device) + .await?; + + // Get the target input devices from the config + let target_devices_config = config.target_devices.clone(); + + // Create the composite deivce + self.start_composite_device( + dev, + config.clone(), + target_devices_config, + source_device.clone(), + ) + .await?; + + return Ok(()); + } + let source_devices = config.source_devices.clone(); match device.subsystem().as_str() { "input" => { @@ -1541,17 +1675,27 @@ impl Manager { }; // Wait until the device has initialized with udev - // TODO: Add max wait time for udev initialization + const MAX_TRIES: u8 = 80; + let mut attempt: u8 = 0; loop { + // Break after max attempts reached + if attempt > MAX_TRIES { + log::warn!("Unable to create initialized UdevDevice for {base_path}/{name} after {MAX_TRIES} attempts."); + continue 'outer; + } + + // Try to get the device from udev to check its initialization state { let Ok(device) = ::udev::Device::from_subsystem_sysname( subsystem.to_string(), name.clone(), ) else { - log::warn!( + log::debug!( "Unable to create UdevDevice from {base_path}/{name} to check initialization" ); - continue 'outer; + attempt += 1; + tokio::time::sleep(Duration::from_millis(10)).await; + continue; }; if device.is_initialized() { @@ -1561,6 +1705,7 @@ impl Manager { log::trace!("{base_path}/{name} is not yet initialized by udev"); tokio::time::sleep(Duration::from_millis(10)).await; + attempt += 1; } // Create a udev device for the device diff --git a/src/input/source/evdev/blocked.rs b/src/input/source/evdev/blocked.rs index 4eed36f..f89c966 100644 --- a/src/input/source/evdev/blocked.rs +++ b/src/input/source/evdev/blocked.rs @@ -23,6 +23,7 @@ impl BlockedEventDevice { log::debug!("Opening device at: {}", path); let mut device = Device::open(path.clone())?; device.grab()?; + log::info!("Blocking input events from {path}"); Ok(Self { device }) } diff --git a/src/input/source/hidraw.rs b/src/input/source/hidraw.rs index 5422fbf..d47309b 100644 --- a/src/input/source/hidraw.rs +++ b/src/input/source/hidraw.rs @@ -149,20 +149,22 @@ impl HidRawDevice { return DriverType::Fts3528Touchscreen; } - // XpadUhid - if drivers::xpad_uhid::driver::VIDS.contains(&vid) - && drivers::xpad_uhid::driver::PIDS.contains(&pid) - { - log::info!("Detected UHID XPAD"); - return DriverType::XpadUhid; - } - // Rog Ally if vid == drivers::rog_ally::driver::VID && drivers::rog_ally::driver::PIDS.contains(&pid) { log::info!("Detected ROG Ally"); return DriverType::RogAlly; } + // XpadUhid + let drivers = device.drivers(); + if drivers.contains(&"microsoft".to_string()) { + let syspath = device.syspath(); + if syspath.contains("uhid") { + log::info!("Detected UHID XPAD"); + return DriverType::XpadUhid; + } + } + // Unknown log::warn!("No driver for hidraw interface found. VID: {vid}, PID: {pid}"); DriverType::Unknown diff --git a/src/udev/device.rs b/src/udev/device.rs index 65a1d68..f1718c9 100644 --- a/src/udev/device.rs +++ b/src/udev/device.rs @@ -26,10 +26,15 @@ pub trait AttributeGetter { fn product(&self) -> String; fn serial_number(&self) -> String; fn uniq(&self) -> String; + fn get_attributes(&self) -> HashMap; /// Returns the value of the given property from the device fn get_property(&self, property: &str) -> Option; /// Returns device properties for the device. E.g. {"ID_INPUT": "1", ...} fn get_properties(&self) -> HashMap; + /// Returns a list of all drivers used for this device. This list will be + /// in ascending order, with the first item in the list being the first + /// discovered driver in the device tree. + fn drivers(&self) -> Vec; } impl AttributeGetter for ::udev::Device { @@ -164,6 +169,35 @@ impl AttributeGetter for ::udev::Device { attr } + /// Returns a list of all drivers used for this device. This list will be + /// in ascending order, with the first item in the list being the first + /// discovered driver in the device tree. + fn drivers(&self) -> Vec { + let mut drivers = vec![]; + if let Some(driver) = self.driver() { + let value = driver.to_string_lossy().to_string(); + if !value.is_empty() { + drivers.push(value); + } + } + + // Walk up the device tree and query for each driver + let mut parent = self.parent(); + while parent.is_some() { + let current_parent = parent.unwrap(); + if let Some(driver) = current_parent.driver() { + let value = driver.to_string_lossy().to_string(); + if !value.is_empty() { + drivers.push(value); + } + } + + parent = current_parent.parent(); + } + + drivers + } + /// Looks for the given attribute at the given path using sysfs. fn get_attribute_from_sysfs(&self, path: &str, attribute: &str) -> Option { let parent = self.parent()?; @@ -197,6 +231,37 @@ impl AttributeGetter for ::udev::Device { None } + /// Recursively gets attributes for this device and all parent devices. + fn get_attributes(&self) -> HashMap { + let mut attributes = HashMap::new(); + for attr in self.attributes() { + let key = attr.name().to_string_lossy().to_string(); + if attributes.contains_key(&key) { + continue; + } + let value = attr.value().to_string_lossy().to_string(); + attributes.insert(key, value); + } + + // Walk up the device tree and query each device + let mut parent = self.parent(); + while parent.is_some() { + let current_parent = parent.unwrap(); + for attr in current_parent.attributes() { + let key = attr.name().to_string_lossy().to_string(); + if attributes.contains_key(&key) { + continue; + } + let value = attr.value().to_string_lossy().to_string(); + attributes.insert(key, value); + } + + parent = current_parent.parent(); + } + + attributes + } + /// Gets an attribute from the first device in the device tree to match the attribute. fn get_attribute_from_tree(&self, attribute: &str) -> String { // Check if the current device has this attribute @@ -345,6 +410,14 @@ impl UdevDevice { device.devpath().to_string_lossy().to_string() } + /// Recursively returns all drivers associated with the device. + pub fn drivers(&self) -> Vec { + let Ok(device) = self.get_device() else { + return vec![]; + }; + device.drivers() + } + /// Return the bustype attribute from the device pub fn id_bustype(&self) -> u16 { if let Some(bus_type) = self.bus_type { @@ -447,6 +520,11 @@ impl UdevDevice { self.sysname.clone() } + /// Returns the syspath of the device. + /// + /// The path is an absolute path and includes the sys mount point. For example, the syspath for + /// `tty0` could be `/sys/devices/virtual/tty/tty0`, which includes the sys mount point, + /// `/sys`. pub fn syspath(&self) -> String { self.syspath.clone() } @@ -476,6 +554,27 @@ impl UdevDevice { } } + /// Recursively gets attributes for this device and all parent devices. + pub fn get_attributes(&self) -> HashMap { + let Ok(device) = self.get_device() else { + return HashMap::new(); + }; + device.get_attributes() + } + + /// Gets an attribute from the first device in the device tree to match the attribute. + pub fn get_attribute_from_tree(&self, attribute: &str) -> Option { + let Ok(device) = self.get_device() else { + return None; + }; + let value = device.get_attribute_from_tree(attribute); + if value.is_empty() { + None + } else { + Some(value) + } + } + /// Returns the value of the given property from the device pub fn get_property(&self, property: &str) -> Option { let Ok(device) = self.get_device() else {