Skip to content

Commit

Permalink
Merge pull request #285 from blindedone1458/compositedevice-persist
Browse files Browse the repository at this point in the history
feat(CompositeDevice): Add persist option
  • Loading branch information
ShadowApex authored Feb 15, 2025
2 parents 89e861a + 6a6e3b7 commit b2feb73
Show file tree
Hide file tree
Showing 11 changed files with 103 additions and 24 deletions.
5 changes: 5 additions & 0 deletions rootfs/usr/share/inputplumber/schema/composite_device_v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@
"description": "If true, InputPlumber will automatically try to manage the input device. If this is false, InputPlumber will not try to manage the device unless an external service enables management of the device. Defaults to 'false'",
"type": "boolean",
"default": false
},
"persist": {
"description": "If true, InputPlumber will not stop the CompositeDevice if all the SourceDevices have stopped. This will persist the virtual TargetDevice to allow reconnecting a SourceDevice and resuming input. Defaults to 'false'",
"type": "boolean",
"default": false
}
},
"title": "Options"
Expand Down
1 change: 1 addition & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ pub struct CompositeDeviceConfigOptions {
/// If this is false, InputPlumber will not try to manage the device unless
/// an external service enables management of all devices.
pub auto_manage: Option<bool>,
pub persist: Option<bool>,
}

