From fafca4c2652ac5f95863fd2947b2e053556ce45e Mon Sep 17 00:00:00 2001 From: portersky Date: Sat, 29 Aug 2026 15:39:03 +0200 Subject: [PATCH] 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 --- CMakeLists.txt | 3 +- PLAN.md | 18 ++- include/hid/hid_device.hpp | 6 +- include/hid/hid_report.hpp | 89 ++++++++++++++ src/hid/hid_report.cpp | 241 +++++++++++++++++++++++++++++++++++++ tests/CMakeLists.txt | 7 ++ tests/test_hid.cpp | 176 +++++++++++++++++++++++++++ 7 files changed, 534 insertions(+), 6 deletions(-) create mode 100644 include/hid/hid_report.hpp create mode 100644 src/hid/hid_report.cpp create mode 100644 tests/test_hid.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f50c206..180e27c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ if(NOT CMAKE_GENERATOR MATCHES "^(Ninja|Xcode)$") endif() cmake_minimum_required(VERSION 3.21) -project(xone_macos VERSION 0.1.27 LANGUAGES CXX Swift) +project(xone_macos VERSION 0.1.28 LANGUAGES CXX Swift) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -71,6 +71,7 @@ target_link_libraries(xone_gip PUBLIC xone_auth xone_mt76) # Virtual HID gamepad presentation add_library(xone_hid STATIC "src/hid/hid_device.cpp" + "src/hid/hid_report.cpp" ) target_include_directories(xone_hid PUBLIC "${CMAKE_SOURCE_DIR}/include") target_compile_features(xone_hid PRIVATE cxx_std_23) diff --git a/PLAN.md b/PLAN.md index 9cec7d4..5dc2875 100644 --- a/PLAN.md +++ b/PLAN.md @@ -49,6 +49,10 @@ and exposes connected controllers as HID gamepads. - C API + Swift app: async session (fast probe; firmware + radio on a worker thread), state display (idle/starting/ready/error), controller list with live input monitor, battery level, and a rumble test button. +- Phase 4 (HID) started: standard Xbox One report layout, HID report + descriptor, and GIP-to-report mapping implemented in `src/hid/` and unit + tested. Remaining: present the reports as a virtual device via a DriverKit + extension (needs a Developer ID with the DriverKit entitlement). The dongle LED is driven at chip level for pairing and client lifecycle. Phase 3 is complete. Next: Phase 4 (virtual HID gamepad) and Phase 5 (app @@ -153,7 +157,7 @@ Extract the protocol logic from Linux kernel code into standalone C. - `ieee80211_*` → custom 802.11 frame builders - `cfg80211_*` → nothing (no regulatory domain reporting needed) -### Phase 4: Virtual HID Gamepad (new) +### Phase 4: Virtual HID Gamepad (in progress) **src/hid/** @@ -164,10 +168,16 @@ Extract the protocol logic from Linux kernel code into standalone C. - **IOHIDSystem user-space**: Create virtual HID device entirely in user-space. May not work for all games. - **Gamepad wrapper**: Lower-level, translate input events to HID reports. -- Map Xbox controller buttons/sticks/triggers to standard Xbox 360/One HID - report descriptor -- Handle force feedback (rumble): send back to dongle via GIP +- Map Xbox controller buttons/sticks/triggers to standard Xbox One HID + report descriptor: done. `hid/hid_report.hpp` defines the 64-byte + input/output report layout and the HID report descriptor; GIP state maps + to input reports via `make_input_report()` (unit tested). +- Handle force feedback (rumble): send back to dongle via GIP. TX is done + (Phase 3); output-report parsing for the relay is implemented in + `parse_output_report()`. - Battery status reporting +- Remaining: present the reports as a virtual device (DriverKit extension). + Needs a Developer ID with the DriverKit entitlement to build and load. ### Phase 5: macOS App (new) diff --git a/include/hid/hid_device.hpp b/include/hid/hid_device.hpp index 3d43d5a..00dd225 100644 --- a/include/hid/hid_device.hpp +++ b/include/hid/hid_device.hpp @@ -7,10 +7,14 @@ // Preferred path: DriverKit extension implementing IOHIDDriver with an // Xbox 360/One-compatible report descriptor (input reports from GIP, output // reports for rumble relayed back via GIP). +// +// The standard Xbox One report layout and its HID report descriptor live in +// hid/hid_report.hpp; this module still needs the virtual device that +// presents them to macOS. // ============================================================================== namespace xone::hid { -// TODO(phase 4): report descriptor + virtual device presentation. +// TODO(phase 4): virtual device presentation (DriverKit IOHIDDriver). } // namespace xone::hid diff --git a/include/hid/hid_report.hpp b/include/hid/hid_report.hpp new file mode 100644 index 0000000..764ec6c --- /dev/null +++ b/include/hid/hid_report.hpp @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include +#include + +// ============================================================================== +// Standard Xbox One HID report layout +// ============================================================================== +// The virtual gamepad presents the standard Xbox One controller report +// layout so games that expect an Xbox controller (raw HID or via the +// GameController framework) recognize it natively. +// +// Input report (ID 1, 64 bytes): +// [0] report ID +// [1..2] buttons, little-endian u16: +// A,B,X,Y,LB,RB,LS,RS,Back,Start,DpadU,DpadD,DpadL,DpadR +// [3] guide button (0/1) +// [4] left trigger (8-bit, HID usage Z) +// [5] right trigger (8-bit, HID usage Rz) +// [6..7] left stick X (s16 LE, usage X) +// [8..9] left stick Y (s16 LE, usage Y) +// [10..11] right stick X (s16 LE, usage Rx) +// [12..13] right stick Y (s16 LE, usage Ry) +// [14..63] reserved (zero) +// +// Output report (ID 1, 64 bytes): +// [0] report ID +// [1] reserved +// [2] left motor intensity (8-bit) +// [3] right motor intensity (8-bit) +// [4..63] reserved (zero) +// ============================================================================== + +namespace xone::hid { + +constexpr std::uint8_t k_report_id = 1; +constexpr std::size_t k_input_report_size = 64; +constexpr std::size_t k_output_report_size = 64; + +// Offsets within the input report. +constexpr std::size_t k_in_buttons = 1; // u16 LE +constexpr std::size_t k_in_guide = 3; +constexpr std::size_t k_in_trigger_left = 4; +constexpr std::size_t k_in_trigger_right = 5; +constexpr std::size_t k_in_stick_left_x = 6; // s16 LE +constexpr std::size_t k_in_stick_left_y = 8; +constexpr std::size_t k_in_stick_right_x = 10; +constexpr std::size_t k_in_stick_right_y = 12; + +// Offsets within the output report. +constexpr std::size_t k_out_motor_left = 2; +constexpr std::size_t k_out_motor_right = 3; + +// HID report descriptor matching the layout above (input + output). +auto xbox_one_report_descriptor() -> std::span; + +// Normalized controller input, as tracked from GIP events. Stick Y is +// already oriented so up is positive; triggers are 10-bit values (0..1023) +// in the low bits of the u16. Buttons use the GIP bitmask (A=bit4 ... +// RS=bit15, Start=bit2, Select=bit3). +struct gamepad_input { + std::uint16_t buttons = 0; + bool guide_down = false; + std::uint16_t trigger_left = 0; + std::uint16_t trigger_right = 0; + std::int16_t stick_left_x = 0; + std::int16_t stick_left_y = 0; + std::int16_t stick_right_x = 0; + std::int16_t stick_right_y = 0; +}; + +// Build a standard Xbox One input report from normalized GIP state. +auto make_input_report(gamepad_input const& in) + -> std::array; + +// Motor intensities parsed from an output report (0..255 each). +struct motor_values { + std::uint8_t left = 0; + std::uint8_t right = 0; +}; + +// Parse a standard Xbox One output report. Returns zeroed motors (which +// stops the rumble) if the report ID or size does not match. +auto parse_output_report(std::span report) + -> motor_values; + +} // namespace xone::hid diff --git a/src/hid/hid_report.cpp b/src/hid/hid_report.cpp new file mode 100644 index 0000000..6c4253e --- /dev/null +++ b/src/hid/hid_report.cpp @@ -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(v) * k_trigger_max_8bit + + (k_trigger_max_10bit / 2); + return static_cast(scaled / k_trigger_max_10bit); +} + +auto store_le16(std::array& report, + std::size_t offset, std::uint16_t value) -> void +{ + report[offset] = static_cast(value & 0xff); + report[offset + 1] = static_cast(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 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 +{ + return k_descriptor; +} + +auto make_input_report(gamepad_input const& in) + -> std::array +{ + std::array 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(in.stick_left_x)); + store_le16(report, k_in_stick_left_y, + static_cast(in.stick_left_y)); + store_le16(report, k_in_stick_right_x, + static_cast(in.stick_right_x)); + store_le16(report, k_in_stick_right_y, + static_cast(in.stick_right_y)); + return report; +} + +auto parse_output_report(std::span 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 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cd6dbcf..bb8edf6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -50,6 +50,13 @@ target_compile_features(test_gip PRIVATE cxx_std_23) add_test(NAME test_gip COMMAND test_gip) list(APPEND TEST_TARGETS test_gip) +add_executable(test_hid test_hid.cpp) +target_include_directories(test_hid PRIVATE "${CMAKE_SOURCE_DIR}/include") +target_link_libraries(test_hid PRIVATE xone_hid Unity::Unity) +target_compile_features(test_hid PRIVATE cxx_std_23) +add_test(NAME test_hid COMMAND test_hid) +list(APPEND TEST_TARGETS test_hid) + add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --test-dir "${CMAKE_BINARY_DIR}" diff --git a/tests/test_hid.cpp b/tests/test_hid.cpp new file mode 100644 index 0000000..396178a --- /dev/null +++ b/tests/test_hid.cpp @@ -0,0 +1,176 @@ +#include "unity.h" + +#include +#include +#include + +#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 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 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 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(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(); +}