diff --git a/rootfs/usr/share/inputplumber/capability_maps/msiclaw_type1_dinput.yaml b/rootfs/usr/share/inputplumber/capability_maps/msiclaw_type1_dinput.yaml deleted file mode 100644 index bb622d8e..00000000 --- a/rootfs/usr/share/inputplumber/capability_maps/msiclaw_type1_dinput.yaml +++ /dev/null @@ -1,183 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/ShadowBlip/InputPlumber/main/rootfs/usr/share/inputplumber/schema/capability_map_v2.json -# Schema version number -version: 2 - -# The type of configuration schema -kind: CapabilityMap - -# Name for the device event map -name: MSI Claw Type 1 DirectInput mode - -# Unique identifier of the capability mapping -id: claw1-dinput - -# List of mapped events that are activated by a specific set of activation keys. -mapping: - - # dinput mappings - - name: Start - source_events: - - evdev: - event_type: KEY - event_code: BTN_TR2 - value_type: button - target_event: - gamepad: - button: Start - - - name: Select - source_events: - - evdev: - event_type: KEY - event_code: BTN_TL2 - value_type: button - target_event: - gamepad: - button: Select - - - name: A - source_events: - - evdev: - event_type: KEY - event_code: BTN_EAST - value_type: button - target_event: - gamepad: - button: South - - - name: B - source_events: - - evdev: - event_type: KEY - event_code: BTN_C - value_type: button - target_event: - gamepad: - button: East - - - name: X - source_events: - - evdev: - event_type: KEY - event_code: BTN_SOUTH - value_type: button - target_event: - gamepad: - button: North - - - name: Y - source_events: - - evdev: - event_type: KEY - event_code: BTN_NORTH - value_type: button - target_event: - gamepad: - button: West - - - name: L1 - source_events: - - evdev: - event_type: KEY - event_code: BTN_WEST - value_type: button - target_event: - gamepad: - button: LeftBumper - - - name: L2 - source_events: - - evdev: - event_type: ABS - event_code: ABS_RX - value_type: trigger - - evdev: - event_type: KEY - event_code: BTN_TL - value_type: button - target_event: - gamepad: - trigger: - name: LeftTrigger - - - name: L3 - source_events: - - evdev: - event_type: KEY - event_code: BTN_SELECT - value_type: button - target_event: - gamepad: - button: LeftStick - - - name: R1 - source_events: - - evdev: - event_type: KEY - event_code: BTN_Z - value_type: button - target_event: - gamepad: - button: RightBumper - - - name: R2 - source_events: - - evdev: - event_type: ABS - event_code: ABS_RY - value_type: trigger - - evdev: - event_type: KEY - event_code: BTN_TR - value_type: button - target_event: - gamepad: - trigger: - name: RightTrigger - - - name: R3 - source_events: - - evdev: - event_type: KEY - event_code: BTN_START - value_type: button - target_event: - gamepad: - button: RightStick - - - name: Right Stick - source_events: - - evdev: - event_type: ABS - event_code: ABS_Z - value_type: joystick_x - - evdev: - event_type: ABS - event_code: ABS_RZ - value_type: joystick_y - target_event: - gamepad: - axis: - name: RightStick - - - name: Left Paddle - source_events: - - evdev: - event_type: KEY - event_code: BTN_TRIGGER_HAPPY1 - value_type: button - target_event: - gamepad: - button: LeftPaddle1 - - # the right paddle uses scancode 90010, which coheres with nothing, - # remapped to 0x2c1 in the hwdb file - - name: Right Paddle - source_events: - - evdev: - event_type: KEY - event_code: BTN_TRIGGER_HAPPY2 - value_type: button - target_event: - gamepad: - button: RightPaddle1 diff --git a/rootfs/usr/share/inputplumber/devices/50-msi_claw7_a2vm.yaml b/rootfs/usr/share/inputplumber/devices/50-msi_claw7_a2vm.yaml index fb2b334f..313195d7 100644 --- a/rootfs/usr/share/inputplumber/devices/50-msi_claw7_a2vm.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-msi_claw7_a2vm.yaml @@ -91,6 +91,7 @@ source_devices: # Dinput Gamepad - group: gamepad + blocked: true udev: properties: - name: ID_INPUT_JOYSTICK @@ -102,7 +103,12 @@ source_devices: value: "1902" - name: phys value: "usb-0000:00:14.0-2/input0" - capability_map_id: claw1-dinput + - group: gamepad + unique: true + hidraw: + vendor_id: 0x0db0 + product_id: 0x1902 + interface_num: 0 # Dinput Mkey Extra Buttons - group: gamepad diff --git a/rootfs/usr/share/inputplumber/devices/50-msi_claw8_a2vm.yaml b/rootfs/usr/share/inputplumber/devices/50-msi_claw8_a2vm.yaml index 9fdcebd5..6c74eb66 100644 --- a/rootfs/usr/share/inputplumber/devices/50-msi_claw8_a2vm.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-msi_claw8_a2vm.yaml @@ -91,6 +91,7 @@ source_devices: # Dinput Gamepad - group: gamepad + blocked: true udev: properties: - name: ID_INPUT_JOYSTICK @@ -102,7 +103,12 @@ source_devices: value: "1902" - name: phys value: "usb-0000:00:14.0-2/input0" - capability_map_id: claw1-dinput + - group: gamepad + unique: true + hidraw: + vendor_id: 0x0db0 + product_id: 0x1902 + interface_num: 0 # Dinput Mkey Extra Buttons - group: gamepad diff --git a/rootfs/usr/share/inputplumber/devices/50-msi_claw8_ex_ai_cg3em.yaml b/rootfs/usr/share/inputplumber/devices/50-msi_claw8_ex_ai_cg3em.yaml index e20addd1..df14771b 100644 --- a/rootfs/usr/share/inputplumber/devices/50-msi_claw8_ex_ai_cg3em.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-msi_claw8_ex_ai_cg3em.yaml @@ -91,6 +91,7 @@ source_devices: # Dinput Gamepad - group: gamepad + blocked: true udev: properties: - name: ID_INPUT_JOYSTICK @@ -102,7 +103,12 @@ source_devices: value: "1902" - name: phys value: "usb-0000:00:14.0-4/input0" - capability_map_id: claw1-dinput + - group: gamepad + unique: true + hidraw: + vendor_id: 0x0db0 + product_id: 0x1902 + interface_num: 0 # Dinput Mkey Extra Buttons - group: gamepad diff --git a/rootfs/usr/share/inputplumber/devices/50-msi_claw_a1m.yaml b/rootfs/usr/share/inputplumber/devices/50-msi_claw_a1m.yaml index ce7c9aeb..9a7c6f35 100644 --- a/rootfs/usr/share/inputplumber/devices/50-msi_claw_a1m.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-msi_claw_a1m.yaml @@ -91,6 +91,7 @@ source_devices: # Dinput Gamepad - group: gamepad + blocked: true udev: properties: - name: ID_INPUT_JOYSTICK @@ -102,7 +103,12 @@ source_devices: value: "1902" - name: phys value: "usb-0000:00:14.0-9/input0" - capability_map_id: claw1-dinput + - group: gamepad + unique: true + hidraw: + vendor_id: 0x0db0 + product_id: 0x1902 + interface_num: 0 # Dinput Mkey Extra Buttons - group: gamepad diff --git a/rootfs/usr/share/inputplumber/devices/50-msi_claw_a8_bz2e.yaml b/rootfs/usr/share/inputplumber/devices/50-msi_claw_a8_bz2e.yaml index 21185965..53823457 100644 --- a/rootfs/usr/share/inputplumber/devices/50-msi_claw_a8_bz2e.yaml +++ b/rootfs/usr/share/inputplumber/devices/50-msi_claw_a8_bz2e.yaml @@ -92,6 +92,7 @@ source_devices: # Dinput Gamepad - group: gamepad + blocked: true udev: properties: - name: ID_INPUT_JOYSTICK @@ -103,7 +104,12 @@ source_devices: value: "1902" - name: phys value: "usb-0000:c6:00.0-3/input0" - capability_map_id: claw1-dinput + - group: gamepad + unique: true + hidraw: + vendor_id: 0x0db0 + product_id: 0x1902 + interface_num: 0 # Dinput Mkey Extra Buttons - group: gamepad diff --git a/src/drivers/mod.rs b/src/drivers/mod.rs index 7a8477ec..8402fd2a 100644 --- a/src/drivers/mod.rs +++ b/src/drivers/mod.rs @@ -6,6 +6,7 @@ pub mod horipad_steam; pub mod iio_imu; pub mod lego; pub mod legos; +pub mod msi_claw; pub mod opineo; pub mod oxp_hid; pub mod oxp_tty; diff --git a/src/drivers/msi_claw/driver.rs b/src/drivers/msi_claw/driver.rs new file mode 100644 index 00000000..f9fe1d77 --- /dev/null +++ b/src/drivers/msi_claw/driver.rs @@ -0,0 +1,224 @@ +use std::{error::Error, ffi::CString}; + +use hidapi::HidDevice; +use packed_struct::PackedStruct; + +use super::{ + event::{AxisEvent, BinaryInput, ButtonEvent, Event, JoyAxisInput, TriggerEvent, TriggerInput}, + hid_report::{InputDataReport, RumbleOutputDataReport}, + HID_TIMEOUT, INPUT_PACKET_SIZE, PID, VID, +}; + +pub struct Driver { + /// HIDRAW device instance + device: HidDevice, + /// State for the internal gamepad controller + input_state: Option, +} + +impl Driver { + pub fn new(path: String) -> Result> { + let fmtpath = path.clone(); + let path = CString::new(path)?; + let api = hidapi::HidApi::new()?; + let device = api.open_path(&path)?; + let info = device.get_device_info()?; + + if info.vendor_id() != VID || info.product_id() != PID { + return Err(format!("Device '{fmtpath}' is not an MSI Claw Controller").into()); + } + Ok(Self { + device, + input_state: None, + }) + } + + /// Poll the device and read input reports + pub fn poll(&mut self) -> Result, Box> { + // Read data from the device into a buffer + let mut buf = [0; INPUT_PACKET_SIZE]; + let bytes_read = self.device.read_timeout(&mut buf[..], HID_TIMEOUT)?; + + if bytes_read != INPUT_PACKET_SIZE { + return Ok(vec![]); + } + + match self.handle_input_report(buf) { + Ok(events) => Ok(events), + Err(_e) => Ok(vec![]), + } + } + + /// Writes the given output state to the gamepad. This can be used to change + /// the color of LEDs, activate rumble, etc. + pub fn write(&self, buf: &[u8]) -> Result<(), Box> { + let bytes_written = self.device.write(buf)?; + log::debug!("Wrote {bytes_written} bytes"); + Ok(()) + } + + pub fn haptic_rumble( + &self, + strong_magnitude: u8, + weak_magnitude: u8, + ) -> Result<(), Box> { + let report = RumbleOutputDataReport { + strong_magnitude, + weak_magnitude, + ..Default::default() + }; + log::debug!("Got rumble event: {report:?}"); + + let buf = report.pack()?; + self.write(&buf) + } + + /* GamePad */ + /// Unpacks the buffer into a [InputDataReport] structure and updates + /// the internal input_state + fn handle_input_report( + &mut self, + buf: [u8; INPUT_PACKET_SIZE], + ) -> Result, Box> { + let input_report = InputDataReport::unpack(&buf)?; + + // Print input report for debugging + //log::debug!("--- Input report ---"); + //log::debug!("{input_report}"); + //log::debug!(" ---- End Report ----"); + + // Update the state + let old_input_state = self.update_input_state(input_report); + + // Translate the state into a stream of input events + let events = self.translate_input(old_input_state); + + Ok(events) + } + + /// Update gamepad state + fn update_input_state(&mut self, input_report: InputDataReport) -> Option { + let old_state = self.input_state; + self.input_state = Some(input_report); + old_state + } + + /// Translate the state into individual events + fn translate_input(&self, old_state: Option) -> Vec { + let mut events = Vec::new(); + let Some(state) = self.input_state else { + return events; + }; + + // Translate state changes into events if they have changed + let Some(old_state) = old_state else { + return events; + }; + + // Binary Events + if state.button_a != old_state.button_a { + events.push(Event::Button(ButtonEvent::A(BinaryInput { + pressed: state.button_a, + }))); + } + if state.button_b != old_state.button_b { + events.push(Event::Button(ButtonEvent::B(BinaryInput { + pressed: state.button_b, + }))); + } + if state.button_x != old_state.button_x { + events.push(Event::Button(ButtonEvent::X(BinaryInput { + pressed: state.button_x, + }))); + } + if state.button_y != old_state.button_y { + events.push(Event::Button(ButtonEvent::Y(BinaryInput { + pressed: state.button_y, + }))); + } + if state.button_menu != old_state.button_menu { + events.push(Event::Button(ButtonEvent::Menu(BinaryInput { + pressed: state.button_menu, + }))); + } + if state.button_view != old_state.button_view { + events.push(Event::Button(ButtonEvent::View(BinaryInput { + pressed: state.button_view, + }))); + } + if state.button_lb != old_state.button_lb { + events.push(Event::Button(ButtonEvent::LB(BinaryInput { + pressed: state.button_lb, + }))); + } + if state.button_rb != old_state.button_rb { + events.push(Event::Button(ButtonEvent::RB(BinaryInput { + pressed: state.button_rb, + }))); + } + if state.button_l3 != old_state.button_l3 { + events.push(Event::Button(ButtonEvent::ThumbL(BinaryInput { + pressed: state.button_l3, + }))); + } + if state.button_r3 != old_state.button_r3 { + events.push(Event::Button(ButtonEvent::ThumbR(BinaryInput { + pressed: state.button_r3, + }))); + } + + // Dpad Events + if state.dpad_dir != old_state.dpad_dir { + let (new_up, new_right, new_down, new_left) = state.dpad_dir.button_states(); + let (old_up, old_right, old_down, old_left) = old_state.dpad_dir.button_states(); + + if new_up != old_up { + events.push(Event::Button(ButtonEvent::DPadUp(BinaryInput { + pressed: new_up, + }))); + } + if new_right != old_right { + events.push(Event::Button(ButtonEvent::DPadRight(BinaryInput { + pressed: new_right, + }))); + } + if new_down != old_down { + events.push(Event::Button(ButtonEvent::DPadDown(BinaryInput { + pressed: new_down, + }))); + } + if new_left != old_left { + events.push(Event::Button(ButtonEvent::DPadLeft(BinaryInput { + pressed: new_left, + }))); + } + } + + // Axis events + if state.l_stick_x != old_state.l_stick_x || state.l_stick_y != old_state.l_stick_y { + events.push(Event::Axis(AxisEvent::LStick(JoyAxisInput { + x: state.l_stick_x, + y: state.l_stick_y, + }))); + } + if state.r_stick_x != old_state.r_stick_x || state.r_stick_y != old_state.r_stick_y { + events.push(Event::Axis(AxisEvent::RStick(JoyAxisInput { + x: state.r_stick_x, + y: state.r_stick_y, + }))); + } + + if state.trigger_l != old_state.trigger_l { + events.push(Event::Trigger(TriggerEvent::ATriggerL(TriggerInput { + value: state.trigger_l, + }))); + } + if state.trigger_r != old_state.trigger_r { + events.push(Event::Trigger(TriggerEvent::ATriggerR(TriggerInput { + value: state.trigger_r, + }))); + } + + events + } +} diff --git a/src/drivers/msi_claw/event.rs b/src/drivers/msi_claw/event.rs new file mode 100644 index 00000000..1d35b9b6 --- /dev/null +++ b/src/drivers/msi_claw/event.rs @@ -0,0 +1,73 @@ +/// Events that can be emitted by the Legion Go controller +#[derive(Clone, Debug)] +pub enum Event { + Axis(AxisEvent), + Button(ButtonEvent), + Trigger(TriggerEvent), +} + +/// Binary input contain either pressed or unpressed +#[derive(Clone, Debug)] +pub struct BinaryInput { + pub pressed: bool, +} + +/// Button events represend binary inputs +#[derive(Clone, Debug)] +pub enum ButtonEvent { + /// A Button + A(BinaryInput), + /// X Button + X(BinaryInput), + /// B Button + B(BinaryInput), + /// Y Button + Y(BinaryInput), + /// Hamburger (☰) button + Menu(BinaryInput), + /// Overlapping square ⧉ button + View(BinaryInput), + /// DPad down + DPadDown(BinaryInput), + /// DPad up + DPadUp(BinaryInput), + /// DPad left + DPadLeft(BinaryInput), + /// DPad right + DPadRight(BinaryInput), + /// Left shoulder button + LB(BinaryInput), + /// Z-axis button on the left stick + ThumbL(BinaryInput), + /// Right shoulder button + RB(BinaryInput), + /// Z-axis button on the right stick + ThumbR(BinaryInput), +} + +/// Axis input contain (x, y) coordinates +#[derive(Clone, Debug)] +pub struct JoyAxisInput { + pub x: u8, + pub y: u8, +} + +/// Axis events are events that have (x, y) values +#[derive(Clone, Debug)] +pub enum AxisEvent { + LStick(JoyAxisInput), + RStick(JoyAxisInput), +} + +/// Trigger input contains non-negative integars +#[derive(Clone, Debug)] +pub struct TriggerInput { + pub value: u8, +} + +/// Trigger events contain values indicating how far a trigger is pulled +#[derive(Clone, Debug)] +pub enum TriggerEvent { + ATriggerL(TriggerInput), + ATriggerR(TriggerInput), +} diff --git a/src/drivers/msi_claw/hid_report.rs b/src/drivers/msi_claw/hid_report.rs new file mode 100644 index 00000000..3dab73dd --- /dev/null +++ b/src/drivers/msi_claw/hid_report.rs @@ -0,0 +1,138 @@ +//! Reference: https://gitlab.com/open-sd/opensd/-/blob/main/src/opensdd/drivers/gamepad/hid_reports.hpp +#![allow(warnings)] +use packed_struct::prelude::*; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum ReportId { + InputData = 0x01, + RumbleData = 0x05, +} + +impl ReportId { + pub fn to_u8(&self) -> u8 { + match self { + &ReportId::InputData => ReportId::InputData as u8, + &ReportId::RumbleData => ReportId::RumbleData as u8, + } + } +} + +#[derive(PrimitiveEnum_u8, Clone, Copy, PartialEq, Debug, Default)] +pub enum Direction { + Up = 0, + UpRight = 1, + Right = 2, + DownRight = 3, + Down = 4, + DownLeft = 5, + Left = 6, + UpLeft = 7, + #[default] + None = 15, +} + +impl Direction { + /// Translates the raw hardware hat-switch value into 4 independent button states + /// Returns: (Up, Right, Down, Left) + pub fn button_states(&self) -> (bool, bool, bool, bool) { + match *self { + Self::Up => (true, false, false, false), + Self::UpRight => (true, true, false, false), + Self::Right => (false, true, false, false), + Self::DownRight => (false, true, true, false), + Self::Down => (false, false, true, false), + Self::DownLeft => (false, false, true, true), + Self::Left => (false, false, false, true), + Self::UpLeft => (true, false, false, true), + Self::None => (false, false, false, false), + } + } +} + +// Dinput Data +#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)] +#[packed_struct(bit_numbering = "msb0", size_bytes = "64")] +pub struct InputDataReport { + // byte 0 + #[packed_field(bytes = "0")] + pub report_id: u8, + // byte 1 + #[packed_field(bytes = "1")] + pub l_stick_x: u8, + // byte 2 + #[packed_field(bytes = "2")] + pub l_stick_y: u8, + // byte 3 + #[packed_field(bytes = "3")] + pub r_stick_x: u8, + // byte 4 + #[packed_field(bytes = "4")] + pub r_stick_y: u8, + + // byte 5 + #[packed_field(bits = "40")] + pub button_y: bool, + #[packed_field(bits = "41")] + pub button_b: bool, + #[packed_field(bits = "42")] + pub button_a: bool, + #[packed_field(bits = "43")] + pub button_x: bool, + #[packed_field(bits = "44..=47", ty = "enum")] + pub dpad_dir: Direction, + + // byte 6 + #[packed_field(bits = "48")] + pub button_r3: bool, + #[packed_field(bits = "49")] + pub button_l3: bool, + #[packed_field(bits = "50")] + pub button_menu: bool, + #[packed_field(bits = "51")] + pub button_view: bool, + #[packed_field(bits = "52")] + pub unused_52: bool, + #[packed_field(bits = "53")] + pub unused_53: bool, + #[packed_field(bits = "54")] + pub button_rb: bool, + #[packed_field(bits = "55")] + pub button_lb: bool, + + // byte 8 + #[packed_field(bytes = "8")] + pub trigger_l: u8, + // byte 9 + #[packed_field(bytes = "9")] + pub trigger_r: u8, +} + +#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)] +#[packed_struct(bit_numbering = "msb0", size_bytes = "11")] +pub struct RumbleOutputDataReport { + #[packed_field(bytes = "0")] + pub report_id: u8, + #[packed_field(bytes = "1")] + pub unk_1: u8, + #[packed_field(bytes = "2")] + pub unk_2: u8, + #[packed_field(bytes = "3")] + pub unk_3: u8, + #[packed_field(bytes = "4")] + pub weak_magnitude: u8, + #[packed_field(bytes = "5")] + pub strong_magnitude: u8, +} + +impl Default for RumbleOutputDataReport { + fn default() -> Self { + Self { + report_id: ReportId::RumbleData.to_u8(), + unk_1: 0x01, + unk_2: Default::default(), + unk_3: Default::default(), + weak_magnitude: Default::default(), + strong_magnitude: Default::default(), + } + } +} diff --git a/src/drivers/msi_claw/mod.rs b/src/drivers/msi_claw/mod.rs new file mode 100644 index 00000000..f0a463ad --- /dev/null +++ b/src/drivers/msi_claw/mod.rs @@ -0,0 +1,16 @@ +pub mod driver; +pub mod event; +pub mod hid_report; + +// Hardware ID's +pub const VID: u16 = 0x0db0; +pub const PID: u16 = 0x1902; + +// Input report sizes +const INPUT_PACKET_SIZE: usize = 64; + +// Input report axis ranges +pub const AXIS_MAX: f64 = 255.0; + +// Timeouts +const HID_TIMEOUT: i32 = 10; diff --git a/src/input/source/hidraw.rs b/src/input/source/hidraw.rs index 92a7b003..5c8b1f44 100644 --- a/src/input/source/hidraw.rs +++ b/src/input/source/hidraw.rs @@ -10,6 +10,7 @@ pub mod legion_go2; pub mod legos_imu; pub mod legos_touchpad; pub mod legos_xinput; +pub mod msi_claw; pub mod opineo; pub mod oxp_hid; pub mod rog_ally; @@ -23,11 +24,13 @@ use std::{error::Error, time::Duration}; use crate::{ config, constants::BUS_SOURCES_PREFIX, - drivers, + drivers::{self}, input::{ - capability::Capability, composite_device::client::CompositeDeviceClient, - info::DeviceInfoRef, output_capability::OutputCapability, - source::hidraw::ultimate_2::Ultimate2, + capability::Capability, + composite_device::client::CompositeDeviceClient, + info::DeviceInfoRef, + output_capability::OutputCapability, + source::hidraw::{msi_claw::MsiClawController, ultimate_2::Ultimate2}, }, udev::device::UdevDevice, }; @@ -56,6 +59,7 @@ enum DriverType { LegionGoSImu, LegionGoSTouchpad, LegionGoSXInput, + MsiClaw, OrangePiNeo, OxpHid, RogAlly, @@ -81,6 +85,7 @@ pub enum HidRawDevice { LegionGoSImu(SourceDriver), LegionGoSTouchpad(SourceDriver), LegionGoSXInput(SourceDriver), + MsiClawController(SourceDriver), OrangePiNeo(SourceDriver), OxpHid(SourceDriver), RogAlly(SourceDriver), @@ -105,6 +110,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::LegionGoSImu(source_driver) => source_driver.info_ref(), HidRawDevice::LegionGoSTouchpad(source_driver) => source_driver.info_ref(), HidRawDevice::LegionGoSXInput(source_driver) => source_driver.info_ref(), + HidRawDevice::MsiClawController(source_driver) => source_driver.info_ref(), HidRawDevice::OrangePiNeo(source_driver) => source_driver.info_ref(), HidRawDevice::OxpHid(source_driver) => source_driver.info_ref(), HidRawDevice::RogAlly(source_driver) => source_driver.info_ref(), @@ -129,6 +135,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::LegionGoSImu(source_driver) => source_driver.get_id(), HidRawDevice::LegionGoSTouchpad(source_driver) => source_driver.get_id(), HidRawDevice::LegionGoSXInput(source_driver) => source_driver.get_id(), + HidRawDevice::MsiClawController(source_driver) => source_driver.get_id(), HidRawDevice::OrangePiNeo(source_driver) => source_driver.get_id(), HidRawDevice::OxpHid(source_driver) => source_driver.get_id(), HidRawDevice::RogAlly(source_driver) => source_driver.get_id(), @@ -153,6 +160,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::LegionGoSImu(source_driver) => source_driver.client(), HidRawDevice::LegionGoSTouchpad(source_driver) => source_driver.client(), HidRawDevice::LegionGoSXInput(source_driver) => source_driver.client(), + HidRawDevice::MsiClawController(source_driver) => source_driver.client(), HidRawDevice::OrangePiNeo(source_driver) => source_driver.client(), HidRawDevice::OxpHid(source_driver) => source_driver.client(), HidRawDevice::RogAlly(source_driver) => source_driver.client(), @@ -177,6 +185,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::LegionGoSImu(source_driver) => source_driver.run().await, HidRawDevice::LegionGoSTouchpad(source_driver) => source_driver.run().await, HidRawDevice::LegionGoSXInput(source_driver) => source_driver.run().await, + HidRawDevice::MsiClawController(source_driver) => source_driver.run().await, HidRawDevice::OrangePiNeo(source_driver) => source_driver.run().await, HidRawDevice::OxpHid(source_driver) => source_driver.run().await, HidRawDevice::RogAlly(source_driver) => source_driver.run().await, @@ -203,6 +212,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::LegionGoSImu(source_driver) => source_driver.get_capabilities(), HidRawDevice::LegionGoSTouchpad(source_driver) => source_driver.get_capabilities(), HidRawDevice::LegionGoSXInput(source_driver) => source_driver.get_capabilities(), + HidRawDevice::MsiClawController(source_driver) => source_driver.get_capabilities(), HidRawDevice::OrangePiNeo(source_driver) => source_driver.get_capabilities(), HidRawDevice::OxpHid(source_driver) => source_driver.get_capabilities(), HidRawDevice::RogAlly(source_driver) => source_driver.get_capabilities(), @@ -235,6 +245,9 @@ impl SourceDeviceCompatible for HidRawDevice { source_driver.get_output_capabilities() } HidRawDevice::LegionGoSXInput(source_driver) => source_driver.get_output_capabilities(), + HidRawDevice::MsiClawController(source_driver) => { + source_driver.get_output_capabilities() + } HidRawDevice::OrangePiNeo(source_driver) => source_driver.get_output_capabilities(), HidRawDevice::OxpHid(source_driver) => source_driver.get_output_capabilities(), HidRawDevice::RogAlly(source_driver) => source_driver.get_output_capabilities(), @@ -259,6 +272,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::LegionGoSImu(source_driver) => source_driver.get_device_path(), HidRawDevice::LegionGoSTouchpad(source_driver) => source_driver.get_device_path(), HidRawDevice::LegionGoSXInput(source_driver) => source_driver.get_device_path(), + HidRawDevice::MsiClawController(source_driver) => source_driver.get_device_path(), HidRawDevice::OrangePiNeo(source_driver) => source_driver.get_device_path(), HidRawDevice::OxpHid(source_driver) => source_driver.get_device_path(), HidRawDevice::RogAlly(source_driver) => source_driver.get_device_path(), @@ -387,6 +401,12 @@ impl HidRawDevice { ); Ok(Self::LegionGoSXInput(source_device)) } + DriverType::MsiClaw => { + let device = MsiClawController::new(device_info.clone())?; + let source_device = + SourceDriver::new(composite_device, device, device_info.into(), conf); + Ok(Self::MsiClawController(source_device)) + } DriverType::OrangePiNeo => { let device = OrangePiNeoTouchpad::new(device_info.clone())?; let source_device = @@ -553,6 +573,13 @@ impl HidRawDevice { return DriverType::LegionGoSXInput; } + // MSI Claw Dinput + if vid == drivers::msi_claw::VID && pid == drivers::msi_claw::PID { + log::info!("Detected MSI Claw"); + + return DriverType::MsiClaw; + } + // OrangePi NEO if vid == drivers::opineo::driver::VID && pid == drivers::opineo::driver::PID { log::info!("Detected OrangePi NEO"); diff --git a/src/input/source/hidraw/msi_claw.rs b/src/input/source/hidraw/msi_claw.rs new file mode 100644 index 00000000..b78a71e1 --- /dev/null +++ b/src/input/source/hidraw/msi_claw.rs @@ -0,0 +1,373 @@ +use std::{collections::HashMap, error::Error, fmt::Debug}; + +use evdev::{FFEffectData, FFEffectKind, InputEvent}; +use packed_struct::{types::SizedInteger, PrimitiveEnum}; + +use crate::{ + drivers::{ + dualsense::hid_report::SetStatePackedOutputData, + msi_claw::{driver::Driver, event, AXIS_MAX}, + steam_deck::hid_report::{PackedHapticReport, PadSide}, + }, + input::{ + capability::{Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger}, + event::{ + native::NativeEvent, + value::{normalize_signed_value, normalize_unsigned_value, InputValue}, + }, + output_capability::{Haptic, OutputCapability}, + output_event::OutputEvent, + source::{InputError, OutputError, SourceInputDevice, SourceOutputDevice}, + }, + udev::device::UdevDevice, +}; + +/// MSI ClawController source device implementation +pub struct MsiClawController { + driver: Driver, + ff_evdev_effects: HashMap, +} + +impl MsiClawController { + /// Create a new MSI Claw controller source device with the given udev + /// device information + pub fn new(device_info: UdevDevice) -> Result> { + let driver = Driver::new(device_info.devnode())?; + Ok(Self { + driver, + ff_evdev_effects: HashMap::new(), + }) + } + + /// Process the given evdev force feedback event. + fn process_evdev_ff(&mut self, input_event: InputEvent) -> Result<(), Box> { + // Get the code (effect id) and value of the event + let (code, value) = + if let evdev::EventSummary::ForceFeedback(_, code, value) = input_event.destructure() { + (code, value) + } else { + log::debug!("Unhandled evdev output event: {:?}", input_event); + return Ok(()); + }; + + // Find the effect data for this event + let effect_id = code.0 as i16; + let Some(effect_data) = self.ff_evdev_effects.get(&effect_id) else { + log::warn!("No effect id found: {}", code.0); + return Ok(()); + }; + + // The value determines if the effect should be playing or not. + if value == 0 { + if let Err(e) = self.driver.haptic_rumble(0, 0) { + log::debug!("Failed to stop haptic rumble: {:?}", e); + return Ok(()); + } + } + + // Perform the rumble based on the effect + // TODO: handle effect duration, etc. + match effect_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 { + strong_magnitude, + weak_magnitude, + } => { + // Set rumble values based on the effect data + let left_speed = (strong_magnitude / 256) as u8; + let right_speed = (weak_magnitude / 256) as u8; + + // Do rumble + if let Err(e) = self.driver.haptic_rumble(left_speed, right_speed) { + let err = format!("Failed to do haptic rumble: {e:?}"); + return Err(err.into()); + } + } + } + + Ok(()) + } + + /// Process dualsense force feedback output reports + fn process_dualsense_ff( + &mut self, + report: SetStatePackedOutputData, + ) -> Result<(), Box> { + // Set the rumble values based on the DualSense output report + let left_speed = report.rumble_emulation_left; + let right_speed = report.rumble_emulation_right; + + if let Err(e) = self.driver.haptic_rumble(left_speed, right_speed) { + let err = format!("Failed to do haptic rumble: {e:?}"); + return Err(err.into()); + } + + Ok(()) + } + + fn process_haptic_ff( + &self, + report: PackedHapticReport, + ) -> Result<(), Box> { + let intensity = report.intensity.to_primitive() + 1; + let scaled_gain = (report.gain + 24) as u8 * intensity; + let normalized_gain = normalize_unsigned_value(scaled_gain as f64, 150.0); + let new_gain = normalized_gain * u8::MAX as f64; + let new_gain = new_gain as u8; + + match report.side { + PadSide::Left => self.driver.haptic_rumble(new_gain, 0)?, + PadSide::Right => self.driver.haptic_rumble(0, new_gain)?, + PadSide::Both => self.driver.haptic_rumble(new_gain, new_gain)?, + } + + Ok(()) + } +} + +impl SourceInputDevice for MsiClawController { + /// Poll the source device for input events + fn poll(&mut self) -> Result, InputError> { + let events = self.driver.poll()?; + let native_events = translate_events(events); + Ok(native_events) + } + + /// Returns the possible input events this device is capable of emitting + fn get_capabilities(&self) -> Result, InputError> { + Ok(CAPABILITIES.into()) + } +} + +impl SourceOutputDevice for MsiClawController { + /// Returns the possible output events this device is capable of handling + fn get_output_capabilities(&self) -> Result, OutputError> { + let capabilities = vec![ + OutputCapability::ForceFeedback, + OutputCapability::ForceFeedbackUpload, + OutputCapability::ForceFeedbackErase, + OutputCapability::Haptics(Haptic::TrackpadLeft), + OutputCapability::Haptics(Haptic::TrackpadRight), + //OutputCapability::LED(LED::Color), + //OutputCapability::LED(LED::Brightness), + ]; + Ok(capabilities) + } + /// Write the given output event to the source device. Output events are + /// events that flow from an application (like a game) to the physical + /// input device, such as force feedback events. + fn write_event(&mut self, event: OutputEvent) -> Result<(), OutputError> { + log::trace!("Received output event: {:?}", event); + match event { + OutputEvent::Evdev(input_event) => { + self.process_evdev_ff(input_event)?; + } + OutputEvent::DualSense(report) => { + log::debug!("Received DualSense output report"); + if report.use_rumble_not_haptics || report.enable_improved_rumble_emulation { + self.process_dualsense_ff(report)?; + } + } + OutputEvent::Uinput(_) => (), + OutputEvent::SteamDeckHaptics(report) => self.process_haptic_ff(report)?, + OutputEvent::SteamDeckRumble(report) => { + let l_speed = (report.left_speed.to_primitive() / 256) as u8; + let r_speed = (report.right_speed.to_primitive() / 256) as u8; + self.driver.haptic_rumble(l_speed, r_speed)?; + } + OutputEvent::GenericRumble { + weak_magnitude, + strong_magnitude, + } => { + let left_speed = (strong_magnitude / 256) as u8; + let right_speed = (weak_magnitude / 256) as u8; + self.driver.haptic_rumble(left_speed, right_speed)?; + } + } + + Ok(()) + } +} + +impl Debug for MsiClawController { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MSI ClawSController").finish() + } +} + +/// Normalize the value to something between -1.0 and 1.0 based on the Deck's +/// minimum and maximum axis ranges. +fn normalize_axis_value(event: event::AxisEvent) -> InputValue { + match event { + event::AxisEvent::LStick(value) => { + let max = AXIS_MAX; + let min = 0.0; + + let x = normalize_signed_value(value.x as f64, min, max); + let x = Some(x); + + let y = normalize_signed_value(value.y as f64, min, max); + let y = Some(y); + + InputValue::Vector2 { x, y } + } + event::AxisEvent::RStick(value) => { + let max = AXIS_MAX; + let min = 0.0; + + let x = normalize_signed_value(value.x as f64, min, max); + let x = Some(x); + + let y = normalize_signed_value(value.y as f64, min, max); + let y = Some(y); + + InputValue::Vector2 { x, y } + } + } +} + +/// Normalize the trigger value to something between 0.0 and 1.0 based on the +/// MSI Claw Go's maximum axis ranges. +fn normalize_trigger_value(event: event::TriggerEvent) -> InputValue { + match event { + event::TriggerEvent::ATriggerL(value) => { + let max = AXIS_MAX; + InputValue::Float(normalize_unsigned_value(value.value as f64, max)) + } + event::TriggerEvent::ATriggerR(value) => { + let max = AXIS_MAX; + InputValue::Float(normalize_unsigned_value(value.value as f64, max)) + } + } +} + +/// Translate the given MSI Clawevents into native events +fn translate_events(events: Vec) -> Vec { + events.into_iter().map(translate_event).collect() +} + +/// Translate the given MSI Clawevent into a native event +fn translate_event(event: event::Event) -> NativeEvent { + match event { + event::Event::Button(button) => match button { + event::ButtonEvent::A(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::South)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::X(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::North)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::B(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::East)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::Y(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::West)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::Menu(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::Start)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::View(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::Select)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::DPadDown(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadDown)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::DPadUp(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadUp)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::DPadLeft(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadLeft)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::DPadRight(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadRight)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::LB(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftBumper)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::ThumbL(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftStick)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::RB(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::RightBumper)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::ThumbR(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::RightStick)), + InputValue::Bool(value.pressed), + ), + }, + event::Event::Axis(axis) => match axis.clone() { + event::AxisEvent::LStick(_) => NativeEvent::new( + Capability::Gamepad(Gamepad::Axis(GamepadAxis::LeftStick)), + normalize_axis_value(axis), + ), + event::AxisEvent::RStick(_) => NativeEvent::new( + Capability::Gamepad(Gamepad::Axis(GamepadAxis::RightStick)), + normalize_axis_value(axis), + ), + }, + event::Event::Trigger(trigg) => match trigg.clone() { + event::TriggerEvent::ATriggerL(_) => NativeEvent::new( + Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTrigger)), + normalize_trigger_value(trigg), + ), + event::TriggerEvent::ATriggerR(_) => NativeEvent::new( + Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTrigger)), + normalize_trigger_value(trigg), + ), + }, + } +} + +/// List of all capabilities that the MSI Clawdriver implements +pub const CAPABILITIES: &[Capability] = &[ + Capability::Gamepad(Gamepad::Axis(GamepadAxis::LeftStick)), + Capability::Gamepad(Gamepad::Axis(GamepadAxis::RightStick)), + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadDown)), + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadLeft)), + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadRight)), + Capability::Gamepad(Gamepad::Button(GamepadButton::DPadUp)), + Capability::Gamepad(Gamepad::Button(GamepadButton::East)), + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftBumper)), + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftStick)), + Capability::Gamepad(Gamepad::Button(GamepadButton::North)), + Capability::Gamepad(Gamepad::Button(GamepadButton::RightBumper)), + Capability::Gamepad(Gamepad::Button(GamepadButton::RightStick)), + Capability::Gamepad(Gamepad::Button(GamepadButton::Select)), + Capability::Gamepad(Gamepad::Button(GamepadButton::South)), + Capability::Gamepad(Gamepad::Button(GamepadButton::Start)), + Capability::Gamepad(Gamepad::Button(GamepadButton::West)), + Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTrigger)), + Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTrigger)), +];