diff --git a/rootfs/usr/share/inputplumber/devices/50-legion_go.yaml b/rootfs/usr/share/inputplumber/devices/50-legion_go.yaml index 2abec577..6e5abf53 100644 --- a/rootfs/usr/share/inputplumber/devices/50-legion_go.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-legion_go.yaml @@ -25,12 +25,17 @@ source_devices: vendor_id: 0x17ef product_id: 0x6182 interface_num: 0 + - group: mouse # Touch Device + hidraw: + vendor_id: 0x17ef + product_id: 0x6182 + interface_num: 1 - group: gamepad hidraw: vendor_id: 0x17ef product_id: 0x6182 interface_num: 2 - - group: mouse # Only for optical X/Y + - group: mouse # Only for optical X/Y hidraw: vendor_id: 0x17ef product_id: 0x6182 @@ -51,6 +56,7 @@ source_devices: product_id: "6182" name: "Generic X-Box pad" - group: keyboard + unique: false evdev: vendor_id: "17ef" product_id: "618*" diff --git a/rootfs/usr/share/inputplumber/schema/composite_device_v1.json b/rootfs/usr/share/inputplumber/schema/composite_device_v1.json index 6cf27555..821eb19b 100644 --- a/rootfs/usr/share/inputplumber/schema/composite_device_v1.json +++ b/rootfs/usr/share/inputplumber/schema/composite_device_v1.json @@ -135,6 +135,10 @@ }, "hidraw": { "$ref": "#/definitions/Hidraw" + }, + "unique": { + "description": "If false, any devices matching this description will be added to the existing composite device. Defaults to true.", + "type": "boolean" } }, "required": [ diff --git a/src/config/mod.rs b/src/config/mod.rs index 7fff20c6..c0f594d4 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,8 +1,7 @@ -use std::error::Error; use std::io; use glob_match::glob_match; -use hidapi::{DeviceInfo, HidApi}; +use hidapi::DeviceInfo; use serde::Deserialize; use thiserror::Error; @@ -75,14 +74,14 @@ impl CapabilityMap { } /// Defines a platform match for loading a [CompositeDevice] -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] pub struct Match { pub dmi_data: Option, } /// Match DMI data for loading a [CompositeDevice] -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] pub struct DMIMatch { pub bios_release: Option, @@ -96,15 +95,16 @@ pub struct DMIMatch { pub cpu_vendor: Option, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] pub struct SourceDevice { pub group: String, pub evdev: Option, pub hidraw: Option, + pub unique: Option, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] pub struct Evdev { pub name: Option, @@ -114,7 +114,7 @@ pub struct Evdev { pub product_id: Option, } -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] pub struct Hidraw { pub vendor_id: Option, @@ -124,7 +124,7 @@ pub struct Hidraw { } /// Defines a combined device -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] pub struct CompositeDeviceConfig { pub version: u32, @@ -167,92 +167,83 @@ impl CompositeDeviceConfig { } /// Returns true if a given hidraw device is within a list of hidraw configs. - pub fn has_matching_hidraw(&self, device: &DeviceInfo, hidraw_configs: &Vec) -> bool { - log::debug!("Checking hidraw config: {:?}", hidraw_configs); - for hidraw_config in hidraw_configs.clone() { - let hidraw_config = hidraw_config.clone(); + pub fn has_matching_hidraw(&self, device: &DeviceInfo, hidraw_config: &Hidraw) -> bool { + log::debug!("Checking hidraw config: {:?}", hidraw_config); + let hidraw_config = hidraw_config.clone(); - if let Some(vendor_id) = hidraw_config.vendor_id { - if device.vendor_id() != vendor_id { - continue; - } + if let Some(vendor_id) = hidraw_config.vendor_id { + if device.vendor_id() != vendor_id { + return false; } + } - if let Some(product_id) = hidraw_config.product_id { - if device.product_id() != product_id { - continue; - } + if let Some(product_id) = hidraw_config.product_id { + if device.product_id() != product_id { + return false; } + } - if let Some(interface_num) = hidraw_config.interface_num { - if device.interface_number() != interface_num { - continue; - } + if let Some(interface_num) = hidraw_config.interface_num { + if device.interface_number() != interface_num { + return false; } - - return true; } - false + true } /// Returns true if a given evdev device is within a list of evdev configs. pub fn has_matching_evdev( &self, device: &procfs::device::Device, - evdev_configs: &Vec, + evdev_config: &Evdev, ) -> bool { - // TODO: Maybe in the future we will support virtual devices if we figure something - // out. Ignore virtual devices. + //TODO: Check if the evdev has no proterties defined, that would always match. + if is_virtual(device) { log::debug!("{} is virtual, skipping.", device.name); return false; } - for evdev_config in evdev_configs.clone() { - let evdev_config = evdev_config.clone(); + let evdev_config = evdev_config.clone(); - if let Some(name) = evdev_config.name { - if !glob_match(name.as_str(), device.name.as_str()) { - continue; - } + if let Some(name) = evdev_config.name { + if !glob_match(name.as_str(), device.name.as_str()) { + return false; } + } - if let Some(phys_path) = evdev_config.phys_path { - if !glob_match(phys_path.as_str(), device.phys_path.as_str()) { - continue; - } + if let Some(phys_path) = evdev_config.phys_path { + if !glob_match(phys_path.as_str(), device.phys_path.as_str()) { + return false; } + } - if let Some(handler) = evdev_config.handler { - let mut has_matches = false; - for handle in device.handlers.clone() { - if !glob_match(handler.as_str(), handle.as_str()) { - continue; - } - has_matches = true; - } - if !has_matches { + if let Some(handler) = evdev_config.handler { + let mut has_matches = false; + for handle in device.handlers.clone() { + if !glob_match(handler.as_str(), handle.as_str()) { continue; } + has_matches = true; } - - if let Some(vendor_id) = evdev_config.vendor_id { - if !glob_match(vendor_id.as_str(), device.id.vendor.as_str()) { - continue; - } + if !has_matches { + return false; } + } - if let Some(product_id) = evdev_config.product_id { - if !glob_match(product_id.as_str(), device.id.product.as_str()) { - continue; - } + if let Some(vendor_id) = evdev_config.vendor_id { + if !glob_match(vendor_id.as_str(), device.id.vendor.as_str()) { + return false; } - - return true; } - false + if let Some(product_id) = evdev_config.product_id { + if !glob_match(product_id.as_str(), device.id.product.as_str()) { + return false; + } + } + true } /// Returns true if the configuration has a valid set of matches. This will @@ -350,12 +341,12 @@ impl CompositeDeviceConfig { /// Determines if a procfs device is virtual or real. fn is_virtual(device: &procfs::device::Device) -> bool { - if device.phys_path != "" { + if !device.phys_path.is_empty() { return false; } if device.sysfs_path.contains("/devices/virtual") { return true; } - return false; + false } diff --git a/src/input/composite_device/mod.rs b/src/input/composite_device/mod.rs index 9a856e5d..a23d5e64 100644 --- a/src/input/composite_device/mod.rs +++ b/src/input/composite_device/mod.rs @@ -192,8 +192,6 @@ pub struct CompositeDevice { source_devices: Vec, /// Physical device path for source devices. E.g. ["/dev/input/event0"] source_device_paths: Vec, - /// Unique identifiers for source devices. E.g. ["evdev://event0"] - source_device_ids: Vec, /// All currently running source device threads source_device_tasks: JoinSet<()>, /// Unique identifiers for running source devices. E.g. ["evdev://event0"] @@ -228,7 +226,6 @@ impl CompositeDevice { rx, source_devices: Vec::new(), source_device_paths: Vec::new(), - source_device_ids: Vec::new(), source_device_tasks: JoinSet::new(), source_devices_used: Vec::new(), target_devices: HashMap::new(), @@ -311,22 +308,22 @@ impl CompositeDevice { } } Command::SourceDeviceStopped(device_id) => { - log::debug!("Detected source device removal: {}", device_id); - let idx = self - .source_devices_used - .iter() - .position(|v| v.clone() == device_id); - if let Some(idx) = idx { - self.source_devices_used.remove(idx); + log::debug!("Detected source device stopped: {}", device_id); + if let Err(e) = self.on_source_device_removed(device_id).await { + log::error!("Failed to remove source device: {:?}", e); } if self.source_devices_used.is_empty() { break; } } - Command::SourceDeviceRemoved(id) => { - if let Err(e) = self.on_source_device_removed(id).await { + Command::SourceDeviceRemoved(device_id) => { + log::debug!("Detected source device removed: {}", device_id); + if let Err(e) = self.on_source_device_removed(device_id).await { log::error!("Failed to remove source device: {:?}", e); } + if self.source_devices_used.is_empty() { + break; + } } Command::Stop => { log::debug!("Stopping CompositeDevice"); @@ -393,8 +390,8 @@ impl CompositeDevice { } /// Returns an array of all source devices ids being used by this device. - pub fn get_source_device_ids(&self) -> Vec { - self.source_device_ids.clone() + pub fn get_source_devices_used(&self) -> Vec { + self.source_devices_used.clone() } /// Sets the DBus target devices on the [CompositeDevice]. @@ -806,7 +803,7 @@ impl CompositeDevice { self.run_source_devices().await?; log::debug!( "Finished adding source device. All sources: {:?}", - self.source_device_ids + self.source_devices_used ); Ok(()) } @@ -820,11 +817,11 @@ impl CompositeDevice { self.source_device_paths.remove(idx); }; - let Some(idx) = self.source_device_ids.iter().position(|str| str == &id) else { + let Some(idx) = self.source_devices_used.iter().position(|str| str == &id) else { return Ok(()); }; - self.source_device_ids.remove(idx); + self.source_devices_used.remove(idx); } if id.starts_with("hidraw://") { let name = id.strip_prefix("hidraw://").unwrap(); @@ -834,11 +831,11 @@ impl CompositeDevice { self.source_device_paths.remove(idx); }; - let Some(idx) = self.source_device_ids.iter().position(|str| str == &id) else { + let Some(idx) = self.source_devices_used.iter().position(|str| str == &id) else { return Ok(()); }; - self.source_device_ids.remove(idx); + self.source_devices_used.remove(idx); } Ok(()) @@ -862,7 +859,7 @@ impl CompositeDevice { let source_device = source::SourceDevice::EventDevice(device); self.source_devices.push(source_device); self.source_device_paths.push(device_path); - self.source_device_ids.push(id); + self.source_devices_used.push(id); } SourceDeviceInfo::HIDRawDeviceInfo(info) => { @@ -877,7 +874,7 @@ impl CompositeDevice { let source_device = source::SourceDevice::HIDRawDevice(device); self.source_devices.push(source_device); self.source_device_paths.push(device_path); - self.source_device_ids.push(id); + self.source_devices_used.push(id); } } Ok(()) diff --git a/src/input/manager.rs b/src/input/manager.rs index e041b2ea..42b7dcfa 100644 --- a/src/input/manager.rs +++ b/src/input/manager.rs @@ -11,6 +11,7 @@ use zbus_macros::dbus_interface; use crate::config::CapabilityMap; use crate::config::CompositeDeviceConfig; +use crate::config::SourceDevice; use crate::constants::BUS_PREFIX; use crate::constants::BUS_TARGETS_PREFIX; use crate::dmi::data::DMIData; @@ -113,15 +114,22 @@ pub struct Manager { /// The receive side of the channel used to listen for [Command] messages /// from other objects. rx: broadcast::Receiver, + /// Mapping of source devices to their SourceDevice objects. + /// E.g. {"evdev://event0": } + source_devices: HashMap, /// Mapping of source devices to their DBus path /// E.g. {"evdev://event0": "/org/shadowblip/InputPlumber/devices/source/event0"} - source_devices: HashMap, + source_device_dbus_paths: HashMap, /// Map of source devices being used by a [CompositeDevice]. /// E.g. {"evdev://event0": "/org/shadowblip/InputPlumber/CompositeDevice0"} source_devices_used: HashMap, /// Mapping of DBus path to its corresponding [CompositeDevice] handle /// E.g. {"/org/shadowblip/InputPlumber/CompositeDevice0": } composite_devices: HashMap, + /// Mapping of all source devices used by composite devices with the CompositeDevice path as + /// the key for the hashmap. + /// E.g. {"/org/shadowblip/InputPlumber/CompositeDevice0": Vec} + composite_device_sources: HashMap>, /// Mapping of DBus path to its corresponding [CompositeDeviceConfig] /// E.g. {"/org/shadowblip/InputPlumber/CompositeDevice0": } used_configs: HashMap, @@ -146,9 +154,11 @@ impl Manager { tx, composite_devices: HashMap::new(), source_devices: HashMap::new(), + source_device_dbus_paths: HashMap::new(), source_devices_used: HashMap::new(), target_devices: HashMap::new(), used_configs: HashMap::new(), + composite_device_sources: HashMap::new(), } } @@ -245,7 +255,7 @@ impl Manager { // these source devices. // TODO: Should we allow multiple composite devices with the same source? let mut devices_in_use = false; - let source_device_ids = device.get_source_device_ids(); + let source_device_ids = device.get_source_devices_used(); for (id, path) in self.source_devices_used.iter() { if !source_device_ids.contains(id) { continue; @@ -364,17 +374,30 @@ impl Manager { mut device: CompositeDevice, config: CompositeDeviceConfig, target_types: Option>, + source_device: SourceDevice, ) -> Result<(), Box> { // Generate the DBus tree path for this composite device let path = self.next_composite_dbus_path(); // Keep track of the source devices that this composite device is // using. - let source_device_ids = device.get_source_device_ids(); + let source_device_ids = device.get_source_devices_used(); for id in source_device_ids { - self.source_devices_used.insert(id, path.clone()); + self.source_devices_used.insert(id.clone(), path.clone()); + self.source_devices.insert(id, source_device.clone()); } + let composite_id = path.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); + // Create a DBus interface for the device device.listen_on_dbus(path.clone()).await?; @@ -464,25 +487,40 @@ impl Manager { ) -> Result<(), Box> { // Check all existing composite devices to see if this device is part of // their config - for composite_device in self.composite_devices.keys() { + 'start: for composite_device in self.composite_devices.keys() { let Some(config) = self.used_configs.get(composite_device) else { continue; }; log::debug!("Checking existing config {:?} for device", config.name); let source_devices = config.source_devices.clone(); - let device_info = device_info.clone(); - match device_info { + match device_info.clone() { SourceDeviceInfo::EvdevDeviceInfo(info) => { log::debug!("Checking if existing composite device is missing event device"); - let mut configs = Vec::new(); for source_device in source_devices { - if let Some(evdev) = source_device.evdev { - configs.push(evdev); - } - if configs.is_empty() { + if source_device.evdev.is_none() { continue; } - if config.has_matching_evdev(&info, &configs) { + if config.has_matching_evdev(&info, &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(unique) = source_device.clone().unique { + if unique { + log::debug!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); + break 'start; + } + } else { + log::debug!("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() { @@ -494,23 +532,51 @@ impl Manager { } self.add_event_device_to_composite_device(&info, handle.unwrap())?; self.source_devices_used - .insert(id, composite_device.clone()); + .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(()); } } } SourceDeviceInfo::HIDRawDeviceInfo(info) => { - let mut configs = Vec::new(); log::debug!("Checking if existing composite device is missing hidraw device"); for source_device in source_devices { - if let Some(hidraw) = source_device.hidraw { - configs.push(hidraw); - } - if configs.is_empty() { + if source_device.hidraw.is_none() { continue; } - if config.has_matching_hidraw(&info, &configs) { + if config.has_matching_hidraw(&info, &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(unique) = source_device.clone().unique { + if unique { + log::debug!("Found unique device {:?}, not adding to composite device {}", source_device, composite_device); + break 'start; + } + } else { + log::debug!("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() { @@ -522,8 +588,19 @@ impl Manager { } self.add_hidraw_device_to_composite_device(&info, handle.unwrap())?; self.source_devices_used - .insert(id, composite_device.clone()); + .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(()); } } @@ -547,55 +624,60 @@ impl Manager { } let source_devices = config.source_devices.clone(); - let dev_info = device_info.clone(); - match dev_info { + match device_info.clone() { SourceDeviceInfo::EvdevDeviceInfo(info) => { - let mut configs = Vec::new(); for source_device in source_devices { - if let Some(evdev) = source_device.evdev { - configs.push(evdev); - } - if configs.is_empty() { + if source_device.evdev.is_none() { continue; } - if config.has_matching_evdev(&info, &configs) { + // how to refrence source devices used by this config? + + if config.has_matching_evdev(&info, &source_device.clone().evdev.unwrap()) { log::info!("Found a matching event device, creating composite device"); let device = self - .create_composite_device_from_config(&config, device_info) + .create_composite_device_from_config(&config, device_info.clone()) .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(device, config, target_devices_config) - .await?; + self.start_composite_device( + device, + config, + target_devices_config, + source_device.clone(), + ) + .await?; return Ok(()); } } } SourceDeviceInfo::HIDRawDeviceInfo(info) => { - let mut configs = Vec::new(); for source_device in source_devices { - if let Some(hidraw) = source_device.hidraw { - configs.push(hidraw); - } - if configs.is_empty() { + if source_device.hidraw.is_none() { continue; } - if config.has_matching_hidraw(&info, &configs) { + if config.has_matching_hidraw(&info, &source_device.clone().hidraw.unwrap()) + { log::info!("Found a matching hidraw device, creating composite device"); let device = self - .create_composite_device_from_config(&config, device_info) + .create_composite_device_from_config(&config, device_info.clone()) .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(device, config, target_devices_config) - .await?; + self.start_composite_device( + device, + config, + target_devices_config, + source_device.clone(), + ) + .await?; + return Ok(()); } } @@ -622,8 +704,21 @@ impl Manager { handle .tx - .send(composite_device::Command::SourceDeviceRemoved(id))?; + .send(composite_device::Command::SourceDeviceRemoved(id.clone()))?; + + let Some(sources) = self.composite_device_sources.get_mut(composite_device_path) else { + return Err(format!("CompostiteDevice {} not found", composite_device_path).into()); + }; + let Some(device) = self.source_devices.get(&id) else { + return Err(format!("Device {} not found in source devices", id).into()); + }; + let idx = sources.iter().position(|item| item == device); + if idx.is_none() { + return Err(format!("Device {} not found in composite device sources", id).into()); + } + sources.remove(idx.unwrap()); + self.source_devices.remove(&id); Ok(()) } @@ -663,7 +758,7 @@ impl Manager { // Add the device as a source device let path = source::evdev::get_dbus_path(handler.clone()); - self.source_devices.insert(id, path); + self.source_device_dbus_paths.insert(id, path); Ok(()) } @@ -674,7 +769,7 @@ impl Manager { // Remove the device from our hashmap let id = format!("evdev://{}", handler); - self.source_devices.remove(&id); + self.source_device_dbus_paths.remove(&id); self.source_devices_used.remove(&id); // Remove the DBus interface @@ -718,7 +813,7 @@ impl Manager { // Add the device as a source device let path = source::hidraw::get_dbus_path(path); - self.source_devices.insert(id, path); + self.source_device_dbus_paths.insert(id, path); Ok(()) } @@ -732,7 +827,7 @@ impl Manager { self.tx .send(Command::SourceDeviceRemoved { id: id.clone() })?; - self.source_devices.remove(&id); + self.source_device_dbus_paths.remove(&id); self.source_devices_used.remove(&id); Ok(())