From ad2aca709d84b763894f77b13feb2faca5a47de2 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Wed, 16 Sep 2026 12:51:15 -0700 Subject: [PATCH 1/7] fix(udev): Block IIO Sensors Proxy for managed devices --- .../99-inputplumber-device-setup.rules | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/rootfs/usr/lib/udev/rules.d/99-inputplumber-device-setup.rules b/rootfs/usr/lib/udev/rules.d/99-inputplumber-device-setup.rules index 635dff49..bb34d442 100644 --- a/rootfs/usr/lib/udev/rules.d/99-inputplumber-device-setup.rules +++ b/rootfs/usr/lib/udev/rules.d/99-inputplumber-device-setup.rules @@ -1,34 +1,38 @@ # AOKZOE/OneXPlayer # Turbo button takeover -ACTION=="add|change", KERNEL=="oxp-platform", SUBSYSTEM=="platform", DRIVER=="oxp-platform", ATTR{tt_toggle}="1", GOTO="end" +ACTION=="add|change", KERNEL=="oxp-platform", SUBSYSTEM=="platform", DRIVER=="oxp-platform", ATTR{tt_toggle}="1", GOTO="inputplumber_setup_end" # Lenovo # Legion Go S Controller Settings -ACTION=="add|change|bind", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="e31[01]", SUBSYSTEM=="hid", DRIVER=="lenovo-legos-hid", ATTR{gamepad/auto_sleep_time}="0", ATTR{gamepad/dpad_mode}="8-way", ATTR{gamepad/mode}="xinput", ATTR{gamepad/poll_rate}="250", ATTR{os_mode}="linux", ATTR{touchpad/linux_mode}="absolute", GOTO="end" -ACTION=="add|change|bind", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="e31[01]", SUBSYSTEM=="hid", DRIVER=="hid-lenovo-go-s", ATTR{gamepad/auto_sleep_time}="0", ATTR{gamepad/dpad_mode}="8-way", ATTR{gamepad/mode}="xinput", ATTR{gamepad/poll_rate}="250", ATTR{os_mode}="linux", ATTR{touchpad/linux_mode}="absolute", GOTO="end" +ACTION=="add|change|bind", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="e31[01]", SUBSYSTEM=="hid", DRIVER=="lenovo-legos-hid", ATTR{gamepad/auto_sleep_time}="0", ATTR{gamepad/dpad_mode}="8-way", ATTR{gamepad/mode}="xinput", ATTR{gamepad/poll_rate}="250", ATTR{os_mode}="linux", ATTR{touchpad/linux_mode}="absolute", GOTO="inputplumber_setup_end" +ACTION=="add|change|bind", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="e31[01]", SUBSYSTEM=="hid", DRIVER=="hid-lenovo-go-s", ATTR{gamepad/auto_sleep_time}="0", ATTR{gamepad/dpad_mode}="8-way", ATTR{gamepad/mode}="xinput", ATTR{gamepad/poll_rate}="250", ATTR{os_mode}="linux", ATTR{touchpad/linux_mode}="absolute", GOTO="inputplumber_setup_end" # Legion Go/Go2 Controller Settings -ACTION=="add|change|bind", ATTRS{idVendor}=="17ef", ATTRS{idProduct}=="61e[bcde]", SUBSYSTEM=="hid", DRIVER=="hid-lenovo-go", ATTR{os_mode}="linux", ATTR{left_handle/imu_bypass_enable}="true", ATTR{right_handle/imu_bypass_enable}="true", ATTR{touchpad/vibration_enable}="false", GOTO="end" +ACTION=="add|change|bind", ATTRS{idVendor}=="17ef", ATTRS{idProduct}=="61e[bcde]", SUBSYSTEM=="hid", DRIVER=="hid-lenovo-go", ATTR{os_mode}="linux", ATTR{left_handle/imu_bypass_enable}="true", ATTR{right_handle/imu_bypass_enable}="true", ATTR{touchpad/vibration_enable}="false", GOTO="inputplumber_setup_end" # ASUS # ROG Ally Controller Settings ## asus_ally_hid uses string values for gamepad_mode, i.e. "gamepad", asus_rog_ally uses numeric values, i.e. "1" -#ACTION=="add|change", KERNEL=="0003:0B05:1ABE*", SUBSYSTEM=="hid", DRIVER=="asus_ally_hid", ATTRS{bInterfaceNumber}=="02", ATTR{btn_m1/remap}="KB_F15", ATTR{btn_m2/remap}="KB_F14", ATTR{gamepad_mode}="gamepad", GOTO="end" -#ACTION=="add|change", KERNEL=="0003:0B05:1ABE*", SUBSYSTEM=="hid", DRIVER=="asus_ally_hid", ATTRS{bInterfaceNumber}=="05", ATTR{qam_mode}="0", GOTO="end" -#ACTION=="add|change", KERNEL=="0003:0B05:1ABE*", SUBSYSTEM=="hid", DRIVER=="asus_rog_ally", ATTRS{bInterfaceNumber}=="02", ATTR{btn_m1/remap}="KB_F15", ATTR{btn_m2/remap}="KB_F14", ATTR{gamepad_mode}="1", ATTR{apply_all}="1", GOTO="end" -#ACTION=="add|change", KERNEL=="0003:0B05:1ABE*", SUBSYSTEM=="hid", DRIVER=="asus_rog_ally", ATTRS{bInterfaceNumber}=="05", ATTR{qam_mode}="0", GOTO="end" +#ACTION=="add|change", KERNEL=="0003:0B05:1ABE*", SUBSYSTEM=="hid", DRIVER=="asus_ally_hid", ATTRS{bInterfaceNumber}=="02", ATTR{btn_m1/remap}="KB_F15", ATTR{btn_m2/remap}="KB_F14", ATTR{gamepad_mode}="gamepad", GOTO="inputplumber_setup_end" +#ACTION=="add|change", KERNEL=="0003:0B05:1ABE*", SUBSYSTEM=="hid", DRIVER=="asus_ally_hid", ATTRS{bInterfaceNumber}=="05", ATTR{qam_mode}="0", GOTO="inputplumber_setup_end" +#ACTION=="add|change", KERNEL=="0003:0B05:1ABE*", SUBSYSTEM=="hid", DRIVER=="asus_rog_ally", ATTRS{bInterfaceNumber}=="02", ATTR{btn_m1/remap}="KB_F15", ATTR{btn_m2/remap}="KB_F14", ATTR{gamepad_mode}="1", ATTR{apply_all}="1", GOTO="inputplumber_setup_end" +#ACTION=="add|change", KERNEL=="0003:0B05:1ABE*", SUBSYSTEM=="hid", DRIVER=="asus_rog_ally", ATTRS{bInterfaceNumber}=="05", ATTR{qam_mode}="0", GOTO="inputplumber_setup_end" # ## ROG Ally X Controller Settings -#ACTION=="add|change", KERNEL=="0003:0B05:1B4C*", SUBSYSTEM=="hid", DRIVER=="asus_ally_hid", ATTRS{bInterfaceNumber}=="02", ATTR{btn_m1/remap}="KB_F15", ATTR{btn_m2/remap}="KB_F14", ATTR{gamepad_mode}="gamepad", GOTO="end" -#ACTION=="add|change", KERNEL=="0003:0B05:1B4C*", SUBSYSTEM=="hid", DRIVER=="asus_ally_hid", ATTRS{bInterfaceNumber}=="05", ATTR{qam_mode}="0", GOTO="end" -#ACTION=="add|change", KERNEL=="0003:0B05:1B4C*", SUBSYSTEM=="hid", DRIVER=="asus_rog_ally", ATTRS{bInterfaceNumber}=="02", ATTR{btn_m1/remap}="KB_F15", ATTR{btn_m2/remap}="KB_F14", ATTR{gamepad_mode}="1", ATTR{apply_all}="1", GOTO="end" -#ACTION=="add|change", KERNEL=="0003:0B05:1B4C*", SUBSYSTEM=="hid", DRIVER=="asus_rog_ally", ATTRS{bInterfaceNumber}=="05", ATTR{qam_mode}="0", GOTO="end" +#ACTION=="add|change", KERNEL=="0003:0B05:1B4C*", SUBSYSTEM=="hid", DRIVER=="asus_ally_hid", ATTRS{bInterfaceNumber}=="02", ATTR{btn_m1/remap}="KB_F15", ATTR{btn_m2/remap}="KB_F14", ATTR{gamepad_mode}="gamepad", GOTO="inputplumber_setup_end" +#ACTION=="add|change", KERNEL=="0003:0B05:1B4C*", SUBSYSTEM=="hid", DRIVER=="asus_ally_hid", ATTRS{bInterfaceNumber}=="05", ATTR{qam_mode}="0", GOTO="inputplumber_setup_end" +#ACTION=="add|change", KERNEL=="0003:0B05:1B4C*", SUBSYSTEM=="hid", DRIVER=="asus_rog_ally", ATTRS{bInterfaceNumber}=="02", ATTR{btn_m1/remap}="KB_F15", ATTR{btn_m2/remap}="KB_F14", ATTR{gamepad_mode}="1", ATTR{apply_all}="1", GOTO="inputplumber_setup_end" +#ACTION=="add|change", KERNEL=="0003:0B05:1B4C*", SUBSYSTEM=="hid", DRIVER=="asus_rog_ally", ATTRS{bInterfaceNumber}=="05", ATTR{qam_mode}="0", GOTO="inputplumber_setup_end" # MSI Claw Settings ACTION=="add|change|bind", ATTRS{idVendor}=="0db0", ATTRS{idProduct}=="190[1-3]", SUBSYSTEM=="hid", DRIVER=="hid-msi", ATTR{button_m1}="KEY_RIGHTBRACE", ATTR{button_m2}="KEY_LEFTBRACE", GOTO="claw_desktop" LABEL="claw_desktop" -ACTION=="add|change|bind", ATTRS{idVendor}=="0db0", ATTRS{idProduct}=="1901", SUBSYSTEM=="hid", DRIVER=="hid-msi", ATTR{gamepad_mode}=="desktop", ATTR{gamepad_mode}="xinput", GOTO="end" -ACTION=="add|change|bind", ATTRS{idVendor}=="0db0", ATTRS{idProduct}=="1902", SUBSYSTEM=="hid", DRIVER=="hid-msi", ATTR{gamepad_mode}=="desktop", ATTR{gamepad_mode}="dinput", GOTO="end" -ACTION=="add|change|bind", ATTRS{idVendor}=="0db0", ATTRS{idProduct}=="1903", SUBSYSTEM=="hid", DRIVER=="hid-msi", ATTR{gamepad_mode}="xinput", GOTO="end" +ACTION=="add|change|bind", ATTRS{idVendor}=="0db0", ATTRS{idProduct}=="1901", SUBSYSTEM=="hid", DRIVER=="hid-msi", ATTR{gamepad_mode}=="desktop", ATTR{gamepad_mode}="xinput", GOTO="inputplumber_setup_end" +ACTION=="add|change|bind", ATTRS{idVendor}=="0db0", ATTRS{idProduct}=="1902", SUBSYSTEM=="hid", DRIVER=="hid-msi", ATTR{gamepad_mode}=="desktop", ATTR{gamepad_mode}="dinput", GOTO="inputplumber_setup_end" +ACTION=="add|change|bind", ATTRS{idVendor}=="0db0", ATTRS{idProduct}=="1903", SUBSYSTEM=="hid", DRIVER=="hid-msi", ATTR{gamepad_mode}="xinput", GOTO="inputplumber_setup_end" -LABEL="end" +# IMUs, block iio-sensor-proxy if inputplumber autostarts on that device +ACTION=="add|change", SUBSYSTEM=="iio", ENV{IIO_SENSOR_PROXY_TYPE}!="", IMPORT{builtin}="hwdb '$attr{[dmi/id]modalias}'" +ACTION=="add|change", SUBSYSTEM=="iio", ENV{USE_INPUTPLUMBER}=="1", ENV{IIO_SENSOR_PROXY_TYPE}="", ENV{SYSTEMD_WANTS}="", GOTO="inputplumber_setup_end" + +LABEL="inputplumber_setup_end" From 3028ecf53913c4fd8007684d500e27722cadf2d3 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Mon, 14 Sep 2026 21:47:35 -0700 Subject: [PATCH 2/7] fix(Hardware Support): Fix gyro axis mapping for various handhelds --- .../share/inputplumber/devices/50-legion_go.yaml | 7 +++++++ .../inputplumber/devices/50-msi_claw_a8_bz2e.yaml | 14 ++++++++++---- .../devices/50-onexplayer_onexfly.yaml | 4 ++++ .../share/inputplumber/devices/50-rog_ally.yaml | 2 +- .../share/inputplumber/devices/50-rog_ally_x.yaml | 2 +- src/drivers/lego/go2_driver.rs | 4 ++-- 6 files changed, 25 insertions(+), 8 deletions(-) diff --git a/rootfs/usr/share/inputplumber/devices/50-legion_go.yaml b/rootfs/usr/share/inputplumber/devices/50-legion_go.yaml index ddacee3b..b33853c8 100644 --- a/rootfs/usr/share/inputplumber/devices/50-legion_go.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-legion_go.yaml @@ -222,6 +222,13 @@ source_devices: x: [0, 1, 0] y: [1, 0, 0] z: [0, 0, 1] + - group: imu + iio: + name: accel_3d + mount_matrix: + x: [0, 1, 0] + y: [1, 0, 0] + z: [0, 0, 1] # Optional configuration for the composite device options: diff --git a/rootfs/usr/share/inputplumber/devices/50-msi_claw_a8_bz2e.yaml b/rootfs/usr/share/inputplumber/devices/50-msi_claw_a8_bz2e.yaml index 5b6ab87d..1737b2b4 100644 --- a/rootfs/usr/share/inputplumber/devices/50-msi_claw_a8_bz2e.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-msi_claw_a8_bz2e.yaml @@ -135,10 +135,16 @@ source_devices: iio: name: gyro_3d mount_matrix: - # TODO: Verify - x: [0, 1, 0] - y: [-1, 0, 0] - z: [0, 0, -1] + x: [1, 0, 0] + y: [0, -1, 0] + z: [0, 0, 1] + - group: imu + iio: + name: accel_3d + mount_matrix: + x: [1, 0, 0] + y: [0, -1, 0] + z: [0, 0, 1] # Optional configuration for the composite device options: diff --git a/rootfs/usr/share/inputplumber/devices/50-onexplayer_onexfly.yaml b/rootfs/usr/share/inputplumber/devices/50-onexplayer_onexfly.yaml index d4cf3338..2c4bee7f 100644 --- a/rootfs/usr/share/inputplumber/devices/50-onexplayer_onexfly.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-onexplayer_onexfly.yaml @@ -53,6 +53,10 @@ source_devices: - group: imu iio: name: "{i2c-BMI0160:00,bmi260}" + mount_matrix: + x: [0, -1, 0] + y: [1, 0, 0] + z: [0, 0, -1] # Optional configuration for the composite device options: diff --git a/rootfs/usr/share/inputplumber/devices/50-rog_ally.yaml b/rootfs/usr/share/inputplumber/devices/50-rog_ally.yaml index 64e9ac13..09712f0c 100644 --- a/rootfs/usr/share/inputplumber/devices/50-rog_ally.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-rog_ally.yaml @@ -44,7 +44,7 @@ source_devices: name: bmi323-imu mount_matrix: x: [1, 0, 0] - y: [0, -1, 0] + y: [0, 1, 0] z: [0, 0, -1] - group: led udev: diff --git a/rootfs/usr/share/inputplumber/devices/50-rog_ally_x.yaml b/rootfs/usr/share/inputplumber/devices/50-rog_ally_x.yaml index a4b15a52..5d5028af 100644 --- a/rootfs/usr/share/inputplumber/devices/50-rog_ally_x.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-rog_ally_x.yaml @@ -43,7 +43,7 @@ source_devices: name: bmi323-imu mount_matrix: x: [1, 0, 0] - y: [0, -1, 0] + y: [0, 1, 0] z: [0, 0, -1] - group: led udev: diff --git a/src/drivers/lego/go2_driver.rs b/src/drivers/lego/go2_driver.rs index 7fa1b912..0c7faab1 100644 --- a/src/drivers/lego/go2_driver.rs +++ b/src/drivers/lego/go2_driver.rs @@ -448,7 +448,7 @@ impl Driver { events.push(Event::Axis(AxisEvent::MultiAccel(ImuAxisInput { pitch: -(state.left_accel_x + state.right_accel_x) / 2, roll: (state.left_accel_y + state.right_accel_y) / 2, - yaw: (state.left_accel_z + state.right_accel_z) / 2, + yaw: -(state.left_accel_z + state.right_accel_z) / 2, }))) } if !self @@ -491,7 +491,7 @@ impl Driver { events.push(Event::Axis(AxisEvent::MultiGyro(ImuAxisInput { pitch: -(state.left_gyro_x + state.right_gyro_x) / 2, roll: (state.left_gyro_y + state.right_gyro_y) / 2, - yaw: (state.left_gyro_z + state.right_gyro_z) / 2, + yaw: -(state.left_gyro_z + state.right_gyro_z) / 2, }))) } } From 215007bb55f0c6cea5d53ffc7a116d9de6c883bb Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Wed, 16 Sep 2026 16:18:58 -0700 Subject: [PATCH 3/7] fix: Gracefully shutdown all tasks Currently we don't track tasks started by manager. When InputPlumber stops the Drop::drop() callback never runs. As this will be necessary later when adding hrtimer, fix it now --- src/input/manager.rs | 30 ++++++++++++++++++++++++++---- src/main.rs | 2 ++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/input/manager.rs b/src/input/manager.rs index 1696f67b..8c6b1619 100644 --- a/src/input/manager.rs +++ b/src/input/manager.rs @@ -12,7 +12,7 @@ use mio::{Events, Interest, Poll, Token}; use thiserror::Error; use tokio::{ sync::mpsc, - task::{self, JoinHandle}, + task::{self, JoinSet}, }; use zbus::{fdo::ManagedObjects, zvariant::ObjectPath, Connection}; @@ -163,6 +163,8 @@ pub struct Manager { /// Map of target devices being used by a [CompositeDevice]. /// E.g. {"/org/shadowblip/InputPlumber/CompositeDevice0": Vec<"/org/shadowblip/InputPlumber/devices/target/dbus0">} composite_device_targets: HashMap>, + /// Track all composite device tasks so they can be gracefully stopped + composite_device_tasks: JoinSet<()>, /// Mapping of DBus path to its corresponding [CompositeDeviceConfig] /// E.g. {"/org/shadowblip/InputPlumber/CompositeDevice0": } used_configs: HashMap, @@ -215,6 +217,7 @@ impl Manager { used_configs: HashMap::new(), composite_device_sources: HashMap::new(), composite_device_targets: HashMap::new(), + composite_device_tasks: Default::default(), manage_all_devices: false, target_gamepad_order: vec![], target_gamepad_order_changing: false, @@ -259,6 +262,25 @@ impl Manager { Ok(()) } + /// Perform a gracefull shutdown of all [CompositeDevice] + pub async fn shutdown(&mut self) { + log::info!("Shutting down input manager"); + + for (path, device) in self.composite_devices.iter() { + if let Err(e) = device.stop().await { + log::warn!("Failed to send stop command to composite device {path}: {e:?}"); + } + } + + while let Some(res) = self.composite_device_tasks.join_next().await { + if let Err(e) = res { + log::warn!("Composite device task panicked during shutdown: {e:?}"); + } + } + + log::info!("Input manager shutdown complete"); + } + /// Manage events generated by various components async fn events_loop(&mut self) -> Result<(), Box> { // Loop and listen for command events @@ -816,7 +838,7 @@ impl Manager { config: CompositeDeviceConfig, target_types: Option>, source_device: SourceDevice, - ) -> Result, Box> { + ) -> Result<(), Box> { // Keep track of the source devices that this composite device is // using. let source_device_ids = device.get_source_devices_used(); @@ -883,7 +905,7 @@ impl Manager { // Run the device let composite_path = String::from(device.dbus_path()); let tx = self.tx.clone(); - let task = tokio::spawn(async move { + self.composite_device_tasks.spawn(async move { if let Err(e) = device.run().await { log::error!("Error running {composite_path}: {e}"); } @@ -900,7 +922,7 @@ impl Manager { } }); - Ok(task) + Ok(()) } /// Called when a composite device stops running diff --git a/src/main.rs b/src/main.rs index f203a8c4..20c6b95b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -98,6 +98,8 @@ async fn main() -> Result<(), Box> { } } + input_manager.shutdown().await; + // Unhide all devices on shutdown if let Err(e) = unhide_all().await { log::error!("Unable to un-hide devices: {:?}", e); From 91084805c5d33d7c6b06c922b3d8d108ecc319c7 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Wed, 16 Sep 2026 16:40:57 -0700 Subject: [PATCH 4/7] chore(capability): Remove deprecated capabilities Removes the Gamepad::Accelerometer and Gamepad::Gyro capabilities and all workarounds related to them. --- src/drivers/dualsense/driver.rs | 1 - src/drivers/ultimate_2/mod.rs | 3 - src/input/capability.rs | 39 ------------ src/input/composite_device/mod.rs | 62 +------------------ src/input/event/evdev.rs | 12 ++-- src/input/event/value.rs | 23 ------- src/input/source/hidraw/dualsense.rs | 9 +-- .../source/hidraw/flydigi_vader_4_pro.rs | 10 +-- src/input/source/hidraw/horipad_steam.rs | 12 ++-- src/input/source/hidraw/legos_imu.rs | 10 +-- src/input/source/hidraw/legos_xinput.rs | 2 - src/input/source/hidraw/steam_deck.rs | 12 ++-- src/input/source/iio/bmi_imu.rs | 10 +-- src/input/target/dualsense.rs | 55 ++-------------- src/input/target/horipad_steam.rs | 43 +++---------- src/input/target/steam_deck.rs | 34 ++-------- src/input/target/steam_deck_uhid.rs | 34 ++-------- src/input/target/ulitmate_2.rs | 48 ++------------ src/input/target/unified_gamepad.rs | 34 ---------- 19 files changed, 65 insertions(+), 388 deletions(-) diff --git a/src/drivers/dualsense/driver.rs b/src/drivers/dualsense/driver.rs index 06798d00..59411bcd 100644 --- a/src/drivers/dualsense/driver.rs +++ b/src/drivers/dualsense/driver.rs @@ -48,7 +48,6 @@ pub const STICK_Y_MAX: f64 = u8::MAX as f64; pub const TRIGGER_MAX: f64 = u8::MAX as f64; // DualSense hardware limits -pub const DS5_ACC_RES_PER_G: u32 = 8192; pub const DS5_TOUCHPAD_WIDTH: f64 = 1919.0; pub const DS5_TOUCHPAD_HEIGHT: f64 = 1079.0; diff --git a/src/drivers/ultimate_2/mod.rs b/src/drivers/ultimate_2/mod.rs index 4d17578f..92242833 100644 --- a/src/drivers/ultimate_2/mod.rs +++ b/src/drivers/ultimate_2/mod.rs @@ -12,8 +12,5 @@ pub const JOY_AXIS_MAX: f64 = 255.0; pub const JOY_AXIS_MIN: f64 = 0.0; pub const TRIGGER_AXIS_MAX: f64 = 255.0; -// Accel scale: 4096 raw units = 1G (derived from SDL_hidapi_8bitdo.c) -pub const ACCEL_SCALE: f64 = 4096.0; - pub const REPORT_ID_INPUT: u8 = 0x01; pub const REPORT_ID_RUMBLE: u8 = 0x05; diff --git a/src/input/capability.rs b/src/input/capability.rs index f9168730..7cb84423 100644 --- a/src/input/capability.rs +++ b/src/input/capability.rs @@ -49,8 +49,6 @@ impl Capability { Gamepad::Button(button) => format!("Gamepad:Button:{button}"), Gamepad::Axis(axis) => format!("Gamepad:Axis:{axis}"), Gamepad::Trigger(trigger) => format!("Gamepad:Trigger:{trigger}"), - Gamepad::Accelerometer => "Gamepad:Accelerometer".to_string(), - Gamepad::Gyro => "Gamepad:Gyro".to_string(), Gamepad::Dial(dial) => format!("Gamepad:Dial:{dial}"), }, Capability::Mouse(mouse) => match mouse { @@ -203,31 +201,6 @@ impl From for Capability { return Capability::Gamepad(Gamepad::Trigger(trigger)); } - // Gyro - if let Some(gyro_capability) = gamepad.gyro.as_ref() { - let gyro = Gamepad::from_str(&gyro_capability.name); - if gyro.is_err() { - log::error!("Invalid or unimplemented gyro: {}", gyro_capability.name); - return Capability::NotImplemented; - } - - return Capability::Gamepad(Gamepad::Gyro); - } - - // Accelerometer - if let Some(accelerometer_capability) = gamepad.accelerometer.as_ref() { - let accelerometer = Gamepad::from_str(&accelerometer_capability.name); - if accelerometer.is_err() { - log::error!( - "Invalid or unimplemented gyro: {}", - accelerometer_capability.name - ); - return Capability::NotImplemented; - } - - return Capability::Gamepad(Gamepad::Accelerometer); - } - // Dials/wheels if let Some(dial_config) = gamepad.dial.as_ref() { let dial = GamepadDial::from_str(&dial_config.name); @@ -385,14 +358,6 @@ pub enum Gamepad { /// Gamepad Trigger typically uses a single unsigned integar value that represents /// how far a trigger has been pulled Trigger(GamepadTrigger), - /// Accelerometer events measure the current acceleration of a device. This is - /// normally used to determine which way is "down" as there will be a constant - /// acceleration towards the center of the earth at 9.8 meters per second. - /// Typical will use (x, y, z) values normalized to meters per second. - Accelerometer, - /// Gyro events measure the angular velocity of a device measured - /// with (x, y, z) values normalized to degrees per second. - Gyro, /// Dials and wheels Dial(GamepadDial), } @@ -403,8 +368,6 @@ impl fmt::Display for Gamepad { Gamepad::Button(_) => write!(f, "Button"), Gamepad::Axis(_) => write!(f, "Axis"), Gamepad::Trigger(_) => write!(f, "Trigger"), - Gamepad::Accelerometer => write!(f, "Accelerometer"), - Gamepad::Gyro => write!(f, "Gyro"), Gamepad::Dial(_) => write!(f, "Dial"), } } @@ -427,8 +390,6 @@ impl FromStr for Gamepad { "Trigger" => Ok(Gamepad::Trigger(GamepadTrigger::from_str( parts.join(":").as_str(), )?)), - "Accelerometer" => Ok(Gamepad::Accelerometer), - "Gyro" => Ok(Gamepad::Gyro), "Dial" => Ok(Gamepad::Dial(GamepadDial::from_str( parts.join(":").as_str(), )?)), diff --git a/src/input/composite_device/mod.rs b/src/input/composite_device/mod.rs index 144372ad..fd2003f1 100644 --- a/src/input/composite_device/mod.rs +++ b/src/input/composite_device/mod.rs @@ -972,10 +972,7 @@ impl CompositeDevice { } } Gamepad::Dial(_) => {} - Gamepad::Axis(_) - | Gamepad::Trigger(_) - | Gamepad::Accelerometer - | Gamepad::Gyro => {} + Gamepad::Axis(_) | Gamepad::Trigger(_) => {} }, Capability::Mouse(ref t) => match t { Mouse::Motion => {} @@ -1104,28 +1101,6 @@ impl CompositeDevice { return Ok(()); } - //TODO: Temporary force of all Gyro and Accel events to legacy Gamepad:: format. - // Remove this after targets can handle translation profiles. - if matches!(cap, Capability::Accelerometer(_)) { - let event = NativeEvent::new_translated( - cap, - Capability::Gamepad(Gamepad::Accelerometer), - event.get_value(), - ); - self.targets.write_event(event).await; - return Ok(()); - } - - if matches!(cap, Capability::Gyroscope(_)) { - let event = NativeEvent::new_translated( - cap, - Capability::Gamepad(Gamepad::Gyro), - event.get_value(), - ); - self.targets.write_event(event).await; - return Ok(()); - } - // Write the event to all target devices capable of handling the event self.targets.write_event(event).await; @@ -1792,41 +1767,6 @@ impl CompositeDevice { } self.capabilities.insert(cap.clone()); } - // If Gyroscope or Accelerometer capabilties exist, report that Gamepad::Accelerometer and - // Gamepad::Gyro exist so the tester works. This is because we're blanket converting - // them before sending to targets. - //TODO: Remove this one Gamepad::Gyro/Gamepad::Accel are removed. - if self - .capabilities - .iter() - .any(|cap| matches!(cap, Capability::Accelerometer(_))) - { - self.capabilities - .retain(|cap| !matches!(cap, Capability::Accelerometer(_))); - if !self - .capabilities - .contains(&Capability::Gamepad(Gamepad::Accelerometer)) - { - self.capabilities - .insert(Capability::Gamepad(Gamepad::Accelerometer)); - } - } - if self - .capabilities - .iter() - .any(|cap| matches!(cap, Capability::Gyroscope(_))) - { - self.capabilities - .retain(|cap| !matches!(cap, Capability::Gyroscope(_))); - if !self - .capabilities - .contains(&Capability::Gamepad(Gamepad::Gyro)) - { - self.capabilities.insert(Capability::Gamepad(Gamepad::Gyro)); - } - } - - self.capabilities_by_source.insert(id.clone(), capabilities); // Get the output capabilities of the source device and keep track // of them. diff --git a/src/input/event/evdev.rs b/src/input/event/evdev.rs index 45fd6c66..fe460599 100644 --- a/src/input/event/evdev.rs +++ b/src/input/event/evdev.rs @@ -117,8 +117,12 @@ impl EvdevEvent { if let Some(info) = self.abs_info { let code = self.event.code(); match AbsoluteAxisCode(code) { - AbsoluteAxisCode::ABS_Z => normalize_unsigned_value(raw_value, info.minimum(), info.maximum()), - AbsoluteAxisCode::ABS_RZ => normalize_unsigned_value(raw_value, info.minimum(), info.maximum()), + AbsoluteAxisCode::ABS_Z => { + normalize_unsigned_value(raw_value, info.minimum(), info.maximum()) + } + AbsoluteAxisCode::ABS_RZ => { + normalize_unsigned_value(raw_value, info.minimum(), info.maximum()) + } _ => normalize_signed_value(raw_value, info.minimum(), info.maximum()), } } else { @@ -591,8 +595,6 @@ fn event_type_from_capability(capability: Capability) -> Option { Gamepad::Axis(_) => Some(EventType::ABSOLUTE), Gamepad::Trigger(_) => Some(EventType::ABSOLUTE), Gamepad::Dial(_) => Some(EventType::RELATIVE), - Gamepad::Accelerometer => None, - Gamepad::Gyro => None, }, _ => None, } @@ -694,8 +696,6 @@ fn event_codes_from_capability(capability: Capability) -> Vec { GamepadTrigger::RightTouchpadForce => vec![], GamepadTrigger::RightStickForce => vec![], }, - Gamepad::Accelerometer => vec![], - Gamepad::Gyro => vec![], Gamepad::Dial(dial) => match dial { GamepadDial::LeftStickDial => { vec![RelativeAxisCode::REL_HWHEEL.0] diff --git a/src/input/event/value.rs b/src/input/event/value.rs index 2ae9a12a..53c40b55 100644 --- a/src/input/event/value.rs +++ b/src/input/event/value.rs @@ -194,10 +194,6 @@ impl InputValue { Gamepad::Axis(_) => self.translate_button_to_axis(target_config), // Gamepad Button -> Trigger Gamepad::Trigger(_) => Ok(self.translate_button_to_trigger()), - // Gamepad Button -> Accelerometer - Gamepad::Accelerometer => Err(TranslationError::NotImplemented), - // Gamepad Button -> Gyro - Gamepad::Gyro => Err(TranslationError::NotImplemented), Gamepad::Dial(_) => Ok(self.clone()), }, // Gamepad Button -> Mouse @@ -281,10 +277,6 @@ impl InputValue { } // Axis -> Trigger Gamepad::Trigger(_) => Err(TranslationError::NotImplemented), - // Axis -> Accelerometer - Gamepad::Accelerometer => Err(TranslationError::NotImplemented), - // Axis -> Gyro - Gamepad::Gyro => Err(TranslationError::NotImplemented), Gamepad::Dial(_) => Err(TranslationError::NotImplemented), }, // Axis -> Mouse @@ -330,10 +322,6 @@ impl InputValue { } // Trigger -> Trigger Gamepad::Trigger(_) => Ok(self.clone()), - // Trigger -> Accelerometer - Gamepad::Accelerometer => Err(TranslationError::NotImplemented), - // Trigger -> Gyro - Gamepad::Gyro => Err(TranslationError::NotImplemented), Gamepad::Dial(_) => self.translate_trigger_to_button(source_config), }, // Trigger -> Mouse @@ -359,10 +347,6 @@ impl InputValue { // Trigger -> Accelerometer Capability::Accelerometer(_) => Err(TranslationError::NotImplemented), }, - // Accelerometer -> ... - Gamepad::Accelerometer => Err(TranslationError::NotImplemented), - // Gyro -> ... - Gamepad::Gyro => Err(TranslationError::NotImplemented), // Dial mapping to -> .. Gamepad::Dial(_) => match target_cap { Capability::None => Ok(InputValue::None), @@ -373,8 +357,6 @@ impl InputValue { Gamepad::Button(_) => self.translate_dial_to_button(source_config), Gamepad::Axis(_) => Err(TranslationError::NotImplemented), Gamepad::Trigger(_) => Err(TranslationError::NotImplemented), - Gamepad::Accelerometer => Err(TranslationError::NotImplemented), - Gamepad::Gyro => Err(TranslationError::NotImplemented), Gamepad::Dial(_) => Ok(self.clone()), }, Capability::Mouse(mouse) => match mouse { @@ -416,8 +398,6 @@ impl InputValue { Gamepad::Button(_) => Ok(self.clone()), Gamepad::Axis(_) => Err(TranslationError::NotImplemented), Gamepad::Trigger(_) => Err(TranslationError::NotImplemented), - Gamepad::Accelerometer => Err(TranslationError::NotImplemented), - Gamepad::Gyro => Err(TranslationError::NotImplemented), Gamepad::Dial(_) => Ok(self.clone()), }, // Keyboard Key -> Mouse @@ -1196,8 +1176,6 @@ impl InputValue { Gamepad::Button(_) => Ok(self.clone()), Gamepad::Axis(_) => self.translate_button_to_axis(target_config), Gamepad::Trigger(_) => Ok(self.translate_button_to_trigger()), - Gamepad::Accelerometer => Err(TranslationError::NotImplemented), - Gamepad::Gyro => Err(TranslationError::NotImplemented), Gamepad::Dial(_) => Ok(self.clone()), }, Capability::Mouse(mouse) => match mouse { @@ -1229,4 +1207,3 @@ impl InputValue { } } } - diff --git a/src/input/source/hidraw/dualsense.rs b/src/input/source/hidraw/dualsense.rs index 9abb5991..1af11a05 100644 --- a/src/input/source/hidraw/dualsense.rs +++ b/src/input/source/hidraw/dualsense.rs @@ -6,6 +6,7 @@ use packed_struct::types::SizedInteger; use crate::drivers::dualsense::driver::{DS5_EDGE_PID, DS5_PID, DS5_VID}; use crate::drivers::steam_deck::hid_report::PackedRumbleReport; +use crate::input::capability::Source; use crate::input::output_capability::{OutputCapability, LED}; use crate::{ drivers::dualsense::{self, driver::Driver}, @@ -338,7 +339,7 @@ fn translate_event(event: dualsense::event::Event) -> NativeEvent { }, dualsense::event::Event::Accelerometer(accel) => match accel { dualsense::event::AccelerometerEvent::Accelerometer(value) => NativeEvent::new( - Capability::Gamepad(Gamepad::Accelerometer), + Capability::Accelerometer(Source::Center), InputValue::Vector3 { x: Some(value.x as f64), y: Some(value.y as f64), @@ -346,7 +347,7 @@ fn translate_event(event: dualsense::event::Event) -> NativeEvent { }, ), dualsense::event::AccelerometerEvent::Gyro(value) => NativeEvent::new( - Capability::Gamepad(Gamepad::Gyro), + Capability::Gyroscope(Source::Center), InputValue::Vector3 { x: Some(value.x as f64), y: Some(value.y as f64), @@ -449,7 +450,7 @@ fn normalize_trigger_value(event: &dualsense::event::TriggerEvent) -> InputValue /// List of all capabilities that the DualSense driver implements pub const CAPABILITIES: &[Capability] = &[ - Capability::Gamepad(Gamepad::Accelerometer), + Capability::Accelerometer(Source::Center), Capability::Gamepad(Gamepad::Axis(GamepadAxis::LeftStick)), Capability::Gamepad(Gamepad::Axis(GamepadAxis::RightStick)), Capability::Gamepad(Gamepad::Button(GamepadButton::DPadDown)), @@ -476,9 +477,9 @@ pub const CAPABILITIES: &[Capability] = &[ Capability::Gamepad(Gamepad::Button(GamepadButton::South)), Capability::Gamepad(Gamepad::Button(GamepadButton::Start)), Capability::Gamepad(Gamepad::Button(GamepadButton::West)), - Capability::Gamepad(Gamepad::Gyro), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTrigger)), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTrigger)), + Capability::Gyroscope(Source::Center), Capability::Touchpad(Touchpad::CenterPad(Touch::Button(TouchButton::Press))), Capability::Touchpad(Touchpad::CenterPad(Touch::Button(TouchButton::Touch))), Capability::Touchpad(Touchpad::CenterPad(Touch::Motion)), diff --git a/src/input/source/hidraw/flydigi_vader_4_pro.rs b/src/input/source/hidraw/flydigi_vader_4_pro.rs index 1a8d4845..85dcc48d 100644 --- a/src/input/source/hidraw/flydigi_vader_4_pro.rs +++ b/src/input/source/hidraw/flydigi_vader_4_pro.rs @@ -6,7 +6,7 @@ use crate::{ event, }, input::{ - capability::{Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger}, + capability::{Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger, Source}, event::{ native::NativeEvent, value::InputValue, @@ -236,7 +236,7 @@ fn translate_event(event: event::Event) -> NativeEvent { }, event::Event::Inertia(accel_event) => match accel_event { event::InertialEvent::Accelerometer(value) => NativeEvent::new( - Capability::Gamepad(Gamepad::Accelerometer), + Capability::Accelerometer(Source::Center), InputValue::Vector3 { x: Some(value.x as f64), y: Some(value.y as f64), @@ -244,7 +244,7 @@ fn translate_event(event: event::Event) -> NativeEvent { }, ), event::InertialEvent::Gyro(value) => NativeEvent::new( - Capability::Gamepad(Gamepad::Gyro), + Capability::Gyroscope(Source::Center), InputValue::Vector3 { x: Some(value.x as f64), y: Some(value.y as f64), @@ -257,7 +257,7 @@ fn translate_event(event: event::Event) -> NativeEvent { /// List of all capabilities that the driver implements pub const CAPABILITIES: &[Capability] = &[ - Capability::Gamepad(Gamepad::Accelerometer), + Capability::Accelerometer(Source::Center), Capability::Gamepad(Gamepad::Axis(GamepadAxis::LeftStick)), Capability::Gamepad(Gamepad::Axis(GamepadAxis::RightStick)), Capability::Gamepad(Gamepad::Button(GamepadButton::DPadDown)), @@ -284,7 +284,7 @@ pub const CAPABILITIES: &[Capability] = &[ Capability::Gamepad(Gamepad::Button(GamepadButton::South)), Capability::Gamepad(Gamepad::Button(GamepadButton::Start)), Capability::Gamepad(Gamepad::Button(GamepadButton::West)), - Capability::Gamepad(Gamepad::Gyro), + Capability::Gyroscope(Source::Center), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTrigger)), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTrigger)), ]; diff --git a/src/input/source/hidraw/horipad_steam.rs b/src/input/source/hidraw/horipad_steam.rs index 6f1b25bc..2e4566f7 100644 --- a/src/input/source/hidraw/horipad_steam.rs +++ b/src/input/source/hidraw/horipad_steam.rs @@ -3,7 +3,7 @@ use std::{error::Error, fmt::Debug}; use crate::{ drivers::horipad_steam::{driver::Driver, event, JOY_AXIS_MAX, JOY_AXIS_MIN, TRIGGER_AXIS_MAX}, input::{ - capability::{Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger}, + capability::{Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger, Source}, event::{ native::NativeEvent, value::{normalize_signed_value, normalize_unsigned_value, InputValue}, @@ -224,9 +224,9 @@ fn translate_event(event: event::Event) -> NativeEvent { normalize_trigger_value(trigg), ), }, - event::Event::Inertia(accel_event) => match accel_event { + event::Event::Inertia(intertia) => match intertia { event::InertialEvent::Accelerometer(value) => NativeEvent::new( - Capability::Gamepad(Gamepad::Accelerometer), + Capability::Accelerometer(Source::Center), InputValue::Vector3 { x: Some(value.x as f64), y: Some(value.y as f64), @@ -234,7 +234,7 @@ fn translate_event(event: event::Event) -> NativeEvent { }, ), event::InertialEvent::Gyro(value) => NativeEvent::new( - Capability::Gamepad(Gamepad::Gyro), + Capability::Gyroscope(Source::Center), InputValue::Vector3 { x: Some(value.x as f64), y: Some(value.y as f64), @@ -247,7 +247,7 @@ fn translate_event(event: event::Event) -> NativeEvent { /// List of all capabilities that the driver implements pub const CAPABILITIES: &[Capability] = &[ - Capability::Gamepad(Gamepad::Accelerometer), + Capability::Accelerometer(Source::Center), Capability::Gamepad(Gamepad::Axis(GamepadAxis::LeftStick)), Capability::Gamepad(Gamepad::Axis(GamepadAxis::RightStick)), Capability::Gamepad(Gamepad::Button(GamepadButton::DPadDown)), @@ -274,7 +274,7 @@ pub const CAPABILITIES: &[Capability] = &[ Capability::Gamepad(Gamepad::Button(GamepadButton::South)), Capability::Gamepad(Gamepad::Button(GamepadButton::Start)), Capability::Gamepad(Gamepad::Button(GamepadButton::West)), - Capability::Gamepad(Gamepad::Gyro), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTrigger)), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTrigger)), + Capability::Gyroscope(Source::Center), ]; diff --git a/src/input/source/hidraw/legos_imu.rs b/src/input/source/hidraw/legos_imu.rs index 22925859..2a6ce4d3 100644 --- a/src/input/source/hidraw/legos_imu.rs +++ b/src/input/source/hidraw/legos_imu.rs @@ -3,7 +3,7 @@ use std::{error::Error, fmt::Debug}; use crate::{ drivers::legos::{event, imu_driver::IMUDriver}, input::{ - capability::{Capability, Gamepad}, + capability::{Capability, Source}, event::{native::NativeEvent, value::InputValue}, output_event::OutputEvent, source::{InputError, OutputError, SourceInputDevice, SourceOutputDevice}, @@ -64,7 +64,7 @@ fn translate_event(event: event::Event) -> NativeEvent { match event { event::Event::Inertia(motion) => match motion { event::InertialEvent::Accelerometer(value) => NativeEvent::new( - Capability::Gamepad(Gamepad::Accelerometer), + Capability::Accelerometer(Source::Center), InputValue::Vector3 { x: Some(value.x as f64), y: Some(value.y as f64), @@ -72,7 +72,7 @@ fn translate_event(event: event::Event) -> NativeEvent { }, ), event::InertialEvent::Gyro(value) => NativeEvent::new( - Capability::Gamepad(Gamepad::Gyro), + Capability::Gyroscope(Source::Center), InputValue::Vector3 { x: Some(value.x as f64), y: Some(value.y as f64), @@ -86,6 +86,6 @@ fn translate_event(event: event::Event) -> NativeEvent { /// List of all capabilities that the Legion Go driver implements pub const CAPABILITIES: &[Capability] = &[ - Capability::Gamepad(Gamepad::Accelerometer), - Capability::Gamepad(Gamepad::Gyro), + Capability::Accelerometer(Source::Center), + Capability::Gyroscope(Source::Center), ]; diff --git a/src/input/source/hidraw/legos_xinput.rs b/src/input/source/hidraw/legos_xinput.rs index 7fbefc66..f13ece3b 100644 --- a/src/input/source/hidraw/legos_xinput.rs +++ b/src/input/source/hidraw/legos_xinput.rs @@ -374,7 +374,6 @@ fn translate_event(event: event::Event) -> NativeEvent { /// List of all capabilities that the Legion Go driver implements pub const CAPABILITIES: &[Capability] = &[ - Capability::Gamepad(Gamepad::Accelerometer), Capability::Gamepad(Gamepad::Axis(GamepadAxis::LeftStick)), Capability::Gamepad(Gamepad::Axis(GamepadAxis::RightStick)), Capability::Gamepad(Gamepad::Button(GamepadButton::DPadDown)), @@ -397,7 +396,6 @@ pub const CAPABILITIES: &[Capability] = &[ Capability::Gamepad(Gamepad::Button(GamepadButton::South)), Capability::Gamepad(Gamepad::Button(GamepadButton::Start)), Capability::Gamepad(Gamepad::Button(GamepadButton::West)), - Capability::Gamepad(Gamepad::Gyro), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTrigger)), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTrigger)), ]; diff --git a/src/input/source/hidraw/steam_deck.rs b/src/input/source/hidraw/steam_deck.rs index ee52077f..91d28717 100644 --- a/src/input/source/hidraw/steam_deck.rs +++ b/src/input/source/hidraw/steam_deck.rs @@ -21,8 +21,8 @@ use crate::{ }, input::{ capability::{ - Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger, Touch, TouchButton, - Touchpad, + Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger, Source, Touch, + TouchButton, Touchpad, }, event::{ native::NativeEvent, @@ -530,7 +530,7 @@ fn translate_event(event: steam_deck::event::Event) -> NativeEvent { }, steam_deck::event::Event::Accelerometer(accel) => match accel { steam_deck::event::AccelerometerEvent::Accelerometer(value) => NativeEvent::new( - Capability::Gamepad(Gamepad::Accelerometer), + Capability::Accelerometer(Source::Center), InputValue::Vector3 { x: Some(value.x as f64 * ACCEL_SCALE), y: Some(value.y as f64 * ACCEL_SCALE), @@ -538,7 +538,7 @@ fn translate_event(event: steam_deck::event::Event) -> NativeEvent { }, ), steam_deck::event::AccelerometerEvent::Attitude(value) => NativeEvent::new( - Capability::Gamepad(Gamepad::Gyro), + Capability::Gyroscope(Source::Center), InputValue::Vector3 { x: Some(value.x as f64), y: Some(value.y as f64), @@ -591,7 +591,7 @@ fn translate_event(event: steam_deck::event::Event) -> NativeEvent { /// List of all capabilities that the Steam Deck driver implements pub const CAPABILITIES: &[Capability] = &[ - Capability::Gamepad(Gamepad::Accelerometer), + Capability::Accelerometer(Source::Center), Capability::Gamepad(Gamepad::Axis(GamepadAxis::LeftStick)), Capability::Gamepad(Gamepad::Axis(GamepadAxis::RightStick)), Capability::Gamepad(Gamepad::Button(GamepadButton::DPadDown)), @@ -618,9 +618,9 @@ pub const CAPABILITIES: &[Capability] = &[ Capability::Gamepad(Gamepad::Button(GamepadButton::South)), Capability::Gamepad(Gamepad::Button(GamepadButton::Start)), Capability::Gamepad(Gamepad::Button(GamepadButton::West)), - Capability::Gamepad(Gamepad::Gyro), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTrigger)), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTrigger)), + Capability::Gyroscope(Source::Center), Capability::Touchpad(Touchpad::LeftPad(Touch::Button(TouchButton::Press))), Capability::Touchpad(Touchpad::LeftPad(Touch::Button(TouchButton::Touch))), Capability::Touchpad(Touchpad::LeftPad(Touch::Motion)), diff --git a/src/input/source/iio/bmi_imu.rs b/src/input/source/iio/bmi_imu.rs index 329c675f..0480bdfe 100644 --- a/src/input/source/iio/bmi_imu.rs +++ b/src/input/source/iio/bmi_imu.rs @@ -4,7 +4,7 @@ use crate::{ config, drivers::iio_imu::{self, driver::Driver, info::MountMatrix}, input::{ - capability::{Capability, Gamepad}, + capability::{Capability, Source}, event::{native::NativeEvent, value::InputValue}, source::{InputError, SourceInputDevice, SourceOutputDevice}, }, @@ -100,7 +100,7 @@ fn translate_events(events: Vec) -> Vec { fn translate_event(event: iio_imu::event::Event) -> NativeEvent { match event { iio_imu::event::Event::Accelerometer(data) => { - let cap = Capability::Gamepad(Gamepad::Accelerometer); + let cap = Capability::Accelerometer(Source::Center); let value = InputValue::Vector3 { x: Some(data.roll), y: Some(data.pitch), @@ -114,7 +114,7 @@ fn translate_event(event: iio_imu::event::Event) -> NativeEvent { // Adjusting the scale will increase the granularity of the motion by slowing // incrementing closer to 2:1 motion. From testing this is the highest scale we can // apply before noise is amplified to the point the gyro cannot calibrate. - let cap = Capability::Gamepad(Gamepad::Gyro); + let cap = Capability::Gyroscope(Source::Center); let value = InputValue::Vector3 { x: Some(data.roll * (180.0 / PI) * 12.0), y: Some(data.pitch * (180.0 / PI) * 12.0), @@ -127,6 +127,6 @@ fn translate_event(event: iio_imu::event::Event) -> NativeEvent { /// List of all capabilities that the driver implements pub const CAPABILITIES: &[Capability] = &[ - Capability::Gamepad(Gamepad::Accelerometer), - Capability::Gamepad(Gamepad::Gyro), + Capability::Accelerometer(Source::Center), + Capability::Gyroscope(Source::Center), ]; diff --git a/src/input/target/dualsense.rs b/src/input/target/dualsense.rs index dbbc87ea..133dfec8 100644 --- a/src/input/target/dualsense.rs +++ b/src/input/target/dualsense.rs @@ -4,6 +4,7 @@ //! https://github.com/NeroReflex/ROGueENEMY/ use std::{cmp::Ordering, error::Error, fmt::Debug, fs::File, time::Duration}; +use crate::input::capability::Source; use packed_struct::prelude::*; use rand::Rng; use uhid_virt::{Bus, CreateParams, StreamError, UHIDDevice}; @@ -11,8 +12,8 @@ use uhid_virt::{Bus, CreateParams, StreamError, UHIDDevice}; use crate::{ drivers::dualsense::{ driver::{ - DS5_ACC_RES_PER_G, DS5_EDGE_NAME, DS5_EDGE_PID, DS5_EDGE_VERSION, DS5_EDGE_VID, - DS5_NAME, DS5_PID, DS5_TOUCHPAD_HEIGHT, DS5_TOUCHPAD_WIDTH, DS5_VERSION, DS5_VID, + DS5_EDGE_NAME, DS5_EDGE_PID, DS5_EDGE_VERSION, DS5_EDGE_VID, DS5_NAME, DS5_PID, + DS5_TOUCHPAD_HEIGHT, DS5_TOUCHPAD_WIDTH, DS5_VERSION, DS5_VID, FEATURE_REPORT_CALIBRATION, FEATURE_REPORT_FIRMWARE_INFO, FEATURE_REPORT_PAIRING_INFO, OUTPUT_REPORT_BT, OUTPUT_REPORT_BT_SIZE, OUTPUT_REPORT_USB, OUTPUT_REPORT_USB_SHORT_SIZE, OUTPUT_REPORT_USB_SIZE, STICK_X_MAX, STICK_X_MIN, @@ -556,32 +557,6 @@ impl DualSenseDevice { GamepadTrigger::RightTouchpadForce => (), GamepadTrigger::RightStickForce => (), }, - Gamepad::Accelerometer => { - if let InputValue::Vector3 { x, y, z } = value { - if let Some(x) = x { - state.accel_x = Integer::from_primitive(denormalize_accel_value(x)) - } - if let Some(y) = y { - state.accel_y = Integer::from_primitive(denormalize_accel_value(y)) - } - if let Some(z) = z { - state.accel_z = Integer::from_primitive(denormalize_accel_value(z)) - } - } - } - Gamepad::Gyro => { - if let InputValue::Vector3 { x, y, z } = value { - if let Some(x) = x { - state.pitch = Integer::from_primitive(denormalize_gyro_value(x)); - } - if let Some(y) = y { - state.yaw = Integer::from_primitive(denormalize_gyro_value(y)) - } - if let Some(z) = z { - state.roll = Integer::from_primitive(denormalize_gyro_value(z)) - } - } - } _ => (), }, //TODO: Remove RightPad when we add target profiles @@ -979,7 +954,7 @@ impl TargetInputDevice for DualSenseDevice { fn get_capabilities(&self) -> Result, InputError> { Ok(vec![ - Capability::Gamepad(Gamepad::Accelerometer), + Capability::Accelerometer(Source::Center), Capability::Gamepad(Gamepad::Axis(GamepadAxis::LeftStick)), Capability::Gamepad(Gamepad::Axis(GamepadAxis::RightStick)), Capability::Gamepad(Gamepad::Button(GamepadButton::DPadDown)), @@ -1005,9 +980,9 @@ impl TargetInputDevice for DualSenseDevice { Capability::Gamepad(Gamepad::Button(GamepadButton::South)), Capability::Gamepad(Gamepad::Button(GamepadButton::Start)), Capability::Gamepad(Gamepad::Button(GamepadButton::West)), - Capability::Gamepad(Gamepad::Gyro), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTrigger)), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTrigger)), + Capability::Gyroscope(Source::Center), Capability::Touchpad(Touchpad::CenterPad(Touch::Button(TouchButton::Press))), Capability::Touchpad(Touchpad::CenterPad(Touch::Button(TouchButton::Touch))), Capability::Touchpad(Touchpad::CenterPad(Touch::Motion)), @@ -1187,23 +1162,3 @@ impl Debug for DualSenseDevice { .finish() } } - -/// De-normalizes the given value in meters per second into a real value that -/// the DS5 controller understands. -/// DualSense accelerometer values are measured in [DS5_ACC_RES_PER_G] -/// units of G acceleration (1G == 9.8m/s). InputPlumber accelerometer -/// values are measured in units of meters per second. To denormalize -/// the value, it needs to be converted into G units (by dividing by 9.8), -/// then multiplying that value by the [DS5_ACC_RES_PER_G]. -fn denormalize_accel_value(value_meters_sec: f64) -> i16 { - let value_g = value_meters_sec / 9.8; - let value = value_g * DS5_ACC_RES_PER_G as f64; - value as i16 -} - -/// DualSense gyro values are measured in units of degrees per second. -/// InputPlumber gyro values are also measured in degrees per second. -fn denormalize_gyro_value(value_degrees_sec: f64) -> i16 { - let value = value_degrees_sec; - value as i16 -} diff --git a/src/input/target/horipad_steam.rs b/src/input/target/horipad_steam.rs index c94f8ffc..6bd97456 100644 --- a/src/input/target/horipad_steam.rs +++ b/src/input/target/horipad_steam.rs @@ -2,6 +2,7 @@ use std::{cmp::Ordering, error::Error, fmt::Debug, fs::File, time::Duration}; use packed_struct::prelude::*; +use crate::input::capability::Source; use uhid_virt::{Bus, CreateParams, StreamError, UHIDDevice}; use crate::{ @@ -240,57 +241,31 @@ impl HoripadSteamDevice { GamepadTrigger::RightTouchpadForce => (), GamepadTrigger::RightStickForce => (), }, - Gamepad::Accelerometer => { - if let InputValue::Vector3 { x, y, z } = value { - if let Some(x) = x { - self.state.accel_x = Integer::from_primitive(denormalize_accel_value(x)) - } - if let Some(y) = y { - self.state.accel_y = Integer::from_primitive(denormalize_accel_value(y)) - } - if let Some(z) = z { - self.state.accel_z = Integer::from_primitive(denormalize_accel_value(z)) - } - } - } - Gamepad::Gyro => { - if let InputValue::Vector3 { x, y, z } = value { - if let Some(x) = x { - self.state.pitch = Integer::from_primitive(denormalize_gyro_value(x)); - } - if let Some(y) = y { - self.state.yaw = Integer::from_primitive(denormalize_gyro_value(y)) - } - if let Some(z) = z { - self.state.roll = Integer::from_primitive(denormalize_gyro_value(z)) - } - } - } _ => (), }, Capability::Gyroscope(_) => { if let InputValue::Vector3 { x, y, z } = value { if let Some(x) = x { - self.state.pitch = Integer::from_primitive(x as i16); + self.state.pitch = Integer::from_primitive(denormalize_gyro_value(x)); } if let Some(y) = y { - self.state.yaw = Integer::from_primitive(y as i16); + self.state.yaw = Integer::from_primitive(denormalize_gyro_value(y)); } if let Some(z) = z { - self.state.roll = Integer::from_primitive(z as i16); + self.state.roll = Integer::from_primitive(denormalize_gyro_value(z)); } } } Capability::Accelerometer(_) => { if let InputValue::Vector3 { x, y, z } = value { if let Some(x) = x { - self.state.accel_x = Integer::from_primitive(x as i16); + self.state.accel_x = Integer::from_primitive(denormalize_accel_value(x)); } if let Some(y) = y { - self.state.accel_y = Integer::from_primitive(y as i16); + self.state.accel_y = Integer::from_primitive(denormalize_accel_value(y)); } if let Some(z) = z { - self.state.accel_z = Integer::from_primitive(z as i16); + self.state.accel_z = Integer::from_primitive(denormalize_accel_value(z)); } } } @@ -332,7 +307,7 @@ impl TargetInputDevice for HoripadSteamDevice { fn get_capabilities(&self) -> Result, InputError> { Ok(vec![ - Capability::Gamepad(Gamepad::Accelerometer), + Capability::Accelerometer(Source::Center), Capability::Gamepad(Gamepad::Axis(GamepadAxis::LeftStick)), Capability::Gamepad(Gamepad::Axis(GamepadAxis::RightStick)), Capability::Gamepad(Gamepad::Button(GamepadButton::DPadDown)), @@ -360,9 +335,9 @@ impl TargetInputDevice for HoripadSteamDevice { Capability::Gamepad(Gamepad::Button(GamepadButton::South)), Capability::Gamepad(Gamepad::Button(GamepadButton::Start)), Capability::Gamepad(Gamepad::Button(GamepadButton::West)), - Capability::Gamepad(Gamepad::Gyro), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTrigger)), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTrigger)), + Capability::Gyroscope(Source::Center), ]) } diff --git a/src/input/target/steam_deck.rs b/src/input/target/steam_deck.rs index e4199eae..a5db6615 100644 --- a/src/input/target/steam_deck.rs +++ b/src/input/target/steam_deck.rs @@ -33,8 +33,8 @@ use crate::{ }, input::{ capability::{ - Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger, Touch, TouchButton, - Touchpad, + Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger, Source, Touch, + TouchButton, Touchpad, }, composite_device::client::CompositeDeviceClient, event::{ @@ -678,32 +678,6 @@ impl SteamDeckDevice { } } }, - Gamepad::Accelerometer => { - if let InputValue::Vector3 { x, y, z } = value { - if let Some(x) = x { - self.state.accel_x = Integer::from_primitive(x as i16); - } - if let Some(y) = y { - self.state.accel_y = Integer::from_primitive(y as i16); - } - if let Some(z) = z { - self.state.accel_z = Integer::from_primitive(z as i16); - } - } - } - Gamepad::Gyro => { - if let InputValue::Vector3 { x, y, z } = value { - if let Some(x) = x { - self.state.pitch = Integer::from_primitive(x as i16); - } - if let Some(y) = y { - self.state.yaw = Integer::from_primitive(y as i16); - } - if let Some(z) = z { - self.state.roll = Integer::from_primitive(z as i16); - } - } - } _ => (), }, Capability::Touchpad(touch) => match touch { @@ -832,7 +806,7 @@ impl TargetInputDevice for SteamDeckDevice { fn get_capabilities(&self) -> Result, InputError> { Ok(vec![ - Capability::Gamepad(Gamepad::Accelerometer), + Capability::Accelerometer(Source::Center), Capability::Gamepad(Gamepad::Axis(GamepadAxis::LeftStick)), Capability::Gamepad(Gamepad::Axis(GamepadAxis::RightStick)), Capability::Gamepad(Gamepad::Button(GamepadButton::DPadDown)), @@ -860,13 +834,13 @@ impl TargetInputDevice for SteamDeckDevice { Capability::Gamepad(Gamepad::Button(GamepadButton::South)), Capability::Gamepad(Gamepad::Button(GamepadButton::Start)), Capability::Gamepad(Gamepad::Button(GamepadButton::West)), - Capability::Gamepad(Gamepad::Gyro), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftStickForce)), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTouchpadForce)), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTrigger)), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightStickForce)), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTouchpadForce)), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTrigger)), + Capability::Gyroscope(Source::Center), Capability::Touchpad(Touchpad::LeftPad(Touch::Button(TouchButton::Press))), Capability::Touchpad(Touchpad::LeftPad(Touch::Button(TouchButton::Touch))), Capability::Touchpad(Touchpad::LeftPad(Touch::Motion)), diff --git a/src/input/target/steam_deck_uhid.rs b/src/input/target/steam_deck_uhid.rs index 786f2052..71f3e3d8 100644 --- a/src/input/target/steam_deck_uhid.rs +++ b/src/input/target/steam_deck_uhid.rs @@ -26,8 +26,8 @@ use crate::{ }, input::{ capability::{ - Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger, Touch, TouchButton, - Touchpad, + Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger, Source, Touch, + TouchButton, Touchpad, }, composite_device::client::CompositeDeviceClient, event::{ @@ -283,32 +283,6 @@ impl SteamDeckUhidDevice { } } }, - Gamepad::Accelerometer => { - if let InputValue::Vector3 { x, y, z } = value { - if let Some(x) = x { - self.state.accel_x = Integer::from_primitive(x as i16); - } - if let Some(y) = y { - self.state.accel_y = Integer::from_primitive(y as i16); - } - if let Some(z) = z { - self.state.accel_z = Integer::from_primitive(z as i16); - } - } - } - Gamepad::Gyro => { - if let InputValue::Vector3 { x, y, z } = value { - if let Some(x) = x { - self.state.pitch = Integer::from_primitive(x as i16); - } - if let Some(y) = y { - self.state.yaw = Integer::from_primitive(y as i16); - } - if let Some(z) = z { - self.state.roll = Integer::from_primitive(z as i16); - } - } - } _ => (), }, Capability::Touchpad(touch) => match touch { @@ -732,7 +706,7 @@ impl TargetInputDevice for SteamDeckUhidDevice { fn get_capabilities(&self) -> Result, InputError> { Ok(vec![ - Capability::Gamepad(Gamepad::Accelerometer), + Capability::Accelerometer(Source::Center), Capability::Gamepad(Gamepad::Axis(GamepadAxis::LeftStick)), Capability::Gamepad(Gamepad::Axis(GamepadAxis::RightStick)), Capability::Gamepad(Gamepad::Button(GamepadButton::DPadDown)), @@ -760,13 +734,13 @@ impl TargetInputDevice for SteamDeckUhidDevice { Capability::Gamepad(Gamepad::Button(GamepadButton::South)), Capability::Gamepad(Gamepad::Button(GamepadButton::Start)), Capability::Gamepad(Gamepad::Button(GamepadButton::West)), - Capability::Gamepad(Gamepad::Gyro), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftStickForce)), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTouchpadForce)), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTrigger)), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightStickForce)), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTouchpadForce)), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTrigger)), + Capability::Gyroscope(Source::Center), Capability::Touchpad(Touchpad::LeftPad(Touch::Button(TouchButton::Press))), Capability::Touchpad(Touchpad::LeftPad(Touch::Button(TouchButton::Touch))), Capability::Touchpad(Touchpad::LeftPad(Touch::Motion)), diff --git a/src/input/target/ulitmate_2.rs b/src/input/target/ulitmate_2.rs index dc1ddb3b..e67f053d 100644 --- a/src/input/target/ulitmate_2.rs +++ b/src/input/target/ulitmate_2.rs @@ -14,10 +14,10 @@ use crate::{ drivers::ultimate_2::{ hid_report::{DPadDirection, PackedInputDataReport, PackedRumbleOutputReport}, report_descriptor::REPORT_DESCRIPTOR, - ACCEL_SCALE, JOY_AXIS_MAX, PID, REPORT_ID_RUMBLE, TRIGGER_AXIS_MAX, VID, + JOY_AXIS_MAX, PID, REPORT_ID_RUMBLE, TRIGGER_AXIS_MAX, VID, }, input::{ - capability::{Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger}, + capability::{Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger, Source}, composite_device::client::CompositeDeviceClient, event::{ native::{NativeEvent, ScheduledNativeEvent}, @@ -31,8 +31,6 @@ use crate::{ use super::{InputError, OutputError, TargetInputDevice, TargetOutputDevice}; -const GRAVITY: f64 = 9.80665; - // The minimum interval between button events must wait between // each other for chords. const MIN_CHORD_TIME: Duration = Duration::from_millis(80); @@ -186,38 +184,6 @@ impl Ultimate2WirelessDevice { _ => (), }, - // Axis layout (x=pitch, y=yaw, z=roll): yaw/roll axes are swapped - // relative to SDL sGyro/sAccel naming; pitch is negated. - Gamepad::Accelerometer => { - if let InputValue::Vector3 { x, y, z } = value { - if let Some(x) = x { - self.state.accel_y = - Integer::from_primitive(denormalize_accel(x).wrapping_neg()); - } - if let Some(y) = y { - self.state.accel_z = Integer::from_primitive(denormalize_accel(y)); - } - if let Some(z) = z { - self.state.accel_x = - Integer::from_primitive(denormalize_accel(z).wrapping_neg()); - } - } - } - - Gamepad::Gyro => { - if let InputValue::Vector3 { x, y, z } = value { - if let Some(x) = x { - self.state.gyro_y = Integer::from_primitive((x as i16).wrapping_neg()); - } - if let Some(y) = y { - self.state.gyro_z = Integer::from_primitive(y as i16); - } - if let Some(z) = z { - self.state.gyro_x = Integer::from_primitive((z as i16).wrapping_neg()); - } - } - } - _ => (), }, @@ -341,8 +307,7 @@ impl TargetInputDevice for Ultimate2WirelessDevice { fn get_capabilities(&self) -> Result, InputError> { Ok(vec![ - Capability::Gamepad(Gamepad::Accelerometer), - Capability::Gamepad(Gamepad::Gyro), + Capability::Accelerometer(Source::Center), Capability::Gamepad(Gamepad::Button(GamepadButton::QuickAccess)), Capability::Gamepad(Gamepad::Button(GamepadButton::Screenshot)), Capability::Gamepad(Gamepad::Axis(GamepadAxis::LeftStick)), @@ -370,6 +335,7 @@ impl TargetInputDevice for Ultimate2WirelessDevice { Capability::Gamepad(Gamepad::Button(GamepadButton::West)), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTrigger)), Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTrigger)), + Capability::Gyroscope(Source::Center), ]) } @@ -468,9 +434,3 @@ impl Debug for Ultimate2WirelessDevice { .finish() } } - -// m/s² → raw i16 (4096 units = 1G) -fn denormalize_accel(value_m_s2: f64) -> i16 { - let g = value_m_s2 / GRAVITY; - (g * ACCEL_SCALE).clamp(i16::MIN as f64, i16::MAX as f64) as i16 -} diff --git a/src/input/target/unified_gamepad.rs b/src/input/target/unified_gamepad.rs index fe1bbbd3..07ef9012 100644 --- a/src/input/target/unified_gamepad.rs +++ b/src/input/target/unified_gamepad.rs @@ -463,36 +463,6 @@ impl From for StateUpdate { Self { capability, value } } - Gamepad::Accelerometer => { - let value = match event.get_value() { - InputValue::Vector3 { x, y, z } => Int16Vector3Update { - x: x.map(|x| (x * ACCEL_SCALE_FACTOR) as i16), - y: y.map(|y| (y * ACCEL_SCALE_FACTOR) as i16), - z: z.map(|z| (z * ACCEL_SCALE_FACTOR) as i16), - }, - _ => { - return Self::default(); - } - }; - let value = ValueUpdate::Int16Vector3(value); - - Self { capability, value } - } - Gamepad::Gyro => { - let value = match event.get_value() { - InputValue::Vector3 { x, y, z } => Int16Vector3Update { - x: x.map(|x| (x * GYRO_SCALE_FACTOR) as i16), - y: y.map(|y| (y * GYRO_SCALE_FACTOR) as i16), - z: z.map(|z| (z * GYRO_SCALE_FACTOR) as i16), - }, - _ => { - return Self::default(); - } - }; - let value = ValueUpdate::Int16Vector3(value); - - Self { capability, value } - } Gamepad::Dial(_) => { let value = match event.get_value() { InputValue::Bool(n) => BoolUpdate { value: n }, @@ -780,8 +750,6 @@ impl From for InputCapability { GamepadTrigger::RightTouchpadForce => Self::GamepadTriggerRightTouchpadForce, GamepadTrigger::RightStickForce => Self::GamepadTriggerRightStickForce, }, - Gamepad::Accelerometer => Self::AccelerometerCenter, - Gamepad::Gyro => Self::GyroscopeCenter, Gamepad::Dial(dial) => match dial { GamepadDial::LeftStickDial => Self::GamepadDialLeft, GamepadDial::RightStickDial => Self::GamepadDialRight, @@ -1011,8 +979,6 @@ impl From for InputCapabilityInfo { Gamepad::Button(_) => Self::new(capability, ValueType::Bool), Gamepad::Axis(_) => Self::new(capability, ValueType::UInt16Vector2), Gamepad::Trigger(_) => Self::new(capability, ValueType::UInt8), - Gamepad::Accelerometer => Self::new(capability, ValueType::Int16Vector3), - Gamepad::Gyro => Self::new(capability, ValueType::Int16Vector3), Gamepad::Dial(_) => Self::new(capability, ValueType::Int8), }, Capability::Mouse(_) => Self::default(), From f5d72788d5a4df835127445bee48816ad003d728 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Wed, 16 Sep 2026 16:40:57 -0700 Subject: [PATCH 5/7] feat(IMU): Refactor iio_imu drivers Separates accel_gyro_3d and bmi_imu drivers as they have different handling requirements and refactors them to eliminate the flawed industrial-io crate. The drivers now read a single buffer. The data format is interpreted directly from the sysfs reported types. Poll rate of the driver is calculated from the set sampling rate of the source driver after attempting to set it to 200Hz. hid_sfh_driver: - Uses its built in timers for each instance, accel and gyro are separated in this configuration by the driver. - Applies a scale correction due to a flaw in the hid rdesc from adm_sfh. This is yet to be tested on Intel devices and may need some detection method or a config file flag if the intel driver doesn't have the same issue. bmi_driver: - Creates and binds an hrtimer instance that is cleaned up when the device closes. - The kernel driver provides both accel and gyro buffers on a single udev. --- src/drivers/iio_imu/bmi_driver.rs | 176 +++++++ src/drivers/iio_imu/driver.rs | 496 ------------------- src/drivers/iio_imu/event.rs | 2 +- src/drivers/iio_imu/hid_sfh_driver.rs | 189 +++++++ src/drivers/iio_imu/info.rs | 686 +++++++++++++++++++++++++- src/drivers/iio_imu/mod.rs | 3 +- src/input/source/iio.rs | 23 +- src/input/source/iio/accel_gyro_3d.rs | 38 +- src/input/source/iio/bmi_imu.rs | 27 +- src/input/source/mod.rs | 4 +- 10 files changed, 1094 insertions(+), 550 deletions(-) create mode 100644 src/drivers/iio_imu/bmi_driver.rs delete mode 100644 src/drivers/iio_imu/driver.rs create mode 100644 src/drivers/iio_imu/hid_sfh_driver.rs diff --git a/src/drivers/iio_imu/bmi_driver.rs b/src/drivers/iio_imu/bmi_driver.rs new file mode 100644 index 00000000..73d8d991 --- /dev/null +++ b/src/drivers/iio_imu/bmi_driver.rs @@ -0,0 +1,176 @@ +use std::{ + collections::HashSet, + error::Error, + fmt, + path::PathBuf, + thread, + time::{Duration, Instant}, +}; + +use crate::input::capability::{Capability, Source}; + +use super::{ + event::{AxisData, Event}, + info::{ + compute_layout, configure_buffer, decode_group, discover_group, discover_timestamp, + read_mount_matrix, BlockingFdSource, HrtimerTriggerGuard, MountMatrix, RecordLayout, + RecordSource, TriggerStrategy, + }, +}; + +pub struct Driver { + mount_matrix: MountMatrix, + layout: RecordLayout, + source: Box, + record_buf: Vec, + filtered_events: HashSet, + poll_interval: Duration, + accel_state: Option, + gyro_state: Option, + // Kept alive only so its Drop unbinds/removes the hrtimer trigger. + _trigger_guard: Option, +} + +impl fmt::Debug for Driver { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Driver") + .field("layout", &self.layout) + .finish() + } +} + +impl Driver { + pub fn new( + name: String, + matrix: Option, + sample_rate: Option, + ) -> Result> { + log::debug!("Creating IIO IMU driver instance for {name}"); + + let base = PathBuf::from(format!("/sys/bus/iio/devices/{name}")); + if !base.is_dir() { + return Err(format!("IIO device path not found: {}", base.display()).into()); + } + let devnode = PathBuf::from(format!("/dev/{name}")); + + let mount_matrix = if let Some(matrix) = matrix { + matrix + } else if let Some(matrix_str) = read_mount_matrix(&base) { + log::debug!("Found mount matrix: {matrix_str}"); + let matrix = MountMatrix::new(matrix_str)?; + log::debug!("Decoded mount matrix: {matrix}"); + matrix + } else { + MountMatrix::default() + }; + + let accel = discover_group(&base, "accel")?; + let gyro = discover_group(&base, "anglvel")?; + let timestamp = discover_timestamp(&base)?; + + log::debug!("accel present: {}", !accel.is_empty()); + log::debug!("gyro present: {}", !gyro.is_empty()); + log::debug!("timestamp present: {}", timestamp.is_some()); + + let trigger_name = format!("inputplumber-{}", name.replace(':', "_")); + let buffer_config = configure_buffer( + &base, + &accel, + &gyro, + sample_rate, + TriggerStrategy::Hrtimer(trigger_name), + )?; + let poll_interval = Duration::from_secs_f64(1.0 / buffer_config.rate); + + let layout = compute_layout(accel, gyro, timestamp); + log::debug!("Computed record layout: {layout:?}"); + + let source = BlockingFdSource::open(&devnode, None) + .map_err(|e| format!("failed to open {}: {e}", devnode.display()))?; + + let record_buf = vec![0u8; layout.record_len]; + + Ok(Self { + mount_matrix, + layout, + source: Box::new(source), + record_buf, + filtered_events: Default::default(), + poll_interval, + accel_state: None, + gyro_state: None, + _trigger_guard: buffer_config.trigger_guard, + }) + } + + pub fn update_filtered_events(&mut self, events: HashSet) { + self.filtered_events = events; + } + + /// Skips a group's event if unchanged since the last poll; paces total call time to poll_interval. + pub fn poll(&mut self) -> Result, Box> { + let start = Instant::now(); + + self.source.read_one(&mut self.record_buf)?; + + let mut events = Vec::new(); + + if !self.layout.accel.is_empty() + && !self + .filtered_events + .contains(&Capability::Accelerometer(Source::Center)) + { + let mut data = decode_group(&self.layout.accel, &self.record_buf); + self.rotate_value(&mut data); + if self.accel_state.as_ref() != Some(&data) { + self.accel_state = Some(data.clone()); + events.push(Event::Accelerometer(data)); + } + } + + if !self.layout.gyro.is_empty() + && !self + .filtered_events + .contains(&Capability::Gyroscope(Source::Center)) + { + let mut data = decode_group(&self.layout.gyro, &self.record_buf); + self.rotate_value(&mut data); + if self.gyro_state.as_ref() != Some(&data) { + self.gyro_state = Some(data.clone()); + events.push(Event::Gyro(data)); + } + } + + log::trace!("Got IIO IMU events: {:?}", events); + + if let Some(remaining) = self.poll_interval.checked_sub(start.elapsed()) { + thread::sleep(remaining); + } + + Ok(events) + } + + /// Rotate the given axis data according to the mount matrix. This is used + /// to calculate the final value according to the sensor oritentation. + // Values are intended to be multiplied as: + // x' = mxx * x + myx * y + mzx * z + // y' = mxy * x + myy * y + mzy * z + // z' = mxz * x + myz * y + mzz * z + fn rotate_value(&self, value: &mut AxisData) { + let x = value.roll; + let y = value.pitch; + let z = value.yaw; + let mxx = self.mount_matrix.x.0; + let myx = self.mount_matrix.x.1; + let mzx = self.mount_matrix.x.2; + let mxy = self.mount_matrix.y.0; + let myy = self.mount_matrix.y.1; + let mzy = self.mount_matrix.y.2; + let mxz = self.mount_matrix.z.0; + let myz = self.mount_matrix.z.1; + let mzz = self.mount_matrix.z.2; + value.roll = mxx * x + myx * y + mzx * z; + value.pitch = mxy * x + myy * y + mzy * z; + value.yaw = mxz * x + myz * y + mzz * z; + } +} diff --git a/src/drivers/iio_imu/driver.rs b/src/drivers/iio_imu/driver.rs deleted file mode 100644 index 9e236726..00000000 --- a/src/drivers/iio_imu/driver.rs +++ /dev/null @@ -1,496 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - error::Error, - fs::File, - io::{self, BufRead, BufReader}, -}; - -use industrial_io::{Channel, ChannelType, Device, Direction}; - -use crate::{ - drivers::iio_imu::info::MountMatrix, - input::capability::{Capability, Source}, -}; - -use super::{ - event::{AxisData, Event}, - info::AxisInfo, -}; - -const DEFAULT_SAMPLE_RATE: f64 = 200.0; - -/// Driver for reading IIO IMU data -pub struct Driver { - _device: Device, // must outlive Channel raw pointers - mount_matrix: MountMatrix, - accel: HashMap, - accel_info: HashMap, - gyro: HashMap, - gyro_info: HashMap, - /// List of events that should not be generated - filtered_events: HashSet, -} - -impl Driver { - pub fn new( - id: String, - name: String, - matrix: Option, - sample_rate: Option, - ) -> Result> { - log::debug!("Creating IIO IMU driver instance for {name}"); - - // Create an IIO local context used to query for devices - let ctx = industrial_io::context::Context::new()?; - log::debug!("IIO context version: {}", ctx.version()); - - // Find the IMU device - let Some(device) = ctx.find_device(id.as_str()) else { - return Err("Failed to find device".into()); - }; - - // Try finding the mount matrix to determine how sensors were mounted inside - // the device. - // https://github.com/torvalds/linux/blob/master/Documentation/devicetree/bindings/iio/mount-matrix.txt - let mount_matrix = if let Some(matrix) = matrix { - // Use the provided mount matrix if it is defined - matrix - } else if let Some(mount) = device.find_channel("mount", Direction::Input) { - // Read from the matrix - let matrix_str = mount.attr_read_str("matrix")?; - log::debug!("Found mount matrix: {matrix_str}"); - let matrix = MountMatrix::new(matrix_str)?; - log::debug!("Decoded mount matrix: {matrix}"); - matrix - } else { - MountMatrix::default() - }; - - // Find all accelerometer and gyro channels and insert them into a hashmap - let (accel, accel_info) = get_channels_with_type(&device, ChannelType::Accel); - for attr in &accel_info { - log::debug!("Found accel_info: {:?}", attr); - } - let (gyro, gyro_info) = get_channels_with_type(&device, ChannelType::AnglVel); - for attr in &gyro_info { - log::debug!("Found gyro_info: {:?}", attr); - } - - // Log device attributes - for attr in device.attributes() { - log::trace!("Found device attribute: {:?}", attr) - } - - // Log all found channels - for channel in device.channels() { - log::trace!("Found channel: {:?} {:?}", channel.id(), channel.name()); - log::trace!(" Is output: {}", channel.is_output()); - log::trace!(" Is scan element: {}", channel.is_scan_element()); - for attr in channel.attrs() { - log::trace!(" Found attribute: {:?}", attr); - } - } - - // Request a higher sampling rate - for (channels, ch_type) in [(&accel, ChannelType::Accel), (&gyro, ChannelType::AnglVel)] { - if channels.is_empty() { - continue; - } - if let Err(err) = - set_sample_rate_or_default(&device, channels, ch_type, sample_rate) - { - log::warn!("Failed to set sample rate: {err}, falling back to max available"); - set_sample_rate_max(&device, channels, ch_type); - } - } - - Ok(Self { - _device: device, - mount_matrix, - accel, - accel_info, - gyro, - gyro_info, - filtered_events: Default::default(), - }) - } - - //TODO: Using InputPlumber Capability enum prevents this driver from having the ability to be - //a standalone crate. When this driver is eventually separated, refactor the Event type to - //follow the pattern DeviceEvent(Event, Value) and create a match table for - //Capability->Event/Event->Capability in the SourceDriver implementation. - pub fn update_filtered_events(&mut self, events: HashSet) { - self.filtered_events = events; - } - - pub fn get_default_event_filter( - &self, - ) -> Result, Box> { - let filtered_events = match is_driver_loaded("hid_lenovo_go") { - Ok(true) => { - log::debug!("Found hid-lenovo-go driver. Disabling internal gyroscope."); - HashSet::from([ - Capability::Accelerometer(Source::Center), - Capability::Gyroscope(Source::Center), - ]) - } - Ok(false) => { - log::debug!("Did not find hid-lenovo-go driver. Enabling internal gyroscope."); - HashSet::new() - } - Err(e) => { - return Err(format!("Failed to read '/proc/modules': {e:?}").into()); - } - }; - Ok(filtered_events) - } - - /// Poll the device for data - pub fn poll(&self) -> Result, Box> { - let mut events = vec![]; - - // Read from the accelerometer - if !self - .filtered_events - .contains(&Capability::Accelerometer(Source::Center)) - { - if let Some(event) = self.poll_accel()? { - events.push(event); - } - } - - // Read from the gyro - if !self - .filtered_events - .contains(&Capability::Gyroscope(Source::Center)) - { - if let Some(event) = self.poll_gyro()? { - events.push(event); - } - } - - Ok(events) - } - - /// Polls all the channels from the accelerometer - fn poll_accel(&self) -> Result, Box> { - if self.accel.is_empty() { - return Ok(None); - } - - // Read from each accel channel - let mut accel_input = AxisData::default(); - for (id, channel) in self.accel.iter() { - // Get the info for the axis and read the data - let Some(info) = self.accel_info.get(id) else { - continue; - }; - let data = channel.attr_read_int("raw")?; - - // processed_value = (raw + offset) * scale - let value = (data + info.offset) as f64 * info.scale; - if id.ends_with('x') { - accel_input.roll = value; - } - if id.ends_with('y') { - accel_input.pitch = value; - } - if id.ends_with('z') { - accel_input.yaw = value; - } - } - self.rotate_value(&mut accel_input); - - Ok(Some(Event::Accelerometer(accel_input))) - } - - /// Polls all the channels from the gyro - fn poll_gyro(&self) -> Result, Box> { - if self.gyro.is_empty() { - return Ok(None); - } - - let mut gyro_input = AxisData::default(); - for (id, channel) in self.gyro.iter() { - // Get the info for the axis and read the data - let Some(info) = self.gyro_info.get(id) else { - continue; - }; - let data = channel.attr_read_int("raw")?; - - // processed_value = (raw + offset) * scale - let value = (data + info.offset) as f64 * info.scale; - - if id.ends_with('x') { - gyro_input.roll = value; - } - if id.ends_with('y') { - gyro_input.pitch = value; - } - if id.ends_with('z') { - gyro_input.yaw = value; - } - } - self.rotate_value(&mut gyro_input); - - Ok(Some(Event::Gyro(gyro_input))) - } - - /// Rotate the given axis data according to the mount matrix. This is used - /// to calculate the final value according to the sensor oritentation. - // Values are intended to be multiplied as: - // x' = mxx * x + myx * y + mzx * z - // y' = mxy * x + myy * y + mzy * z - // z' = mxz * x + myz * y + mzz * z - fn rotate_value(&self, value: &mut AxisData) { - let x = value.roll; - let y = value.pitch; - let z = value.yaw; - let mxx = self.mount_matrix.x.0; - let myx = self.mount_matrix.x.1; - let mzx = self.mount_matrix.x.2; - let mxy = self.mount_matrix.y.0; - let myy = self.mount_matrix.y.1; - let mzy = self.mount_matrix.y.2; - let mxz = self.mount_matrix.z.0; - let myz = self.mount_matrix.z.1; - let mzz = self.mount_matrix.z.2; - value.roll = mxx * x + myx * y + mzx * z; - value.pitch = mxy * x + myy * y + mzy * z; - value.yaw = mxz * x + myz * y + mzz * z; - } -} - -/// Returns all channels and channel information from the given device matching -/// the given channel type. -fn get_channels_with_type( - device: &Device, - channel_type: ChannelType, -) -> (HashMap, HashMap) { - let mut channels = HashMap::new(); - let mut channel_info = HashMap::new(); - device - .channels() - .filter(|channel| channel.channel_type() == channel_type) - .for_each(|channel| { - let Some(id) = channel.id() else { - log::warn!("Unable to get channel id for channel: {:?}", channel); - return; - }; - log::debug!("Found channel: {id}"); - - // Get the offset of the axis - let offset = match channel.attr_read_int("offset") { - Ok(v) => v, - Err(e) => { - log::debug!("Unable to read offset for channel {id}: {:?}", e); - 0 - } - }; - - // Get the sample rate of the axis - let sample_rate = match channel.attr_read_float("sampling_frequency") { - Ok(v) => v, - Err(e) => { - log::warn!("Unable to read sample rate for channel {id}: {:?}", e); - 4.0 - } - }; - - let sample_rates_avail = match channel.attr_read_str("sampling_frequency_available") { - Ok(v) => { - let mut all_scales = Vec::new(); - for val in v.split_whitespace() { - // convert the string into f64 - all_scales.push(val.parse::().unwrap()); - } - all_scales - } - Err(e) => { - log::warn!( - "Unable to read available sample rates for channel {id}: {:?}", - e - ); - vec![4.0] - } - }; - - // Get the scale of the axis to normalize values to meters per second or rads per - // second - let scale = match channel.attr_read_float("scale") { - Ok(v) => v, - Err(e) => { - log::warn!("Unable to read scale for channel {id}: {:?}", e); - 1.0 - } - }; - - let scales_avail = match channel.attr_read_str("scale_available") { - Ok(v) => { - let mut all_scales = Vec::new(); - for val in v.split_whitespace() { - // convert the string into f64 - all_scales.push(val.parse::().unwrap()); - } - all_scales - } - Err(e) => { - log::warn!("Unable to read available scales for channel {id}: {:?}", e); - vec![1.0] - } - }; - - let info = AxisInfo { - offset, - sample_rate, - sample_rates_avail, - scale, - scales_avail, - }; - channel_info.insert(id.clone(), info); - channels.insert(id, channel); - }); - - (channels, channel_info) -} - -fn is_driver_loaded(driver_name: &str) -> io::Result { - let file = File::open("/proc/modules")?; - let reader = BufReader::new(file); - - for line in reader.lines() { - let line = line?; - if line.starts_with(driver_name) { - return Ok(true); - } - } - Ok(false) -} - -/// Try to set a specific or default sampling rate. Returns Err if the -/// requested rate is not in the hardware's available list. -fn set_sample_rate_or_default( - device: &Device, - channels: &HashMap, - channel_type: ChannelType, - target_rate: Option, -) -> Result<(), Box> { - let rate = target_rate.unwrap_or(DEFAULT_SAMPLE_RATE); - let avail = read_sample_rates_available(device, channels, &channel_type); - - if !avail.is_empty() && !avail.contains(&rate) { - return Err(format!( - "Requested {rate} Hz not in available rates: {avail:?}" - ) - .into()); - } - - write_sample_rate(device, channels, channel_type, rate) -} - -/// Set sampling rate to the maximum reported by the hardware. -/// Falls back to DEFAULT_SAMPLE_RATE if no available rates are reported. -fn set_sample_rate_max( - device: &Device, - channels: &HashMap, - channel_type: ChannelType, -) { - let avail = read_sample_rates_available(device, channels, &channel_type); - let rate = if avail.is_empty() { - log::warn!( - "No available sample rates reported, using default {DEFAULT_SAMPLE_RATE} Hz" - ); - DEFAULT_SAMPLE_RATE - } else { - let max = avail.iter().cloned().fold(f64::NEG_INFINITY, f64::max); - log::info!("Using max available sample rate: {max} Hz"); - max - }; - - if let Err(err) = write_sample_rate(device, channels, channel_type, rate) { - log::warn!("Failed to set max sample rate: {err}"); - } -} - -/// Write a sampling rate to the device. Tries per-channel first (BMI-style), -/// then falls back to device-level attribute (HID Sensor Hub). -fn write_sample_rate( - device: &Device, - channels: &HashMap, - channel_type: ChannelType, - rate: f64, -) -> Result<(), Box> { - for (id, channel) in channels.iter() { - match channel.attr_write_float("sampling_frequency", rate) { - Ok(_) => { - match channel.attr_read_float("sampling_frequency") { - Ok(actual) => { - log::info!("Set sampling_frequency to {actual} Hz via channel {id}") - } - Err(err) => log::warn!( - "Set sampling_frequency for {id} but read-back failed: {err}, assuming {rate} Hz" - ), - } - return Ok(()); - } - Err(err) => { - log::warn!( - "Per-channel sampling_frequency write failed for {id}: {err}" - ); - } - } - } - - let attr = match channel_type { - ChannelType::Accel => "in_accel_sampling_frequency", - ChannelType::AnglVel => "in_anglvel_sampling_frequency", - _ => return Err("Unknown channel type".into()), - }; - - device.attr_write_float(attr, rate)?; - match device.attr_read_float(attr) { - Ok(actual) => log::info!("Set device-level {attr} to {actual} Hz"), - Err(err) => log::warn!( - "Set {attr} but read-back failed: {err}, assuming {rate} Hz" - ), - } - Ok(()) -} - -/// Read the list of supported sampling rates from the hardware. -/// Tries per-channel attribute first, then device-level global attribute. -fn read_sample_rates_available( - device: &Device, - channels: &HashMap, - channel_type: &ChannelType, -) -> Vec { - for channel in channels.values() { - if let Ok(val) = channel.attr_read_str("sampling_frequency_available") { - let rates: Vec = val - .split_whitespace() - .filter_map(|s| s.parse().ok()) - .collect(); - if !rates.is_empty() { - return rates; - } - } - } - - let attr = match channel_type { - ChannelType::Accel => "in_accel_sampling_frequency_available", - ChannelType::AnglVel => "in_anglvel_sampling_frequency_available", - _ => return vec![], - }; - - if let Ok(val) = device.attr_read_str(attr) { - let rates: Vec = val - .split_whitespace() - .filter_map(|s| s.parse().ok()) - .collect(); - if !rates.is_empty() { - return rates; - } - } - - vec![] -} diff --git a/src/drivers/iio_imu/event.rs b/src/drivers/iio_imu/event.rs index bdc25150..85d45639 100644 --- a/src/drivers/iio_imu/event.rs +++ b/src/drivers/iio_imu/event.rs @@ -10,7 +10,7 @@ pub enum Event { } /// AxisData represents the state of the accelerometer or gyro (x, y, z) values -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct AxisData { pub roll: f64, pub pitch: f64, diff --git a/src/drivers/iio_imu/hid_sfh_driver.rs b/src/drivers/iio_imu/hid_sfh_driver.rs new file mode 100644 index 00000000..66358249 --- /dev/null +++ b/src/drivers/iio_imu/hid_sfh_driver.rs @@ -0,0 +1,189 @@ +use std::{ + collections::HashSet, + error::Error, + fmt, fs, + path::PathBuf, + thread, + time::{Duration, Instant}, +}; + +use crate::input::capability::{Capability, Source}; + +use super::{ + event::{AxisData, Event}, + info::{ + compute_layout, configure_buffer, decode_group, discover_group, discover_timestamp, + read_mount_matrix, BlockingFdSource, MountMatrix, RecordLayout, RecordSource, + TriggerStrategy, + }, +}; + +pub struct Driver { + mount_matrix: MountMatrix, + layout: RecordLayout, + source: Box, + record_buf: Vec, + filtered_events: HashSet, + poll_interval: Duration, + accel_state: Option, + gyro_state: Option, +} + +impl fmt::Debug for Driver { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Driver") + .field("layout", &self.layout) + .finish() + } +} + +impl Driver { + pub fn new( + name: String, + matrix: Option, + sample_rate: Option, + ) -> Result> { + log::debug!("Creating IIO IMU driver instance for {name}"); + + let base = PathBuf::from(format!("/sys/bus/iio/devices/{name}")); + if !base.is_dir() { + return Err(format!("IIO device path not found: {}", base.display()).into()); + } + let devnode = PathBuf::from(format!("/dev/{name}")); + + let mount_matrix = if let Some(matrix) = matrix { + matrix + } else if let Some(matrix_str) = read_mount_matrix(&base) { + log::debug!("Found mount matrix: {matrix_str}"); + let matrix = MountMatrix::new(matrix_str)?; + log::debug!("Decoded mount matrix: {matrix}"); + matrix + } else { + MountMatrix::default() + }; + + let accel = discover_group(&base, "accel")?; + let gyro = discover_group(&base, "anglvel")?; + let timestamp = discover_timestamp(&base)?; + + log::debug!("accel present: {}", !accel.is_empty()); + log::debug!("gyro present: {}", !gyro.is_empty()); + log::debug!("timestamp present: {}", timestamp.is_some()); + + let kick_path = accel + .first() + .or(gyro.first()) + .map(|chan| base.join(format!("{}_raw", chan.id))); + + let buffer_config = + configure_buffer(&base, &accel, &gyro, sample_rate, TriggerStrategy::FindExisting)?; + let poll_interval = Duration::from_secs_f64(1.0 / buffer_config.rate); + + let layout = compute_layout(accel, gyro, timestamp); + log::debug!("Computed record layout: {layout:?}"); + + let source = BlockingFdSource::open(&devnode, kick_path) + .map_err(|e| format!("failed to open {}: {e}", devnode.display()))?; + + let record_buf = vec![0u8; layout.record_len]; + + Ok(Self { + mount_matrix, + layout, + source: Box::new(source), + record_buf, + filtered_events: Default::default(), + poll_interval, + accel_state: None, + gyro_state: None, + }) + } + + //TODO: Using InputPlumber Capability enum prevents this driver from having the ability to be + //a standalone crate. When this driver is eventually separated, refactor the Event type to + //follow the pattern DeviceEvent(Event, Value) and create a match table for + //Capability->Event/Event->Capability in the SourceDriver implementation. + pub fn update_filtered_events(&mut self, events: HashSet) { + self.filtered_events = events; + } + + pub fn get_default_event_filter( + &self, + ) -> Result, Box> { + match fs::read_to_string("/proc/modules") { + Ok(modules) if modules.contains("hid_lenovo_go") => Ok(HashSet::from([ + Capability::Accelerometer(Source::Center), + Capability::Gyroscope(Source::Center), + ])), + Ok(_) => Ok(HashSet::new()), + Err(e) => Err(format!("Failed to read '/proc/modules': {e:?}").into()), + } + } + + /// Skips a group's event if unchanged since the last poll; paces total call time to poll_interval. + pub fn poll(&mut self) -> Result, Box> { + let start = Instant::now(); + + self.source.read_one(&mut self.record_buf)?; + + let mut events = Vec::new(); + + if !self.layout.accel.is_empty() + && !self + .filtered_events + .contains(&Capability::Accelerometer(Source::Center)) + { + let mut data = decode_group(&self.layout.accel, &self.record_buf); + self.rotate_value(&mut data); + if self.accel_state.as_ref() != Some(&data) { + self.accel_state = Some(data.clone()); + events.push(Event::Accelerometer(data)); + } + } + + if !self.layout.gyro.is_empty() + && !self + .filtered_events + .contains(&Capability::Gyroscope(Source::Center)) + { + let mut data = decode_group(&self.layout.gyro, &self.record_buf); + self.rotate_value(&mut data); + if self.gyro_state.as_ref() != Some(&data) { + self.gyro_state = Some(data.clone()); + events.push(Event::Gyro(data)); + } + } + + log::trace!("Got IIO IMU events: {:?}", events); + + if let Some(remaining) = self.poll_interval.checked_sub(start.elapsed()) { + thread::sleep(remaining); + } + + Ok(events) + } + + /// Rotate the given axis data according to the mount matrix. This is used + /// to calculate the final value according to the sensor oritentation. + // Values are intended to be multiplied as: + // x' = mxx * x + myx * y + mzx * z + // y' = mxy * x + myy * y + mzy * z + // z' = mxz * x + myz * y + mzz * z + fn rotate_value(&self, value: &mut AxisData) { + let x = value.roll; + let y = value.pitch; + let z = value.yaw; + let mxx = self.mount_matrix.x.0; + let myx = self.mount_matrix.x.1; + let mzx = self.mount_matrix.x.2; + let mxy = self.mount_matrix.y.0; + let myy = self.mount_matrix.y.1; + let mzy = self.mount_matrix.y.2; + let mxz = self.mount_matrix.z.0; + let myz = self.mount_matrix.z.1; + let mzz = self.mount_matrix.z.2; + value.roll = mxx * x + myx * y + mzx * z; + value.pitch = mxy * x + myy * y + mzy * z; + value.yaw = mxz * x + myz * y + mzz * z; + } +} diff --git a/src/drivers/iio_imu/info.rs b/src/drivers/iio_imu/info.rs index 0d308f0a..f47a2882 100644 --- a/src/drivers/iio_imu/info.rs +++ b/src/drivers/iio_imu/info.rs @@ -1,4 +1,22 @@ -use std::{error::Error, fmt}; +use std::{ + collections::HashMap, + error::Error, + fmt, fs, + io::{self, Read}, + os::fd::AsRawFd, + path::{Path, PathBuf}, + thread, + time::Duration, +}; + +use nix::libc::{self, c_int}; + +use crate::drivers::iio_imu::event::AxisData; + +/// Maximum delay before triggering device stall actions. +const STALL_TIMEOUT_MS: c_int = 2000; + +const DEFAULT_SAMPLE_RATE: f64 = 200.0; /// The [MountMatrix] is used to define how sensors are oriented inside a device /// https://github.com/torvalds/linux/blob/master/Documentation/devicetree/bindings/iio/mount-matrix.txt @@ -81,3 +99,669 @@ pub struct AxisInfo { pub scale: f64, pub scales_avail: Vec, } + +/// Blocks until exactly one record's worth of bytes is available, then +/// fills `buf` with it. +pub trait RecordSource: fmt::Debug + Send { + fn read_one(&mut self, buf: &mut [u8]) -> io::Result<()>; +} + +#[derive(Debug)] +pub struct BlockingFdSource { + file: fs::File, + kick_path: Option, +} + +impl BlockingFdSource { + pub fn open(devnode: &Path, kick_path: Option) -> io::Result { + let file = fs::OpenOptions::new().read(true).open(devnode)?; + Ok(Self { file, kick_path }) + } +} + +impl RecordSource for BlockingFdSource { + fn read_one(&mut self, buf: &mut [u8]) -> io::Result<()> { + if let Some(path) = &self.kick_path { + let _ = fs::read_to_string(path); + } + wait_readable(self.file.as_raw_fd())?; + self.file.read_exact(buf) + } +} + +fn wait_readable(fd: c_int) -> io::Result<()> { + let mut fds = [libc::pollfd { + fd, + events: libc::POLLIN, + revents: 0, + }]; + loop { + let ret = unsafe { libc::poll(fds.as_mut_ptr(), 1, STALL_TIMEOUT_MS) }; + if ret < 0 { + let err = io::Error::last_os_error(); + if err.kind() == io::ErrorKind::Interrupted { + continue; + } + return Err(err); + } + if ret == 0 { + return Err(io::Error::new( + io::ErrorKind::TimedOut, + "no IMU sample within stall timeout; trigger may have stopped", + )); + } + if fds[0].revents & (libc::POLLERR | libc::POLLHUP | libc::POLLNVAL) != 0 { + return Err(io::Error::other("iio buffer devnode reported error/hangup")); + } + return Ok(()); + } +} + +/// Parsed form of a `scan_elements/in_*_type` value, e.g. "le:s16/16>>0". +#[derive(Debug, Clone, Copy)] +pub struct ScanFormat { + little_endian: bool, + signed: bool, + /// Bits actually allotted in the record (padded to a byte boundary). + storage_bits: u8, + /// Bits of real, significant data once shifted into place. + real_bits: u8, + shift: u8, +} + +impl ScanFormat { + fn parse(s: &str) -> Result> { + let (endian, rest) = s + .split_once(':') + .ok_or_else(|| format!("malformed scan type '{s}' (missing endianness)"))?; + let little_endian = match endian { + "le" => true, + "be" => false, + other => return Err(format!("unknown endianness '{other}' in scan type '{s}'").into()), + }; + + let (sign, rest) = rest.split_at(1); + let signed = match sign { + "s" => true, + "u" => false, + other => return Err(format!("unknown sign flag '{other}' in scan type '{s}'").into()), + }; + + let (bits_str, rest) = rest + .split_once('/') + .ok_or_else(|| format!("malformed scan type '{s}' (missing real bits)"))?; + let real_bits: u8 = bits_str.parse()?; + + let (storage_str, shift_str) = rest + .split_once(">>") + .ok_or_else(|| format!("malformed scan type '{s}' (missing shift)"))?; + let storage_bits: u8 = storage_str.parse()?; + let shift: u8 = shift_str.parse()?; + + Ok(Self { + little_endian, + signed, + storage_bits, + real_bits, + shift, + }) + } + + fn storage_bytes(&self) -> usize { + (self.storage_bits as usize).div_ceil(8) + } + + /// Decode this channel's raw bytes into a signed integer. + fn decode(&self, raw: &[u8]) -> i64 { + let n = self.storage_bytes().min(8); + let mut buf = [0u8; 8]; + if self.little_endian { + buf[..n].copy_from_slice(&raw[..n]); + } else { + for i in 0..n { + buf[n - 1 - i] = raw[i]; + } + } + let mut value = u64::from_le_bytes(buf); + value >>= self.shift; + let mask = if self.real_bits >= 64 { + u64::MAX + } else { + (1u64 << self.real_bits) - 1 + }; + value &= mask; + + if self.signed && self.real_bits < 64 { + let sign_bit = 1u64 << (self.real_bits - 1); + if value & sign_bit != 0 { + return (value as i64) - (1i64 << self.real_bits); + } + } + value as i64 + } +} + +/// A single enabled scan channel discovered from sysfs, before the +/// buffer's byte layout has been computed. +#[derive(Debug, Clone)] +pub struct DiscoveredChannel { + /// e.g. "in_accel_x" + pub id: String, + pub axis: char, + pub scan_index: i32, + pub format: ScanFormat, + pub offset: i64, + pub scale: f64, +} + +/// One channel's final position within a fixed-size buffer record, plus +/// its calibration data. +#[derive(Debug, Clone)] +pub struct ChannelLayout { + byte_offset: usize, + format: ScanFormat, + offset: i64, + scale: f64, +} + +/// Fixed layout of one complete buffer record, computed once at startup. +#[derive(Debug, Clone, Default)] +pub struct RecordLayout { + pub record_len: usize, + pub accel: HashMap, + pub gyro: HashMap, +} + +pub fn read_trim(path: &Path) -> io::Result { + Ok(fs::read_to_string(path)?.trim().to_string()) +} + +/// Reads the device's mount matrix: a shared attribute first, then per-group. +pub fn read_mount_matrix(base: &Path) -> Option { + read_trim(&base.join("in_mount_matrix")) + .ok() + .or_else(|| read_trim(&base.join("in_accel_mount_matrix")).ok()) + .or_else(|| read_trim(&base.join("in_anglvel_mount_matrix")).ok()) +} + +/// Discover the x/y/z scan channels for one group from `scan_elements/in___*`. +pub fn discover_group( + base: &Path, + prefix: &str, +) -> Result, Box> { + let scan_dir = base.join("scan_elements"); + let mut out = Vec::new(); + + for axis in ['x', 'y', 'z'] { + let id = format!("in_{prefix}_{axis}"); + let index_path = scan_dir.join(format!("{id}_index")); + if !index_path.exists() { + continue; + } + + let scan_index: i32 = read_trim(&index_path)?.trim().parse()?; + let type_str = read_trim(&scan_dir.join(format!("{id}_type")))?; + let format = ScanFormat::parse(&type_str)?; + + let offset = read_trim(&base.join(format!("{id}_offset"))) + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + + let scale = read_trim(&base.join(format!("{id}_scale"))) + .ok() + .and_then(|s| s.parse().ok()) + .or_else(|| { + read_trim(&base.join(format!("in_{prefix}_scale"))) + .ok() + .and_then(|s| s.parse().ok()) + }) + .unwrap_or(1.0); + + log::debug!( + "Found channel {id} (scan_index={scan_index}, offset={offset}, scale={scale}, type={type_str})" + ); + + out.push(DiscoveredChannel { + id, + axis, + scan_index, + format, + offset, + scale, + }); + } + + Ok(out) +} + +/// Look up the `in_timestamp` scan channel, if the device exposes one. +pub fn discover_timestamp( + base: &Path, +) -> Result, Box> { + let scan_dir = base.join("scan_elements"); + let index_path = scan_dir.join("in_timestamp_index"); + if !index_path.exists() { + return Ok(None); + } + let scan_index: i32 = read_trim(&index_path)?.trim().parse()?; + let format = ScanFormat::parse(&read_trim(&scan_dir.join("in_timestamp_type"))?)?; + Ok(Some((scan_index, format))) +} + +enum ScanEntry { + Accel(DiscoveredChannel), + Gyro(DiscoveredChannel), + Timestamp(ScanFormat), +} + +/// Computes each channel's byte offset within one buffer record, packing +/// them in ascending scan-index order with natural alignment and padding +/// the whole record to the widest element. +pub fn compute_layout( + accel: Vec, + gyro: Vec, + timestamp: Option<(i32, ScanFormat)>, +) -> RecordLayout { + let mut all: Vec<(i32, ScanEntry)> = accel + .into_iter() + .map(|c| (c.scan_index, ScanEntry::Accel(c))) + .chain(gyro.into_iter().map(|c| (c.scan_index, ScanEntry::Gyro(c)))) + .collect(); + if let Some((scan_index, format)) = timestamp { + all.push((scan_index, ScanEntry::Timestamp(format))); + } + all.sort_by_key(|(scan_index, _)| *scan_index); + + let mut offset = 0usize; + let mut max_align = 1usize; + let mut accel_layout = HashMap::new(); + let mut gyro_layout = HashMap::new(); + + for (_, entry) in all { + let format = match &entry { + ScanEntry::Accel(c) | ScanEntry::Gyro(c) => c.format, + ScanEntry::Timestamp(f) => *f, + }; + let size = format.storage_bytes().max(1); + max_align = max_align.max(size); + if !offset.is_multiple_of(size) { + offset += size - (offset % size); + } + + match entry { + ScanEntry::Accel(c) => { + accel_layout.insert( + c.axis, + ChannelLayout { + byte_offset: offset, + format: c.format, + offset: c.offset, + scale: c.scale, + }, + ); + } + ScanEntry::Gyro(c) => { + gyro_layout.insert( + c.axis, + ChannelLayout { + byte_offset: offset, + format: c.format, + offset: c.offset, + scale: c.scale, + }, + ); + } + ScanEntry::Timestamp(_) => {} + } + offset += size; + } + + if max_align > 0 && !offset.is_multiple_of(max_align) { + offset += max_align - (offset % max_align); + } + + RecordLayout { + record_len: offset, + accel: accel_layout, + gyro: gyro_layout, + } +} + +pub fn decode_group(layout: &HashMap, record: &[u8]) -> AxisData { + let mut out = AxisData::default(); + for (&axis, chan) in layout.iter() { + let end = chan.byte_offset + chan.format.storage_bytes(); + let raw = chan.format.decode(&record[chan.byte_offset..end]); + let value = (raw + chan.offset) as f64 * chan.scale; + match axis { + 'x' => out.roll = value, + 'y' => out.pitch = value, + 'z' => out.yaw = value, + _ => unreachable!("discover_group only ever produces x/y/z"), + } + } + out +} + +fn write_bool_verified(path: &Path, value: bool) -> Result<(), Box> { + let s = if value { "1" } else { "0" }; + fs::write(path, s)?; + let actual = read_trim(path)?; + if actual != s { + return Err(format!( + "write to {} did not take effect: wrote '{s}', read back '{actual}'", + path.display() + ) + .into()); + } + Ok(()) +} + +fn write_str_verified(path: &Path, value: &str) -> Result<(), Box> { + fs::write(path, value)?; + let actual = read_trim(path)?; + if actual != value { + return Err(format!( + "write to {} did not take effect: wrote '{value}', read back '{actual}'", + path.display() + ) + .into()); + } + Ok(()) +} + +const HRTIMER_TRIGGER_ROOT: &str = "/sys/kernel/config/iio/triggers/hrtimer"; + +/// How to obtain the trigger bound to a buffer during setup. +pub enum TriggerStrategy { + /// Search for an existing IIO trigger that already references this device. + FindExisting, + /// Always create (or reuse) and bind a dedicated hrtimer trigger. + Hrtimer(String), +} + +/// Unbinds and removes a dedicated hrtimer trigger when the owning +/// driver is dropped, so it doesn't accumulate in configfs across +/// restarts or unplug/replug cycles. +pub struct HrtimerTriggerGuard { + base: PathBuf, + name: String, +} + +impl Drop for HrtimerTriggerGuard { + fn drop(&mut self) { + let _ = fs::write(self.base.join("trigger/current_trigger"), ""); + let trigger_dir = Path::new(HRTIMER_TRIGGER_ROOT).join(&self.name); + if let Err(e) = fs::remove_dir(&trigger_dir) { + log::warn!( + "Failed to remove hrtimer trigger {}: {e}", + trigger_dir.display() + ); + } + } +} + +fn bind_trigger( + base: &Path, + strategy: &TriggerStrategy, + rate: f64, +) -> Result, Box> { + match strategy { + TriggerStrategy::FindExisting => { + if let Some(trigger) = find_trigger_for(base)? { + write_str_verified(&base.join("trigger/current_trigger"), &trigger) + .map_err(|e| format!("failed to bind trigger '{trigger}': {e}"))?; + } else { + log::debug!( + "No matching trigger found for {}; assuming none required", + base.display() + ); + } + Ok(None) + } + TriggerStrategy::Hrtimer(name) => { + if !Path::new(HRTIMER_TRIGGER_ROOT).is_dir() { + return Err( + "hrtimer trigger support not available - is iio_trig_hrtimer loaded?".into(), + ); + } + let trigger_dir = Path::new(HRTIMER_TRIGGER_ROOT).join(name); + if !trigger_dir.is_dir() { + fs::create_dir(&trigger_dir).map_err(|e| { + format!( + "failed to create hrtimer trigger {}: {e}", + trigger_dir.display() + ) + })?; + } + + // The hrtimer trigger only latches sampling_frequency at + // bind/enable time, so this must happen before it's bound. + match find_trigger_device(name) { + Some(trig_dev) => { + let path = trig_dev.join("sampling_frequency"); + match fs::write(&path, rate.to_string()) { + Ok(_) => { + if let Ok(actual) = read_trim(&path) { + log::info!("Set trigger sampling_frequency to {actual} Hz"); + } + } + Err(e) => log::warn!("Failed to set trigger sampling_frequency: {e:?}"), + } + } + None => log::warn!( + "Could not locate trigger device for '{name}'; its firing rate will stay at kernel default" + ), + } + + write_str_verified(&base.join("trigger/current_trigger"), name) + .map_err(|e| format!("failed to bind hrtimer trigger '{name}': {e}"))?; + + Ok(Some(HrtimerTriggerGuard { + base: base.to_path_buf(), + name: name.clone(), + })) + } + } +} + +/// Finds the `/sys/bus/iio/devices/triggerN` entry for a bound trigger by +/// its exact name, so its own `sampling_frequency` can be set. Doesn't +/// gate on `trigger_now` - hrtimer-created triggers don't expose it, +/// unlike hardware triggers, and an exact self-chosen name is unambiguous +/// without that filter anyway. +fn find_trigger_device(name: &str) -> Option { + let devices_root = Path::new("/sys/bus/iio/devices"); + fs::read_dir(devices_root) + .ok()? + .filter_map(|e| e.ok()) + .find_map(|entry| { + let path = entry.path(); + (read_trim(&path.join("name")).ok()? == name).then_some(path) + }) +} + +/// Finds a trigger device for the given sensor +fn find_trigger_for(base: &Path) -> Result, Box> { + let own_name = read_trim(&base.join("name")).unwrap_or_default(); + if own_name.is_empty() { + return Ok(None); + } + + let devices_root = Path::new("/sys/bus/iio/devices"); + for entry in fs::read_dir(devices_root)? { + let path = entry?.path(); + if path == base || !path.join("trigger_now").exists() { + continue; // not a trigger device + } + if let Ok(name) = read_trim(&path.join("name")) { + if name.contains(&own_name) { + return Ok(Some(name)); + } + } + } + Ok(None) +} + +fn read_available_rates(base: &Path, channels: &[DiscoveredChannel], prefix: &str) -> Vec { + for chan in channels { + if let Ok(v) = read_trim(&base.join(format!("{}_sampling_frequency_available", chan.id))) { + let rates: Vec = v + .split_whitespace() + .filter_map(|s| s.parse().ok()) + .collect(); + if !rates.is_empty() { + return rates; + } + } + } + read_trim(&base.join(format!("in_{prefix}_sampling_frequency_available"))) + .map(|v| { + v.split_whitespace() + .filter_map(|s| s.parse().ok()) + .collect() + }) + .unwrap_or_default() +} + +/// Write a sampling rate to the device. Tries per-channel first, then falls +/// back to the device-level attribute. +fn write_sample_rate(base: &Path, channels: &[DiscoveredChannel], prefix: &str, rate: f64) { + for chan in channels { + let path = base.join(format!("{}_sampling_frequency", chan.id)); + if path.exists() { + match fs::write(&path, rate.to_string()) { + Ok(_) => { + if let Ok(actual) = read_trim(&path) { + log::info!("Set {} sampling_frequency to {actual} Hz", chan.id); + } + return; + } + Err(e) => log::warn!( + "Per-channel sampling_frequency write failed for {}: {e:?}", + chan.id + ), + } + } + } + + let attr = format!("in_{prefix}_sampling_frequency"); + let path = base.join(&attr); + match fs::write(&path, rate.to_string()) { + Ok(_) => { + if let Ok(actual) = read_trim(&path) { + log::info!("Set device-level {attr} to {actual} Hz"); + } + } + Err(e) => log::warn!("Failed to set {attr}: {e:?}"), + } +} + +fn negotiate_sample_rate( + base: &Path, + channels: &[DiscoveredChannel], + prefix: &str, + target: Option, +) -> f64 { + if channels.is_empty() { + return DEFAULT_SAMPLE_RATE; + } + let rate = target.unwrap_or(DEFAULT_SAMPLE_RATE); + let avail = read_available_rates(base, channels, prefix); + + let chosen = if avail.is_empty() { + log::warn!("No available sample rates reported for {prefix}; requesting {rate} Hz anyway"); + rate + } else if avail.contains(&rate) { + rate + } else { + let max = avail.iter().cloned().fold(f64::NEG_INFINITY, f64::max); + log::warn!( + "Requested {rate} Hz not in available rates {avail:?} for {prefix}; using max {max} Hz" + ); + max + }; + + write_sample_rate(base, channels, prefix, chosen); + chosen +} + +/// Clear any leftover enabled/triggered state from a previous crashed +/// instance before reconfiguring. +fn warm_reset(base: &Path, buffer_dir: &Path) { + let _ = fs::write(base.join("trigger/current_trigger"), ""); + if fs::write(buffer_dir.join("enable"), "0").is_err() { + thread::sleep(Duration::from_millis(100)); + let _ = fs::write(buffer_dir.join("enable"), "0"); + } + thread::sleep(Duration::from_millis(50)); +} + +/// Result of `configure_buffer`: the negotiated sample rate, and a +/// trigger guard the caller must keep alive for as long as the buffer +/// is in use (only `Some` for `TriggerStrategy::Hrtimer`). +pub struct BufferConfig { + pub rate: f64, + pub trigger_guard: Option, +} + +/// Run the setup sequence +pub fn configure_buffer( + base: &Path, + accel: &[DiscoveredChannel], + gyro: &[DiscoveredChannel], + sample_rate: Option, + trigger: TriggerStrategy, +) -> Result> { + let buffer_dir = if base.join("buffer0").is_dir() { + base.join("buffer0") + } else { + base.join("buffer") + }; + let scan_dir = base.join("scan_elements"); + + warm_reset(base, &buffer_dir); + + write_bool_verified(&buffer_dir.join("enable"), false) + .map_err(|e| format!("failed to disable buffer before reconfiguring: {e}"))?; + + let accel_rate = negotiate_sample_rate(base, accel, "accel", sample_rate); + let gyro_rate = negotiate_sample_rate(base, gyro, "anglvel", sample_rate); + let effective_rate = if !accel.is_empty() { + accel_rate + } else { + gyro_rate + }; + + let trigger_guard = bind_trigger(base, &trigger, effective_rate)?; + + for chan in accel.iter().chain(gyro.iter()) { + write_bool_verified(&scan_dir.join(format!("{}_en", chan.id)), true) + .map_err(|e| format!("failed to enable scan element {}: {e}", chan.id))?; + } + + // Timestamp channel isn't decoded, but leaving it disabled would + // change bytes_per_datum out from under compute_layout's count. + let timestamp_en = scan_dir.join("in_timestamp_en"); + if timestamp_en.exists() { + write_bool_verified(×tamp_en, true) + .map_err(|e| format!("failed to enable in_timestamp: {e}"))?; + } + + // Lowest-latency watermark: wake as soon as a single sample is ready, + // and never let the kernel accumulate more than one unread record. + let watermark_path = buffer_dir.join("watermark"); + if watermark_path.exists() { + write_str_verified(&watermark_path, "1") + .map_err(|e| format!("failed to set watermark to 1: {e}"))?; + } + + write_bool_verified(&buffer_dir.join("enable"), true) + .map_err(|e| format!("failed to arm buffer: {e}"))?; + + Ok(BufferConfig { + rate: effective_rate, + trigger_guard, + }) +} diff --git a/src/drivers/iio_imu/mod.rs b/src/drivers/iio_imu/mod.rs index 8ea65200..3480fe41 100644 --- a/src/drivers/iio_imu/mod.rs +++ b/src/drivers/iio_imu/mod.rs @@ -1,3 +1,4 @@ -pub mod driver; +pub mod bmi_driver; pub mod event; +pub mod hid_sfh_driver; pub mod info; diff --git a/src/input/source/iio.rs b/src/input/source/iio.rs index 8f2a871a..6dc4dffb 100644 --- a/src/input/source/iio.rs +++ b/src/input/source/iio.rs @@ -1,7 +1,7 @@ pub mod accel_gyro_3d; pub mod bmi_imu; -use std::error::Error; +use std::{error::Error, time::Duration}; use glob_match::glob_match; @@ -10,7 +10,7 @@ use crate::{ constants::BUS_SOURCES_PREFIX, input::{ capability::Capability, composite_device::client::CompositeDeviceClient, - info::DeviceInfoRef, output_capability::OutputCapability, + info::DeviceInfoRef, output_capability::OutputCapability, source::SourceDriverOptions, }, udev::device::UdevDevice, }; @@ -102,9 +102,19 @@ impl IioDevice { Ok(Self::BmiImu(source_device)) } DriverType::AccelGryo3D => { + let options = SourceDriverOptions { + poll_rate: Duration::from_millis(0), + buffer_size: 2048, + }; + let device = AccelGyro3dImu::new(device_info.clone(), iio_config)?; - let source_device = - SourceDriver::new(composite_device, device, device_info.into(), conf); + let source_device = SourceDriver::new_with_options( + composite_device, + device, + device_info.into(), + options, + conf, + ); Ok(Self::AccelGryo3D(source_device)) } } @@ -116,7 +126,10 @@ impl IioDevice { let name = device_name.as_str(); log::debug!("Finding driver for IIO interface: {name}"); // BMI_IMU (Bosch BMI160/260/323 and InvenSense ICM42xxx) - if glob_match("{i2c-10EC5280*,i2c-BOSC*,i2c-BMI*,bmi*-imu,bmi260,icm4*}", name) { + if glob_match( + "{i2c-10EC5280*,i2c-BOSC*,i2c-BMI*,bmi*-imu,bmi260,icm4*}", + name, + ) { log::info!("Detected IMU: {name}"); return DriverType::BmiImu; } diff --git a/src/input/source/iio/accel_gyro_3d.rs b/src/input/source/iio/accel_gyro_3d.rs index 737dacaa..1b48fcff 100644 --- a/src/input/source/iio/accel_gyro_3d.rs +++ b/src/input/source/iio/accel_gyro_3d.rs @@ -2,7 +2,7 @@ use std::{collections::HashSet, error::Error, fmt::Debug}; use crate::{ config, - drivers::iio_imu::{self, driver::Driver, info::MountMatrix}, + drivers::iio_imu::{self, hid_sfh_driver::Driver, info::MountMatrix}, input::{ capability::{Capability, Source}, event::{native::NativeEvent, value::InputValue}, @@ -11,17 +11,15 @@ use crate::{ udev::device::UdevDevice, }; -// Scale from IIO SI units to Steam Deck UHID raw LSB. -// IIO channels report m/s² for accel and rad/s for gyro after applying scale: -// https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-bus-iio -// UHID LSB constants from src/drivers/steam_deck/driver.rs. -const ACCEL_SCALE_FACTOR: f64 = 1632.6530612244898; // 1 / 0.0006125 (m/s² → UHID LSB) -const GYRO_SCALE_FACTOR: f64 = 916.7324722093172; // (180/π) / 0.0625 (rad/s → °/s → UHID LSB) - pub struct AccelGyro3dImu { driver: Driver, } +// Sensor fusion hub devices produce bogus scale factors that reduce the output by the below +// factors. These were determined using real world testing. Without adjustment, Accelerometer +// data is in cm/s^2 instead of m/s^2 and Gyroscope data is milliradians instead of radians. +const SFH_CORRECTION: f64 = 100.0; + impl AccelGyro3dImu { /// Create a new Accel Gyro 3D source device with the given udev /// device information @@ -49,8 +47,7 @@ impl AccelGyro3dImu { let sample_rate = config.as_ref().and_then(|c| c.sample_rate); let id = device_info.sysname(); - let name = device_info.name(); - let driver = Driver::new(id, name, mount_matrix, sample_rate)?; + let driver = Driver::new(id, mount_matrix, sample_rate)?; Ok(Self { driver }) } @@ -75,14 +72,7 @@ impl SourceInputDevice for AccelGyro3dImu { } fn get_default_event_filter(&self) -> Result, InputError> { - let filtered_events = self.driver.get_default_event_filter(); - let filtered_events = match filtered_events { - Ok(events) => events, - Err(e) => { - return Err(format!("Failed to get default event filter: {:?}", e).into()); - } - }; - Ok(filtered_events) + Ok(self.driver.get_default_event_filter()?) } } @@ -109,18 +99,18 @@ fn translate_event(event: iio_imu::event::Event) -> NativeEvent { iio_imu::event::Event::Accelerometer(data) => { let cap = Capability::Accelerometer(Source::Center); let value = InputValue::Vector3 { - x: Some(data.roll * ACCEL_SCALE_FACTOR), - y: Some(data.pitch * ACCEL_SCALE_FACTOR), - z: Some(data.yaw * ACCEL_SCALE_FACTOR), + x: Some(data.roll * SFH_CORRECTION), + y: Some(data.pitch * SFH_CORRECTION), + z: Some(data.yaw * SFH_CORRECTION), }; NativeEvent::new(cap, value) } iio_imu::event::Event::Gyro(data) => { let cap = Capability::Gyroscope(Source::Center); let value = InputValue::Vector3 { - x: Some(data.roll * GYRO_SCALE_FACTOR), - y: Some(data.pitch * GYRO_SCALE_FACTOR), - z: Some(data.yaw * GYRO_SCALE_FACTOR), + x: Some(data.roll * SFH_CORRECTION), + y: Some(data.pitch * SFH_CORRECTION), + z: Some(data.yaw * SFH_CORRECTION), }; NativeEvent::new(cap, value) } diff --git a/src/input/source/iio/bmi_imu.rs b/src/input/source/iio/bmi_imu.rs index 0480bdfe..2b2a8264 100644 --- a/src/input/source/iio/bmi_imu.rs +++ b/src/input/source/iio/bmi_imu.rs @@ -1,8 +1,8 @@ -use std::{collections::HashSet, error::Error, f64::consts::PI, fmt::Debug}; +use std::{collections::HashSet, error::Error, fmt::Debug}; use crate::{ config, - drivers::iio_imu::{self, driver::Driver, info::MountMatrix}, + drivers::iio_imu::{self, bmi_driver::Driver, info::MountMatrix}, input::{ capability::{Capability, Source}, event::{native::NativeEvent, value::InputValue}, @@ -42,8 +42,7 @@ impl BmiImu { let sample_rate = config.as_ref().and_then(|c| c.sample_rate); let id = device_info.sysname(); - let name = device_info.name(); - let driver = Driver::new(id, name, mount_matrix, sample_rate)?; + let driver = Driver::new(id, mount_matrix, sample_rate)?; Ok(Self { driver }) } @@ -68,14 +67,7 @@ impl SourceInputDevice for BmiImu { } fn get_default_event_filter(&self) -> Result, InputError> { - let filtered_events = self.driver.get_default_event_filter(); - let filtered_events = match filtered_events { - Ok(events) => events, - Err(e) => { - return Err(format!("Failed to get default event filter: {:?}", e).into()); - } - }; - Ok(filtered_events) + Ok(HashSet::new()) } } @@ -109,16 +101,11 @@ fn translate_event(event: iio_imu::event::Event) -> NativeEvent { NativeEvent::new(cap, value) } iio_imu::event::Event::Gyro(data) => { - // Translate gyro values into the expected units of degrees per sec - // We apply a 12x scale so the lowest (default) value feels like natural 1:1 motion. - // Adjusting the scale will increase the granularity of the motion by slowing - // incrementing closer to 2:1 motion. From testing this is the highest scale we can - // apply before noise is amplified to the point the gyro cannot calibrate. let cap = Capability::Gyroscope(Source::Center); let value = InputValue::Vector3 { - x: Some(data.roll * (180.0 / PI) * 12.0), - y: Some(data.pitch * (180.0 / PI) * 12.0), - z: Some(data.yaw * (180.0 / PI) * 12.0), + x: Some(data.roll), + y: Some(data.pitch), + z: Some(data.yaw), }; NativeEvent::new(cap, value) } diff --git a/src/input/source/mod.rs b/src/input/source/mod.rs index da26e2ba..5844fce6 100644 --- a/src/input/source/mod.rs +++ b/src/input/source/mod.rs @@ -5,7 +5,7 @@ use std::{ str::FromStr, sync::{Arc, Mutex, MutexGuard}, thread, - time::Duration, + time::{Duration, Instant}, }; use ::evdev::FFEffectData; @@ -384,7 +384,7 @@ impl SourceDriver log::error!("Failed to set default event filter for {device_id}: {e}"); }; loop { - let poll_time_start = std::time::Instant::now(); + let poll_time_start = Instant::now(); // Create a context with performance metrics for each event let mut context = if metrics_enabled { From 2937b970aa5e3bee4434539b6eeafadba23e1385 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Wed, 16 Sep 2026 16:40:57 -0700 Subject: [PATCH 6/7] fix: Standardize all IMU source and target units to SI --- src/drivers/dualsense/driver.rs | 61 ++++--------------- src/drivers/dualsense/event.rs | 18 +++--- src/drivers/dualsense/hid_report.rs | 2 +- src/drivers/dualsense/mod.rs | 42 +++++++++++++ src/drivers/flydigi_vader_4_pro/driver.rs | 14 ++--- src/drivers/flydigi_vader_4_pro/event.rs | 8 +-- src/drivers/horipad_steam/driver.rs | 19 +++--- src/drivers/horipad_steam/event.rs | 8 +-- src/drivers/horipad_steam/mod.rs | 5 ++ src/drivers/legos/event.rs | 6 +- src/drivers/legos/imu_driver.rs | 14 ++--- src/drivers/legos/mod.rs | 5 +- src/drivers/steam_deck/driver.rs | 34 +++++------ src/drivers/steam_deck/event.rs | 16 ++--- src/drivers/steam_deck/mod.rs | 6 ++ src/drivers/ultimate_2/mod.rs | 3 + src/input/source/hidraw/dualsense.rs | 45 +++++++------- .../source/hidraw/flydigi_vader_4_pro.rs | 17 +++--- src/input/source/hidraw/horipad_steam.rs | 14 ++--- src/input/source/hidraw/legos_imu.rs | 12 ++-- src/input/source/hidraw/steam_deck.rs | 25 +++----- src/input/target/dualsense.rs | 50 +++++++++------ src/input/target/horipad_steam.rs | 33 +++++----- src/input/target/steam_deck.rs | 24 +++++--- src/input/target/steam_deck_uhid.rs | 24 +++++--- src/input/target/ulitmate_2.rs | 21 ++++--- 26 files changed, 291 insertions(+), 235 deletions(-) diff --git a/src/drivers/dualsense/driver.rs b/src/drivers/dualsense/driver.rs index 59411bcd..b7b51cfa 100644 --- a/src/drivers/dualsense/driver.rs +++ b/src/drivers/dualsense/driver.rs @@ -9,48 +9,11 @@ use crate::drivers::dualsense::{ }; use super::{ - event::{AccelerometerEvent, AccelerometerInput, AxisEvent, AxisInput, Event, TouchAxisInput}, + event::{AxisEvent, AxisInput, Event, InertialInput, IntertialEvent, TouchAxisInput}, hid_report::{PackedInputDataReport, SetStatePackedOutputData, UsbPackedOutputReport}, + DS5_ACCEL_TO_SI, DS5_GYRO_TO_RADS, DS5_VID, INPUT_REPORT_BT_SIZE, PIDS, }; -// Source: https://github.com/torvalds/linux/blob/master/drivers/hid/hid-playstation.c -pub const DS5_EDGE_NAME: &str = "Sony Interactive Entertainment DualSense Edge Wireless Controller"; -pub const DS5_EDGE_VERSION: u16 = 256; -pub const DS5_EDGE_VID: u16 = 0x054c; -pub const DS5_EDGE_PID: u16 = 0x0df2; - -pub const DS5_NAME: &str = "Sony Interactive Entertainment DualSense Wireless Controller"; -pub const DS5_VERSION: u16 = 0x8111; -pub const DS5_VID: u16 = 0x054c; -pub const DS5_PID: u16 = 0x0ce6; - -pub const PIDS: [u16; 2] = [DS5_EDGE_PID, DS5_PID]; - -pub const FEATURE_REPORT_PAIRING_INFO: u8 = 0x09; -pub const FEATURE_REPORT_FIRMWARE_INFO: u8 = 0x20; -pub const FEATURE_REPORT_CALIBRATION: u8 = 0x05; - -pub const INPUT_REPORT_USB: u8 = 0x01; -pub const INPUT_REPORT_USB_SIZE: usize = 64; -pub const INPUT_REPORT_BT: u8 = 0x31; -pub const INPUT_REPORT_BT_SIZE: usize = 78; -pub const OUTPUT_REPORT_USB: u8 = 0x02; -pub const OUTPUT_REPORT_USB_SIZE: usize = 63; -pub const OUTPUT_REPORT_USB_SHORT_SIZE: usize = 48; -pub const OUTPUT_REPORT_BT: u8 = 0x31; -pub const OUTPUT_REPORT_BT_SIZE: usize = 78; - -// Input report axis ranges -pub const STICK_X_MIN: f64 = u8::MIN as f64; -pub const STICK_X_MAX: f64 = u8::MAX as f64; -pub const STICK_Y_MIN: f64 = u8::MIN as f64; -pub const STICK_Y_MAX: f64 = u8::MAX as f64; -pub const TRIGGER_MAX: f64 = u8::MAX as f64; - -// DualSense hardware limits -pub const DS5_TOUCHPAD_WIDTH: f64 = 1919.0; -pub const DS5_TOUCHPAD_HEIGHT: f64 = 1079.0; - /// PS5 Dualsense controller driver for reading gamepad input pub struct Driver { state: Option, @@ -419,18 +382,18 @@ impl Driver { } // Accelerometer events - events.push(Event::Accelerometer(AccelerometerEvent::Accelerometer( - AccelerometerInput { - x: state.accel_x.to_primitive(), - y: state.accel_y.to_primitive(), - z: state.accel_z.to_primitive(), + events.push(Event::Accelerometer(IntertialEvent::Accelerometer( + InertialInput { + x: state.accel_x.to_primitive() as f64 * DS5_ACCEL_TO_SI, + y: state.accel_y.to_primitive() as f64 * DS5_ACCEL_TO_SI, + z: state.accel_z.to_primitive() as f64 * DS5_ACCEL_TO_SI, }, ))); - events.push(Event::Accelerometer(AccelerometerEvent::Gyro( - AccelerometerInput { - x: state.pitch.to_primitive(), - y: state.yaw.to_primitive(), - z: state.roll.to_primitive(), + events.push(Event::Accelerometer(IntertialEvent::Gyroscope( + InertialInput { + x: state.pitch.to_primitive() as f64 * DS5_GYRO_TO_RADS, + y: state.yaw.to_primitive() as f64 * DS5_GYRO_TO_RADS, + z: state.roll.to_primitive() as f64 * DS5_GYRO_TO_RADS, }, ))); diff --git a/src/drivers/dualsense/event.rs b/src/drivers/dualsense/event.rs index 98ff1ec9..f1a8d67e 100644 --- a/src/drivers/dualsense/event.rs +++ b/src/drivers/dualsense/event.rs @@ -2,7 +2,7 @@ #[derive(Clone, Debug)] pub enum Event { Button(ButtonEvent), - Accelerometer(AccelerometerEvent), + Accelerometer(IntertialEvent), Axis(AxisEvent), Trigger(TriggerEvent), } @@ -78,18 +78,18 @@ pub enum TriggerEvent { R2(TriggerInput), } -/// AccelerometerInput represents the state of the accelerometer (x, y, z) values +/// InertialInput represents the state of the IMU (x, y, z) values #[derive(Clone, Debug)] -pub struct AccelerometerInput { - pub x: i16, - pub y: i16, - pub z: i16, +pub struct InertialInput { + pub x: f64, + pub y: f64, + pub z: f64, } /// AccelerometerEvent has data from the accelerometer #[derive(Clone, Debug)] -pub enum AccelerometerEvent { - Accelerometer(AccelerometerInput), +pub enum IntertialEvent { + Accelerometer(InertialInput), /// Pitch, yaw, roll - Gyro(AccelerometerInput), + Gyroscope(InertialInput), } diff --git a/src/drivers/dualsense/hid_report.rs b/src/drivers/dualsense/hid_report.rs index 0faddf26..5ca96d36 100644 --- a/src/drivers/dualsense/hid_report.rs +++ b/src/drivers/dualsense/hid_report.rs @@ -5,7 +5,7 @@ use std::{error::Error, fmt::Display}; use packed_struct::prelude::*; -use super::driver::*; +use super::{INPUT_REPORT_BT, INPUT_REPORT_BT_SIZE, INPUT_REPORT_USB, INPUT_REPORT_USB_SIZE}; /// DualSense input report for USB and Bluetooth #[derive(Debug, Copy, Clone)] diff --git a/src/drivers/dualsense/mod.rs b/src/drivers/dualsense/mod.rs index 64242bb3..f81eca28 100644 --- a/src/drivers/dualsense/mod.rs +++ b/src/drivers/dualsense/mod.rs @@ -4,3 +4,45 @@ pub mod hid_report; #[cfg(test)] mod hid_report_test; pub mod report_descriptor; + +// Source: https://github.com/torvalds/linux/blob/master/drivers/hid/hid-playstation.c +pub const DS5_EDGE_NAME: &str = "Sony Interactive Entertainment DualSense Edge Wireless Controller"; +pub const DS5_EDGE_VERSION: u16 = 256; +pub const DS5_EDGE_VID: u16 = 0x054c; +pub const DS5_EDGE_PID: u16 = 0x0df2; + +pub const DS5_NAME: &str = "Sony Interactive Entertainment DualSense Wireless Controller"; +pub const DS5_VERSION: u16 = 0x8111; +pub const DS5_VID: u16 = 0x054c; +pub const DS5_PID: u16 = 0x0ce6; + +pub const PIDS: [u16; 2] = [DS5_EDGE_PID, DS5_PID]; + +pub const FEATURE_REPORT_PAIRING_INFO: u8 = 0x09; +pub const FEATURE_REPORT_FIRMWARE_INFO: u8 = 0x20; +pub const FEATURE_REPORT_CALIBRATION: u8 = 0x05; + +pub const INPUT_REPORT_USB: u8 = 0x01; +pub const INPUT_REPORT_USB_SIZE: usize = 64; +pub const INPUT_REPORT_BT: u8 = 0x31; +pub const INPUT_REPORT_BT_SIZE: usize = 78; +pub const OUTPUT_REPORT_USB: u8 = 0x02; +pub const OUTPUT_REPORT_USB_SIZE: usize = 63; +pub const OUTPUT_REPORT_USB_SHORT_SIZE: usize = 48; +pub const OUTPUT_REPORT_BT: u8 = 0x31; +pub const OUTPUT_REPORT_BT_SIZE: usize = 78; + +// Input report axis ranges +pub const STICK_X_MIN: f64 = u8::MIN as f64; +pub const STICK_X_MAX: f64 = u8::MAX as f64; +pub const STICK_Y_MIN: f64 = u8::MIN as f64; +pub const STICK_Y_MAX: f64 = u8::MAX as f64; +pub const TRIGGER_MAX: f64 = u8::MAX as f64; + +// DualSense hardware limits +pub const DS5_ACCEL_TO_SI: f64 = 0.00119710083; +pub const DS5_SI_TO_ACCEL: f64 = 0.101971621; +pub const DS5_GYRO_TO_RADS: f64 = 0.00001706026; +pub const DS5_RADS_TO_GYRO: f64 = 57.29577951; +pub const DS5_TOUCHPAD_WIDTH: f64 = 1919.0; +pub const DS5_TOUCHPAD_HEIGHT: f64 = 1079.0; diff --git a/src/drivers/flydigi_vader_4_pro/driver.rs b/src/drivers/flydigi_vader_4_pro/driver.rs index 1d411b55..238e798a 100644 --- a/src/drivers/flydigi_vader_4_pro/driver.rs +++ b/src/drivers/flydigi_vader_4_pro/driver.rs @@ -284,16 +284,16 @@ impl Driver { // Accelerometer events events.push(Event::Inertia(InertialEvent::Accelerometer( InertialInput { - x: -state.accel_x.to_primitive(), - y: state.accel_y.to_primitive(), - z: state.accel_z.to_primitive(), + x: -state.accel_x.to_primitive() as f64, + y: state.accel_y.to_primitive() as f64, + z: state.accel_z.to_primitive() as f64, }, ))); // Gyro events. They need to be rotated in order for them to be read properly - events.push(Event::Inertia(InertialEvent::Gyro(InertialInput { - x: -(state.gyro_x.to_primitive() as i32 * 1143239 / i16::MAX as i32) as i16, - y: -(state.get_y() as i32 * 1143239 / i16::MAX as i32) as i16, - z: -(state.gyro_z.to_primitive() as i32 * 17873 / i16::MAX as i32) as i16, + events.push(Event::Inertia(InertialEvent::Gyroscope(InertialInput { + x: -(state.gyro_x.to_primitive() as f64 * 1143239.0 / i16::MAX as f64), + y: -(state.get_y() as f64 * 1143239.0 / i16::MAX as f64), + z: -(state.gyro_z.to_primitive() as f64 * 17873.0 / i16::MAX as f64), }))); events } diff --git a/src/drivers/flydigi_vader_4_pro/event.rs b/src/drivers/flydigi_vader_4_pro/event.rs index 3ac2f5d8..da4a532d 100644 --- a/src/drivers/flydigi_vader_4_pro/event.rs +++ b/src/drivers/flydigi_vader_4_pro/event.rs @@ -69,16 +69,16 @@ pub enum ButtonEvent { /// [InertialInput] represents the state of the IMU (x, y, z) values #[derive(Clone, Debug)] pub struct InertialInput { - pub x: i16, - pub y: i16, - pub z: i16, + pub x: f64, + pub y: f64, + pub z: f64, } /// [InertialEvent] has data from the IMU #[derive(Clone, Debug)] pub enum InertialEvent { Accelerometer(InertialInput), - Gyro(InertialInput), + Gyroscope(InertialInput), } /// [JoystickInput] is a double (x, y) axis diff --git a/src/drivers/horipad_steam/driver.rs b/src/drivers/horipad_steam/driver.rs index 50acb0f3..3d58b191 100644 --- a/src/drivers/horipad_steam/driver.rs +++ b/src/drivers/horipad_steam/driver.rs @@ -3,7 +3,10 @@ use std::{error::Error, ffi::CString}; use hidapi::HidDevice; use packed_struct::{types::SizedInteger, PackedStruct}; -use crate::{drivers::horipad_steam::hid_report::Direction, udev::device::UdevDevice}; +use crate::{ + drivers::horipad_steam::{hid_report::Direction, HORIPAD_ACCEL_TO_SI, HORIPAD_GYRO_TO_RADS}, + udev::device::UdevDevice, +}; use super::{ event::{ @@ -273,15 +276,15 @@ impl Driver { // Accelerometer events events.push(Event::Inertia(InertialEvent::Accelerometer( InertialInput { - x: -state.accel_x.to_primitive(), - y: state.accel_y.to_primitive(), - z: -state.accel_z.to_primitive(), + x: -state.accel_x.to_primitive() as f64 * HORIPAD_ACCEL_TO_SI, + y: state.accel_y.to_primitive() as f64 * HORIPAD_ACCEL_TO_SI, + z: -state.accel_z.to_primitive() as f64 * HORIPAD_ACCEL_TO_SI, }, ))); - events.push(Event::Inertia(InertialEvent::Gyro(InertialInput { - x: -state.pitch.to_primitive(), - y: state.yaw.to_primitive(), - z: -state.roll.to_primitive(), + events.push(Event::Inertia(InertialEvent::Gyroscope(InertialInput { + x: -state.pitch.to_primitive() as f64 * HORIPAD_GYRO_TO_RADS, + y: state.yaw.to_primitive() as f64 * HORIPAD_GYRO_TO_RADS, + z: -state.roll.to_primitive() as f64 * HORIPAD_GYRO_TO_RADS, }))); log::trace!("Got events: {events:?}"); diff --git a/src/drivers/horipad_steam/event.rs b/src/drivers/horipad_steam/event.rs index c61ccbdb..3ee157c1 100644 --- a/src/drivers/horipad_steam/event.rs +++ b/src/drivers/horipad_steam/event.rs @@ -69,16 +69,16 @@ pub enum ButtonEvent { /// [InertialInput] represents the state of the IMU (x, y, z) values #[derive(Clone, Debug)] pub struct InertialInput { - pub x: i16, - pub y: i16, - pub z: i16, + pub x: f64, + pub y: f64, + pub z: f64, } /// [InertialEvent] has data from the IMU #[derive(Clone, Debug)] pub enum InertialEvent { Accelerometer(InertialInput), - Gyro(InertialInput), + Gyroscope(InertialInput), } /// [JoystickInput] is a double (x, y) axis diff --git a/src/drivers/horipad_steam/mod.rs b/src/drivers/horipad_steam/mod.rs index 2fea094e..a9a0349b 100644 --- a/src/drivers/horipad_steam/mod.rs +++ b/src/drivers/horipad_steam/mod.rs @@ -21,3 +21,8 @@ pub const TRIGGER_AXIS_MAX: f64 = 255.0; pub const VID: u16 = 0x0F0D; pub const PIDS: [u16; 2] = [0x0196, 0x01AB]; + +pub const HORIPAD_ACCEL_TO_SI: f64 = 0.00059855041; +pub const HORIPAD_SI_TO_ACCEL: f64 = 0.101971621; +pub const HORIPAD_GYRO_TO_RADS: f64 = 0.00106422393; +pub const HORIPAD_RADS_TO_GYRO: f64 = 57.29577951; diff --git a/src/drivers/legos/event.rs b/src/drivers/legos/event.rs index 64e535db..01d1bfe0 100644 --- a/src/drivers/legos/event.rs +++ b/src/drivers/legos/event.rs @@ -87,9 +87,9 @@ pub enum AxisEvent { /// [InertialInput] represents the state of the IMU (x, y, z) values #[derive(Clone, Debug)] pub struct InertialInput { - pub x: i16, - pub y: i16, - pub z: i16, + pub x: f64, + pub y: f64, + pub z: f64, } /// [InertialEvent] has data from the IMU diff --git a/src/drivers/legos/imu_driver.rs b/src/drivers/legos/imu_driver.rs index 6d92be25..5200043f 100644 --- a/src/drivers/legos/imu_driver.rs +++ b/src/drivers/legos/imu_driver.rs @@ -6,7 +6,7 @@ use packed_struct::{types::SizedInteger, PackedStruct}; use super::{ event::{Event, InertialEvent, InertialInput}, hid_report::{InertialDataReport, InputReportType}, - GYRO_SCALE, IMU_IID, IMU_TIMEOUT, INERTIAL_PACKET_SIZE, PIDS, VID, + ACCEL_TO_SI, GPS_TO_RADS, IMU_TIMEOUT, IMU_IID, INERTIAL_PACKET_SIZE, PIDS, VID, }; pub struct IMUDriver { @@ -132,9 +132,9 @@ impl IMUDriver { if state.x != old_state.x || state.y != old_state.y || state.z != old_state.z { events.push(Event::Inertia(InertialEvent::Accelerometer( InertialInput { - x: -state.x.to_primitive(), - y: -state.y.to_primitive(), - z: -state.z.to_primitive(), + x: -state.x.to_primitive() as f64 * ACCEL_TO_SI, + y: -state.y.to_primitive() as f64 * ACCEL_TO_SI, + z: -state.z.to_primitive() as f64 * ACCEL_TO_SI, }, ))) }; @@ -156,9 +156,9 @@ impl IMUDriver { if state.x != old_state.x || state.y != old_state.y || state.z != old_state.z { events.push(Event::Inertia(InertialEvent::Gyro(InertialInput { - x: -state.x.to_primitive() * GYRO_SCALE, - y: -state.y.to_primitive() * GYRO_SCALE, - z: -state.z.to_primitive() * GYRO_SCALE, + x: -state.x.to_primitive() as f64 * GPS_TO_RADS, + y: -state.y.to_primitive() as f64 * GPS_TO_RADS, + z: -state.z.to_primitive() as f64 * GPS_TO_RADS, }))) }; diff --git a/src/drivers/legos/mod.rs b/src/drivers/legos/mod.rs index 6a5cd8bc..d15a969f 100644 --- a/src/drivers/legos/mod.rs +++ b/src/drivers/legos/mod.rs @@ -1,3 +1,5 @@ +use std::f64::consts::PI; + pub mod event; pub mod hid_report; pub mod imu_driver; @@ -20,7 +22,8 @@ const TOUCH_PACKET_SIZE: usize = 10; const XINPUT_PACKET_SIZE: usize = 32; // Input report axis ranges -pub const GYRO_SCALE: i16 = 2; +pub const ACCEL_TO_SI: f64 = 0.0980665; +pub const GPS_TO_RADS: f64 = 0.1 * (PI / 180.0); pub const PAD_FORCE_MAX: f64 = 127.0; pub const PAD_FORCE_NORMAL: u8 = 32; /* Simulated average pressure */ pub const PAD_MOTION_MAX: f64 = 400.0; diff --git a/src/drivers/steam_deck/driver.rs b/src/drivers/steam_deck/driver.rs index 87b31db9..2ace0ed7 100644 --- a/src/drivers/steam_deck/driver.rs +++ b/src/drivers/steam_deck/driver.rs @@ -1,6 +1,8 @@ use std::{error::Error, ffi::CString}; -use crate::drivers::steam_deck::{hid_report::PackedInputDataReport, ProductId, VID}; +use crate::drivers::steam_deck::{ + hid_report::PackedInputDataReport, ProductId, DECK_ACCEL_TO_SI, DECK_GYRO_TO_RADS, VID, +}; use hidapi::HidDevice; use packed_struct::{ types::{Integer, SizedInteger}, @@ -9,16 +11,12 @@ use packed_struct::{ use super::{ event::{ - AccelerometerEvent, AccelerometerInput, AxisEvent, AxisInput, BinaryInput, ButtonEvent, - Event, TouchAxisInput, TriggerEvent, TriggerInput, + AxisEvent, AxisInput, BinaryInput, ButtonEvent, Event, IntertialEvent, IntertialInput, + TouchAxisInput, TriggerEvent, TriggerInput, }, hid_report::{PackedMappingsReport, PackedRumbleReport, Register, ReportType, TrackpadMode}, }; -/// Scale to multiply accelerometer values to get in units of meters per second -pub const ACCEL_SCALE: f64 = 0.0006125; -/// Scale to multiply gyro values to get in units of degrees per second -//pub const GYRO_SCALE: f64 = 0.0625; /// Size of the HID packet const PACKET_SIZE: usize = 64; /// Timeout in milliseconds for reading an HID packet @@ -407,18 +405,20 @@ impl Driver { } // Accelerometer events - events.push(Event::Accelerometer(AccelerometerEvent::Accelerometer( - AccelerometerInput { - x: state.accel_x.to_primitive(), - y: state.accel_y.to_primitive(), - z: state.accel_z.to_primitive(), + events.push(Event::Accelerometer(IntertialEvent::Accelerometer( + IntertialInput { + // Convert to Intermediate SI units for NativeEvent handling + x: state.accel_x.to_primitive() as f64 * DECK_ACCEL_TO_SI, + y: state.accel_y.to_primitive() as f64 * DECK_ACCEL_TO_SI, + z: state.accel_z.to_primitive() as f64 * DECK_ACCEL_TO_SI, }, ))); - events.push(Event::Accelerometer(AccelerometerEvent::Attitude( - AccelerometerInput { - x: state.pitch.to_primitive(), - y: state.yaw.to_primitive(), - z: state.roll.to_primitive(), + events.push(Event::Accelerometer(IntertialEvent::Gyroscope( + IntertialInput { + // Convert to Intermediate SI units for NativeEvent handling + x: state.pitch.to_primitive() as f64 * DECK_GYRO_TO_RADS, + y: state.yaw.to_primitive() as f64 * DECK_GYRO_TO_RADS, + z: state.roll.to_primitive() as f64 * DECK_GYRO_TO_RADS, }, ))); }; diff --git a/src/drivers/steam_deck/event.rs b/src/drivers/steam_deck/event.rs index ee474802..800b1bea 100644 --- a/src/drivers/steam_deck/event.rs +++ b/src/drivers/steam_deck/event.rs @@ -2,7 +2,7 @@ #[derive(Clone, Debug)] pub enum Event { Button(ButtonEvent), - Accelerometer(AccelerometerEvent), + Accelerometer(IntertialEvent), Axis(AxisEvent), Trigger(TriggerEvent), } @@ -118,16 +118,16 @@ pub enum TriggerEvent { /// AccelerometerInput represents the state of the accelerometer (x, y, z) values #[derive(Clone, Debug)] -pub struct AccelerometerInput { - pub x: i16, - pub y: i16, - pub z: i16, +pub struct IntertialInput { + pub x: f64, + pub y: f64, + pub z: f64, } /// AccelerometerEvent has data from the accelerometer #[derive(Clone, Debug)] -pub enum AccelerometerEvent { - Accelerometer(AccelerometerInput), +pub enum IntertialEvent { + Accelerometer(IntertialInput), /// Pitch, yaw, roll - Attitude(AccelerometerInput), + Gyroscope(IntertialInput), } diff --git a/src/drivers/steam_deck/mod.rs b/src/drivers/steam_deck/mod.rs index ef5606c0..83a01578 100644 --- a/src/drivers/steam_deck/mod.rs +++ b/src/drivers/steam_deck/mod.rs @@ -19,6 +19,12 @@ pub enum ProductId { /// Vendor ID pub const VID: u16 = 0x28de; +/// Scale to multiply accelerometer values to get in units of meters per second +pub const DECK_ACCEL_TO_SI: f64 = 0.0006125; +pub const DECK_SI_TO_ACCEL: f64 = 1632.6530612244898; +pub const DECK_GYRO_TO_RADS: f64 = 0.00109083078; +pub const DECK_RADS_TO_GYRO: f64 = 916.7324722093181; + impl ProductId { pub fn to_u16(&self) -> u16 { match self { diff --git a/src/drivers/ultimate_2/mod.rs b/src/drivers/ultimate_2/mod.rs index 92242833..4d17578f 100644 --- a/src/drivers/ultimate_2/mod.rs +++ b/src/drivers/ultimate_2/mod.rs @@ -12,5 +12,8 @@ pub const JOY_AXIS_MAX: f64 = 255.0; pub const JOY_AXIS_MIN: f64 = 0.0; pub const TRIGGER_AXIS_MAX: f64 = 255.0; +// Accel scale: 4096 raw units = 1G (derived from SDL_hidapi_8bitdo.c) +pub const ACCEL_SCALE: f64 = 4096.0; + pub const REPORT_ID_INPUT: u8 = 0x01; pub const REPORT_ID_RUMBLE: u8 = 0x05; diff --git a/src/input/source/hidraw/dualsense.rs b/src/input/source/hidraw/dualsense.rs index 1af11a05..9427e10e 100644 --- a/src/input/source/hidraw/dualsense.rs +++ b/src/input/source/hidraw/dualsense.rs @@ -4,7 +4,10 @@ use std::{collections::HashMap, error::Error}; use evdev::{FFEffectData, FFEffectKind}; use packed_struct::types::SizedInteger; -use crate::drivers::dualsense::driver::{DS5_EDGE_PID, DS5_PID, DS5_VID}; +use crate::drivers::dualsense::{ + DS5_EDGE_PID, DS5_PID, DS5_TOUCHPAD_HEIGHT, DS5_TOUCHPAD_WIDTH, DS5_VID, STICK_X_MAX, + STICK_X_MIN, STICK_Y_MAX, STICK_Y_MIN, TRIGGER_MAX, +}; use crate::drivers::steam_deck::hid_report::PackedRumbleReport; use crate::input::capability::Source; use crate::input::output_capability::{OutputCapability, LED}; @@ -338,20 +341,20 @@ fn translate_event(event: dualsense::event::Event) -> NativeEvent { ), }, dualsense::event::Event::Accelerometer(accel) => match accel { - dualsense::event::AccelerometerEvent::Accelerometer(value) => NativeEvent::new( + dualsense::event::IntertialEvent::Accelerometer(value) => NativeEvent::new( Capability::Accelerometer(Source::Center), InputValue::Vector3 { - x: Some(value.x as f64), - y: Some(value.y as f64), - z: Some(value.z as f64), + x: Some(value.x), + y: Some(value.y), + z: Some(value.z), }, ), - dualsense::event::AccelerometerEvent::Gyro(value) => NativeEvent::new( + dualsense::event::IntertialEvent::Gyroscope(value) => NativeEvent::new( Capability::Gyroscope(Source::Center), InputValue::Vector3 { - x: Some(value.x as f64), - y: Some(value.y as f64), - z: Some(value.z as f64), + x: Some(value.x), + y: Some(value.y), + z: Some(value.z), }, ), }, @@ -387,12 +390,12 @@ fn translate_event(event: dualsense::event::Event) -> NativeEvent { fn normalize_axis_value(event: &dualsense::event::AxisEvent) -> InputValue { match event { dualsense::event::AxisEvent::Pad(value) => { - let max = dualsense::driver::DS5_TOUCHPAD_WIDTH; + let max = DS5_TOUCHPAD_WIDTH; let x = normalize_unsigned_value(value.x as f64, max); let x = Some(x); - let max = dualsense::driver::DS5_TOUCHPAD_HEIGHT; + let max = DS5_TOUCHPAD_HEIGHT; let y = normalize_unsigned_value(value.y as f64, max); let y = Some(y); @@ -405,26 +408,26 @@ fn normalize_axis_value(event: &dualsense::event::AxisEvent) -> InputValue { } } dualsense::event::AxisEvent::LStick(value) => { - let min = dualsense::driver::STICK_X_MIN; - let max = dualsense::driver::STICK_X_MAX; + let min = STICK_X_MIN; + let max = STICK_X_MAX; let x = normalize_signed_value(value.x as f64, min, max); let x = Some(x); - let min = dualsense::driver::STICK_Y_MIN; - let max = dualsense::driver::STICK_Y_MAX; + let min = STICK_Y_MIN; + let max = STICK_Y_MAX; let y = normalize_signed_value(value.y as f64, min, max); let y = Some(y); InputValue::Vector2 { x, y } } dualsense::event::AxisEvent::RStick(value) => { - let min = dualsense::driver::STICK_X_MIN; - let max = dualsense::driver::STICK_X_MAX; + let min = STICK_X_MIN; + let max = STICK_X_MAX; let x = normalize_signed_value(value.x as f64, min, max); let x = Some(x); - let min = dualsense::driver::STICK_Y_MIN; - let max = dualsense::driver::STICK_Y_MAX; + let min = STICK_Y_MIN; + let max = STICK_Y_MAX; let y = normalize_signed_value(value.y as f64, min, max); let y = Some(y); @@ -438,11 +441,11 @@ fn normalize_axis_value(event: &dualsense::event::AxisEvent) -> InputValue { fn normalize_trigger_value(event: &dualsense::event::TriggerEvent) -> InputValue { match event { dualsense::event::TriggerEvent::L2(value) => { - let max = dualsense::driver::TRIGGER_MAX; + let max = TRIGGER_MAX; InputValue::Float(normalize_unsigned_value(value.value as f64, max)) } dualsense::event::TriggerEvent::R2(value) => { - let max = dualsense::driver::TRIGGER_MAX; + let max = TRIGGER_MAX; InputValue::Float(normalize_unsigned_value(value.value as f64, max)) } } diff --git a/src/input/source/hidraw/flydigi_vader_4_pro.rs b/src/input/source/hidraw/flydigi_vader_4_pro.rs index 85dcc48d..e96ee1e0 100644 --- a/src/input/source/hidraw/flydigi_vader_4_pro.rs +++ b/src/input/source/hidraw/flydigi_vader_4_pro.rs @@ -9,8 +9,7 @@ use crate::{ capability::{Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger, Source}, event::{ native::NativeEvent, - value::InputValue, - value::{normalize_signed_value, normalize_unsigned_value}, + value::{normalize_signed_value, normalize_unsigned_value, InputValue}, }, source::{InputError, SourceInputDevice, SourceOutputDevice}, }, @@ -238,17 +237,17 @@ fn translate_event(event: event::Event) -> NativeEvent { event::InertialEvent::Accelerometer(value) => NativeEvent::new( Capability::Accelerometer(Source::Center), InputValue::Vector3 { - x: Some(value.x as f64), - y: Some(value.y as f64), - z: Some(value.z as f64), + x: Some(value.x), + y: Some(value.y), + z: Some(value.z), }, ), - event::InertialEvent::Gyro(value) => NativeEvent::new( + event::InertialEvent::Gyroscope(value) => NativeEvent::new( Capability::Gyroscope(Source::Center), InputValue::Vector3 { - x: Some(value.x as f64), - y: Some(value.y as f64), - z: Some(value.z as f64), + x: Some(value.x), + y: Some(value.y), + z: Some(value.z), }, ), }, diff --git a/src/input/source/hidraw/horipad_steam.rs b/src/input/source/hidraw/horipad_steam.rs index 2e4566f7..04f955f9 100644 --- a/src/input/source/hidraw/horipad_steam.rs +++ b/src/input/source/hidraw/horipad_steam.rs @@ -228,17 +228,17 @@ fn translate_event(event: event::Event) -> NativeEvent { event::InertialEvent::Accelerometer(value) => NativeEvent::new( Capability::Accelerometer(Source::Center), InputValue::Vector3 { - x: Some(value.x as f64), - y: Some(value.y as f64), - z: Some(value.z as f64), + x: Some(value.x), + y: Some(value.y), + z: Some(value.z), }, ), - event::InertialEvent::Gyro(value) => NativeEvent::new( + event::InertialEvent::Gyroscope(value) => NativeEvent::new( Capability::Gyroscope(Source::Center), InputValue::Vector3 { - x: Some(value.x as f64), - y: Some(value.y as f64), - z: Some(value.z as f64), + x: Some(value.x), + y: Some(value.y), + z: Some(value.z), }, ), }, diff --git a/src/input/source/hidraw/legos_imu.rs b/src/input/source/hidraw/legos_imu.rs index 2a6ce4d3..bc65650c 100644 --- a/src/input/source/hidraw/legos_imu.rs +++ b/src/input/source/hidraw/legos_imu.rs @@ -66,17 +66,17 @@ fn translate_event(event: event::Event) -> NativeEvent { event::InertialEvent::Accelerometer(value) => NativeEvent::new( Capability::Accelerometer(Source::Center), InputValue::Vector3 { - x: Some(value.x as f64), - y: Some(value.y as f64), - z: Some(value.z as f64), + x: Some(value.x), + y: Some(value.y), + z: Some(value.z), }, ), event::InertialEvent::Gyro(value) => NativeEvent::new( Capability::Gyroscope(Source::Center), InputValue::Vector3 { - x: Some(value.x as f64), - y: Some(value.y as f64), - z: Some(value.z as f64), + x: Some(value.x), + y: Some(value.y), + z: Some(value.z), }, ), }, diff --git a/src/input/source/hidraw/steam_deck.rs b/src/input/source/hidraw/steam_deck.rs index 91d28717..9514c5f2 100644 --- a/src/input/source/hidraw/steam_deck.rs +++ b/src/input/source/hidraw/steam_deck.rs @@ -13,11 +13,7 @@ use packed_struct::PackedStruct; use crate::{ drivers::{ dualsense::hid_report::SetStatePackedOutputData, - steam_deck::{ - self, - driver::{Driver, ACCEL_SCALE}, - hid_report::LIZARD_SLEEP_SEC, - }, + steam_deck::{self, driver::Driver, hid_report::LIZARD_SLEEP_SEC}, }, input::{ capability::{ @@ -26,8 +22,7 @@ use crate::{ }, event::{ native::NativeEvent, - value::InputValue, - value::{normalize_signed_value, normalize_unsigned_value}, + value::{normalize_signed_value, normalize_unsigned_value, InputValue}, }, output_capability::{Haptic, OutputCapability}, output_event::OutputEvent, @@ -529,20 +524,20 @@ fn translate_event(event: steam_deck::event::Event) -> NativeEvent { ), }, steam_deck::event::Event::Accelerometer(accel) => match accel { - steam_deck::event::AccelerometerEvent::Accelerometer(value) => NativeEvent::new( + steam_deck::event::IntertialEvent::Accelerometer(value) => NativeEvent::new( Capability::Accelerometer(Source::Center), InputValue::Vector3 { - x: Some(value.x as f64 * ACCEL_SCALE), - y: Some(value.y as f64 * ACCEL_SCALE), - z: Some(value.z as f64 * ACCEL_SCALE), + x: Some(value.x), + y: Some(value.y), + z: Some(value.z), }, ), - steam_deck::event::AccelerometerEvent::Attitude(value) => NativeEvent::new( + steam_deck::event::IntertialEvent::Gyroscope(value) => NativeEvent::new( Capability::Gyroscope(Source::Center), InputValue::Vector3 { - x: Some(value.x as f64), - y: Some(value.y as f64), - z: Some(value.z as f64), + x: Some(value.x), + y: Some(value.y), + z: Some(value.z), }, ), }, diff --git a/src/input/target/dualsense.rs b/src/input/target/dualsense.rs index 133dfec8..0782b835 100644 --- a/src/input/target/dualsense.rs +++ b/src/input/target/dualsense.rs @@ -4,21 +4,12 @@ //! https://github.com/NeroReflex/ROGueENEMY/ use std::{cmp::Ordering, error::Error, fmt::Debug, fs::File, time::Duration}; -use crate::input::capability::Source; -use packed_struct::prelude::*; +use packed_struct::{prelude::*, types::bits::Bits}; use rand::Rng; use uhid_virt::{Bus, CreateParams, StreamError, UHIDDevice}; use crate::{ drivers::dualsense::{ - driver::{ - DS5_EDGE_NAME, DS5_EDGE_PID, DS5_EDGE_VERSION, DS5_EDGE_VID, DS5_NAME, DS5_PID, - DS5_TOUCHPAD_HEIGHT, DS5_TOUCHPAD_WIDTH, DS5_VERSION, DS5_VID, - FEATURE_REPORT_CALIBRATION, FEATURE_REPORT_FIRMWARE_INFO, FEATURE_REPORT_PAIRING_INFO, - OUTPUT_REPORT_BT, OUTPUT_REPORT_BT_SIZE, OUTPUT_REPORT_USB, - OUTPUT_REPORT_USB_SHORT_SIZE, OUTPUT_REPORT_USB_SIZE, STICK_X_MAX, STICK_X_MIN, - STICK_Y_MAX, STICK_Y_MIN, TRIGGER_MAX, - }, hid_report::{ Direction, PackedInputDataReport, USBPackedInputDataReport, UsbPackedOutputReport, UsbPackedOutputReportShort, @@ -26,11 +17,17 @@ use crate::{ report_descriptor::{ DS_BT_DESCRIPTOR, DS_EDGE_BT_DESCRIPTOR, DS_EDGE_USB_DESCRIPTOR, DS_USB_DESCRIPTOR, }, + DS5_EDGE_NAME, DS5_EDGE_PID, DS5_EDGE_VERSION, DS5_EDGE_VID, DS5_NAME, DS5_PID, + DS5_RADS_TO_GYRO, DS5_SI_TO_ACCEL, DS5_TOUCHPAD_HEIGHT, DS5_TOUCHPAD_WIDTH, DS5_VERSION, + DS5_VID, FEATURE_REPORT_CALIBRATION, FEATURE_REPORT_FIRMWARE_INFO, + FEATURE_REPORT_PAIRING_INFO, OUTPUT_REPORT_BT, OUTPUT_REPORT_BT_SIZE, OUTPUT_REPORT_USB, + OUTPUT_REPORT_USB_SHORT_SIZE, OUTPUT_REPORT_USB_SIZE, STICK_X_MAX, STICK_X_MIN, + STICK_Y_MAX, STICK_Y_MIN, TRIGGER_MAX, }, input::{ capability::{ - Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger, Touch, TouchButton, - Touchpad, + Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger, Source, Touch, + TouchButton, Touchpad, }, composite_device::client::CompositeDeviceClient, event::{ @@ -614,26 +611,26 @@ impl DualSenseDevice { Capability::Gyroscope(_) => { if let InputValue::Vector3 { x, y, z } = value { if let Some(x) = x { - state.pitch = Integer::from_primitive(x as i16); + state.pitch = denormalize_gyro_value(x); } if let Some(y) = y { - state.yaw = Integer::from_primitive(y as i16); + state.yaw = denormalize_gyro_value(y); } if let Some(z) = z { - state.roll = Integer::from_primitive(z as i16); + state.roll = denormalize_gyro_value(z); } } } Capability::Accelerometer(_) => { if let InputValue::Vector3 { x, y, z } = value { if let Some(x) = x { - state.accel_x = Integer::from_primitive(x as i16); + state.accel_x = denormalize_accel_value(x); } if let Some(y) = y { - state.accel_y = Integer::from_primitive(y as i16); + state.accel_y = denormalize_accel_value(y); } if let Some(z) = z { - state.accel_z = Integer::from_primitive(z as i16); + state.accel_z = denormalize_accel_value(z); } } } @@ -1162,3 +1159,20 @@ impl Debug for DualSenseDevice { .finish() } } + +/// De-normalizes the given value in meters per second into a real value that +/// the DS5 controller understands. +/// DualSense accelerometer values are measured in [DS5_ACC_RES_PER_G] +/// units of G acceleration (1G == 9.8m/s). InputPlumber accelerometer +/// values are measured in units of meters per second. To denormalize +/// the value, it needs to be converted into G units (by dividing by 9.8), +/// then multiplying that value by the [DS5_ACC_RES_PER_G]. +fn denormalize_accel_value(value_meters_sec: f64) -> Integer> { + Integer::from_primitive((value_meters_sec * DS5_SI_TO_ACCEL) as i16) +} + +/// DualSense gyro values are measured in units of degrees per second. +/// InputPlumber gyro values are measured in radians per second. +fn denormalize_gyro_value(value_degrees_sec: f64) -> Integer> { + Integer::from_primitive((value_degrees_sec * DS5_RADS_TO_GYRO) as i16) +} diff --git a/src/input/target/horipad_steam.rs b/src/input/target/horipad_steam.rs index 6bd97456..925cec48 100644 --- a/src/input/target/horipad_steam.rs +++ b/src/input/target/horipad_steam.rs @@ -1,23 +1,22 @@ //! Emulates a Horipad Steam Controller as a target input device. use std::{cmp::Ordering, error::Error, fmt::Debug, fs::File, time::Duration}; -use packed_struct::prelude::*; -use crate::input::capability::Source; +use packed_struct::{prelude::*, types::bits::Bits}; use uhid_virt::{Bus, CreateParams, StreamError, UHIDDevice}; use crate::{ drivers::horipad_steam::{ hid_report::{Direction, PackedInputDataReport}, report_descriptor::REPORT_DESCRIPTOR, - JOY_AXIS_MAX, JOY_AXIS_MIN, PIDS, TRIGGER_AXIS_MAX, VID, + HORIPAD_RADS_TO_GYRO, HORIPAD_SI_TO_ACCEL, JOY_AXIS_MAX, JOY_AXIS_MIN, PIDS, + TRIGGER_AXIS_MAX, VID, }, input::{ - capability::{Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger}, + capability::{Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger, Source}, composite_device::client::CompositeDeviceClient, event::{ native::{NativeEvent, ScheduledNativeEvent}, - value::InputValue, - value::{denormalize_signed_value_u8, denormalize_unsigned_value_u8}, + value::{denormalize_signed_value_u8, denormalize_unsigned_value_u8, InputValue}, }, output_capability::OutputCapability, output_event::OutputEvent, @@ -246,26 +245,26 @@ impl HoripadSteamDevice { Capability::Gyroscope(_) => { if let InputValue::Vector3 { x, y, z } = value { if let Some(x) = x { - self.state.pitch = Integer::from_primitive(denormalize_gyro_value(x)); + self.state.pitch = denormalize_gyro_value(x); } if let Some(y) = y { - self.state.yaw = Integer::from_primitive(denormalize_gyro_value(y)); + self.state.yaw = denormalize_gyro_value(y); } if let Some(z) = z { - self.state.roll = Integer::from_primitive(denormalize_gyro_value(z)); + self.state.roll = denormalize_gyro_value(z); } } } Capability::Accelerometer(_) => { if let InputValue::Vector3 { x, y, z } = value { if let Some(x) = x { - self.state.accel_x = Integer::from_primitive(denormalize_accel_value(x)); + self.state.accel_x = denormalize_accel_value(x); } if let Some(y) = y { - self.state.accel_y = Integer::from_primitive(denormalize_accel_value(y)); + self.state.accel_y = denormalize_accel_value(y); } if let Some(z) = z { - self.state.accel_z = Integer::from_primitive(denormalize_accel_value(z)); + self.state.accel_z = denormalize_accel_value(z); } } } @@ -506,16 +505,14 @@ impl Debug for HoripadSteamDevice { /// values are measured in units of meters per second. To denormalize /// the value, it needs to be converted into G units (by dividing by 9.8), /// then multiplying that value by the []. -fn denormalize_accel_value(value_meters_sec: f64) -> i16 { - let value = value_meters_sec; - value as i16 +fn denormalize_accel_value(value_meters_sec: f64) -> Integer> { + Integer::from_primitive((value_meters_sec * HORIPAD_SI_TO_ACCEL) as i16) } /// SDL negates all gyro axes when reading from this device (SDL_hidapi_steam_hori.c L329-331): /// imu_data[N] = -1.0f * LOAD16(data[...]) /// We invert here so that SDL produces the correct sign after its negation. /// https://github.com/libsdl-org/SDL/blob/main/src/joystick/hidapi/SDL_hidapi_steam_hori.c#L329-L331 -fn denormalize_gyro_value(value_degrees_sec: f64) -> i16 { - let value = -value_degrees_sec; - value as i16 +fn denormalize_gyro_value(value_degrees_sec: f64) -> Integer> { + Integer::from_primitive((value_degrees_sec * HORIPAD_RADS_TO_GYRO) as i16) } diff --git a/src/input/target/steam_deck.rs b/src/input/target/steam_deck.rs index a5db6615..9271b524 100644 --- a/src/input/target/steam_deck.rs +++ b/src/input/target/steam_deck.rs @@ -1,5 +1,5 @@ use packed_struct::{ - types::{Integer, SizedInteger}, + types::{bits::Bits, Integer, SizedInteger}, PackedStruct, }; use std::{ @@ -29,7 +29,7 @@ use crate::{ STICK_X_MAX, STICK_X_MIN, STICK_Y_MAX, STICK_Y_MIN, TRIGG_MAX, }, report_descriptor::{CONTROLLER_DESCRIPTOR, KEYBOARD_DESCRIPTOR, MOUSE_DESCRIPTOR}, - ProductId, VID, + ProductId, DECK_RADS_TO_GYRO, DECK_SI_TO_ACCEL, VID, }, input::{ capability::{ @@ -746,26 +746,26 @@ impl SteamDeckDevice { Capability::Gyroscope(_) => { if let InputValue::Vector3 { x, y, z } = value { if let Some(x) = x { - self.state.pitch = Integer::from_primitive(x as i16); + self.state.pitch = denormalize_gyro_value(x); } if let Some(y) = y { - self.state.yaw = Integer::from_primitive(y as i16); + self.state.yaw = denormalize_gyro_value(y); } if let Some(z) = z { - self.state.roll = Integer::from_primitive(z as i16); + self.state.roll = denormalize_gyro_value(z); } } } Capability::Accelerometer(_) => { if let InputValue::Vector3 { x, y, z } = value { if let Some(x) = x { - self.state.accel_x = Integer::from_primitive(x as i16); + self.state.accel_x = denormalize_accel_value(x); } if let Some(y) = y { - self.state.accel_y = Integer::from_primitive(y as i16); + self.state.accel_y = denormalize_accel_value(y); } if let Some(z) = z { - self.state.accel_z = Integer::from_primitive(z as i16); + self.state.accel_z = denormalize_accel_value(z); } } } @@ -989,3 +989,11 @@ pub fn denormalize_unsigned_to_signed_value(normal_value: f64, min: f64, max: f6 let normal_value = (normal_value * 2.0) - 1.0; denormalize_signed_value_i16(normal_value, min, max) } + +fn denormalize_accel_value(value_meters_sec: f64) -> Integer> { + Integer::from_primitive((value_meters_sec * DECK_SI_TO_ACCEL) as i16) +} + +fn denormalize_gyro_value(value_degrees_sec: f64) -> Integer> { + Integer::from_primitive((value_degrees_sec * DECK_RADS_TO_GYRO) as i16) +} diff --git a/src/input/target/steam_deck_uhid.rs b/src/input/target/steam_deck_uhid.rs index 71f3e3d8..14a5ef1c 100644 --- a/src/input/target/steam_deck_uhid.rs +++ b/src/input/target/steam_deck_uhid.rs @@ -8,7 +8,7 @@ use std::{ }; use packed_struct::{ - types::{Integer, SizedInteger}, + types::{bits::Bits, Integer, SizedInteger}, PackedStruct, }; use uhid_virt::{Bus, CreateParams, StreamError, UHIDDevice}; @@ -22,7 +22,7 @@ use crate::{ STICK_X_MAX, STICK_X_MIN, STICK_Y_MAX, STICK_Y_MIN, TRIGG_MAX, }, report_descriptor::CONTROLLER_DESCRIPTOR, - ProductId, VID, + ProductId, DECK_RADS_TO_GYRO, DECK_SI_TO_ACCEL, VID, }, input::{ capability::{ @@ -351,26 +351,26 @@ impl SteamDeckUhidDevice { Capability::Gyroscope(_) => { if let InputValue::Vector3 { x, y, z } = value { if let Some(x) = x { - self.state.pitch = Integer::from_primitive(x as i16); + self.state.pitch = denormalize_gyro_value(x); } if let Some(y) = y { - self.state.yaw = Integer::from_primitive(y as i16); + self.state.yaw = denormalize_gyro_value(y); } if let Some(z) = z { - self.state.roll = Integer::from_primitive(z as i16); + self.state.roll = denormalize_gyro_value(z); } } } Capability::Accelerometer(_) => { if let InputValue::Vector3 { x, y, z } = value { if let Some(x) = x { - self.state.accel_x = Integer::from_primitive(x as i16); + self.state.accel_x = denormalize_accel_value(x); } if let Some(y) = y { - self.state.accel_y = Integer::from_primitive(y as i16); + self.state.accel_y = denormalize_accel_value(y); } if let Some(z) = z { - self.state.accel_z = Integer::from_primitive(z as i16); + self.state.accel_z = denormalize_accel_value(z); } } } @@ -933,3 +933,11 @@ impl Debug for SteamDeckUhidDevice { .finish() } } + +fn denormalize_accel_value(value_meters_sec: f64) -> Integer> { + Integer::from_primitive((value_meters_sec * DECK_SI_TO_ACCEL) as i16) +} + +fn denormalize_gyro_value(value_degrees_sec: f64) -> Integer> { + Integer::from_primitive((value_degrees_sec * DECK_RADS_TO_GYRO) as i16) +} diff --git a/src/input/target/ulitmate_2.rs b/src/input/target/ulitmate_2.rs index e67f053d..e79cf015 100644 --- a/src/input/target/ulitmate_2.rs +++ b/src/input/target/ulitmate_2.rs @@ -6,23 +6,22 @@ use core::option::Option::None; use std::{cmp::Ordering, error::Error, fmt::Debug, fs::File, time::Duration}; -use packed_struct::prelude::*; use packed_struct::types::SizedInteger; +use packed_struct::{prelude::*, types::bits::Bits}; use uhid_virt::{Bus, CreateParams, StreamError, UHIDDevice}; use crate::{ drivers::ultimate_2::{ hid_report::{DPadDirection, PackedInputDataReport, PackedRumbleOutputReport}, report_descriptor::REPORT_DESCRIPTOR, - JOY_AXIS_MAX, PID, REPORT_ID_RUMBLE, TRIGGER_AXIS_MAX, VID, + ACCEL_SCALE, JOY_AXIS_MAX, PID, REPORT_ID_RUMBLE, TRIGGER_AXIS_MAX, VID, }, input::{ capability::{Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger, Source}, composite_device::client::CompositeDeviceClient, event::{ native::{NativeEvent, ScheduledNativeEvent}, - value::denormalize_unsigned_value_u8, - value::InputValue, + value::{denormalize_unsigned_value_u8, InputValue}, }, output_capability::OutputCapability, output_event::OutputEvent, @@ -31,6 +30,8 @@ use crate::{ use super::{InputError, OutputError, TargetInputDevice, TargetOutputDevice}; +const GRAVITY: f64 = 9.80665; + // The minimum interval between button events must wait between // each other for chords. const MIN_CHORD_TIME: Duration = Duration::from_millis(80); @@ -203,13 +204,13 @@ impl Ultimate2WirelessDevice { Capability::Accelerometer(_) => { if let InputValue::Vector3 { x, y, z } = value { if let Some(x) = x { - self.state.accel_x = Integer::from_primitive(x as i16); + self.state.accel_x = denormalize_accel_value(x); } if let Some(y) = y { - self.state.accel_y = Integer::from_primitive(y as i16); + self.state.accel_y = denormalize_accel_value(y); } if let Some(z) = z { - self.state.accel_z = Integer::from_primitive(z as i16); + self.state.accel_z = denormalize_accel_value(z); } } } @@ -434,3 +435,9 @@ impl Debug for Ultimate2WirelessDevice { .finish() } } + +// m/s² → raw i16 (4096 units = 1G) +fn denormalize_accel_value(value_meters_sec: f64) -> Integer> { + let g = value_meters_sec / GRAVITY; + Integer::from_primitive((g * ACCEL_SCALE).clamp(i16::MIN as f64, i16::MAX as f64) as i16) +} From 7fbac11e12bcdae7ab1fc37719f15b781fc1c373 Mon Sep 17 00:00:00 2001 From: "Derek J. Clark" Date: Wed, 16 Sep 2026 16:15:51 -0700 Subject: [PATCH 7/7] chore: Remove libiio as a dependency While at it, add hrtimer config --- Cargo.lock | 18 ------------------ Cargo.toml | 11 +++++++---- Dockerfile | 4 +--- Makefile | 3 +++ docs/install.md | 1 - pkg/archlinux/PKGBUILD | 2 +- pkg/rpm/inputplumber.spec | 2 -- .../modules-load.d/inputplumber-hrtimer.conf | 1 + 8 files changed, 13 insertions(+), 29 deletions(-) create mode 100644 rootfs/usr/lib/modules-load.d/inputplumber-hrtimer.conf diff --git a/Cargo.lock b/Cargo.lock index 7ee8341f..96938362 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1013,17 +1013,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "industrial-io" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348ab060d6e63ff76337f57d7646fc3bd38d7abc0ac8c98b7acedecf2262e90f" -dependencies = [ - "libiio-sys", - "nix 0.29.0", - "thiserror 2.0.17", -] - [[package]] name = "inotify" version = "0.11.0" @@ -1057,7 +1046,6 @@ dependencies = [ "futures", "glob-match", "hidapi", - "industrial-io", "inotify", "log", "mio", @@ -1226,12 +1214,6 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" -[[package]] -name = "libiio-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f643b99b21452dd9bb6cb5a517cfa8342e90c6e9427a2b6274d5331dfb9ad98" - [[package]] name = "libudev" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 10d55c34..a41e02ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,11 +28,10 @@ assets = [ auto-req = "no" [package.metadata.generate-rpm.requires] -libiio = "*" libevdev = "*" -# debian package needs pkg-config, libclang-dev, libiio-dev and libudev-dev to build -# while runtime dependencies are libiio0 and libudev1 +# debian package needs pkg-config, libclang-dev, and libudev-dev to build +# while runtime dependencies are libudev1 # libevdev2 and libevdev-dev are listed but not needed [package.metadata.deb] license-file = ["LICENSE", "4"] @@ -65,6 +64,11 @@ assets = [ "usr/share/polkit-1/rules.d/org.shadowblip.InputPlumber.rules", "644", ], + [ + "rootfs/usr/lib/modules-load.d/*", + "usr/lib/modules-load.d/", + "644", + ], [ "rootfs/usr/lib/systemd/system/*", "usr/lib/systemd/system/", @@ -116,7 +120,6 @@ evdev = { git = "https://github.com/emberian/evdev.git", features = [ futures = "0.3.31" glob-match = "0.2.1" hidapi = "2.6.4" -industrial-io = "0.6.1" #evdev = { version = "0.12.1", features = ["tokio"] } inotify = "0.11.0" # Omit trace logging for release builds diff --git a/Dockerfile b/Dockerfile index d1b09fc9..a5377c92 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,14 +5,12 @@ RUN apt-get update && apt-get install -y \ zstd \ libclang-dev \ libudev-dev \ - libiio-dev \ squashfs-tools RUN apt-get install -y \ g++-aarch64-linux-gnu \ libc6-dev-arm64-cross \ - libudev-dev:arm64 \ - libiio-dev:arm64 + libudev-dev:arm64 RUN rustup target add aarch64-unknown-linux-gnu RUN rustup toolchain install --force-non-host stable-aarch64-unknown-linux-gnu diff --git a/Makefile b/Makefile index 3b6a3104..81c7fc43 100644 --- a/Makefile +++ b/Makefile @@ -58,6 +58,8 @@ install: build ## Install inputplumber to the given prefix (default: PREFIX=/usr rootfs/usr/share/polkit-1/actions/* install -D -m 644 -t $(PREFIX)/share/polkit-1/rules.d/ \ rootfs/usr/share/polkit-1/rules.d/* + install -D -m 644 -t $(PREFIX)/lib/modules-load.d/ \ + rootfs/usr/lib/modules-load.d/* install -D -m 644 -t $(PREFIX)/lib/systemd/system/ \ rootfs/usr/lib/systemd/system/* install -D -m 644 -t $(PREFIX)/lib/udev/hwdb.d/ \ @@ -83,6 +85,7 @@ uninstall: ## Uninstall inputplumber rm $(PREFIX)/share/dbus-1/system.d/$(DBUS_NAME).conf rm $(PREFIX)/share/polkit-1/actions/$(DBUS_NAME).policy rm $(PREFIX)/share/polkit-1/rules.d/$(DBUS_NAME).rules + rm $(PREFIX)/lib/modules-load.d/inputplumber-hrtimer.conf rm $(PREFIX)/lib/systemd/system/$(NAME).service rm $(PREFIX)/lib/systemd/system/$(NAME)-suspend.service rm $(PREFIX)/lib/udev/hwdb.d/59-inputplumber.hwdb diff --git a/docs/install.md b/docs/install.md index bd1160e2..5bdf670f 100644 --- a/docs/install.md +++ b/docs/install.md @@ -85,7 +85,6 @@ InputPlumber requires the following system dependencies: - `libevdev` - - `libiio` - `rust` To install the package, first visit the [release page](https://github.com/ShadowBlip/InputPlumber/releases) and download diff --git a/pkg/archlinux/PKGBUILD b/pkg/archlinux/PKGBUILD index b2a8ac29..efb15439 100644 --- a/pkg/archlinux/PKGBUILD +++ b/pkg/archlinux/PKGBUILD @@ -7,7 +7,7 @@ pkgdesc="Open source input router and remapper daemon for Linux" arch=('x86_64') url="https://github.com/ShadowBlip/inputplumber" license=('GPL') -depends=('dbus' 'libevdev' 'libiio') +depends=('dbus' 'libevdev') optdepends=('polkit') provides=('inputplumber') conflicts=('inputplumber-git') diff --git a/pkg/rpm/inputplumber.spec b/pkg/rpm/inputplumber.spec index b653c040..9d4389f2 100644 --- a/pkg/rpm/inputplumber.spec +++ b/pkg/rpm/inputplumber.spec @@ -7,7 +7,6 @@ URL: https://github.com/ShadowBlip/InputPlumber Source0: %{url}/archive/refs/tags/v%{version}.tar.gz BuildRequires: libevdev-devel -BuildRequires: libiio-devel BuildRequires: git BuildRequires: make BuildRequires: cargo @@ -16,7 +15,6 @@ BuildRequires: llvm-devel BuildRequires: clang-devel BuildRequires: systemd-rpm-macros Requires: libevdev -Requires: libiio Requires: polkit Recommends: linuxconsoletools Provides: inputplumber diff --git a/rootfs/usr/lib/modules-load.d/inputplumber-hrtimer.conf b/rootfs/usr/lib/modules-load.d/inputplumber-hrtimer.conf new file mode 100644 index 00000000..a8009e05 --- /dev/null +++ b/rootfs/usr/lib/modules-load.d/inputplumber-hrtimer.conf @@ -0,0 +1 @@ +iio_trig_hrtimer