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:
+2
-1
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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}"
|
||||
|
||||
@@ -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