Skip to content

Commit 58e3927

Browse files
committed
feat(ForceFeedback): add dbus interface for rumble
1 parent e8ad42e commit 58e3927

File tree

22 files changed

+575
-37
lines changed

22 files changed

+575
-37
lines changed

src/dbus/interface/composite_device.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,21 @@ impl CompositeDeviceInterface {
297297
Ok(capability_strings)
298298
}
299299

300+
#[zbus(property)]
301+
async fn output_capabilities(&self) -> fdo::Result<Vec<String>> {
302+
let capabilities = self
303+
.composite_device
304+
.get_output_capabilities()
305+
.await
306+
.map_err(|e| fdo::Error::Failed(e.to_string()))?;
307+
let capability_strings = capabilities
308+
.into_iter()
309+
.map(|cap| cap.to_string())
310+
.collect();
311+
312+
Ok(capability_strings)
313+
}
314+
300315
/// List of capabilities that all target devices implement
301316
#[zbus(property)]
302317
async fn target_capabilities(&self) -> fdo::Result<Vec<String>> {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use std::{error::Error, future::Future};
2+
3+
use packed_struct::types::{Integer, SizedInteger};
4+
use zbus::fdo;
5+
use zbus_macros::interface;
6+
7+
use crate::{
8+
drivers::steam_deck::hid_report::PackedRumbleReport,
9+
input::{composite_device::client::CompositeDeviceClient, output_event::OutputEvent},
10+
};
11+
12+
/// [ForceFeedbacker] is any device that can implement force feedback
13+
pub trait ForceFeedbacker {
14+
fn rumble(&mut self, value: f64) -> impl Future<Output = Result<(), Box<dyn Error>>> + Send;
15+
fn stop(&mut self) -> impl Future<Output = Result<(), Box<dyn Error>>> + Send;
16+
}
17+
18+
impl ForceFeedbacker for CompositeDeviceClient {
19+
async fn rumble(&mut self, value: f64) -> Result<(), Box<dyn Error>> {
20+
let value = value.min(1.0);
21+
let value = value.max(0.0);
22+
log::debug!("Sending rumble event with value: {value}");
23+
let report = PackedRumbleReport {
24+
intensity: (value * u8::MAX as f64) as u8,
25+
left_speed: Integer::from_primitive((value * u16::MAX as f64) as u16),
26+
right_speed: Integer::from_primitive((value * u16::MAX as f64) as u16),
27+
..Default::default()
28+
};
29+
let event = OutputEvent::SteamDeckRumble(report);
30+
self.process_output_event(event).await?;
31+
Ok(())
32+
}
33+
34+
async fn stop(&mut self) -> Result<(), Box<dyn Error>> {
35+
self.rumble(0.0).await
36+
}
37+
}
38+
39+
/// The [ForceFeedbackInterface] provides a DBus interface that can be exposed for
40+
/// managing force feedback events over dbus.
41+
pub struct ForceFeedbackInterface<T>
42+
where
43+
T: ForceFeedbacker + Send + Sync,
44+
{
45+
device: T,
46+
}
47+
48+
impl<T> ForceFeedbackInterface<T>
49+
where
50+
T: ForceFeedbacker + Send + Sync + 'static,
51+
{
52+
/// Create a new dbus interface for the given force feedback device
53+
pub fn new(device: T) -> Self {
54+
Self { device }
55+
}
56+
}
57+
58+
#[interface(
59+
name = "org.shadowblip.Output.ForceFeedback",
60+
proxy(default_service = "org.shadowblip.InputPlumber",)
61+
)]
62+
impl<T> ForceFeedbackInterface<T>
63+
where
64+
T: ForceFeedbacker + Send + Sync + 'static,
65+
{
66+
/// Send a simple rumble event
67+
async fn rumble(&mut self, value: f64) -> fdo::Result<()> {
68+
self.device
69+
.rumble(value)
70+
.await
71+
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
72+
Ok(())
73+
}
74+
75+
/// Stop all currently playing force feedback effects
76+
async fn stop(&mut self) -> fdo::Result<()> {
77+
self.device
78+
.stop()
79+
.await
80+
.map_err(|err| fdo::Error::Failed(err.to_string()))?;
81+
Ok(())
82+
}
83+
}

