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
+2 -1
View File
@@ -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)
+14 -4
View File
@@ -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)
+5 -1
View File
@@ -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
+89
View File
@@ -0,0 +1,89 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <span>
// ==============================================================================
// 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<std::uint8_t const>;
// 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<std::uint8_t, k_input_report_size>;
// 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<std::uint8_t const> report)
-> motor_values;
} // namespace xone::hid
+241
View File
@@ -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
+7
View File
@@ -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}"
+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();
}