From d47180a9d3f6895bb99dac6da865f2c00d1b1fa9 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-legion_go.yaml | 14 + .../inputplumber/devices/50-steam_deck.yaml | 12 +- .../schema/composite_device_v1.json | 49 ++ src/config/mod.rs | 26 + src/input/composite_device/mod.rs | 5 +- src/input/source/evdev.rs | 29 +- src/input/source/evdev/touchscreen.rs | 457 ++++++++++++++++++ src/input/target/touchscreen.rs | 126 ++++- 8 files changed, 701 insertions(+), 17 deletions(-) create mode 100644 src/input/source/evdev/touchscreen.rs diff --git a/rootfs/usr/share/inputplumber/devices/50-legion_go.yaml b/rootfs/usr/share/inputplumber/devices/50-legion_go.yaml index 6a9fee72..5ad13d0c 100644 --- a/rootfs/usr/share/inputplumber/devices/50-legion_go.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-legion_go.yaml @@ -138,6 +138,19 @@ source_devices: y: [-1, 0, 0] z: [0, 0, -1] + # Touchscreen + #- group: touchscreen + # evdev: + # name: "NVTK0603:00 0603:F001" + # vendor_id: "*603" + # product_id: "f001" + # handler: event* + # config: + # touchscreen: + # orientation: "left" + # width: 2560 + # height: 1600 + # Block all evdev devices; mouse, touchpad, gamepad, keyboard - group: gamepad blocked: true @@ -160,3 +173,4 @@ target_devices: - mouse - keyboard - touchpad + - touchscreen diff --git a/rootfs/usr/share/inputplumber/devices/50-steam_deck.yaml b/rootfs/usr/share/inputplumber/devices/50-steam_deck.yaml index 890d38e4..012a4813 100644 --- a/rootfs/usr/share/inputplumber/devices/50-steam_deck.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-steam_deck.yaml @@ -34,16 +34,16 @@ 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* + config: + touchscreen: + orientation: "right" + width: 1280 + height: 800 # 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 5f046adc..dbcdb6d9 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,51 @@ ], "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.", + "type": "integer" + }, + "height": { + "description": "Height of the touchscreen in pixels.", + "type": "integer" + } + } + }, + "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 e9ad6bee..f7169c56 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -308,11 +308,36 @@ 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 + pub width: Option, + /// Height of the touchscreen + pub height: 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 +384,7 @@ pub struct UdevAttribute { pub struct IIO { pub id: Option, pub name: Option, + // DEPRECATED: 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 d7160264..1e75ae06 100644 --- a/src/input/composite_device/mod.rs +++ b/src/input/composite_device/mod.rs @@ -1404,11 +1404,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 4179c529..fe48850b 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 00000000..26f641ac --- /dev/null +++ b/src/input/source/evdev/touchscreen.rs @@ -0,0 +1,457 @@ +use std::collections::HashSet; +use std::fmt::Debug; +use std::{collections::HashMap, error::Error, os::fd::AsRawFd}; + +use evdev::{ + AbsInfo, AbsoluteAxisCode, Device, EventSummary, EventType, 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, Gamepad, GamepadAxis, GamepadButton}, + event::{evdev::EvdevEvent, 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 + } + + 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), + } + } + + 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], + 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))?; + + // 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); + axes_info.insert(axis, 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, + }) + } + + /* + Input device ID: bus 0x18 vendor 0x603 product 0xf001 version 0x100 + Input device name: "NVTK0603:00 0603:F001" + Supported events: + Event type 0 (EV_SYN) + Event type 1 (EV_KEY) + Event code 330 (BTN_TOUCH) + Event type 3 (EV_ABS) + Event code 0 (ABS_X) + Value 1491 + Min 0 + Max 1600 + Resolution 14 + Event code 1 (ABS_Y) + Value 2275 + Min 0 + Max 2560 + Resolution 14 + Event code 24 (ABS_PRESSURE) + Value 0 + Min 0 + Max 255 + Event code 47 (ABS_MT_SLOT) + Value 0 + Min 0 + Max 9 + Event code 48 (ABS_MT_TOUCH_MAJOR) + Value 0 + Min 0 + Max 255 + Resolution 1 + Event code 49 (ABS_MT_TOUCH_MINOR) + Value 0 + Min 0 + Max 255 + Resolution 1 + Event code 52 (ABS_MT_ORIENTATION) + Value 0 + Min 0 + Max 1 + Event code 53 (ABS_MT_POSITION_X) + Value 0 + Min 0 + Max 1600 + Resolution 14 + Event code 54 (ABS_MT_POSITION_Y) + Value 0 + Min 0 + Max 2560 + Resolution 14 + Event code 57 (ABS_MT_TRACKING_ID) + Value 0 + Min 0 + Max 65535 + Event code 58 (ABS_MT_PRESSURE) + Value 0 + Min 0 + Max 255 + Event type 4 (EV_MSC) + Event code 5 (MSC_TIMESTAMP) + Properties: + Property type 1 (INPUT_PROP_DIRECT) + */ + + /* + Event: time 1737575066.505726, type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 613 + Event: time 1737575066.505726, type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 589 + Event: time 1737575066.505726, type 3 (EV_ABS), code 48 (ABS_MT_TOUCH_MAJOR), value 4 + Event: time 1737575066.505726, type 3 (EV_ABS), code 47 (ABS_MT_SLOT), value 1 + Event: time 1737575066.505726, type 3 (EV_ABS), code 57 (ABS_MT_TRACKING_ID), value 8 + Event: time 1737575066.505726, type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 872 + Event: time 1737575066.505726, type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 831 + Event: time 1737575066.505726, type 3 (EV_ABS), code 58 (ABS_MT_PRESSURE), value 10 + Event: time 1737575066.505726, type 3 (EV_ABS), code 48 (ABS_MT_TOUCH_MAJOR), value 3 + Event: time 1737575066.505726, type 3 (EV_ABS), code 49 (ABS_MT_TOUCH_MINOR), value 3 + Event: time 1737575066.505726, type 3 (EV_ABS), code 0 (ABS_X), value 613 + Event: time 1737575066.505726, type 3 (EV_ABS), code 1 (ABS_Y), value 589 + Event: time 1737575066.505726, type 4 (EV_MSC), code 5 (MSC_TIMESTAMP), value 231300 + Event: time 1737575066.505726, -------------- SYN_REPORT ------------ + Event: time 1737575066.512719, type 3 (EV_ABS), code 47 (ABS_MT_SLOT), value 0 + Event: time 1737575066.512719, type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 614 + Event: time 1737575066.512719, type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 590 + Event: time 1737575066.512719, type 3 (EV_ABS), code 47 (ABS_MT_SLOT), value 1 + Event: time 1737575066.512719, type 3 (EV_ABS), code 58 (ABS_MT_PRESSURE), value 69 + Event: time 1737575066.512719, type 3 (EV_ABS), code 0 (ABS_X), value 614 + Event: time 1737575066.512719, type 3 (EV_ABS), code 1 (ABS_Y), value 590 + Event: time 1737575066.512719, type 4 (EV_MSC), code 5 (MSC_TIMESTAMP), value 238200 + Event: time 1737575066.512719, -------------- SYN_REPORT ------------ + */ + + /// 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 + match event.destructure() { + 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; + } + 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); + } + } + 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); + } + EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_MT_TRACKING_ID, -1) => { + // Touch was lifted if MT_TRACKING_ID is -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); + } + } + 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); + } + } + 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); + } + } + 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> { + let mut capabilities = vec![]; + + // Loop through all support events + let events = self.device.supported_events(); + for event in events.iter() { + match event { + EventType::SYNCHRONIZATION => { + capabilities.push(Capability::Sync); + } + EventType::KEY => { + let Some(keys) = self.device.supported_keys() else { + continue; + }; + for key in keys.iter() { + let input_event = InputEvent::new(event.0, key.0, 0); + let evdev_event = EvdevEvent::from(input_event); + let cap = evdev_event.as_capability(); + capabilities.push(cap); + } + } + EventType::RELATIVE => { + let Some(rel) = self.device.supported_relative_axes() else { + continue; + }; + for axis in rel.iter() { + let input_event = InputEvent::new(event.0, axis.0, 0); + let evdev_event = EvdevEvent::from(input_event); + let cap = evdev_event.as_capability(); + capabilities.push(cap); + } + } + EventType::ABSOLUTE => { + let Some(abs) = self.device.supported_absolute_axes() else { + continue; + }; + for axis in abs.iter() { + let input_event = InputEvent::new(event.0, axis.0, 0); + let evdev_event = EvdevEvent::from(input_event); + let cap = evdev_event.as_capability(); + if cap == Capability::Gamepad(Gamepad::Axis(GamepadAxis::Hat0)) { + capabilities + .push(Capability::Gamepad(Gamepad::Button(GamepadButton::DPadUp))); + capabilities.push(Capability::Gamepad(Gamepad::Button( + GamepadButton::DPadDown, + ))); + capabilities.push(Capability::Gamepad(Gamepad::Button( + GamepadButton::DPadLeft, + ))); + capabilities.push(Capability::Gamepad(Gamepad::Button( + GamepadButton::DPadRight, + ))); + continue; + } + capabilities.push(cap); + } + } + EventType::MISC => (), + EventType::SWITCH => (), + EventType::LED => (), + EventType::SOUND => (), + EventType::REPEAT => (), + EventType::FORCEFEEDBACK => (), + EventType::POWER => (), + EventType::FORCEFEEDBACKSTATUS => (), + EventType::UINPUT => (), + _ => (), + } + } + + Ok(capabilities) + } +} + +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 89d1d756..9fdd2f57 100644 --- a/src/input/target/touchscreen.rs +++ b/src/input/target/touchscreen.rs @@ -20,16 +20,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 +79,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 +96,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 +361,86 @@ 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) = tokio::sync::mpsc::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 { + // Get the configuration from the composite device + 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 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; + } + } + + 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 +454,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 +485,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;