feat: add Xbox One HID report layout and mapping
Phase 4 foundation, independent of the presentation mechanism: - hid/hid_report.hpp: standard Xbox One report layout (64-byte input: buttons/guide/triggers/sticks; 64-byte output: motors), the HID report descriptor, and GIP state to report mapping. - make_input_report(): remaps GIP button bits to the standard Xbox bitmask, scales 10-bit triggers to 8-bit, sticks pass through. - parse_output_report(): extracts motor intensities from output reports for the rumble relay; malformed reports stop the motors. - test_hid: unit tests for the mapping, scaling, and descriptor. Report packing verified with a descriptor walker: input and output are exactly 64 bytes each. Next step is presentation via DriverKit, which needs a Developer ID with the DriverKit entitlement. Co-Authored-By: qwen3.8-27b@q3_k_xl: implemented HID report layer
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
// Standard Xbox One HID report layout (see hid/hid_report.hpp).
|
||||
|
||||
#include "hid/hid_report.hpp"
|
||||
|
||||
namespace xone::hid {
|
||||
|
||||
namespace {
|
||||
|
||||
// GIP button bitmask (enum gip_gamepad_button in the upstream driver; the
|
||||
// app's Swift model uses the same values).
|
||||
constexpr std::uint16_t k_gip_btn_a = 0x0010; // bit 4
|
||||
constexpr std::uint16_t k_gip_btn_b = 0x0020; // bit 5
|
||||
constexpr std::uint16_t k_gip_btn_x = 0x0040; // bit 6
|
||||
constexpr std::uint16_t k_gip_btn_y = 0x0080; // bit 7
|
||||
constexpr std::uint16_t k_gip_btn_dpad_u = 0x0100; // bit 8
|
||||
constexpr std::uint16_t k_gip_btn_dpad_d = 0x0200; // bit 9
|
||||
constexpr std::uint16_t k_gip_btn_dpad_l = 0x0400; // bit 10
|
||||
constexpr std::uint16_t k_gip_btn_dpad_r = 0x0800; // bit 11
|
||||
constexpr std::uint16_t k_gip_btn_bumper_l = 0x1000; // bit 12
|
||||
constexpr std::uint16_t k_gip_btn_bumper_r = 0x2000; // bit 13
|
||||
constexpr std::uint16_t k_gip_btn_stick_l = 0x4000; // bit 14
|
||||
constexpr std::uint16_t k_gip_btn_stick_r = 0x8000; // bit 15
|
||||
constexpr std::uint16_t k_gip_btn_start = 0x0004; // bit 2 (Menu)
|
||||
constexpr std::uint16_t k_gip_btn_select = 0x0008; // bit 3 (Back/View)
|
||||
|
||||
// Standard Xbox One HID button bitmask (input report bytes [1..2]).
|
||||
constexpr std::uint16_t k_hid_btn_a = 0x0001; // bit 0
|
||||
constexpr std::uint16_t k_hid_btn_b = 0x0002; // bit 1
|
||||
constexpr std::uint16_t k_hid_btn_x = 0x0004; // bit 2
|
||||
constexpr std::uint16_t k_hid_btn_y = 0x0008; // bit 3
|
||||
constexpr std::uint16_t k_hid_btn_bumper_l = 0x0010; // bit 4
|
||||
constexpr std::uint16_t k_hid_btn_bumper_r = 0x0020; // bit 5
|
||||
constexpr std::uint16_t k_hid_btn_stick_l = 0x0040; // bit 6
|
||||
constexpr std::uint16_t k_hid_btn_stick_r = 0x0080; // bit 7
|
||||
constexpr std::uint16_t k_hid_btn_back = 0x0100; // bit 8 (Select/View)
|
||||
constexpr std::uint16_t k_hid_btn_start = 0x0200; // bit 9 (Menu)
|
||||
constexpr std::uint16_t k_hid_btn_dpad_u = 0x0400; // bit 10
|
||||
constexpr std::uint16_t k_hid_btn_dpad_d = 0x0800; // bit 11
|
||||
constexpr std::uint16_t k_hid_btn_dpad_l = 0x1000; // bit 12
|
||||
constexpr std::uint16_t k_hid_btn_dpad_r = 0x2000; // bit 13
|
||||
|
||||
struct button_map {
|
||||
std::uint16_t gip;
|
||||
std::uint16_t hid;
|
||||
};
|
||||
|
||||
// GIP -> HID button remap. The guide button is not in the bitmask; it has
|
||||
// its own byte in the report (k_in_guide).
|
||||
constexpr button_map k_button_map[] = {
|
||||
{k_gip_btn_a, k_hid_btn_a},
|
||||
{k_gip_btn_b, k_hid_btn_b},
|
||||
{k_gip_btn_x, k_hid_btn_x},
|
||||
{k_gip_btn_y, k_hid_btn_y},
|
||||
{k_gip_btn_bumper_l, k_hid_btn_bumper_l},
|
||||
{k_gip_btn_bumper_r, k_hid_btn_bumper_r},
|
||||
{k_gip_btn_stick_l, k_hid_btn_stick_l},
|
||||
{k_gip_btn_stick_r, k_hid_btn_stick_r},
|
||||
{k_gip_btn_select, k_hid_btn_back},
|
||||
{k_gip_btn_start, k_hid_btn_start},
|
||||
{k_gip_btn_dpad_u, k_hid_btn_dpad_u},
|
||||
{k_gip_btn_dpad_d, k_hid_btn_dpad_d},
|
||||
{k_gip_btn_dpad_l, k_hid_btn_dpad_l},
|
||||
{k_gip_btn_dpad_r, k_hid_btn_dpad_r},
|
||||
};
|
||||
|
||||
constexpr std::uint16_t k_trigger_max_10bit = 1023;
|
||||
constexpr std::uint8_t k_trigger_max_8bit = 255;
|
||||
|
||||
// Scale a 10-bit trigger (0..1023) to the 8-bit report value, rounding.
|
||||
auto scale_trigger(std::uint16_t v) -> std::uint8_t
|
||||
{
|
||||
if (v > k_trigger_max_10bit)
|
||||
v = k_trigger_max_10bit;
|
||||
auto scaled = static_cast<std::uint32_t>(v) * k_trigger_max_8bit
|
||||
+ (k_trigger_max_10bit / 2);
|
||||
return static_cast<std::uint8_t>(scaled / k_trigger_max_10bit);
|
||||
}
|
||||
|
||||
auto store_le16(std::array<std::uint8_t, k_input_report_size>& report,
|
||||
std::size_t offset, std::uint16_t value) -> void
|
||||
{
|
||||
report[offset] = static_cast<std::uint8_t>(value & 0xff);
|
||||
report[offset + 1] = static_cast<std::uint8_t>(value >> 8);
|
||||
}
|
||||
|
||||
// HID report descriptor: standard Xbox One layout (see header). The field
|
||||
// order and sizes pack to exactly the documented report bytes.
|
||||
constexpr std::array<std::uint8_t, 167> k_descriptor = {
|
||||
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||
0x09, 0x04, // Usage (Game Pad)
|
||||
0xa1, 0x01, // Collection (Application)
|
||||
|
||||
// ---- Input report (ID 1) ----
|
||||
0x85, 0x01, // Report ID (1)
|
||||
|
||||
// Buttons A,B,X,Y,LB,RB,LS,RS,Back,Start,DpadU,DpadD,DpadL,DpadR
|
||||
0x05, 0x09, // Usage Page (Buttons)
|
||||
0x19, 0x01, // Usage Minimum (Button 1)
|
||||
0x29, 0x0e, // Usage Maximum (Button 14)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x01, // Logical Maximum (1)
|
||||
0x75, 0x01, // Report Size (1)
|
||||
0x95, 0x0e, // Report Count (14)
|
||||
0x81, 0x02, // Input (Data,Var,Abs)
|
||||
|
||||
// Two padding bits so the guide byte starts on a byte boundary.
|
||||
0x75, 0x02, // Report Size (2)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x26, 0x03, 0x00, // Logical Maximum (3)
|
||||
0x81, 0x01, // Input (Const,Var,Abs)
|
||||
|
||||
// Guide button (own byte).
|
||||
0x09, 0x0f, // Usage (Button 15)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x01, // Logical Maximum (1)
|
||||
0x81, 0x02, // Input (Data,Var,Abs)
|
||||
|
||||
// Triggers: Z (left), Rz (right).
|
||||
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||
0x09, 0x32, // Usage (Z)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x26, 0xff, 0x00, // Logical Maximum (255)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x81, 0x02, // Input (Data,Var,Abs)
|
||||
|
||||
0x09, 0x35, // Usage (Rz)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x81, 0x02, // Input (Data,Var,Abs)
|
||||
|
||||
// Left stick X/Y.
|
||||
0x09, 0x30, // Usage (X)
|
||||
0x15, 0x81, // Logical Minimum (-32768)
|
||||
0x26, 0x7f, 0x01, // Logical Maximum (32767)
|
||||
0x75, 0x10, // Report Size (16)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x81, 0x02, // Input (Data,Var,Abs)
|
||||
|
||||
0x09, 0x31, // Usage (Y)
|
||||
0x75, 0x10, // Report Size (16)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x81, 0x02, // Input (Data,Var,Abs)
|
||||
|
||||
// Right stick Rx/Ry.
|
||||
0x09, 0x33, // Usage (Rx)
|
||||
0x75, 0x10, // Report Size (16)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x81, 0x02, // Input (Data,Var,Abs)
|
||||
|
||||
0x09, 0x34, // Usage (Ry)
|
||||
0x75, 0x10, // Report Size (16)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x81, 0x02, // Input (Data,Var,Abs)
|
||||
|
||||
// Padding to the standard 64-byte report size.
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x95, 0x32, // Report Count (50)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x26, 0xff, 0x00, // Logical Maximum (255)
|
||||
0x81, 0x01, // Input (Const,Var,Abs)
|
||||
|
||||
// ---- Output report (ID 1) ----
|
||||
0x85, 0x01, // Report ID (1)
|
||||
|
||||
// Reserved byte.
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x26, 0xff, 0x00, // Logical Maximum (255)
|
||||
0x91, 0x01, // Output (Const,Var,Abs)
|
||||
|
||||
// Motors (vendor-defined usages).
|
||||
0x06, 0x00, 0xff, // Usage Page (Vendor Defined 0xFF00)
|
||||
0x09, 0x21, // Usage (left motor)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x26, 0xff, 0x00, // Logical Maximum (255)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x91, 0x02, // Output (Data,Var,Abs)
|
||||
|
||||
0x09, 0x22, // Usage (right motor)
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x91, 0x02, // Output (Data,Var,Abs)
|
||||
|
||||
// Padding to the standard 64-byte report size.
|
||||
0x75, 0x08, // Report Size (8)
|
||||
0x95, 0x3c, // Report Count (60)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x26, 0xff, 0x00, // Logical Maximum (255)
|
||||
0x91, 0x01, // Output (Const,Var,Abs)
|
||||
|
||||
0xc0 // End Collection
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
auto xbox_one_report_descriptor() -> std::span<std::uint8_t const>
|
||||
{
|
||||
return k_descriptor;
|
||||
}
|
||||
|
||||
auto make_input_report(gamepad_input const& in)
|
||||
-> std::array<std::uint8_t, k_input_report_size>
|
||||
{
|
||||
std::array<std::uint8_t, k_input_report_size> report{};
|
||||
report[0] = k_report_id;
|
||||
|
||||
std::uint16_t buttons = 0;
|
||||
for (auto const& [gip, hid] : k_button_map) {
|
||||
if ((in.buttons & gip) != 0)
|
||||
buttons |= hid;
|
||||
}
|
||||
store_le16(report, k_in_buttons, buttons);
|
||||
report[k_in_guide] = in.guide_down ? 1 : 0;
|
||||
report[k_in_trigger_left] = scale_trigger(in.trigger_left);
|
||||
report[k_in_trigger_right] = scale_trigger(in.trigger_right);
|
||||
store_le16(report, k_in_stick_left_x,
|
||||
static_cast<std::uint16_t>(in.stick_left_x));
|
||||
store_le16(report, k_in_stick_left_y,
|
||||
static_cast<std::uint16_t>(in.stick_left_y));
|
||||
store_le16(report, k_in_stick_right_x,
|
||||
static_cast<std::uint16_t>(in.stick_right_x));
|
||||
store_le16(report, k_in_stick_right_y,
|
||||
static_cast<std::uint16_t>(in.stick_right_y));
|
||||
return report;
|
||||
}
|
||||
|
||||
auto parse_output_report(std::span<std::uint8_t const> report)
|
||||
-> motor_values
|
||||
{
|
||||
if (report.size() != k_output_report_size || report[0] != k_report_id)
|
||||
return {};
|
||||
return {report[k_out_motor_left], report[k_out_motor_right]};
|
||||
}
|
||||
|
||||
} // namespace xone::hid
|
||||
Reference in New Issue
Block a user