From 4548df204eb2a7c32d6430caac56d91d5d6cc024 Mon Sep 17 00:00:00 2001 From: William Edwards Date: Wed, 22 Jan 2025 21:35:57 -0800 Subject: [PATCH] feat(Touchscreen): add support for generic touchscreen devices --- .../inputplumber/devices/50-ayaneo_2.yaml | 11 + .../inputplumber/devices/50-ayaneo_2s.yaml | 11 + .../inputplumber/devices/50-ayaneo_flip.yaml | 19 + .../inputplumber/devices/50-legion_go.yaml | 13 + .../inputplumber/devices/50-msi_claw.yaml | 10 + .../inputplumber/devices/50-rog_ally.yaml | 8 + .../inputplumber/devices/50-rog_ally_x.yaml | 8 + .../inputplumber/devices/50-steam_deck.yaml | 19 +- .../schema/composite_device_v1.json | 53 +++ src/config/mod.rs | 34 ++ src/input/composite_device/mod.rs | 5 +- src/input/source/evdev.rs | 29 +- src/input/source/evdev/touchscreen.rs | 351 ++++++++++++++++++ src/input/target/touchscreen.rs | 233 +++++++++++- src/udev/device.rs | 50 ++- 15 files changed, 825 insertions(+), 29 deletions(-) create mode 100644 src/input/source/evdev/touchscreen.rs diff --git a/rootfs/usr/share/inputplumber/devices/50-ayaneo_2.yaml b/rootfs/usr/share/inputplumber/devices/50-ayaneo_2.yaml index a68cbe8..8faa84c 100644 --- a/rootfs/usr/share/inputplumber/devices/50-ayaneo_2.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-ayaneo_2.yaml @@ -47,6 +47,16 @@ source_devices: x: [0, -1, 0] y: [-1, 0, 0] z: [0, 0, -1] + #- group: touchscreen + # udev: + # properties: + # - name: ID_INPUT_TOUCHSCREEN + # value: "1" + # sys_name: "event*" + # subsystem: input + # config: + # touchscreen: + # orientation: "right" # Optional configuration for the composite device options: @@ -60,6 +70,7 @@ target_devices: - xbox-elite - mouse - keyboard + #- touchscreen # The ID of a device event mapping in the 'event_maps' folder capability_map_id: aya4 diff --git a/rootfs/usr/share/inputplumber/devices/50-ayaneo_2s.yaml b/rootfs/usr/share/inputplumber/devices/50-ayaneo_2s.yaml index ff374a5..8fa4cd5 100644 --- a/rootfs/usr/share/inputplumber/devices/50-ayaneo_2s.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-ayaneo_2s.yaml @@ -47,6 +47,16 @@ source_devices: x: [0, -1, 0] y: [-1, 0, 0] z: [0, 0, -1] + #- group: touchscreen + # udev: + # properties: + # - name: ID_INPUT_TOUCHSCREEN + # value: "1" + # sys_name: "event*" + # subsystem: input + # config: + # touchscreen: + # orientation: "right" # Optional configuration for the composite device options: @@ -60,6 +70,7 @@ target_devices: - xbox-elite - mouse - keyboard + #- touchscreen # The ID of a device event mapping in the 'event_maps' folder capability_map_id: aya4 diff --git a/rootfs/usr/share/inputplumber/devices/50-ayaneo_flip.yaml b/rootfs/usr/share/inputplumber/devices/50-ayaneo_flip.yaml index fa8796f..e5aed19 100644 --- a/rootfs/usr/share/inputplumber/devices/50-ayaneo_flip.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-ayaneo_flip.yaml @@ -47,6 +47,24 @@ source_devices: x: [0, -1, 0] y: [-1, 0, 0] z: [0, 0, -1] + - group: touchscreen + udev: + properties: + - name: ID_INPUT_TOUCHSCREEN + value: "1" + attributes: + - name: id/vendor + value: "0416" + - name: id/product + value: "1001" + sys_name: "event*" + subsystem: input + config: + touchscreen: + orientation: "normal" + override_source_size: true + width: 1200 + height: 1920 # Optional configuration for the composite device options: @@ -60,6 +78,7 @@ target_devices: - xbox-elite - mouse - keyboard + - touchscreen # The ID of a device event mapping in the 'event_maps' folder capability_map_id: aya4 diff --git a/rootfs/usr/share/inputplumber/devices/50-legion_go.yaml b/rootfs/usr/share/inputplumber/devices/50-legion_go.yaml index 6a9fee7..a7539e1 100644 --- a/rootfs/usr/share/inputplumber/devices/50-legion_go.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-legion_go.yaml @@ -138,6 +138,18 @@ source_devices: y: [-1, 0, 0] z: [0, 0, -1] + # Touchscreen + #- group: touchscreen + # udev: + # properties: + # - name: ID_INPUT_TOUCHSCREEN + # value: "1" + # sys_name: "event*" + # subsystem: input + # config: + # touchscreen: + # orientation: "left" + # Block all evdev devices; mouse, touchpad, gamepad, keyboard - group: gamepad blocked: true @@ -160,3 +172,4 @@ target_devices: - mouse - keyboard - touchpad + #- touchscreen diff --git a/rootfs/usr/share/inputplumber/devices/50-msi_claw.yaml b/rootfs/usr/share/inputplumber/devices/50-msi_claw.yaml index 89c1735..2842540 100644 --- a/rootfs/usr/share/inputplumber/devices/50-msi_claw.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-msi_claw.yaml @@ -53,6 +53,15 @@ source_devices: y: [-1, 0, 0] z: [0, 0, -1] + # Touchscreen + #- group: touchscreen + # udev: + # properties: + # - name: ID_INPUT_TOUCHSCREEN + # value: "1" + # sys_name: "event*" + # subsystem: input + # Optional configuration for the composite device options: # If true, InputPlumber will automatically try to manage the input device. If @@ -65,6 +74,7 @@ target_devices: - xbox-elite - mouse - keyboard + #- touchscreen # The ID of a device event mapping in the 'event_maps' folder capability_map_id: claw1 diff --git a/rootfs/usr/share/inputplumber/devices/50-rog_ally.yaml b/rootfs/usr/share/inputplumber/devices/50-rog_ally.yaml index 8e9fbd6..930e2ed 100644 --- a/rootfs/usr/share/inputplumber/devices/50-rog_ally.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-rog_ally.yaml @@ -53,6 +53,13 @@ source_devices: x: [1, 0, 0] y: [0, -1, 0] z: [0, 0, -1] + #- group: touchscreen + # udev: + # properties: + # - name: ID_INPUT_TOUCHSCREEN + # value: "1" + # sys_name: "event*" + # subsystem: input # Optional configuration for the composite device options: @@ -66,6 +73,7 @@ target_devices: - xbox-elite - mouse - keyboard + #- touchscreen # The ID of a device event mapping in the 'event_maps' folder capability_map_id: aly1 diff --git a/rootfs/usr/share/inputplumber/devices/50-rog_ally_x.yaml b/rootfs/usr/share/inputplumber/devices/50-rog_ally_x.yaml index 8f5b2f7..b37099a 100644 --- a/rootfs/usr/share/inputplumber/devices/50-rog_ally_x.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-rog_ally_x.yaml @@ -52,6 +52,13 @@ source_devices: x: [1, 0, 0] y: [0, -1, 0] z: [0, 0, -1] + #- group: touchscreen + # udev: + # properties: + # - name: ID_INPUT_TOUCHSCREEN + # value: "1" + # sys_name: "event*" + # subsystem: input # Optional configuration for the composite device options: @@ -65,6 +72,7 @@ target_devices: - xbox-elite - mouse - keyboard + #- touchscreen # The ID of a device event mapping in the 'event_maps' folder capability_map_id: aly1 diff --git a/rootfs/usr/share/inputplumber/devices/50-steam_deck.yaml b/rootfs/usr/share/inputplumber/devices/50-steam_deck.yaml index 890d38e..bf14d53 100644 --- a/rootfs/usr/share/inputplumber/devices/50-steam_deck.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-steam_deck.yaml @@ -34,16 +34,17 @@ source_devices: product_id: 0x1205 interface_num: 2 # Touchscreen - - group: mouse - hidraw: - vendor_id: 0x2808 - product_id: 0x1015 - - group: mouse + - group: touchscreen unique: false - blocked: true - evdev: - name: "FTS3528:00 2808:1015*" - handler: event* + udev: + properties: + - name: ID_INPUT_TOUCHSCREEN + value: "1" + sys_name: "event*" + subsystem: input + config: + touchscreen: + orientation: "right" # Keyboard - group: keyboard evdev: diff --git a/rootfs/usr/share/inputplumber/schema/composite_device_v1.json b/rootfs/usr/share/inputplumber/schema/composite_device_v1.json index 5f046ad..c7b3dbe 100644 --- a/rootfs/usr/share/inputplumber/schema/composite_device_v1.json +++ b/rootfs/usr/share/inputplumber/schema/composite_device_v1.json @@ -161,6 +161,7 @@ "keyboard", "mouse", "gamepad", + "touchscreen", "imu" ] }, @@ -186,6 +187,9 @@ "iio": { "$ref": "#/definitions/IIO" }, + "config": { + "$ref": "#/definitions/SourceDeviceConfig" + }, "unique": { "description": "If false, any devices matching this description will be added to the existing composite device. Defaults to true.", "type": "boolean" @@ -196,6 +200,55 @@ ], "title": "SourceDevice" }, + "SourceDeviceConfig": { + "type": "object", + "additionalProperties": false, + "properties": { + "touchscreen": { + "$ref": "#/definitions/TouchscreenConfig" + }, + "imu": { + "$ref": "#/definitions/ImuConfig" + } + } + }, + "TouchscreenConfig": { + "type": "object", + "additionalProperties": false, + "properties": { + "orientation": { + "description": "Orientation of the touchscreen device. Defaults to normal.", + "type": "string", + "enum": [ + "normal", + "left", + "right", + "upsidedown" + ] + }, + "width": { + "description": "Width of the touchscreen in pixels. If set, any virtual touchscreens will use this width instead of querying the source device for its size.", + "type": "integer" + }, + "height": { + "description": "Height of the touchscreen in pixels. If set, any virtual touchscreens will use this height instead of querying the source device for its size.", + "type": "integer" + }, + "override_source_size": { + "description": "If true, the source device will use the width/height defined in this configuration instead of the size advertised by the device itself. Defaults to false.", + "type": "boolean" + } + } + }, + "ImuConfig": { + "type": "object", + "additionalProperties": false, + "properties": { + "mount_matrix": { + "$ref": "#/definitions/MountMatrix" + } + } + }, "Udev": { "description": "Source device to manage. Properties support globbing patterns.", "type": "object", diff --git a/src/config/mod.rs b/src/config/mod.rs index e9ad6be..8d62c1b 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -308,11 +308,41 @@ pub struct SourceDevice { pub hidraw: Option, pub iio: Option, pub udev: Option, + pub config: Option, pub unique: Option, pub blocked: Option, pub ignore: Option, } +#[derive(Debug, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct SourceDeviceConfig { + pub touchscreen: Option, + pub imu: Option, +} + +#[derive(Debug, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct TouchscreenConfig { + /// Orientation of the touchscreen. Can be one of: ["normal", "left", "right", "upsidedown"] + pub orientation: Option, + /// Width of the touchscreen. If set, any virtual touchscreens will use this width + /// instead of querying the source device for its size. + pub width: Option, + /// Height of the touchscreen. If set, any virtual touchscreens will use this height + /// instead of querying the source device for its size. + pub height: Option, + /// If true, the source device will use the width/height defined in this configuration + /// instead of the size advertised by the device itself. + pub override_source_size: Option, +} + +#[derive(Debug, Deserialize, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct ImuConfig { + pub mount_matrix: Option, +} + #[derive(Debug, Deserialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] pub struct Evdev { @@ -359,6 +389,10 @@ pub struct UdevAttribute { pub struct IIO { pub id: Option, pub name: Option, + #[deprecated( + since = "0.43.0", + note = "please use `.config.imu.mount_matrix` instead" + )] pub mount_matrix: Option, } diff --git a/src/input/composite_device/mod.rs b/src/input/composite_device/mod.rs index 315c310..7ebbdb6 100644 --- a/src/input/composite_device/mod.rs +++ b/src/input/composite_device/mod.rs @@ -1454,11 +1454,14 @@ impl CompositeDevice { let source_device = match subsystem.as_str() { "input" => { + // Get any defined config for the event device + let config = self.config.get_matching_device(&device); + log::debug!("Adding source device: {:?}", device.name()); if is_blocked { is_blocked_evdev = true; } - let device = EventDevice::new(device, self.client(), is_blocked)?; + let device = EventDevice::new(device, self.client(), config, is_blocked)?; SourceDevice::Event(device) } "hidraw" => { diff --git a/src/input/source/evdev.rs b/src/input/source/evdev.rs index 4179c52..fe48850 100644 --- a/src/input/source/evdev.rs +++ b/src/input/source/evdev.rs @@ -1,12 +1,14 @@ pub mod blocked; pub mod gamepad; +pub mod touchscreen; use std::{collections::HashMap, error::Error, time::Duration}; use evdev::{Device, EventType}; +use touchscreen::TouchscreenEventDevice; use crate::{ - constants::BUS_SOURCES_PREFIX, input::composite_device::client::CompositeDeviceClient, + config, constants::BUS_SOURCES_PREFIX, input::composite_device::client::CompositeDeviceClient, udev::device::UdevDevice, }; @@ -18,6 +20,7 @@ use super::{SourceDeviceCompatible, SourceDriver, SourceDriverOptions}; enum DriverType { Blocked, Gamepad, + Touchscreen, } /// [EventDevice] represents an input device using the input event subsystem. @@ -25,6 +28,7 @@ enum DriverType { pub enum EventDevice { Blocked(SourceDriver), Gamepad(SourceDriver), + Touchscreen(SourceDriver), } impl SourceDeviceCompatible for EventDevice { @@ -32,6 +36,7 @@ impl SourceDeviceCompatible for EventDevice { match self { EventDevice::Blocked(source_driver) => source_driver.info_ref(), EventDevice::Gamepad(source_driver) => source_driver.info_ref(), + EventDevice::Touchscreen(source_driver) => source_driver.info_ref(), } } @@ -39,6 +44,7 @@ impl SourceDeviceCompatible for EventDevice { match self { EventDevice::Blocked(source_driver) => source_driver.get_id(), EventDevice::Gamepad(source_driver) => source_driver.get_id(), + EventDevice::Touchscreen(source_driver) => source_driver.get_id(), } } @@ -46,6 +52,7 @@ impl SourceDeviceCompatible for EventDevice { match self { EventDevice::Blocked(source_driver) => source_driver.client(), EventDevice::Gamepad(source_driver) => source_driver.client(), + EventDevice::Touchscreen(source_driver) => source_driver.client(), } } @@ -53,6 +60,7 @@ impl SourceDeviceCompatible for EventDevice { match self { EventDevice::Blocked(source_driver) => source_driver.run().await, EventDevice::Gamepad(source_driver) => source_driver.run().await, + EventDevice::Touchscreen(source_driver) => source_driver.run().await, } } @@ -62,6 +70,7 @@ impl SourceDeviceCompatible for EventDevice { match self { EventDevice::Blocked(source_driver) => source_driver.get_capabilities(), EventDevice::Gamepad(source_driver) => source_driver.get_capabilities(), + EventDevice::Touchscreen(source_driver) => source_driver.get_capabilities(), } } @@ -69,6 +78,7 @@ impl SourceDeviceCompatible for EventDevice { match self { EventDevice::Blocked(source_driver) => source_driver.get_device_path(), EventDevice::Gamepad(source_driver) => source_driver.get_device_path(), + EventDevice::Touchscreen(source_driver) => source_driver.get_device_path(), } } } @@ -77,6 +87,7 @@ impl EventDevice { pub fn new( device_info: UdevDevice, composite_device: CompositeDeviceClient, + config: Option, is_blocked: bool, ) -> Result> { let driver_type = EventDevice::get_driver_type(&device_info, is_blocked); @@ -97,6 +108,12 @@ impl EventDevice { let source_device = SourceDriver::new(composite_device, device, device_info); Ok(Self::Gamepad(source_device)) } + DriverType::Touchscreen => { + let config = config.and_then(|c| c.config).and_then(|c| c.touchscreen); + let device = TouchscreenEventDevice::new(device_info.clone(), config)?; + let source_device = SourceDriver::new(composite_device, device, device_info); + Ok(Self::Touchscreen(source_device)) + } } } @@ -108,6 +125,16 @@ impl EventDevice { if is_blocked { return DriverType::Blocked; } + + let properties = device.get_properties(); + if properties.contains_key("ID_INPUT_TOUCHSCREEN") { + return DriverType::Touchscreen; + } + if properties.contains_key("ID_INPUT_JOYSTICK") { + return DriverType::Gamepad; + } + + log::debug!("Unknown input device, falling back to gamepad implementation"); DriverType::Gamepad } } diff --git a/src/input/source/evdev/touchscreen.rs b/src/input/source/evdev/touchscreen.rs new file mode 100644 index 0000000..1d62542 --- /dev/null +++ b/src/input/source/evdev/touchscreen.rs @@ -0,0 +1,351 @@ +use std::collections::HashSet; +use std::fmt::Debug; +use std::{collections::HashMap, error::Error, os::fd::AsRawFd}; + +use evdev::{ + AbsInfo, AbsoluteAxisCode, Device, EventSummary, InputEvent, KeyCode, MiscCode, + SynchronizationCode, +}; +use nix::fcntl::{FcntlArg, OFlag}; + +use crate::config::TouchscreenConfig; +use crate::input::capability::Touch; +use crate::input::event::value::InputValue; +use crate::{ + input::{ + capability::Capability, + event::native::NativeEvent, + source::{InputError, SourceInputDevice, SourceOutputDevice}, + }, + udev::device::UdevDevice, +}; + +/// Orientation of the touchscreen used to translate touch +#[derive(Debug, Clone, Copy, Default)] +enum Orientation { + #[default] + Normal, + RotateLeft, + RotateRight, + UpsideDown, +} + +impl From<&str> for Orientation { + fn from(value: &str) -> Self { + match value { + "normal" => Self::Normal, + "left" => Self::RotateLeft, + "right" => Self::RotateRight, + "upsidedown" => Self::UpsideDown, + _ => Self::Normal, + } + } +} + +/// TouchState represents the state of a single touch +#[derive(Debug, Clone)] +struct TouchState { + is_touching: bool, + pressure: f64, + x: f64, + y: f64, +} + +impl Default for TouchState { + fn default() -> Self { + Self { + is_touching: Default::default(), + pressure: 1.0, + x: Default::default(), + y: Default::default(), + } + } +} + +impl TouchState { + /// Rotates the touch input to the given orientation + fn rotate(&self, orientation: Orientation) -> Self { + let mut value = self.clone(); + let (x, y) = match orientation { + Orientation::Normal => (self.x, self.y), + Orientation::UpsideDown => (1.0 - self.x, 1.0 - self.y), + Orientation::RotateLeft => (1.0 - self.y, self.x), + Orientation::RotateRight => (self.y, 1.0 - self.x), + }; + value.x = x; + value.y = y; + + value + } + + /// Convert the touch into an InputPlumber value with the given touch index + fn to_value(&self, idx: u8) -> InputValue { + InputValue::Touch { + index: idx, + is_touching: self.is_touching, + pressure: Some(self.pressure), + x: Some(self.x), + y: Some(self.y), + } + } + + /// Convert the touch into an InputPlumber event with the given touch index + fn to_native_event(&self, idx: u8) -> NativeEvent { + NativeEvent::new(Capability::Touchscreen(Touch::Motion), self.to_value(idx)) + } +} + +/// Source device implementation for evdev touchscreens +/// https://www.kernel.org/doc/Documentation/input/multi-touch-protocol.txt +pub struct TouchscreenEventDevice { + device: Device, + orientation: Orientation, + axes_info: HashMap, + touch_state: [TouchState; 10], // NOTE: Max of 10 touch inputs + dirty_states: HashSet, + last_touch_idx: usize, +} + +impl TouchscreenEventDevice { + /// Create a new Touchscreen source device from the given udev info + pub fn new( + device_info: UdevDevice, + config: Option, + ) -> Result> { + let path = device_info.devnode(); + log::debug!("Opening device at: {}", path); + let mut device = Device::open(path.clone())?; + device.grab()?; + + // Set the device to do non-blocking reads + // TODO: use epoll to wake up when data is available + // https://github.com/emberian/evdev/blob/main/examples/evtest_nonblocking.rs + let raw_fd = device.as_raw_fd(); + nix::fcntl::fcntl(raw_fd, FcntlArg::F_SETFL(OFlag::O_NONBLOCK))?; + + // Check to see if the user wants to override the screen width/height. + let override_size = { + let override_opt = config.as_ref().and_then(|c| c.override_source_size); + override_opt.unwrap_or_default() + }; + + // Query information about the device to get the absolute ranges + let mut axes_info = HashMap::new(); + for (axis, info) in device.get_absinfo()? { + log::trace!("Found axis: {:?}", axis); + log::trace!("Found info: {:?}", info); + + // If the user isn't overriding the size or this axis doesn't contain + // size information, then use the original limits advertised by the device. + let is_size_info = axis == AbsoluteAxisCode::ABS_MT_POSITION_X + || axis == AbsoluteAxisCode::ABS_MT_POSITION_Y; + if !override_size || !is_size_info { + axes_info.insert(axis, info); + continue; + } + + // Override the axis maximum if the user has that defined. + let mut maximum = info.maximum(); + + let width = config.as_ref().and_then(|c| c.width); + if axis == AbsoluteAxisCode::ABS_MT_POSITION_X && width.is_some() { + maximum = width.unwrap_or_default() as i32; + } + + let height = config.as_ref().and_then(|c| c.height); + if axis == AbsoluteAxisCode::ABS_MT_POSITION_Y && height.is_some() { + maximum = height.unwrap_or_default() as i32; + } + + let modified_info = AbsInfo::new( + info.value(), + info.minimum(), + maximum, + info.fuzz(), + info.flat(), + info.resolution(), + ); + axes_info.insert(axis, modified_info); + } + + // Configure the orientation of the touchscreen + let orientation = + if let Some(orientation) = config.as_ref().and_then(|c| c.orientation.as_ref()) { + Orientation::from(orientation.as_str()) + } else { + Orientation::default() + }; + log::debug!("Configured touchscreen orientation: {orientation:?}"); + + Ok(Self { + device, + orientation, + axes_info, + touch_state: Default::default(), + dirty_states: HashSet::with_capacity(10), + last_touch_idx: 0, + }) + } + + /// Translate the given evdev event into a native event + fn translate(&mut self, event: InputEvent) -> Vec { + log::trace!("Received event: {:?}", event); + + // Update internal touch state until a synchronization event occurs. Each + // event that comes in will update the touch state and update 'dirty_states' + // to indicate that events should be sent for that touch index when a + // SYN_REPORT event occurs. + match event.destructure() { + // Synchronization events indicate that touch events can be emitted + EventSummary::Synchronization(_, SynchronizationCode::SYN_REPORT, _) => { + let mut events = Vec::with_capacity(self.dirty_states.len()); + + // Send events for any dirty touch states + for idx in self.dirty_states.drain() { + let Some(touch) = self.touch_state.get_mut(idx) else { + continue; + }; + + // Rotate values based on config + let rotated_touch = touch.rotate(self.orientation); + let event = rotated_touch.to_native_event(idx as u8); + events.push(event); + } + + return events; + } + // The BTN_TOUCH event occurs whenever touches have started or stopped. + // This can be used to reset the last touch index when no touches are + // detected. + EventSummary::Key(_, KeyCode::BTN_TOUCH, value) => { + if value == 0 { + // Reset last touch index when all touches stop + self.last_touch_idx = 0; + } else { + self.dirty_states.insert(0); + } + } + // The ABS_MT_SLOT event defines the index of the touch. E.g. "0" would + // be the first touch, "1", the second, etc. Upon receiving this event, + // any following ABS_X/Y events are associated with this touch index. + EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_MT_SLOT, value) => { + // Select the current slot to update + let slot = value as usize; + self.last_touch_idx = slot; + self.dirty_states.insert(slot); + } + // Whenever a touch is lifted, an ABS_MT_TRACKING_ID event with a value of + // -1 event will occur. + EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_MT_TRACKING_ID, -1) => { + if let Some(touch) = self.touch_state.get_mut(self.last_touch_idx) { + touch.is_touching = false; + self.dirty_states.insert(self.last_touch_idx); + } + } + // Emitted whenever touch motion is detected for the X axis + EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_MT_POSITION_X, value) => { + // Get the axis information so the value can be normalized + let Some(info) = self.axes_info.get(&AbsoluteAxisCode::ABS_MT_POSITION_X) else { + return vec![]; + }; + let normal_value = normalize_unsigned_value(value, info.maximum()); + + // Select the current slot to update + if let Some(touch) = self.touch_state.get_mut(self.last_touch_idx) { + touch.is_touching = true; + touch.x = normal_value; + self.dirty_states.insert(self.last_touch_idx); + } + } + // Emitted whenever touch motion is detected for the Y axis + EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_MT_POSITION_Y, value) => { + // Get the axis information so the value can be normalized + let Some(info) = self.axes_info.get(&AbsoluteAxisCode::ABS_MT_POSITION_Y) else { + return vec![]; + }; + let normal_value = normalize_unsigned_value(value, info.maximum()); + + // Select the current slot to update + if let Some(touch) = self.touch_state.get_mut(self.last_touch_idx) { + touch.is_touching = true; + touch.y = normal_value; + self.dirty_states.insert(self.last_touch_idx); + } + } + // Some touchscreens support touch pressure and emit this event. + EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_PRESSURE, value) => { + // Get the axis information so the value can be normalized + let Some(info) = self.axes_info.get(&AbsoluteAxisCode::ABS_PRESSURE) else { + return vec![]; + }; + let normal_value = normalize_unsigned_value(value, info.maximum()); + + // Select the current slot to update + if let Some(touch) = self.touch_state.get_mut(self.last_touch_idx) { + touch.pressure = normal_value; + self.dirty_states.insert(self.last_touch_idx); + } + } + EventSummary::Misc(_, MiscCode::MSC_TIMESTAMP, _) => (), + _ => (), + } + + vec![] + } +} + +impl SourceInputDevice for TouchscreenEventDevice { + /// Poll the given input device for input events + fn poll(&mut self) -> Result, InputError> { + // Read events from the device + let events = { + let result = self.device.fetch_events(); + let events = match result { + Ok(events) => events, + Err(err) => match err.kind() { + // Do nothing if this would block + std::io::ErrorKind::WouldBlock => return Ok(vec![]), + _ => { + log::trace!("Failed to fetch events: {:?}", err); + let msg = format!("Failed to fetch events: {:?}", err); + return Err(msg.into()); + } + }, + }; + + let events: Vec = events.into_iter().collect(); + events + }; + + // Convert the events into native events + let native_events = events + .into_iter() + .map(|e| self.translate(e)) + .filter(|events| !events.is_empty()) + .flatten() + .collect(); + + Ok(native_events) + } + + /// Returns the possible input events this device is capable of emitting + fn get_capabilities(&self) -> Result, InputError> { + Ok(vec![Capability::Touchscreen(Touch::Motion)]) + } +} + +impl SourceOutputDevice for TouchscreenEventDevice {} + +impl Debug for TouchscreenEventDevice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TouchscreenEventDevice") + .field("axes_info", &self.axes_info) + .finish() + } +} + +// Returns a value between 0.0 and 1.0 based on the given value with its +// maximum. +fn normalize_unsigned_value(raw_value: i32, max: i32) -> f64 { + raw_value as f64 / max as f64 +} diff --git a/src/input/target/touchscreen.rs b/src/input/target/touchscreen.rs index 89d1d75..f3fc94a 100644 --- a/src/input/target/touchscreen.rs +++ b/src/input/target/touchscreen.rs @@ -1,4 +1,4 @@ -use std::{error::Error, os::fd::AsRawFd}; +use std::{error::Error, os::fd::AsRawFd, time::Duration}; use evdev::{ uinput::{VirtualDevice, VirtualDeviceBuilder}, @@ -6,12 +6,16 @@ use evdev::{ MiscCode, PropType, UinputAbsSetup, }; use nix::fcntl::{FcntlArg, OFlag}; - -use crate::input::{ - capability::{Capability, Touch}, - composite_device::client::CompositeDeviceClient, - event::{native::NativeEvent, value::InputValue}, - output_event::OutputEvent, +use tokio::sync::mpsc::{channel, Receiver}; + +use crate::{ + input::{ + capability::{Capability, Touch}, + composite_device::client::CompositeDeviceClient, + event::{native::NativeEvent, value::InputValue}, + output_event::OutputEvent, + }, + udev::device::UdevDevice, }; use super::{InputError, OutputError, TargetInputDevice, TargetOutputDevice}; @@ -20,16 +24,25 @@ use super::{InputError, OutputError, TargetInputDevice, TargetOutputDevice}; /// on whether the screen is rotated. #[derive(Debug, Clone, Default)] pub enum TouchscreenOrientation { - #[allow(dead_code)] + #[default] Normal, - #[allow(dead_code)] UpsideDown, - #[default] RotateLeft, - #[allow(dead_code)] RotateRight, } +impl From<&str> for TouchscreenOrientation { + fn from(value: &str) -> Self { + match value { + "normal" => Self::Normal, + "left" => Self::RotateLeft, + "right" => Self::RotateRight, + "upsidedown" => Self::UpsideDown, + _ => Self::Normal, + } + } +} + /// Configuration of the target touchscreen device. #[derive(Debug, Clone)] pub struct TouchscreenConfig { @@ -70,7 +83,8 @@ pub struct TouchEvent { #[derive(Debug)] pub struct TouchscreenDevice { config: TouchscreenConfig, - device: VirtualDevice, + config_rx: Option>, + device: Option, is_touching: bool, should_set_timestamp: bool, timestamp: i32, @@ -86,10 +100,10 @@ impl TouchscreenDevice { /// Create a new emulated touchscreen device with the given configuration. pub fn new_with_config(config: TouchscreenConfig) -> Result> { - let device = TouchscreenDevice::create_virtual_device(&config)?; Ok(Self { config, - device, + config_rx: None, + device: None, is_touching: false, should_set_timestamp: true, timestamp: 0, @@ -351,10 +365,175 @@ impl TouchscreenDevice { } impl TargetInputDevice for TouchscreenDevice { + /// Start the driver when attached to a composite device. + fn on_composite_device_attached( + &mut self, + composite_device: CompositeDeviceClient, + ) -> Result<(), InputError> { + let (tx, rx) = channel(1); + let mut device_config = self.config.clone(); + + // Spawn a task to wait for the composite device config. This is done + // to prevent potential deadlocks if the composite device and target + // device are both waiting for a response from each other. + tokio::task::spawn(async move { + // Wait to ensure the composite device has grabbed all sources + // NOTE: We should look at other ways of signalling to target devices + // that a new source device has been added. + tokio::time::sleep(Duration::from_millis(500)).await; + + // Get the source devices attached to the composite device. + let paths = match composite_device.get_source_device_paths().await { + Ok(paths) => paths, + Err(e) => { + log::error!("Failed to get source devices from composite device: {e:?}"); + return; + } + }; + log::debug!("Found source devices: {paths:?}"); + + // Check all the source devices attached to the composite device and + // check to see if any of them are touchscreen devices. + let mut info_x = None; + let mut info_y = None; + for path in paths { + let device = UdevDevice::from_devnode_path(path.as_str()); + let is_touchscreen = device + .get_property_from_tree("ID_INPUT_TOUCHSCREEN") + .is_some(); + if !is_touchscreen { + continue; + } + + // If the device is a touchscreen, open it to query the touchscreen + // resolution. + log::debug!("Opening source touchscreen device {path} to query dimensions"); + let device = match evdev::Device::open(path.clone()) { + Ok(dev) => dev, + Err(e) => { + log::warn!( + "Unable to open device {path} to check touchscreen settings: {e:?}" + ); + continue; + } + }; + + // Query the ABS info to get the touchscreen resolution. + log::debug!("Querying touchscreen device {path} for dimensions"); + let abs_info = match device.get_absinfo() { + Ok(info) => info, + Err(e) => { + log::warn!("Unable to get ABS info for device {path}: {e:?}"); + continue; + } + }; + for (code, info) in abs_info { + if code.0 == AbsoluteAxisCode::ABS_MT_POSITION_X.0 { + info_x = Some(info); + } + if code.0 == AbsoluteAxisCode::ABS_MT_POSITION_Y.0 { + info_y = Some(info); + } + } + } + + // Update the configuration for the target touchscreen based on the detected + // dimensions of the source device. + if let Some(info) = info_x.as_ref() { + log::debug!("Detected source X axis info: {info:?}"); + device_config.width = info.maximum() as u16; + } + if let Some(info) = info_y.as_ref() { + log::debug!("Detected source Y axis info: {info:?}"); + device_config.height = info.maximum() as u16; + } + let detected_from_source = info_x.is_some() || info_y.is_some(); + + // Get the configuration from the composite device + log::debug!("Querying Composite Device for configuration"); + let composite_config = match composite_device.get_config().await { + Ok(config) => config, + Err(e) => { + log::error!("Failed to get config from composite device: {e:?}"); + return; + } + }; + + // Check to see if the composite device configuration has a touchscreen + let mut screen_config = None; + for src in composite_config.source_devices.into_iter() { + let Some(src_config) = src.config else { + continue; + }; + + let Some(touch_config) = src_config.touchscreen else { + continue; + }; + + screen_config = Some(touch_config); + break; + } + + // Build the config to use for the virtual touchscreen device based on + // the touchscreen configuration from the composite device. + if let Some(screen_config) = screen_config { + if let Some(orientation) = screen_config.orientation { + // Set the target screen orientation based on the source screen. + // If the display is rotated, the target display must be rotated + // in the opposite direction. + let orientation = TouchscreenOrientation::from(orientation.as_str()); + let new_orientation = match orientation { + TouchscreenOrientation::Normal => TouchscreenOrientation::Normal, + TouchscreenOrientation::UpsideDown => TouchscreenOrientation::UpsideDown, + TouchscreenOrientation::RotateLeft => TouchscreenOrientation::RotateRight, + TouchscreenOrientation::RotateRight => TouchscreenOrientation::RotateLeft, + }; + device_config.orientation = new_orientation; + + // If the dimensions of the screen were detected from a source device, + // flip the values based on orientation. + if detected_from_source { + let width = device_config.width; + let height = device_config.height; + match device_config.orientation { + TouchscreenOrientation::RotateLeft + | TouchscreenOrientation::RotateRight => { + device_config.width = height; + device_config.height = width; + } + _ => (), + } + } + } + if let Some(width) = screen_config.width { + device_config.width = width as u16; + } + if let Some(height) = screen_config.height { + device_config.height = height as u16; + } + } + + log::debug!("Sending touchscreen configuration to target device"); + if let Err(e) = tx.send(device_config).await { + log::error!("Failed to send touchscreen config: {e:?}"); + } + }); + + // Save the receiver to wait for the touchscreen config. + self.config_rx = Some(rx); + + Ok(()) + } + fn write_event(&mut self, event: NativeEvent) -> Result<(), InputError> { log::trace!("Received event: {event:?}"); let evdev_events = self.translate_event(event); - self.device.emit(evdev_events.as_slice())?; + + let Some(device) = self.device.as_mut() else { + log::trace!("Touchscreen was never started"); + return Ok(()); + }; + device.emit(evdev_events.as_slice())?; Ok(()) } @@ -368,6 +547,28 @@ impl TargetOutputDevice for TouchscreenDevice { // Check to see if MSC_TIMESTAMP events should be sent. Timestamp events // should be sent continuously during active touches. fn poll(&mut self, _: &Option) -> Result, OutputError> { + // Create and start the device if needed + if let Some(rx) = self.config_rx.as_mut() { + if rx.is_empty() { + // If the queue is empty, we're still waiting for a response from + // the composite device. + return Ok(vec![]); + } + let config = match rx.blocking_recv() { + Some(config) => config, + None => self.config.clone(), + }; + + let device = TouchscreenDevice::create_virtual_device(&config)?; + self.device = Some(device); + self.config = config; + } + + let Some(device) = self.device.as_mut() else { + log::trace!("Touchscreen not started"); + return Ok(vec![]); + }; + // Send timestamp events whenever a touch is active let touching = self.is_touching; let set_timestamp = self.should_set_timestamp; @@ -377,7 +578,7 @@ impl TargetOutputDevice for TouchscreenDevice { if set_timestamp { let value = self.timestamp; let event = InputEvent::new(EventType::MISC.0, MiscCode::MSC_TIMESTAMP.0, value); - self.device.emit(&[event])?; + device.emit(&[event])?; self.timestamp = self.timestamp.wrapping_add(10000); } else { self.should_set_timestamp = true; diff --git a/src/udev/device.rs b/src/udev/device.rs index f1718c9..3ef834b 100644 --- a/src/udev/device.rs +++ b/src/udev/device.rs @@ -3,7 +3,7 @@ use std::{ error::Error, ffi::OsStr, fs::{self, read_link}, - path::Path, + path::{Path, PathBuf}, }; pub trait AttributeGetter { @@ -28,7 +28,10 @@ pub trait AttributeGetter { fn uniq(&self) -> String; fn get_attributes(&self) -> HashMap; /// Returns the value of the given property from the device + #[allow(dead_code)] fn get_property(&self, property: &str) -> Option; + /// Gets a property from the first device in the device tree to match the property. + fn get_property_from_tree(&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 @@ -287,6 +290,16 @@ impl AttributeGetter for ::udev::Device { .map(|v| v.to_string_lossy().to_string()) } + /// Gets a property from the first device in the device tree to match the property. + fn get_property_from_tree(&self, property: &str) -> Option { + if let Some(prop) = self.property_value(property) { + return Some(prop.to_string_lossy().to_string()); + } + + let parent = self.parent()?; + parent.get_property_from_tree(property) + } + /// Returns device properties for the device. E.g. {"ID_INPUT": "1", ...} fn get_properties(&self) -> HashMap { let mut properties = HashMap::new(); @@ -372,10 +385,17 @@ impl UdevDevice { .unwrap_or_default() .to_string(); + // Try to look up the syspath of the device + let result = ::udev::Device::from_subsystem_sysname(subsystem.clone(), name.to_string()); + let syspath = match result { + Ok(device) => device.syspath().to_string_lossy().to_string(), + Err(_) => "".to_string(), + }; + Self { devnode, subsystem, - syspath: "".to_string(), + syspath, sysname: name.to_string(), name: None, vendor_id: None, @@ -384,6 +404,23 @@ impl UdevDevice { } } + /// Returns a UdevDevice object from the given path. + /// e.g. UdevDevice::from_devnode_path("/dev/hidraw0"); + pub fn from_devnode_path(path: &str) -> Self { + // Break up the dev node name and base path + let path_buf = PathBuf::from(path); + let name = path_buf + .file_name() + .and_then(|p| p.to_str()) + .unwrap_or_default(); + let base_path = path_buf + .parent() + .and_then(|p| p.to_str()) + .unwrap_or_default(); + + UdevDevice::from_devnode(base_path, name) + } + /// Returns a udev::Device from the stored syspath. pub fn get_device(&self) -> Result<::udev::Device, Box> { match ::udev::Device::from_syspath(Path::new(self.syspath.as_str())) { @@ -583,6 +620,15 @@ impl UdevDevice { device.get_property(property) } + /// Returns the value of the given property from the first device in the + /// device tree. + pub fn get_property_from_tree(&self, property: &str) -> Option { + let Ok(device) = self.get_device() else { + return None; + }; + device.get_property_from_tree(property) + } + /// Returns device properties for the device. E.g. {"ID_INPUT": "1", ...} pub fn get_properties(&self) -> HashMap { let Ok(device) = self.get_device() else {