From 20f1243608688ec2320f96196d0ca89936c634b1 Mon Sep 17 00:00:00 2001 From: Denis Benato Date: Mon, 30 Dec 2024 23:51:23 +0100 Subject: [PATCH 1/3] feat(config) implement LED configuration Implement Led configuration support with a fixed color. Cleanup wasteful clone() calls in various lookup methods. --- src/config/mod.rs | 71 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 12 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index e9ad6be..f67cba2 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 led: Option, pub udev: Option, pub unique: Option, pub blocked: Option, @@ -362,6 +363,24 @@ pub struct IIO { pub mount_matrix: Option, } +#[derive(Debug, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +#[allow(clippy::upper_case_acronyms)] +pub struct Led { + pub id: Option, + pub name: Option, + pub led_fixed_color: Option, +} + +#[derive(Debug, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +#[allow(clippy::upper_case_acronyms)] +pub struct LedFixedColor { + pub r: u8, + pub g: u8, + pub b: u8, +} + #[derive(Debug, Deserialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] #[allow(clippy::upper_case_acronyms)] @@ -460,6 +479,15 @@ impl CompositeDeviceConfig { } } } + "leds" => { + for config in self.source_devices.iter() { + if let Some(led_config) = config.led.as_ref() { + if self.has_matching_led(udevice, led_config) { + return Some(config.clone()); + } + } + } + } _ => (), }; None @@ -578,7 +606,6 @@ impl CompositeDeviceConfig { /// 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,); - let hidraw_config = hidraw_config.clone(); // TODO: Switch either evdev of hidraw configs to use the same type. Legacy version had i16 // for hidraw and string for evdev. @@ -606,7 +633,7 @@ impl CompositeDeviceConfig { } } - if let Some(name) = hidraw_config.name { + if let Some(name) = hidraw_config.name.as_ref() { let dname = device.name(); log::trace!("Checking name: {name} against {dname}"); if !glob_match(name.as_str(), dname.as_str()) { @@ -620,9 +647,31 @@ impl CompositeDeviceConfig { /// Returns true if a given iio device is within a list of iio configs. pub fn has_matching_iio(&self, device: &UdevDevice, iio_config: &IIO) -> bool { log::trace!("Checking iio config: {:?} against {:?}", iio_config, device); - let iio_config = iio_config.clone(); - if let Some(id) = iio_config.id { + if let Some(id) = iio_config.id.as_ref() { + let dsyspath = device.syspath(); + log::trace!("Checking id: {id} against {dsyspath}"); + if !glob_match(id.as_str(), dsyspath.as_str()) { + return false; + } + } + + if let Some(name) = iio_config.name.as_ref() { + let dname = device.name(); + log::trace!("Checking name: {name} against {dname}"); + if !glob_match(name.as_str(), dname.as_str()) { + return false; + } + } + + true + } + + /// Returns true if a given iio device is within a list of iio configs. + pub fn has_matching_led(&self, device: &UdevDevice, led_config: &Led) -> bool { + log::trace!("Checking led config: {:?} against {:?}", led_config, device); + + if let Some(id) = led_config.id.as_ref() { let dsyspath = device.syspath(); log::trace!("Checking id: {id} against {dsyspath}"); if !glob_match(id.as_str(), dsyspath.as_str()) { @@ -630,7 +679,7 @@ impl CompositeDeviceConfig { } } - if let Some(name) = iio_config.name { + if let Some(name) = led_config.name.as_ref() { let dname = device.name(); log::trace!("Checking name: {name} against {dname}"); if !glob_match(name.as_str(), dname.as_str()) { @@ -650,9 +699,7 @@ impl CompositeDeviceConfig { device ); - let evdev_config = evdev_config.clone(); - - if let Some(name) = evdev_config.name { + if let Some(name) = evdev_config.name.as_ref() { let dname = device.name(); log::trace!("Checking name: {name} against {dname}"); if !glob_match(name.as_str(), dname.as_str()) { @@ -660,7 +707,7 @@ impl CompositeDeviceConfig { } } - if let Some(phys_path) = evdev_config.phys_path { + if let Some(phys_path) = evdev_config.phys_path.as_ref() { let dphys_path = device.phys(); log::trace!("Checking phys_path: {phys_path} against {dphys_path}"); if !glob_match(phys_path.as_str(), dphys_path.as_str()) { @@ -668,7 +715,7 @@ impl CompositeDeviceConfig { } } - if let Some(handler) = evdev_config.handler { + if let Some(handler) = evdev_config.handler.as_ref() { let handle = device.sysname(); log::trace!("Checking handler: {handler} against {handle}"); if !glob_match(handler.as_str(), handle.as_str()) { @@ -676,7 +723,7 @@ impl CompositeDeviceConfig { } } - if let Some(vendor_id) = evdev_config.vendor_id { + if let Some(vendor_id) = evdev_config.vendor_id.as_ref() { let id_vendor = format!("{:04x}", device.id_vendor()); log::trace!("Checking vendor ID: {vendor_id} against {id_vendor}"); if !glob_match(vendor_id.as_str(), id_vendor.as_str()) { @@ -684,7 +731,7 @@ impl CompositeDeviceConfig { } } - if let Some(product_id) = evdev_config.product_id { + if let Some(product_id) = evdev_config.product_id.as_ref() { let id_product = format!("{:04x}", device.id_product()); log::trace!("Checking product ID: {product_id} against {id_product}"); if !glob_match(product_id.as_str(), id_product.as_str()) { From ad295a983eb64187b0381c93978faab76c47b3b4 Mon Sep 17 00:00:00 2001 From: Denis Benato Date: Tue, 31 Dec 2024 15:54:49 +0100 Subject: [PATCH 2/3] chore(LED): add configuration for the ASUS ROG Ally device --- rootfs/usr/share/inputplumber/devices/50-rog_ally.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rootfs/usr/share/inputplumber/devices/50-rog_ally.yaml b/rootfs/usr/share/inputplumber/devices/50-rog_ally.yaml index 8e9fbd6..44e72c6 100644 --- a/rootfs/usr/share/inputplumber/devices/50-rog_ally.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-rog_ally.yaml @@ -53,6 +53,9 @@ source_devices: x: [1, 0, 0] y: [0, -1, 0] z: [0, 0, -1] + - group: led + led: + name: ASUS ROG Ally Config # Optional configuration for the composite device options: From 9d76067b5e515da17f04965f50556ee2860e3292 Mon Sep 17 00:00:00 2001 From: Denis Benato Date: Tue, 31 Dec 2024 03:12:25 +0100 Subject: [PATCH 3/3] feat(LED): LEDs initial implementation Enable acquisition of LED devices and setting of a fixed color if specified in the configuration. --- src/dbus/interface/source/led.rs | 47 ++++++ src/dbus/interface/source/mod.rs | 1 + src/input/composite_device/mod.rs | 15 +- src/input/manager.rs | 34 +++++ src/input/source/led.rs | 98 ++++++++++++ src/input/source/led/multicolor_chassis.rs | 167 +++++++++++++++++++++ src/input/source/mod.rs | 9 ++ src/udev/device.rs | 3 + 8 files changed, 373 insertions(+), 1 deletion(-) create mode 100644 src/dbus/interface/source/led.rs create mode 100644 src/input/source/led.rs create mode 100644 src/input/source/led/multicolor_chassis.rs diff --git a/src/dbus/interface/source/led.rs b/src/dbus/interface/source/led.rs new file mode 100644 index 0000000..7bc7ad7 --- /dev/null +++ b/src/dbus/interface/source/led.rs @@ -0,0 +1,47 @@ +use crate::input::source::iio::get_dbus_path; +use crate::udev::device::UdevDevice; +use std::error::Error; +use zbus::{fdo, Connection}; +use zbus_macros::interface; + +/// DBusInterface exposing information about a led device +pub struct SourceLedInterface { + device: UdevDevice, +} + +impl SourceLedInterface { + pub fn new(device: UdevDevice) -> SourceLedInterface { + SourceLedInterface { device } + } + /// Creates a new instance of the source led interface on DBus. Returns + /// a structure with information about the source device. + pub async fn listen_on_dbus( + conn: Connection, + device: UdevDevice, + ) -> Result<(), Box> { + let iface = SourceLedInterface::new(device); + let Ok(id) = iface.id() else { + return Ok(()); + }; + let path = get_dbus_path(id); + tokio::task::spawn(async move { + log::debug!("Starting dbus interface: {path}"); + let result = conn.object_server().at(path.clone(), iface).await; + if let Err(e) = result { + log::debug!("Failed to start dbus interface {path}: {e:?}"); + } else { + log::debug!("Started dbus interface: {path}"); + } + }); + Ok(()) + } +} + +#[interface(name = "org.shadowblip.Input.Source.LEDDevice")] +impl SourceLedInterface { + /// Returns the human readable name of the device (e.g. XBox 360 Pad) + #[zbus(property)] + fn id(&self) -> fdo::Result { + Ok(self.device.sysname()) + } +} diff --git a/src/dbus/interface/source/mod.rs b/src/dbus/interface/source/mod.rs index d87e299..ab930c4 100644 --- a/src/dbus/interface/source/mod.rs +++ b/src/dbus/interface/source/mod.rs @@ -1,4 +1,5 @@ pub mod evdev; pub mod hidraw; pub mod iio_imu; +pub mod led; pub mod udev; diff --git a/src/input/composite_device/mod.rs b/src/input/composite_device/mod.rs index b0079b9..632dfc9 100644 --- a/src/input/composite_device/mod.rs +++ b/src/input/composite_device/mod.rs @@ -31,7 +31,9 @@ use crate::{ Event, }, output_event::UinputOutputEvent, - source::{evdev::EventDevice, hidraw::HidRawDevice, iio::IioDevice, SourceDevice}, + source::{ + evdev::EventDevice, hidraw::HidRawDevice, iio::IioDevice, led::LedDevice, SourceDevice, + }, }, udev::{device::UdevDevice, hide_device, unhide_device}, }; @@ -1423,6 +1425,17 @@ impl CompositeDevice { let device = IioDevice::new(device, self.client(), config)?; SourceDevice::Iio(device) } + "leds" => { + // Get any defined config for the IIO device + let config = if let Some(device_config) = self.config.get_matching_device(&device) { + device_config.led + } else { + None + }; + + log::debug!("Adding source device: {:?}", device.name()); + SourceDevice::Led(LedDevice::new(device, self.client(), config)?) + } _ => { return Err(format!( "Unspported subsystem: {subsystem}, unable to add source device {}", diff --git a/src/input/manager.rs b/src/input/manager.rs index dd50581..a8e2e92 100644 --- a/src/input/manager.rs +++ b/src/input/manager.rs @@ -29,6 +29,7 @@ use crate::dbus::interface::manager::ManagerInterface; use crate::dbus::interface::source::evdev::SourceEventDeviceInterface; use crate::dbus::interface::source::hidraw::SourceHIDRawInterface; use crate::dbus::interface::source::iio_imu::SourceIioImuInterface; +use crate::dbus::interface::source::led::SourceLedInterface; use crate::dbus::interface::source::udev::SourceUdevDeviceInterface; use crate::dmi::data::DMIData; use crate::dmi::get_cpu_info; @@ -37,6 +38,7 @@ use crate::input::composite_device::CompositeDevice; use crate::input::source::evdev; use crate::input::source::hidraw; use crate::input::source::iio; +use crate::input::source::led; use crate::input::target::TargetDevice; use crate::input::target::TargetDeviceTypeId; use crate::udev; @@ -1188,6 +1190,35 @@ impl Manager { log::debug!("Finished adding event device {id}"); } + "leds" => { + log::debug!("LED device added: {} ({})", device.name(), device.sysname()); + + // Create a DBus interface for the event device + let conn = self.dbus.clone(); + log::debug!("Attempting to listen on dbus for {dev_path} | {sysname}"); + task::spawn(async move { + let result = SourceLedInterface::listen_on_dbus(conn, dev).await; + if let Err(e) = result { + log::error!("Error creating source evdev dbus interface: {e:?}"); + } + log::debug!("Finished adding source device on dbus"); + }); + // Add the device as a source device + let path = led::get_dbus_path(sys_name.clone()); + self.source_device_dbus_paths.insert(id.clone(), path); + // Check to see if the device is virtual + if device.is_virtual() { + log::debug!("{} is virtual, skipping consideration.", dev_path); + return Ok(()); + } else { + log::trace!("Real device: {}", dev_path); + } + // Signal that a source device was added + log::debug!("Spawing task to add source device: {id}"); + self.on_source_device_added(id.clone(), device).await?; + log::debug!("Finished adding event device {id}"); + } + _ => { return Err(format!("Device subsystem not supported: {subsystem:?}").into()); } @@ -1447,6 +1478,9 @@ impl Manager { let iio_devices = udev::discover_devices("iio")?; let iio_devices = iio_devices.into_iter().map(|dev| dev.into()).collect(); Manager::discover_devices(cmd_tx, iio_devices).await?; + let led_devices = udev::discover_devices("leds")?; + let led_devices = led_devices.into_iter().map(|dev| dev.into()).collect(); + Manager::discover_devices(&cmd_tx, led_devices).await?; Ok(()) } diff --git a/src/input/source/led.rs b/src/input/source/led.rs new file mode 100644 index 0000000..92a77b1 --- /dev/null +++ b/src/input/source/led.rs @@ -0,0 +1,98 @@ +pub mod multicolor_chassis; +use std::error::Error; +//use glob_match::glob_match; +use self::multicolor_chassis::MultiColorChassis; +use super::{SourceDeviceCompatible, SourceDriver}; +use crate::{ + config, constants::BUS_SOURCES_PREFIX, input::composite_device::client::CompositeDeviceClient, + udev::device::UdevDevice, +}; +/// List of available drivers +enum DriverType { + MultiColorChassis, +} +/// [LedDevice] represents an input device using the leds subsystem. +#[derive(Debug)] +pub enum LedDevice { + MultiColorChassis(SourceDriver), +} + +impl SourceDeviceCompatible for LedDevice { + fn get_device_ref(&self) -> &UdevDevice { + match self { + LedDevice::MultiColorChassis(source_driver) => source_driver.info_ref(), + } + } + + fn get_id(&self) -> String { + match self { + LedDevice::MultiColorChassis(source_driver) => source_driver.get_id(), + } + } + + fn client(&self) -> super::client::SourceDeviceClient { + match self { + LedDevice::MultiColorChassis(source_driver) => source_driver.client(), + } + } + + async fn run(self) -> Result<(), Box> { + match self { + LedDevice::MultiColorChassis(source_driver) => source_driver.run().await, + } + } + + fn get_capabilities( + &self, + ) -> Result, super::InputError> { + match self { + LedDevice::MultiColorChassis(source_driver) => source_driver.get_capabilities(), + } + } + + fn get_device_path(&self) -> String { + match self { + LedDevice::MultiColorChassis(source_driver) => source_driver.get_device_path(), + } + } +} + +impl LedDevice { + /// Create a new [IioDevice] associated with the given device and + /// composite device. The appropriate driver will be selected based on + /// the provided device. + pub fn new( + device_info: UdevDevice, + composite_device: CompositeDeviceClient, + config: Option, + ) -> Result> { + let driver_type = LedDevice::get_driver_type(&device_info); + match driver_type { + DriverType::MultiColorChassis => { + let device = MultiColorChassis::new( + device_info.clone(), + match config { + Some(cfg) => cfg.led_fixed_color, + None => None, + }, + )?; + let source_device = SourceDriver::new(composite_device, device, device_info); + Ok(Self::MultiColorChassis(source_device)) + } + } + } + /// Return the driver type for the given device info + fn get_driver_type(device: &UdevDevice) -> DriverType { + let device_name = device.name(); + let name = device_name.as_str(); + log::debug!("Finding driver for LED interface: {name}"); + + // TODO: for now everthing is a MultiColorChassis + DriverType::MultiColorChassis + } +} +/// Returns the DBus path for an [LedDevice] from a device id (E.g. iio:device0) +pub fn get_dbus_path(id: String) -> String { + let name = id.replace(':', "_"); + format!("{}/{}", BUS_SOURCES_PREFIX, name) +} diff --git a/src/input/source/led/multicolor_chassis.rs b/src/input/source/led/multicolor_chassis.rs new file mode 100644 index 0000000..73ffdd6 --- /dev/null +++ b/src/input/source/led/multicolor_chassis.rs @@ -0,0 +1,167 @@ +use crate::{ + config::LedFixedColor, + input::source::{SourceInputDevice, SourceOutputDevice}, + udev::device::UdevDevice, +}; +use std::{error::Error, fmt::Debug, path::PathBuf}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum MultiColorChassisError { + #[error("Error reading multi_index: {0}")] + MultiIndexError(std::io::Error), + + #[error("File multi_intensity not found: {0}")] + MultiIntensityNotFound(PathBuf), + + #[error("Error updating multi_intensity: {0}")] + MultiIntensityUpdateError(std::io::Error), + + #[error("Unsupported index type: {0}")] + UnsupportedIndexType(String), +} + +enum IndexType { + RGB, +} + +/// MultiColorChassis source device implementation +pub struct MultiColorChassis { + multi_intensity_path: PathBuf, + multi_index_map: Vec, + fixed_color: Option, +} + +impl MultiColorChassis { + /// Create a new MultiColorChassis source device with the given udev + /// device information + pub fn new( + device_info: UdevDevice, + fixed_color: Option, + ) -> Result> { + let multi_intensity_path = + PathBuf::from(device_info.syspath().as_str()).join("multi_intensity"); + if !multi_intensity_path.exists() { + return Err(Box::new(MultiColorChassisError::MultiIntensityNotFound( + multi_intensity_path, + ))); + } + + let multi_index = PathBuf::from(device_info.syspath().as_str()).join("multi_index"); + let contents = std::fs::read_to_string(multi_index) + .map_err(|err| Box::new(MultiColorChassisError::MultiIndexError(err)))?; + let multi_index_strings = contents + .split_whitespace() + .into_iter() + .map(|str| String::from(str)) + .collect::>(); + let mut multi_index_map = Vec::::with_capacity(multi_index_strings.len()); + for idx in multi_index_strings.iter() { + match idx.as_str() { + "rgb" => multi_index_map.push(IndexType::RGB), + _ => { + return Err(Box::new(MultiColorChassisError::UnsupportedIndexType( + idx.clone(), + ))) + } + } + } + + let result = Self { + fixed_color, + multi_intensity_path, + multi_index_map, + }; + + // Immediatly set the user-defined color if one is provided + if let Some(color) = result.fixed_color.as_ref() { + result.write_color(color.r, color.g, color.b)? + }; + + Ok(result) + } + + fn write_color(&self, r: u8, g: u8, b: u8) -> Result<(), Box> { + let contents = std::fs::read_to_string(self.multi_intensity_path.as_path()) + .map_err(|err| Box::new(MultiColorChassisError::MultiIndexError(err)))?; + let contents = (self + .multi_index_map + .iter() + .zip(contents.split_whitespace().into_iter()) + .map(|(index_type, index_value)| match index_type { + IndexType::RGB => { + (((r as u32) << 16u32) | ((g as u32) << 8u32) | ((b as u32) << 0u32)) + .to_string() + } + _ => String::from(index_value), + }) + .collect::>()) + .join(" "); + Ok( + std::fs::write(self.multi_intensity_path.as_path(), contents) + .map_err(|err| Box::new(MultiColorChassisError::MultiIntensityUpdateError(err)))?, + ) + } +} + +impl Debug for MultiColorChassis { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MultiColorChassis").finish() + } +} + +impl SourceInputDevice for MultiColorChassis { + fn poll( + &mut self, + ) -> Result, crate::input::source::InputError> + { + Ok(Vec::new()) + } + + fn get_capabilities( + &self, + ) -> Result, crate::input::source::InputError> { + Ok(Vec::new()) + } +} + +impl SourceOutputDevice for MultiColorChassis { + fn write_event( + &mut self, + event: crate::input::output_event::OutputEvent, + ) -> Result<(), crate::input::source::OutputError> { + //log::trace!("Received output event: {event:?}"); + let _ = event; + Ok(()) + } + + fn upload_effect( + &mut self, + effect: evdev::FFEffectData, + ) -> Result { + //log::trace!("Received upload effect: {effect:?}"); + let _ = effect; + Ok(-1) + } + + fn update_effect( + &mut self, + effect_id: i16, + effect: evdev::FFEffectData, + ) -> Result<(), crate::input::source::OutputError> { + //log::trace!("Received update effect: {effect_id:?} {effect:?}"); + let _ = effect; + let _ = effect_id; + Ok(()) + } + + fn erase_effect(&mut self, effect_id: i16) -> Result<(), crate::input::source::OutputError> { + //log::trace!("Received erase effect: {effect_id:?}"); + let _ = effect_id; + Ok(()) + } + + fn stop(&mut self) -> Result<(), crate::input::source::OutputError> { + Ok(()) + } +} diff --git a/src/input/source/mod.rs b/src/input/source/mod.rs index e8ef128..e5e01fa 100644 --- a/src/input/source/mod.rs +++ b/src/input/source/mod.rs @@ -6,6 +6,7 @@ use std::{ }; use ::evdev::FFEffectData; +use led::LedDevice; use thiserror::Error; use tokio::sync::mpsc::{self, error::TryRecvError}; @@ -28,6 +29,7 @@ pub mod command; pub mod evdev; pub mod hidraw; pub mod iio; +pub mod led; /// Size of the [SourceCommand] buffer for receiving output events const BUFFER_SIZE: usize = 2048; @@ -373,6 +375,7 @@ pub enum SourceDevice { Event(EventDevice), HidRaw(HidRawDevice), Iio(IioDevice), + Led(LedDevice), } impl SourceDevice { @@ -382,6 +385,7 @@ impl SourceDevice { SourceDevice::Event(device) => device.get_device_ref(), SourceDevice::HidRaw(device) => device.get_device_ref(), SourceDevice::Iio(device) => device.get_device_ref(), + SourceDevice::Led(device) => device.get_device_ref(), } } @@ -391,6 +395,7 @@ impl SourceDevice { SourceDevice::Event(device) => device.get_id(), SourceDevice::HidRaw(device) => device.get_id(), SourceDevice::Iio(device) => device.get_id(), + SourceDevice::Led(device) => device.get_id(), } } @@ -400,6 +405,7 @@ impl SourceDevice { SourceDevice::Event(device) => device.client(), SourceDevice::HidRaw(device) => device.client(), SourceDevice::Iio(device) => device.client(), + SourceDevice::Led(device) => device.client(), } } @@ -409,6 +415,7 @@ impl SourceDevice { SourceDevice::Event(device) => device.run().await, SourceDevice::HidRaw(device) => device.run().await, SourceDevice::Iio(device) => device.run().await, + SourceDevice::Led(device) => device.run().await, } } @@ -418,6 +425,7 @@ impl SourceDevice { SourceDevice::Event(device) => device.get_capabilities(), SourceDevice::HidRaw(device) => device.get_capabilities(), SourceDevice::Iio(device) => device.get_capabilities(), + SourceDevice::Led(device) => device.get_capabilities(), } } @@ -427,6 +435,7 @@ impl SourceDevice { SourceDevice::Event(device) => device.get_device_path(), SourceDevice::HidRaw(device) => device.get_device_path(), SourceDevice::Iio(device) => device.get_device_path(), + SourceDevice::Led(device) => device.get_device_path(), } } } diff --git a/src/udev/device.rs b/src/udev/device.rs index f1718c9..31f69eb 100644 --- a/src/udev/device.rs +++ b/src/udev/device.rs @@ -550,6 +550,9 @@ impl UdevDevice { "iio" => { format!("iio://{}", self.sysname) } + "leds" => { + format!("leds://{}", self.sysname) + } _ => "".to_string(), } }