Skip to content

Commit 2b17271

Browse files
committed
feat(Force Feedback): add interface to toggle force feedback on/off
1 parent 80d495a commit 2b17271

File tree

6 files changed

+184
-12
lines changed

6 files changed

+184
-12
lines changed

rootfs/usr/share/polkit-1/actions/org.shadowblip.InputPlumber.policy

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,16 @@
823823
</defaults>
824824
</action>
825825

826+
<action id="org.shadowblip.Output.ForceFeedback.Enable">
827+
<description>Allow toggling support for rumble events</description>
828+
<message>Authentication is required to toggle rumble support</message>
829+
<defaults>
830+
<allow_any>no</allow_any>
831+
<allow_inactive>no</allow_inactive>
832+
<allow_active>auth_admin</allow_active>
833+
</defaults>
834+
</action>
835+
826836
<action id="org.shadowblip.Output.ForceFeedback.Rumble">
827837
<description>Allow sending force feedback rumble events</description>
828838
<message>Authentication is required to send rumble events</message>

src/dbus/interface/force_feedback.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,25 @@ use super::Unregisterable;
1414

1515
/// [ForceFeedbacker] is any device that can implement force feedback
1616
pub trait ForceFeedbacker {
17+
fn get_enabled(&self) -> impl Future<Output = Result<bool, Box<dyn Error>>> + Send;
18+
fn set_enabled(
19+
&mut self,
20+
enabled: bool,
21+
) -> impl Future<Output = Result<(), Box<dyn Error>>> + Send;
1722
fn rumble(&mut self, value: f64) -> impl Future<Output = Result<(), Box<dyn Error>>> + Send;
1823
fn stop(&mut self) -> impl Future<Output = Result<(), Box<dyn Error>>> + Send;
1924
}
2025

