diff --git a/Cargo.lock b/Cargo.lock index 33171951..b94ac211 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -229,6 +229,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -243,9 +252,9 @@ checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" [[package]] name = "bytes" -version = "1.6.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] name = "cassowary" @@ -430,6 +439,15 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -470,6 +488,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "darling" version = "0.20.10" @@ -505,6 +533,12 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "data-encoding" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" + [[package]] name = "deranged" version = "0.3.11" @@ -520,6 +554,16 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "either" version = "1.12.0" @@ -761,6 +805,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -859,6 +913,23 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" + [[package]] name = "humantime" version = "2.1.0" @@ -929,7 +1000,7 @@ dependencies = [ "clap 3.2.25", "libiio-sys", "nix 0.29.0", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -977,8 +1048,9 @@ dependencies = [ "serde", "serde_yaml", "tabled", - "thiserror", + "thiserror 1.0.69", "tokio", + "tokio-tungstenite", "udev", "uhid-virt", "virtual-usb", @@ -1636,6 +1708,17 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1848,7 +1931,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] @@ -1862,6 +1954,17 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "time" version = "0.3.36" @@ -1925,6 +2028,18 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "toml_datetime" version = "0.6.8" @@ -1973,6 +2088,29 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror 2.0.11", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + [[package]] name = "udev" version = "0.9.3" @@ -2058,6 +2196,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8parse" version = "0.2.1" @@ -2073,6 +2217,12 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "virtual-usb" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index c268c90a..684b4730 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ serde_yaml = "0.9.34" tabled = { version = "0.18.0", features = ["ansi"] } thiserror = "1.0.69" tokio = { version = "1.43.0", features = ["full"] } +tokio-tungstenite = { version = "0.26.2" } udev = { version = "0.9.3", features = ["mio"] } uhid-virt = "0.0.8" virtual-usb = { git = "https://github.com/ShadowBlip/virtual-usb-rs.git", rev = "4bca5c6fb9f2b63944a286854405e3e7e0b5d259" } @@ -59,6 +60,9 @@ xdg = "2.5.2" zbus = { version = "5.5.0", default-features = false, features = ["tokio"] } zbus_macros = "5.5.0" +#[features] +#networking = ["dep:tokio-tungstenite"] + [profile.release] debug = false strip = true diff --git a/README.md b/README.md index 13065b04..b211e5f1 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ and translate their input to a variety of virtual device formats. - [x] Emulate mouse, keyboard, and gamepad inputs - [x] Intercept and route input over DBus for overlay interface control - [x] Input mapping profiles to translate source input into the desired target input -- [ ] Route input over the network +- [x] Route input over the network ## Install diff --git a/rootfs/usr/share/inputplumber/devices/80-websockets.yaml b/rootfs/usr/share/inputplumber/devices/80-websockets.yaml new file mode 100644 index 00000000..9662f6a0 --- /dev/null +++ b/rootfs/usr/share/inputplumber/devices/80-websockets.yaml @@ -0,0 +1,36 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/ShadowBlip/InputPlumber/main/rootfs/usr/share/inputplumber/schema/composite_device_v1.json +# Schema version number +version: 1 + +# The type of configuration schema +kind: CompositeDevice + +# Name of the composite device mapping +name: Websockets + +# Maximum number of source devices per CompositeDevice. +maximum_sources: 1 + +# Only use this profile if *any* of the given matches matches. If this list is +# empty, then the source devices will *always* be checked. +# /sys/class/dmi/id/product_name +matches: [] + +# One or more source devices to combine into a single virtual device. The events +# from these devices will be watched and translated according to the key map. +source_devices: + - group: gamepad + websocket: + address: "0.0.0.0" + port: 12907 + +# Optional configuration for the composite device +options: + # If true, InputPlumber will automatically try to manage the input device. If + # this is false, InputPlumber will not try to manage the device unless an + # external service enables management of the device. Defaults to 'false' + auto_manage: true + +# The target input device(s) to emulate by default +target_devices: + - xbox-elite diff --git a/src/cli/device.rs b/src/cli/device.rs index 8c0dc1d8..4e4f4686 100644 --- a/src/cli/device.rs +++ b/src/cli/device.rs @@ -11,6 +11,7 @@ use zbus::Connection; use crate::cli::get_managed_objects; use crate::dbus::interface::composite_device::CompositeDeviceInterfaceProxy; use crate::dbus::interface::manager::ManagerInterfaceProxy; +use crate::dbus::interface::target::websocket::TargetWebsocketInterfaceProxy; use crate::dbus::interface::target::TargetInterfaceProxy; use crate::input::target::TargetDeviceTypeId; @@ -42,6 +43,11 @@ pub enum DeviceCommand { }, /// Test input menu Test, + /// Connect this input device to a remote instance of InputPlumber + Connect { + /// URL of the remote instance of InputPlumber (e.g. "ws://192.168.0.100:12901") + url: String, + }, } #[derive(Subcommand, Debug, Clone)] @@ -286,6 +292,20 @@ pub async fn handle_device( return Err(e.to_string().into()); } } + DeviceCommand::Connect { url } => { + device + .set_target_devices(vec!["websocket".to_string()]) + .await?; + let target_devices = device.target_devices().await?; + for path in target_devices { + let websocket = TargetWebsocketInterfaceProxy::builder(&conn) + .path(path)? + .build() + .await?; + websocket.connect(url.clone()).await?; + println!("Successfully connected to: {url}"); + } + } } Ok(()) diff --git a/src/config/mod.rs b/src/config/mod.rs index 04620e82..1cdbea9e 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -10,7 +10,11 @@ use thiserror::Error; use crate::{ dmi::data::DMIData, - input::event::{native::NativeEvent, value::InputValue}, + input::{ + event::{native::NativeEvent, value::InputValue}, + info::DeviceInfo, + }, + network::websocket::WebsocketClient, udev::device::UdevDevice, }; @@ -316,6 +320,8 @@ pub struct SourceDevice { pub led: Option, /// Devices that match the given udev properties will be captured by InputPlumber pub udev: Option, + /// Websocket clients that match the given websocket settings will be captured by InputPlumber + pub websocket: Option, /// Device configuration options are used to alter how the source device is managed pub config: Option, /// If false, any devices matching this description will be added to the @@ -450,6 +456,17 @@ pub struct MountMatrix { pub z: [f64; 3], } +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +#[allow(clippy::upper_case_acronyms)] +pub struct Websocket { + pub port: Option, + pub address: Option, + pub tls: Option, + pub client_port: Option, + pub client_address: Option, +} + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(rename_all = "snake_case")] pub struct EventsConfig { @@ -505,59 +522,120 @@ impl CompositeDeviceConfig { .collect() } - /// Returns a [SourceDevice] if it matches the given [UdevDevice]. Will return + /// Returns a [SourceDevice] if it matches the given [DeviceInfo]. Will return /// the first [SourceDevice] match found if multiple matches exist. - pub fn get_matching_device(&self, udevice: &UdevDevice) -> Option { + pub fn get_matching_device(&self, device: &DeviceInfo) -> Option { for config in self.source_devices.iter() { - // Check udev matches first - if let Some(udev_config) = config.udev.as_ref() { - if self.has_matching_udev(udevice, udev_config) { - return Some(config.clone()); - } + let matched_config = match device { + DeviceInfo::Udev(udevice) => self.get_matching_udev_device(config, udevice), + DeviceInfo::Websocket(client) => self.get_matching_websocket_device(config, client), + }; + if matched_config.is_some() { + return matched_config; } + } - // Use subsystem-specific device matching - let subsystem = udevice.subsystem(); - match subsystem.as_str() { - "input" => { - let Some(evdev_config) = config.evdev.as_ref() else { - continue; - }; - if self.has_matching_evdev(udevice, evdev_config) { - return Some(config.clone()); - } + None + } + + /// Returns a copy of the given [SourceDevice] config if it matches the given + /// [UdevDevice]. + fn get_matching_udev_device( + &self, + config: &SourceDevice, + udevice: &UdevDevice, + ) -> Option { + // Check udev matches first + if let Some(udev_config) = config.udev.as_ref() { + if self.has_matching_udev(udevice, udev_config) { + return Some(config.clone()); + } + } + + // Use subsystem-specific device matching + let subsystem = udevice.subsystem(); + match subsystem.as_str() { + "input" => { + let evdev_config = config.evdev.as_ref()?; + if self.has_matching_evdev(udevice, evdev_config) { + return Some(config.clone()); } - "hidraw" => { - let Some(hidraw_config) = config.hidraw.as_ref() else { - continue; - }; - if self.has_matching_hidraw(udevice, hidraw_config) { - return Some(config.clone()); - } + } + "hidraw" => { + let hidraw_config = config.hidraw.as_ref()?; + if self.has_matching_hidraw(udevice, hidraw_config) { + return Some(config.clone()); } - "iio" => { - let Some(iio_config) = config.iio.as_ref() else { - continue; - }; - if self.has_matching_iio(udevice, iio_config) { - return Some(config.clone()); - } + } + "iio" => { + let iio_config = config.iio.as_ref()?; + if self.has_matching_iio(udevice, iio_config) { + return Some(config.clone()); } - "leds" => { - let Some(led_config) = config.led.as_ref() else { - continue; - }; - if self.has_matching_led(udevice, led_config) { - return Some(config.clone()); - } + } + "leds" => { + let led_config = config.led.as_ref()?; + if self.has_matching_led(udevice, led_config) { + return Some(config.clone()); } - _ => (), } + _ => (), } None } + /// Returns a copy of the given [SourceDevice] config if it matches the given + /// [WebsocketClient]. + fn get_matching_websocket_device( + &self, + config: &SourceDevice, + client: &WebsocketClient, + ) -> Option { + let websocket_config = config.websocket.as_ref()?; + + if self.has_matching_websocket(client, websocket_config) { + return Some(config.clone()); + } + + None + } + + /// Returns true if the given websocket client matches the given config + pub fn has_matching_websocket( + &self, + client: &WebsocketClient, + websocket_config: &Websocket, + ) -> bool { + log::debug!("Checking websocket config: '{websocket_config:?}'"); + + if let Some(address) = websocket_config.address.as_ref() { + if !glob_match(address, client.server_addr.ip().to_string().as_str()) { + return false; + } + } + + if let Some(port) = websocket_config.port { + if port != client.server_addr.port() { + return false; + } + } + + if let Some(client_addr) = websocket_config.client_address.as_ref() { + if !glob_match(client_addr, client.addr.ip().to_string().as_str()) { + return false; + } + } + + if let Some(port) = websocket_config.client_port { + if port != client.addr.port() { + return false; + } + } + + true + } + /// Returns true if a given device matches the given udev config pub fn has_matching_udev(&self, device: &UdevDevice, udev_config: &Udev) -> bool { log::trace!("Checking udev config '{:?}'", udev_config); diff --git a/src/dbus/interface/target/mod.rs b/src/dbus/interface/target/mod.rs index 6ad77c87..bc4d9041 100644 --- a/src/dbus/interface/target/mod.rs +++ b/src/dbus/interface/target/mod.rs @@ -4,6 +4,7 @@ pub mod gamepad; pub mod keyboard; pub mod mouse; pub mod touchscreen; +pub mod websocket; use zbus::fdo; use zbus_macros::interface; diff --git a/src/dbus/interface/target/websocket.rs b/src/dbus/interface/target/websocket.rs new file mode 100644 index 00000000..36fe5aeb --- /dev/null +++ b/src/dbus/interface/target/websocket.rs @@ -0,0 +1,55 @@ +use futures::StreamExt; +use zbus::{fdo, object_server::SignalEmitter}; +use zbus_macros::interface; + +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpStream, + sync::mpsc::Sender, +}; +use tokio_tungstenite::{connect_async, MaybeTlsStream, WebSocketStream}; + +use crate::input::target::websocket::DeviceCommand; + +/// The [TargetWebsocketInterface] provides a DBus interface that can be exposed for managing +/// a websocket device. +pub struct TargetWebsocketInterface { + pub connection: Option>>, + pub device_tx: Sender, +} + +impl TargetWebsocketInterface { + pub fn new(device_tx: Sender) -> TargetWebsocketInterface { + TargetWebsocketInterface { + connection: None, + device_tx, + } + } +} + +#[interface( + name = "org.shadowblip.Input.Websocket", + proxy(default_service = "org.shadowblip.InputPlumber",) +)] +impl TargetWebsocketInterface { + /// Connect to the given InputPlumber websocket server + pub async fn connect(&mut self, url: String) -> fdo::Result<()> { + let (ws_stream, _resp) = connect_async(&url) + .await + .map_err(|e| fdo::Error::Failed(e.to_string()))?; + + log::info!("WebSocket handshake has been successfully completed"); + self.connection = Some(ws_stream); + self.device_tx + .send(DeviceCommand::WebsocketConnected) + .await + .map_err(|e| fdo::Error::Failed(e.to_string()))?; + + Ok(()) + } + + #[zbus(property)] + pub async fn connected(&self) -> fdo::Result { + Ok(self.connection.is_some()) + } +} diff --git a/src/input/composite_device/client.rs b/src/input/composite_device/client.rs index 4439f824..87d4e535 100644 --- a/src/input/composite_device/client.rs +++ b/src/input/composite_device/client.rs @@ -4,6 +4,7 @@ use tokio::sync::mpsc::{channel, error::SendError, Sender}; use crate::config::CompositeDeviceConfig; use crate::input::event::native::NativeEvent; +use crate::input::info::DeviceInfo; use crate::input::target::client::TargetDeviceClient; use crate::input::{capability::Capability, event::Event, output_event::OutputEvent}; use crate::udev::device::UdevDevice; @@ -207,7 +208,7 @@ impl CompositeDeviceClient { } /// Add the given source device to the composite device - pub async fn add_source_device(&self, device: UdevDevice) -> Result<(), ClientError> { + pub async fn add_source_device(&self, device: DeviceInfo) -> Result<(), ClientError> { self.tx .send(CompositeCommand::SourceDeviceAdded(device)) .await?; @@ -215,7 +216,7 @@ impl CompositeDeviceClient { } /// Remove the given source device from the composite device - pub async fn remove_source_device(&self, device: UdevDevice) -> Result<(), ClientError> { + pub async fn remove_source_device(&self, device: DeviceInfo) -> Result<(), ClientError> { self.tx .send(CompositeCommand::SourceDeviceRemoved(device)) .await?; diff --git a/src/input/composite_device/command.rs b/src/input/composite_device/command.rs index b281c5de..b4850181 100644 --- a/src/input/composite_device/command.rs +++ b/src/input/composite_device/command.rs @@ -7,10 +7,10 @@ use crate::{ input::{ capability::Capability, event::{native::NativeEvent, Event}, + info::DeviceInfo, output_event::OutputEvent, target::client::TargetDeviceClient, }, - udev::device::UdevDevice, }; use super::InterceptMode; @@ -18,7 +18,7 @@ use super::InterceptMode; /// CompositeDevice commands define all the different ways to interact with [CompositeDevice] /// over a channel. These commands are processed in an asyncronous thread and /// dispatched as they come in. -#[derive(Debug, Clone)] +#[derive(Debug)] pub enum CompositeCommand { AttachTargetDevices(HashMap), GetConfig(mpsc::Sender), @@ -39,9 +39,9 @@ pub enum CompositeCommand { SetInterceptActivation(Vec, Capability), SetInterceptMode(InterceptMode), SetTargetDevices(Vec), - SourceDeviceAdded(UdevDevice), - SourceDeviceRemoved(UdevDevice), - SourceDeviceStopped(UdevDevice), + SourceDeviceAdded(DeviceInfo), + SourceDeviceRemoved(DeviceInfo), + SourceDeviceStopped(DeviceInfo), UpdateSourceCapabilities(String, HashSet), UpdateTargetCapabilities(String, HashSet), WriteChordEvent(Vec), diff --git a/src/input/composite_device/mod.rs b/src/input/composite_device/mod.rs index 45945f48..6db63f91 100644 --- a/src/input/composite_device/mod.rs +++ b/src/input/composite_device/mod.rs @@ -35,17 +35,18 @@ use crate::{ }, output_event::UinputOutputEvent, source::{ - evdev::EventDevice, hidraw::HidRawDevice, iio::IioDevice, led::LedDevice, SourceDevice, + evdev::EventDevice, hidraw::HidRawDevice, iio::IioDevice, led::LedDevice, + network::NetworkDevice, SourceDevice, }, }, - udev::{device::UdevDevice, hide_device, unhide_device}, + udev::{hide_device, unhide_device}, }; use self::{client::CompositeDeviceClient, command::CompositeCommand}; use super::{ - manager::ManagerCommand, output_event::OutputEvent, source::client::SourceDeviceClient, - target::client::TargetDeviceClient, + info::DeviceInfo, manager::ManagerCommand, output_event::OutputEvent, + source::client::SourceDeviceClient, target::client::TargetDeviceClient, }; /// Size of the command channel buffer for processing input events and commands. @@ -176,7 +177,7 @@ impl CompositeDevice { conn: Connection, manager: mpsc::Sender, config: CompositeDeviceConfig, - device_info: UdevDevice, + device_info: DeviceInfo, dbus_path: String, capability_map: Option, ) -> Result> { @@ -376,13 +377,13 @@ impl CompositeDevice { } } CompositeCommand::SourceDeviceStopped(device) => { - log::debug!("Detected source device stopped: {}", device.devnode()); + log::debug!("Detected source device stopped: {}", device.path()); if let Err(e) = self.on_source_device_removed(device).await { log::error!("Failed to remove source device: {:?}", e); } } CompositeCommand::SourceDeviceRemoved(device) => { - log::debug!("Detected source device removed: {}", device.devnode()); + log::debug!("Detected source device removed: {}", device.path()); devices_removed = true; if let Err(e) = self.on_source_device_removed(device).await { log::error!("Failed to remove source device: {:?}", e); @@ -631,6 +632,7 @@ impl CompositeDevice { let sources = self.source_devices_discovered.drain(..); for source_device in sources { let device_id = source_device.get_id(); + log::debug!("Starting source device: {device_id}"); // If the source device is blocked, don't bother running it if self.source_devices_blocked.contains(&device_id) { log::debug!("Source device '{device_id}' blocked. Skipping running."); @@ -643,9 +645,13 @@ impl CompositeDevice { // Add the IIO IMU Dbus interface. We do this here because it needs the source // device transmitter and this is the only place we can refrence it at the moment. - let device = source_device.get_device_ref().clone(); + let device = source_device.get_device_ref().to_owned(); if let SourceDevice::Iio(_) = source_device { - SourceIioImuInterface::listen_on_dbus(self.conn.clone(), device.clone()).await?; + let DeviceInfo::Udev(device) = device.clone() else { + log::error!("Invalid device info for IIO device"); + continue; + }; + SourceIioImuInterface::listen_on_dbus(self.conn.clone(), device).await?; } self.source_device_tasks.spawn(async move { @@ -1408,7 +1414,7 @@ impl CompositeDevice { } /// Executed whenever a source device is added to this [CompositeDevice]. - async fn on_source_device_added(&mut self, device: UdevDevice) -> Result<(), Box> { + async fn on_source_device_added(&mut self, device: DeviceInfo) -> Result<(), Box> { if let Err(e) = self.add_source_device(device) { return Err(e.to_string().into()); } @@ -1425,8 +1431,8 @@ impl CompositeDevice { } /// Executed whenever a source device is removed from this [CompositeDevice] - async fn on_source_device_removed(&mut self, device: UdevDevice) -> Result<(), Box> { - let path = device.devnode(); + async fn on_source_device_removed(&mut self, device: DeviceInfo) -> Result<(), Box> { + let path = device.path(); let id = device.get_id(); if let Some(idx) = self.source_device_paths.iter().position(|str| str == &path) { @@ -1456,7 +1462,7 @@ impl CompositeDevice { /// Creates and adds a source device using the given [SourceDeviceInfo] fn add_source_device( &mut self, - device: UdevDevice, + device: DeviceInfo, ) -> Result<(), Box> { // Check to see if this source device should be blocked. let mut is_blocked = false; @@ -1468,49 +1474,56 @@ impl CompositeDevice { } } - let subsystem = device.subsystem(); - - // Hide the device if specified - let should_passthru = source_config - .as_ref() - .and_then(|c| c.passthrough) - .unwrap_or(false); - let should_hide = !should_passthru && subsystem.as_str() != "iio"; - if should_hide { - let source_path = device.devnode(); - self.source_devices_to_hide.push(source_path); - } + log::debug!("Adding source device: {:?}", device.name()); + let source_device = match device { + DeviceInfo::Udev(device) => { + let subsystem = device.subsystem(); + + // Hide the device if specified + let should_passthru = source_config + .as_ref() + .and_then(|c| c.passthrough) + .unwrap_or(false); + let should_hide = !should_passthru && subsystem.as_str() != "iio"; + if should_hide { + let source_path = device.devnode(); + self.source_devices_to_hide.push(source_path); + } - let source_device = match subsystem.as_str() { - "input" => { - log::debug!("Adding source device: {:?}", device.name()); - if is_blocked { - is_blocked_evdev = true; + match subsystem.as_str() { + "input" => { + if is_blocked { + is_blocked_evdev = true; + } + let device = + EventDevice::new(device, self.client(), source_config.clone())?; + SourceDevice::Event(device) + } + "hidraw" => { + let device = + HidRawDevice::new(device, self.client(), source_config.clone())?; + SourceDevice::HidRaw(device) + } + "iio" => { + let device = IioDevice::new(device, self.client(), source_config.clone())?; + SourceDevice::Iio(device) + } + "leds" => { + let device = LedDevice::new(device, self.client(), source_config.clone())?; + SourceDevice::Led(device) + } + _ => { + return Err(format!( + "Unspported subsystem: {subsystem}, unable to add source device {}", + device.name() + ) + .into()) + } } - let device = EventDevice::new(device, self.client(), source_config.clone())?; - SourceDevice::Event(device) - } - "hidraw" => { - log::debug!("Adding source device: {:?}", device.name()); - let device = HidRawDevice::new(device, self.client(), source_config.clone())?; - SourceDevice::HidRaw(device) - } - "iio" => { - log::debug!("Adding source device: {:?}", device.name()); - let device = IioDevice::new(device, self.client(), source_config.clone())?; - SourceDevice::Iio(device) - } - "leds" => { - log::debug!("Adding source device: {:?}", device.name()); - let device = LedDevice::new(device, self.client(), source_config.clone())?; - SourceDevice::Led(device) } - _ => { - return Err(format!( - "Unspported subsystem: {subsystem}, unable to add source device {}", - device.name() - ) - .into()) + DeviceInfo::Websocket(client) => { + let device = NetworkDevice::new(client, self.client(), source_config.clone())?; + SourceDevice::Network(device) } }; @@ -1531,7 +1544,7 @@ impl CompositeDevice { let id = source_device.get_id(); if let Some(device_config) = self .config - .get_matching_device(source_device.get_device_ref()) + .get_matching_device(&source_device.get_device_ref().to_owned()) { if let Some(blocked) = device_config.blocked { // Blocked event devices should still be run so they can be diff --git a/src/input/event/mod.rs b/src/input/event/mod.rs index 58da8068..d9c29633 100644 --- a/src/input/event/mod.rs +++ b/src/input/event/mod.rs @@ -1,6 +1,7 @@ pub mod dbus; pub mod evdev; pub mod native; +pub mod ucis; pub mod value; /// Events are events that flow from source devices to target devices diff --git a/src/input/event/ucis.rs b/src/input/event/ucis.rs new file mode 100644 index 00000000..e8798afe --- /dev/null +++ b/src/input/event/ucis.rs @@ -0,0 +1,730 @@ +use crate::{ + drivers::unified_gamepad::{capability::InputCapability, event::Event, value::Value}, + input::capability::{ + Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger, Touch, TouchButton, + Touchpad, + }, +}; +use packed_struct::prelude::*; + +use super::{native::NativeEvent, value::InputValue}; + +const GYRO_SCALE_FACTOR: f64 = 10.0; // amount to scale imu data +const ACCEL_SCALE_FACTOR: f64 = 3000.0; // amount to scale imu data + +impl From for NativeEvent { + fn from(value: Event) -> Self { + let capability = value.capability.into(); + let value = value.value.into(); + Self::new(capability, value) + } +} + +impl From for InputValue { + fn from(value: Value) -> Self { + match value { + Value::None => InputValue::None, + Value::Bool(value) => InputValue::Bool(value.value), + Value::UInt8(value) => InputValue::Float(value.value as f64 / u8::MAX as f64), + Value::UInt16(value) => InputValue::Float(value.value as f64 / u16::MAX as f64), + Value::UInt16Vector2(value) => { + // Denormalize the x and y values from 0 and u16::MAX to -1.0 -> 1.0 + let x = ((value.x as f64 / u16::MAX as f64) * 2.0) - 1.0; + let y = ((value.y as f64 / u16::MAX as f64) * 2.0) - 1.0; + InputValue::Vector2 { + x: Some(x), + y: Some(y), + } + } + Value::Int16Vector3(value) => { + let x = Some(value.x as f64); + let y = Some(value.y as f64); + let z = Some(value.z as f64); + InputValue::Vector3 { x, y, z } + } + Value::Touch(value) => InputValue::Touch { + index: value.index.to_primitive(), + is_touching: value.is_touching, + pressure: Some((value.pressure / u8::MAX) as f64), + x: Some(value.x as f64 / u16::MAX as f64), + y: Some(value.y as f64 / u16::MAX as f64), + }, + } + } +} + +impl From for Capability { + fn from(value: InputCapability) -> Self { + let capability = match value { + InputCapability::None => Capability::None, + InputCapability::KeyboardKeyEsc => Capability::NotImplemented, + InputCapability::KeyboardKey1 => Capability::NotImplemented, + InputCapability::KeyboardKey2 => Capability::NotImplemented, + InputCapability::KeyboardKey3 => Capability::NotImplemented, + InputCapability::KeyboardKey4 => Capability::NotImplemented, + InputCapability::KeyboardKey5 => Capability::NotImplemented, + InputCapability::KeyboardKey6 => Capability::NotImplemented, + InputCapability::KeyboardKey7 => Capability::NotImplemented, + InputCapability::KeyboardKey8 => Capability::NotImplemented, + InputCapability::KeyboardKey9 => Capability::NotImplemented, + InputCapability::KeyboardKey0 => Capability::NotImplemented, + InputCapability::KeyboardKeyMinus => Capability::NotImplemented, + InputCapability::KeyboardKeyEqual => Capability::NotImplemented, + InputCapability::KeyboardKeyBackspace => Capability::NotImplemented, + InputCapability::KeyboardKeyTab => Capability::NotImplemented, + InputCapability::KeyboardKeyQ => Capability::NotImplemented, + InputCapability::KeyboardKeyW => Capability::NotImplemented, + InputCapability::KeyboardKeyE => Capability::NotImplemented, + InputCapability::KeyboardKeyR => Capability::NotImplemented, + InputCapability::KeyboardKeyT => Capability::NotImplemented, + InputCapability::KeyboardKeyY => Capability::NotImplemented, + InputCapability::KeyboardKeyU => Capability::NotImplemented, + InputCapability::KeyboardKeyI => Capability::NotImplemented, + InputCapability::KeyboardKeyO => Capability::NotImplemented, + InputCapability::KeyboardKeyP => Capability::NotImplemented, + InputCapability::KeyboardKeyLeftBrace => Capability::NotImplemented, + InputCapability::KeyboardKeyRightBrace => Capability::NotImplemented, + InputCapability::KeyboardKeyEnter => Capability::NotImplemented, + InputCapability::KeyboardKeyLeftCtrl => Capability::NotImplemented, + InputCapability::KeyboardKeyA => Capability::NotImplemented, + InputCapability::KeyboardKeyS => Capability::NotImplemented, + InputCapability::KeyboardKeyD => Capability::NotImplemented, + InputCapability::KeyboardKeyF => Capability::NotImplemented, + InputCapability::KeyboardKeyG => Capability::NotImplemented, + InputCapability::KeyboardKeyH => Capability::NotImplemented, + InputCapability::KeyboardKeyJ => Capability::NotImplemented, + InputCapability::KeyboardKeyK => Capability::NotImplemented, + InputCapability::KeyboardKeyL => Capability::NotImplemented, + InputCapability::KeyboardKeySemicolon => Capability::NotImplemented, + InputCapability::KeyboardKeyApostrophe => Capability::NotImplemented, + InputCapability::KeyboardKeyGrave => Capability::NotImplemented, + InputCapability::KeyboardKeyLeftShift => Capability::NotImplemented, + InputCapability::KeyboardKeyBackslash => Capability::NotImplemented, + InputCapability::KeyboardKeyZ => Capability::NotImplemented, + InputCapability::KeyboardKeyX => Capability::NotImplemented, + InputCapability::KeyboardKeyC => Capability::NotImplemented, + InputCapability::KeyboardKeyV => Capability::NotImplemented, + InputCapability::KeyboardKeyB => Capability::NotImplemented, + InputCapability::KeyboardKeyN => Capability::NotImplemented, + InputCapability::KeyboardKeyM => Capability::NotImplemented, + InputCapability::KeyboardKeyComma => Capability::NotImplemented, + InputCapability::KeyboardKeyDot => Capability::NotImplemented, + InputCapability::KeyboardKeySlash => Capability::NotImplemented, + InputCapability::KeyboardKeyRightShift => Capability::NotImplemented, + InputCapability::KeyboardKeyKpAsterisk => Capability::NotImplemented, + InputCapability::KeyboardKeyLeftAlt => Capability::NotImplemented, + InputCapability::KeyboardKeySpace => Capability::NotImplemented, + InputCapability::KeyboardKeyCapsLock => Capability::NotImplemented, + InputCapability::KeyboardKeyF1 => Capability::NotImplemented, + InputCapability::KeyboardKeyF2 => Capability::NotImplemented, + InputCapability::KeyboardKeyF3 => Capability::NotImplemented, + InputCapability::KeyboardKeyF4 => Capability::NotImplemented, + InputCapability::KeyboardKeyF5 => Capability::NotImplemented, + InputCapability::KeyboardKeyF6 => Capability::NotImplemented, + InputCapability::KeyboardKeyF7 => Capability::NotImplemented, + InputCapability::KeyboardKeyF8 => Capability::NotImplemented, + InputCapability::KeyboardKeyF9 => Capability::NotImplemented, + InputCapability::KeyboardKeyF10 => Capability::NotImplemented, + InputCapability::KeyboardKeyNumLock => Capability::NotImplemented, + InputCapability::KeyboardKeyScrollLock => Capability::NotImplemented, + InputCapability::KeyboardKeyKP7 => Capability::NotImplemented, + InputCapability::KeyboardKeyKP8 => Capability::NotImplemented, + InputCapability::KeyboardKeyKP9 => Capability::NotImplemented, + InputCapability::KeyboardKeyKPMinus => Capability::NotImplemented, + InputCapability::KeyboardKeyKP4 => Capability::NotImplemented, + InputCapability::KeyboardKeyKP5 => Capability::NotImplemented, + InputCapability::KeyboardKeyKP6 => Capability::NotImplemented, + InputCapability::KeyboardKeyKPPlus => Capability::NotImplemented, + InputCapability::KeyboardKeyKP1 => Capability::NotImplemented, + InputCapability::KeyboardKeyKP2 => Capability::NotImplemented, + InputCapability::KeyboardKeyKP3 => Capability::NotImplemented, + InputCapability::KeyboardKeyKP0 => Capability::NotImplemented, + InputCapability::KeyboardKeyKPDot => Capability::NotImplemented, + InputCapability::KeyboardKeyZenkakuhankaku => Capability::NotImplemented, + InputCapability::KeyboardKey102nd => Capability::NotImplemented, + InputCapability::KeyboardKeyF11 => Capability::NotImplemented, + InputCapability::KeyboardKeyF12 => Capability::NotImplemented, + InputCapability::KeyboardKeyRo => Capability::NotImplemented, + InputCapability::KeyboardKeyKatakana => Capability::NotImplemented, + InputCapability::KeyboardKeyHiragana => Capability::NotImplemented, + InputCapability::KeyboardKeyHenkan => Capability::NotImplemented, + InputCapability::KeyboardKeyKatakanaHiragana => Capability::NotImplemented, + InputCapability::KeyboardKeyMuhenkan => Capability::NotImplemented, + InputCapability::KeyboardKeyKPJpComma => Capability::NotImplemented, + InputCapability::KeyboardKeyKPEnter => Capability::NotImplemented, + InputCapability::KeyboardKeyRightCtrl => Capability::NotImplemented, + InputCapability::KeyboardKeyKPSlash => Capability::NotImplemented, + InputCapability::KeyboardKeySysRq => Capability::NotImplemented, + InputCapability::KeyboardKeyRightAlt => Capability::NotImplemented, + InputCapability::KeyboardKeyLineFeed => Capability::NotImplemented, + InputCapability::KeyboardKeyHome => Capability::NotImplemented, + InputCapability::KeyboardKeyUp => Capability::NotImplemented, + InputCapability::KeyboardKeyPageUp => Capability::NotImplemented, + InputCapability::KeyboardKeyLeft => Capability::NotImplemented, + InputCapability::KeyboardKeyRight => Capability::NotImplemented, + InputCapability::KeyboardKeyEnd => Capability::NotImplemented, + InputCapability::KeyboardKeyDown => Capability::NotImplemented, + InputCapability::KeyboardKeyPageDown => Capability::NotImplemented, + InputCapability::KeyboardKeyInsert => Capability::NotImplemented, + InputCapability::KeyboardKeyDelete => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro => Capability::NotImplemented, + InputCapability::KeyboardKeyMute => Capability::NotImplemented, + InputCapability::KeyboardKeyVolumeDown => Capability::NotImplemented, + InputCapability::KeyboardKeyVolumeUp => Capability::NotImplemented, + InputCapability::KeyboardKeyPower => Capability::NotImplemented, + InputCapability::KeyboardKeyKPEqual => Capability::NotImplemented, + InputCapability::KeyboardKeyKPPlusMinus => Capability::NotImplemented, + InputCapability::KeyboardKeyPause => Capability::NotImplemented, + InputCapability::KeyboardKeyScale => Capability::NotImplemented, + InputCapability::KeyboardKeyKPComma => Capability::NotImplemented, + InputCapability::KeyboardKeyHangeul => Capability::NotImplemented, + InputCapability::KeyboardKeyHanja => Capability::NotImplemented, + InputCapability::KeyboardKeyYen => Capability::NotImplemented, + InputCapability::KeyboardKeyLeftMeta => Capability::NotImplemented, + InputCapability::KeyboardKeyRightMeta => Capability::NotImplemented, + InputCapability::KeyboardKeyCompose => Capability::NotImplemented, + InputCapability::KeyboardKeyStop => Capability::NotImplemented, + InputCapability::KeyboardKeyAgain => Capability::NotImplemented, + InputCapability::KeyboardKeyProps => Capability::NotImplemented, + InputCapability::KeyboardKeyUndo => Capability::NotImplemented, + InputCapability::KeyboardKeyFront => Capability::NotImplemented, + InputCapability::KeyboardKeyCopy => Capability::NotImplemented, + InputCapability::KeyboardKeyOpen => Capability::NotImplemented, + InputCapability::KeyboardKeyPaste => Capability::NotImplemented, + InputCapability::KeyboardKeyFind => Capability::NotImplemented, + InputCapability::KeyboardKeyCut => Capability::NotImplemented, + InputCapability::KeyboardKeyHelp => Capability::NotImplemented, + InputCapability::KeyboardKeyMenu => Capability::NotImplemented, + InputCapability::KeyboardKeyCalc => Capability::NotImplemented, + InputCapability::KeyboardKeySetup => Capability::NotImplemented, + InputCapability::KeyboardKeySleep => Capability::NotImplemented, + InputCapability::KeyboardKeyWakeup => Capability::NotImplemented, + InputCapability::KeyboardKeyFile => Capability::NotImplemented, + InputCapability::KeyboardKeySendFile => Capability::NotImplemented, + InputCapability::KeyboardKeyDeleteFile => Capability::NotImplemented, + InputCapability::KeyboardKeyXfer => Capability::NotImplemented, + InputCapability::KeyboardKeyProg1 => Capability::NotImplemented, + InputCapability::KeyboardKeyProg2 => Capability::NotImplemented, + InputCapability::KeyboardKeyWww => Capability::NotImplemented, + InputCapability::KeyboardKeyMsdos => Capability::NotImplemented, + InputCapability::KeyboardKeyScreenLock => Capability::NotImplemented, + InputCapability::KeyboardKeyRotateDisplay => Capability::NotImplemented, + InputCapability::KeyboardKeyCycleWindows => Capability::NotImplemented, + InputCapability::KeyboardKeyMail => Capability::NotImplemented, + InputCapability::KeyboardKeyBookmarks => Capability::NotImplemented, + InputCapability::KeyboardKeyComputer => Capability::NotImplemented, + InputCapability::KeyboardKeyBack => Capability::NotImplemented, + InputCapability::KeyboardKeyForward => Capability::NotImplemented, + InputCapability::KeyboardKeyCloseCd => Capability::NotImplemented, + InputCapability::KeyboardKeyEjectCd => Capability::NotImplemented, + InputCapability::KeyboardKeyEjectCloseCd => Capability::NotImplemented, + InputCapability::KeyboardKeyNextSong => Capability::NotImplemented, + InputCapability::KeyboardKeyPlayPause => Capability::NotImplemented, + InputCapability::KeyboardKeyPreviousSong => Capability::NotImplemented, + InputCapability::KeyboardKeyStopCd => Capability::NotImplemented, + InputCapability::KeyboardKeyRecord => Capability::NotImplemented, + InputCapability::KeyboardKeyRewind => Capability::NotImplemented, + InputCapability::KeyboardKeyPhone => Capability::NotImplemented, + InputCapability::KeyboardKeyIso => Capability::NotImplemented, + InputCapability::KeyboardKeyConfig => Capability::NotImplemented, + InputCapability::KeyboardKeyHomepage => Capability::NotImplemented, + InputCapability::KeyboardKeyRefresh => Capability::NotImplemented, + InputCapability::KeyboardKeyExit => Capability::NotImplemented, + InputCapability::KeyboardKeyMove => Capability::NotImplemented, + InputCapability::KeyboardKeyEdit => Capability::NotImplemented, + InputCapability::KeyboardKeyScrollUp => Capability::NotImplemented, + InputCapability::KeyboardKeyScrollDown => Capability::NotImplemented, + InputCapability::KeyboardKeyKPLeftParen => Capability::NotImplemented, + InputCapability::KeyboardKeyKPRightParen => Capability::NotImplemented, + InputCapability::KeyboardKeyNew => Capability::NotImplemented, + InputCapability::KeyboardKeyRedo => Capability::NotImplemented, + InputCapability::KeyboardKeyF13 => Capability::NotImplemented, + InputCapability::KeyboardKeyF14 => Capability::NotImplemented, + InputCapability::KeyboardKeyF15 => Capability::NotImplemented, + InputCapability::KeyboardKeyF16 => Capability::NotImplemented, + InputCapability::KeyboardKeyF17 => Capability::NotImplemented, + InputCapability::KeyboardKeyF18 => Capability::NotImplemented, + InputCapability::KeyboardKeyF19 => Capability::NotImplemented, + InputCapability::KeyboardKeyF20 => Capability::NotImplemented, + InputCapability::KeyboardKeyF21 => Capability::NotImplemented, + InputCapability::KeyboardKeyF22 => Capability::NotImplemented, + InputCapability::KeyboardKeyF23 => Capability::NotImplemented, + InputCapability::KeyboardKeyF24 => Capability::NotImplemented, + InputCapability::KeyboardKeyPlayCd => Capability::NotImplemented, + InputCapability::KeyboardKeyPauseCd => Capability::NotImplemented, + InputCapability::KeyboardKeyProg3 => Capability::NotImplemented, + InputCapability::KeyboardKeyProg4 => Capability::NotImplemented, + InputCapability::KeyboardKeyDashboard => Capability::NotImplemented, + InputCapability::KeyboardKeySuspend => Capability::NotImplemented, + InputCapability::KeyboardKeyClose => Capability::NotImplemented, + InputCapability::KeyboardKeyPlay => Capability::NotImplemented, + InputCapability::KeyboardKeyFastForward => Capability::NotImplemented, + InputCapability::KeyboardKeyBassBoost => Capability::NotImplemented, + InputCapability::KeyboardKeyPrint => Capability::NotImplemented, + InputCapability::KeyboardKeyHp => Capability::NotImplemented, + InputCapability::KeyboardKeyCamera => Capability::NotImplemented, + InputCapability::KeyboardKeySound => Capability::NotImplemented, + InputCapability::KeyboardKeyQuestion => Capability::NotImplemented, + InputCapability::KeyboardKeyEmail => Capability::NotImplemented, + InputCapability::KeyboardKeyChat => Capability::NotImplemented, + InputCapability::KeyboardKeySearch => Capability::NotImplemented, + InputCapability::KeyboardKeyConnect => Capability::NotImplemented, + InputCapability::KeyboardKeyFinance => Capability::NotImplemented, + InputCapability::KeyboardKeySport => Capability::NotImplemented, + InputCapability::KeyboardKeyShop => Capability::NotImplemented, + InputCapability::KeyboardKeyAltErase => Capability::NotImplemented, + InputCapability::KeyboardKeyCancel => Capability::NotImplemented, + InputCapability::KeyboardKeyBrightnessDown => Capability::NotImplemented, + InputCapability::KeyboardKeyBrightnessUp => Capability::NotImplemented, + InputCapability::KeyboardKeyMedia => Capability::NotImplemented, + InputCapability::KeyboardKeySwitchVideoMode => Capability::NotImplemented, + InputCapability::KeyboardKeyKBDillumToggle => Capability::NotImplemented, + InputCapability::KeyboardKeyKBDillumDown => Capability::NotImplemented, + InputCapability::KeyboardKeyKBDillumUp => Capability::NotImplemented, + InputCapability::KeyboardKeySend => Capability::NotImplemented, + InputCapability::KeyboardKeyReply => Capability::NotImplemented, + InputCapability::KeyboardKeyForwardMail => Capability::NotImplemented, + InputCapability::KeyboardKeySave => Capability::NotImplemented, + InputCapability::KeyboardKeyDocuments => Capability::NotImplemented, + InputCapability::KeyboardKeyBattery => Capability::NotImplemented, + InputCapability::KeyboardKeyBluetooth => Capability::NotImplemented, + InputCapability::KeyboardKeyWlan => Capability::NotImplemented, + InputCapability::KeyboardKeyUwb => Capability::NotImplemented, + InputCapability::KeyboardKeyUnknown => Capability::NotImplemented, + InputCapability::KeyboardKeyVideoNext => Capability::NotImplemented, + InputCapability::KeyboardKeyVideoPrev => Capability::NotImplemented, + InputCapability::KeyboardKeyBrightnessCycle => Capability::NotImplemented, + InputCapability::KeyboardKeyBrightnessAuto => Capability::NotImplemented, + InputCapability::KeyboardKeyDisplayOff => Capability::NotImplemented, + InputCapability::KeyboardKeyWwan => Capability::NotImplemented, + InputCapability::KeyboardKeyRfKill => Capability::NotImplemented, + InputCapability::KeyboardKeyMicMute => Capability::NotImplemented, + InputCapability::KeyboardKeyOk => Capability::NotImplemented, + InputCapability::KeyboardKeySelect => Capability::NotImplemented, + InputCapability::KeyboardKeyGoto => Capability::NotImplemented, + InputCapability::KeyboardKeyClear => Capability::NotImplemented, + InputCapability::KeyboardKeyPower2 => Capability::NotImplemented, + InputCapability::KeyboardKeyOption => Capability::NotImplemented, + InputCapability::KeyboardKeyInfo => Capability::NotImplemented, + InputCapability::KeyboardKeyTime => Capability::NotImplemented, + InputCapability::KeyboardKeyVendor => Capability::NotImplemented, + InputCapability::KeyboardKeyArchive => Capability::NotImplemented, + InputCapability::KeyboardKeyProgram => Capability::NotImplemented, + InputCapability::KeyboardKeyChannel => Capability::NotImplemented, + InputCapability::KeyboardKeyFavorites => Capability::NotImplemented, + InputCapability::KeyboardKeyEpg => Capability::NotImplemented, + InputCapability::KeyboardKeyPvr => Capability::NotImplemented, + InputCapability::KeyboardKeyMhp => Capability::NotImplemented, + InputCapability::KeyboardKeyLanguage => Capability::NotImplemented, + InputCapability::KeyboardKeyTitle => Capability::NotImplemented, + InputCapability::KeyboardKeySubtitle => Capability::NotImplemented, + InputCapability::KeyboardKeyAngle => Capability::NotImplemented, + InputCapability::KeyboardKeyFullScreen => Capability::NotImplemented, + InputCapability::KeyboardKeyMode => Capability::NotImplemented, + InputCapability::KeyboardKeyKeyboard => Capability::NotImplemented, + InputCapability::KeyboardKeyAspectRatio => Capability::NotImplemented, + InputCapability::KeyboardKeyPc => Capability::NotImplemented, + InputCapability::KeyboardKeyTv => Capability::NotImplemented, + InputCapability::KeyboardKeyTv2 => Capability::NotImplemented, + InputCapability::KeyboardKeyVcr => Capability::NotImplemented, + InputCapability::KeyboardKeyVcr2 => Capability::NotImplemented, + InputCapability::KeyboardKeySat => Capability::NotImplemented, + InputCapability::KeyboardKeySat2 => Capability::NotImplemented, + InputCapability::KeyboardKeyCd => Capability::NotImplemented, + InputCapability::KeyboardKeyTape => Capability::NotImplemented, + InputCapability::KeyboardKeyRadio => Capability::NotImplemented, + InputCapability::KeyboardKeyTuner => Capability::NotImplemented, + InputCapability::KeyboardKeyPlayer => Capability::NotImplemented, + InputCapability::KeyboardKeyText => Capability::NotImplemented, + InputCapability::KeyboardKeyDvd => Capability::NotImplemented, + InputCapability::KeyboardKeyAux => Capability::NotImplemented, + InputCapability::KeyboardKeyMp3 => Capability::NotImplemented, + InputCapability::KeyboardKeyAudio => Capability::NotImplemented, + InputCapability::KeyboardKeyVideo => Capability::NotImplemented, + InputCapability::KeyboardKeyDirectory => Capability::NotImplemented, + InputCapability::KeyboardKeyList => Capability::NotImplemented, + InputCapability::KeyboardKeyMemo => Capability::NotImplemented, + InputCapability::KeyboardKeyCalendar => Capability::NotImplemented, + InputCapability::KeyboardKeyRed => Capability::NotImplemented, + InputCapability::KeyboardKeyGreen => Capability::NotImplemented, + InputCapability::KeyboardKeyYellow => Capability::NotImplemented, + InputCapability::KeyboardKeyBlue => Capability::NotImplemented, + InputCapability::KeyboardKeyChannelUp => Capability::NotImplemented, + InputCapability::KeyboardKeyChannelDown => Capability::NotImplemented, + InputCapability::KeyboardKeyFirst => Capability::NotImplemented, + InputCapability::KeyboardKeyLast => Capability::NotImplemented, + InputCapability::KeyboardKeyAb => Capability::NotImplemented, + InputCapability::KeyboardKeyNext => Capability::NotImplemented, + InputCapability::KeyboardKeyRestart => Capability::NotImplemented, + InputCapability::KeyboardKeySlow => Capability::NotImplemented, + InputCapability::KeyboardKeyShuffle => Capability::NotImplemented, + InputCapability::KeyboardKeyBreak => Capability::NotImplemented, + InputCapability::KeyboardKeyPrevious => Capability::NotImplemented, + InputCapability::KeyboardKeyDigits => Capability::NotImplemented, + InputCapability::KeyboardKeyTeen => Capability::NotImplemented, + InputCapability::KeyboardKeyTwen => Capability::NotImplemented, + InputCapability::KeyboardKeyVideoPhone => Capability::NotImplemented, + InputCapability::KeyboardKeyGames => Capability::NotImplemented, + InputCapability::KeyboardKeyZoomIn => Capability::NotImplemented, + InputCapability::KeyboardKeyZoomOut => Capability::NotImplemented, + InputCapability::KeyboardKeyZoomReset => Capability::NotImplemented, + InputCapability::KeyboardKeyWordProcessor => Capability::NotImplemented, + InputCapability::KeyboardKeyEditor => Capability::NotImplemented, + InputCapability::KeyboardKeySpreadsheet => Capability::NotImplemented, + InputCapability::KeyboardKeyGraphicsEditor => Capability::NotImplemented, + InputCapability::KeyboardKeyPresentation => Capability::NotImplemented, + InputCapability::KeyboardKeyDatabase => Capability::NotImplemented, + InputCapability::KeyboardKeyNews => Capability::NotImplemented, + InputCapability::KeyboardKeyVoicemail => Capability::NotImplemented, + InputCapability::KeyboardKeyAddressbook => Capability::NotImplemented, + InputCapability::KeyboardKeyMessenger => Capability::NotImplemented, + InputCapability::KeyboardKeyDisplayToggle => Capability::NotImplemented, + InputCapability::KeyboardKeySpellcheck => Capability::NotImplemented, + InputCapability::KeyboardKeyLogoff => Capability::NotImplemented, + InputCapability::KeyboardKeyDollar => Capability::NotImplemented, + InputCapability::KeyboardKeyEuro => Capability::NotImplemented, + InputCapability::KeyboardKeyFrameBack => Capability::NotImplemented, + InputCapability::KeyboardKeyFrameForward => Capability::NotImplemented, + InputCapability::KeyboardKeyContextMenu => Capability::NotImplemented, + InputCapability::KeyboardKeyMediaRepeat => Capability::NotImplemented, + InputCapability::KeyboardKey10ChannelsUp => Capability::NotImplemented, + InputCapability::KeyboardKey10ChannelsDown => Capability::NotImplemented, + InputCapability::KeyboardKeyImages => Capability::NotImplemented, + InputCapability::KeyboardKeyNotificationCenter => Capability::NotImplemented, + InputCapability::KeyboardKeyPickupPhone => Capability::NotImplemented, + InputCapability::KeyboardKeyHangupPhone => Capability::NotImplemented, + InputCapability::KeyboardKeyDelEol => Capability::NotImplemented, + InputCapability::KeyboardKeyDelEos => Capability::NotImplemented, + InputCapability::KeyboardKeyInsLine => Capability::NotImplemented, + InputCapability::KeyboardKeyDelLine => Capability::NotImplemented, + InputCapability::KeyboardKeyFn => Capability::NotImplemented, + InputCapability::KeyboardKeyFnEsc => Capability::NotImplemented, + InputCapability::KeyboardKeyFnF1 => Capability::NotImplemented, + InputCapability::KeyboardKeyFnF2 => Capability::NotImplemented, + InputCapability::KeyboardKeyFnF3 => Capability::NotImplemented, + InputCapability::KeyboardKeyFnF4 => Capability::NotImplemented, + InputCapability::KeyboardKeyFnF5 => Capability::NotImplemented, + InputCapability::KeyboardKeyFnF6 => Capability::NotImplemented, + InputCapability::KeyboardKeyFnF7 => Capability::NotImplemented, + InputCapability::KeyboardKeyFnF8 => Capability::NotImplemented, + InputCapability::KeyboardKeyFnF9 => Capability::NotImplemented, + InputCapability::KeyboardKeyFnF10 => Capability::NotImplemented, + InputCapability::KeyboardKeyFnF11 => Capability::NotImplemented, + InputCapability::KeyboardKeyFnF12 => Capability::NotImplemented, + InputCapability::KeyboardKeyFn1 => Capability::NotImplemented, + InputCapability::KeyboardKeyFn2 => Capability::NotImplemented, + InputCapability::KeyboardKeyFnD => Capability::NotImplemented, + InputCapability::KeyboardKeyFnE => Capability::NotImplemented, + InputCapability::KeyboardKeyFnF => Capability::NotImplemented, + InputCapability::KeyboardKeyFnS => Capability::NotImplemented, + InputCapability::KeyboardKeyFnB => Capability::NotImplemented, + InputCapability::KeyboardKeyFnRightShift => Capability::NotImplemented, + InputCapability::KeyboardKeyBrlDot1 => Capability::NotImplemented, + InputCapability::KeyboardKeyBrlDot2 => Capability::NotImplemented, + InputCapability::KeyboardKeyBrlDot3 => Capability::NotImplemented, + InputCapability::KeyboardKeyBrlDot4 => Capability::NotImplemented, + InputCapability::KeyboardKeyBrlDot5 => Capability::NotImplemented, + InputCapability::KeyboardKeyBrlDot6 => Capability::NotImplemented, + InputCapability::KeyboardKeyBrlDot7 => Capability::NotImplemented, + InputCapability::KeyboardKeyBrlDot8 => Capability::NotImplemented, + InputCapability::KeyboardKeyBrlDot9 => Capability::NotImplemented, + InputCapability::KeyboardKeyBrlDot10 => Capability::NotImplemented, + InputCapability::KeyboardKeyNumeric0 => Capability::NotImplemented, + InputCapability::KeyboardKeyNumeric1 => Capability::NotImplemented, + InputCapability::KeyboardKeyNumeric2 => Capability::NotImplemented, + InputCapability::KeyboardKeyNumeric3 => Capability::NotImplemented, + InputCapability::KeyboardKeyNumeric4 => Capability::NotImplemented, + InputCapability::KeyboardKeyNumeric5 => Capability::NotImplemented, + InputCapability::KeyboardKeyNumeric6 => Capability::NotImplemented, + InputCapability::KeyboardKeyNumeric7 => Capability::NotImplemented, + InputCapability::KeyboardKeyNumeric8 => Capability::NotImplemented, + InputCapability::KeyboardKeyNumeric9 => Capability::NotImplemented, + InputCapability::KeyboardKeyNumericStar => Capability::NotImplemented, + InputCapability::KeyboardKeyNumericPound => Capability::NotImplemented, + InputCapability::KeyboardKeyNumericA => Capability::NotImplemented, + InputCapability::KeyboardKeyNumericB => Capability::NotImplemented, + InputCapability::KeyboardKeyNumericC => Capability::NotImplemented, + InputCapability::KeyboardKeyNumericD => Capability::NotImplemented, + InputCapability::KeyboardKeyCameraFocus => Capability::NotImplemented, + InputCapability::KeyboardKeyWpsButton => Capability::NotImplemented, + InputCapability::KeyboardKeyTouchpadToggle => Capability::NotImplemented, + InputCapability::KeyboardKeyTouchpadOn => Capability::NotImplemented, + InputCapability::KeyboardKeyTouchpadOff => Capability::NotImplemented, + InputCapability::KeyboardKeyCameraZoomIn => Capability::NotImplemented, + InputCapability::KeyboardKeyCameraZoomOut => Capability::NotImplemented, + InputCapability::KeyboardKeyCameraUp => Capability::NotImplemented, + InputCapability::KeyboardKeyCameraDown => Capability::NotImplemented, + InputCapability::KeyboardKeyCameraLeft => Capability::NotImplemented, + InputCapability::KeyboardKeyCameraRight => Capability::NotImplemented, + InputCapability::KeyboardKeyAttendantOn => Capability::NotImplemented, + InputCapability::KeyboardKeyAttendantOff => Capability::NotImplemented, + InputCapability::KeyboardKeyAttendantToggle => Capability::NotImplemented, + InputCapability::KeyboardKeyLightsToggle => Capability::NotImplemented, + InputCapability::KeyboardKeyAlsToggle => Capability::NotImplemented, + InputCapability::KeyboardKeyRotateLockToggle => Capability::NotImplemented, + InputCapability::KeyboardKeyRefreshRateToggle => Capability::NotImplemented, + InputCapability::KeyboardKeyButtonConfig => Capability::NotImplemented, + InputCapability::KeyboardKeyTaskManager => Capability::NotImplemented, + InputCapability::KeyboardKeyJournal => Capability::NotImplemented, + InputCapability::KeyboardKeyControlPanel => Capability::NotImplemented, + InputCapability::KeyboardKeyAppSelect => Capability::NotImplemented, + InputCapability::KeyboardKeyScreensaver => Capability::NotImplemented, + InputCapability::KeyboardKeyVoiceCommand => Capability::NotImplemented, + InputCapability::KeyboardKeyAssistant => Capability::NotImplemented, + InputCapability::KeyboardKeyKbdLayoutNext => Capability::NotImplemented, + InputCapability::KeyboardKeyEmojiPicker => Capability::NotImplemented, + InputCapability::KeyboardKeyDictate => Capability::NotImplemented, + InputCapability::KeyboardKeyCameraAccessEnable => Capability::NotImplemented, + InputCapability::KeyboardKeyCameraAccessDisable => Capability::NotImplemented, + InputCapability::KeyboardKeyCameraAccessToggle => Capability::NotImplemented, + InputCapability::KeyboardKeyAccessibility => Capability::NotImplemented, + InputCapability::KeyboardKeyDoNotDisturb => Capability::NotImplemented, + InputCapability::KeyboardKeyBrightnessMin => Capability::NotImplemented, + InputCapability::KeyboardKeyBrightnessMax => Capability::NotImplemented, + InputCapability::KeyboardKeyKbdInputAssistPrev => Capability::NotImplemented, + InputCapability::KeyboardKeyKbdInputAssistNext => Capability::NotImplemented, + InputCapability::KeyboardKeyKbdInputAssistPrevGroup => Capability::NotImplemented, + InputCapability::KeyboardKeyKbdInputAssistNextGroup => Capability::NotImplemented, + InputCapability::KeyboardKeyKbdInputAssistAccept => Capability::NotImplemented, + InputCapability::KeyboardKeyKbdInputAssistCancel => Capability::NotImplemented, + InputCapability::KeyboardKeyRightUp => Capability::NotImplemented, + InputCapability::KeyboardKeyRightDown => Capability::NotImplemented, + InputCapability::KeyboardKeyLeftUp => Capability::NotImplemented, + InputCapability::KeyboardKeyLeftDown => Capability::NotImplemented, + InputCapability::KeyboardKeyRootMenu => Capability::NotImplemented, + InputCapability::KeyboardKeyMediaTopMenu => Capability::NotImplemented, + InputCapability::KeyboardKeyNumeric11 => Capability::NotImplemented, + InputCapability::KeyboardKeyNumeric12 => Capability::NotImplemented, + InputCapability::KeyboardKeyAudioDesc => Capability::NotImplemented, + InputCapability::KeyboardKey3dMode => Capability::NotImplemented, + InputCapability::KeyboardKeyNextFavorite => Capability::NotImplemented, + InputCapability::KeyboardKeyStopRecord => Capability::NotImplemented, + InputCapability::KeyboardKeyPauseRecord => Capability::NotImplemented, + InputCapability::KeyboardKeyVod => Capability::NotImplemented, + InputCapability::KeyboardKeyUnmute => Capability::NotImplemented, + InputCapability::KeyboardKeyFastReverse => Capability::NotImplemented, + InputCapability::KeyboardKeySlowReverse => Capability::NotImplemented, + InputCapability::KeyboardKeyData => Capability::NotImplemented, + InputCapability::KeyboardKeyOnscreenKeyboard => Capability::NotImplemented, + InputCapability::KeyboardKeyPrivacyScreenToggle => Capability::NotImplemented, + InputCapability::KeyboardKeySelectiveScreenshot => Capability::NotImplemented, + InputCapability::KeyboardKeyNextElement => Capability::NotImplemented, + InputCapability::KeyboardKeyPreviousElement => Capability::NotImplemented, + InputCapability::KeyboardKeyAutopilotEngageToggle => Capability::NotImplemented, + InputCapability::KeyboardKeyMarkWaypoint => Capability::NotImplemented, + InputCapability::KeyboardKeySos => Capability::NotImplemented, + InputCapability::KeyboardKeyNavChart => Capability::NotImplemented, + InputCapability::KeyboardKeyFishingChart => Capability::NotImplemented, + InputCapability::KeyboardKeySingleRangeRadar => Capability::NotImplemented, + InputCapability::KeyboardKeyDualRangeRadar => Capability::NotImplemented, + InputCapability::KeyboardKeyRadarOverlay => Capability::NotImplemented, + InputCapability::KeyboardKeyTraditionalSonar => Capability::NotImplemented, + InputCapability::KeyboardKeyClearvuSonar => Capability::NotImplemented, + InputCapability::KeyboardKeySidevuSonar => Capability::NotImplemented, + InputCapability::KeyboardKeyNavInfo => Capability::NotImplemented, + InputCapability::KeyboardKeyBrightnessMenu => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro1 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro2 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro3 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro4 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro5 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro6 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro7 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro8 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro9 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro10 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro11 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro12 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro13 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro14 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro15 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro16 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro17 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro18 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro19 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro20 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro21 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro22 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro23 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro24 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro25 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro26 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro27 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro28 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro29 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacro30 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacroRecordStart => Capability::NotImplemented, + InputCapability::KeyboardKeyMacroRecordStop => Capability::NotImplemented, + InputCapability::KeyboardKeyMacroPresetCycle => Capability::NotImplemented, + InputCapability::KeyboardKeyMacroPreset1 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacroPreset2 => Capability::NotImplemented, + InputCapability::KeyboardKeyMacroPreset3 => Capability::NotImplemented, + InputCapability::KeyboardKeyKbdLcdMenu1 => Capability::NotImplemented, + InputCapability::KeyboardKeyKbdLcdMenu2 => Capability::NotImplemented, + InputCapability::KeyboardKeyKbdLcdMenu3 => Capability::NotImplemented, + InputCapability::KeyboardKeyKbdLcdMenu4 => Capability::NotImplemented, + InputCapability::KeyboardKeyKbdLcdMenu5 => Capability::NotImplemented, + InputCapability::MouseButtonLeft => Capability::NotImplemented, + InputCapability::MouseButtonRight => Capability::NotImplemented, + InputCapability::MouseButtonMiddle => Capability::NotImplemented, + InputCapability::MouseButtonSide => Capability::NotImplemented, + InputCapability::MouseButtonExtra => Capability::NotImplemented, + InputCapability::GamepadButtonQuick => { + Capability::Gamepad(Gamepad::Button(GamepadButton::QuickAccess)) + } + InputCapability::GamepadButtonQuick2 => { + Capability::Gamepad(Gamepad::Button(GamepadButton::QuickAccess2)) + } + InputCapability::GamepadButtonSouth => { + Capability::Gamepad(Gamepad::Button(GamepadButton::South)) + } + InputCapability::GamepadButtonEast => { + Capability::Gamepad(Gamepad::Button(GamepadButton::East)) + } + InputCapability::GamepadButtonNorth => { + Capability::Gamepad(Gamepad::Button(GamepadButton::North)) + } + InputCapability::GamepadButtonWest => { + Capability::Gamepad(Gamepad::Button(GamepadButton::West)) + } + InputCapability::GamepadButtonSelect => { + Capability::Gamepad(Gamepad::Button(GamepadButton::Select)) + } + InputCapability::GamepadButtonStart => { + Capability::Gamepad(Gamepad::Button(GamepadButton::Start)) + } + InputCapability::GamepadButtonGuide => { + Capability::Gamepad(Gamepad::Button(GamepadButton::Guide)) + } + InputCapability::GamepadButtonDpadUp => { + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadUp)) + } + InputCapability::GamepadButtonDpadDown => { + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadDown)) + } + InputCapability::GamepadButtonDpadLeft => { + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadLeft)) + } + InputCapability::GamepadButtonDpadRight => { + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadRight)) + } + InputCapability::GamepadButtonLeftBumper => { + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftBumper)) + } + InputCapability::GamepadButtonRightBumper => { + Capability::Gamepad(Gamepad::Button(GamepadButton::RightBumper)) + } + InputCapability::GamepadButtonLeftTrigger => { + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftTrigger)) + } + InputCapability::GamepadButtonRightTrigger => { + Capability::Gamepad(Gamepad::Button(GamepadButton::RightTrigger)) + } + InputCapability::GamepadButtonLeftStick => { + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftStick)) + } + InputCapability::GamepadButtonRightStick => { + Capability::Gamepad(Gamepad::Button(GamepadButton::RightStick)) + } + InputCapability::GamepadButtonKeyboard => { + Capability::Gamepad(Gamepad::Button(GamepadButton::Keyboard)) + } + InputCapability::GamepadButtonScreenshot => { + Capability::Gamepad(Gamepad::Button(GamepadButton::Screenshot)) + } + InputCapability::GamepadButtonMute => { + Capability::Gamepad(Gamepad::Button(GamepadButton::Mute)) + } + InputCapability::GamepadButtonLeftPaddle1 => { + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftPaddle1)) + } + InputCapability::GamepadButtonLeftPaddle2 => { + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftPaddle2)) + } + InputCapability::GamepadButtonLeftPaddle3 => { + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftPaddle3)) + } + InputCapability::GamepadButtonRightPaddle1 => { + Capability::Gamepad(Gamepad::Button(GamepadButton::RightPaddle1)) + } + InputCapability::GamepadButtonRightPaddle2 => { + Capability::Gamepad(Gamepad::Button(GamepadButton::RightPaddle2)) + } + InputCapability::GamepadButtonRightPaddle3 => { + Capability::Gamepad(Gamepad::Button(GamepadButton::RightPaddle3)) + } + InputCapability::GamepadButtonLeftStickTouch => { + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftStickTouch)) + } + InputCapability::GamepadButtonRightStickTouch => { + Capability::Gamepad(Gamepad::Button(GamepadButton::RightStickTouch)) + } + InputCapability::GamepadButtonLeftTop => { + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftTop)) + } + InputCapability::GamepadButtonRightTop => { + Capability::Gamepad(Gamepad::Button(GamepadButton::RightTop)) + } + InputCapability::GamepadAxisLeftStick => { + Capability::Gamepad(Gamepad::Axis(GamepadAxis::LeftStick)) + } + InputCapability::GamepadAxisRightStick => { + Capability::Gamepad(Gamepad::Axis(GamepadAxis::RightStick)) + } + InputCapability::GamepadTriggerLeft => { + Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTrigger)) + } + InputCapability::GamepadTriggerRight => { + Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTrigger)) + } + InputCapability::GamepadTriggerLeftTouchpadForce => { + Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTouchpadForce)) + } + InputCapability::GamepadTriggerLeftStickForce => { + Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftStickForce)) + } + InputCapability::GamepadTriggerRightTouchpadForce => { + Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTouchpadForce)) + } + InputCapability::GamepadTriggerRightStickForce => { + Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightStickForce)) + } + InputCapability::GamepadGyroCenter => Capability::Gamepad(Gamepad::Gyro), + InputCapability::GamepadGyroLeft => Capability::Gamepad(Gamepad::Gyro), + InputCapability::GamepadGyroRight => Capability::Gamepad(Gamepad::Gyro), + InputCapability::GamepadAccelerometerCenter => { + Capability::Gamepad(Gamepad::Accelerometer) + } + InputCapability::GamepadAccelerometerLeft => { + Capability::Gamepad(Gamepad::Accelerometer) + } + InputCapability::GamepadAccelerometerRight => { + Capability::Gamepad(Gamepad::Accelerometer) + } + InputCapability::TouchpadLeftMotion => { + Capability::Touchpad(Touchpad::LeftPad(Touch::Motion)) + } + InputCapability::TouchpadCenterMotion => { + Capability::Touchpad(Touchpad::CenterPad(Touch::Motion)) + } + InputCapability::TouchpadRightMotion => { + Capability::Touchpad(Touchpad::RightPad(Touch::Motion)) + } + InputCapability::TouchpadLeftButton => { + Capability::Touchpad(Touchpad::LeftPad(Touch::Button(TouchButton::Press))) + } + InputCapability::TouchpadCenterButton => { + Capability::Touchpad(Touchpad::CenterPad(Touch::Button(TouchButton::Press))) + } + InputCapability::TouchpadRightButton => { + Capability::Touchpad(Touchpad::RightPad(Touch::Button(TouchButton::Press))) + } + InputCapability::TouchscreenMotion => Capability::Touchscreen(Touch::Motion), + InputCapability::TouchscreenTopMotion => Capability::Touchscreen(Touch::Motion), + }; + if capability == Capability::NotImplemented { + log::warn!("Translation not implemented for: {value:?}"); + } + capability + } +} diff --git a/src/input/info.rs b/src/input/info.rs new file mode 100644 index 00000000..da4a218f --- /dev/null +++ b/src/input/info.rs @@ -0,0 +1,88 @@ +use crate::{network::websocket::WebsocketClient, udev::device::UdevDevice}; + +#[derive(Debug, Clone)] +pub enum DeviceInfo { + Udev(UdevDevice), + Websocket(WebsocketClient), +} + +impl DeviceInfo { + /// Name of the device + pub fn name(&self) -> String { + match self { + DeviceInfo::Udev(device) => device.name(), + DeviceInfo::Websocket(client) => client.addr.to_string(), + } + } + + /// Return a unique identifier for the device based on the subsystem and + /// sysname. E.g. "evdev://event3", "hidraw://hidraw0", "ws://127.0.0.1:8080::192.168.0.3:12345" + pub fn get_id(&self) -> String { + match self { + DeviceInfo::Udev(device) => device.get_id(), + DeviceInfo::Websocket(client) => client.get_id(), + } + } + + /// Path to the device (e.g. /dev/hidraw1, /dev/input/event12, ws://127.0.0.1:12345) + pub fn path(&self) -> String { + match self { + DeviceInfo::Udev(device) => device.devnode(), + DeviceInfo::Websocket(client) => format!("ws://{}", client.addr), + } + } + + /// The subsystem of the device if it is udev, or networking type + pub fn kind(&self) -> String { + match self { + DeviceInfo::Udev(device) => device.subsystem(), + DeviceInfo::Websocket(_) => "websocket".into(), + } + } +} + +impl From<::udev::Device> for DeviceInfo { + fn from(device: ::udev::Device) -> Self { + Self::Udev(device.into()) + } +} + +impl From for DeviceInfo { + fn from(device: UdevDevice) -> Self { + Self::Udev(device) + } +} + +impl From for DeviceInfo { + fn from(value: WebsocketClient) -> Self { + Self::Websocket(value) + } +} + +/// Reference to device information +#[derive(Debug, Clone)] +pub enum DeviceInfoRef<'a> { + Udev(&'a UdevDevice), + Websocket(&'a WebsocketClient), +} + +impl DeviceInfoRef<'_> { + pub fn to_owned(&self) -> DeviceInfo { + match self { + DeviceInfoRef::Udev(device) => DeviceInfo::Udev(device.to_owned().clone()), + DeviceInfoRef::Websocket(client) => DeviceInfo::Websocket(client.to_owned().clone()), + } + } +} + +impl<'a> From<&'a UdevDevice> for DeviceInfoRef<'a> { + fn from(device: &'a UdevDevice) -> Self { + Self::Udev(device) + } +} + +impl<'a> From<&'a WebsocketClient> for DeviceInfoRef<'a> { + fn from(value: &'a WebsocketClient) -> Self { + Self::Websocket(value) + } +} diff --git a/src/input/manager.rs b/src/input/manager.rs index 954c4754..8a754499 100644 --- a/src/input/manager.rs +++ b/src/input/manager.rs @@ -43,11 +43,13 @@ use crate::input::source::iio; use crate::input::source::led; use crate::input::target::TargetDevice; use crate::input::target::TargetDeviceTypeId; +use crate::network::websocket::{watch_websockets, WebsocketClient}; use crate::udev; use crate::udev::device::AttributeGetter; use crate::udev::device::UdevDevice; use super::composite_device::client::CompositeDeviceClient; +use super::info::DeviceInfo; use super::target::client::TargetDeviceClient; use crate::watcher; @@ -68,13 +70,13 @@ pub enum ManagerError { /// Manager commands define all the different ways to interact with [Manager] /// over a channel. These commands are processed in an asyncronous thread and /// dispatched as they come in. -#[derive(Debug, Clone)] +#[derive(Debug)] pub enum ManagerCommand { DeviceAdded { - device: UdevDevice, + device: DeviceInfo, }, DeviceRemoved { - device: UdevDevice, + device: DeviceInfo, }, CreateCompositeDevice { config: CompositeDeviceConfig, @@ -239,6 +241,8 @@ impl Manager { log::debug!("Starting input manager task..."); + tokio::spawn(watch_websockets(self.tx.clone())); + let _ = tokio::join!( Self::discover_all_devices(&cmd_tx_all_devices), Self::watch_iio_devices(self.tx.clone()), @@ -377,19 +381,32 @@ impl Manager { .collect(); log::info!("Gamepad order: {:?}", self.target_gamepad_order); } - ManagerCommand::DeviceAdded { device } => { - let dev_name = device.name(); - let dev_sysname = device.sysname(); + ManagerCommand::DeviceAdded { device } => match device { + DeviceInfo::Udev(device) => { + let dev_name = device.name(); + let dev_sysname = device.sysname(); - if let Err(e) = self.on_device_added(device).await { - log::error!("Error adding device '{dev_name} ({dev_sysname})': {e}"); + if let Err(e) = self.on_udev_device_added(device).await { + log::error!("Error adding device '{dev_name} ({dev_sysname})': {e}"); + } } - } - ManagerCommand::DeviceRemoved { device } => { - if let Err(e) = self.on_device_removed(device).await { - log::error!("Error removing device: {e}"); + DeviceInfo::Websocket(client) => { + if let Err(e) = self.on_websocket_device_added(client).await { + log::error!("Error adding websocket client: {e}"); + } } - } + }, + ManagerCommand::DeviceRemoved { device } => match device { + DeviceInfo::Udev(device) => { + if let Err(e) = self.on_udev_device_removed(device).await { + log::error!("Error removing device: {e}"); + } + } + DeviceInfo::Websocket(websocket_client) => { + // TODO: implement + log::error!("TODO: ADD REMOVE LOGIC FOR WEBSOCKETS"); + } + }, ManagerCommand::SetManageAllDevices(manage_all_devices) => { log::debug!("Setting management of all devices to: {manage_all_devices}"); if self.manage_all_devices == manage_all_devices { @@ -528,7 +545,7 @@ impl Manager { async fn create_composite_device_from_config( &mut self, config: &CompositeDeviceConfig, - device: UdevDevice, + device: DeviceInfo, ) -> Result> { // Lookup the capability map associated with this config if it exists let capability_map = if let Some(map_id) = config.capability_map_id.clone() { @@ -916,7 +933,7 @@ impl Manager { async fn on_source_device_added( &mut self, id: String, - device: UdevDevice, + device: DeviceInfo, ) -> Result<(), Box> { // Check all existing composite devices to see if this device is part of // their config @@ -997,7 +1014,7 @@ impl Manager { } } - log::info!("Found missing {} device, adding source device {id} to existing composite device: {composite_device:?}", device.subsystem()); + log::info!("Found missing {} device, adding source device {id} to existing composite device: {composite_device:?}", device.kind()); let Some(client) = self.composite_devices.get(composite_device.as_str()) else { log::error!("No existing composite device found for key {composite_device:?}"); continue; @@ -1060,7 +1077,7 @@ impl Manager { } log::info!( "Found a matching {} device {id}, creating CompositeDevice", - device.subsystem() + device.kind() ); let dev = self .create_composite_device_from_config(&config, device) @@ -1091,7 +1108,7 @@ impl Manager { /// Called when any source device is removed async fn on_source_device_removed( &mut self, - device: UdevDevice, + device: DeviceInfo, id: String, ) -> Result<(), Box> { let dev_name = device.name(); @@ -1128,8 +1145,22 @@ impl Manager { Ok(()) } + /// Called when a new network device has connected + async fn on_websocket_device_added( + &mut self, + client: WebsocketClient, + ) -> Result<(), Box> { + // TODO: Create a dbus interface for the network device + + log::debug!("Websocket client connected: {client:?}"); + let id = client.get_id(); + self.on_source_device_added(id, client.into()).await?; + + Ok(()) + } + /// Called when a new device is detected by udev - async fn on_device_added(&mut self, device: UdevDevice) -> Result<(), Box> { + async fn on_udev_device_added(&mut self, device: UdevDevice) -> Result<(), Box> { let dev_path = device.devpath(); let dev_name = device.name(); let dev_sysname = device.sysname(); @@ -1220,7 +1251,8 @@ impl Manager { // Signal that a source device was added log::debug!("Spawning task to add source device: {id}"); - self.on_source_device_added(id.clone(), device).await?; + self.on_source_device_added(id.clone(), device.into()) + .await?; log::debug!("Finished adding {id}"); } "hidraw" => { @@ -1318,7 +1350,8 @@ impl Manager { // 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?; + self.on_source_device_added(id.clone(), device.into()) + .await?; log::debug!("Finished adding hidraw device {id}"); } @@ -1368,7 +1401,8 @@ impl Manager { // 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?; + self.on_source_device_added(id.clone(), device.into()) + .await?; log::debug!("Finished adding event device {id}"); } @@ -1397,7 +1431,8 @@ impl Manager { } // 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?; + self.on_source_device_added(id.clone(), device.into()) + .await?; log::debug!("Finished adding LED device {id}"); } @@ -1409,7 +1444,7 @@ impl Manager { Ok(()) } - async fn on_device_removed(&mut self, device: UdevDevice) -> Result<(), Box> { + async fn on_udev_device_removed(&mut self, device: UdevDevice) -> Result<(), Box> { let dev_name = device.name(); let sys_name = device.sysname(); let subsystem = device.subsystem(); @@ -1467,7 +1502,7 @@ impl Manager { log::debug!("Device ID: {id}"); // Signal that a source device was removed - self.on_source_device_removed(device, id).await?; + self.on_source_device_removed(device.into(), id).await?; Ok(()) } @@ -1641,7 +1676,11 @@ impl Manager { WatchEvent::Delete { name, base_path } => { let device = UdevDevice::from_devnode(base_path.as_str(), name.as_str()); log::debug!("Got inotify remove action for {base_path}/{name}"); - let result = cmd_tx.send(ManagerCommand::DeviceRemoved { device }).await; + let result = cmd_tx + .send(ManagerCommand::DeviceRemoved { + device: device.into(), + }) + .await; if let Err(e) = result { log::error!("Unable to send command: {:?}", e); } @@ -1680,7 +1719,9 @@ impl Manager { ) -> Result<(), Box> { for device in devices { manager_tx - .send(ManagerCommand::DeviceAdded { device }) + .send(ManagerCommand::DeviceAdded { + device: device.into(), + }) .await?; } @@ -1804,7 +1845,7 @@ impl Manager { async fn add_device_to_composite_device( &self, - device: UdevDevice, + device: DeviceInfo, client: &CompositeDeviceClient, ) -> Result<(), Box> { client.add_source_device(device).await?; diff --git a/src/input/mod.rs b/src/input/mod.rs index b2e2445e..7a961771 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -2,6 +2,7 @@ pub mod capability; pub mod composite_device; pub mod event; +pub mod info; pub mod manager; pub mod output_capability; pub mod output_event; diff --git a/src/input/source/evdev.rs b/src/input/source/evdev.rs index 1b200c17..14b7f6da 100644 --- a/src/input/source/evdev.rs +++ b/src/input/source/evdev.rs @@ -10,7 +10,9 @@ use keyboard::KeyboardEventDevice; use touchscreen::TouchscreenEventDevice; use crate::{ - config, constants::BUS_SOURCES_PREFIX, input::composite_device::client::CompositeDeviceClient, + config, + constants::BUS_SOURCES_PREFIX, + input::{composite_device::client::CompositeDeviceClient, info::DeviceInfoRef}, udev::device::UdevDevice, }; @@ -36,7 +38,7 @@ pub enum EventDevice { } impl SourceDeviceCompatible for EventDevice { - fn get_device_ref(&self) -> &UdevDevice { + fn get_device_ref(&self) -> DeviceInfoRef { match self { EventDevice::Blocked(source_driver) => source_driver.info_ref(), EventDevice::Gamepad(source_driver) => source_driver.info_ref(), @@ -112,7 +114,7 @@ impl EventDevice { let source_device = SourceDriver::new_with_options( composite_device, device, - device_info, + device_info.into(), options, conf, ); @@ -120,7 +122,8 @@ impl EventDevice { } DriverType::Gamepad => { let device = GamepadEventDevice::new(device_info.clone())?; - let source_device = SourceDriver::new(composite_device, device, device_info, conf); + let source_device = + SourceDriver::new(composite_device, device, device_info.into(), conf); Ok(Self::Gamepad(source_device)) } DriverType::Touchscreen => { @@ -129,12 +132,14 @@ impl EventDevice { .and_then(|c| c.config.clone()) .and_then(|c| c.touchscreen); let device = TouchscreenEventDevice::new(device_info.clone(), touch_config)?; - let source_device = SourceDriver::new(composite_device, device, device_info, conf); + let source_device = + SourceDriver::new(composite_device, device, device_info.into(), conf); Ok(Self::Touchscreen(source_device)) } DriverType::Keyboard => { let device = KeyboardEventDevice::new(device_info.clone(), &conf)?; - let source_device = SourceDriver::new(composite_device, device, device_info, conf); + let source_device = + SourceDriver::new(composite_device, device, device_info.into(), conf); Ok(Self::Keyboard(source_device)) } } diff --git a/src/input/source/hidraw.rs b/src/input/source/hidraw.rs index 214790b8..879abbca 100644 --- a/src/input/source/hidraw.rs +++ b/src/input/source/hidraw.rs @@ -18,8 +18,11 @@ use rog_ally::RogAlly; use xpad_uhid::XpadUhid; use crate::{ - config, constants::BUS_SOURCES_PREFIX, drivers, - input::composite_device::client::CompositeDeviceClient, udev::device::UdevDevice, + config, + constants::BUS_SOURCES_PREFIX, + drivers, + input::{composite_device::client::CompositeDeviceClient, info::DeviceInfoRef}, + udev::device::UdevDevice, }; use self::{ @@ -66,7 +69,7 @@ pub enum HidRawDevice { } impl SourceDeviceCompatible for HidRawDevice { - fn get_device_ref(&self) -> &UdevDevice { + fn get_device_ref(&self) -> DeviceInfoRef { match self { HidRawDevice::DualSense(source_driver) => source_driver.info_ref(), HidRawDevice::Fts3528Touchscreen(source_driver) => source_driver.info_ref(), @@ -193,7 +196,7 @@ impl HidRawDevice { let source_device = SourceDriver::new_with_options( composite_device, device, - device_info, + device_info.into(), options, conf, ); @@ -208,7 +211,7 @@ impl HidRawDevice { let source_device = SourceDriver::new_with_options( composite_device, device, - device_info, + device_info.into(), options, conf, ); @@ -216,42 +219,50 @@ impl HidRawDevice { } DriverType::LegionGoDCombined => { let device = LegionControllerDCombined::new(device_info.clone())?; - let source_device = SourceDriver::new(composite_device, device, device_info, conf); + let source_device = + SourceDriver::new(composite_device, device, device_info.into(), conf); Ok(Self::LegionGoDCombined(source_device)) } DriverType::LegionGoDSplit => { let device = LegionControllerDSplit::new(device_info.clone())?; - let source_device = SourceDriver::new(composite_device, device, device_info, conf); + let source_device = + SourceDriver::new(composite_device, device, device_info.into(), conf); Ok(Self::LegionGoDSplit(source_device)) } DriverType::LegionGoFPS => { let device = LegionControllerFPS::new(device_info.clone())?; - let source_device = SourceDriver::new(composite_device, device, device_info, conf); + let source_device = + SourceDriver::new(composite_device, device, device_info.into(), conf); Ok(Self::LegionGoFPS(source_device)) } DriverType::LegionGoX => { let device = LegionControllerX::new(device_info.clone())?; - let source_device = SourceDriver::new(composite_device, device, device_info, conf); + let source_device = + SourceDriver::new(composite_device, device, device_info.into(), conf); Ok(Self::LegionGoX(source_device)) } DriverType::LegionGoS => { let device = LegionSController::new(device_info.clone())?; - let source_device = SourceDriver::new(composite_device, device, device_info, conf); + let source_device = + SourceDriver::new(composite_device, device, device_info.into(), conf); Ok(Self::LegionGoS(source_device)) } DriverType::OrangePiNeo => { let device = OrangePiNeoTouchpad::new(device_info.clone())?; - let source_device = SourceDriver::new(composite_device, device, device_info, conf); + let source_device = + SourceDriver::new(composite_device, device, device_info.into(), conf); Ok(Self::OrangePiNeo(source_device)) } DriverType::Fts3528Touchscreen => { let device = Fts3528Touchscreen::new(device_info.clone())?; - let source_device = SourceDriver::new(composite_device, device, device_info, conf); + let source_device = + SourceDriver::new(composite_device, device, device_info.into(), conf); Ok(Self::Fts3528Touchscreen(source_device)) } DriverType::XpadUhid => { let device = XpadUhid::new(device_info.clone())?; - let source_device = SourceDriver::new(composite_device, device, device_info, conf); + let source_device = + SourceDriver::new(composite_device, device, device_info.into(), conf); Ok(Self::XpadUhid(source_device)) } DriverType::RogAlly => { @@ -263,7 +274,7 @@ impl HidRawDevice { let source_device = SourceDriver::new_with_options( composite_device, device, - device_info, + device_info.into(), options, conf, ); @@ -271,7 +282,8 @@ impl HidRawDevice { } DriverType::HoripadSteam => { let device = HoripadSteam::new(device_info.clone())?; - let source_device = SourceDriver::new(composite_device, device, device_info, conf); + let source_device = + SourceDriver::new(composite_device, device, device_info.into(), conf); Ok(Self::HoripadSteam(source_device)) } } diff --git a/src/input/source/iio.rs b/src/input/source/iio.rs index c2d2629b..1f96da4c 100644 --- a/src/input/source/iio.rs +++ b/src/input/source/iio.rs @@ -8,7 +8,9 @@ use std::error::Error; use glob_match::glob_match; use crate::{ - config, constants::BUS_SOURCES_PREFIX, input::composite_device::client::CompositeDeviceClient, + config, + constants::BUS_SOURCES_PREFIX, + input::{composite_device::client::CompositeDeviceClient, info::DeviceInfoRef}, udev::device::UdevDevice, }; @@ -31,7 +33,7 @@ pub enum IioDevice { } impl SourceDeviceCompatible for IioDevice { - fn get_device_ref(&self) -> &UdevDevice { + fn get_device_ref(&self) -> DeviceInfoRef { match self { IioDevice::BmiImu(source_driver) => source_driver.info_ref(), IioDevice::AccelGryo3D(source_driver) => source_driver.info_ref(), @@ -92,12 +94,14 @@ impl IioDevice { DriverType::Unknown => Err("No driver for iio interface found".into()), DriverType::BmiImu => { let device = BmiImu::new(device_info.clone(), iio_config)?; - let source_device = SourceDriver::new(composite_device, device, device_info, conf); + let source_device = + SourceDriver::new(composite_device, device, device_info.into(), conf); Ok(Self::BmiImu(source_device)) } DriverType::AccelGryo3D => { let device = AccelGyro3dImu::new(device_info.clone(), iio_config)?; - let source_device = SourceDriver::new(composite_device, device, device_info, conf); + let source_device = + SourceDriver::new(composite_device, device, device_info.into(), conf); Ok(Self::AccelGryo3D(source_device)) } } diff --git a/src/input/source/led.rs b/src/input/source/led.rs index b5b662c5..c843d873 100644 --- a/src/input/source/led.rs +++ b/src/input/source/led.rs @@ -2,7 +2,9 @@ pub mod multicolor_chassis; use self::multicolor_chassis::MultiColorChassis; use super::{SourceDeviceCompatible, SourceDriver}; use crate::{ - config, constants::BUS_SOURCES_PREFIX, input::composite_device::client::CompositeDeviceClient, + config, + constants::BUS_SOURCES_PREFIX, + input::{composite_device::client::CompositeDeviceClient, info::DeviceInfoRef}, udev::device::UdevDevice, }; use std::error::Error; @@ -17,7 +19,7 @@ pub enum LedDevice { } impl SourceDeviceCompatible for LedDevice { - fn get_device_ref(&self) -> &UdevDevice { + fn get_device_ref(&self) -> DeviceInfoRef { match self { LedDevice::MultiColorChassis(source_driver) => source_driver.info_ref(), } @@ -79,7 +81,8 @@ impl LedDevice { None => None, }, )?; - let source_device = SourceDriver::new(composite_device, device, device_info, conf); + let source_device = + SourceDriver::new(composite_device, device, device_info.into(), conf); Ok(Self::MultiColorChassis(source_device)) } } diff --git a/src/input/source/mod.rs b/src/input/source/mod.rs index 612b8de6..a1c68977 100644 --- a/src/input/source/mod.rs +++ b/src/input/source/mod.rs @@ -9,10 +9,11 @@ use std::{ use ::evdev::FFEffectData; use led::LedDevice; +use network::NetworkDevice; use thiserror::Error; use tokio::sync::mpsc::{self, error::TryRecvError}; -use crate::{config, udev::device::UdevDevice}; +use crate::config; use self::{ client::SourceDeviceClient, command::SourceCommand, evdev::EventDevice, hidraw::HidRawDevice, @@ -23,6 +24,7 @@ use super::{ capability::Capability, composite_device::client::CompositeDeviceClient, event::{native::NativeEvent, Event}, + info::{DeviceInfo, DeviceInfoRef}, output_event::OutputEvent, }; @@ -32,6 +34,7 @@ pub mod evdev; pub mod hidraw; pub mod iio; pub mod led; +pub mod network; /// Size of the [SourceCommand] buffer for receiving output events const BUFFER_SIZE: usize = 2048; @@ -178,7 +181,7 @@ pub struct SourceDriver { event_include_list: HashSet, event_exclude_list: HashSet, implementation: Arc>, - device_info: UdevDevice, + device_info: DeviceInfo, composite_device: CompositeDeviceClient, tx: mpsc::Sender, rx: mpsc::Receiver, @@ -189,7 +192,7 @@ impl SourceDriver pub fn new( composite_device: CompositeDeviceClient, device: T, - device_info: UdevDevice, + device_info: DeviceInfo, config: Option, ) -> Self { let options = SourceDriverOptions::default(); @@ -200,7 +203,7 @@ impl SourceDriver pub fn new_with_options( composite_device: CompositeDeviceClient, device: T, - device_info: UdevDevice, + device_info: DeviceInfo, options: SourceDriverOptions, config: Option, ) -> Self { @@ -233,7 +236,7 @@ impl SourceDriver } let event_filter_enabled = !events_exclude.is_empty() || !events_include.is_empty(); if event_filter_enabled { - let devnode = device_info.devnode(); + let devnode = device_info.path(); if !events_include.is_empty() { log::debug!("Source device '{devnode}' filter includes events: {events_include:?}"); } @@ -313,7 +316,7 @@ impl SourceDriver /// Returns the path to the device (e.g. "/dev/input/event0") pub fn get_device_path(&self) -> String { - self.device_info.devnode() + self.device_info.path() } /// Returns a transmitter channel that can be used to send events to this device @@ -322,8 +325,11 @@ impl SourceDriver } /// Returns udev device information about the device as a reference - pub fn info_ref(&self) -> &UdevDevice { - &self.device_info + pub fn info_ref(&self) -> DeviceInfoRef { + match &self.device_info { + DeviceInfo::Udev(device) => device.into(), + DeviceInfo::Websocket(client) => client.into(), + } } /// Run the source device, consuming the device. @@ -445,7 +451,7 @@ impl SourceDriver pub(crate) trait SourceDeviceCompatible { /// Returns a copy of the UdevDevice - fn get_device_ref(&self) -> &UdevDevice; + fn get_device_ref(&self) -> DeviceInfoRef; /// Returns a unique identifier for the source device. fn get_id(&self) -> String; @@ -470,16 +476,18 @@ pub enum SourceDevice { HidRaw(HidRawDevice), Iio(IioDevice), Led(LedDevice), + Network(NetworkDevice), } impl SourceDevice { /// Returns a copy of the UdevDevice - pub fn get_device_ref(&self) -> &UdevDevice { + pub fn get_device_ref(&self) -> DeviceInfoRef { match self { 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(), + SourceDevice::Network(device) => device.get_device_ref(), } } @@ -490,6 +498,7 @@ impl SourceDevice { SourceDevice::HidRaw(device) => device.get_id(), SourceDevice::Iio(device) => device.get_id(), SourceDevice::Led(device) => device.get_id(), + SourceDevice::Network(device) => device.get_id(), } } @@ -500,6 +509,7 @@ impl SourceDevice { SourceDevice::HidRaw(device) => device.client(), SourceDevice::Iio(device) => device.client(), SourceDevice::Led(device) => device.client(), + SourceDevice::Network(device) => device.client(), } } @@ -510,6 +520,7 @@ impl SourceDevice { SourceDevice::HidRaw(device) => device.run().await, SourceDevice::Iio(device) => device.run().await, SourceDevice::Led(device) => device.run().await, + SourceDevice::Network(device) => device.run().await, } } @@ -520,6 +531,7 @@ impl SourceDevice { SourceDevice::HidRaw(device) => device.get_capabilities(), SourceDevice::Iio(device) => device.get_capabilities(), SourceDevice::Led(device) => device.get_capabilities(), + SourceDevice::Network(device) => device.get_capabilities(), } } @@ -530,6 +542,7 @@ impl SourceDevice { SourceDevice::HidRaw(device) => device.get_device_path(), SourceDevice::Iio(device) => device.get_device_path(), SourceDevice::Led(device) => device.get_device_path(), + SourceDevice::Network(device) => device.get_device_path(), } } } diff --git a/src/input/source/network.rs b/src/input/source/network.rs index e69de29b..f9a29fd1 100644 --- a/src/input/source/network.rs +++ b/src/input/source/network.rs @@ -0,0 +1,70 @@ +use std::error::Error; + +use websockets::WebsocketDevice; + +use crate::{ + config, + input::{composite_device::client::CompositeDeviceClient, info::DeviceInfoRef}, + network::websocket::WebsocketClient, +}; + +use super::{SourceDeviceCompatible, SourceDriver}; + +pub mod websockets; + +#[derive(Debug)] +pub enum NetworkDevice { + Websocket(SourceDriver), +} + +impl NetworkDevice { + pub fn new( + device_info: WebsocketClient, + composite_device: CompositeDeviceClient, + conf: Option, + ) -> Result> { + let device = WebsocketDevice::new(device_info.clone())?; + let source_device = SourceDriver::new(composite_device, device, device_info.into(), conf); + Ok(Self::Websocket(source_device)) + } +} + +impl SourceDeviceCompatible for NetworkDevice { + fn get_device_ref(&self) -> DeviceInfoRef { + match self { + Self::Websocket(source_driver) => source_driver.info_ref(), + } + } + + fn get_id(&self) -> String { + match self { + Self::Websocket(source_driver) => source_driver.get_id(), + } + } + + fn client(&self) -> super::client::SourceDeviceClient { + match self { + Self::Websocket(source_driver) => source_driver.client(), + } + } + + async fn run(self) -> Result<(), Box> { + match self { + Self::Websocket(source_driver) => source_driver.run().await, + } + } + + fn get_capabilities( + &self, + ) -> Result, super::InputError> { + match self { + Self::Websocket(source_driver) => source_driver.get_capabilities(), + } + } + + fn get_device_path(&self) -> String { + match self { + Self::Websocket(source_driver) => source_driver.get_device_path(), + } + } +} diff --git a/src/input/source/network/websockets.rs b/src/input/source/network/websockets.rs new file mode 100644 index 00000000..7b13989d --- /dev/null +++ b/src/input/source/network/websockets.rs @@ -0,0 +1,218 @@ +use packed_struct::prelude::*; +use std::error::Error; + +use futures::stream::SplitSink; +use tokio::{ + net::TcpStream, + sync::mpsc::{self, Receiver}, +}; +use tokio_tungstenite::{tungstenite::Message, WebSocketStream}; + +use crate::{ + drivers::unified_gamepad::{ + event::Event, + reports::{ + input_capability_report::InputCapabilityReport, input_data_report::InputDataReport, + ReportType, UNIFIED_SPEC_VERSION_MAJOR, UNIFIED_SPEC_VERSION_MINOR, + }, + }, + input::{ + capability::Capability, + event::native::NativeEvent, + source::{InputError, SourceInputDevice, SourceOutputDevice}, + }, + network::websocket::{SplitStream, WebsocketClient}, +}; + +/// [WebsocketDevice] is a [SourceInputDevice] implementation that reads input +/// events over a websocket stream. +#[derive(Debug)] +pub struct WebsocketDevice { + capabilities: Option, + state: Option, + stream_read: Option>, + stream_write: Option, Message>>, + open_rx: Option>, +} + +impl WebsocketDevice { + pub fn new(client: WebsocketClient) -> Result> { + log::info!("Starting websocket source device"); + let (open_tx, open_rx) = mpsc::channel(2); + let client_clone = client.clone(); + tokio::task::spawn(async move { + log::debug!("Opening websocket stream"); + let Some(stream) = client_clone.open().await else { + log::error!("Websocket stream was already opened"); + return; + }; + if let Err(e) = open_tx.send(stream).await { + log::error!("Failed to send opened stream to running source device: {e}"); + } + }); + + Ok(Self { + capabilities: None, + state: None, + stream_read: None, + stream_write: None, + open_rx: Some(open_rx), + }) + } + + fn handle_report( + &mut self, + buf: &[u8], + _bytes_read: usize, + ) -> Result, Box> { + if buf.len() < 3 { + log::debug!("Invalid report data"); + return Ok(vec![]); + } + + // The first and second bytes contain the specification version + let major_ver = buf[0]; + let minor_ver = buf[1]; + + // The major version indicates there are breaking changes. + if major_ver != UNIFIED_SPEC_VERSION_MAJOR { + return Err(format!("Device major version (v{major_ver}) is not compatible with this implementation (v{UNIFIED_SPEC_VERSION_MAJOR})").into()); + } + // Minor versions are backwards compatible + if minor_ver > UNIFIED_SPEC_VERSION_MINOR { + return Err(format!("Device minor version (v{minor_ver}) is newer than this implementation supports (v{UNIFIED_SPEC_VERSION_MINOR})").into()); + } + + let report_type = ReportType::from(buf[2]); + log::trace!("Received report: {report_type:?}"); + match report_type { + ReportType::Unknown => (), + ReportType::InputCapabilityReport => { + let report = InputCapabilityReport::unpack(buf)?; + self.capabilities = Some(report); + // If the capabilities change, zero out the old state + self.state = None; + } + ReportType::InputDataReport => { + return self.handle_input_report(buf); + } + ReportType::OutputCapabilityReport => (), + ReportType::OutputDataReport => (), + } + + Ok(vec![]) + } + + fn handle_input_report( + &mut self, + buf: &[u8], + ) -> Result, Box> { + let buffer = buf.try_into()?; + let report = InputDataReport::unpack(buffer)?; + let old_state = self.state.take(); + self.state = Some(report); + + let Some(old_state) = old_state.as_ref() else { + return Ok(vec![]); + }; + let Some(state) = self.state.as_ref() else { + return Ok(vec![]); + }; + if state.state_version == old_state.state_version { + return Ok(vec![]); + } + let Some(capabilities) = self.capabilities.as_ref() else { + return Ok(vec![]); + }; + + let old_values = capabilities.decode_data_report(old_state)?; + let values = capabilities.decode_data_report(state)?; + let values_iter = old_values.iter().zip(values.iter()); + + let mut events = Vec::new(); + for (info, (old_value, value)) in capabilities.get_capabilities().iter().zip(values_iter) { + if old_value == value { + continue; + } + let capability = info.capability; + let event = Event { + capability, + value: value.to_owned(), + }; + events.push(event); + } + + Ok(events) + } +} + +impl SourceInputDevice for WebsocketDevice { + /// Poll the given input device for input events + fn poll(&mut self) -> Result, InputError> { + // Open the websocket stream if it's not already + if let Some(mut open_rx) = self.open_rx.take() { + if open_rx.is_closed() && open_rx.is_empty() { + log::error!("Channel to open stream is already closed"); + return Err(InputError::DeviceError("Failed to open stream".into())); + } + if open_rx.is_empty() { + self.open_rx = Some(open_rx); + return Ok(vec![]); + } + let Some(stream) = open_rx.blocking_recv() else { + log::error!("No stream found after opening"); + return Ok(vec![]); + }; + let (writer, reader) = stream; + self.stream_read = Some(reader); + self.stream_write = Some(writer); + log::debug!("Successfully opened websocket stream"); + } + + // Read all messages from the stream + let msgs = { + let Some(reader) = self.stream_read.as_mut() else { + log::debug!("No stream receiver exists!"); + return Ok(vec![]); + }; + if reader.is_closed() { + log::debug!("Stream is closed!"); + return Err(InputError::DeviceError("Stream closed".into())); + } + if reader.is_empty() { + return Ok(vec![]); + } + + // Read messages from the socket + let mut messages = vec![]; + while !(reader.is_empty()) { + let Some(msg) = reader.blocking_recv() else { + break; + }; + messages.push(msg); + } + messages + }; + + // Handle each message as an HID report + let mut events = Vec::with_capacity(msgs.len()); + for msg in msgs { + let data = msg.into_data(); + let websocket_events = self + .handle_report(&data, data.len())? + .into_iter() + .map(|e| e.into()); + events.extend(websocket_events); + } + log::trace!("Got events: {events:?}"); + + Ok(events) + } + + /// Returns the possible input events this device is capable of emitting + fn get_capabilities(&self) -> Result, InputError> { + Ok(vec![]) + } +} + +impl SourceOutputDevice for WebsocketDevice {} diff --git a/src/input/target/mod.rs b/src/input/target/mod.rs index 04f6e34f..8c4d08d0 100644 --- a/src/input/target/mod.rs +++ b/src/input/target/mod.rs @@ -11,6 +11,7 @@ use horipad_steam::HoripadSteamDevice; use thiserror::Error; use tokio::sync::mpsc::{self, error::TryRecvError}; use unified_gamepad::UnifiedGamepadDevice; +use websocket::WebsocketDevice; use crate::dbus::interface::target::{gamepad::TargetGamepadInterface, TargetInterface}; @@ -52,6 +53,7 @@ pub mod steam_deck; pub mod touchpad; pub mod touchscreen; pub mod unified_gamepad; +pub mod websocket; pub mod xb360; pub mod xbox_elite; pub mod xbox_series; @@ -216,6 +218,10 @@ impl TargetDeviceTypeId { id: "debug", name: "Debug Device", }, + TargetDeviceTypeId { + id: "websocket", + name: "Websocket Device", + }, ] } @@ -233,7 +239,14 @@ impl TargetDeviceTypeId { pub fn is_gamepad(&self) -> bool { !matches!( self.id, - "dbus" | "null" | "touchscreen" | "touchpad" | "mouse" | "keyboard" | "debug" + "dbus" + | "null" + | "touchscreen" + | "touchpad" + | "mouse" + | "keyboard" + | "debug" + | "websocket" ) } } @@ -600,6 +613,7 @@ pub enum TargetDevice { XBoxElite(TargetDriver), XBoxSeries(TargetDriver), UnifiedGamepad(TargetDriver), + Websocket(TargetDriver), } impl TargetDevice { @@ -713,6 +727,11 @@ impl TargetDevice { let driver = TargetDriver::new(id, device, dbus); Ok(Self::UnifiedGamepad(driver)) } + "websocket" => { + let device = WebsocketDevice::new(dbus.clone()); + let driver = TargetDriver::new(id, device, dbus); + Ok(Self::Websocket(driver)) + } "null" => Ok(Self::Null), _ => Ok(Self::Null), } @@ -746,6 +765,7 @@ impl TargetDevice { TargetDevice::XBoxElite(_) => vec!["xbox-elite".try_into().unwrap()], TargetDevice::XBoxSeries(_) => vec!["xbox-series".try_into().unwrap()], TargetDevice::UnifiedGamepad(_) => vec!["unified-gamepad".try_into().unwrap()], + TargetDevice::Websocket(_) => vec!["websocket".try_into().unwrap()], } } @@ -768,6 +788,7 @@ impl TargetDevice { TargetDevice::XBoxElite(_) => "gamepad", TargetDevice::XBoxSeries(_) => "gamepad", TargetDevice::UnifiedGamepad(_) => "gamepad", + TargetDevice::Websocket(_) => "websocket", } } @@ -788,6 +809,7 @@ impl TargetDevice { TargetDevice::XBoxElite(device) => Some(device.client()), TargetDevice::XBoxSeries(device) => Some(device.client()), TargetDevice::UnifiedGamepad(device) => Some(device.client()), + TargetDevice::Websocket(device) => Some(device.client()), } } @@ -808,6 +830,7 @@ impl TargetDevice { TargetDevice::XBoxElite(device) => device.run(dbus_path).await, TargetDevice::XBoxSeries(device) => device.run(dbus_path).await, TargetDevice::UnifiedGamepad(device) => device.run(dbus_path).await, + TargetDevice::Websocket(device) => device.run(dbus_path).await, } } } diff --git a/src/input/target/websocket.rs b/src/input/target/websocket.rs new file mode 100644 index 00000000..0804021d --- /dev/null +++ b/src/input/target/websocket.rs @@ -0,0 +1,348 @@ +use std::{collections::HashSet, error::Error, fmt::Debug}; + +use futures::SinkExt; +use packed_struct::prelude::*; +use tokio::sync::mpsc::{self, error::TryRecvError, Receiver}; +use tokio_tungstenite::tungstenite::Message; +use zbus::Connection; + +use crate::{ + dbus::interface::target::{websocket::TargetWebsocketInterface, TargetInterface}, + drivers::unified_gamepad::reports::{ + input_capability_report::{InputCapabilityInfo, InputCapabilityReport}, + input_data_report::InputDataReport, + }, + input::{ + capability::Capability, composite_device::client::CompositeDeviceClient, + event::native::NativeEvent, output_capability::OutputCapability, output_event::OutputEvent, + }, +}; + +use super::{ + client::TargetDeviceClient, InputError, OutputError, TargetDeviceTypeId, TargetInputDevice, + TargetOutputDevice, +}; + +/// Commands that can be sent from the dbus interface +pub enum DeviceCommand { + WebsocketConnected, +} + +/// A [WebsocketDevice] implements the Unified Controller Input Specification but writes +/// to a websocket server instead of an hidraw device. +pub struct WebsocketDevice { + conn: Connection, + dbus_path: Option, + dbus_rx: Option>, + composite_device: Option, + capabilities: HashSet, + capabilities_rx: Option>>, + capability_report: InputCapabilityReport, + state: InputDataReport, +} + +impl WebsocketDevice { + /// Create a new [WebsocketDevice] + pub fn new(conn: Connection) -> Self { + Self { + conn, + dbus_path: None, + dbus_rx: None, + composite_device: None, + capabilities: HashSet::new(), + capabilities_rx: None, + capability_report: InputCapabilityReport::default(), + state: InputDataReport::default(), + } + } + + /// Checks to see if new capabilities are available in the capabilities channel + fn receive_new_capabilities(&mut self) -> Option> { + let rx = self.capabilities_rx.as_mut()?; + + match rx.try_recv() { + Ok(capabilities) => Some(capabilities), + Err(e) => match e { + TryRecvError::Empty => None, + TryRecvError::Disconnected => { + self.capabilities_rx = None; + None + } + }, + } + } + + /// Send the capability report to the connected websocket server + fn send_capability_report(&self) { + // Signal that capabilities have changed + let capability_report = self.capability_report.clone(); + let Some(dbus_path) = self.dbus_path.clone() else { + log::warn!("No dbus interface exists with websocket stream"); + return; + }; + let conn = self.conn.clone(); + tokio::task::spawn(async move { + log::debug!("Sending capability report"); + // Pack the capability report to bytes + let capability_report = match capability_report.pack_to_vec() { + Ok(report) => report, + Err(e) => { + log::error!("Failed to pack capability report: {e}"); + return; + } + }; + + // Get the object instance at the given path so we can send DBus signal + // updates + let iface_ref = match conn + .object_server() + .interface::<_, TargetWebsocketInterface>(dbus_path.clone()) + .await + { + Ok(iface) => iface, + Err(e) => { + log::error!( + "Failed to get DBus interface for websocket device to signal: {e:?}" + ); + return; + } + }; + let mut iface = iface_ref.get_mut().await; + let Some(ws_conn) = iface.connection.as_mut() else { + log::debug!("No websocket connection exists to send capability report"); + return; + }; + let data = Message::binary(capability_report); + if let Err(e) = ws_conn.send(data).await { + log::error!("Failed to send capability report: {e}"); + } + }); + } + + /// Update the device capabilities with the given capabilities + fn update_capabilities(&mut self, capabilities: HashSet) { + log::debug!("Updating device capabilities with: {capabilities:?}"); + let Some(composite_device) = self.composite_device.as_ref() else { + log::warn!("No composite device set to update capabilities"); + return; + }; + + // Set the capabilities of the device + self.capabilities = capabilities.clone(); + + // Update the capability report with the source capabilities + let mut cap_info: Vec = capabilities + .clone() + .into_iter() + .map(|cap| cap.into()) + .collect(); + cap_info.sort_by_key(|cap| cap.value_type.order_priority()); + + // Update the capability report + self.capability_report = InputCapabilityReport::default(); + for info in cap_info { + log::trace!("Updating report with info: {info}"); + if let Err(e) = self.capability_report.add_capability(info) { + log::warn!("Failed to add input capability for gamepad: {e}"); + } + } + log::debug!("Using capability report: {}", self.capability_report); + + // Inform the composite device that the capabilities have changed + if let Some(dbus_path) = self.dbus_path.as_ref() { + log::debug!("Updating composite device with new capabilities"); + if let Err(e) = composite_device + .blocking_update_target_capabilities(dbus_path.clone(), capabilities) + { + log::warn!("Failed to update target capabilities: {e:?}"); + } + } + + // Signal that capabilities have changed + self.send_capability_report(); + + log::debug!("Updated capabilities"); + } + + /// Write the current device state to the virtual device + fn write_state(&mut self) -> Result<(), Box> { + let Some(dbus_path) = self.dbus_path.clone() else { + return Ok(()); + }; + let conn = self.conn.clone(); + + // Write the state to the dbus interface + let data = self.state.pack()?; + tokio::task::spawn(async move { + let data = Message::binary(data.to_vec()); + // Get the object instance at the given path so we can send DBus signal + // updates + let iface_ref = match conn + .object_server() + .interface::<_, TargetWebsocketInterface>(dbus_path.clone()) + .await + { + Ok(iface) => iface, + Err(e) => { + log::error!("Failed to get DBus interface for debug device to signal: {e:?}"); + return; + } + }; + let mut iface = iface_ref.get_mut().await; + let Some(ws_conn) = iface.connection.as_mut() else { + return; + }; + if let Err(e) = ws_conn.send(data).await { + log::error!("Failed to send state update: {e}"); + } + }); + + Ok(()) + } +} + +impl TargetInputDevice for WebsocketDevice { + /// Start the DBus interface for this target device + fn start_dbus_interface( + &mut self, + dbus: Connection, + path: String, + client: TargetDeviceClient, + type_id: TargetDeviceTypeId, + ) { + log::debug!("Starting dbus interface: {path}"); + log::trace!("Using device client: {client:?}"); + let (tx, rx) = mpsc::channel(128); + self.dbus_path = Some(path.clone()); + self.dbus_rx = Some(rx); + tokio::task::spawn(async move { + let generic_interface = TargetInterface::new(&type_id); + let iface = TargetWebsocketInterface::new(tx); + + let object_server = dbus.object_server(); + let (gen_result, result) = tokio::join!( + object_server.at(path.clone(), generic_interface), + object_server.at(path.clone(), iface) + ); + + if gen_result.is_err() || result.is_err() { + log::debug!("Failed to start dbus interface: {path} generic: {gen_result:?} type-specific: {result:?}"); + } else { + log::debug!("Started dbus interface: {path}"); + } + }); + } + + fn stop_dbus_interface(&mut self, dbus: Connection, path: String) { + log::debug!("Stopping dbus interface for {path}"); + tokio::task::spawn(async move { + let object_server = dbus.object_server(); + let (target, generic) = tokio::join!( + object_server.remove::(path.clone()), + object_server.remove::(path.clone()) + ); + if generic.is_err() || target.is_err() { + if let Err(err) = target { + log::debug!("Failed to stop debug interface {path}: {err}"); + } + if let Err(err) = generic { + log::debug!("Failed to stop target interface {path}: {err}"); + } + } else { + log::debug!("Stopped dbus interface for {path}"); + } + }); + } + + fn on_composite_device_attached( + &mut self, + composite_device: CompositeDeviceClient, + ) -> Result<(), InputError> { + self.composite_device = Some(composite_device.clone()); + + // Spawn a task to asyncronously fetch the source capabilities of + // the composite device. + let (tx, rx) = mpsc::channel(1); + tokio::task::spawn(async move { + log::debug!("Getting capabilities from the composite device!"); + let capabilities = match composite_device.get_capabilities().await { + Ok(caps) => caps, + Err(e) => { + log::warn!("Failed to fetch composite device capabilities: {e:?}"); + return; + } + }; + if let Err(e) = tx.send(capabilities).await { + log::warn!("Failed to send composite device capabilities: {e:?}"); + } + }); + + // Keep a reference to the receiver so it can be checked every poll iteration + self.capabilities_rx = Some(rx); + + Ok(()) + } + + fn write_event(&mut self, event: NativeEvent) -> Result<(), InputError> { + log::trace!("Received event: {event:?}"); + + // Update the internal controller state when events are emitted. + if let Err(e) = self.state.update(&self.capability_report, event.into()) { + log::warn!("Failed to update gamepad state: {e}"); + log::warn!("Current capability report: {}", self.capability_report); + } + + // Write the current state + self.write_state()?; + + Ok(()) + } + + fn get_capabilities(&self) -> Result, InputError> { + // Get the input capabilities from the source device(s) + let capabilities = Vec::from_iter(self.capabilities.iter().cloned()); + Ok(capabilities) + } +} + +impl TargetOutputDevice for WebsocketDevice { + fn poll( + &mut self, + _composite_device: &Option, + ) -> Result, OutputError> { + // Check to see if there are any capability updates + if let Some(new_capabilities) = self.receive_new_capabilities() { + self.update_capabilities(new_capabilities); + } + + // Check for dbus events + if let Some(rx) = self.dbus_rx.as_mut() { + if !rx.is_empty() { + let Some(cmd) = rx.blocking_recv() else { + return Err(OutputError::DeviceError("Dbus channel was closed".into())); + }; + match cmd { + DeviceCommand::WebsocketConnected => { + log::debug!("Connected to websocket server"); + self.send_capability_report(); + } + } + } + } + + Ok(vec![]) + } + + fn get_output_capabilities(&self) -> Result, OutputError> { + // TODO: Get the output capabilities from the source device(s) + Ok(vec![]) + } +} + +impl Debug for WebsocketDevice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DebugDevice") + .field("state", &self.state) + .finish() + } +} diff --git a/src/lib.rs b/src/lib.rs index 61bb101f..fcd7c4b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,5 +7,6 @@ pub mod dmi; pub mod drivers; pub mod iio; pub mod input; +pub mod network; pub mod udev; pub mod watcher; diff --git a/src/main.rs b/src/main.rs index e502931a..11926b8f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,7 @@ mod dmi; mod drivers; mod iio; mod input; +mod network; mod udev; mod watcher; diff --git a/src/network/mod.rs b/src/network/mod.rs new file mode 100644 index 00000000..6eba44d9 --- /dev/null +++ b/src/network/mod.rs @@ -0,0 +1 @@ +pub mod websocket; diff --git a/src/network/websocket.rs b/src/network/websocket.rs new file mode 100644 index 00000000..1b0e32c7 --- /dev/null +++ b/src/network/websocket.rs @@ -0,0 +1,216 @@ +use std::net::SocketAddr; + +use futures::{stream::SplitSink, StreamExt}; +use tokio::{ + fs, + net::{TcpListener, TcpStream}, + sync::mpsc::{self, Receiver, Sender}, +}; +use tokio_tungstenite::{tungstenite::Message, WebSocketStream}; + +use crate::{ + config::{path::get_devices_paths, CompositeDeviceConfig, Websocket}, + input::manager::ManagerCommand, +}; + +/// Write/Read halfs of a websocket stream +pub type SplitStream = ( + SplitSink, Message>, + Receiver, +); +pub type StreamRequestor = Sender>>; + +/// Websocket client information and channels +#[derive(Debug, Clone)] +pub struct WebsocketClient { + pub addr: SocketAddr, + pub server_addr: SocketAddr, + tx: StreamRequestor, +} + +impl WebsocketClient { + /// Create a new [WebsocketClient] instance + pub fn new(addr: SocketAddr, server_addr: SocketAddr, tx: StreamRequestor) -> Self { + Self { + addr, + server_addr, + tx, + } + } + + /// Return a unique identifier for this websocket client connection. E.g. + /// "ws://127.0.0.1:8080::192.168.0.2:12345" + pub fn get_id(&self) -> String { + format!( + "ws://{}:{}::{}:{}", + self.server_addr.ip(), + self.server_addr.port(), + self.addr.ip(), + self.addr.port() + ) + } + + /// Open the [WebSocketStream] associated with this client. This will return + /// `None` if the stream is unavailable or has already been opened. + pub async fn open(&self) -> Option { + let (tx, mut rx) = mpsc::channel(1); + self.tx.send(tx).await.ok()?; + rx.recv().await? + } +} + +pub async fn watch_websockets(cmd_tx: Sender) { + // Look at all composite device configs for any websocket configurations + let mut websocket_configs = vec![]; + let paths = get_devices_paths(); + for path in paths.iter() { + log::trace!("Checking {path:?} for websocket config"); + let Ok(mut files) = fs::read_dir(path).await else { + continue; + }; + + while let Ok(Some(entry)) = files.next_entry().await { + let filename = entry.file_name().to_string_lossy().to_string(); + if !filename.ends_with(".yaml") { + continue; + } + + let Ok(config) = + CompositeDeviceConfig::from_yaml_file(entry.path().display().to_string()) + else { + continue; + }; + + // Look for any 'websocket' config entries + for source_config in config.source_devices { + let Some(websocket_config) = source_config.websocket else { + continue; + }; + websocket_configs.push(websocket_config); + } + } + } + + // Spawn a websocket listener for each websocket config found + for config in websocket_configs { + let address = config.address.clone().unwrap_or("127.0.0.1".to_string()); + let port = config.port.clone().unwrap_or(12907); + let Ok(server_addr) = format!("{address}:{port}").parse::() else { + log::warn!("Invalid websocket listen address: {address}:{port}"); + continue; + }; + + let cmd_tx = cmd_tx.clone(); + tokio::spawn(async move { + // Create the event loop and TCP listener we'll accept connections on. + let listener = match TcpListener::bind(server_addr).await { + Ok(result) => result, + Err(e) => { + log::error!("Failed to bind to `{server_addr}`: {e}"); + return; + } + }; + log::info!("Listening on: {server_addr}"); + + // Listen for client connections + while let Ok((stream, addr)) = listener.accept().await { + tokio::spawn(accept_connection( + cmd_tx.clone(), + stream, + addr, + server_addr, + config.clone(), + )); + } + }); + } +} + +async fn accept_connection( + cmd_tx: Sender, + stream: TcpStream, + addr: SocketAddr, + server_addr: SocketAddr, + config: Websocket, +) { + // TODO: Verify in the config if the connection should be accepted + + log::info!("Client connected: {}", addr); + + // Create a websocket stream from the connection + let ws_stream = match tokio_tungstenite::accept_async(stream).await { + Ok(stream) => stream, + Err(e) => { + log::warn!("Error during websocket handshake: {e}"); + return; + } + }; + + log::info!("Client successfully connected: {}", addr); + + // Notify the manager that a new client device has connected + let (open_tx, mut open_rx) = mpsc::channel(1); + let client = WebsocketClient::new(addr, server_addr, open_tx); + let result = cmd_tx + .send(ManagerCommand::DeviceAdded { + device: client.clone().into(), + }) + .await; + if let Err(err) = result { + log::error!("Failed to notify manager of new client device: {err}"); + return; + } + + // Split the stream into the read and write halfs + let (write, mut read) = ws_stream.split(); + + // Create a channel to read websocket messages from + let (read_tx, read_rx) = mpsc::channel(2048); + let mut stream = Some((write, read_rx)); + + // Listen for messages + loop { + tokio::select! { + // Send the stream receiver if a consumer opened the stream + opener = open_rx.recv() => { + let Some(opener) = opener else { + break; + }; + log::debug!("Received request to open stream"); + if let Err(e) = opener.send(stream.take()).await { + log::debug!("Failed to send websocket message: {e}"); + } + }, + // Read messages from the stream and send them through the read + // channel + msg = read.next() => { + let Some(msg) = msg else { + break; + }; + log::trace!("Received websocket message"); + let msg = match msg { + Ok(msg) => msg, + Err(e) => { + log::warn!("Error processing websocket message: {e}"); + break; + } + }; + if let Err(e) = read_tx.send(msg).await { + log::debug!("Failed to send websocket message: {e}"); + } + }, + } + } + + log::info!("Closing websocket connection: {}", addr); + + // Notify the manager that the client was removed + let result = cmd_tx + .send(ManagerCommand::DeviceRemoved { + device: client.into(), + }) + .await; + if let Err(err) = result { + log::error!("Failed to notify manager of client device removal: {err}"); + } +}