diff --git a/vuinputd-tests/src/devices/device_base.rs b/vuinputd-tests/src/devices/device_base.rs index 314bda3..e239a54 100644 --- a/vuinputd-tests/src/devices/device_base.rs +++ b/vuinputd-tests/src/devices/device_base.rs @@ -3,7 +3,7 @@ // Author: Johannes Leupolz use crate::test_log::LoggedInputEvent; -use libc::{c_int, close, open, write, O_NONBLOCK, O_WRONLY}; +use libc::{c_int, close, open, write, O_NONBLOCK, O_RDWR, O_WRONLY}; use libc::{input_event, timespec, uinput_setup, CLOCK_MONOTONIC}; use std::ffi::{CStr, CString}; use std::fs::File; @@ -107,12 +107,20 @@ pub trait Device: Sized { } /// Setup the uinput device (calls ui_dev_setup and ui_get_sysname) - fn setup_device(&self, name: &str, vendor: u16, product: u16, bustype: u16) -> io::Result<()> { + fn setup_device( + &self, + name: &str, + vendor: u16, + product: u16, + bustype: u16, + ff_effects_max: u32, + ) -> io::Result<()> { unsafe { let mut usetup: uinput_setup = zeroed(); usetup.id.bustype = bustype; usetup.id.vendor = vendor; usetup.id.product = product; + usetup.ff_effects_max = ff_effects_max; let name_cstr = CString::new(name).unwrap(); let name_ptr = usetup.name.as_mut_ptr() as *mut c_char; @@ -237,7 +245,7 @@ pub fn open_uinput(device: Option<&str>) -> io::Result { }; let path = std::ffi::CString::new(device).unwrap(); - let fd = unsafe { open(path.as_ptr(), O_WRONLY | O_NONBLOCK) }; + let fd = unsafe { open(path.as_ptr(), O_RDWR | O_NONBLOCK) }; if fd < 0 { eprintln!("error opening uinput"); return Err(io::Error::last_os_error()); diff --git a/vuinputd-tests/src/devices/keyboard.rs b/vuinputd-tests/src/devices/keyboard.rs index 49b7c1d..2d6eb01 100644 --- a/vuinputd-tests/src/devices/keyboard.rs +++ b/vuinputd-tests/src/devices/keyboard.rs @@ -298,7 +298,7 @@ impl Device for KeyboardDevice { events: Vec::new(), }, }; - temp_device.setup_device(name, 0xbeef, 0xdead, BUS_USB)?; + temp_device.setup_device(name, 0xbeef, 0xdead, BUS_USB, 0)?; unsafe { ui_dev_create(fd).map_err(|e| { diff --git a/vuinputd-tests/src/devices/mouse.rs b/vuinputd-tests/src/devices/mouse.rs index 60a1ed4..b0bf082 100644 --- a/vuinputd-tests/src/devices/mouse.rs +++ b/vuinputd-tests/src/devices/mouse.rs @@ -67,7 +67,7 @@ impl Device for MouseDevice { events: Vec::new(), }, }; - temp_device.setup_device(name, 0xbeef, 0xdead, BUS_USB)?; + temp_device.setup_device(name, 0xbeef, 0xdead, BUS_USB, 0)?; unsafe { ui_dev_create(fd).map_err(|e| { diff --git a/vuinputd-tests/src/devices/ps4_gamepad.rs b/vuinputd-tests/src/devices/ps4_gamepad.rs index 11f02b0..bc5c03a 100644 --- a/vuinputd-tests/src/devices/ps4_gamepad.rs +++ b/vuinputd-tests/src/devices/ps4_gamepad.rs @@ -133,7 +133,7 @@ impl Device for Ps4GamepadDevice { events: Vec::new(), }, }; - temp_device.setup_device(name, 0xbeef, 0xdead, BUS_USB)?; + temp_device.setup_device(name, 0xbeef, 0xdead, BUS_USB, 10)?; unsafe { ui_dev_create(fd).map_err(|e| { diff --git a/vuinputd-tests/src/devices/xbox_gamepad.rs b/vuinputd-tests/src/devices/xbox_gamepad.rs index 19ba583..69d3a23 100644 --- a/vuinputd-tests/src/devices/xbox_gamepad.rs +++ b/vuinputd-tests/src/devices/xbox_gamepad.rs @@ -3,7 +3,8 @@ // Author: Johannes Leupolz use crate::devices::device_base::{fetch_device_node, open_uinput, Device, DeviceState, BUS_USB}; -use libc::{c_int, close, open}; +use libc::{c_int, close, ff_effect, input_event, open, uinput_ff_upload}; +use nix::{ioctl_write_int, ioctl_write_ptr}; use std::io; use uinput_ioctls::*; @@ -42,6 +43,15 @@ pub const FF_RAMP: u16 = 0x57; pub const FF_SINE: u16 = 0x5a; pub const FF_GAIN: u16 = 0x60; +const EV_UINPUT: u16 = 0x0101; +const UI_FF_UPLOAD: u16 = 1; +const UI_FF_ERASE: u16 = 2; + +// EVIOCSFF ioctl command for Force Feedback Upload +ioctl_write_ptr!(eviocsff, b'E', 0x80, ff_effect); +// EVIOCRMFF ioctl command for Force Feedback erase +ioctl_write_int!(eviocrmff, b'E', 0x81); + /// Setup Xbox gamepad device /// https://github.com/LizardByte/Sunshine/blob/master/src/platform/linux/input/inputtino_gamepad.cpp /// https://github.com/games-on-whales/inputtino/blob/stable/src/uinput/joypad_xbox.cpp @@ -161,6 +171,36 @@ unsafe fn setup_xbox_gamepad(fd: c_int) -> io::Result<()> { Ok(()) } +/// Generates the opaque `u` array for a rumble effect on 64-bit systems +#[cfg(target_pointer_width = "64")] +pub fn create_rumble_array(strong_magnitude: u16, weak_magnitude: u16) -> [u64; 4] { + let mut u = [0u64; 4]; + + // Create an 8-byte array representing the memory of a u64 + let mut bytes = [0u8; 8]; + + // Place the strong magnitude at offset 0 and weak at offset 2 + // using native endianness to match exactly what the kernel expects. + bytes[0..2].copy_from_slice(&strong_magnitude.to_ne_bytes()); + bytes[2..4].copy_from_slice(&weak_magnitude.to_ne_bytes()); + + // Convert those bytes back into a native u64 and place it in the union array + u[0] = u64::from_ne_bytes(bytes); + + u +} + +/// Upload a force feedback effect to the device +/// Returns the effect id on success +pub fn upload_effect(fd: c_int, effect: *mut ff_effect) -> io::Result { + unsafe { + eviocsff(fd, effect).unwrap(); + } + // Effect id is saved as effect.id + let id = unsafe { (*effect).id }; + Ok(id) +} + pub struct XboxGamepadDevice { state: DeviceState, } @@ -197,7 +237,7 @@ impl Device for XboxGamepadDevice { events: Vec::new(), }, }; - temp_device.setup_device(name, 0xbeef, 0xdead, BUS_USB)?; + temp_device.setup_device(name, 0xbeef, 0xdead, BUS_USB, 10)?; unsafe { ui_dev_create(fd).map_err(|e| { @@ -242,3 +282,50 @@ impl Device for XboxGamepadDevice { } } } + +impl XboxGamepadDevice { + pub fn read_process_ff_event_from_uinput(&self) { + // Copy the i32 file descriptor so we can move it into the thread safely + let fd = self.state().uinput_fd; + + std::thread::spawn(move || { + // Buffer for the raw bytes + let mut buffer = [0u8; 256]; + + // Calling C functions always requires an unsafe block + let result = + unsafe { libc::read(fd, buffer.as_mut_ptr() as *mut libc::c_void, buffer.len()) }; + + // libc::read returns an isize (ssize_t in C) + if result < 0 { + // result < 0 means an error occurred. We use std::io::Error::last_os_error() + // to get the correct OS error message based on the C `errno`. + eprintln!( + "Error reading in thread: {}", + std::io::Error::last_os_error() + ); + return; + } else if result == 0 { + // 0 bytes usually means End-Of-File (EOF) or that the device was closed + println!("0 bytes (EOF) - Terminating thread"); + return; + } else if result == 24 { + println!("read_process_ff_event_from_uinput: processing input event (read)"); + let input_event = buffer.as_ptr() as *const libc::input_event; + let input_event = unsafe { *input_event }; + if input_event.type_ == EV_UINPUT && input_event.code == UI_FF_UPLOAD { + let mut upload: uinput_ff_upload = unsafe { std::mem::zeroed() }; + upload.request_id = input_event.value.try_into().unwrap(); + unsafe { + let ptr = &mut upload as *mut uinput_ff_upload; + ui_begin_ff_upload(fd, ptr).unwrap(); + println!("effect type: {}", upload.effect.type_); + ui_end_ff_upload(fd, ptr).unwrap(); + }; + } + } else { + println!("Read {} bytes", result); + } + }); + } +} diff --git a/vuinputd-tests/src/scenarios/ff_xbox_gamepad.rs b/vuinputd-tests/src/scenarios/ff_xbox_gamepad.rs index de255ad..91ab78b 100644 --- a/vuinputd-tests/src/scenarios/ff_xbox_gamepad.rs +++ b/vuinputd-tests/src/scenarios/ff_xbox_gamepad.rs @@ -5,12 +5,11 @@ use std::thread; use std::time::Duration; -use crate::devices::xbox_gamepad::XboxGamepadDevice; +use crate::devices::xbox_gamepad::{self, upload_effect, XboxGamepadDevice, FF_RUMBLE}; use crate::devices::{Device, EV_FF}; use crate::scenarios::ScenarioArgs; use crate::test_log::{LoggedInputEvent, TestLog}; - -const BTN_A: u16 = 304; +use libc::{self, ff_effect, ff_replay, ff_trigger}; pub struct FfXboxGamepad; @@ -26,18 +25,32 @@ impl FfXboxGamepad { thread::sleep(Duration::from_secs(1)); - let effect = libc::ff_effect { - type_: todo!(), - id: todo!(), - direction: todo!(), - trigger: todo!(), - replay: todo!(), - u: todo!(), - }; + eprintln!("upload a simple RUMBLE effect"); + let mut effect: ff_effect = unsafe { std::mem::zeroed() }; + effect.type_ = FF_RUMBLE; + effect.id = -1; // new effect + effect.direction = 0; + effect.trigger.button = 0; + effect.trigger.interval = 0; + effect.replay.length = 5000; + effect.replay.delay = 1000; + effect.u = xbox_gamepad::create_rumble_array(0x8000, 0x0); - let _ev_play_effect = gamepad.emit_read_and_log(EV_FF, effect.id.try_into().unwrap(), 3)?; + // ensure uploaded effect gets processed + gamepad.read_process_ff_event_from_uinput(); + // Upload effect via ioctl + let effect_id = upload_effect(gamepad.state().event_device_fd, &mut effect)?; + + eprintln!("Uploaded effect with id: {} {}", effect_id, effect.id); + thread::sleep(Duration::from_secs(1)); + + // Play effect (value=1) + let _play_effect_event = + gamepad.emit_read_and_log(EV_FF, effect_id.try_into().unwrap(), 1)?; + thread::sleep(Duration::from_secs(1)); + let _stop_effect_event = + gamepad.emit_read_and_log(EV_FF, effect_id.try_into().unwrap(), 0)?; thread::sleep(Duration::from_secs(1)); - let _ev_stop_effect = gamepad.emit_read_and_log(EV_FF, effect.id.try_into().unwrap(), 0)?; let eventlog = TestLog { events: gamepad.event_log().to_vec(),