/// Defines a platform match for loading a [CompositeDeviceConfig]
Expand Down
32 changes: 23 additions & 9 deletions src/input/composite_device/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,16 @@ impl CompositeDevice {
// Start all source devices
self.run_source_devices().await?;

// Set persist value from config if set, used to determine
// if CompositeDevice self-closes after all SourceDevices have
// been removed.
let persist = self
.config
.options
.as_ref()
.map(|options| options.persist.unwrap_or(false))
.unwrap_or(false);

// Loop and listen for command events
log::debug!("CompositeDevice started");
let mut buffer = Vec::with_capacity(BUFFER_SIZE);
Expand Down Expand Up @@ -363,12 +373,6 @@ impl CompositeDevice {
if let Err(e) = self.on_source_device_removed(device).await {
log::error!("Failed to remove source device: {:?}", e);
}
if self.source_devices_used.is_empty() {
log::debug!(
"No source devices remain. Stopping CompositeDevice {dbus_path}"
);
break 'main;
}
}
CompositeCommand::SourceDeviceRemoved(device) => {
log::debug!("Detected source device removed: {}", device.devnode());
Expand Down Expand Up @@ -492,10 +496,20 @@ impl CompositeDevice {
}

// If no source devices remain after processing the queue, stop
// the device.
// the device unless configured to persist.
if devices_removed && self.source_devices_used.is_empty() {
log::debug!("No source devices remain. Stopping CompositeDevice {dbus_path}");
break 'main;
if persist {
log::debug!("No source devices remain, but CompositeDevice {dbus_path} has persist enabled. Clearing target devices states.");
for (path, target) in &self.target_devices {
log::debug!("Clearing target device: {path}");
if let Err(e) = target.clear_state().await {
log::error!("Failed to clear target device state {path}: {e:?}");
}
}
} else {
log::debug!("No source devices remain. Stopping CompositeDevice {dbus_path}");
break 'main;
}
}
}
log::info!("CompositeDevice stopping: {dbus_path}");
Expand Down
23 changes: 9 additions & 14 deletions src/input/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -930,30 +930,25 @@ impl Manager {
};
log::debug!("Checking if existing composite device {composite_device:?} with config {:?} is missing device: {id:?}", config.name);

// If the CompositeDevice only allows a single source device, skip its
// consideration.
if config.single_source.unwrap_or(false) {
log::trace!("{:?} is a single source device. Skipping.", config.name);
continue;
}
if config.maximum_sources.unwrap_or(0) == 1 {
log::trace!("{:?} is a single source device. Skipping.", config.name);
continue;
}
log::trace!(
"Composite device has {} source devices defined",
config.source_devices.len()
);

let max_sources = config.maximum_sources.unwrap_or_else(|| {
if config.single_source.unwrap_or(false) {
1
} else {
0
}
});

// If the CompositeDevice only allows a maximum number of source devices,
// check to see if that limit has been reached. If that limit is reached,
// then a new CompositeDevice will be created for the source device.
// If maximum_sources is less than 1 (e.g. 0, -1) then consider
// the maximum to be 'unlimited'.
if let Some(max_sources) = config
.maximum_sources
.filter(|max_sources| *max_sources > 0)
{
if max_sources > 0 {
// Check to see how many source devices this composite device is
// currently managing.
if self
Expand Down
10 changes: 10 additions & 0 deletions src/input/target/dualsense.rs
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,16 @@ impl TargetInputDevice for DualSenseDevice {
let _ = self.device.destroy();
Ok(())
}

/// Clear any local state on the target device.
fn clear_state(&mut self) {
self.state = match self.state {
PackedInputDataReport::Usb(_) => PackedInputDataReport::Usb(Default::default()),
PackedInputDataReport::Bluetooth(_) => {
PackedInputDataReport::Bluetooth(Default::default())
}
};
}
}

impl TargetOutputDevice for DualSenseDevice {
Expand Down
5 changes: 5 additions & 0 deletions src/input/target/horipad_steam.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,11 @@ impl TargetInputDevice for HoripadSteamDevice {
let _ = self.device.destroy();
Ok(())
}

/// Clear any local state on the target device.
fn clear_state(&mut self) {
self.state = Default::default();
}
}

impl TargetOutputDevice for HoripadSteamDevice {
Expand Down
4 changes: 3 additions & 1 deletion src/input/target/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,9 @@ pub trait TargetInputDevice {
/// Clear any local state on the target device. This is typically called
/// whenever the composite device has entered intercept mode to indicate
/// that the target device should stop sending input.
fn clear_state(&mut self) {}
fn clear_state(&mut self) {
log::debug!("Generic clear state called. Do nothing.");
}

/// Called when the target device has been attached to a composite device.
fn on_composite_device_attached(
Expand Down
5 changes: 5 additions & 0 deletions src/input/target/steam_deck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,11 @@ impl TargetInputDevice for SteamDeckDevice {
log::debug!("Finished stopping");
Ok(())
}

/// Clear any local state on the target device.
fn clear_state(&mut self) {
self.state = Default::default();
}
}

impl TargetOutputDevice for SteamDeckDevice {
Expand Down
14 changes: 14 additions & 0 deletions src/input/target/xb360.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::input::capability::{Capability, Gamepad, GamepadAxis, GamepadButton,
use crate::input::composite_device::client::CompositeDeviceClient;
use crate::input::event::evdev::EvdevEvent;
use crate::input::event::native::{NativeEvent, ScheduledNativeEvent};
use crate::input::event::value::InputValue;
use crate::input::output_capability::OutputCapability;
use crate::input::output_event::{OutputEvent, UinputOutputEvent};

Expand Down Expand Up @@ -218,6 +219,19 @@ impl TargetInputDevice for XBox360Controller {
}
Some(self.queued_events.drain(..).collect())
}

/// Clear any local state on the target device.
fn clear_state(&mut self) {
let caps = self.get_capabilities().unwrap_or_else(|_| {
log::error!("No target device capabilities found while clearing state.");
Vec::new()
});
for cap in caps {
let ev = NativeEvent::new(cap, InputValue::Bool(false));
self.queued_events
.push(ScheduledNativeEvent::new(ev, Duration::from_millis(0)));
}
}
}

impl TargetOutputDevice for XBox360Controller {
Expand Down
14 changes: 14 additions & 0 deletions src/input/target/xbox_elite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::input::capability::{Capability, Gamepad, GamepadAxis, GamepadButton,
use crate::input::composite_device::client::CompositeDeviceClient;
use crate::input::event::evdev::EvdevEvent;
use crate::input::event::native::{NativeEvent, ScheduledNativeEvent};
use crate::input::event::value::InputValue;
use crate::input::output_capability::OutputCapability;
use crate::input::output_event::{OutputEvent, UinputOutputEvent};

Expand Down Expand Up @@ -229,6 +230,19 @@ impl TargetInputDevice for XboxEliteController {
}
Some(self.queued_events.drain(..).collect())
}

/// Clear any local state on the target device.
fn clear_state(&mut self) {
let caps = self.get_capabilities().unwrap_or_else(|_| {
log::error!("No target device capabilities found while clearing state.");
Vec::new()
});
for cap in caps {
let ev = NativeEvent::new(cap, InputValue::Bool(false));
self.queued_events
.push(ScheduledNativeEvent::new(ev, Duration::from_millis(0)));
}
}
}

impl TargetOutputDevice for XboxEliteController {
Expand Down
14 changes: 14 additions & 0 deletions src/input/target/xbox_series.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use crate::input::capability::{Capability, Gamepad, GamepadAxis, GamepadButton,
use crate::input::composite_device::client::CompositeDeviceClient;
use crate::input::event::evdev::EvdevEvent;
use crate::input::event::native::{NativeEvent, ScheduledNativeEvent};
use crate::input::event::value::InputValue;
use crate::input::output_capability::OutputCapability;
use crate::input::output_event::{OutputEvent, UinputOutputEvent};

Expand Down Expand Up @@ -220,6 +221,19 @@ impl TargetInputDevice for XboxSeriesController {
}
Some(self.queued_events.drain(..).collect())
}

/// Clear any local state on the target device.
fn clear_state(&mut self) {
let caps = self.get_capabilities().unwrap_or_else(|_| {
log::error!("No target device capabilities found while clearing state.");
Vec::new()
});
for cap in caps {
let ev = NativeEvent::new(cap, InputValue::Bool(false));
self.queued_events
.push(ScheduledNativeEvent::new(ev, Duration::from_millis(0)));
}
}
}

impl TargetOutputDevice for XboxSeriesController {
Expand Down

0 comments on commit b2feb73

Please sign in to comment.