Skip to content

Commit

Permalink
feat(CompositeDevice): Add device profiles.
Browse files Browse the repository at this point in the history
  • Loading branch information
pastaq authored and ShadowApex committed Mar 31, 2024
1 parent 549d6a1 commit d8ed6d5
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 23 deletions.
62 changes: 60 additions & 2 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ pub enum LoadError {
DeserializeError(#[from] serde_yaml::Error),
}

#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub struct DeviceProfile {
pub version: u32, //useful?
pub kind: String, //useful?
pub name: String, //useful?
pub mapping: Vec<ProfileMapping>,
}

#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub struct ProfileMapping {
pub name: String,
pub source_event: Capability,
pub target_events: Vec<Capability>,
}

#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub struct CapabilityMap {
Expand All @@ -41,14 +58,40 @@ pub struct Capability {
pub gamepad: Option<GamepadCapability>,
pub keyboard: Option<String>,
pub mouse: Option<MouseCapability>,
pub dbus: Option<String>,
}

#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub struct GamepadCapability {
pub axis: Option<String>,
pub axis: Option<AxisCapability>,
pub button: Option<String>,
pub trigger: Option<String>,
pub trigger: Option<TriggerCapability>,
pub gyro: Option<GyroCapability>,
}

#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub struct AxisCapability {
pub name: String,
pub direction: Option<String>,
pub deadzone: Option<f64>,
}

#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub struct TriggerCapability {
pub name: String,
pub deadzone: Option<f64>,
}

#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub struct GyroCapability {
pub name: String,
pub direction: Option<String>,
pub deadzone: Option<f64>,
pub axis: Option<String>,
}

#[derive(Debug, Deserialize, Clone)]
Expand All @@ -58,6 +101,21 @@ pub struct MouseCapability {
pub motion: Option<String>,
}

impl DeviceProfile {
/// Load a [CapabilityProfile] from the given YAML string
pub fn from_yaml(content: String) -> Result<DeviceProfile, LoadError> {
let device: DeviceProfile = serde_yaml::from_str(content.as_str())?;
Ok(device)
}

/// Load a [CapabilityProfile] from the given YAML file
pub fn from_yaml_file(path: String) -> Result<DeviceProfile, LoadError> {
let file = std::fs::File::open(path)?;
let device: DeviceProfile = serde_yaml::from_reader(file)?;
Ok(device)
}
}

