Add simple force feedback test scenario in test-scenarios

This commit is contained in:
Johannes Leupolz 2026-03-31 21:21:26 +00:00
parent e4eb1d77da
commit 21bcff7fa3
6 changed files with 129 additions and 21 deletions

View file

@ -3,7 +3,7 @@
// Author: Johannes Leupolz <dev@leupolz.eu>
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<i32> {
};
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());

View file

@ -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| {

View file

@ -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| {

View file

@ -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| {

View file

@ -3,7 +3,8 @@
// Author: Johannes Leupolz <dev@leupolz.eu>
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<i16> {
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);
}
});
}
}

View file

@ -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(),