src/dbus/interface/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod composite_device;
2+
pub mod force_feedback;
23
pub mod manager;
34
pub mod performance;
45
pub mod source;

src/input/composite_device/client.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use tokio::sync::mpsc::{channel, error::SendError, Sender};
88
use crate::config::CompositeDeviceConfig;
99
use crate::input::event::native::NativeEvent;
1010
use crate::input::info::DeviceInfo;
11+
use crate::input::output_capability::OutputCapability;
1112
use crate::input::target::client::TargetDeviceClient;
1213
use crate::input::target::TargetDeviceTypeId;
1314
use crate::input::{capability::Capability, event::Event, output_event::OutputEvent};
@@ -18,6 +19,7 @@ use super::{CompositeCommand, InterceptMode};
1819
/// Maximum duration to wait for a response from a command. If this timeout
1920
/// is reached, that typically indicates a deadlock somewhere in the code.
2021
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
22+
2123
/// Possible errors for a composite device client
2224
#[derive(Error, Debug)]
2325
pub enum ClientError {
@@ -152,6 +154,31 @@ impl CompositeDeviceClient {
152154
Err(ClientError::ChannelClosed)
153155
}
154156

157+
/// Get the output capabilities from all source devices
158+
pub async fn get_output_capabilities(&self) -> Result<HashSet<OutputCapability>, ClientError> {
159+
let (tx, rx) = channel(1);
160+
self.tx
161+
.send(CompositeCommand::GetOutputCapabilities(tx))
162+
.await?;
163+
if let Some(capabilities) = Self::recv(rx).await {
164+
return Ok(capabilities);
165+
}
166+
Err(ClientError::ChannelClosed)
167+
}
168+
169+
/// Get the output capabilities from all source devices (blocking)
170+
#[allow(dead_code)]
171+
pub fn blocking_get_output_capabilities(
172+
&self,
173+
) -> Result<HashSet<OutputCapability>, ClientError> {
174+
let (tx, mut rx) = channel(1);
175+
self.tx
176+
.blocking_send(CompositeCommand::GetOutputCapabilities(tx))?;
177+
if let Some(capabilities) = rx.blocking_recv() {
178+
return Ok(capabilities);
179+
}
180+
Err(ClientError::ChannelClosed)
181+
}
155182

156183
/// Get the [CompositeDeviceConfig] from the [CompositeDevice]
157184
pub async fn get_config(&self) -> Result<CompositeDeviceConfig, ClientError> {

src/input/composite_device/command.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::{
88
capability::Capability,
99
event::{native::NativeEvent, Event},
1010
info::DeviceInfo,
11+
output_capability::OutputCapability,
1112
output_event::OutputEvent,
1213
target::{client::TargetDeviceClient, TargetDeviceTypeId},
1314
},
@@ -23,6 +24,7 @@ pub enum CompositeCommand {
2324
AttachTargetDevices(HashMap<String, TargetDeviceClient>),
2425
GetConfig(mpsc::Sender<CompositeDeviceConfig>),
2526
GetCapabilities(mpsc::Sender<HashSet<Capability>>),
27+
GetOutputCapabilities(mpsc::Sender<HashSet<OutputCapability>>),
2628
GetDBusDevicePaths(mpsc::Sender<Vec<String>>),
2729
GetInterceptMode(mpsc::Sender<InterceptMode>),
2830
GetName(mpsc::Sender<String>),

src/input/composite_device/mod.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ use crate::{
3535
value::{InputValue, TranslationError},
3636
Event,
3737
},
38+
output_capability::OutputCapability,
3839
output_event::UinputOutputEvent,
3940
source::{
4041
evdev::EventDevice, hidraw::HidRawDevice, iio::IioDevice, led::LedDevice, SourceDevice,
@@ -82,6 +83,8 @@ pub struct CompositeDevice {
8283
name: String,
8384
/// Capabilities describe all input capabilities from all source devices
8485
capabilities: HashSet<Capability>,
86+
/// Output capabilities describe all output capabilities from all source devices
87+
output_capabilities: HashSet<OutputCapability>,
8588
/// Capability mapping for the CompositeDevice
8689
capability_map: Option<CapabilityMapConfig>,
8790
/// Currently loaded [DeviceProfile] for the [CompositeDevice]. The [DeviceProfile]
@@ -173,6 +176,7 @@ impl CompositeDevice {
173176
config,
174177
name,
175178
capabilities: HashSet::new(),
179+
output_capabilities: HashSet::new(),
176180
capability_map,
177181
device_profile: None,
178182
device_profile_path: None,
@@ -254,7 +258,7 @@ impl CompositeDevice {
254258
}
255259

256260
/// Creates a new instance of the composite device interface on DBus.
257-
pub async fn listen_on_dbus(&self) -> Result<JoinHandle<()>, Box<dyn Error>> {
261+
pub async fn listen_on_dbus(&mut self) -> Result<JoinHandle<()>, Box<dyn Error>> {
258262
let conn = self.conn.clone();
259263
let client = self.client();
260264
let path = String::from(self.dbus_path());
@@ -323,6 +327,11 @@ impl CompositeDevice {
323327
log::error!("Failed to send capabilities: {:?}", e);
324328
}
325329
}
330+
CompositeCommand::GetOutputCapabilities(sender) => {
331+
if let Err(e) = sender.send(self.output_capabilities.clone()).await {
332+
log::debug!("Failed to send output capabilities: {:?}", e);
333+
}
334+
}
326335
CompositeCommand::GetTargetCapabilities(sender) => {
327336
let target_caps = match self.targets.get_capabilities().await {
328337
Ok(caps) => caps,
@@ -1619,6 +1628,11 @@ impl CompositeDevice {
16191628
}
16201629
self.capabilities.insert(cap);
16211630
}
1631+
1632+
let output_capabilities = source_device.get_output_capabilities()?;
1633+
for cap in output_capabilities {
1634+
self.output_capabilities.insert(cap);
1635+
}
16221636
}
16231637

16241638
// Check if this device should be blocked from sending events to target devices.

src/input/composite_device/targets.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,28 @@ impl CompositeDeviceTargets {
9191

9292
/// Sets the DBus target devices attached to a [CompositeDevice]
9393
pub fn set_dbus_devices(&mut self, devices: HashMap<String, TargetDeviceClient>) {
94+
// Notify the dbus device of the source device(s) capabilities
95+
for dbus_device in devices.values() {
96+
let device = self.device.clone();
97+
let target = dbus_device.clone();
98+
tokio::task::spawn(async move {
99+
let source_capabilities = device.get_capabilities().await.unwrap_or_default();
100+
let source_output_capabilities =
101+
device.get_output_capabilities().await.unwrap_or_default();
102+
target
103+
.set_composite_device(device)
104+
.await
105+
.unwrap_or_default();
106+
target
107+
.notify_capabilities_changed(source_capabilities)
108+
.await
109+
.unwrap_or_default();
110+
target
111+
.notify_output_capabilities_changed(source_output_capabilities)
112+
.await
113+
.unwrap_or_default();
114+
});
115+
}
94116
self.target_dbus_devices = devices;
95117
}
96118

@@ -437,6 +459,23 @@ impl CompositeDeviceTargets {
437459
}
438460
log::debug!("[{dbus_path}] Attached device {path}");
439461

462+
// Notify the target device of the supported source capabilities
463+
let device = self.device.clone();
464+
let target_clone = target.clone();
465+
tokio::task::spawn(async move {
466+
let source_capabilities = device.get_capabilities().await.unwrap_or_default();
467+
let source_output_capabilities =
468+
device.get_output_capabilities().await.unwrap_or_default();
469+
target_clone
470+
.notify_capabilities_changed(source_capabilities)
471+
.await
472+
.unwrap_or_default();
473+
target_clone
474+
.notify_output_capabilities_changed(source_output_capabilities)
475+
.await
476+
.unwrap_or_default();
477+
});
478+
440479
// Track the target device by capabilities it has
441480
for cap in caps {
442481
self.target_devices_by_capability

src/input/output_capability.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::fmt::Display;
2+
13
/// Output capabilities describe what kind of output events a source input device
24
/// is capable of handling. E.g. Force Feedback, LED control, etc.
35
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
@@ -11,6 +13,21 @@ pub enum OutputCapability {
1113
LED(LED),
1214
}
1315

16+
impl Display for OutputCapability {
17+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18+
let str = match self {
19+
OutputCapability::NotImplemented => "NotImplemented".to_string(),
20+
OutputCapability::ForceFeedback => "ForceFeedback".to_string(),
21+
OutputCapability::ForceFeedbackUpload => "ForceFeedbackUpload".to_string(),
22+
OutputCapability::ForceFeedbackErase => "ForceFeedbackErase".to_string(),
23+
OutputCapability::Haptics(haptic) => format!("Haptics:{haptic}"),
24+
OutputCapability::LED(led) => format!("LED:{led}"),
25+
};
26+
27+
write!(f, "{}", str)
28+
}
29+
}
30+
1431
/// LED capability
1532
#[allow(clippy::upper_case_acronyms)]
1633
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
@@ -19,10 +36,32 @@ pub enum LED {
1936
Color,
2037
}
2138

39+
impl Display for LED {
40+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41+
let str = match self {
42+
LED::Brightness => "Brightness",
43+
LED::Color => "Color",
44+
};
45+
46+
write!(f, "{}", str)
47+
}
48+
}
49+
2250
/// Haptic capabilities
2351
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2452
pub enum Haptic {
2553
TrackpadLeft,
2654
TrackpadRight,
2755
//TrackpadCenter,
2856
}
57+
58+
impl Display for Haptic {
59+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60+
let str = match self {
61+
Haptic::TrackpadLeft => "TrackpadLeft",
62+
Haptic::TrackpadRight => "TrackpadRight",
63+
};
64+
65+
write!(f, "{}", str)
66+
}
67+
}

src/input/source/evdev.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ use crate::{
1515
capability_map::{load_capability_mappings, CapabilityMapConfig},
1616
},
1717
constants::BUS_SOURCES_PREFIX,
18-
input::{composite_device::client::CompositeDeviceClient, info::DeviceInfoRef},
18+
input::{
19+
capability::Capability, composite_device::client::CompositeDeviceClient,
20+
info::DeviceInfoRef, output_capability::OutputCapability,
21+
},
1922
udev::device::UdevDevice,
2023
};
2124

2225
use self::{blocked::BlockedEventDevice, gamepad::GamepadEventDevice};
2326

24-
use super::{SourceDeviceCompatible, SourceDriver, SourceDriverOptions};
27+
use super::{OutputError, SourceDeviceCompatible, SourceDriver, SourceDriverOptions};
2528

2629
/// List of available drivers
2730
enum DriverType {
@@ -77,9 +80,7 @@ impl SourceDeviceCompatible for EventDevice {
7780
}
7881
}
7982

80-
fn get_capabilities(
81-
&self,
82-
) -> Result<Vec<crate::input::capability::Capability>, super::InputError> {
83+
fn get_capabilities(&self) -> Result<Vec<Capability>, super::InputError> {
8384
match self {
8485
EventDevice::Blocked(source_driver) => source_driver.get_capabilities(),
8586
EventDevice::Gamepad(source_driver) => source_driver.get_capabilities(),
@@ -88,6 +89,15 @@ impl SourceDeviceCompatible for EventDevice {
8889
}
8990
}
9091

92+
fn get_output_capabilities(&self) -> Result<Vec<OutputCapability>, OutputError> {
93+
match self {
94+
EventDevice::Blocked(source_driver) => source_driver.get_output_capabilities(),
95+
EventDevice::Gamepad(source_driver) => source_driver.get_output_capabilities(),
96+
EventDevice::Touchscreen(source_driver) => source_driver.get_output_capabilities(),
97+
EventDevice::Keyboard(source_driver) => source_driver.get_output_capabilities(),
98+
}
99+
}
100+
91101
fn get_device_path(&self) -> String {
92102
match self {
93103
EventDevice::Blocked(source_driver) => source_driver.get_device_path(),

0 commit comments

Comments
 (0)