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:
portersky
2026-08-29 15:39:03 +02:00
parent 73c026218d
commit fafca4c265
7 changed files with 534 additions and 6 deletions
+176
View File
@@ -0,0 +1,176 @@
#include "unity.h"
#include <array>
#include <cstddef>
#include <cstdint>
#include "hid/hid_report.hpp"
void setUp() {}
void tearDown() {}
namespace {
using xone::hid::gamepad_input;
using xone::hid::make_input_report;
using xone::hid::parse_output_report;
using xone::hid::xbox_one_report_descriptor;
// GIP bitmask with every button but the guide pressed.
constexpr std::uint16_t k_gip_all_buttons =
0x0004 | // start
0x0008 | // select
0x0010 | // a
0x0020 | // b
0x0040 | // x
0x0080 | // y
0x0100 | // dpad up
0x0200 | // dpad down
0x0400 | // dpad left
0x0800 | // dpad right
0x1000 | // bumper left
0x2000 | // bumper right
0x4000 | // stick left
0x8000; // stick right
} // namespace
void test_input_report_idle(void)
{
auto report = make_input_report(gamepad_input{});
TEST_ASSERT_EQUAL_UINT8(1, report[0]);
for (std::size_t i = 1; i < report.size(); ++i)
TEST_ASSERT_EQUAL_UINT8(0, report[i]);
}
void test_input_report_all_buttons(void)
{
gamepad_input in;
in.buttons = k_gip_all_buttons;
in.guide_down = true;
auto report = make_input_report(in);
// Every HID button bit (0..13) set: LE16 0x3FFF.
TEST_ASSERT_EQUAL_UINT8(0xff, report[1]);
TEST_ASSERT_EQUAL_UINT8(0x3f, report[2]);
TEST_ASSERT_EQUAL_UINT8(1, report[3]);
}
void test_input_report_button_remap(void)
{
// GIP A (bit 4) -> HID bit 0.
gamepad_input in;
in.buttons = 0x0010;
auto report = make_input_report(in);
TEST_ASSERT_EQUAL_UINT8(0x01, report[1]);
TEST_ASSERT_EQUAL_UINT8(0x00, report[2]);
// GIP Select (bit 3) -> HID Back (bit 8).
in.buttons = 0x0008;
report = make_input_report(in);
TEST_ASSERT_EQUAL_UINT8(0x00, report[1]);
TEST_ASSERT_EQUAL_UINT8(0x01, report[2]);
// GIP Dpad right (bit 11) -> HID bit 13.
in.buttons = 0x0800;
report = make_input_report(in);
TEST_ASSERT_EQUAL_UINT8(0x00, report[1]);
TEST_ASSERT_EQUAL_UINT8(0x20, report[2]);
}
void test_input_report_triggers(void)
{
gamepad_input in;
in.trigger_left = 511;
in.trigger_right = 512;
auto report = make_input_report(in);
TEST_ASSERT_EQUAL_UINT8(127, report[4]);
TEST_ASSERT_EQUAL_UINT8(128, report[5]);
in.trigger_left = 0;
in.trigger_right = 1023;
report = make_input_report(in);
TEST_ASSERT_EQUAL_UINT8(0, report[4]);
TEST_ASSERT_EQUAL_UINT8(255, report[5]);
// Values above the 10-bit range clamp to full.
in.trigger_left = 0xffff;
in.trigger_right = 0;
report = make_input_report(in);
TEST_ASSERT_EQUAL_UINT8(255, report[4]);
TEST_ASSERT_EQUAL_UINT8(0, report[5]);
}
void test_input_report_sticks(void)
{
gamepad_input in;
in.stick_left_x = -1;
in.stick_left_y = 32767;
in.stick_right_x = -32768;
in.stick_right_y = 0;
auto report = make_input_report(in);
TEST_ASSERT_EQUAL_UINT8(0xff, report[6]);
TEST_ASSERT_EQUAL_UINT8(0xff, report[7]);
TEST_ASSERT_EQUAL_UINT8(0xff, report[8]);
TEST_ASSERT_EQUAL_UINT8(0x7f, report[9]);
TEST_ASSERT_EQUAL_UINT8(0x00, report[10]);
TEST_ASSERT_EQUAL_UINT8(0x80, report[11]);
}
void test_output_report_parse(void)
{
std::array<std::uint8_t, 64> report{};
report[0] = 1;
report[2] = 0x80;
report[3] = 0xc0;
auto motors = parse_output_report(report);
TEST_ASSERT_EQUAL_UINT8(0x80, motors.left);
TEST_ASSERT_EQUAL_UINT8(0xc0, motors.right);
}
void test_output_report_rejects_malformed(void)
{
std::array<std::uint8_t, 64> report{};
report[0] = 2; // wrong report ID
report[2] = 0x80;
auto motors = parse_output_report(report);
TEST_ASSERT_EQUAL_UINT8(0, motors.left);
TEST_ASSERT_EQUAL_UINT8(0, motors.right);
std::array<std::uint8_t, 10> short_report{};
short_report[0] = 1;
short_report[2] = 0x80;
motors = parse_output_report(short_report);
TEST_ASSERT_EQUAL_UINT8(0, motors.left);
TEST_ASSERT_EQUAL_UINT8(0, motors.right);
}
void test_descriptor_sanity(void)
{
auto desc = xbox_one_report_descriptor();
TEST_ASSERT_EQUAL_INT(167, static_cast<int>(desc.size()));
// Generic Desktop / Game Pad / Application collection.
TEST_ASSERT_EQUAL_UINT8(0x05, desc[0]);
TEST_ASSERT_EQUAL_UINT8(0x01, desc[1]);
TEST_ASSERT_EQUAL_UINT8(0x09, desc[2]);
TEST_ASSERT_EQUAL_UINT8(0x04, desc[3]);
TEST_ASSERT_EQUAL_UINT8(0xa1, desc[4]);
TEST_ASSERT_EQUAL_UINT8(0x01, desc[5]);
// Ends with End Collection.
TEST_ASSERT_EQUAL_UINT8(0xc0, desc.back());
}
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_input_report_idle);
RUN_TEST(test_input_report_all_buttons);
RUN_TEST(test_input_report_button_remap);
RUN_TEST(test_input_report_triggers);
RUN_TEST(test_input_report_sticks);
RUN_TEST(test_output_report_parse);
RUN_TEST(test_output_report_rejects_malformed);
RUN_TEST(test_descriptor_sanity);
return UNITY_END();
}