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() endif()
cmake_minimum_required(VERSION 3.21) 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) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
@@ -71,6 +71,7 @@ target_link_libraries(xone_gip PUBLIC xone_auth xone_mt76)
# Virtual HID gamepad presentation # Virtual HID gamepad presentation
add_library(xone_hid STATIC add_library(xone_hid STATIC
"src/hid/hid_device.cpp" "src/hid/hid_device.cpp"
"src/hid/hid_report.cpp"
) )
target_include_directories(xone_hid PUBLIC "${CMAKE_SOURCE_DIR}/include") target_include_directories(xone_hid PUBLIC "${CMAKE_SOURCE_DIR}/include")
target_compile_features(xone_hid PRIVATE cxx_std_23) 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 - C API + Swift app: async session (fast probe; firmware + radio on a worker
thread), state display (idle/starting/ready/error), controller list with thread), state display (idle/starting/ready/error), controller list with
live input monitor, battery level, and a rumble test button. 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. 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 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 - `ieee80211_*` → custom 802.11 frame builders
- `cfg80211_*` → nothing (no regulatory domain reporting needed) - `cfg80211_*` → nothing (no regulatory domain reporting needed)
### Phase 4: Virtual HID Gamepad (new) ### Phase 4: Virtual HID Gamepad (in progress)
**src/hid/** **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 - **IOHIDSystem user-space**: Create virtual HID device entirely in
user-space. May not work for all games. user-space. May not work for all games.
- **Gamepad wrapper**: Lower-level, translate input events to HID reports. - **Gamepad wrapper**: Lower-level, translate input events to HID reports.
- Map Xbox controller buttons/sticks/triggers to standard Xbox 360/One HID - Map Xbox controller buttons/sticks/triggers to standard Xbox One HID
report descriptor report descriptor: done. `hid/hid_report.hpp` defines the 64-byte
- Handle force feedback (rumble): send back to dongle via GIP 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 - 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) ### Phase 5: macOS App (new)
+5 -1
View File
@@ -7,10 +7,14 @@
// Preferred path: DriverKit extension implementing IOHIDDriver with an // Preferred path: DriverKit extension implementing IOHIDDriver with an
// Xbox 360/One-compatible report descriptor (input reports from GIP, output // Xbox 360/One-compatible report descriptor (input reports from GIP, output
// reports for rumble relayed back via GIP). // 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 { namespace xone::hid {
// TODO(phase 4): report descriptor + virtual device presentation. // TODO(phase 4): virtual device presentation (DriverKit IOHIDDriver).
} // namespace xone::hid } // 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) add_test(NAME test_gip COMMAND test_gip)
list(APPEND TEST_TARGETS 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 add_custom_target(check
COMMAND ${CMAKE_CTEST_COMMAND} COMMAND ${CMAKE_CTEST_COMMAND}
--test-dir "${CMAKE_BINARY_DIR}" --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();
}