impl CapabilityMap {
/// Load a [CapabilityMap] from the given YAML string
pub fn from_yaml(content: String) -> Result<CapabilityMap, LoadError> {
Expand Down
9 changes: 6 additions & 3 deletions src/input/capability.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,13 @@ impl From<config::Capability> for Capability {
let button = button.unwrap();
return Capability::Gamepad(Gamepad::Button(button));
}
if let Some(trigger_string) = gamepad.trigger.clone() {
let trigger = GamepadTrigger::from_str(&trigger_string);
if let Some(trigger_capability) = gamepad.trigger.clone() {
let trigger = GamepadTrigger::from_str(&trigger_capability.name);
if trigger.is_err() {
log::error!("Invalid or unimplemented trigger: {trigger_string}");
log::error!(
"Invalid or unimplemented trigger: {}",
trigger_capability.name
);
return Capability::NotImplemented;
}

Expand Down
104 changes: 86 additions & 18 deletions src/input/composite_device/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use std::{
collections::{HashMap, HashSet},
error::Error,
str::FromStr,
};

use tokio::{
Expand All @@ -12,7 +11,7 @@ use zbus::{fdo, Connection};
use zbus_macros::dbus_interface;

use crate::{
config::{CapabilityMap, CapabilityMapping, CompositeDeviceConfig},
config::{CapabilityMap, CapabilityMapping, CompositeDeviceConfig, DeviceProfile},
input::{
capability::{Gamepad, GamepadButton},
event::native::NativeEvent,
Expand All @@ -22,7 +21,7 @@ use crate::{
};

use super::{
capability::{Capability, Keyboard},
capability::Capability,
event::{native::InputValue, Event},
manager::SourceDeviceInfo,
source::SourceDevice,
Expand Down Expand Up @@ -217,6 +216,10 @@ pub struct CompositeDevice {
capabilities: HashSet<Capability>,
/// Capability mapping for the CompositeDevice
capability_map: Option<CapabilityMap>,
/// Name of the urrently loaded [DeviceProfile] for the CompositeDevice
device_profile: Option<String>,
/// Map of profile capabilities and the capabilities they translate into
device_profile_map: HashMap<Capability, Vec<Capability>>,
/// List of input capabilities that can be translated by the capability map
translatable_capabilities: Vec<Capability>,
/// List of currently "pressed" actions used to translate multiple input
Expand Down Expand Up @@ -263,6 +266,8 @@ impl CompositeDevice {
config,
capabilities: HashSet::new(),
capability_map,
device_profile: None,
device_profile_map: HashMap::new(),
translatable_capabilities: Vec::new(),
translatable_active_inputs: Vec::new(),
emitted_mappings: HashMap::new(),
Expand All @@ -283,6 +288,16 @@ impl CompositeDevice {
device.load_capability_map()?;
}

// Load the default profile
let profile_path = "/usr/share/inputplumber/profiles/default.yaml";
if let Err(error) = device.load_device_profile_from_path(profile_path.to_string()) {
log::warn!(
"Unable to load default profile at {}. {}",
profile_path,
error
);
};

// If a capability map is defined, add those target capabilities to
// the hashset of implemented capabilities.
if let Some(map) = device.capability_map.as_ref() {
Expand Down Expand Up @@ -564,24 +579,31 @@ impl CompositeDevice {

/// Translate and write the given event to the appropriate target devices
async fn handle_event(&mut self, event: NativeEvent) -> Result<(), Box<dyn Error>> {
// TODO: Translate the event based on the device profile.
// Translate using the device profile.

// Process the event depending on the intercept mode
let mode = self.intercept_mode.clone();
let cap = event.as_capability();
if matches!(mode, InterceptMode::Pass)
&& cap == Capability::Gamepad(Gamepad::Button(GamepadButton::Guide))
{
// Set the intercept mode while the button is pressed
if event.pressed() {
log::debug!("Intercepted guide button press");
self.set_intercept_mode(InterceptMode::Always);
}
}
let events = if self.device_profile.is_some() {
self.translate_event(&event).await?
} else {
vec![event]
};

// Write the event
self.write_event(event).await?;
for event in events {
// Process the event depending on the intercept mode
let mode = self.intercept_mode.clone();
let cap = event.as_capability();
if matches!(mode, InterceptMode::Pass)
&& cap == Capability::Gamepad(Gamepad::Button(GamepadButton::Guide))
{
// Set the intercept mode while the button is pressed
if event.pressed() {
log::debug!("Intercepted guide button press");
self.set_intercept_mode(InterceptMode::Always);
}
}

// Write the event
self.write_event(event).await?;
}
Ok(())
}

Expand Down Expand Up @@ -863,4 +885,50 @@ impl CompositeDevice {

Ok(())
}

/// Translates the given event into a Vec of events based on the currently loaded
/// [DeviceProfile]
async fn translate_event(
&self,
event: &NativeEvent,
) -> Result<Vec<NativeEvent>, Box<dyn Error>> {
let cap = event.as_capability();
if let Some(target_capabilities) = self.device_profile_map.get(&cap) {
let mut events = Vec::new();
for capy in target_capabilities {
let event = NativeEvent::new(capy.clone(), event.get_value());
events.push(event);
}
return Ok(events);
}

Ok(vec![event.clone()])
}

pub fn load_device_profile_from_path(&mut self, path: String) -> Result<(), Box<dyn Error>> {
// Remove all outdated capabily mappings.
self.device_profile_map.clear();
// Open the device profile.
let profile = DeviceProfile::from_yaml_file(path.clone());
match profile {
Ok(profile) => {
self.device_profile = Some(profile.name);
// Loop through every mapping in the profile, extrace the source and target events,
// and map them into out profile map.
for mapping in profile.mapping.iter() {
let source_event_cap: Capability = mapping.source_event.clone().into();
let mut capabilities = Vec::new();
for cap in mapping.target_events.clone() {
let capy: Capability = cap.into();
capabilities.push(capy);
}
self.device_profile_map
.insert(source_event_cap, capabilities);
}
Ok(())
}
// Something went wrong here
Err(error) => Err(error.into()),
}
}
}

0 comments on commit d8ed6d5

Please sign in to comment.