Skip to content

Commit

Permalink
feat(Steam Deck): Add rumble support to Steam Deck
Browse files Browse the repository at this point in the history
- Adds Steam Deck Target device rumble event report handling.
- Adds Steam Deck Source device handling of Steam Deck Rumble events.
- Adds Evdev Source device handling of Steam Deck Rumble Events.
- Adds dualsense Source device handling of Steam Deck Rumble Events.
- Adds Steam Deck Target Device haptic event report handling.
- Adds Steam Deck Source Device handling of Steam Deck Haptic events.
  • Loading branch information
pastaq committed Jan 16, 2025
1 parent 90d65b2 commit 6f323a8
Show file tree
Hide file tree
Showing 10 changed files with 291 additions and 72 deletions.
11 changes: 5 additions & 6 deletions src/drivers/steam_deck/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,12 @@ impl Driver {
/// Rumble the gamepad
pub fn haptic_rumble(
&mut self,
intensity: u16,
left_speed: u16,
right_speed: u16,
left_gain: u8,
right_gain: u8,
) -> Result<(), Box<dyn Error + Send + Sync>> {
let mut report = PackedRumbleReport::new();
report.intensity = Integer::from_primitive(intensity);
report.left_speed = Integer::from_primitive(left_speed);
report.right_speed = Integer::from_primitive(right_speed);
report.left_gain = left_gain;
report.right_gain = right_gain;

// Write the report to the device
let buf = report.pack()?;
Expand All @@ -90,6 +84,11 @@ impl Driver {
Ok(())
}

pub fn write(&self, buf: &[u8]) -> Result<(), Box<dyn Error + Send + Sync>> {
self.device.write(buf)?;
Ok(())
}

/// Set lizard mode, which will automatically try to emulate mouse/keyboard
/// if enabled.
pub fn set_lizard_mode(&self, enabled: bool) -> Result<(), Box<dyn Error + Send + Sync>> {
Expand Down
96 changes: 79 additions & 17 deletions src/drivers/steam_deck/hid_report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,12 +509,28 @@ impl Default for PackedInputDataReport {
}

#[derive(PrimitiveEnum_u8, Clone, Copy, PartialEq, Debug)]
pub enum Pad {
pub enum PadSide {
Left = 0,
Right = 1,
Both = 2,
}

#[derive(PrimitiveEnum_u8, Clone, Copy, PartialEq, Debug)]
pub enum Intensity {
Default = 0,
Short = 1,
Medium = 2,
Long = 3,
Insane = 4,
}

#[derive(PrimitiveEnum_u8, Clone, Copy, PartialEq, Debug)]
pub enum CommandType {
Off = 0,
Tick = 1,
Click = 2,
}

/*
* Send a haptic pulse to the trackpads
* Duration and interval are measured in microseconds, count is the number
Expand All @@ -529,7 +545,7 @@ pub struct PackedHapticPulseReport {
#[packed_field(bytes = "1")]
pub report_size: u8,
#[packed_field(bytes = "2", ty = "enum")]
pub side: Pad,
pub side: PadSide,
#[packed_field(bytes = "3..=4", endian = "lsb")]
pub amplitude: Integer<u16, packed_bits::Bits<16>>,
#[packed_field(bytes = "5..=6", endian = "lsb")]
Expand All @@ -543,7 +559,7 @@ impl PackedHapticPulseReport {
Self {
report_id: ReportType::TriggerHapticPulse as u8,
report_size: 9,
side: Pad::Both,
side: PadSide::Both,
amplitude: Integer::from_primitive(0),
period: Integer::from_primitive(0),
count: Integer::from_primitive(0),
Expand All @@ -558,36 +574,34 @@ impl Default for PackedHapticPulseReport {
}

#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)]
#[packed_struct(bit_numbering = "msb0")]
#[packed_struct(bit_numbering = "msb0", size_bytes = "64")]
pub struct PackedRumbleReport {
#[packed_field(bytes = "0")]
pub report_id: u8,
pub cmd_id: u8,
#[packed_field(bytes = "1")]
pub report_size: u8,
#[packed_field(bytes = "3..=4", endian = "lsb")]
pub intensity: Integer<u16, packed_bits::Bits<16>>,
#[packed_field(bytes = "2")]
pub unk_2: u8,
#[packed_field(bytes = "3", endian = "lsb")]
pub event_type: u8,
#[packed_field(bytes = "4", endian = "lsb")]
pub intensity: u8,
#[packed_field(bytes = "5..=6", endian = "lsb")]
pub left_speed: Integer<u16, packed_bits::Bits<16>>,
#[packed_field(bytes = "7..=8", endian = "lsb")]
pub right_speed: Integer<u16, packed_bits::Bits<16>>,
/// Max gain: 135
#[packed_field(bytes = "9")]
pub left_gain: u8,
/// Max gain: 135
#[packed_field(bytes = "10")]
pub right_gain: u8,
}

impl PackedRumbleReport {
pub fn new() -> Self {
Self {
report_id: ReportType::TriggerRumbleCommand as u8,
cmd_id: ReportType::TriggerRumbleCommand as u8,
report_size: 9,
intensity: Integer::from_primitive(1),
unk_2: 0,
event_type: 0,
intensity: 0,
left_speed: Integer::from_primitive(0),
right_speed: Integer::from_primitive(0),
left_gain: 130,
right_gain: 130,
}
}
}
Expand All @@ -598,6 +612,54 @@ impl Default for PackedRumbleReport {
}
}

#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)]
#[packed_struct(bit_numbering = "msb0", size_bytes = "64")]
pub struct PackedHapticReport {
#[packed_field(bytes = "0")]
pub cmd_id: u8,
#[packed_field(bytes = "1")]
pub report_size: u8,
#[packed_field(bytes = "2", ty = "enum")]
pub side: PadSide,
#[packed_field(bytes = "3", ty = "enum")]
pub cmd_type: CommandType,
#[packed_field(bytes = "4", ty = "enum")]
pub intensity: Intensity,
#[packed_field(bytes = "5")]
pub gain: i8,
#[packed_field(bytes = "6")]
pub unk_6: u8,
#[packed_field(bytes = "7")]
pub unk_7: u8,
#[packed_field(bytes = "8")]
pub unk_8: u8,
#[packed_field(bytes = "12")]
pub unk_12: u8,
}

impl PackedHapticReport {
pub fn new() -> Self {
Self {
cmd_id: ReportType::TriggerHapticCommand as u8,
report_size: 13,
side: PadSide::Left,
cmd_type: CommandType::Off,
intensity: Intensity::Default,
gain: 0,
unk_6: 95,
unk_7: 204,
unk_8: 3,
unk_12: 16,
}
}
}

impl Default for PackedHapticReport {
fn default() -> Self {
Self::new()
}
}

#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)]
#[packed_struct(bit_numbering = "msb0", size_bytes = "64")]
pub struct PackedMappingsReport {
Expand Down
6 changes: 0 additions & 6 deletions src/input/capability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,12 +441,8 @@ impl fmt::Display for GamepadButton {
GamepadButton::DPadUp => write!(f, "DPadUp"),
GamepadButton::East => write!(f, "East"),
GamepadButton::Guide => write!(f, "Guide"),
GamepadButton::QuickAccess => write!(f, "QuickAccess"),
GamepadButton::QuickAccess2 => write!(f, "QuickAccess2"),
GamepadButton::Keyboard => write!(f, "Keyboard"),
GamepadButton::LeftBumper => write!(f, "LeftBumper"),
GamepadButton::LeftTop => write!(f, "LeftTop"),
GamepadButton::LeftTrigger => write!(f, "LeftTrigger"),
GamepadButton::LeftPaddle1 => write!(f, "LeftPaddle1"),
GamepadButton::LeftPaddle2 => write!(f, "LeftPaddle2"),
GamepadButton::LeftPaddle3 => write!(f, "LeftPaddle3"),
Expand All @@ -459,8 +455,6 @@ impl fmt::Display for GamepadButton {
GamepadButton::QuickAccess => write!(f, "QuickAccess"),
GamepadButton::QuickAccess2 => write!(f, "QuickAccess2"),
GamepadButton::RightBumper => write!(f, "RightBumper"),
GamepadButton::RightTop => write!(f, "RightTop"),
GamepadButton::RightTrigger => write!(f, "RightTrigger"),
GamepadButton::RightPaddle1 => write!(f, "RightPaddle1"),
GamepadButton::RightPaddle2 => write!(f, "RightPaddle2"),
GamepadButton::RightPaddle3 => write!(f, "RightPaddle3"),
Expand Down
10 changes: 10 additions & 0 deletions src/input/output_capability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub enum OutputCapability {
ForceFeedback,
ForceFeedbackUpload,
ForceFeedbackErase,
Haptics(Haptic),
#[allow(clippy::upper_case_acronyms)]
LED(LED),
}
Expand All @@ -17,3 +18,12 @@ pub enum LED {
Brightness,
Color,
}

/// Haptic capabilities
#[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Haptic {
TrackpadLeft,
TrackpadRight,
TrackpadCenter,
}
17 changes: 15 additions & 2 deletions src/input/output_event/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ use std::sync::mpsc::Sender;

use ::evdev::{FFEffectData, InputEvent};

use crate::drivers::dualsense::hid_report::SetStatePackedOutputData;
use crate::drivers::{
dualsense::hid_report::SetStatePackedOutputData,
steam_deck::hid_report::{PackedHapticReport, PackedRumbleReport, PadSide},
};

use super::output_capability::OutputCapability;
use super::output_capability::{Haptic, OutputCapability};

/// Output events are events that flow from target devices back to source devices
#[derive(Debug, Clone)]
pub enum OutputEvent {
Evdev(InputEvent),
Uinput(UinputOutputEvent),
DualSense(SetStatePackedOutputData),
SteamDeckHaptics(PackedHapticReport),
SteamDeckRumble(PackedRumbleReport),
}

impl OutputEvent {
Expand Down Expand Up @@ -47,6 +52,14 @@ impl OutputEvent {
OutputCapability::NotImplemented
}
}
OutputEvent::SteamDeckHaptics(packed_haptic_report) => {
match packed_haptic_report.side {
PadSide::Left => OutputCapability::Haptics(Haptic::TrackpadLeft),
PadSide::Right => OutputCapability::Haptics(Haptic::TrackpadRight),
PadSide::Both => OutputCapability::NotImplemented,
}
}
OutputEvent::SteamDeckRumble(_) => OutputCapability::ForceFeedback,
}
}
}
Expand Down
79 changes: 78 additions & 1 deletion src/input/source/evdev/gamepad.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use evdev::{
FFTrigger, InputEvent,
};
use nix::fcntl::{FcntlArg, OFlag};
use packed_struct::types::SizedInteger;

use crate::drivers::steam_deck::hid_report::PackedRumbleReport;
use crate::{
drivers::dualsense::hid_report::SetStatePackedOutputData,
input::{
Expand All @@ -24,6 +26,7 @@ pub struct GamepadEventDevice {
axes_info: HashMap<AbsoluteAxisCode, AbsInfo>,
ff_effects: HashMap<i16, FFEffect>,
ff_effects_dualsense: Option<i16>,
ff_effects_deck: Option<i16>,
hat_state: HashMap<AbsoluteAxisCode, i32>,
}

Expand Down Expand Up @@ -54,6 +57,7 @@ impl GamepadEventDevice {
axes_info,
ff_effects: HashMap::new(),
ff_effects_dualsense: None,
ff_effects_deck: None,
hat_state: HashMap::new(),
})
}
Expand Down Expand Up @@ -172,6 +176,71 @@ impl GamepadEventDevice {

Ok(())
}

// Process Steam Deck FFB events.
fn process_deck_ff(&mut self, report: PackedRumbleReport) -> Result<(), Box<dyn Error>> {
// If no effect was uploaded to handle DualSense 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 left_speed = report.left_speed.to_primitive();
let right_speed = report.right_speed.to_primitive();

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

// 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: 0,
},
replay: FFReplay {
length: 60000,
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 @@ -312,12 +381,20 @@ impl SourceOutputDevice for GamepadEventDevice {
log::debug!("Received DualSense output report");
if report.use_rumble_not_haptics || report.enable_improved_rumble_emulation {
if let Err(e) = self.process_dualsense_ff(report) {
log::error!("Failed to process dualsense output report: {:?}", e);
log::error!("Failed to process dualsense output report: {e:?}");
}
}
Ok(())
}
OutputEvent::Uinput(_) => Ok(()),
OutputEvent::SteamDeckHaptics(_report) => Ok(()),
OutputEvent::SteamDeckRumble(report) => {
log::debug!("Received Steam Deck FFB Output Report");
if let Err(e) = self.process_deck_ff(report) {
log::error!("Failed to process Steam Deck Force Feedback Report: {e:?}")
}
Ok(())
}
}
}

Expand Down
Loading

0 comments on commit 6f323a8

Please sign in to comment.