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 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" 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/dualsense/driver.rs b/src/drivers/dualsense/driver.rs index 06798d00..b7b51cfa 100644 --- a/src/drivers/dualsense/driver.rs +++ b/src/drivers/dualsense/driver.rs @@ -9,49 +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_ACC_RES_PER_G: u32 = 8192; -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, @@ -420,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/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/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, }))) } } 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/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/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/input/source/hidraw/dualsense.rs b/src/input/source/hidraw/dualsense.rs index 9abb5991..9427e10e 100644 --- a/src/input/source/hidraw/dualsense.rs +++ b/src/input/source/hidraw/dualsense.rs @@ -4,8 +4,12 @@ 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}; use crate::{ drivers::dualsense::{self, driver::Driver}, @@ -337,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( - Capability::Gamepad(Gamepad::Accelerometer), + 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( - Capability::Gamepad(Gamepad::Gyro), + 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), }, ), }, @@ -386,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); @@ -404,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); @@ -437,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)) } } @@ -449,7 +453,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 +480,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..e96ee1e0 100644 --- a/src/input/source/hidraw/flydigi_vader_4_pro.rs +++ b/src/input/source/hidraw/flydigi_vader_4_pro.rs @@ -6,11 +6,10 @@ use crate::{ event, }, input::{ - capability::{Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger}, + 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}, }, @@ -236,19 +235,19 @@ 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), - 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::Gamepad(Gamepad::Gyro), + 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), }, ), }, @@ -257,7 +256,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 +283,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..04f955f9 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,21 +224,21 @@ 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), - 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::Gamepad(Gamepad::Gyro), + 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), }, ), }, @@ -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..bc65650c 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,19 +64,19 @@ 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), - 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::Gamepad(Gamepad::Gyro), + 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), }, ), }, @@ -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..9514c5f2 100644 --- a/src/input/source/hidraw/steam_deck.rs +++ b/src/input/source/hidraw/steam_deck.rs @@ -13,21 +13,16 @@ 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::{ - Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger, Touch, TouchButton, - Touchpad, + Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger, Source, Touch, + TouchButton, Touchpad, }, 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( - Capability::Gamepad(Gamepad::Accelerometer), + 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( - Capability::Gamepad(Gamepad::Gyro), + 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), }, ), }, @@ -591,7 +586,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 +613,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.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 329c675f..2b2a8264 100644 --- a/src/input/source/iio/bmi_imu.rs +++ b/src/input/source/iio/bmi_imu.rs @@ -1,10 +1,10 @@ -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, Gamepad}, + capability::{Capability, Source}, event::{native::NativeEvent, value::InputValue}, source::{InputError, SourceInputDevice, SourceOutputDevice}, }, @@ -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()) } } @@ -100,7 +92,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), @@ -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::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), - 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) } @@ -127,6 +114,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/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 { diff --git a/src/input/target/dualsense.rs b/src/input/target/dualsense.rs index dbbc87ea..0782b835 100644 --- a/src/input/target/dualsense.rs +++ b/src/input/target/dualsense.rs @@ -4,20 +4,12 @@ //! https://github.com/NeroReflex/ROGueENEMY/ use std::{cmp::Ordering, error::Error, fmt::Debug, fs::File, time::Duration}; -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_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, - 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, @@ -25,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::{ @@ -556,32 +554,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 @@ -639,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); } } } @@ -979,7 +951,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 +977,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)), @@ -1195,15 +1167,12 @@ impl Debug for DualSenseDevice { /// 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 +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 also measured in degrees per second. -fn denormalize_gyro_value(value_degrees_sec: f64) -> i16 { - let value = value_degrees_sec; - value as i16 +/// 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 c94f8ffc..925cec48 100644 --- a/src/input/target/horipad_steam.rs +++ b/src/input/target/horipad_steam.rs @@ -1,22 +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 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, @@ -240,57 +240,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 = 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); } } } @@ -332,7 +306,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 +334,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), ]) } @@ -531,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 e4199eae..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,12 +29,12 @@ 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::{ - 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 { @@ -772,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); } } } @@ -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)), @@ -1015,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 786f2052..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,12 +22,12 @@ 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::{ - 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 { @@ -377,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); } } } @@ -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)), @@ -959,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 dc1ddb3b..e79cf015 100644 --- a/src/input/target/ulitmate_2.rs +++ b/src/input/target/ulitmate_2.rs @@ -6,8 +6,8 @@ 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::{ @@ -17,12 +17,11 @@ use crate::{ ACCEL_SCALE, 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}, - value::denormalize_unsigned_value_u8, - value::InputValue, + value::{denormalize_unsigned_value_u8, InputValue}, }, output_capability::OutputCapability, output_event::OutputEvent, @@ -186,38 +185,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()); - } - } - } - _ => (), }, @@ -237,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); } } } @@ -341,8 +308,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 +336,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), ]) } @@ -470,7 +437,7 @@ impl Debug for Ultimate2WirelessDevice { } // 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 +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) } 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(), 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);