From eddcdbc62d7895cc9541f6c25621d2cef2ffd3e0 Mon Sep 17 00:00:00 2001 From: William Edwards Date: Tue, 4 Mar 2025 21:37:18 -0800 Subject: [PATCH] feat(Networking): add websocket client/server support --- .github/workflows/test.yaml | 9 + Cargo.lock | 69 ++ Cargo.toml | 8 + Makefile | 23 +- README.md | 2 +- docs/install.md | 13 + .../inputplumber/devices/80-websockets.yaml | 36 + src/cli/device.rs | 23 + src/config/mod.rs | 72 ++ src/dbus/interface/target/mod.rs | 2 + src/dbus/interface/target/websocket.rs | 54 ++ src/input/composite_device/mod.rs | 8 + src/input/event/mod.rs | 2 + src/input/event/ucis.rs | 730 ++++++++++++++++++ src/input/info.rs | 30 + src/input/manager.rs | 31 + src/input/source/mod.rs | 26 + src/input/source/network.rs | 78 ++ src/input/source/network/websockets.rs | 218 ++++++ src/input/target/mod.rs | 37 +- src/input/target/websocket.rs | 315 ++++++++ src/lib.rs | 2 + src/main.rs | 2 + src/network/mod.rs | 1 + src/network/websocket.rs | 216 ++++++ 25 files changed, 1999 insertions(+), 8 deletions(-) create mode 100644 rootfs/usr/share/inputplumber/devices/80-websockets.yaml create mode 100644 src/dbus/interface/target/websocket.rs create mode 100644 src/input/event/ucis.rs create mode 100644 src/input/source/network/websockets.rs create mode 100644 src/input/target/websocket.rs create mode 100644 src/network/mod.rs create mode 100644 src/network/websocket.rs diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8d8c2f62e..2eeddd5bc 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -39,3 +39,12 @@ jobs: - uses: actions/checkout@v3 - run: | make in-docker TARGET='dist' BUILD_TYPE='debug' TARGET_ARCH='aarch64-unknown-linux-gnu' + + build-without-networking: + name: Run build without networking support + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - run: | + make in-docker TARGET='test' FEATURES='' diff --git a/Cargo.lock b/Cargo.lock index 7ee8341fe..1178d4932 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -537,6 +537,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + [[package]] name = "deltae" version = "0.3.2" @@ -964,6 +970,22 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "iana-time-zone" version = "0.1.64" @@ -1074,6 +1096,7 @@ dependencies = [ "tabled", "thiserror 2.0.17", "tokio", + "tokio-tungstenite", "udev", "uhid-virt", "version-compare", @@ -2143,6 +2166,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -2525,6 +2559,18 @@ dependencies = [ "syn 2.0.111", ] +[[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.7.5+spec-1.1.0" @@ -2586,6 +2632,23 @@ 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 0.9.2", + "sha1", + "thiserror 2.0.17", + "utf-8", +] + [[package]] name = "typenum" version = "1.19.0" @@ -2686,6 +2749,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.2" diff --git a/Cargo.toml b/Cargo.toml index 10d55c348..39813645f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,6 +106,13 @@ assets = [ name = "generate" path = "./src/generate.rs" +# The networking feature adds websocket support (routing input over the +# network). It is enabled by default and can be disabled with: +# cargo build --no-default-features +[features] +default = ["networking"] +networking = ["dep:tokio-tungstenite"] + [dependencies] clap = { version = "4.5.54", features = ["derive", "string"] } clap_complete = "4.5.65" @@ -138,6 +145,7 @@ serialport = "4.8.1" tabled = { version = "0.20.0", features = ["ansi"] } thiserror = "2.0.17" tokio = { version = "1.49.0", features = ["full"] } +tokio-tungstenite = { version = "0.26.2", optional = true } udev = { version = "0.9.3", features = ["mio"] } uhid-virt = "0.0.8" version-compare = "0.2.1" diff --git a/Makefile b/Makefile index 3b6a3104f..152560796 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,16 @@ PREFIX ?= /usr CACHE_DIR := .cache ENABLE_METRICS ?= 1 +# Cargo features to build with. Networking (websocket) support is enabled by +# default; set FEATURES to an empty string to compile without it: +# make build FEATURES="" +FEATURES ?= networking +ifeq ($(FEATURES),) +FEATURE_FLAGS := --no-default-features +else +FEATURE_FLAGS := --features "$(FEATURES)" +endif + ifeq ($(ARCH),x86_64) ARCH_DEB = amd64 endif @@ -103,12 +113,12 @@ build: target/$(TARGET_ARCH)/$(BUILD_TYPE)/$(NAME) .PHONY: debug debug: target/$(TARGET_ARCH)/debug/$(NAME) ## Build debug build target/$(TARGET_ARCH)/debug/$(NAME): $(ALL_RS) Cargo.lock - cargo build --target $(TARGET_ARCH) + cargo build --target $(TARGET_ARCH) $(FEATURE_FLAGS) .PHONY: release release: target/$(TARGET_ARCH)/release/$(NAME) ## Build release build target/$(TARGET_ARCH)/release/$(NAME): $(ALL_RS) Cargo.lock - cargo build --release --target $(TARGET_ARCH) + cargo build --release --target $(TARGET_ARCH) $(FEATURE_FLAGS) .PHONY: all all: build debug ## Build release and debug builds @@ -132,8 +142,8 @@ format: ## Run rustfmt on all source files .PHONY: test test: test-autostart-rules ## Run all tests - cargo clippy --all -- -D warnings - cargo test -- --show-output + cargo clippy --all $(FEATURE_FLAGS) -- -D warnings + cargo test $(FEATURE_FLAGS) -- --show-output .PHONY: test-autostart-rules test-autostart-rules: ## Test to ensure autostart rules are up-to-date @@ -145,7 +155,7 @@ test-polkit-usage: ## Test to ensure polkit policy exists for all actions .PHONY: generate generate: ## Generate schema definitions for configs - cargo run --bin generate + cargo run --bin generate $(FEATURE_FLAGS) .PHONY: setup setup: /usr/share/dbus-1/system.d/$(DBUS_NAME).conf ## Install dbus policies @@ -260,7 +270,8 @@ in-docker: -e ARCH=$(ARCH) \ -e TARGET_ARCH=$(TARGET_ARCH) \ -e BUILD_TYPE=$(BUILD_TYPE) \ - -e PKG_CONFIG_SYSROOT_DIR="/usr/$(ARCH)-linux-gnu" \ + -e FEATURES=$(FEATURES) \ + -e PKG_CONFIG_SYSROOT_DIR="/usr/$(ARCH)-linux-gnu" \ --user $(shell id -u):$(shell id -g) \ $(IMAGE_NAME):$(IMAGE_TAG) \ make BUILD_TYPE=$(BUILD_TYPE) $(TARGET) diff --git a/README.md b/README.md index 9324860b0..91dd0dcff 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,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 (optional `networking` cargo feature, enabled by default) ## Documentation diff --git a/docs/install.md b/docs/install.md index bd1160e29..68401baa7 100644 --- a/docs/install.md +++ b/docs/install.md @@ -99,6 +99,19 @@ sudo make install ``` + InputPlumber is built with the `networking` feature enabled by default, which + adds websocket support for routing input over the network. To compile without + it, pass an empty `FEATURES` value (or use cargo directly): + + ```bash + make build FEATURES="" + # or + cargo build --no-default-features + ``` + + When built without the `networking` feature, websocket devices are not + supported and any `websocket` entries in device configs are ignored. + Then start the service with: ```bash 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 000000000..9662f6a08 --- /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 08cd9b9ca..093c892de 100644 --- a/src/cli/device.rs +++ b/src/cli/device.rs @@ -11,6 +11,8 @@ use zbus::Connection; use crate::cli::get_managed_objects; use crate::dbus::interface::composite_device::CompositeDeviceInterfaceProxy; use crate::dbus::interface::manager::ManagerInterfaceProxy; +#[cfg(feature = "networking")] +use crate::dbus::interface::target::websocket::TargetWebsocketInterfaceProxy; use crate::dbus::interface::target::TargetInterfaceProxy; use crate::input::target::TargetDeviceTypeId; @@ -48,6 +50,12 @@ pub enum DeviceCommand { }, /// Test input menu Test, + /// Connect this input device to a remote instance of InputPlumber + #[cfg(feature = "networking")] + Connect { + /// URL of the remote instance of InputPlumber (e.g. "ws://192.168.0.100:12901") + url: String, + }, } #[derive(Subcommand, Debug, Clone)] @@ -338,6 +346,21 @@ pub async fn handle_device( return Err(e.to_string().into()); } } + #[cfg(feature = "networking")] + 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 714d12132..52bface82 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -21,6 +21,9 @@ use crate::{ udev::device::UdevDevice, }; +#[cfg(feature = "networking")] +use crate::network::websocket::WebsocketClient; + /// Represents all possible errors loading a [CompositeDevice] #[derive(Debug, Error)] pub enum LoadError { @@ -250,6 +253,9 @@ pub struct SourceDevice { /// Devices that match the given tty propertied will be captured by InputPlumber #[serde(skip_serializing_if = "Option::is_none")] pub tty: Option, + /// Websocket clients that match the given websocket settings will be captured by InputPlumber + #[cfg(feature = "networking")] + pub websocket: Option, /// Device configuration options are used to alter how the source device is managed #[serde(skip_serializing_if = "Option::is_none")] pub config: Option, @@ -444,6 +450,17 @@ pub struct MountMatrix { pub z: [f64; 3], } +#[cfg(feature = "networking")] +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[serde(rename_all = "snake_case")] +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 { @@ -520,6 +537,8 @@ impl CompositeDeviceConfig { for config in self.source_devices.iter() { let matched_config = match device { DeviceInfo::Udev(udevice) => self.get_matching_udev_device(config, udevice), + #[cfg(feature = "networking")] + DeviceInfo::Websocket(client) => self.get_matching_websocket_device(config, client), }; if matched_config.is_some() { return matched_config; @@ -589,6 +608,59 @@ impl CompositeDeviceConfig { None } + #[cfg(feature = "networking")] + /// 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 + } + + #[cfg(feature = "networking")] + /// 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::debug!("Checking udev config '{:?}'", udev_config); diff --git a/src/dbus/interface/target/mod.rs b/src/dbus/interface/target/mod.rs index 213253f37..26f0d3b0f 100644 --- a/src/dbus/interface/target/mod.rs +++ b/src/dbus/interface/target/mod.rs @@ -4,6 +4,8 @@ pub mod gamepad; pub mod keyboard; pub mod mouse; pub mod touchscreen; +#[cfg(feature = "networking")] +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 000000000..12f7e61ad --- /dev/null +++ b/src/dbus/interface/target/websocket.rs @@ -0,0 +1,54 @@ +use zbus::fdo; +use zbus_macros::interface; + +use tokio::{net::TcpStream, sync::mpsc::Sender}; +use tokio_tungstenite::{connect_async, MaybeTlsStream, WebSocketStream}; + +use crate::input::target::websocket::DeviceCommand; + +use super::Unregisterable; + +/// 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()) + } +} + +impl Unregisterable for TargetWebsocketInterface {} diff --git a/src/input/composite_device/mod.rs b/src/input/composite_device/mod.rs index 144372adc..90b11cbae 100644 --- a/src/input/composite_device/mod.rs +++ b/src/input/composite_device/mod.rs @@ -43,6 +43,9 @@ use crate::{ udev::{hide_device, unhide_device, HideFlag}, }; +#[cfg(feature = "networking")] +use crate::input::source::network::NetworkDevice; + use self::{client::CompositeDeviceClient, command::CompositeCommand}; use super::{ @@ -1776,6 +1779,11 @@ impl CompositeDevice { } } } + #[cfg(feature = "networking")] + DeviceInfo::Websocket(client) => { + let device = NetworkDevice::new(client, self.client(), source_config.clone())?; + SourceDevice::Network(device) + } }; // Get the capabilities of the source device. diff --git a/src/input/event/mod.rs b/src/input/event/mod.rs index 84bcc77cb..38db36d06 100644 --- a/src/input/event/mod.rs +++ b/src/input/event/mod.rs @@ -2,6 +2,8 @@ pub mod context; pub mod dbus; pub mod evdev; pub mod native; +#[cfg(feature = "networking")] +pub mod ucis; pub mod value; #[cfg(test)] diff --git a/src/input/event/ucis.rs b/src/input/event/ucis.rs new file mode 100644 index 000000000..86f3ae02f --- /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, GamepadDial, GamepadTrigger, Touch, + TouchButton, + Touchpad, + }, +}; +use packed_struct::prelude::*; + +use super::{native::NativeEvent, value::InputValue}; + +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::GamepadDialLeft => Capability::Gamepad(Gamepad::Dial(GamepadDial::LeftStickDial)), + InputCapability::GamepadDialRight => Capability::Gamepad(Gamepad::Dial(GamepadDial::RightStickDial)), + InputCapability::GyroscopeCenter => Capability::Gamepad(Gamepad::Gyro), + InputCapability::GyroscopeLeft => Capability::Gamepad(Gamepad::Gyro), + InputCapability::GyroscopeRight => Capability::Gamepad(Gamepad::Gyro), + InputCapability::AccelerometerCenter => { + Capability::Gamepad(Gamepad::Accelerometer) + } + InputCapability::AccelerometerLeft => { + Capability::Gamepad(Gamepad::Accelerometer) + } + InputCapability::AccelerometerRight => { + 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 index e9e2c63d4..19593c5ff 100644 --- a/src/input/info.rs +++ b/src/input/info.rs @@ -1,8 +1,12 @@ +#[cfg(feature = "networking")] +use crate::network::websocket::WebsocketClient; use crate::udev::device::UdevDevice; #[derive(Debug, Clone)] pub enum DeviceInfo { Udev(UdevDevice), + #[cfg(feature = "networking")] + Websocket(WebsocketClient), } impl DeviceInfo { @@ -10,6 +14,8 @@ impl DeviceInfo { pub fn name(&self) -> String { match self { DeviceInfo::Udev(device) => device.name(), + #[cfg(feature = "networking")] + DeviceInfo::Websocket(client) => client.addr.to_string(), } } @@ -18,6 +24,8 @@ impl DeviceInfo { pub fn get_id(&self) -> String { match self { DeviceInfo::Udev(device) => device.get_id(), + #[cfg(feature = "networking")] + DeviceInfo::Websocket(client) => client.get_id(), } } @@ -25,6 +33,8 @@ impl DeviceInfo { pub fn path(&self) -> String { match self { DeviceInfo::Udev(device) => device.devnode(), + #[cfg(feature = "networking")] + DeviceInfo::Websocket(client) => format!("ws://{}", client.addr), } } @@ -32,6 +42,8 @@ impl DeviceInfo { pub fn kind(&self) -> String { match self { DeviceInfo::Udev(device) => device.subsystem(), + #[cfg(feature = "networking")] + DeviceInfo::Websocket(_) => "websocket".into(), } } } @@ -48,16 +60,27 @@ impl From for DeviceInfo { } } +#[cfg(feature = "networking")] +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), + #[cfg(feature = "networking")] + Websocket(&'a WebsocketClient), } impl DeviceInfoRef<'_> { pub fn to_owned(&self) -> DeviceInfo { match self { DeviceInfoRef::Udev(device) => DeviceInfo::Udev(device.to_owned().clone()), + #[cfg(feature = "networking")] + DeviceInfoRef::Websocket(client) => DeviceInfo::Websocket(client.to_owned().clone()), } } } @@ -67,3 +90,10 @@ impl<'a> From<&'a UdevDevice> for DeviceInfoRef<'a> { Self::Udev(device) } } + +#[cfg(feature = "networking")] +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 1696f67bb..96b1f053b 100644 --- a/src/input/manager.rs +++ b/src/input/manager.rs @@ -38,6 +38,8 @@ use crate::input::{ source::{evdev, hidraw, iio, led, tty}, target::{TargetDevice, TargetDeviceTypeId}, }; +#[cfg(feature = "networking")] +use crate::network::websocket::{watch_websockets, WebsocketClient}; use crate::udev::{ self, device::{AttributeGetter, UdevDevice}, @@ -249,6 +251,9 @@ impl Manager { log::debug!("Starting input manager task..."); self.listen_on_dbus(); + + #[cfg(feature = "networking")] + 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()), @@ -423,6 +428,12 @@ impl Manager { log::error!("Error adding device '{dev_name} ({dev_sysname})': {e}"); } } + #[cfg(feature = "networking")] + 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) => { @@ -430,6 +441,11 @@ impl Manager { log::error!("Error removing device: {e}"); } } + #[cfg(feature = "networking")] + 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}"); @@ -1172,6 +1188,21 @@ impl Manager { Ok(()) } + /// Called when a new network device has connected + #[cfg(feature = "networking")] + 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_udev_device_added(&mut self, device: UdevDevice) -> Result<(), Box> { let dev_path = device.devpath(); diff --git a/src/input/source/mod.rs b/src/input/source/mod.rs index da26e2ba1..e320950c4 100644 --- a/src/input/source/mod.rs +++ b/src/input/source/mod.rs @@ -10,6 +10,8 @@ use std::{ use ::evdev::FFEffectData; use led::LedDevice; +#[cfg(feature = "networking")] +use network::NetworkDevice; use thiserror::Error; use tokio::sync::mpsc::{self, error::TryRecvError}; @@ -35,6 +37,8 @@ pub mod evdev; pub mod hidraw; pub mod iio; pub mod led; +#[cfg(feature = "networking")] +pub mod network; pub mod tty; /// Size of the [SourceCommand] buffer for receiving output events @@ -361,6 +365,8 @@ impl SourceDriver pub fn info_ref(&self) -> DeviceInfoRef<'_> { match &self.device_info { DeviceInfo::Udev(device) => device.into(), + #[cfg(feature = "networking")] + DeviceInfo::Websocket(client) => client.into(), } } @@ -605,6 +611,8 @@ pub(crate) trait SourceDeviceCompatible { None } + #[cfg(feature = "networking")] + DeviceInfoRef::Websocket(_) => None, } } @@ -631,6 +639,8 @@ pub enum SourceDevice { HidRaw(HidRawDevice), Iio(IioDevice), Led(LedDevice), + #[cfg(feature = "networking")] + Network(NetworkDevice), Tty(TtyDevice), } @@ -642,6 +652,8 @@ impl SourceDevice { SourceDevice::HidRaw(device) => device.get_device_ref(), SourceDevice::Iio(device) => device.get_device_ref(), SourceDevice::Led(device) => device.get_device_ref(), + #[cfg(feature = "networking")] + SourceDevice::Network(device) => device.get_device_ref(), SourceDevice::Tty(device) => device.get_device_ref(), } } @@ -653,6 +665,8 @@ impl SourceDevice { SourceDevice::HidRaw(device) => device.get_id(), SourceDevice::Iio(device) => device.get_id(), SourceDevice::Led(device) => device.get_id(), + #[cfg(feature = "networking")] + SourceDevice::Network(device) => device.get_id(), SourceDevice::Tty(device) => device.get_id(), } } @@ -664,6 +678,8 @@ impl SourceDevice { SourceDevice::HidRaw(device) => device.get_serial(), SourceDevice::Iio(_) => None, SourceDevice::Led(_) => None, + #[cfg(feature = "networking")] + SourceDevice::Network(_) => None, SourceDevice::Tty(_) => None, } } @@ -675,6 +691,8 @@ impl SourceDevice { SourceDevice::HidRaw(device) => device.client(), SourceDevice::Iio(device) => device.client(), SourceDevice::Led(device) => device.client(), + #[cfg(feature = "networking")] + SourceDevice::Network(device) => device.client(), SourceDevice::Tty(device) => device.client(), } } @@ -686,6 +704,8 @@ impl SourceDevice { SourceDevice::HidRaw(device) => device.run().await, SourceDevice::Iio(device) => device.run().await, SourceDevice::Led(device) => device.run().await, + #[cfg(feature = "networking")] + SourceDevice::Network(device) => device.run().await, SourceDevice::Tty(device) => device.run().await, } } @@ -697,6 +717,8 @@ impl SourceDevice { SourceDevice::HidRaw(device) => device.get_capabilities(), SourceDevice::Iio(device) => device.get_capabilities(), SourceDevice::Led(device) => device.get_capabilities(), + #[cfg(feature = "networking")] + SourceDevice::Network(device) => device.get_capabilities(), SourceDevice::Tty(device) => device.get_capabilities(), } } @@ -708,6 +730,8 @@ impl SourceDevice { SourceDevice::HidRaw(device) => device.get_output_capabilities(), SourceDevice::Iio(device) => device.get_output_capabilities(), SourceDevice::Led(device) => device.get_output_capabilities(), + #[cfg(feature = "networking")] + SourceDevice::Network(device) => device.get_output_capabilities(), SourceDevice::Tty(device) => device.get_output_capabilities(), } } @@ -719,6 +743,8 @@ impl SourceDevice { SourceDevice::HidRaw(device) => device.get_device_path(), SourceDevice::Iio(device) => device.get_device_path(), SourceDevice::Led(device) => device.get_device_path(), + #[cfg(feature = "networking")] + SourceDevice::Network(device) => device.get_device_path(), SourceDevice::Tty(device) => device.get_device_path(), } } diff --git a/src/input/source/network.rs b/src/input/source/network.rs index e69de29bb..68cb02200 100644 --- a/src/input/source/network.rs +++ b/src/input/source/network.rs @@ -0,0 +1,78 @@ +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_output_capabilities( + &self, + ) -> Result, super::OutputError> { + match self { + Self::Websocket(source_driver) => source_driver.get_output_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 000000000..7b13989d0 --- /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 873b0a835..e9f3f33c3 100644 --- a/src/input/target/mod.rs +++ b/src/input/target/mod.rs @@ -12,6 +12,8 @@ pub mod touchpad; pub mod touchscreen; pub mod ulitmate_2; pub mod unified_gamepad; +#[cfg(feature = "networking")] +pub mod websocket; pub mod xpad; use std::{ @@ -66,6 +68,8 @@ use self::steam_deck::SteamDeckDevice; use self::touchpad::TouchpadDevice; use self::touchscreen::TouchscreenDevice; use self::ulitmate_2::Ultimate2WirelessDevice; +#[cfg(feature = "networking")] +use self::websocket::WebsocketDevice; /// Possible errors for a target device client #[derive(Error, Debug)] @@ -254,6 +258,12 @@ impl TargetDeviceTypeId { name: "Debug Device", device_class: TargetDeviceClass::Debug, }, + #[cfg(feature = "networking")] + TargetDeviceTypeId { + id: "websocket", + name: "Websocket Device", + device_class: TargetDeviceClass::Websocket, + }, ] } @@ -271,7 +281,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" ) } @@ -317,6 +334,8 @@ pub enum TargetDeviceClass { Mouse, Touchscreen, Touchpad, + #[cfg(feature = "networking")] + Websocket, } impl Display for TargetDeviceClass { @@ -330,6 +349,8 @@ impl Display for TargetDeviceClass { TargetDeviceClass::Mouse => "mouse", TargetDeviceClass::Touchscreen => "touchscreen", TargetDeviceClass::Touchpad => "touchpad", + #[cfg(feature = "networking")] + TargetDeviceClass::Websocket => "websocket", }; write!(f, "{str}") } @@ -803,6 +824,8 @@ pub enum TargetDevice { Ultimate2Wireless(TargetDriver), XBoxController(TargetDriver), UnifiedGamepad(TargetDriver), + #[cfg(feature = "networking")] + Websocket(TargetDriver), } impl TargetDevice { @@ -927,6 +950,12 @@ impl TargetDevice { let driver = TargetDriver::new(id, device, dbus); Ok(Self::UnifiedGamepad(driver)) } + #[cfg(feature = "networking")] + "websocket" => { + let device = WebsocketDevice::new(dbus.connection().clone()); + let driver = TargetDriver::new(id, device, dbus); + Ok(Self::Websocket(driver)) + } "null" => Ok(Self::Null), _ => Ok(Self::Null), } @@ -965,6 +994,8 @@ impl TargetDevice { ] } TargetDevice::UnifiedGamepad(_) => vec!["unified-gamepad".try_into().unwrap()], + #[cfg(feature = "networking")] + TargetDevice::Websocket(_) => vec!["websocket".try_into().unwrap()], } } @@ -985,6 +1016,8 @@ impl TargetDevice { TargetDevice::Touchscreen(device) => Some(device.client()), TargetDevice::XBoxController(device) => Some(device.client()), TargetDevice::UnifiedGamepad(device) => Some(device.client()), + #[cfg(feature = "networking")] + TargetDevice::Websocket(device) => Some(device.client()), } } @@ -1005,6 +1038,8 @@ impl TargetDevice { TargetDevice::Touchscreen(device) => device.run().await, TargetDevice::XBoxController(device) => device.run().await, TargetDevice::UnifiedGamepad(device) => device.run().await, + #[cfg(feature = "networking")] + TargetDevice::Websocket(device) => device.run().await, } } } diff --git a/src/input/target/websocket.rs b/src/input/target/websocket.rs new file mode 100644 index 000000000..b70a7822e --- /dev/null +++ b/src/input/target/websocket.rs @@ -0,0 +1,315 @@ +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, DBusInterfaceManager}, + 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.clone() { + log::debug!("Updating composite device with new capabilities"); + let composite_device = composite_device.clone(); + tokio::task::spawn(async move { + if let Err(e) = composite_device + .update_target_capabilities(dbus_path, capabilities) + .await + { + 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: &mut DBusInterfaceManager, + client: TargetDeviceClient, + _type_id: TargetDeviceTypeId, + ) { + log::trace!("Using device client: {client:?}"); + self.dbus_path = Some(dbus.path().to_owned()); + let (tx, rx) = mpsc::channel(128); + self.dbus_rx = Some(rx); + let iface = TargetWebsocketInterface::new(tx); + dbus.register(iface); + } + + 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 dddfe88b2..2ced3453b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,8 @@ pub mod dbus; pub mod dmi; pub mod drivers; pub mod input; +#[cfg(feature = "networking")] +pub mod network; pub mod sync; pub mod udev; pub mod watcher; diff --git a/src/main.rs b/src/main.rs index f203a8c4b..4005bd1be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,8 @@ mod dbus; mod dmi; mod drivers; mod input; +#[cfg(feature = "networking")] +mod network; mod sync; mod udev; mod watcher; diff --git a/src/network/mod.rs b/src/network/mod.rs new file mode 100644 index 000000000..6eba44d95 --- /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 000000000..6dde6b6b3 --- /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.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}"); + } +}