Skip to content

Commit

Permalink
fix(Steam Haptics): Push haptic events to Go S hidraw and event devices.
Browse files Browse the repository at this point in the history
  • Loading branch information
pastaq committed Feb 8, 2025
1 parent 2746d28 commit e0bc071
Show file tree
Hide file tree
Showing 4 changed files with 356 additions and 22 deletions.
38 changes: 30 additions & 8 deletions src/drivers/legos/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ use std::{error::Error, ffi::CString};
use hidapi::HidDevice;
use packed_struct::{types::SizedInteger, PackedStruct};

use crate::drivers::legos::hid_report::ReportType;

use super::{
event::{
AxisEvent, BinaryInput, ButtonEvent, Event, InertialEvent, InertialInput, JoyAxisInput,
TriggerEvent, TriggerInput,
},
hid_report::{InertialInputDataReport, XInputDataReport},
hid_report::{
InertialInputDataReport, InputReportType, OutputReportType, RumbleOutputDataReport,
XInputDataReport,
},
};

// Hardware ID's
Expand Down Expand Up @@ -53,7 +54,6 @@ impl Driver {
if info.vendor_id() != VID || info.product_id() != PID {
return Err(format!("Device '{fmtpath}' is not a Legion Go S Controller").into());
}

Ok(Self {
device,
accel_state: None,
Expand Down Expand Up @@ -97,6 +97,28 @@ impl Driver {
Ok(events)
}

/// Writes the given output state to the gamepad. This can be used to change
/// the color of LEDs, activate rumble, etc.
pub fn write(&self, buf: &[u8]) -> Result<(), Box<dyn Error + Send + Sync>> {
let _bytes_written = self.device.write(buf)?;

Ok(())
}

pub fn haptic_rumble(
&self,
l_motor_speed: u8,
r_motor_speed: u8,
) -> Result<(), Box<dyn Error + Send + Sync>> {
let mut report = RumbleOutputDataReport::default();
report.l_motor_speed = l_motor_speed;
report.r_motor_speed = r_motor_speed;
log::debug!("Got rumble event: {report:?}");

let buf = report.pack()?;
self.write(&buf)
}

/// Unpacks the buffer into a [XinputDataReport] structure and updates
/// the internal xinput_state
fn handle_xinput_report(
Expand Down Expand Up @@ -307,23 +329,23 @@ impl Driver {
//log::debug!(" ---- End Report ----");

let report_type = match input_report.report_id {
1 => ReportType::AccelData,
2 => ReportType::GyroData,
1 => InputReportType::AccelData,
2 => InputReportType::GyroData,
_ => {
let report_id = input_report.report_id;
return Err(format!("Unknown report type: {report_id}").into());
}
};

match report_type {
ReportType::AccelData => {
InputReportType::AccelData => {
// Update the state
let old_state = self.update_accel_state(input_report);
// Translate the state into a stream of input events
let events = self.translate_accel_data(old_state);
Ok(events)
}
ReportType::GyroData => {
InputReportType::GyroData => {
// Update the state
let old_state = self.update_gyro_state(input_report);
// Translate the state into a stream of input events
Expand Down
60 changes: 56 additions & 4 deletions src/drivers/legos/hid_report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,32 @@
use packed_struct::prelude::*;

/// Different reports types
pub enum ReportType {
pub enum InputReportType {
AccelData = 0x01,
GyroData = 0x02,
}

impl ReportType {
impl InputReportType {
pub fn to_u8(&self) -> u8 {
match self {
ReportType::AccelData => ReportType::AccelData as u8,
ReportType::GyroData => ReportType::GyroData as u8,
InputReportType::AccelData => InputReportType::AccelData as u8,
InputReportType::GyroData => InputReportType::GyroData as u8,
}
}
}

pub enum OutputReportType {
RumbleData = 0x04,
}

impl OutputReportType {
pub fn to_u8(&self) -> u8 {
match self {
&OutputReportType::RumbleData => OutputReportType::RumbleData as u8,
}
}
}

//XInputData
#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)]
#[packed_struct(bit_numbering = "msb0", size_bytes = "32")]
Expand Down Expand Up @@ -145,3 +158,42 @@ pub struct InertialInputDataReport {
#[packed_field(bytes = "7..=8", endian = "lsb")]
pub z: Integer<i16, packed_bits::Bits<16>>,
}

#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)]
#[packed_struct(bit_numbering = "msb0", size_bytes = "9")]
pub struct RumbleOutputDataReport {
#[packed_field(bytes = "0")]
pub report_id: u8,
#[packed_field(bytes = "1")]
pub unk_1: u8,
#[packed_field(bytes = "2")]
pub unk_2: u8,
#[packed_field(bytes = "3")]
pub unk_3: u8,
#[packed_field(bytes = "4")]
pub l_motor_speed: u8,
#[packed_field(bytes = "5")]
pub r_motor_speed: u8,
#[packed_field(bytes = "6")]
pub work_mode: u8,
#[packed_field(bytes = "7")]
pub l_motor_feature: u8,
#[packed_field(bytes = "8")]
pub r_motor_feature: u8,
}

impl Default for RumbleOutputDataReport {
fn default() -> Self {
Self {
report_id: OutputReportType::RumbleData.to_u8(),
unk_1: 0x00,
unk_2: 0x08,
unk_3: 0x00,
l_motor_speed: 0x00,
r_motor_speed: 0x00,
work_mode: 0x00,
l_motor_feature: 0x00,
r_motor_feature: 0x00,
}
}
}
121 changes: 118 additions & 3 deletions src/input/source/evdev/gamepad.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ use evdev::{
};
use nix::fcntl::{FcntlArg, OFlag};
use packed_struct::types::SizedInteger;
use packed_struct::PrimitiveEnum;

use crate::drivers::steam_deck::hid_report::PackedRumbleReport;
use crate::drivers::steam_deck::hid_report::{
CommandType, PackedHapticReport, PackedRumbleReport, PadSide,
};
use crate::{
drivers::dualsense::hid_report::SetStatePackedOutputData,
input::{
Expand All @@ -27,6 +30,7 @@ pub struct GamepadEventDevice {
ff_effects: HashMap<i16, FFEffect>,
ff_effects_dualsense: Option<i16>,
ff_effects_deck: Option<i16>,
haptic_effects_deck: Option<i16>,
hat_state: HashMap<AbsoluteAxisCode, i32>,
}

Expand Down Expand Up @@ -58,6 +62,7 @@ impl GamepadEventDevice {
ff_effects: HashMap::new(),
ff_effects_dualsense: None,
ff_effects_deck: None,
haptic_effects_deck: None,
hat_state: HashMap::new(),
})
}
Expand Down Expand Up @@ -241,6 +246,104 @@ impl GamepadEventDevice {

Ok(())
}

// Process Steam Deck Haptic events.
fn process_haptic_ff(&mut self, report: PackedHapticReport) -> Result<(), Box<dyn Error>> {
// If no effect was uploaded to handle Steam Deck force feedback, upload one.
if self.ff_effects_deck.is_none() {
let effect_data = FFEffectData {
direction: 0,
trigger: FFTrigger {
button: 0,
interval: 0,
},
replay: FFReplay {
length: 50,
delay: 0,
},
kind: FFEffectKind::Rumble {
strong_magnitude: 0,
weak_magnitude: 0,
},
};
log::debug!("Uploading FF effect data");
let effect = self.device.upload_ff_effect(effect_data)?;
let id = effect.id() as i16;
self.ff_effects.insert(id, effect);
self.ff_effects_deck = Some(id);
}

let effect_id = self.ff_effects_deck.unwrap();
let effect = self.ff_effects.get_mut(&effect_id).unwrap();

let intensity = report.intensity.to_primitive() + 1;
let scaled_gain = (report.gain + 24) as u8 * intensity;
let normalized_gain = normalize_unsigned_value(scaled_gain as f64, 150.0);
let new_gain = normalized_gain * u16::MAX as f64;
let new_gain = new_gain as u16;

let left_speed = match report.side {
PadSide::Left => new_gain,
PadSide::Right => 0,
PadSide::Both => new_gain,
};

let left_speed = match report.cmd_type {
CommandType::Off => 0,
CommandType::Tick => left_speed,
CommandType::Click => left_speed,
};

let right_speed = match report.side {
PadSide::Left => 0,
PadSide::Right => new_gain,
PadSide::Both => new_gain,
};

let right_speed = match report.cmd_type {
CommandType::Off => 0,
CommandType::Tick => right_speed,
CommandType::Click => right_speed,
};

let length = match report.cmd_type {
CommandType::Off => 0,
CommandType::Tick => 50,
CommandType::Click => 150,
};

log::debug!("Got FF event data, Left Speed: {left_speed}, Right Speed: {right_speed}, Length: {length}");

// Stop playing the effect if values are set to zero
if left_speed == 0 && right_speed == 0 {
log::trace!("Stopping FF effect");
effect.stop()?;
return Ok(());
}

// Set the values of the effect and play it
let effect_data = FFEffectData {
direction: 0,
trigger: FFTrigger {
button: 0,
interval: 25,
},
replay: FFReplay {
length: length,
delay: 0,
},
kind: FFEffectKind::Rumble {
strong_magnitude: left_speed,
weak_magnitude: right_speed,
},
};
log::trace!("Updating effect data");
effect.update(effect_data)?;
log::trace!("Playing effect with data: {:?}", effect_data);
effect.play(1)?;

Ok(())
}
}

impl SourceInputDevice for GamepadEventDevice {
Expand Down Expand Up @@ -387,9 +490,15 @@ impl SourceOutputDevice for GamepadEventDevice {
Ok(())
}
OutputEvent::Uinput(_) => Ok(()),
OutputEvent::SteamDeckHaptics(_report) => Ok(()),
OutputEvent::SteamDeckHaptics(report) => {
log::debug!("Received Steam Deck Haptic Output Report");
if let Err(e) = self.process_haptic_ff(report) {
log::error!("Failed to process Steam Deck Haptic Output Report: {e:?}")
}
Ok(())
}
OutputEvent::SteamDeckRumble(report) => {
log::debug!("Received Steam Deck FFB Output Report");
log::debug!("Received Steam Deck Force Feedback Report");
if let Err(e) = self.process_deck_ff(report) {
log::error!("Failed to process Steam Deck Force Feedback Report: {e:?}")
}
Expand Down Expand Up @@ -457,3 +566,9 @@ impl Debug for GamepadEventDevice {
.finish()
}
}

// Returns a value between 0.0 and 1.0 based on the given value with its
// maximum.
fn normalize_unsigned_value(raw_value: f64, max: f64) -> f64 {
raw_value / max
}
Loading

0 comments on commit e0bc071

Please sign in to comment.