From 86aa9fa7ac728bb8862da0d9b8b9ff03e64375ca Mon Sep 17 00:00:00 2001 From: Luke Short Date: Tue, 18 Nov 2025 16:53:46 -0700 Subject: [PATCH] feat(ForceFeedback): add dbus interface for scale Control the sensitivity of force feedback. --- .../org.shadowblip.InputPlumber.policy | 10 +++ src/dbus/interface/force_feedback.rs | 36 +++++++++- src/input/composite_device/client.rs | 20 ++++++ src/input/composite_device/command.rs | 2 + src/input/composite_device/mod.rs | 33 ++++++++-- src/input/output_event/mod.rs | 65 ++++++++++++++++++- 6 files changed, 157 insertions(+), 9 deletions(-) diff --git a/rootfs/usr/share/polkit-1/actions/org.shadowblip.InputPlumber.policy b/rootfs/usr/share/polkit-1/actions/org.shadowblip.InputPlumber.policy index 60a0da8e..80032bc8 100644 --- a/rootfs/usr/share/polkit-1/actions/org.shadowblip.InputPlumber.policy +++ b/rootfs/usr/share/polkit-1/actions/org.shadowblip.InputPlumber.policy @@ -984,6 +984,16 @@ + + Set the scale for force feedback intensity + Authentication is required to set the rumble intensity + + no + auth_admin + auth_admin + + + Allow sending force feedback rumble events Authentication is required to send rumble events diff --git a/src/dbus/interface/force_feedback.rs b/src/dbus/interface/force_feedback.rs index f08ab4b7..287ba93c 100644 --- a/src/dbus/interface/force_feedback.rs +++ b/src/dbus/interface/force_feedback.rs @@ -19,6 +19,8 @@ pub trait ForceFeedbacker { &mut self, enabled: bool, ) -> impl Future>> + Send; + fn get_scale(&self) -> impl Future>> + Send; + fn set_scale(&mut self, scale: f64) -> impl Future>> + Send; fn rumble(&mut self, value: f64) -> impl Future>> + Send; fn stop(&mut self) -> impl Future>> + Send; } @@ -33,6 +35,15 @@ impl ForceFeedbacker for CompositeDeviceClient { Ok(()) } + async fn get_scale(&self) -> Result> { + Ok(self.get_ff_scale().await?) + } + + async fn set_scale(&mut self, scale: f64) -> Result<(), Box> { + self.set_ff_scale(scale).await?; + Ok(()) + } + async fn rumble(&mut self, value: f64) -> Result<(), Box> { let value = value.min(1.0); let value = value.max(0.0); @@ -99,8 +110,29 @@ where self.device .set_enabled(enabled) .await - .map_err(|err| fdo::Error::Failed(err.to_string()))?; - Ok(()) + .map_err(|err| fdo::Error::Failed(err.to_string())) + } + + /// Set the scale for force feedback intensity + #[zbus(property)] + async fn scale(&self) -> fdo::Result { + self.device + .get_scale() + .await + .map_err(|err| fdo::Error::Failed(err.to_string())) + } + #[zbus(property)] + async fn set_scale( + &mut self, + scale: f64, + #[zbus(connection)] conn: &Connection, + #[zbus(header)] hdr: Option>, + ) -> fdo::Result<()> { + check_polkit(conn, hdr, "org.shadowblip.Output.ForceFeedback.Scale").await?; + self.device + .set_scale(scale) + .await + .map_err(|err| fdo::Error::Failed(err.to_string())) } /// Send a simple rumble event diff --git a/src/input/composite_device/client.rs b/src/input/composite_device/client.rs index 16c655fb..055da2c9 100644 --- a/src/input/composite_device/client.rs +++ b/src/input/composite_device/client.rs @@ -585,4 +585,24 @@ impl CompositeDeviceClient { .await?; Ok(()) } + + /// Returns the intensity scale of force feedback where 1.0 is maximum intensity + /// and 0.0 is minimum intensity. + pub async fn get_ff_scale(&self) -> Result { + let (tx, rx) = channel(1); + self.send(CompositeCommand::GetForceFeedbackScale(tx)) + .await?; + if let Some(result) = Self::recv(rx).await { + return Ok(result); + } + Err(ClientError::ChannelClosed) + } + + /// Sets the force feedback intensity to the given value where 1.0 is maximum + /// intensity and 0.0 is minimum intensity. + pub async fn set_ff_scale(&self, scale: f64) -> Result<(), ClientError> { + self.send(CompositeCommand::SetForceFeedbackScale(scale)) + .await?; + Ok(()) + } } diff --git a/src/input/composite_device/command.rs b/src/input/composite_device/command.rs index a3267841..194fccb1 100644 --- a/src/input/composite_device/command.rs +++ b/src/input/composite_device/command.rs @@ -48,6 +48,8 @@ pub enum CompositeCommand { GetFilterableEvents(mpsc::Sender>>), GetForceFeedbackEnabled(mpsc::Sender), SetForceFeedbackEnabled(bool), + GetForceFeedbackScale(mpsc::Sender), + SetForceFeedbackScale(f64), SourceDeviceAdded(DeviceInfo), SourceDeviceRemoved(DeviceInfo), SourceDeviceStopped(DeviceInfo), diff --git a/src/input/composite_device/mod.rs b/src/input/composite_device/mod.rs index 7e2a8920..dea5e3fe 100644 --- a/src/input/composite_device/mod.rs +++ b/src/input/composite_device/mod.rs @@ -141,6 +141,8 @@ pub struct CompositeDevice { /// Whether or not force feedback output events should be routed to /// supported source devices. ff_enabled: bool, + /// Set the scale for the force feedback intensity. + ff_scale: f64, /// Set of available Force Feedback effect IDs that are not in use /// TODO: Just use the keys from ff_effect_id_source_map to determine next id ff_effect_ids: BTreeSet, @@ -207,6 +209,7 @@ impl CompositeDevice { source_devices_used: Vec::new(), targets: CompositeDeviceTargets::new(conn, dbus_path, tx.into(), manager), ff_enabled: true, + ff_scale: 1.0, ff_effect_ids: (0..64).collect(), ff_effect_id_source_map: HashMap::new(), intercept_activation_caps: vec![Capability::Gamepad(Gamepad::Button( @@ -509,6 +512,16 @@ impl CompositeDevice { log::info!("Setting force feedback enabled: {enabled:?}"); self.ff_enabled = enabled; } + CompositeCommand::GetForceFeedbackScale(sender) => { + if let Err(e) = sender.send(self.ff_scale).await { + log::error!("Failed to send force feedback scale status: {e}"); + } + } + CompositeCommand::SetForceFeedbackScale(scale) => { + let scale = scale.clamp(0.0, 1.0); + log::info!("Setting force feedback scale: {scale}"); + self.ff_scale = scale; + } CompositeCommand::Stop => { log::debug!( "Got STOP signal. Stopping CompositeDevice: {}", @@ -743,7 +756,7 @@ impl CompositeDevice { } /// Process a single output event from a target device. - async fn process_output_event(&mut self, event: OutputEvent) -> Result<(), Box> { + async fn process_output_event(&mut self, mut event: OutputEvent) -> Result<(), Box> { log::trace!("Received output event: {:?}", event); // Handle any output events that need to upload FF effect data @@ -833,11 +846,19 @@ impl CompositeDevice { return Ok(()); } - // If force feedback is disabled at the composite device level, don't - // forward any other FF events to source devices. - if !self.ff_enabled && event.is_force_feedback() { - log::trace!("Force feedback not enabled. Nothing to do."); - return Ok(()); + // Process force feedback events + if event.is_force_feedback() { + // If force feedback is disabled at the composite device level, don't + // forward any other FF events to source devices. + if !self.ff_enabled { + log::trace!("Force feedback not enabled. Nothing to do."); + return Ok(()); + } + + // Scale the force feedback event if needed + if self.ff_scale != 1.0 { + event.scale(self.ff_scale); + } } // Write the event to devices that are capable of handling it diff --git a/src/input/output_event/mod.rs b/src/input/output_event/mod.rs index 90146cfa..8548a76a 100644 --- a/src/input/output_event/mod.rs +++ b/src/input/output_event/mod.rs @@ -1,6 +1,7 @@ use std::sync::mpsc::Sender; -use ::evdev::{FFEffectData, InputEvent}; +use ::evdev::{FFEffectData, FFEffectKind, InputEvent}; +use packed_struct::types::{Integer, SizedInteger}; use crate::drivers::{ dualsense::hid_report::SetStatePackedOutputData, @@ -87,6 +88,68 @@ impl OutputEvent { OutputEvent::SteamDeckRumble(_) => true, } } + + /// Scale the intensity value of the output event. A value of 0.0 is the + /// minimum intensity. A value of 1.0 is the maximum intensity. + pub fn scale(&mut self, scale: f64) { + let scale = scale.clamp(0.0, 1.0); + match self { + Self::Evdev(_) => (), + Self::Uinput(event) => match event { + UinputOutputEvent::FFUpload(_, data, _) => match data.kind { + FFEffectKind::Damper => (), + FFEffectKind::Inertia => (), + FFEffectKind::Constant { + level: _, + envelope: _, + } => (), + FFEffectKind::Ramp { + start_level: _, + end_level: _, + envelope: _, + } => (), + FFEffectKind::Periodic { + waveform: _, + period: _, + magnitude: _, + offset: _, + phase: _, + envelope: _, + } => (), + FFEffectKind::Spring { condition: _ } => (), + FFEffectKind::Friction { condition: _ } => (), + FFEffectKind::Rumble { + ref mut strong_magnitude, + ref mut weak_magnitude, + } => { + let scaled_strong = *strong_magnitude as f64 * scale; + *strong_magnitude = scaled_strong as u16; + let scaled_weak = *weak_magnitude as f64 * scale; + *weak_magnitude = scaled_weak as u16; + } + }, + UinputOutputEvent::FFErase(_) => (), + }, + Self::DualSense(report) => { + if report.use_rumble_not_haptics { + let scaled_left = report.rumble_emulation_left as f64 * scale; + report.rumble_emulation_left = scaled_left as u8; + let scaled_right = report.rumble_emulation_right as f64 * scale; + report.rumble_emulation_right = scaled_right as u8; + } + } + Self::SteamDeckHaptics(data) => { + data.gain = (data.gain as f64 * scale) as i8; + } + Self::SteamDeckRumble(data) => { + data.intensity = (data.intensity as f64 * scale) as u8; + let scaled_left_speed = data.left_speed.to_primitive() as f64 * scale; + data.left_speed = Integer::from_primitive(scaled_left_speed as u16); + let scaled_right_speed = data.right_speed.to_primitive() as f64 * scale; + data.right_speed = Integer::from_primitive(scaled_right_speed as u16); + } + } + } } #[derive(Debug, Clone)]