2126
impl ForceFeedbacker for CompositeDeviceClient {
27+
async fn get_enabled(&self) -> Result<bool, Box<dyn Error>> {
28+
Ok(self.get_ff_enabled().await?)
29+
}
30+
31+
async fn set_enabled(&mut self, enabled: bool) -> Result<(), Box<dyn Error>> {
32+
self.set_ff_enabled(enabled).await?;
33+
Ok(())
34+
}
35+
2236
async fn rumble(&mut self, value: f64) -> Result<(), Box<dyn Error>> {
2337
let value = value.min(1.0);
2438
let value = value.max(0.0);
@@ -66,6 +80,28 @@ impl<T> ForceFeedbackInterface<T>
6680
where
6781
T: ForceFeedbacker + Send + Sync + 'static,
6882
{
83+
/// Whether or not the device should send force feedback events
84+
#[zbus(property)]
85+
async fn enabled(&self) -> fdo::Result<bool> {
86+
self.device
87+
.get_enabled()
88+
.await
89+
.map_err(|err| fdo::Error::Failed(err.to_string()))
90+
}
91+
#[zbus(property)]
92+
async fn set_enabled(
93+
&mut self,
94+
enabled: bool,
95+
#[zbus(header)] hdr: Option<Header<'_>>,
96+
) -> fdo::Result<()> {
97+
check_polkit(hdr, "org.shadowblip.Output.ForceFeedback.Enable").await?;
98+
self.device
99+
.set_enabled(enabled)
100+
.await
101+
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
102+
Ok(())
103+
}
104+
69105
/// Send a simple rumble event
70106
async fn rumble(&mut self, value: f64, #[zbus(header)] hdr: Header<'_>) -> fdo::Result<()> {
71107
check_polkit(Some(hdr), "org.shadowblip.Output.ForceFeedback.Rumble").await?;

src/input/composite_device/client.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,4 +523,22 @@ impl CompositeDeviceClient {
523523
}
524524
Err(ClientError::ChannelClosed)
525525
}
526+
527+
/// Returns whether or not the device should emit force feedback events
528+
pub async fn get_ff_enabled(&self) -> Result<bool, ClientError> {
529+
let (tx, rx) = channel(1);
530+
self.send(CompositeCommand::GetForceFeedbackEnabled(tx))
531+
.await?;
532+
if let Some(result) = Self::recv(rx).await {
533+
return Ok(result);
534+
}
535+
Err(ClientError::ChannelClosed)
536+
}
537+
538+
/// Enable or disable force feedback output events from being emitted
539+
pub async fn set_ff_enabled(&self, enabled: bool) -> Result<(), ClientError> {
540+
self.send(CompositeCommand::SetForceFeedbackEnabled(enabled))
541+
.await?;
542+
Ok(())
543+
}
526544
}

src/input/composite_device/command.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ pub enum CompositeCommand {
4242
SetInterceptActivation(Vec<Capability>, Capability),
4343
SetInterceptMode(InterceptMode),
4444
SetTargetDevices(Vec<TargetDeviceTypeId>),
45+
GetForceFeedbackEnabled(mpsc::Sender<bool>),
46+
SetForceFeedbackEnabled(bool),
4547
SourceDeviceAdded(DeviceInfo),
4648
SourceDeviceRemoved(DeviceInfo),
4749
SourceDeviceStopped(DeviceInfo),

src/input/composite_device/mod.rs

Lines changed: 104 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@ use std::{
1414
use evdev::InputEvent;
1515
use targets::CompositeDeviceTargets;
1616
use tokio::{sync::mpsc, task::JoinSet, time::Duration};
17-
use zbus::Connection;
17+
use zbus::{object_server::Interface, Connection};
1818

1919
use crate::{
2020
config::{
2121
capability_map::CapabilityMapConfig, path::get_profiles_path, CompositeDeviceConfig,
2222
DeviceProfile, ProfileMapping,
2323
},
24-
dbus::interface::{composite_device::CompositeDeviceInterface, DBusInterfaceManager},
24+
dbus::interface::{
25+
composite_device::CompositeDeviceInterface, force_feedback::ForceFeedbackInterface,
26+
DBusInterfaceManager,
27+
},
2528
input::{
2629
capability::{Capability, Gamepad, GamepadButton, Mouse},
2730
event::{
@@ -77,8 +80,12 @@ pub struct CompositeDevice {
7780
name: String,
7881
/// Capabilities describe all input capabilities from all source devices
7982
capabilities: HashSet<Capability>,
83+
/// Capabilities sorted by source device id
84+
capabilities_by_source: HashMap<String, HashSet<Capability>>,
8085
/// Output capabilities describe all output capabilities from all source devices
8186
output_capabilities: HashSet<OutputCapability>,
87+
/// Output capabilities sorted by source device id.
88+
output_capabilities_by_source: HashMap<String, HashSet<OutputCapability>>,
8289
/// Capability mapping for the CompositeDevice
8390
capability_map: Option<CapabilityMapConfig>,
8491
/// Currently loaded [DeviceProfile] for the [CompositeDevice]. The [DeviceProfile]
@@ -127,6 +134,9 @@ pub struct CompositeDevice {
127134
source_devices_used: Vec<String>,
128135
/// State of target devices attached to the composite device
129136
targets: CompositeDeviceTargets,
137+
/// Whether or not force feedback output events should be routed to
138+
/// supported source devices.
139+
ff_enabled: bool,
130140
/// Set of available Force Feedback effect IDs that are not in use
131141
/// TODO: Just use the keys from ff_effect_id_source_map to determine next id
132142
ff_effect_ids: BTreeSet<i16>,
@@ -169,7 +179,9 @@ impl CompositeDevice {
169179
config,
170180
name,
171181
capabilities: HashSet::new(),
182+
capabilities_by_source: HashMap::new(),
172183
output_capabilities: HashSet::new(),
184+
output_capabilities_by_source: HashMap::new(),
173185
capability_map,
174186
device_profile: None,
175187
device_profile_path: None,
@@ -189,6 +201,7 @@ impl CompositeDevice {
189201
source_device_tasks: JoinSet::new(),
190202
source_devices_used: Vec::new(),
191203
targets: CompositeDeviceTargets::new(conn, dbus_path, tx.into(), manager),
204+
ff_enabled: true,
192205
ff_effect_ids: (0..64).collect(),
193206
ff_effect_id_source_map: HashMap::new(),
194207
intercept_activation_caps: vec![Capability::Gamepad(Gamepad::Button(
@@ -484,6 +497,15 @@ impl CompositeDevice {
484497
CompositeCommand::SetInterceptActivation(activation_caps, target_cap) => {
485498
self.set_intercept_activation(activation_caps, target_cap)
486499
}
500+
CompositeCommand::GetForceFeedbackEnabled(sender) => {
501+
if let Err(e) = sender.send(self.ff_enabled).await {
502+
log::error!("Failed to send force feedback status: {e}");
503+
}
504+
}
505+
CompositeCommand::SetForceFeedbackEnabled(enabled) => {
506+
log::info!("Setting force feedback enabled: {enabled:?}");
507+
self.ff_enabled = enabled;
508+
}
487509
CompositeCommand::Stop => {
488510
log::debug!(
489511
"Got STOP signal. Stopping CompositeDevice: {}",
@@ -785,6 +807,12 @@ impl CompositeDevice {
785807
return Ok(());
786808
}
787809

810+
// If force feedback is disabled at the composite device level, don't
811+
// forward any other FF events to source devices.
812+
if !self.ff_enabled && event.is_force_feedback() {
813+
return Ok(());
814+
}
815+
788816
// TODO: Only write the event to devices that are capabile of handling it
789817
for (source_id, source) in self.source_devices.iter() {
790818
// If this is a force feedback event, translate the effect id into
@@ -1513,6 +1541,51 @@ impl CompositeDevice {
15131541
self.ff_effect_ids.insert(effect_id);
15141542
}
15151543

1544+
// Remove tracked input capabilities
1545+
self.capabilities_by_source.remove(&id);
1546+
let mut capabilities_to_remove = vec![];
1547+
for capability in self.capabilities.iter() {
1548+
// Check if any surviving source devices use this capability
1549+
let capability_in_use = self
1550+
.capabilities_by_source
1551+
.iter()
1552+
.any(|(_, capabilities)| capabilities.contains(capability));
1553+
if capability_in_use {
1554+
continue;
1555+
}
1556+
capabilities_to_remove.push(capability.clone());
1557+
}
1558+
for capability in capabilities_to_remove {
1559+
self.capabilities.remove(&capability);
1560+
}
1561+
1562+
// Remove tracked output capabilities
1563+
self.output_capabilities_by_source.remove(&id);
1564+
let mut capabilities_to_remove = vec![];
1565+
for capability in self.output_capabilities.iter() {
1566+
// Check if any surviving source devices use this capability
1567+
let capability_in_use = self
1568+
.output_capabilities_by_source
1569+
.iter()
1570+
.any(|(_, capabilities)| capabilities.contains(capability));
1571+
if capability_in_use {
1572+
continue;
1573+
}
1574+
capabilities_to_remove.push(capability.clone());
1575+
}
1576+
for capability in capabilities_to_remove {
1577+
self.output_capabilities.remove(&capability);
1578+
}
1579+
1580+
// Remove any interfaces that are no longer required
1581+
let ff_iface_name = ForceFeedbackInterface::<CompositeDeviceClient>::name();
1582+
let supports_ff = self
1583+
.output_capabilities
1584+
.contains(&OutputCapability::ForceFeedback);
1585+
if !supports_ff && self.dbus.has_interface(&ff_iface_name) {
1586+
self.dbus.unregister(&ff_iface_name);
1587+
}
1588+
15161589
if let Some(idx) = self.source_device_paths.iter().position(|str| str == &path) {
15171590
self.source_device_paths.remove(idx);
15181591
};
@@ -1610,25 +1683,44 @@ impl CompositeDevice {
16101683
};
16111684

16121685
// Get the capabilities of the source device.
1613-
// TODO: When we *remove* a source device, we also need to remove
1614-
// capabilities
1686+
let id = source_device.get_id();
16151687
if !is_blocked {
1616-
let capabilities = source_device.get_capabilities()?;
1617-
for cap in capabilities {
1618-
if self.translatable_capabilities.contains(&cap) {
1688+
// Get the input capabilities of the source device and keep track
1689+
// of them.
1690+
let capabilities: HashSet<Capability> =
1691+
source_device.get_capabilities()?.into_iter().collect();
1692+
for cap in capabilities.iter() {
1693+
if self.translatable_capabilities.contains(cap) {
16191694
continue;
16201695
}
1621-
self.capabilities.insert(cap);
1696+
self.capabilities.insert(cap.clone());
16221697
}
1698+
self.capabilities_by_source.insert(id.clone(), capabilities);
16231699

1624-
let output_capabilities = source_device.get_output_capabilities()?;
1625-
for cap in output_capabilities {
1626-
self.output_capabilities.insert(cap);
1700+
// Get the output capabilities of the source device and keep track
1701+
// of them.
1702+
let output_capabilities: HashSet<OutputCapability> = source_device
1703+
.get_output_capabilities()?
1704+
.into_iter()
1705+
.collect();
1706+
for cap in output_capabilities.iter() {
1707+
self.output_capabilities.insert(cap.clone());
1708+
}
1709+
self.output_capabilities_by_source
1710+
.insert(id.clone(), output_capabilities);
1711+
1712+
// Determine if the FF dbus interface should be created
1713+
let supports_ff = self
1714+
.output_capabilities
1715+
.contains(&OutputCapability::ForceFeedback);
1716+
let ff_iface_name = ForceFeedbackInterface::<CompositeDeviceClient>::name();
1717+
if supports_ff && !self.dbus.has_interface(&ff_iface_name) {
1718+
let iface = ForceFeedbackInterface::new(self.client());
1719+
self.dbus.register(iface);
16271720
}
16281721
}
16291722

16301723
// Check if this device should be blocked from sending events to target devices.
1631-
let id = source_device.get_id();
16321724
if let Some(device_config) = self
16331725
.config
16341726
.get_matching_device(&source_device.get_device_ref().to_owned())

src/input/output_event/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,20 @@ impl OutputEvent {
7474
OutputEvent::SteamDeckRumble(_) => vec![OutputCapability::ForceFeedback],
7575
}
7676
}
77+
78+
/// Returns true if the output event is a force feedback/rumble event
79+
pub fn is_force_feedback(&self) -> bool {
80+
match self {
81+
OutputEvent::Evdev(event) => matches!(
82+
event.destructure(),
83+
evdev::EventSummary::ForceFeedback(_, _, _)
84+
),
85+
OutputEvent::Uinput(_) => true,
86+
OutputEvent::DualSense(report) => report.use_rumble_not_haptics,
87+
OutputEvent::SteamDeckHaptics(_) => true,
88+
OutputEvent::SteamDeckRumble(_) => true,
89+
}
90+
}
7791
}
7892

7993
#[derive(Debug, Clone)]

0 commit comments

Comments
 (0)