diff --git a/rootfs/usr/share/inputplumber/devices/60-8bit_do_ultimate_2.yaml b/rootfs/usr/share/inputplumber/devices/60-8bit_do_ultimate_2.yaml new file mode 100644 index 00000000..d5aec4b5 --- /dev/null +++ b/rootfs/usr/share/inputplumber/devices/60-8bit_do_ultimate_2.yaml @@ -0,0 +1,44 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/ShadowBlip/InputPlumber/main/rootfs/usr/share/inputplumber/schema/composite_device_v1.json +# Schema version number +version: 1 + +# The type of configuration schema +kind: CompositeDevice + +# Name of the composite device mapping +name: 8BitDo Ultimate 2 + +# Only use this profile if *any* of the given matches matches. If this list is +# empty, then the source devices will *always* be checked. +# /sys/class/dmi/id/product_name +matches: [] + +# Only allow a single source device per composite device of this type. +single_source: false + +# Maximum number of source devices per CompositeDevice. +maximum_sources: 2 + +# One or more source devices to combine into a single virtual device. The events +# from these devices will be watched and translated according to the key map. +# Only grabs dinput, xinput is handled by 65-generic-gamepad.yaml +source_devices: + - group: gamepad + blocked: true + evdev: + name: "{8BitDo Ultimate 2 Wireless,8BitDo Ultimate 2 Wireless Controller,8BitDo 8BitDo Ultimate 2 Wireless Controller for PC}" + vendor_id: "2dc8" + product_id: "6012" + capability_map_id: dinput_generic + + - group: gamepad + hidraw: + vendor_id: 0x2dc8 + product_id: 0x6012 + interface_num: 0 + +# The target input device(s) to emulate by default +target_devices: + - 8bitdo-u2 + - mouse + - keyboard diff --git a/rootfs/usr/share/inputplumber/schema/composite_device_v1.json b/rootfs/usr/share/inputplumber/schema/composite_device_v1.json index 1e66fe7a..e601c27f 100644 --- a/rootfs/usr/share/inputplumber/schema/composite_device_v1.json +++ b/rootfs/usr/share/inputplumber/schema/composite_device_v1.json @@ -51,7 +51,7 @@ "$ref": "#/definitions/Options" }, "target_devices": { - "description": "Target input device(s) to emulate by default. Can be one of ['mouse', 'keyboard', 'gamepad', 'xb360', 'xbox-elite', 'xbox-series', 'deck', 'ds5', 'ds5-edge', 'touchscreen', 'touchpad'].", + "description": "Target input device(s) to emulate by default. Can be one of ['mouse', 'keyboard', 'gamepad', 'xb360', 'xbox-elite', 'xbox-series', 'deck', 'ds5', 'ds5-edge', 'touchscreen', 'touchpad', '8bitdo-u2'].", "type": "array", "items": { "type": "string", diff --git a/src/drivers/lego/hid_report.rs b/src/drivers/lego/hid_report.rs index efb5025c..f00e266e 100644 --- a/src/drivers/lego/hid_report.rs +++ b/src/drivers/lego/hid_report.rs @@ -25,7 +25,6 @@ impl ReportType { #[derive(PrimitiveEnum_u8, Clone, Copy, PartialEq, Debug, Default)] pub enum DPadDirection { - #[default] Up = 0, UpRight = 1, Right = 2, @@ -34,6 +33,7 @@ pub enum DPadDirection { DownLeft = 5, Left = 6, UpLeft = 7, + #[default] None = 8, } diff --git a/src/drivers/mod.rs b/src/drivers/mod.rs index 4f058b3d..7a8477ec 100644 --- a/src/drivers/mod.rs +++ b/src/drivers/mod.rs @@ -11,7 +11,7 @@ pub mod oxp_hid; pub mod oxp_tty; pub mod rog_ally; pub mod steam_deck; -pub mod ultimate2_wireless; +pub mod ultimate_2; pub mod unified_gamepad; pub mod xpad_uhid; pub mod zotac_zone; diff --git a/src/drivers/ultimate2_wireless/mod.rs b/src/drivers/ultimate2_wireless/mod.rs deleted file mode 100644 index d0a3a021..00000000 --- a/src/drivers/ultimate2_wireless/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod driver; -pub mod hid_report; -pub mod report_descriptor; diff --git a/src/drivers/ultimate_2/driver.rs b/src/drivers/ultimate_2/driver.rs new file mode 100644 index 00000000..4c4e5150 --- /dev/null +++ b/src/drivers/ultimate_2/driver.rs @@ -0,0 +1,347 @@ +use core::mem::size_of; +use std::{error::Error, ffi::CString}; + +use hidapi::HidDevice; +use packed_struct::PackedStruct; + +use crate::{ + drivers::ultimate_2::{ + event::{InertialEvent, InertialInput}, + hid_report::{DPadDirection, PackedInputDataReport, PackedRumbleOutputReport}, + PID, REPORT_ID_INPUT, REPORT_ID_RUMBLE, VID, + }, + udev::device::UdevDevice, +}; + +use super::event::{ + AxisEvent, BinaryInput, ButtonEvent, Event, JoyAxisInput, TriggerEvent, TriggerInput, +}; + +// Input report size +const PACKET_SIZE: usize = 34; + +// HID buffer read timeout +const HID_TIMEOUT: i32 = 10; + +#[derive(Debug, Clone, Default)] +struct DPadState { + up: bool, + down: bool, + left: bool, + right: bool, +} + +pub struct Driver { + /// HIDRAW device instance + device: HidDevice, + /// State for the device + state: Option, + /// Last DPad state + dpad: DPadState, +} + +impl Driver { + pub fn new(udev_device: UdevDevice) -> Result> { + let fmtpath = udev_device.devnode().clone(); + let path = CString::new(fmtpath.clone())?; + let api = hidapi::HidApi::new()?; + let hid_device = api.open_path(&path)?; + let info = hid_device.get_device_info()?; + + if info.vendor_id() != VID || info.product_id() != PID { + return Err( + format!("Device '{fmtpath}' is not an 8BitDo Ultimate 2 Controller").into(), + ); + } + + Ok(Self { + device: hid_device, + state: None, + dpad: Default::default(), + }) + } + + /// Rumble the gamepad + pub fn rumble(&self, strong: u8, weak: u8) -> Result<(), Box> { + let state = PackedRumbleOutputReport { + report_id: REPORT_ID_RUMBLE, + strong_magnitude: strong, + weak_magnitude: weak, + _padding_3: Default::default(), + _padding_4: Default::default(), + }; + + let buf = state.pack()?; + let bytes_written = self.device.write(&buf)?; + if bytes_written != size_of::() { + return Err("Failed to write rumble report".to_string().into()); + } + Ok(()) + } + + /// 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; PACKET_SIZE]; + let bytes_read = self.device.read_timeout(&mut buf[..], HID_TIMEOUT)?; + + let report_id = buf[0]; + let slice = &buf[..bytes_read]; + log::debug!("Got Report ID: {report_id}"); + log::debug!("Got Report Size: {bytes_read}"); + + let events = match report_id { + REPORT_ID_INPUT => { + log::trace!("Got input data."); + if bytes_read != PACKET_SIZE { + return Err("Invalid packet size for input data.".into()); + } + // Handle the incoming input report + let sized_buf = slice.try_into()?; + log::debug!("Got Report Raw Data: {:?}", sized_buf); + + self.handle_input_report(sized_buf)? + } + _ => { + log::debug!("Invalid Report ID."); + let events = vec![]; + events + } + }; + + Ok(events) + } + + /// Unpacks the buffer into a [DataReport] structure and updates + /// the internal state + fn handle_input_report( + &mut self, + buf: [u8; PACKET_SIZE], + ) -> Result, Box> { + let input_report = PackedInputDataReport::unpack(&buf)?; + + // Print input report for debugging + log::debug!("--- Input report ---"); + log::debug!("{input_report}"); + log::debug!("---- End Report ----"); + + // Update the state + let old_dinput_state = self.update_state(input_report); + + // Translate the state into a stream of input events + let events = self.translate_events(old_dinput_state); + + Ok(events) + } + + /// Update touchinput state + fn update_state( + &mut self, + input_report: PackedInputDataReport, + ) -> Option { + let old_state = self.state; + self.state = Some(input_report); + old_state + } + + /// Translate the state into individual events + fn translate_events(&mut self, old_state: Option) -> Vec { + let mut events = Vec::new(); + let Some(state) = self.state.as_ref() else { + return events; + }; + + let Some(old_state) = old_state else { + return events; + }; + + // Translate state changes into events if they have changed + // 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_view != old_state.button_view { + events.push(Event::Button(ButtonEvent::View(BinaryInput { + pressed: state.button_view, + }))); + } + if state.button_menu != old_state.button_menu { + events.push(Event::Button(ButtonEvent::Menu(BinaryInput { + pressed: state.button_menu, + }))); + } + if state.button_guide != old_state.button_guide { + events.push(Event::Button(ButtonEvent::Guide(BinaryInput { + pressed: state.button_guide, + }))); + } + if state.button_r1 != old_state.button_r1 { + events.push(Event::Button(ButtonEvent::R1(BinaryInput { + pressed: state.button_r1, + }))); + } + if state.button_l1 != old_state.button_l1 { + events.push(Event::Button(ButtonEvent::L1(BinaryInput { + pressed: state.button_l1, + }))); + } + if state.button_r2 != old_state.button_r2 { + events.push(Event::Button(ButtonEvent::R2(BinaryInput { + pressed: state.button_r2, + }))); + } + if state.button_l2 != old_state.button_l2 { + events.push(Event::Button(ButtonEvent::L2(BinaryInput { + pressed: state.button_l2, + }))); + } + if state.button_r3 != old_state.button_r3 { + events.push(Event::Button(ButtonEvent::R3(BinaryInput { + pressed: state.button_r3, + }))); + } + if state.button_l3 != old_state.button_l3 { + events.push(Event::Button(ButtonEvent::L3(BinaryInput { + pressed: state.button_l3, + }))); + } + if state.button_l4 != old_state.button_l4 { + events.push(Event::Button(ButtonEvent::L4(BinaryInput { + pressed: state.button_l4, + }))); + } + if state.button_r4 != old_state.button_r4 { + events.push(Event::Button(ButtonEvent::R4(BinaryInput { + pressed: state.button_r4, + }))); + } + if state.dpad_state != old_state.dpad_state { + let up = [ + DPadDirection::Up, + DPadDirection::UpRight, + DPadDirection::UpLeft, + ] + .contains(&state.dpad_state); + let down = [ + DPadDirection::Down, + DPadDirection::DownRight, + DPadDirection::DownLeft, + ] + .contains(&state.dpad_state); + let left = [ + DPadDirection::Left, + DPadDirection::DownLeft, + DPadDirection::UpLeft, + ] + .contains(&state.dpad_state); + let right = [ + DPadDirection::Right, + DPadDirection::DownRight, + DPadDirection::UpRight, + ] + .contains(&state.dpad_state); + let dpad_state = DPadState { + up, + down, + left, + right, + }; + + if up != self.dpad.up { + events.push(Event::Button(ButtonEvent::DPadUp(BinaryInput { + pressed: up, + }))); + } + if down != self.dpad.down { + events.push(Event::Button(ButtonEvent::DPadDown(BinaryInput { + pressed: down, + }))); + } + if left != self.dpad.left { + events.push(Event::Button(ButtonEvent::DPadLeft(BinaryInput { + pressed: left, + }))); + } + if right != self.dpad.right { + events.push(Event::Button(ButtonEvent::DPadRight(BinaryInput { + pressed: right, + }))); + } + + self.dpad = dpad_state; + } + + // Axis events + if state.joystick_l_x != old_state.joystick_l_x + || state.joystick_l_y != old_state.joystick_l_y + { + events.push(Event::Axis(AxisEvent::LStick(JoyAxisInput { + x: state.joystick_l_x, + y: state.joystick_l_y, + }))); + } + if state.joystick_r_x != old_state.joystick_r_x + || state.joystick_r_y != old_state.joystick_r_y + { + events.push(Event::Axis(AxisEvent::RStick(JoyAxisInput { + x: state.joystick_r_x, + y: state.joystick_r_y, + }))); + } + + if state.trigger_l != old_state.trigger_l { + events.push(Event::Trigger(TriggerEvent::TriggerL(TriggerInput { + value: state.trigger_l, + }))); + } + if state.trigger_l != old_state.trigger_r { + events.push(Event::Trigger(TriggerEvent::TriggerR(TriggerInput { + value: state.trigger_r, + }))); + } + + if state.accel_x != old_state.accel_x + || state.accel_y != old_state.accel_y + || state.accel_z != old_state.accel_z + { + events.push(Event::Inertia(InertialEvent::Accelerometer( + InertialInput { + x: i16::from(state.accel_x), + y: i16::from(state.accel_y), + z: i16::from(state.accel_z), + }, + ))) + }; + + if state.gyro_x != old_state.gyro_x + || state.gyro_y != old_state.gyro_y + || state.gyro_z != old_state.gyro_z + { + events.push(Event::Inertia(InertialEvent::Gyro(InertialInput { + x: i16::from(state.gyro_x), + y: i16::from(state.gyro_y), + z: i16::from(state.gyro_z), + }))) + }; + + log::trace!("Got events: {events:?}"); + events + } +} diff --git a/src/drivers/ultimate_2/event.rs b/src/drivers/ultimate_2/event.rs new file mode 100644 index 00000000..043d1770 --- /dev/null +++ b/src/drivers/ultimate_2/event.rs @@ -0,0 +1,99 @@ +/// Events that can be emitted by the controller +#[derive(Clone, Debug)] +pub enum Event { + Button(ButtonEvent), + Axis(AxisEvent), + Inertia(InertialEvent), + Trigger(TriggerEvent), +} + +/// Binary input contain either pressed or unpressed +#[derive(Clone, Debug)] +pub struct BinaryInput { + pub pressed: bool, +} + +/// Axis input contain (x, y) coordinates +#[derive(Clone, Debug)] +pub struct JoyAxisInput { + pub x: u8, + pub y: u8, +} + +/// Trigger input contains non-negative integars +#[derive(Clone, Debug)] +pub struct TriggerInput { + pub value: u8, +} + +/// 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), + /// Left shoulder button + L1(BinaryInput), + /// Right shoulder button + R1(BinaryInput), + /// View ⧉ button + View(BinaryInput), + /// Menu (☰) button + Menu(BinaryInput), + /// Guide button + Guide(BinaryInput), + /// Z-axis button on the left stick + L2(BinaryInput), + /// Z-axis button on the right stick + R2(BinaryInput), + /// DPad up + DPadUp(BinaryInput), + /// DPad right + DPadRight(BinaryInput), + /// DPad down + DPadDown(BinaryInput), + /// DPad left + DPadLeft(BinaryInput), + /// Paddle button on the left side + L3(BinaryInput), + /// Paddle button on the right side + R3(BinaryInput), + /// Small shoulder button on the left side + L4(BinaryInput), + /// Small shoulder button on the right side + R4(BinaryInput), +} + +/// Axis events are events that have (x, y) values +#[derive(Clone, Debug)] +pub enum AxisEvent { + LStick(JoyAxisInput), + RStick(JoyAxisInput), +} + +/// Trigger events contain values indicating how far a trigger is pulled +#[derive(Clone, Debug)] +pub enum TriggerEvent { + TriggerL(TriggerInput), + TriggerR(TriggerInput), +} + +/// [InertialInput] represents the state of the IMU (x, y, z) values +#[derive(Clone, Debug)] +pub struct InertialInput { + pub x: i16, + pub y: i16, + pub z: i16, +} + +/// [InertialEvent] has data from the IMU +#[derive(Clone, Debug)] +pub enum InertialEvent { + Accelerometer(InertialInput), + Gyro(InertialInput), +} diff --git a/src/drivers/ultimate2_wireless/hid_report.rs b/src/drivers/ultimate_2/hid_report.rs similarity index 51% rename from src/drivers/ultimate2_wireless/hid_report.rs rename to src/drivers/ultimate_2/hid_report.rs index 05f75a72..af5353f1 100644 --- a/src/drivers/ultimate2_wireless/hid_report.rs +++ b/src/drivers/ultimate_2/hid_report.rs @@ -28,10 +28,36 @@ use packed_struct::prelude::*; -use super::driver::{REPORT_ID_INPUT, REPORT_ID_RUMBLE}; +/// 8BitDo Ultimate 2 Wireless rumble output report (5 bytes). +#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)] +#[packed_struct(bit_numbering = "msb0", size_bytes = "5")] +pub struct PackedRumbleOutputReport { + #[packed_field(bytes = "0")] + pub report_id: u8, + #[packed_field(bytes = "1")] + pub strong_magnitude: u8, + #[packed_field(bytes = "2")] + pub weak_magnitude: u8, + #[packed_field(bytes = "3")] + pub _padding_3: u8, + #[packed_field(bytes = "4")] + pub _padding_4: u8, +} + +impl Default for PackedRumbleOutputReport { + fn default() -> Self { + Self { + report_id: super::REPORT_ID_RUMBLE, + strong_magnitude: 0, + weak_magnitude: 0, + _padding_3: 0, + _padding_4: 0, + } + } +} #[derive(PrimitiveEnum_u8, Clone, Copy, PartialEq, Debug, Default)] -pub enum DpadDirection { +pub enum DPadDirection { Up = 0, UpRight = 1, Right = 2, @@ -41,76 +67,56 @@ pub enum DpadDirection { Left = 6, UpLeft = 7, #[default] - None = 8, + None = 15, } -impl DpadDirection { - fn as_bitflag(&self) -> u8 { +impl DPadDirection { + pub fn as_bitflag(&self) -> u8 { match *self { - Self::Up => 0b0001, - Self::UpRight => 0b0011, - Self::Right => 0b0010, - Self::DownRight => 0b0110, - Self::Down => 0b0100, - Self::DownLeft => 0b1100, - Self::Left => 0b1000, - Self::UpLeft => 0b1001, - Self::None => 0b0000, + Self::Up => 0x01, // 00000001 + Self::UpRight => 0x03, // 00000011 + Self::Right => 0x02, // 00000010 + Self::DownRight => 0x06, // 00000110 + Self::Down => 0x04, // 00000100 + Self::DownLeft => 0x0c, // 00001100 + Self::Left => 0x08, // 00001000 + Self::UpLeft => 0x09, // 00001001 + Self::None => 0x0f, // 00000000 } } fn from_bitflag(bits: u8) -> Self { match bits { - 0b0001 => Self::Up, - 0b0011 => Self::UpRight, - 0b0010 => Self::Right, - 0b0110 => Self::DownRight, - 0b0100 => Self::Down, - 0b1100 => Self::DownLeft, - 0b1000 => Self::Left, - 0b1001 => Self::UpLeft, + 0x01 => Self::Up, + 0x03 => Self::UpRight, + 0x02 => Self::Right, + 0x06 => Self::DownRight, + 0x04 => Self::Down, + 0x0c => Self::DownLeft, + 0x08 => Self::Left, + 0x09 => Self::UpLeft, _ => Self::None, } } - pub fn change(&self, direction: DpadDirection, pressed: bool) -> DpadDirection { + pub fn change(&self, direction: DPadDirection, pressed: bool) -> DPadDirection { let old = self.as_bitflag(); let new = if pressed { old | direction.as_bitflag() } else { old & !direction.as_bitflag() }; - DpadDirection::from_bitflag(new) + DPadDirection::from_bitflag(new) } } -/// 8BitDo Ultimate 2 Wireless rumble output report (5 bytes). -#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)] -#[packed_struct(bit_numbering = "msb0", size_bytes = "5")] -pub struct PackedRumbleOutputReport { - #[packed_field(bytes = "0")] - pub report_id: u8, - #[packed_field(bytes = "1")] - pub strong_magnitude: u8, - #[packed_field(bytes = "2")] - pub weak_magnitude: u8, - #[packed_field(bytes = "3")] - pub _padding_3: u8, - #[packed_field(bytes = "4")] - pub _padding_4: u8, -} +// Thumb L +// # ReportID: 1 / Hat switch: 15 | # | X: 127 | Y: 127 | Z: 127 | Rz: 127 | Accelerator: 0 | Brake: 0 | Button: 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 | 0xff000020: 0 , 0 , 0 , 47 , 47 , 15 , 157 , 255 , 169 , 251 , 2 , 0 , 29 , 0 , 250 , 255 , 0 , 0 , 0 , 0 , 0 , 0 , 0 +// E: 000138.814799 34 01 0f 7f 7f 7f 7f 00 00 00 20 00 00 00 00 2f 2f 0f 9d ff a9 fb 02 00 1d 00 fa ff 00 00 00 00 00 00 00 -impl Default for PackedRumbleOutputReport { - fn default() -> Self { - Self { - report_id: REPORT_ID_RUMBLE, - strong_magnitude: 0, - weak_magnitude: 0, - _padding_3: 0, - _padding_4: 0, - } - } -} +// Thumb R +// # ReportID: 1 / Hat switch: 15 | # | X: 127 | Y: 127 | Z: 127 | Rz: 127 | Accelerator: 0 | Brake: 0 | Button: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 | 0xff000020: 0 , 0 , 0 , 47 , 5 , 14 , 196 , 255 , 13 , 8 , 239 , 255 , 239 , 255 , 38 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 +// E: 000006.499840 34 01 0f 7f 7f 7f 7f 00 00 00 40 00 00 00 00 2f 05 0e c4 ff 0d 08 ef ff ef ff 26 00 00 00 00 00 00 00 00 /// 8BitDo Ultimate 2 Wireless DInput input report (34 bytes total). #[derive(PackedStruct, Debug, Copy, Clone, PartialEq)] @@ -120,9 +126,9 @@ pub struct PackedInputDataReport { #[packed_field(bytes = "0")] pub report_id: u8, - // byte 1: D-pad (raw value 0-7 or 8+ for centered) - #[packed_field(bytes = "1")] - pub dpad_raw: u8, + // byte 1: D-pad + #[packed_field(bytes = "1", ty = "enum")] + pub dpad_state: DPadDirection, // bytes 2-5: analog sticks #[packed_field(bytes = "2")] @@ -136,80 +142,70 @@ pub struct PackedInputDataReport { // bytes 6-7: triggers (SDL: [6]=RT, [7]=LT) #[packed_field(bytes = "6")] - pub rt_analog: u8, + pub trigger_r: u8, #[packed_field(bytes = "7")] - pub lt_analog: u8, + pub trigger_l: u8, - // byte 8: buttons group 1 - // data[8]: 0x80=RB 0x40=LB 0x20=PL 0x10=X 0x08=Y 0x04=PR 0x02=B 0x01=A - // (SDL source labels 0x10=Y, 0x08=X, but Steam maps them reversed) #[packed_field(bits = "64")] - pub btn_rb: bool, // mask 0x80 + pub button_r1: bool, #[packed_field(bits = "65")] - pub btn_lb: bool, // mask 0x40 + pub button_l1: bool, #[packed_field(bits = "66")] - pub btn_pl: bool, // mask 0x20 + pub button_l3: bool, #[packed_field(bits = "67")] - pub btn_x: bool, // mask 0x10 + pub button_y: bool, #[packed_field(bits = "68")] - pub btn_y: bool, // mask 0x08 + pub button_x: bool, #[packed_field(bits = "69")] - pub btn_pr: bool, // mask 0x04 + pub button_r3: bool, #[packed_field(bits = "70")] - pub btn_b: bool, // mask 0x02 + pub button_b: bool, #[packed_field(bits = "71")] - pub btn_a: bool, // mask 0x01 + pub button_a: bool, - // byte 9: buttons group 2 - // data[9]: 0x40=R3 0x20=L3 0x10=Guide 0x08=Start 0x04=Select - #[packed_field(bits = "72")] - pub _unused_9_7: bool, // mask 0x80 + //#[packed_field(bits = "72")] + //pub _unused_9_7: bool, #[packed_field(bits = "73")] - pub btn_r3: bool, // mask 0x40 + pub button_r2: bool, #[packed_field(bits = "74")] - pub btn_l3: bool, // mask 0x20 + pub button_l2: bool, #[packed_field(bits = "75")] - pub btn_guide: bool, // mask 0x10 + pub button_guide: bool, #[packed_field(bits = "76")] - pub btn_start: bool, // mask 0x08 + pub button_view: bool, #[packed_field(bits = "77")] - pub btn_select: bool, // mask 0x04 - #[packed_field(bits = "78")] - pub _unused_9_1: bool, // mask 0x02 - #[packed_field(bits = "79")] - pub _unused_9_0: bool, // mask 0x01 + pub button_menu: bool, + //#[packed_field(bits = "78")] + //pub _unknown_9_1: bool, + //#[packed_field(bits = "79")] + //pub _unknown_9_0: bool, - // byte 10: extra back buttons (data[10]: 0x02=R4 0x01=L4) - #[packed_field(bits = "80")] - pub _unused_10_7: bool, - #[packed_field(bits = "81")] - pub _unused_10_6: bool, - #[packed_field(bits = "82")] - pub _unused_10_5: bool, - #[packed_field(bits = "83")] - pub _unused_10_4: bool, - #[packed_field(bits = "84")] - pub _unused_10_3: bool, - #[packed_field(bits = "85")] - pub _unused_10_2: bool, + //#[packed_field(bits = "80")] + //pub _unused_10_7: bool, + //#[packed_field(bits = "81")] + //pub _unused_10_6: bool, + //#[packed_field(bits = "82")] + //pub _unused_10_5: bool, + //#[packed_field(bits = "83")] + //pub _unused_10_4: bool, + //#[packed_field(bits = "84")] + //pub _unused_10_3: bool, + //#[packed_field(bits = "85")] + //pub _unused_10_2: bool, #[packed_field(bits = "86")] - pub btn_r4: bool, // mask 0x02 + pub button_r4: bool, #[packed_field(bits = "87")] - pub btn_l4: bool, // mask 0x01 + pub button_l4: bool, - // bytes 11-13: padding - #[packed_field(bytes = "11")] - pub _padding_11: u8, - #[packed_field(bytes = "12")] - pub _padding_12: u8, - #[packed_field(bytes = "13")] - pub _padding_13: u8, - - // byte 14: battery status (bit7=charging, bits0-6=percent) + //#[packed_field(bytes = "11")] + //pub _padding_11: u8, + //#[packed_field(bytes = "12")] + //pub _padding_12: u8, + //#[packed_field(bytes = "13")] + //pub _padding_13: u8, #[packed_field(bytes = "14")] pub battery: u8, - // bytes 15-26: IMU sensor data (i16 little-endian each) #[packed_field(bytes = "15..=16", endian = "lsb")] pub accel_x: Integer>, #[packed_field(bytes = "17..=18", endian = "lsb")] @@ -223,57 +219,44 @@ pub struct PackedInputDataReport { #[packed_field(bytes = "25..=26", endian = "lsb")] pub gyro_z: Integer>, - // bytes 27-30: timestamp (u32 little-endian, microseconds) #[packed_field(bytes = "27..=30", endian = "lsb")] pub timestamp: Integer>, - - // bytes 31-33: padding - #[packed_field(bytes = "31")] - pub _padding_31: u8, - #[packed_field(bytes = "32")] - pub _padding_32: u8, - #[packed_field(bytes = "33")] - pub _padding_33: u8, + //#[packed_field(bytes = "31")] + //pub _padding_31: u8, + //#[packed_field(bytes = "32")] + //pub _padding_32: u8, + //#[packed_field(bytes = "33")] + //pub _padding_33: u8, } +impl PackedInputDataReport {} + impl Default for PackedInputDataReport { fn default() -> Self { Self { - report_id: REPORT_ID_INPUT, - dpad_raw: 8, // centered + report_id: super::REPORT_ID_INPUT, + dpad_state: Default::default(), joystick_l_x: 0x7f, joystick_l_y: 0x7f, joystick_r_x: 0x7f, joystick_r_y: 0x7f, - rt_analog: 0, - lt_analog: 0, - btn_a: false, - btn_b: false, - btn_pr: false, - btn_x: false, - btn_y: false, - btn_pl: false, - btn_lb: false, - btn_rb: false, - _unused_9_0: false, - _unused_9_1: false, - btn_select: false, - btn_start: false, - btn_guide: false, - btn_l3: false, - btn_r3: false, - _unused_9_7: false, - btn_l4: false, - btn_r4: false, - _unused_10_2: false, - _unused_10_3: false, - _unused_10_4: false, - _unused_10_5: false, - _unused_10_6: false, - _unused_10_7: false, - _padding_11: 0, - _padding_12: 0, - _padding_13: 0, + trigger_r: 0, + trigger_l: 0, + button_a: false, + button_b: false, + button_r3: false, + button_y: false, + button_x: false, + button_l3: false, + button_l1: false, + button_r1: false, + button_l2: false, + button_r2: false, + button_menu: false, + button_view: false, + button_guide: false, + button_l4: false, + button_r4: false, battery: 100, accel_x: Integer::from_primitive(0), accel_y: Integer::from_primitive(0), @@ -282,35 +265,14 @@ impl Default for PackedInputDataReport { gyro_y: Integer::from_primitive(0), gyro_z: Integer::from_primitive(0), timestamp: Integer::from_primitive(0), - _padding_31: 0, - _padding_32: 0, - _padding_33: 0, } } } impl PackedInputDataReport { - pub fn set_dpad(&mut self, direction: DpadDirection, pressed: bool) { - let current = DpadDirection::from_bitflag(self.dpad_as_bitflag()); + pub fn set_dpad(&mut self, direction: DPadDirection, pressed: bool) { + let current = self.dpad_state; let updated = current.change(direction, pressed); - self.dpad_raw = match updated { - DpadDirection::None => 8, - d => d as u8, - }; - } - - fn dpad_as_bitflag(&self) -> u8 { - let dir = match self.dpad_raw { - 0 => DpadDirection::Up, - 1 => DpadDirection::UpRight, - 2 => DpadDirection::Right, - 3 => DpadDirection::DownRight, - 4 => DpadDirection::Down, - 5 => DpadDirection::DownLeft, - 6 => DpadDirection::Left, - 7 => DpadDirection::UpLeft, - _ => DpadDirection::None, - }; - dir.as_bitflag() + self.dpad_state = updated; } } diff --git a/src/drivers/ultimate2_wireless/driver.rs b/src/drivers/ultimate_2/mod.rs similarity index 78% rename from src/drivers/ultimate2_wireless/driver.rs rename to src/drivers/ultimate_2/mod.rs index 4f024145..4d17578f 100644 --- a/src/drivers/ultimate2_wireless/driver.rs +++ b/src/drivers/ultimate_2/mod.rs @@ -1,5 +1,9 @@ -// 8BitDo Ultimate 2 Wireless Controller (DInput mode) +pub mod driver; +pub mod event; +pub mod hid_report; +pub mod report_descriptor; +// 8BitDo Ultimate 2 Wireless Controller (DInput mode) pub const VID: u16 = 0x2DC8; pub const PID: u16 = 0x6012; @@ -11,5 +15,5 @@ pub const TRIGGER_AXIS_MAX: f64 = 255.0; // Accel scale: 4096 raw units = 1G (derived from SDL_hidapi_8bitdo.c) pub const ACCEL_SCALE: f64 = 4096.0; -pub const REPORT_ID_INPUT: u8 = 0x04; +pub const REPORT_ID_INPUT: u8 = 0x01; pub const REPORT_ID_RUMBLE: u8 = 0x05; diff --git a/src/drivers/ultimate2_wireless/report_descriptor.rs b/src/drivers/ultimate_2/report_descriptor.rs similarity index 100% rename from src/drivers/ultimate2_wireless/report_descriptor.rs rename to src/drivers/ultimate_2/report_descriptor.rs diff --git a/src/input/source/hidraw.rs b/src/input/source/hidraw.rs index 7c438b87..cf895fb4 100644 --- a/src/input/source/hidraw.rs +++ b/src/input/source/hidraw.rs @@ -13,6 +13,7 @@ pub mod opineo; pub mod oxp_hid; pub mod rog_ally; pub mod steam_deck; +pub mod ultimate_2; pub mod xpad_uhid; pub mod zotac_zone; @@ -37,6 +38,7 @@ use crate::{ input::{ capability::Capability, composite_device::client::CompositeDeviceClient, info::DeviceInfoRef, output_capability::OutputCapability, + source::hidraw::ultimate_2::Ultimate2, }, udev::device::UdevDevice, }; @@ -66,6 +68,7 @@ enum DriverType { SteamDeck, Unknown, Vader4Pro, + Ultimate2, XpadUhid, ZotacZone, } @@ -88,6 +91,7 @@ pub enum HidRawDevice { RogAlly(SourceDriver), SteamDeck(SourceDriver), Vader4Pro(SourceDriver), + Ultimate2(SourceDriver), XpadUhid(SourceDriver), ZotacZone(SourceDriver), } @@ -110,6 +114,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::RogAlly(source_driver) => source_driver.info_ref(), HidRawDevice::SteamDeck(source_driver) => source_driver.info_ref(), HidRawDevice::Vader4Pro(source_driver) => source_driver.info_ref(), + HidRawDevice::Ultimate2(source_driver) => source_driver.info_ref(), HidRawDevice::XpadUhid(source_driver) => source_driver.info_ref(), HidRawDevice::ZotacZone(source_driver) => source_driver.info_ref(), } @@ -132,6 +137,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::RogAlly(source_driver) => source_driver.get_id(), HidRawDevice::SteamDeck(source_driver) => source_driver.get_id(), HidRawDevice::Vader4Pro(source_driver) => source_driver.get_id(), + HidRawDevice::Ultimate2(source_driver) => source_driver.get_id(), HidRawDevice::XpadUhid(source_driver) => source_driver.get_id(), HidRawDevice::ZotacZone(source_driver) => source_driver.get_id(), } @@ -154,6 +160,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::RogAlly(source_driver) => source_driver.client(), HidRawDevice::SteamDeck(source_driver) => source_driver.client(), HidRawDevice::Vader4Pro(source_driver) => source_driver.client(), + HidRawDevice::Ultimate2(source_driver) => source_driver.client(), HidRawDevice::XpadUhid(source_driver) => source_driver.client(), HidRawDevice::ZotacZone(source_driver) => source_driver.client(), } @@ -176,6 +183,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::RogAlly(source_driver) => source_driver.run().await, HidRawDevice::SteamDeck(source_driver) => source_driver.run().await, HidRawDevice::Vader4Pro(source_driver) => source_driver.run().await, + HidRawDevice::Ultimate2(source_driver) => source_driver.run().await, HidRawDevice::XpadUhid(source_driver) => source_driver.run().await, HidRawDevice::ZotacZone(source_driver) => source_driver.run().await, } @@ -200,6 +208,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::RogAlly(source_driver) => source_driver.get_capabilities(), HidRawDevice::SteamDeck(source_driver) => source_driver.get_capabilities(), HidRawDevice::Vader4Pro(source_driver) => source_driver.get_capabilities(), + HidRawDevice::Ultimate2(source_driver) => source_driver.get_capabilities(), HidRawDevice::XpadUhid(source_driver) => source_driver.get_capabilities(), HidRawDevice::ZotacZone(source_driver) => source_driver.get_capabilities(), } @@ -230,6 +239,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::RogAlly(source_driver) => source_driver.get_output_capabilities(), HidRawDevice::SteamDeck(source_driver) => source_driver.get_output_capabilities(), HidRawDevice::Vader4Pro(source_driver) => source_driver.get_output_capabilities(), + HidRawDevice::Ultimate2(source_driver) => source_driver.get_output_capabilities(), HidRawDevice::XpadUhid(source_driver) => source_driver.get_output_capabilities(), HidRawDevice::ZotacZone(source_driver) => source_driver.get_output_capabilities(), } @@ -252,6 +262,7 @@ impl SourceDeviceCompatible for HidRawDevice { HidRawDevice::RogAlly(source_driver) => source_driver.get_device_path(), HidRawDevice::SteamDeck(source_driver) => source_driver.get_device_path(), HidRawDevice::Vader4Pro(source_driver) => source_driver.get_device_path(), + HidRawDevice::Ultimate2(source_driver) => source_driver.get_device_path(), HidRawDevice::XpadUhid(source_driver) => source_driver.get_device_path(), HidRawDevice::ZotacZone(source_driver) => source_driver.get_device_path(), } @@ -455,6 +466,13 @@ impl HidRawDevice { SourceDriver::new(composite_device, device, device_info.into(), conf); Ok(Self::OxpHid(source_device)) } + + DriverType::Ultimate2 => { + let device = Ultimate2::new(device_info.clone())?; + let source_device = + SourceDriver::new(composite_device, device, device_info.into(), conf); + Ok(Self::Ultimate2(source_device)) + } } } @@ -597,6 +615,11 @@ impl HidRawDevice { return DriverType::GpdWinMiniMacroKeyboard; } + if vid == drivers::ultimate_2::VID && pid == drivers::ultimate_2::PID { + log::info!("Detected 8BitDo Ultimate 2 Gamepad"); + return DriverType::Ultimate2; + } + // Unknown log::warn!("No driver for hidraw interface found. VID: {vid}, PID: {pid}"); DriverType::Unknown diff --git a/src/input/source/hidraw/ultimate_2.rs b/src/input/source/hidraw/ultimate_2.rs new file mode 100644 index 00000000..072f0e10 --- /dev/null +++ b/src/input/source/hidraw/ultimate_2.rs @@ -0,0 +1,403 @@ +use std::{collections::HashMap, error::Error, fmt::Debug}; + +use evdev::{FFEffectData, FFEffectKind}; + +use crate::{ + drivers::ultimate_2::{driver::Driver, event, JOY_AXIS_MAX, JOY_AXIS_MIN, TRIGGER_AXIS_MAX}, + input::{ + capability::{Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger, Source}, + event::{ + native::NativeEvent, + value::{normalize_signed_value, normalize_unsigned_value, InputValue}, + }, + output_capability::OutputCapability, + output_event::OutputEvent, + source::{InputError, OutputError, SourceInputDevice, SourceOutputDevice}, + }, + udev::device::UdevDevice, +}; + +/// XpadUhid source device implementation +pub struct Ultimate2 { + driver: Driver, + ff_evdev_effects: HashMap, +} + +impl Ultimate2 { + /// Create a new source device with the given udev + /// device information + pub fn new(device_info: UdevDevice) -> Result> { + let driver = Driver::new(device_info)?; + Ok(Self { + driver, + ff_evdev_effects: HashMap::new(), + }) + } + + /// Returns the next available evdev effect id + fn next_ff_effect_id(&self) -> i16 { + const MAX: i16 = 2096; + let mut i = 0; + loop { + if !self.ff_evdev_effects.contains_key(&i) { + return i; + } + i += 1; + if i > MAX { + return -1; + } + } + } + + /// Process the given evdev force feedback event. + fn process_evdev_ff(&mut self, input_event: evdev::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 { + log::trace!("Stopping rumble"); + if let Err(e) = self.driver.rumble(0, 0) { + log::debug!("Failed to stop rumble: {:?}", e); + return Ok(()); + } + 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, + } => { + // Scale the rumble values to the DS5 values + let left_speed = (strong_magnitude as f64 / u16::MAX as f64) * u8::MAX as f64; + let left_speed = left_speed.round() as u8; + let right_speed = (weak_magnitude as f64 / u16::MAX as f64) * u8::MAX as f64; + let right_speed = right_speed.round() as u8; + + // Do rumble + if let Err(e) = self.driver.rumble(left_speed, right_speed) { + let err = format!("Failed to do rumble: {e:?}"); + return Err(err.into()); + } + } + } + + Ok(()) + } +} + +impl SourceInputDevice for Ultimate2 { + /// Poll the given input 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 Ultimate2 { + /// Returns the possible output events this device is capable of (e.g. force feedback, LED, + /// etc.) + fn get_output_capabilities(&self) -> Result, OutputError> { + Ok(OUTPUT_CAPABILITIES.into()) + } + + /// 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) => Ok(self.process_evdev_ff(input_event)?), + OutputEvent::DualSense(_) => Ok(()), + OutputEvent::Uinput(_) => Ok(()), + OutputEvent::SteamDeckHaptics(_packed_haptic_report) => Ok(()), + OutputEvent::SteamDeckRumble(_packed_rumble_report) => Ok(()), + OutputEvent::GenericRumble { .. } => Ok(()), + } + } + + /// Upload the given force feedback effect data to the source device. Returns + /// a device-specific id of the uploaded effect if it is successful. + fn upload_effect(&mut self, effect: FFEffectData) -> Result { + log::debug!("Uploading FF effect data"); + let id = self.next_ff_effect_id(); + if id == -1 { + return Err("Maximum FF effects uploaded".into()); + } + self.ff_evdev_effects.insert(id, effect); + + Ok(id) + } + + /// Update the effect with the given id using the given effect data. + fn update_effect(&mut self, effect_id: i16, effect: FFEffectData) -> Result<(), OutputError> { + log::debug!("Updating FF effect data with id {effect_id}"); + self.ff_evdev_effects.insert(effect_id, effect); + Ok(()) + } + + /// Erase the effect with the given id from the source device. + fn erase_effect(&mut self, effect_id: i16) -> Result<(), OutputError> { + log::debug!("Erasing FF effect data"); + self.ff_evdev_effects.remove(&effect_id); + Ok(()) + } +} + +impl Debug for Ultimate2 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("XpadUhid").finish() + } +} + +/// Normalize the value to something between 0 and 1.0 based on the +/// maximum axis range. +fn normalize_axis_value(event: event::AxisEvent) -> InputValue { + let max = JOY_AXIS_MAX; + let min = JOY_AXIS_MIN; + match event { + event::AxisEvent::LStick(value) => { + 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 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 +/// maximum axis range. +fn normalize_trigger_value(event: event::TriggerEvent) -> InputValue { + let max = TRIGGER_AXIS_MAX; + match event { + event::TriggerEvent::TriggerL(value) => { + InputValue::Float(normalize_unsigned_value(value.value as f64, max)) + } + event::TriggerEvent::TriggerR(value) => { + InputValue::Float(normalize_unsigned_value(value.value as f64, max)) + } + } +} + +/// Translate the given events into native events +fn translate_events(events: Vec) -> Vec { + let mut translated = Vec::with_capacity(events.len()); + for event in events.into_iter() { + translated.push(translate_event(event)); + } + if !translated.is_empty() { + log::debug!("Translated events: {translated:?}"); + }; + translated +} + +/// Translate the given event into a native event +fn translate_event(event: event::Event) -> NativeEvent { + log::debug!("Got event {event:?}"); + 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::Guide(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::Guide)), + 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::L1(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftBumper)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::L2(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftStick)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::L3(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftPaddle1)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::L4(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftPaddle2)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::R1(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::RightBumper)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::R2(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::RightStick)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::R3(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::RightPaddle1)), + InputValue::Bool(value.pressed), + ), + event::ButtonEvent::R4(value) => NativeEvent::new( + Capability::Gamepad(Gamepad::Button(GamepadButton::RightPaddle2)), + 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::TriggerL(_) => NativeEvent::new( + Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::LeftTrigger)), + normalize_trigger_value(trigg), + ), + event::TriggerEvent::TriggerR(_) => NativeEvent::new( + Capability::Gamepad(Gamepad::Trigger(GamepadTrigger::RightTrigger)), + normalize_trigger_value(trigg), + ), + }, + event::Event::Inertia(inertia) => match inertia { + event::InertialEvent::Accelerometer(value) => NativeEvent::new( + Capability::Accelerometer(Source::Center), + InputValue::Vector3 { + x: Some(value.x as f64), + y: Some(value.y as f64), + z: Some(value.z as f64), + }, + ), + event::InertialEvent::Gyro(value) => NativeEvent::new( + Capability::Gyroscope(Source::Center), + InputValue::Vector3 { + x: Some(value.x as f64), + y: Some(value.y as f64), + z: Some(value.z as f64), + }, + ), + }, + } +} + +/// List of all capabilities that the driver implements +pub const CAPABILITIES: &[Capability] = &[ + Capability::Accelerometer(Source::Center), + 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::Guide)), + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftBumper)), + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftPaddle1)), + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftPaddle2)), + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftStick)), + Capability::Gamepad(Gamepad::Button(GamepadButton::LeftTrigger)), + Capability::Gamepad(Gamepad::Button(GamepadButton::North)), + Capability::Gamepad(Gamepad::Button(GamepadButton::RightBumper)), + Capability::Gamepad(Gamepad::Button(GamepadButton::RightPaddle1)), + Capability::Gamepad(Gamepad::Button(GamepadButton::RightPaddle2)), + Capability::Gamepad(Gamepad::Button(GamepadButton::RightStick)), + Capability::Gamepad(Gamepad::Button(GamepadButton::RightTrigger)), + 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)), + Capability::Gyroscope(Source::Center), +]; + +pub const OUTPUT_CAPABILITIES: &[OutputCapability] = &[OutputCapability::ForceFeedback]; diff --git a/src/input/target/mod.rs b/src/input/target/mod.rs index 0530bc90..127251f5 100644 --- a/src/input/target/mod.rs +++ b/src/input/target/mod.rs @@ -1,3 +1,19 @@ +pub mod client; +pub mod command; +pub mod dbus; +pub mod debug; +pub mod dualsense; +pub mod horipad_steam; +pub mod keyboard; +pub mod mouse; +pub mod steam_deck; +pub mod steam_deck_uhid; +pub mod touchpad; +pub mod touchscreen; +pub mod ulitmate_2; +pub mod unified_gamepad; +pub mod xpad; + use std::{ collections::HashSet, env, @@ -15,7 +31,6 @@ use tokio::{ sync::mpsc::{self, error::TryRecvError}, task::JoinHandle, }; -use ultimate2_wireless::Ultimate2WirelessDevice; use unified_gamepad::UnifiedGamepadDevice; use crate::{ @@ -50,22 +65,7 @@ use self::mouse::MouseDevice; use self::steam_deck::SteamDeckDevice; use self::touchpad::TouchpadDevice; use self::touchscreen::TouchscreenDevice; - -pub mod client; -pub mod command; -pub mod dbus; -pub mod debug; -pub mod dualsense; -pub mod horipad_steam; -pub mod keyboard; -pub mod mouse; -pub mod steam_deck; -pub mod steam_deck_uhid; -pub mod touchpad; -pub mod touchscreen; -pub mod ultimate2_wireless; -pub mod unified_gamepad; -pub mod xpad; +use self::ulitmate_2::Ultimate2WirelessDevice; /// Possible errors for a target device client #[derive(Error, Debug)] diff --git a/src/input/target/ultimate2_wireless.rs b/src/input/target/ulitmate_2.rs similarity index 84% rename from src/input/target/ultimate2_wireless.rs rename to src/input/target/ulitmate_2.rs index c9b6f083..b768f21e 100644 --- a/src/input/target/ultimate2_wireless.rs +++ b/src/input/target/ulitmate_2.rs @@ -3,6 +3,7 @@ //! report using fixed byte offsets from SDL_hidapi_8bitdo.c. Axis coordinate rotation and //! IMU scale factors are also derived from that source. +use core::option::Option::None; use std::{cmp::Ordering, error::Error, fmt::Debug, fs::File, time::Duration}; use packed_struct::prelude::*; @@ -10,20 +11,18 @@ use packed_struct::types::SizedInteger; use uhid_virt::{Bus, CreateParams, StreamError, UHIDDevice}; use crate::{ - drivers::ultimate2_wireless::{ - driver::{ - ACCEL_SCALE, JOY_AXIS_MAX, JOY_AXIS_MIN, PID, REPORT_ID_RUMBLE, TRIGGER_AXIS_MAX, VID, - }, - hid_report::{DpadDirection, PackedInputDataReport, PackedRumbleOutputReport}, + drivers::ultimate_2::{ + hid_report::{DPadDirection, PackedInputDataReport, PackedRumbleOutputReport}, report_descriptor::REPORT_DESCRIPTOR, + ACCEL_SCALE, JOY_AXIS_MAX, PID, REPORT_ID_RUMBLE, TRIGGER_AXIS_MAX, VID, }, input::{ capability::{Capability, Gamepad, GamepadAxis, GamepadButton, GamepadTrigger}, composite_device::client::CompositeDeviceClient, event::{ native::{NativeEvent, ScheduledNativeEvent}, + value::denormalize_unsigned_value_u8, value::InputValue, - value::{denormalize_signed_value_u8, denormalize_unsigned_value_u8}, }, output_capability::OutputCapability, output_event::OutputEvent, @@ -74,40 +73,33 @@ impl Ultimate2WirelessDevice { match capability { Capability::Gamepad(gamepad) => match gamepad { - Gamepad::Button(btn) => match btn { - GamepadButton::South => self.state.btn_a = event.pressed(), - GamepadButton::East => self.state.btn_b = event.pressed(), - GamepadButton::North => self.state.btn_y = event.pressed(), - GamepadButton::West => self.state.btn_x = event.pressed(), - GamepadButton::Start => self.state.btn_start = event.pressed(), - GamepadButton::Select => self.state.btn_select = event.pressed(), - GamepadButton::Guide => self.state.btn_guide = event.pressed(), - GamepadButton::LeftBumper => self.state.btn_lb = event.pressed(), - GamepadButton::RightBumper => self.state.btn_rb = event.pressed(), - GamepadButton::LeftStick => self.state.btn_l3 = event.pressed(), - GamepadButton::RightStick => self.state.btn_r3 = event.pressed(), - GamepadButton::LeftPaddle1 => self.state.btn_l4 = event.pressed(), - GamepadButton::RightPaddle1 => self.state.btn_r4 = event.pressed(), - GamepadButton::LeftPaddle2 => self.state.btn_pl = event.pressed(), - GamepadButton::RightPaddle2 => self.state.btn_pr = event.pressed(), + Gamepad::Button(button) => match button { + GamepadButton::South => self.state.button_a = event.pressed(), + GamepadButton::East => self.state.button_b = event.pressed(), + GamepadButton::North => self.state.button_x = event.pressed(), + GamepadButton::West => self.state.button_y = event.pressed(), + GamepadButton::Start => self.state.button_view = event.pressed(), + GamepadButton::Select => self.state.button_menu = event.pressed(), + GamepadButton::Guide => self.state.button_guide = event.pressed(), + GamepadButton::LeftBumper => self.state.button_l1 = event.pressed(), + GamepadButton::RightBumper => self.state.button_r1 = event.pressed(), + GamepadButton::LeftStick => self.state.button_l2 = event.pressed(), + GamepadButton::RightStick => self.state.button_r2 = event.pressed(), + GamepadButton::LeftPaddle1 => self.state.button_l3 = event.pressed(), + GamepadButton::RightPaddle1 => self.state.button_r3 = event.pressed(), + GamepadButton::LeftPaddle2 => self.state.button_l4 = event.pressed(), + GamepadButton::RightPaddle2 => self.state.button_r4 = event.pressed(), GamepadButton::DPadUp => { - self.state.set_dpad(DpadDirection::Up, event.pressed()) + self.state.set_dpad(DPadDirection::Up, event.pressed()) } GamepadButton::DPadDown => { - self.state.set_dpad(DpadDirection::Down, event.pressed()) + self.state.set_dpad(DPadDirection::Down, event.pressed()) } GamepadButton::DPadLeft => { - self.state.set_dpad(DpadDirection::Left, event.pressed()) + self.state.set_dpad(DPadDirection::Left, event.pressed()) } GamepadButton::DPadRight => { - self.state.set_dpad(DpadDirection::Right, event.pressed()) - } - // Digital trigger fallback - GamepadButton::LeftTrigger => { - self.state.lt_analog = if event.pressed() { 0xff } else { 0x00 } - } - GamepadButton::RightTrigger => { - self.state.rt_analog = if event.pressed() { 0xff } else { 0x00 } + self.state.set_dpad(DPadDirection::Right, event.pressed()) } _ => (), }, @@ -117,11 +109,11 @@ impl Ultimate2WirelessDevice { if let InputValue::Vector2 { x, y } = value { if let Some(x) = x { self.state.joystick_l_x = - denormalize_signed_value_u8(x, JOY_AXIS_MIN, JOY_AXIS_MAX); + denormalize_unsigned_value_u8(x, JOY_AXIS_MAX); } if let Some(y) = y { self.state.joystick_l_y = - denormalize_signed_value_u8(y, JOY_AXIS_MIN, JOY_AXIS_MAX); + denormalize_unsigned_value_u8(y, JOY_AXIS_MAX); } } } @@ -129,11 +121,11 @@ impl Ultimate2WirelessDevice { if let InputValue::Vector2 { x, y } = value { if let Some(x) = x { self.state.joystick_r_x = - denormalize_signed_value_u8(x, JOY_AXIS_MIN, JOY_AXIS_MAX); + denormalize_unsigned_value_u8(x, JOY_AXIS_MAX); } if let Some(y) = y { self.state.joystick_r_y = - denormalize_signed_value_u8(y, JOY_AXIS_MIN, JOY_AXIS_MAX); + denormalize_unsigned_value_u8(y, JOY_AXIS_MAX); } } } @@ -142,14 +134,14 @@ impl Ultimate2WirelessDevice { if let Some(x) = x { match x.partial_cmp(&0.0) { Some(Ordering::Less) => { - self.state.set_dpad(DpadDirection::Left, true) + self.state.set_dpad(DPadDirection::Left, true) } Some(Ordering::Equal) => { - self.state.set_dpad(DpadDirection::Left, false); - self.state.set_dpad(DpadDirection::Right, false); + self.state.set_dpad(DPadDirection::Left, false); + self.state.set_dpad(DPadDirection::Right, false); } Some(Ordering::Greater) => { - self.state.set_dpad(DpadDirection::Right, true) + self.state.set_dpad(DPadDirection::Right, true) } None => (), } @@ -157,14 +149,14 @@ impl Ultimate2WirelessDevice { if let Some(y) = y { match y.partial_cmp(&0.0) { Some(Ordering::Less) => { - self.state.set_dpad(DpadDirection::Up, true) + self.state.set_dpad(DPadDirection::Up, true) } Some(Ordering::Equal) => { - self.state.set_dpad(DpadDirection::Up, false); - self.state.set_dpad(DpadDirection::Down, false); + self.state.set_dpad(DPadDirection::Up, false); + self.state.set_dpad(DPadDirection::Down, false); } Some(Ordering::Greater) => { - self.state.set_dpad(DpadDirection::Down, true) + self.state.set_dpad(DPadDirection::Down, true) } None => (), } @@ -177,13 +169,13 @@ impl Ultimate2WirelessDevice { Gamepad::Trigger(trigger) => match trigger { GamepadTrigger::LeftTrigger => { if let InputValue::Float(v) = value { - self.state.lt_analog = + self.state.trigger_l = denormalize_unsigned_value_u8(v, TRIGGER_AXIS_MAX); } } GamepadTrigger::RightTrigger => { if let InputValue::Float(v) = value { - self.state.rt_analog = + self.state.trigger_r = denormalize_unsigned_value_u8(v, TRIGGER_AXIS_MAX); } }