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,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();
|
||||
}
|
||||
Reference in New Issue
Block a user