Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 27 additions & 22 deletions src/drivers/xpad_uhid/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use hidapi::HidDevice;
use packed_struct::PackedStruct;

use crate::{
drivers::xpad_uhid::hid_report::{DPadDirection, XBoxSeriesInputDataReport},
drivers::xpad_uhid::hid_report::{
DPadDirection, XBoxOneBtInputDataReport, XBoxSeriesInputDataReport,
},
udev::device::UdevDevice,
};

Expand All @@ -17,8 +19,10 @@ use super::{
pub const DATA: u8 = 0x01;
pub const GUIDE: u8 = 0x02;

// Input report size
// Input report size for Xbox Series controllers
const PACKET_SIZE: usize = 17;
// Input report size for Xbox One Bluetooth controllers
const XBOX_ONE_BT_PACKET_SIZE: usize = 16;

// HID buffer read timeout
const HID_TIMEOUT: i32 = 10;
Expand Down Expand Up @@ -99,20 +103,28 @@ impl Driver {
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 {
DATA => {
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()?;

self.handle_input_report(sized_buf)?
let input_report = match bytes_read {
// Xbox Series controller format (17 bytes)
PACKET_SIZE => XBoxSeriesInputDataReport::unpack(&buf)?,
// Xbox One Bluetooth format (16 bytes)
XBOX_ONE_BT_PACKET_SIZE => XBoxOneBtInputDataReport::unpack(
buf[..XBOX_ONE_BT_PACKET_SIZE].try_into()?,
)?
.to_series_report(),
_ => {
log::warn!(
"Unexpected packet size for input data: {bytes_read} (expected {PACKET_SIZE} or {XBOX_ONE_BT_PACKET_SIZE})"
);
return Ok(vec![]);
}
};
self.handle_input_report(input_report)
}
// XBox One gamepads have a separate report for guide button presses
// for some reason.
Expand All @@ -136,14 +148,9 @@ impl Driver {
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<Vec<Event>, Box<dyn Error + Send + Sync>> {
let input_report = XBoxSeriesInputDataReport::unpack(&buf)?;

/// Updates the internal state with the given input report and translates
/// state changes into a stream of input events
fn handle_input_report(&mut self, input_report: XBoxSeriesInputDataReport) -> Vec<Event> {
// Print input report for debugging
log::trace!("--- Input report ---");
log::trace!("{input_report}");
Expand All @@ -153,9 +160,7 @@ impl Driver {
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)
self.translate_events(old_dinput_state)
}

/// Update touchinput state
Expand Down Expand Up @@ -308,7 +313,7 @@ impl Driver {
value: state.trigger_l,
})));
}
if state.trigger_l != old_state.trigger_r {
if state.trigger_r != old_state.trigger_r {
events.push(Event::Trigger(TriggerEvent::TriggerR(TriggerInput {
value: state.trigger_r,
})));
Expand Down
103 changes: 103 additions & 0 deletions src/drivers/xpad_uhid/hid_report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,109 @@ pub enum DPadDirection {
UpLeft = 8,
}

/// Button state for Xbox One Bluetooth controllers.
/// The Xbox One S/X BT HID report packs 10 buttons into 2 bytes (10 bits + 6 padding),
/// with a different bit layout than the Xbox Series controller.
/// Button order from the HID descriptor (buttons 1-10):
/// A, B, X, Y, LB, RB, View, Menu, LStick, RStick
#[derive(PackedStruct, Debug, Copy, Clone, PartialEq, Default)]
#[packed_struct(bit_numbering = "msb0", size_bytes = "2")]
pub struct XBoxOneBtButtonState {
// byte 0 (byte 14 of report) - msb0: bit 0 = MSB (0x80), bit 7 = LSB (0x01)
#[packed_field(bits = "0")]
pub menu: bool, // 0x80
#[packed_field(bits = "1")]
pub view: bool, // 0x40
#[packed_field(bits = "2")]
pub rb: bool, // 0x20
#[packed_field(bits = "3")]
pub lb: bool, // 0x10
#[packed_field(bits = "4")]
pub y: bool, // 0x08
#[packed_field(bits = "5")]
pub x: bool, // 0x04
#[packed_field(bits = "6")]
pub b: bool, // 0x02
#[packed_field(bits = "7")]
pub a: bool, // 0x01

// byte 1 (byte 15 of report)
#[packed_field(bits = "14")]
pub thumb_r: bool, // 0x02
#[packed_field(bits = "15")]
pub thumb_l: bool, // 0x01
}

/// Xbox One Bluetooth input data report (16 bytes).
/// This is the HID report format used by Xbox One S/X controllers over Bluetooth
/// (e.g., product ID 0x02E0). It uses Report ID 0x01 but has a different layout
/// from the Xbox Series controller: 10-bit triggers, 4-bit hat switch, and
/// 10 buttons in 2 bytes instead of 3.
#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)]
#[packed_struct(bit_numbering = "msb0", size_bytes = "16")]
pub struct XBoxOneBtInputDataReport {
// BYTE 0
#[packed_field(bytes = "0")]
pub report_id: u8,

// Axes
// BYTES 1-2
#[packed_field(bytes = "1..=2", endian = "lsb")]
pub l_stick_x: u16,
// BYTES 3-4
#[packed_field(bytes = "3..=4", endian = "lsb")]
pub l_stick_y: u16,
// BYTES 5-6
#[packed_field(bytes = "5..=6", endian = "lsb")]
pub r_stick_x: u16,
// BYTES 7-8
#[packed_field(bytes = "7..=8", endian = "lsb")]
pub r_stick_y: u16,
// BYTES 9-10: left trigger (10-bit value + 6-bit padding, as u16 LE)
#[packed_field(bytes = "9..=10", endian = "lsb")]
pub trigger_l: u16,
// BYTES 11-12: right trigger (10-bit value + 6-bit padding, as u16 LE)
#[packed_field(bytes = "11..=12", endian = "lsb")]
pub trigger_r: u16,

// BYTE 13: hat switch (lower 4 bits) + padding (upper 4 bits)
#[packed_field(bytes = "13", ty = "enum")]
pub dpad_state: DPadDirection,

// BYTES 14-15: 10 buttons + 6-bit padding
#[packed_field(bytes = "14..=15")]
pub button_state: XBoxOneBtButtonState,
}

impl XBoxOneBtInputDataReport {
/// Convert to an XBoxSeriesInputDataReport for unified event processing
pub fn to_series_report(&self) -> XBoxSeriesInputDataReport {
XBoxSeriesInputDataReport {
report_id: self.report_id,
l_stick_x: self.l_stick_x,
l_stick_y: self.l_stick_y,
r_stick_x: self.r_stick_x,
r_stick_y: self.r_stick_y,
trigger_l: self.trigger_l,
trigger_r: self.trigger_r,
dpad_state: self.dpad_state,
button_state: ButtonState {
a: self.button_state.a,
b: self.button_state.b,
x: self.button_state.x,
y: self.button_state.y,
lb: self.button_state.lb,
rb: self.button_state.rb,
view: self.button_state.view,
menu: self.button_state.menu,
thumb_l: self.button_state.thumb_l,
thumb_r: self.button_state.thumb_r,
..Default::default()
},
}
}
}

#[derive(PackedStruct, Debug, Copy, Clone, PartialEq)]
#[packed_struct(bit_numbering = "msb0", size_bytes = "17")]
pub struct XBoxSeriesInputDataReport {
Expand Down
2 changes: 1 addition & 1 deletion src/input/event/evdev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ impl EvdevEvent {
KeyCode::KEY_MACRO => Capability::NotImplemented,
KeyCode::KEY_MAIL => Capability::NotImplemented,
KeyCode::KEY_MEDIA => Capability::NotImplemented,
KeyCode::KEY_MENU => Capability::NotImplemented,
KeyCode::KEY_MENU => Capability::Gamepad(Gamepad::Button(GamepadButton::Guide)),
KeyCode::KEY_MICMUTE => Capability::NotImplemented,
KeyCode::KEY_MINUS => Capability::Keyboard(Keyboard::KeyMinus),
KeyCode::KEY_MOVE => Capability::NotImplemented,
Expand Down