Add DInput and Mac generic HID modes
This commit is contained in:
parent
ec9359bce8
commit
67801cc52a
22 changed files with 1926 additions and 75 deletions
|
|
@ -118,6 +118,7 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
|||
pico_configuration_storage.cpp
|
||||
usb_configuration_management.cpp
|
||||
xinput_driver.cpp
|
||||
generic_hid_driver.cpp
|
||||
)
|
||||
target_compile_definitions(switch-pico PRIVATE
|
||||
SWITCH_PICO_BLUEPAD32=1
|
||||
|
|
|
|||
|
|
@ -54,26 +54,40 @@ void adapter_host_probe_init(AdapterRequestedMode requested_mode) {
|
|||
case AdapterRequestedMode::kXInput:
|
||||
g_mode = AdapterUsbMode::kXInput;
|
||||
break;
|
||||
case AdapterRequestedMode::kDInput:
|
||||
g_mode = AdapterUsbMode::kDInput;
|
||||
break;
|
||||
case AdapterRequestedMode::kMac:
|
||||
g_mode = AdapterUsbMode::kMac;
|
||||
break;
|
||||
case AdapterRequestedMode::kAuto:
|
||||
g_mode = scratch == kXInputBootMagic
|
||||
? AdapterUsbMode::kXInput
|
||||
: AdapterUsbMode::kSwitchProbe;
|
||||
break;
|
||||
case AdapterRequestedMode::kDInput:
|
||||
case AdapterRequestedMode::kMac:
|
||||
// Keep USB usable without treating unavailable explicit choices
|
||||
// as Auto. Host/controller setters reject these until drivers land.
|
||||
g_mode = AdapterUsbMode::kSwitch;
|
||||
break;
|
||||
}
|
||||
g_probe = {};
|
||||
g_reboot_alarm = 0;
|
||||
PROBE_LOG("[HOST PROBE] boot mode=%s\n",
|
||||
g_mode == AdapterUsbMode::kXInput
|
||||
? "XInput"
|
||||
: (g_mode == AdapterUsbMode::kSwitch
|
||||
? "Switch"
|
||||
: "Switch probe"));
|
||||
#ifdef SWITCH_PICO_LOG
|
||||
const char* mode_name = "Switch probe";
|
||||
switch (g_mode) {
|
||||
case AdapterUsbMode::kSwitch:
|
||||
mode_name = "Switch";
|
||||
break;
|
||||
case AdapterUsbMode::kSwitchProbe:
|
||||
break;
|
||||
case AdapterUsbMode::kXInput:
|
||||
mode_name = "XInput";
|
||||
break;
|
||||
case AdapterUsbMode::kDInput:
|
||||
mode_name = "DInput";
|
||||
break;
|
||||
case AdapterUsbMode::kMac:
|
||||
mode_name = "Mac";
|
||||
break;
|
||||
}
|
||||
PROBE_LOG("[HOST PROBE] boot mode=%s\n", mode_name);
|
||||
#endif
|
||||
}
|
||||
|
||||
AdapterUsbMode adapter_host_probe_mode() { return g_mode; }
|
||||
|
|
|
|||
|
|
@ -110,14 +110,17 @@ AdapterRequestedMode next_mode(AdapterRequestedMode mode) {
|
|||
case AdapterRequestedMode::kSwitch:
|
||||
return AdapterRequestedMode::kXInput;
|
||||
case AdapterRequestedMode::kXInput:
|
||||
return AdapterRequestedMode::kDInput;
|
||||
case AdapterRequestedMode::kDInput:
|
||||
return AdapterRequestedMode::kMac;
|
||||
case AdapterRequestedMode::kMac:
|
||||
return AdapterRequestedMode::kAuto;
|
||||
}
|
||||
return AdapterRequestedMode::kAuto;
|
||||
}
|
||||
|
||||
uint8_t feedback_pulse_count(AdapterRequestedMode mode) {
|
||||
// Profile numbers select both a bounded pulse count and the existing color.
|
||||
uint8_t feedback_profile_number(AdapterRequestedMode mode) {
|
||||
switch (mode) {
|
||||
case AdapterRequestedMode::kAuto:
|
||||
return 1;
|
||||
|
|
@ -127,11 +130,20 @@ uint8_t feedback_pulse_count(AdapterRequestedMode mode) {
|
|||
return 3;
|
||||
case AdapterRequestedMode::kDInput:
|
||||
case AdapterRequestedMode::kMac:
|
||||
return 1;
|
||||
return 4;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Mac reuses the fourth bounded pulse/color identity but is LED-only, making
|
||||
// its confirmation tuple distinct from DInput without exceeding profile bounds.
|
||||
ControllerProfileConfirmationPolicy feedback_policy(
|
||||
AdapterRequestedMode mode) {
|
||||
return mode == AdapterRequestedMode::kMac
|
||||
? ControllerProfileConfirmationPolicy::kLed
|
||||
: ControllerProfileConfirmationPolicy::kRumbleAndLed;
|
||||
}
|
||||
|
||||
void clear_mode_chord(ControllerState* state) {
|
||||
if (state == nullptr) {
|
||||
return;
|
||||
|
|
@ -210,12 +222,14 @@ void finish_successful_mode_write(uint32_t now_ms) {
|
|||
}
|
||||
|
||||
controller_profile_runtime_reset();
|
||||
const uint8_t pulses = feedback_pulse_count(g_operation.target_mode);
|
||||
const uint8_t feedback_profile =
|
||||
feedback_profile_number(g_operation.target_mode);
|
||||
bluepad32_input_backend_queue_profile_feedback(
|
||||
g_operation.slot, g_operation.connection_generation, pulses,
|
||||
ControllerProfileConfirmationPolicy::kRumbleAndLed);
|
||||
g_operation.slot, g_operation.connection_generation,
|
||||
feedback_profile, feedback_policy(g_operation.target_mode));
|
||||
g_operation.feedback_deadline_ms =
|
||||
now_ms + static_cast<uint32_t>(pulses) * 2u * kFeedbackPhaseMs +
|
||||
now_ms + static_cast<uint32_t>(feedback_profile) * 2u *
|
||||
kFeedbackPhaseMs +
|
||||
kFeedbackGuardMs;
|
||||
g_operation.phase = ModeOperationPhase::kAcknowledge;
|
||||
}
|
||||
|
|
@ -268,7 +282,7 @@ void advance_mode_write(uint32_t now_ms) {
|
|||
|
||||
const AdapterModeAvailability& adapter_usb_mode_availability() {
|
||||
static constexpr AdapterModeAvailability kAvailability{
|
||||
true, true, false, false};
|
||||
true, true, true, true};
|
||||
return kAvailability;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ enum class AdapterUsbMode : uint8_t {
|
|||
kSwitch = 0,
|
||||
kSwitchProbe = 1,
|
||||
kXInput = 2,
|
||||
kDInput = 3,
|
||||
kMac = 4,
|
||||
};
|
||||
|
||||
// Availability of USB mode implementations in this firmware build. All mode
|
||||
|
|
|
|||
242
generic_hid_descriptors.h
Normal file
242
generic_hid_descriptors.h
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "controller_state.h"
|
||||
|
||||
#ifndef SWITCH_PICO_HID_INSTANCE_COUNT
|
||||
#define SWITCH_PICO_HID_INSTANCE_COUNT 1
|
||||
#endif
|
||||
|
||||
#if SWITCH_PICO_HID_INSTANCE_COUNT < 1 || SWITCH_PICO_HID_INSTANCE_COUNT > 4
|
||||
#error "SWITCH_PICO_HID_INSTANCE_COUNT must be between 1 and 4"
|
||||
#endif
|
||||
|
||||
namespace GenericHid {
|
||||
|
||||
constexpr uint16_t kDevelopmentVendorId = 0xcafe;
|
||||
constexpr uint16_t kDInputDevelopmentProductId = 0x4020;
|
||||
constexpr uint16_t kMacDevelopmentProductId = 0x4021;
|
||||
constexpr uint16_t kDevelopmentDeviceRevision = 0x0100;
|
||||
|
||||
constexpr uint8_t kReportSize = 15;
|
||||
constexpr uint8_t kEndpointSize = kReportSize;
|
||||
constexpr uint8_t kEndpointIntervalMs = 1;
|
||||
constexpr uint8_t kInterfaceDescriptorSize = 25;
|
||||
constexpr uint16_t kConfigurationDescriptorSize =
|
||||
9 + SWITCH_PICO_HID_INSTANCE_COUNT * kInterfaceDescriptorSize;
|
||||
|
||||
constexpr uint8_t kHatUp = 0;
|
||||
constexpr uint8_t kHatUpRight = 1;
|
||||
constexpr uint8_t kHatRight = 2;
|
||||
constexpr uint8_t kHatDownRight = 3;
|
||||
constexpr uint8_t kHatDown = 4;
|
||||
constexpr uint8_t kHatDownLeft = 5;
|
||||
constexpr uint8_t kHatLeft = 6;
|
||||
constexpr uint8_t kHatUpLeft = 7;
|
||||
constexpr uint8_t kHatCenter = 8;
|
||||
|
||||
constexpr uint16_t kButtonSouth = 1u << 0u;
|
||||
constexpr uint16_t kButtonEast = 1u << 1u;
|
||||
constexpr uint16_t kButtonWest = 1u << 2u;
|
||||
constexpr uint16_t kButtonNorth = 1u << 3u;
|
||||
constexpr uint16_t kButtonLeftShoulder = 1u << 4u;
|
||||
constexpr uint16_t kButtonRightShoulder = 1u << 5u;
|
||||
constexpr uint16_t kButtonSelect = 1u << 6u;
|
||||
constexpr uint16_t kButtonStart = 1u << 7u;
|
||||
constexpr uint16_t kButtonLeftStick = 1u << 8u;
|
||||
constexpr uint16_t kButtonRightStick = 1u << 9u;
|
||||
constexpr uint16_t kButtonSystem = 1u << 10u;
|
||||
constexpr uint16_t kButtonCapture = 1u << 11u;
|
||||
constexpr uint16_t kDefinedButtonMask = 0x0fffu;
|
||||
|
||||
struct InputReport {
|
||||
uint8_t data[kReportSize];
|
||||
};
|
||||
|
||||
static_assert(sizeof(InputReport) == kReportSize);
|
||||
|
||||
constexpr void write_u16_le(uint8_t* destination, uint16_t value) {
|
||||
destination[0] = static_cast<uint8_t>(value);
|
||||
destination[1] = static_cast<uint8_t>(value >> 8u);
|
||||
}
|
||||
|
||||
constexpr uint8_t build_hat(const ControllerState& state) {
|
||||
const bool up = state.dpad_up && !state.dpad_down;
|
||||
const bool down = state.dpad_down && !state.dpad_up;
|
||||
const bool left = state.dpad_left && !state.dpad_right;
|
||||
const bool right = state.dpad_right && !state.dpad_left;
|
||||
|
||||
if (up) {
|
||||
if (right) {
|
||||
return kHatUpRight;
|
||||
}
|
||||
if (left) {
|
||||
return kHatUpLeft;
|
||||
}
|
||||
return kHatUp;
|
||||
}
|
||||
if (down) {
|
||||
if (right) {
|
||||
return kHatDownRight;
|
||||
}
|
||||
if (left) {
|
||||
return kHatDownLeft;
|
||||
}
|
||||
return kHatDown;
|
||||
}
|
||||
if (right) {
|
||||
return kHatRight;
|
||||
}
|
||||
if (left) {
|
||||
return kHatLeft;
|
||||
}
|
||||
return kHatCenter;
|
||||
}
|
||||
|
||||
constexpr uint16_t build_buttons(const ControllerState& state) {
|
||||
return static_cast<uint16_t>(
|
||||
(state.button_south ? kButtonSouth : 0u) |
|
||||
(state.button_east ? kButtonEast : 0u) |
|
||||
(state.button_west ? kButtonWest : 0u) |
|
||||
(state.button_north ? kButtonNorth : 0u) |
|
||||
(state.button_left_shoulder ? kButtonLeftShoulder : 0u) |
|
||||
(state.button_right_shoulder ? kButtonRightShoulder : 0u) |
|
||||
(state.button_select ? kButtonSelect : 0u) |
|
||||
(state.button_start ? kButtonStart : 0u) |
|
||||
(state.button_left_stick ? kButtonLeftStick : 0u) |
|
||||
(state.button_right_stick ? kButtonRightStick : 0u) |
|
||||
(state.button_system ? kButtonSystem : 0u) |
|
||||
(state.button_capture ? kButtonCapture : 0u));
|
||||
}
|
||||
|
||||
constexpr InputReport build_input_report(const ControllerState& state) {
|
||||
InputReport report{};
|
||||
write_u16_le(report.data + 0,
|
||||
static_cast<uint16_t>(state.left_stick_x));
|
||||
write_u16_le(report.data + 2,
|
||||
static_cast<uint16_t>(state.left_stick_y));
|
||||
write_u16_le(report.data + 4,
|
||||
static_cast<uint16_t>(state.right_stick_x));
|
||||
write_u16_le(report.data + 6,
|
||||
static_cast<uint16_t>(state.right_stick_y));
|
||||
write_u16_le(report.data + 8, state.left_trigger);
|
||||
write_u16_le(report.data + 10, state.right_trigger);
|
||||
report.data[12] = build_hat(state);
|
||||
write_u16_le(report.data + 13, build_buttons(state));
|
||||
return report;
|
||||
}
|
||||
|
||||
inline constexpr char kManufacturerString[] = "Switch Pico";
|
||||
inline constexpr char kDInputProductString[] = "DInput Development";
|
||||
inline constexpr char kDInputSerialString[] = "DINPUT-DEV-4020";
|
||||
inline constexpr char kMacProductString[] = "Mac HID Development";
|
||||
inline constexpr char kMacSerialString[] = "MAC-HID-DEV-4021";
|
||||
|
||||
inline constexpr uint8_t kDInputDeviceDescriptor[] = {
|
||||
0x12, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x40,
|
||||
static_cast<uint8_t>(kDevelopmentVendorId),
|
||||
static_cast<uint8_t>(kDevelopmentVendorId >> 8u),
|
||||
static_cast<uint8_t>(kDInputDevelopmentProductId),
|
||||
static_cast<uint8_t>(kDInputDevelopmentProductId >> 8u),
|
||||
static_cast<uint8_t>(kDevelopmentDeviceRevision),
|
||||
static_cast<uint8_t>(kDevelopmentDeviceRevision >> 8u),
|
||||
0x01, 0x02, 0x03, 0x01,
|
||||
};
|
||||
|
||||
inline constexpr uint8_t kMacDeviceDescriptor[] = {
|
||||
0x12, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x40,
|
||||
static_cast<uint8_t>(kDevelopmentVendorId),
|
||||
static_cast<uint8_t>(kDevelopmentVendorId >> 8u),
|
||||
static_cast<uint8_t>(kMacDevelopmentProductId),
|
||||
static_cast<uint8_t>(kMacDevelopmentProductId >> 8u),
|
||||
static_cast<uint8_t>(kDevelopmentDeviceRevision),
|
||||
static_cast<uint8_t>(kDevelopmentDeviceRevision >> 8u),
|
||||
0x01, 0x02, 0x03, 0x01,
|
||||
};
|
||||
|
||||
// One report per interface, so no Report ID item is needed.
|
||||
inline constexpr uint8_t kReportDescriptor[] = {
|
||||
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||
0x09, 0x05, // Usage (Game Pad)
|
||||
0xa1, 0x01, // Collection (Application)
|
||||
|
||||
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||
0x16, 0x00, 0x80, // Logical Minimum (-32768)
|
||||
0x26, 0xff, 0x7f, // Logical Maximum (32767)
|
||||
0x75, 0x10, // Report Size (16)
|
||||
0x95, 0x04, // Report Count (4)
|
||||
0x09, 0x30, // Usage (X)
|
||||
0x09, 0x31, // Usage (Y)
|
||||
0x09, 0x33, // Usage (Rx)
|
||||
0x09, 0x34, // Usage (Ry)
|
||||
0x81, 0x02, // Input (Data, Variable, Absolute)
|
||||
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x27, 0xff, 0xff, 0x00, 0x00, // Logical Maximum (65535)
|
||||
0x95, 0x02, // Report Count (2)
|
||||
0x09, 0x32, // Usage (Z)
|
||||
0x09, 0x35, // Usage (Rz)
|
||||
0x81, 0x02, // Input (Data, Variable, Absolute)
|
||||
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x07, // Logical Maximum (7)
|
||||
0x35, 0x00, // Physical Minimum (0)
|
||||
0x46, 0x3b, 0x01, // Physical Maximum (315)
|
||||
0x65, 0x14, // Unit (English Rotation, Degrees)
|
||||
0x75, 0x04, // Report Size (4)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x09, 0x39, // Usage (Hat Switch)
|
||||
0x81, 0x42, // Input (Data, Variable, Absolute, Null State)
|
||||
0x75, 0x04, // Report Size (4)
|
||||
0x95, 0x01, // Report Count (1)
|
||||
0x81, 0x03, // Input (Constant, Variable, Absolute)
|
||||
|
||||
0x05, 0x09, // Usage Page (Button)
|
||||
0x15, 0x00, // Logical Minimum (0)
|
||||
0x25, 0x01, // Logical Maximum (1)
|
||||
0x35, 0x00, // Physical Minimum (0)
|
||||
0x45, 0x00, // Physical Maximum (0)
|
||||
0x65, 0x00, // Unit (None)
|
||||
0x19, 0x01, // Usage Minimum (Button 1)
|
||||
0x29, 0x10, // Usage Maximum (Button 16)
|
||||
0x75, 0x01, // Report Size (1)
|
||||
0x95, 0x10, // Report Count (16)
|
||||
0x81, 0x02, // Input (Data, Variable, Absolute)
|
||||
0xc0, // End Collection
|
||||
};
|
||||
|
||||
#define GENERIC_HID_INTERFACE(number, endpoint) \
|
||||
0x09, 0x04, number, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, \
|
||||
0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, \
|
||||
static_cast<uint8_t>(sizeof(kReportDescriptor)), \
|
||||
static_cast<uint8_t>(sizeof(kReportDescriptor) >> 8u), \
|
||||
0x07, 0x05, static_cast<uint8_t>(0x80u | endpoint), 0x03, \
|
||||
kEndpointSize, 0x00, kEndpointIntervalMs
|
||||
|
||||
inline constexpr uint8_t kConfigurationDescriptor[] = {
|
||||
0x09, 0x02,
|
||||
static_cast<uint8_t>(kConfigurationDescriptorSize),
|
||||
static_cast<uint8_t>(kConfigurationDescriptorSize >> 8u),
|
||||
SWITCH_PICO_HID_INSTANCE_COUNT,
|
||||
0x01, 0x00, 0x80, 0xfa,
|
||||
GENERIC_HID_INTERFACE(0x00, 0x01),
|
||||
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 2
|
||||
GENERIC_HID_INTERFACE(0x01, 0x02),
|
||||
#endif
|
||||
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 3
|
||||
GENERIC_HID_INTERFACE(0x02, 0x03),
|
||||
#endif
|
||||
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 4
|
||||
GENERIC_HID_INTERFACE(0x03, 0x04),
|
||||
#endif
|
||||
};
|
||||
|
||||
#undef GENERIC_HID_INTERFACE
|
||||
|
||||
static_assert(sizeof(kDInputDeviceDescriptor) == 18);
|
||||
static_assert(sizeof(kMacDeviceDescriptor) == 18);
|
||||
static_assert(sizeof(kConfigurationDescriptor) == kConfigurationDescriptorSize);
|
||||
|
||||
} // namespace GenericHid
|
||||
81
generic_hid_driver.cpp
Normal file
81
generic_hid_driver.cpp
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
#include "generic_hid_driver.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "generic_hid_descriptors.h"
|
||||
|
||||
namespace {
|
||||
|
||||
struct GenericHidContext {
|
||||
ControllerState input{};
|
||||
GenericHid::InputReport input_report{};
|
||||
};
|
||||
|
||||
GenericHidContext g_contexts[SWITCH_PICO_HID_INSTANCE_COUNT]{};
|
||||
|
||||
GenericHidContext* context_for(uint8_t instance) {
|
||||
if (instance >= SWITCH_PICO_HID_INSTANCE_COUNT) {
|
||||
return nullptr;
|
||||
}
|
||||
return &g_contexts[instance];
|
||||
}
|
||||
|
||||
|
||||
void reset_context(GenericHidContext& context) {
|
||||
context = {};
|
||||
context.input_report = GenericHid::build_input_report(context.input);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void generic_hid_init(uint8_t instance) {
|
||||
GenericHidContext* context = context_for(instance);
|
||||
if (context != nullptr) {
|
||||
reset_context(*context);
|
||||
}
|
||||
}
|
||||
|
||||
void generic_hid_set_input(uint8_t instance, const ControllerState& state) {
|
||||
GenericHidContext* context = context_for(instance);
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
context->input = state;
|
||||
context->input_report = GenericHid::build_input_report(state);
|
||||
}
|
||||
|
||||
bool generic_hid_task(uint8_t instance) {
|
||||
GenericHidContext* context = context_for(instance);
|
||||
return context != nullptr && tud_hid_n_ready(instance) &&
|
||||
tud_hid_n_report(instance, 0, &context->input_report,
|
||||
sizeof(context->input_report));
|
||||
}
|
||||
|
||||
bool generic_hid_is_ready(uint8_t instance) {
|
||||
return context_for(instance) != nullptr &&
|
||||
tud_hid_n_ready(instance);
|
||||
}
|
||||
|
||||
uint16_t generic_hid_get_report(uint8_t instance, uint8_t report_id,
|
||||
hid_report_type_t report_type, uint8_t* buffer,
|
||||
uint16_t requested_length) {
|
||||
const GenericHidContext* context = context_for(instance);
|
||||
if (context == nullptr || report_id != 0 ||
|
||||
report_type != HID_REPORT_TYPE_INPUT || buffer == nullptr ||
|
||||
requested_length == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t report_size = sizeof(context->input_report);
|
||||
if (requested_length < report_size) {
|
||||
report_size = requested_length;
|
||||
}
|
||||
memcpy(buffer, &context->input_report, report_size);
|
||||
return report_size;
|
||||
}
|
||||
|
||||
const uint8_t* generic_hid_report_descriptor(uint8_t instance) {
|
||||
return context_for(instance) == nullptr
|
||||
? nullptr
|
||||
: GenericHid::kReportDescriptor;
|
||||
}
|
||||
16
generic_hid_driver.h
Normal file
16
generic_hid_driver.h
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "controller_state.h"
|
||||
#include "tusb.h"
|
||||
|
||||
void generic_hid_init(uint8_t instance);
|
||||
void generic_hid_set_input(uint8_t instance, const ControllerState& state);
|
||||
bool generic_hid_task(uint8_t instance);
|
||||
bool generic_hid_is_ready(uint8_t instance);
|
||||
|
||||
uint16_t generic_hid_get_report(uint8_t instance, uint8_t report_id,
|
||||
hid_report_type_t report_type, uint8_t* buffer,
|
||||
uint16_t requested_length);
|
||||
const uint8_t* generic_hid_report_descriptor(uint8_t instance);
|
||||
|
|
@ -17,7 +17,12 @@ from typing import Any, Protocol, cast
|
|||
|
||||
import usb.core
|
||||
|
||||
USB_IDENTITIES = ((0x057E, 0x2009), (0xCAFE, 0x4010))
|
||||
USB_IDENTITIES = (
|
||||
(0x057E, 0x2009),
|
||||
(0xCAFE, 0x4010),
|
||||
(0xCAFE, 0x4020),
|
||||
(0xCAFE, 0x4021),
|
||||
)
|
||||
REQUEST_VALUE = 0x5350
|
||||
REQUEST_INDEX = 0x0001
|
||||
PROTOCOL_VERSION = 1
|
||||
|
|
@ -74,11 +79,18 @@ REQUESTED_MODE_XINPUT = 2
|
|||
REQUESTED_MODE_DINPUT = 3
|
||||
REQUESTED_MODE_MAC = 4
|
||||
REQUESTED_MODE_NAMES = ("auto", "switch", "xinput", "dinput", "mac")
|
||||
SELECTABLE_MODE_NAMES = REQUESTED_MODE_NAMES[:3]
|
||||
SELECTABLE_MODE_NAMES = REQUESTED_MODE_NAMES
|
||||
ACTIVE_MODE_SWITCH = 0
|
||||
ACTIVE_MODE_SWITCH_PROBE = 1
|
||||
ACTIVE_MODE_XINPUT = 2
|
||||
ACTIVE_MODE_NAMES = ("Switch", "Switch probe", "XInput")
|
||||
ACTIVE_MODE_DINPUT = 3
|
||||
ACTIVE_MODE_MAC = 4
|
||||
ACTIVE_MODE_NAMES = ("Switch", "Switch probe", "XInput", "DInput", "Mac")
|
||||
# USB management info byte 5 capability flags.
|
||||
CAPABILITY_INPUT = 1 << 0
|
||||
CAPABILITY_RUMBLE = 1 << 1
|
||||
CAPABILITY_MOTION = 1 << 2
|
||||
CAPABILITY_MASK = CAPABILITY_INPUT | CAPABILITY_RUMBLE | CAPABILITY_MOTION
|
||||
PAIRING_RECORD_SIZE = 8
|
||||
PAIRING_RECORD_CAPACITY = 16
|
||||
TRANSPORT_UNKNOWN = 0
|
||||
|
|
@ -167,6 +179,7 @@ class DeviceInfo:
|
|||
firmware_version: tuple[int, int, int]
|
||||
board: int
|
||||
active_mode: int
|
||||
capabilities: int
|
||||
maximum_configuration_size: int
|
||||
|
||||
def mode_name(self) -> str:
|
||||
|
|
@ -177,6 +190,20 @@ class DeviceInfo:
|
|||
f"unknown active USB mode {self.active_mode}"
|
||||
) from exc
|
||||
|
||||
def capability_names(self) -> tuple[str, ...]:
|
||||
if self.capabilities == 0:
|
||||
return ("unreported",)
|
||||
names = ["input"]
|
||||
if self.capabilities & CAPABILITY_RUMBLE:
|
||||
names.append("rumble")
|
||||
if self.capabilities & CAPABILITY_MOTION:
|
||||
names.append("motion")
|
||||
return tuple(names)
|
||||
|
||||
def capability_summary(self) -> str:
|
||||
names = self.capability_names()
|
||||
return "input only" if names == ("input",) else ", ".join(names)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AdapterConfiguration:
|
||||
|
|
@ -1295,11 +1322,20 @@ def _control_out(
|
|||
def read_info(device: UsbDevice) -> DeviceInfo:
|
||||
envelope = _control_in(device, OP_INFO)
|
||||
_raise_status(envelope)
|
||||
if len(envelope.payload) != 8 or envelope.payload[5] != 0:
|
||||
if len(envelope.payload) != 8:
|
||||
raise ConfigManagerError("invalid device-info payload")
|
||||
active_mode = envelope.payload[4]
|
||||
capabilities = envelope.payload[5]
|
||||
if active_mode >= len(ACTIVE_MODE_NAMES):
|
||||
raise ConfigManagerError(f"unknown active USB mode {active_mode}")
|
||||
if capabilities & ~CAPABILITY_MASK:
|
||||
raise ConfigManagerError(
|
||||
f"unknown device capability flags 0x{capabilities:02x}"
|
||||
)
|
||||
if capabilities != 0 and not capabilities & CAPABILITY_INPUT:
|
||||
raise ConfigManagerError(
|
||||
"device capability flags omit required input support"
|
||||
)
|
||||
return DeviceInfo(
|
||||
firmware_version=(
|
||||
envelope.payload[0],
|
||||
|
|
@ -1308,6 +1344,7 @@ def read_info(device: UsbDevice) -> DeviceInfo:
|
|||
),
|
||||
board=envelope.payload[3],
|
||||
active_mode=active_mode,
|
||||
capabilities=capabilities,
|
||||
maximum_configuration_size=struct.unpack_from(
|
||||
"<H", envelope.payload, 6
|
||||
)[0],
|
||||
|
|
@ -1427,6 +1464,8 @@ def set_mode(
|
|||
REQUESTED_MODE_AUTO,
|
||||
REQUESTED_MODE_SWITCH,
|
||||
REQUESTED_MODE_XINPUT,
|
||||
REQUESTED_MODE_DINPUT,
|
||||
REQUESTED_MODE_MAC,
|
||||
):
|
||||
raise ConfigManagerError("requested USB mode is not available")
|
||||
transaction_id = _host_transaction_id()
|
||||
|
|
@ -1452,6 +1491,10 @@ def _mode_is_active(requested_mode: int, active_mode: int) -> bool:
|
|||
return active_mode == ACTIVE_MODE_SWITCH
|
||||
if requested_mode == REQUESTED_MODE_XINPUT:
|
||||
return active_mode == ACTIVE_MODE_XINPUT
|
||||
if requested_mode == REQUESTED_MODE_DINPUT:
|
||||
return active_mode == ACTIVE_MODE_DINPUT
|
||||
if requested_mode == REQUESTED_MODE_MAC:
|
||||
return active_mode == ACTIVE_MODE_MAC
|
||||
return False
|
||||
|
||||
|
||||
|
|
@ -1948,6 +1991,8 @@ def configure_mode(
|
|||
REQUESTED_MODE_AUTO,
|
||||
REQUESTED_MODE_SWITCH,
|
||||
REQUESTED_MODE_XINPUT,
|
||||
REQUESTED_MODE_DINPUT,
|
||||
REQUESTED_MODE_MAC,
|
||||
):
|
||||
raise ConfigManagerError("requested USB mode is not available")
|
||||
before_info = read_info(device)
|
||||
|
|
@ -2204,6 +2249,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||
f"{REQUESTED_MODE_NAMES[configuration.requested_mode]}"
|
||||
)
|
||||
print(f"Active USB mode: {info.mode_name()}")
|
||||
print(f"Mode capabilities: {info.capability_summary()}")
|
||||
print(f"Configuration generation: {configuration.generation}")
|
||||
print(f"Configuration CRC: {configuration.crc:08x}")
|
||||
print(
|
||||
|
|
|
|||
|
|
@ -118,9 +118,25 @@ void test_manual_modes_bypass_and_consume_probe_state() {
|
|||
|
||||
reset_harness(kXInputBootMagic);
|
||||
adapter_host_probe_init(AdapterRequestedMode::kDInput);
|
||||
require(adapter_host_probe_mode() == AdapterUsbMode::kSwitch &&
|
||||
require(adapter_host_probe_mode() == AdapterUsbMode::kDInput &&
|
||||
watchdog_registers.scratch[0] == 0 && alarm_count == 0,
|
||||
"unavailable DInput was selected or treated as Auto");
|
||||
"manual DInput did not bypass and consume stale auto scratch");
|
||||
adapter_host_probe_note_string_descriptor(0xee);
|
||||
require(!adapter_host_probe_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_count == 0 && alarm_count == 0,
|
||||
"manual DInput entered the XInput probe path");
|
||||
|
||||
reset_harness(kXInputBootMagic);
|
||||
adapter_host_probe_init(AdapterRequestedMode::kMac);
|
||||
require(adapter_host_probe_mode() == AdapterUsbMode::kMac &&
|
||||
watchdog_registers.scratch[0] == 0 && alarm_count == 0,
|
||||
"manual Mac did not bypass and consume stale auto scratch");
|
||||
adapter_host_probe_note_string_descriptor(0xee);
|
||||
require(!adapter_host_probe_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_count == 0 && alarm_count == 0,
|
||||
"manual Mac entered the XInput probe path");
|
||||
}
|
||||
|
||||
void test_probe_transition_resets_before_watchdog() {
|
||||
|
|
|
|||
|
|
@ -160,9 +160,9 @@ void test_mode_availability_has_one_stable_value() {
|
|||
const AdapterModeAvailability& second =
|
||||
adapter_usb_mode_availability();
|
||||
require(&first == &second && first.switch_mode &&
|
||||
first.xinput_mode && !first.dinput_mode &&
|
||||
!first.mac_mode,
|
||||
"mode availability was not the shared implemented-mode value");
|
||||
first.xinput_mode && first.dinput_mode &&
|
||||
first.mac_mode,
|
||||
"mode availability was not the shared five-mode value");
|
||||
}
|
||||
|
||||
void test_exact_hold_release_wrap_and_consumption() {
|
||||
|
|
@ -226,11 +226,14 @@ void test_cycle_and_slot_isolation() {
|
|||
AdapterRequestedMode::kXInput,
|
||||
"Switch did not cycle to XInput");
|
||||
require(triggered_target(AdapterRequestedMode::kXInput) ==
|
||||
AdapterRequestedMode::kAuto,
|
||||
"XInput did not cycle to Auto");
|
||||
AdapterRequestedMode::kDInput,
|
||||
"XInput did not cycle to DInput");
|
||||
require(triggered_target(AdapterRequestedMode::kDInput) ==
|
||||
AdapterRequestedMode::kMac,
|
||||
"DInput did not cycle to Mac");
|
||||
require(triggered_target(AdapterRequestedMode::kMac) ==
|
||||
AdapterRequestedMode::kAuto,
|
||||
"unimplemented configured value entered the chord cycle");
|
||||
"Mac did not cycle to Auto");
|
||||
|
||||
reset_harness();
|
||||
begin_hold(0, 10, 0);
|
||||
|
|
@ -328,6 +331,53 @@ void test_commit_feedback_then_reboot() {
|
|||
"scheduled watchdog reboot looped");
|
||||
}
|
||||
|
||||
void test_feedback_distinguishes_all_modes() {
|
||||
struct FeedbackCase {
|
||||
AdapterRequestedMode initial_mode;
|
||||
AdapterRequestedMode target_mode;
|
||||
uint8_t pulses;
|
||||
ControllerProfileConfirmationPolicy policy;
|
||||
};
|
||||
constexpr FeedbackCase cases[] = {
|
||||
{AdapterRequestedMode::kAuto, AdapterRequestedMode::kSwitch, 2,
|
||||
ControllerProfileConfirmationPolicy::kRumbleAndLed},
|
||||
{AdapterRequestedMode::kSwitch, AdapterRequestedMode::kXInput, 3,
|
||||
ControllerProfileConfirmationPolicy::kRumbleAndLed},
|
||||
{AdapterRequestedMode::kXInput, AdapterRequestedMode::kDInput, 4,
|
||||
ControllerProfileConfirmationPolicy::kRumbleAndLed},
|
||||
{AdapterRequestedMode::kDInput, AdapterRequestedMode::kMac, 4,
|
||||
ControllerProfileConfirmationPolicy::kLed},
|
||||
{AdapterRequestedMode::kMac, AdapterRequestedMode::kAuto, 1,
|
||||
ControllerProfileConfirmationPolicy::kRumbleAndLed},
|
||||
};
|
||||
|
||||
for (const FeedbackCase& expected : cases) {
|
||||
reset_harness(expected.initial_mode);
|
||||
begin_hold(1, 23, 0);
|
||||
finish_hold(1, 23, 0);
|
||||
set_results = {ConfigurationTransactionStatus::kCommitted};
|
||||
adapter_mode_controller_task(ADAPTER_MODE_CHORD_HOLD_MS);
|
||||
require(set_modes.size() == 1 &&
|
||||
set_modes[0] == expected.target_mode &&
|
||||
feedback_slot == 1 && feedback_generation == 23 &&
|
||||
feedback_pulses == expected.pulses &&
|
||||
feedback_policy == expected.policy &&
|
||||
reboot_count == 0,
|
||||
"mode feedback tuple did not uniquely identify its target");
|
||||
|
||||
const uint32_t feedback_duration =
|
||||
static_cast<uint32_t>(expected.pulses) * 2u * 75u + 75u;
|
||||
adapter_mode_controller_task(
|
||||
ADAPTER_MODE_CHORD_HOLD_MS + feedback_duration - 1u);
|
||||
require(reboot_count == 0,
|
||||
"mode feedback deadline was shorter than its bounded pulse sequence");
|
||||
adapter_mode_controller_task(
|
||||
ADAPTER_MODE_CHORD_HOLD_MS + feedback_duration);
|
||||
require(reboot_count == 1,
|
||||
"mode feedback did not reboot at its bounded deadline");
|
||||
}
|
||||
}
|
||||
|
||||
void test_configuration_failure_and_correlated_reboot() {
|
||||
reset_harness();
|
||||
begin_hold(0, 1, 0);
|
||||
|
|
@ -512,8 +562,8 @@ ConfigurationTransactionStatus configuration_service_set_mode_internal(
|
|||
"recovery reservation admitted a non-Auto internal mode");
|
||||
calls.push_back(Call::kSetMode);
|
||||
require(availability.switch_mode && availability.xinput_mode &&
|
||||
!availability.dinput_mode && !availability.mac_mode,
|
||||
"mode controller advertised unavailable drivers");
|
||||
availability.dinput_mode && availability.mac_mode,
|
||||
"mode controller did not advertise all compiled drivers");
|
||||
set_transaction_ids.push_back(transaction_id);
|
||||
set_modes.push_back(requested_mode);
|
||||
if (next_set_result >= set_results.size()) {
|
||||
|
|
@ -605,6 +655,7 @@ int main() {
|
|||
test_cycle_and_slot_isolation();
|
||||
test_busy_retry_and_one_shot();
|
||||
test_commit_feedback_then_reboot();
|
||||
test_feedback_distinguishes_all_modes();
|
||||
test_configuration_failure_and_correlated_reboot();
|
||||
test_recovery_auto_wins_host_mode_interleaving();
|
||||
test_failed_recovery_never_reboots();
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ void test_service_lifecycle_and_mutations() {
|
|||
g_flash.erase_count == erases_after_seed,
|
||||
"pre-USB initialization wrote flash");
|
||||
|
||||
const AdapterModeAvailability implemented{};
|
||||
const AdapterModeAvailability implemented{true, true, true, true};
|
||||
require(configuration_service_set_mode(
|
||||
1, AdapterRequestedMode::kSwitch, implemented) ==
|
||||
ConfigurationTransactionStatus::kBusy,
|
||||
|
|
@ -200,8 +200,9 @@ void test_service_lifecycle_and_mutations() {
|
|||
"mode-only commit changed pairing state or host status");
|
||||
|
||||
const int programs_before_rejections = g_flash.program_count;
|
||||
const AdapterModeAvailability unavailable{true, true, false, false};
|
||||
require(configuration_service_set_mode(
|
||||
11, AdapterRequestedMode::kDInput, implemented) ==
|
||||
11, AdapterRequestedMode::kDInput, unavailable) ==
|
||||
ConfigurationTransactionStatus::kUnsupportedSchema &&
|
||||
configuration_service_set_mode(
|
||||
11, static_cast<AdapterRequestedMode>(5), implemented) ==
|
||||
|
|
@ -493,7 +494,7 @@ void test_abandoned_host_receive_does_not_block_recovery() {
|
|||
ConfigurationTransactionStatus::kBusy,
|
||||
"recovery reservation did not terminally cancel host receive");
|
||||
|
||||
const AdapterModeAvailability implemented{};
|
||||
const AdapterModeAvailability implemented{true, true, true, true};
|
||||
constexpr uint32_t kRecoveryAuto = 0x80000040u;
|
||||
require(configuration_service_set_mode_internal(
|
||||
kRecoveryAuto, AdapterRequestedMode::kAuto,
|
||||
|
|
|
|||
548
tests/generic_hid_descriptors_test.cpp
Normal file
548
tests/generic_hid_descriptors_test.cpp
Normal file
|
|
@ -0,0 +1,548 @@
|
|||
#include "generic_hid_descriptors.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
#ifndef EXPECTED_HID_INSTANCE_COUNT
|
||||
#error "EXPECTED_HID_INSTANCE_COUNT must be defined by the test build"
|
||||
#endif
|
||||
|
||||
static_assert(SWITCH_PICO_HID_INSTANCE_COUNT == EXPECTED_HID_INSTANCE_COUNT);
|
||||
static_assert(sizeof(GenericHid::kConfigurationDescriptor) ==
|
||||
9u + 25u * EXPECTED_HID_INSTANCE_COUNT);
|
||||
static_assert(sizeof(GenericHid::InputReport) == 15);
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
|
||||
void expect(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t read_u16(const uint8_t* bytes) {
|
||||
return static_cast<uint16_t>(bytes[0]) |
|
||||
static_cast<uint16_t>(static_cast<uint16_t>(bytes[1]) << 8u);
|
||||
}
|
||||
|
||||
int16_t read_i16(const uint8_t* bytes) {
|
||||
return static_cast<int16_t>(read_u16(bytes));
|
||||
}
|
||||
|
||||
struct ItemGolden {
|
||||
uint8_t type;
|
||||
uint8_t tag;
|
||||
uint8_t size;
|
||||
uint32_t value;
|
||||
};
|
||||
|
||||
constexpr std::array<ItemGolden, 43> kReportItemGolden{{
|
||||
{1, 0, 1, 0x01}, {2, 0, 1, 0x05}, {0, 10, 1, 0x01},
|
||||
{1, 0, 1, 0x01}, {1, 1, 2, 0x8000}, {1, 2, 2, 0x7fff},
|
||||
{1, 7, 1, 0x10}, {1, 9, 1, 0x04}, {2, 0, 1, 0x30},
|
||||
{2, 0, 1, 0x31}, {2, 0, 1, 0x33}, {2, 0, 1, 0x34},
|
||||
{0, 8, 1, 0x02}, {1, 1, 1, 0x00}, {1, 2, 4, 0xffff},
|
||||
{1, 9, 1, 0x02}, {2, 0, 1, 0x32}, {2, 0, 1, 0x35},
|
||||
{0, 8, 1, 0x02}, {1, 1, 1, 0x00}, {1, 2, 1, 0x07},
|
||||
{1, 3, 1, 0x00}, {1, 4, 2, 0x013b}, {1, 6, 1, 0x14},
|
||||
{1, 7, 1, 0x04}, {1, 9, 1, 0x01}, {2, 0, 1, 0x39},
|
||||
{0, 8, 1, 0x42}, {1, 7, 1, 0x04}, {1, 9, 1, 0x01},
|
||||
{0, 8, 1, 0x03}, {1, 0, 1, 0x09}, {1, 1, 1, 0x00},
|
||||
{1, 2, 1, 0x01}, {1, 3, 1, 0x00}, {1, 4, 1, 0x00},
|
||||
{1, 6, 1, 0x00}, {2, 1, 1, 0x01}, {2, 2, 1, 0x10},
|
||||
{1, 7, 1, 0x01}, {1, 9, 1, 0x10}, {0, 8, 1, 0x02},
|
||||
{0, 12, 0, 0x00},
|
||||
}};
|
||||
|
||||
int32_t sign_extend(uint32_t value, uint8_t size) {
|
||||
if (size == 4 || size == 0) {
|
||||
return static_cast<int32_t>(value);
|
||||
}
|
||||
const uint8_t bits = static_cast<uint8_t>(size * 8u);
|
||||
const uint32_t sign = 1u << (bits - 1u);
|
||||
return static_cast<int32_t>((value ^ sign) - sign);
|
||||
}
|
||||
|
||||
struct GlobalState {
|
||||
uint32_t usage_page = 0;
|
||||
int32_t logical_minimum = 0;
|
||||
int64_t logical_maximum = 0;
|
||||
int32_t physical_minimum = 0;
|
||||
int64_t physical_maximum = 0;
|
||||
uint32_t unit = 0;
|
||||
uint32_t report_size = 0;
|
||||
uint32_t report_count = 0;
|
||||
};
|
||||
|
||||
struct LocalState {
|
||||
std::array<uint32_t, 4> usages{};
|
||||
uint8_t usage_count = 0;
|
||||
uint32_t usage_minimum = 0;
|
||||
uint32_t usage_maximum = 0;
|
||||
bool has_usage_range = false;
|
||||
|
||||
void clear() {
|
||||
*this = {};
|
||||
}
|
||||
};
|
||||
|
||||
struct InputField {
|
||||
uint16_t bit_offset;
|
||||
uint8_t size;
|
||||
uint8_t count;
|
||||
uint8_t flags;
|
||||
GlobalState globals;
|
||||
LocalState locals;
|
||||
};
|
||||
|
||||
void inspect_report_descriptor() {
|
||||
const uint8_t* descriptor = GenericHid::kReportDescriptor;
|
||||
const size_t descriptor_size = sizeof(GenericHid::kReportDescriptor);
|
||||
size_t offset = 0;
|
||||
size_t decoded = 0;
|
||||
uint16_t report_bits = 0;
|
||||
uint8_t collection_depth = 0;
|
||||
bool gamepad_application = false;
|
||||
bool saw_report_id = false;
|
||||
bool saw_output = false;
|
||||
bool saw_feature = false;
|
||||
GlobalState globals{};
|
||||
LocalState locals{};
|
||||
std::vector<InputField> fields;
|
||||
|
||||
while (offset < descriptor_size) {
|
||||
const uint8_t prefix = descriptor[offset++];
|
||||
expect(prefix != 0xfe, "long HID item is not part of the golden contract");
|
||||
if (prefix == 0xfe) {
|
||||
break;
|
||||
}
|
||||
uint8_t size = prefix & 0x03u;
|
||||
if (size == 3) {
|
||||
size = 4;
|
||||
}
|
||||
expect(offset + size <= descriptor_size,
|
||||
"HID item extends beyond the report descriptor");
|
||||
if (offset + size > descriptor_size) {
|
||||
break;
|
||||
}
|
||||
uint32_t value = 0;
|
||||
for (uint8_t byte = 0; byte < size; ++byte) {
|
||||
value |= static_cast<uint32_t>(descriptor[offset + byte]) <<
|
||||
(8u * byte);
|
||||
}
|
||||
offset += size;
|
||||
const uint8_t type = static_cast<uint8_t>((prefix >> 2u) & 0x03u);
|
||||
const uint8_t tag = static_cast<uint8_t>(prefix >> 4u);
|
||||
|
||||
expect(decoded < kReportItemGolden.size(),
|
||||
"report descriptor contains an extra HID item");
|
||||
if (decoded < kReportItemGolden.size()) {
|
||||
const ItemGolden& golden = kReportItemGolden[decoded];
|
||||
expect(type == golden.type && tag == golden.tag &&
|
||||
size == golden.size && value == golden.value,
|
||||
"decoded HID item differs from the golden contract");
|
||||
}
|
||||
++decoded;
|
||||
|
||||
if (type == 1) {
|
||||
switch (tag) {
|
||||
case 0:
|
||||
globals.usage_page = value;
|
||||
break;
|
||||
case 1:
|
||||
globals.logical_minimum = sign_extend(value, size);
|
||||
break;
|
||||
case 2:
|
||||
globals.logical_maximum = globals.logical_minimum < 0
|
||||
? sign_extend(value, size)
|
||||
: value;
|
||||
break;
|
||||
case 3:
|
||||
globals.physical_minimum = sign_extend(value, size);
|
||||
break;
|
||||
case 4:
|
||||
globals.physical_maximum = globals.physical_minimum < 0
|
||||
? sign_extend(value, size)
|
||||
: value;
|
||||
break;
|
||||
case 6:
|
||||
globals.unit = value;
|
||||
break;
|
||||
case 7:
|
||||
globals.report_size = value;
|
||||
break;
|
||||
case 8:
|
||||
saw_report_id = true;
|
||||
break;
|
||||
case 9:
|
||||
globals.report_count = value;
|
||||
break;
|
||||
default:
|
||||
expect(false, "unexpected global HID item");
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (type == 2) {
|
||||
if (tag == 0) {
|
||||
expect(locals.usage_count < locals.usages.size(),
|
||||
"too many local usages in the report descriptor");
|
||||
if (locals.usage_count < locals.usages.size()) {
|
||||
locals.usages[locals.usage_count++] = value;
|
||||
}
|
||||
} else if (tag == 1) {
|
||||
locals.usage_minimum = value;
|
||||
locals.has_usage_range = true;
|
||||
} else if (tag == 2) {
|
||||
locals.usage_maximum = value;
|
||||
locals.has_usage_range = true;
|
||||
} else {
|
||||
expect(false, "unexpected local HID item");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
expect(type == 0, "reserved HID item type is present");
|
||||
if (type != 0) {
|
||||
continue;
|
||||
}
|
||||
if (tag == 10) {
|
||||
gamepad_application = collection_depth == 0 && value == 1 &&
|
||||
globals.usage_page == 1 &&
|
||||
locals.usage_count == 1 &&
|
||||
locals.usages[0] == 5;
|
||||
++collection_depth;
|
||||
} else if (tag == 12) {
|
||||
expect(collection_depth > 0, "unbalanced End Collection item");
|
||||
if (collection_depth > 0) {
|
||||
--collection_depth;
|
||||
}
|
||||
} else if (tag == 8) {
|
||||
fields.push_back(InputField{
|
||||
report_bits,
|
||||
static_cast<uint8_t>(globals.report_size),
|
||||
static_cast<uint8_t>(globals.report_count),
|
||||
static_cast<uint8_t>(value),
|
||||
globals,
|
||||
locals,
|
||||
});
|
||||
report_bits = static_cast<uint16_t>(
|
||||
report_bits + globals.report_size * globals.report_count);
|
||||
} else if (tag == 9) {
|
||||
saw_output = true;
|
||||
} else if (tag == 11) {
|
||||
saw_feature = true;
|
||||
} else {
|
||||
expect(false, "unexpected main HID item");
|
||||
}
|
||||
locals.clear();
|
||||
}
|
||||
|
||||
expect(offset == descriptor_size, "report descriptor was not fully decoded");
|
||||
expect(decoded == kReportItemGolden.size(),
|
||||
"report descriptor is missing a golden HID item");
|
||||
expect(gamepad_application && collection_depth == 0,
|
||||
"report is not one balanced Game Pad application collection");
|
||||
expect(!saw_report_id, "single-interface report unexpectedly has a Report ID");
|
||||
expect(!saw_output && !saw_feature,
|
||||
"input-only generic HID descriptor declares output or feature data");
|
||||
expect(report_bits == GenericHid::kReportSize * 8u,
|
||||
"input report does not contain exactly 15 bytes");
|
||||
expect(fields.size() == 5, "input report has the wrong field count");
|
||||
if (fields.size() != 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
const InputField& sticks = fields[0];
|
||||
expect(sticks.bit_offset == 0 && sticks.size == 16 && sticks.count == 4 &&
|
||||
sticks.flags == 0x02 && sticks.globals.usage_page == 1 &&
|
||||
sticks.globals.logical_minimum == -32768 &&
|
||||
sticks.globals.logical_maximum == 32767 &&
|
||||
sticks.locals.usage_count == 4 &&
|
||||
sticks.locals.usages[0] == 0x30 &&
|
||||
sticks.locals.usages[1] == 0x31 &&
|
||||
sticks.locals.usages[2] == 0x33 &&
|
||||
sticks.locals.usages[3] == 0x34,
|
||||
"signed X/Y/Rx/Ry field layout is wrong");
|
||||
|
||||
const InputField& triggers = fields[1];
|
||||
expect(triggers.bit_offset == 64 && triggers.size == 16 &&
|
||||
triggers.count == 2 && triggers.flags == 0x02 &&
|
||||
triggers.globals.usage_page == 1 &&
|
||||
triggers.globals.logical_minimum == 0 &&
|
||||
triggers.globals.logical_maximum == 65535 &&
|
||||
triggers.locals.usage_count == 2 &&
|
||||
triggers.locals.usages[0] == 0x32 &&
|
||||
triggers.locals.usages[1] == 0x35,
|
||||
"unsigned Z/Rz trigger field layout is wrong");
|
||||
|
||||
const InputField& hat = fields[2];
|
||||
expect(hat.bit_offset == 96 && hat.size == 4 && hat.count == 1 &&
|
||||
hat.flags == 0x42 && hat.globals.usage_page == 1 &&
|
||||
hat.globals.logical_minimum == 0 &&
|
||||
hat.globals.logical_maximum == 7 &&
|
||||
hat.globals.physical_minimum == 0 &&
|
||||
hat.globals.physical_maximum == 315 &&
|
||||
hat.globals.unit == 0x14 && hat.locals.usage_count == 1 &&
|
||||
hat.locals.usages[0] == 0x39,
|
||||
"Hat Switch field or declared null-state semantics are wrong");
|
||||
|
||||
const InputField& padding = fields[3];
|
||||
expect(padding.bit_offset == 100 && padding.size == 4 &&
|
||||
padding.count == 1 && padding.flags == 0x03 &&
|
||||
padding.locals.usage_count == 0 &&
|
||||
!padding.locals.has_usage_range,
|
||||
"hat padding is not four constant bits");
|
||||
|
||||
const InputField& buttons = fields[4];
|
||||
expect(buttons.bit_offset == 104 && buttons.size == 1 &&
|
||||
buttons.count == 16 && buttons.flags == 0x02 &&
|
||||
buttons.globals.usage_page == 9 &&
|
||||
buttons.globals.logical_minimum == 0 &&
|
||||
buttons.globals.logical_maximum == 1 &&
|
||||
buttons.globals.unit == 0 && buttons.locals.has_usage_range &&
|
||||
buttons.locals.usage_minimum == 1 &&
|
||||
buttons.locals.usage_maximum == 16,
|
||||
"sequential Button 1..16 field layout is wrong");
|
||||
}
|
||||
|
||||
void inspect_device_descriptors_and_strings() {
|
||||
constexpr std::array<uint8_t, 18> dinput_golden{{
|
||||
0x12, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x40, 0xfe,
|
||||
0xca, 0x20, 0x40, 0x00, 0x01, 0x01, 0x02, 0x03, 0x01,
|
||||
}};
|
||||
constexpr std::array<uint8_t, 18> mac_golden{{
|
||||
0x12, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x40, 0xfe,
|
||||
0xca, 0x21, 0x40, 0x00, 0x01, 0x01, 0x02, 0x03, 0x01,
|
||||
}};
|
||||
expect(std::memcmp(GenericHid::kDInputDeviceDescriptor,
|
||||
dinput_golden.data(), dinput_golden.size()) == 0,
|
||||
"DInput development device descriptor differs from its golden");
|
||||
expect(std::memcmp(GenericHid::kMacDeviceDescriptor, mac_golden.data(),
|
||||
mac_golden.size()) == 0,
|
||||
"Mac development device descriptor differs from its golden");
|
||||
expect(read_u16(GenericHid::kDInputDeviceDescriptor + 8) == 0xcafe &&
|
||||
read_u16(GenericHid::kDInputDeviceDescriptor + 10) == 0x4020 &&
|
||||
read_u16(GenericHid::kMacDeviceDescriptor + 8) == 0xcafe &&
|
||||
read_u16(GenericHid::kMacDeviceDescriptor + 10) == 0x4021,
|
||||
"development VID/PIDs are wrong");
|
||||
expect(std::strcmp(GenericHid::kManufacturerString, "Switch Pico") == 0 &&
|
||||
std::strcmp(GenericHid::kDInputProductString,
|
||||
"DInput Development") == 0 &&
|
||||
std::strcmp(GenericHid::kDInputSerialString,
|
||||
"DINPUT-DEV-4020") == 0 &&
|
||||
std::strcmp(GenericHid::kMacProductString,
|
||||
"Mac HID Development") == 0 &&
|
||||
std::strcmp(GenericHid::kMacSerialString,
|
||||
"MAC-HID-DEV-4021") == 0,
|
||||
"generic HID USB strings differ from their goldens");
|
||||
expect(std::strcmp(GenericHid::kDInputProductString,
|
||||
GenericHid::kMacProductString) != 0 &&
|
||||
std::strcmp(GenericHid::kDInputSerialString,
|
||||
GenericHid::kMacSerialString) != 0,
|
||||
"DInput and Mac identities do not have distinct strings");
|
||||
}
|
||||
|
||||
void inspect_configuration_descriptor() {
|
||||
constexpr uint8_t kConfiguration = 0x02;
|
||||
constexpr uint8_t kInterface = 0x04;
|
||||
constexpr uint8_t kEndpoint = 0x05;
|
||||
constexpr uint8_t kHid = 0x21;
|
||||
const uint8_t* descriptor = GenericHid::kConfigurationDescriptor;
|
||||
const size_t descriptor_size = sizeof(GenericHid::kConfigurationDescriptor);
|
||||
|
||||
expect(descriptor[0] == 9 && descriptor[1] == kConfiguration &&
|
||||
read_u16(descriptor + 2) == descriptor_size &&
|
||||
descriptor[4] == EXPECTED_HID_INSTANCE_COUNT,
|
||||
"generic HID configuration header is malformed");
|
||||
|
||||
std::array<bool, EXPECTED_HID_INSTANCE_COUNT> interfaces{};
|
||||
std::array<bool, 16> endpoints{};
|
||||
std::array<uint8_t, EXPECTED_HID_INSTANCE_COUNT> hid_counts{};
|
||||
std::array<uint8_t, EXPECTED_HID_INSTANCE_COUNT> endpoint_counts{};
|
||||
int current_interface = -1;
|
||||
size_t offset = descriptor[0];
|
||||
while (offset < descriptor_size) {
|
||||
const uint8_t length = descriptor[offset];
|
||||
expect(length >= 2 && offset + length <= descriptor_size,
|
||||
"configuration child descriptor has an invalid length");
|
||||
if (length < 2 || offset + length > descriptor_size) {
|
||||
break;
|
||||
}
|
||||
const uint8_t type = descriptor[offset + 1];
|
||||
if (type == kInterface) {
|
||||
expect(length == 9, "HID interface descriptor length is wrong");
|
||||
const uint8_t number = descriptor[offset + 2];
|
||||
expect(number < interfaces.size(),
|
||||
"HID interface number is outside the configured range");
|
||||
if (number < interfaces.size()) {
|
||||
expect(!interfaces[number], "HID interface number is duplicated");
|
||||
interfaces[number] = true;
|
||||
current_interface = number;
|
||||
} else {
|
||||
current_interface = -1;
|
||||
}
|
||||
expect(descriptor[offset + 3] == 0 &&
|
||||
descriptor[offset + 4] == 1 &&
|
||||
descriptor[offset + 5] == 0x03 &&
|
||||
descriptor[offset + 6] == 0 &&
|
||||
descriptor[offset + 7] == 0,
|
||||
"generic HID interface class or endpoint count is wrong");
|
||||
} else if (type == kHid) {
|
||||
expect(current_interface >= 0 && length == 9,
|
||||
"HID descriptor is not attached to an interface");
|
||||
expect(read_u16(descriptor + offset + 7) ==
|
||||
sizeof(GenericHid::kReportDescriptor),
|
||||
"HID descriptor advertises the wrong report length");
|
||||
if (current_interface >= 0) {
|
||||
++hid_counts[static_cast<size_t>(current_interface)];
|
||||
}
|
||||
} else if (type == kEndpoint) {
|
||||
expect(current_interface >= 0 && length == 7,
|
||||
"endpoint is not attached to an interface");
|
||||
const uint8_t address = descriptor[offset + 2];
|
||||
const uint8_t endpoint_number = address & 0x0fu;
|
||||
expect((address & 0x80u) != 0,
|
||||
"generic HID exposes an OUT endpoint");
|
||||
expect(endpoint_number > 0 && endpoint_number < endpoints.size(),
|
||||
"generic HID endpoint number is invalid");
|
||||
if (endpoint_number < endpoints.size()) {
|
||||
expect(!endpoints[endpoint_number],
|
||||
"generic HID endpoint address is duplicated");
|
||||
endpoints[endpoint_number] = true;
|
||||
}
|
||||
expect(current_interface < 0 ||
|
||||
address == static_cast<uint8_t>(
|
||||
0x81u + current_interface),
|
||||
"IN endpoint does not belong to its HID interface");
|
||||
expect(descriptor[offset + 3] == 0x03 &&
|
||||
read_u16(descriptor + offset + 4) ==
|
||||
GenericHid::kEndpointSize &&
|
||||
descriptor[offset + 6] ==
|
||||
GenericHid::kEndpointIntervalMs,
|
||||
"generic HID interrupt endpoint contract is wrong");
|
||||
if (current_interface >= 0) {
|
||||
++endpoint_counts[static_cast<size_t>(current_interface)];
|
||||
}
|
||||
} else {
|
||||
expect(false, "unexpected configuration child descriptor type");
|
||||
}
|
||||
offset += length;
|
||||
}
|
||||
|
||||
expect(offset == descriptor_size,
|
||||
"configuration descriptor was not fully decoded");
|
||||
for (size_t instance = 0; instance < interfaces.size(); ++instance) {
|
||||
expect(interfaces[instance] && hid_counts[instance] == 1 &&
|
||||
endpoint_counts[instance] == 1 && endpoints[instance + 1],
|
||||
"configured HID interface is missing or not isolated");
|
||||
}
|
||||
}
|
||||
|
||||
void inspect_report_encoding() {
|
||||
ControllerState state{};
|
||||
state.left_stick_x = INT16_MIN;
|
||||
state.left_stick_y = INT16_MAX;
|
||||
state.right_stick_x = static_cast<int16_t>(0x1234);
|
||||
state.right_stick_y = static_cast<int16_t>(-0x1234);
|
||||
state.left_trigger = 0;
|
||||
state.right_trigger = UINT16_MAX;
|
||||
GenericHid::InputReport report = GenericHid::build_input_report(state);
|
||||
expect(read_i16(report.data + 0) == INT16_MIN &&
|
||||
read_i16(report.data + 2) == INT16_MAX &&
|
||||
read_i16(report.data + 4) == static_cast<int16_t>(0x1234) &&
|
||||
read_i16(report.data + 6) == static_cast<int16_t>(-0x1234),
|
||||
"signed stick endpoints or little-endian encoding are wrong");
|
||||
expect(read_u16(report.data + 8) == 0 &&
|
||||
read_u16(report.data + 10) == UINT16_MAX,
|
||||
"unsigned trigger endpoints or little-endian encoding are wrong");
|
||||
expect(report.data[12] == GenericHid::kHatCenter &&
|
||||
(report.data[12] & 0xf0u) == 0,
|
||||
"neutral Hat Switch does not use null value 8 with zero padding");
|
||||
|
||||
struct HatCase {
|
||||
bool up;
|
||||
bool down;
|
||||
bool left;
|
||||
bool right;
|
||||
uint8_t expected;
|
||||
};
|
||||
constexpr std::array<HatCase, 9> hats{{
|
||||
{false, false, false, false, GenericHid::kHatCenter},
|
||||
{true, false, false, false, GenericHid::kHatUp},
|
||||
{true, false, false, true, GenericHid::kHatUpRight},
|
||||
{false, false, false, true, GenericHid::kHatRight},
|
||||
{false, true, false, true, GenericHid::kHatDownRight},
|
||||
{false, true, false, false, GenericHid::kHatDown},
|
||||
{false, true, true, false, GenericHid::kHatDownLeft},
|
||||
{false, false, true, false, GenericHid::kHatLeft},
|
||||
{true, false, true, false, GenericHid::kHatUpLeft},
|
||||
}};
|
||||
for (const HatCase& hat : hats) {
|
||||
ControllerState direction{};
|
||||
direction.dpad_up = hat.up;
|
||||
direction.dpad_down = hat.down;
|
||||
direction.dpad_left = hat.left;
|
||||
direction.dpad_right = hat.right;
|
||||
const GenericHid::InputReport direction_report =
|
||||
GenericHid::build_input_report(direction);
|
||||
expect(direction_report.data[12] == hat.expected,
|
||||
"D-pad direction or diagonal maps to the wrong hat value");
|
||||
}
|
||||
ControllerState contradictory{};
|
||||
contradictory.dpad_up = true;
|
||||
contradictory.dpad_down = true;
|
||||
contradictory.dpad_left = true;
|
||||
contradictory.dpad_right = true;
|
||||
expect(GenericHid::build_input_report(contradictory).data[12] ==
|
||||
GenericHid::kHatCenter,
|
||||
"contradictory D-pad input does not resolve to center");
|
||||
|
||||
using ButtonMember = bool ControllerState::*;
|
||||
constexpr std::array<ButtonMember, 12> button_members{{
|
||||
&ControllerState::button_south,
|
||||
&ControllerState::button_east,
|
||||
&ControllerState::button_west,
|
||||
&ControllerState::button_north,
|
||||
&ControllerState::button_left_shoulder,
|
||||
&ControllerState::button_right_shoulder,
|
||||
&ControllerState::button_select,
|
||||
&ControllerState::button_start,
|
||||
&ControllerState::button_left_stick,
|
||||
&ControllerState::button_right_stick,
|
||||
&ControllerState::button_system,
|
||||
&ControllerState::button_capture,
|
||||
}};
|
||||
for (size_t button = 0; button < button_members.size(); ++button) {
|
||||
ControllerState pressed{};
|
||||
pressed.*button_members[button] = true;
|
||||
const GenericHid::InputReport button_report =
|
||||
GenericHid::build_input_report(pressed);
|
||||
expect(read_u16(button_report.data + 13) == (1u << button),
|
||||
"positional button maps to the wrong sequential usage");
|
||||
}
|
||||
ControllerState all_buttons{};
|
||||
for (ButtonMember member : button_members) {
|
||||
all_buttons.*member = true;
|
||||
}
|
||||
expect(read_u16(GenericHid::build_input_report(all_buttons).data + 13) ==
|
||||
GenericHid::kDefinedButtonMask,
|
||||
"button bits 12..15 are not reserved zero");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
inspect_report_descriptor();
|
||||
inspect_device_descriptors_and_strings();
|
||||
inspect_configuration_descriptor();
|
||||
inspect_report_encoding();
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
298
tests/generic_hid_driver_test.cpp
Normal file
298
tests/generic_hid_driver_test.cpp
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
#include "generic_hid_driver.h"
|
||||
|
||||
#include "generic_hid_descriptors.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t kInstanceCount = SWITCH_PICO_HID_INSTANCE_COUNT;
|
||||
constexpr uint8_t kInvalidInstance = kInstanceCount;
|
||||
static_assert(kInstanceCount == 4,
|
||||
"the generic HID driver harness must exercise four interfaces");
|
||||
|
||||
struct SentReport {
|
||||
uint8_t instance = 0xff;
|
||||
uint8_t report_id = 0xff;
|
||||
uint16_t length = 0;
|
||||
GenericHid::InputReport report{};
|
||||
};
|
||||
|
||||
std::array<bool, kInstanceCount> hid_ready{};
|
||||
std::array<bool, kInstanceCount> send_succeeds{};
|
||||
std::array<unsigned, kInstanceCount> send_attempts{};
|
||||
std::array<SentReport, 32> sent_reports{};
|
||||
size_t sent_report_count = 0;
|
||||
int failures = 0;
|
||||
|
||||
void expect(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t read_u16(const uint8_t* bytes) {
|
||||
return static_cast<uint16_t>(bytes[0]) |
|
||||
static_cast<uint16_t>(static_cast<uint16_t>(bytes[1]) << 8u);
|
||||
}
|
||||
|
||||
void reset_harness() {
|
||||
hid_ready.fill(true);
|
||||
send_succeeds.fill(true);
|
||||
send_attempts = {};
|
||||
sent_reports = {};
|
||||
sent_report_count = 0;
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
generic_hid_init(instance);
|
||||
}
|
||||
}
|
||||
|
||||
GenericHid::InputReport get_report(uint8_t instance) {
|
||||
GenericHid::InputReport report{};
|
||||
expect(generic_hid_get_report(instance, 0, HID_REPORT_TYPE_INPUT,
|
||||
report.data, sizeof(report)) ==
|
||||
sizeof(report),
|
||||
"valid input GetReport did not return 15 bytes");
|
||||
return report;
|
||||
}
|
||||
|
||||
void test_initial_report_and_descriptor_routing() {
|
||||
reset_harness();
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
const GenericHid::InputReport report = get_report(instance);
|
||||
expect(report.data[12] == GenericHid::kHatCenter,
|
||||
"initialized generic HID report is not centered");
|
||||
for (size_t byte = 0; byte < sizeof(report); ++byte) {
|
||||
if (byte != 12) {
|
||||
expect(report.data[byte] == 0,
|
||||
"initialized generic HID report is not neutral");
|
||||
}
|
||||
}
|
||||
const uint8_t* descriptor =
|
||||
generic_hid_report_descriptor(instance);
|
||||
expect(descriptor != nullptr &&
|
||||
std::memcmp(descriptor, GenericHid::kReportDescriptor,
|
||||
sizeof(GenericHid::kReportDescriptor)) == 0,
|
||||
"valid HID instance did not receive the shared descriptor");
|
||||
}
|
||||
expect(generic_hid_report_descriptor(kInvalidInstance) == nullptr,
|
||||
"invalid HID instance received a report descriptor");
|
||||
}
|
||||
|
||||
void test_latest_get_report_and_instance_isolation() {
|
||||
reset_harness();
|
||||
std::array<ControllerState, kInstanceCount> states{};
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
ControllerState& state = states[instance];
|
||||
state.left_stick_x = static_cast<int16_t>(-30000 + instance * 1000);
|
||||
state.left_stick_y = static_cast<int16_t>(1000 + instance * 2000);
|
||||
state.right_stick_x = static_cast<int16_t>(3000 + instance * 3000);
|
||||
state.right_stick_y = static_cast<int16_t>(-4000 - instance * 4000);
|
||||
state.left_trigger = static_cast<uint16_t>(0x1111u * (instance + 1u));
|
||||
state.right_trigger = static_cast<uint16_t>(0x8888u + instance * 0x1111u);
|
||||
if (instance == 0) {
|
||||
state.button_south = true;
|
||||
state.dpad_up = true;
|
||||
} else if (instance == 1) {
|
||||
state.button_east = true;
|
||||
state.dpad_right = true;
|
||||
} else if (instance == 2) {
|
||||
state.button_system = true;
|
||||
state.dpad_down = true;
|
||||
} else {
|
||||
state.button_capture = true;
|
||||
state.dpad_left = true;
|
||||
}
|
||||
generic_hid_set_input(instance, state);
|
||||
}
|
||||
|
||||
std::array<GenericHid::InputReport, kInstanceCount> reports{};
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
reports[instance] = get_report(instance);
|
||||
const GenericHid::InputReport expected =
|
||||
GenericHid::build_input_report(states[instance]);
|
||||
expect(std::memcmp(reports[instance].data, expected.data,
|
||||
sizeof(expected)) == 0,
|
||||
"GetReport did not expose the latest addressed input");
|
||||
}
|
||||
for (uint8_t left = 0; left < kInstanceCount; ++left) {
|
||||
for (uint8_t right = static_cast<uint8_t>(left + 1u);
|
||||
right < kInstanceCount; ++right) {
|
||||
expect(std::memcmp(reports[left].data, reports[right].data,
|
||||
sizeof(GenericHid::InputReport)) != 0,
|
||||
"latest reports crossed HID instances");
|
||||
}
|
||||
}
|
||||
|
||||
ControllerState changed = states[2];
|
||||
changed.button_system = false;
|
||||
changed.button_north = true;
|
||||
changed.left_trigger = UINT16_MAX;
|
||||
generic_hid_set_input(2, changed);
|
||||
const GenericHid::InputReport latest = get_report(2);
|
||||
const GenericHid::InputReport unchanged = get_report(1);
|
||||
expect(read_u16(latest.data + 13) == GenericHid::kButtonNorth &&
|
||||
read_u16(latest.data + 8) == UINT16_MAX,
|
||||
"changed input was not materialized before the next task call");
|
||||
expect(std::memcmp(unchanged.data, reports[1].data, sizeof(unchanged)) == 0,
|
||||
"changing one input mutated another HID context");
|
||||
}
|
||||
|
||||
void test_ready_and_periodic_send_routing() {
|
||||
reset_harness();
|
||||
ControllerState state{};
|
||||
state.left_stick_x = INT16_MIN;
|
||||
state.right_stick_y = INT16_MAX;
|
||||
state.left_trigger = 0x1234;
|
||||
state.right_trigger = 0xabcd;
|
||||
state.button_left_stick = true;
|
||||
state.dpad_down = true;
|
||||
state.dpad_right = true;
|
||||
generic_hid_set_input(3, state);
|
||||
|
||||
hid_ready[3] = false;
|
||||
expect(!generic_hid_is_ready(3) && !generic_hid_task(3) &&
|
||||
send_attempts[3] == 0 && sent_report_count == 0,
|
||||
"not-ready HID instance attempted an interrupt send");
|
||||
hid_ready[3] = true;
|
||||
expect(generic_hid_is_ready(3) && generic_hid_task(3) &&
|
||||
send_attempts[3] == 1 && sent_report_count == 1,
|
||||
"ready HID instance did not send its periodic input report");
|
||||
expect(sent_reports[0].instance == 3 &&
|
||||
sent_reports[0].report_id == 0 &&
|
||||
sent_reports[0].length == GenericHid::kReportSize,
|
||||
"generic input used the wrong TinyUSB interface, ID, or length");
|
||||
const GenericHid::InputReport expected =
|
||||
GenericHid::build_input_report(state);
|
||||
expect(std::memcmp(sent_reports[0].report.data, expected.data,
|
||||
sizeof(expected)) == 0,
|
||||
"interrupt send did not use the latest generic input report");
|
||||
|
||||
expect(generic_hid_task(3) && send_attempts[3] == 2 &&
|
||||
sent_report_count == 2,
|
||||
"ready polling did not permit the next report interval send");
|
||||
|
||||
send_succeeds[3] = false;
|
||||
expect(!generic_hid_task(3) && send_attempts[3] == 3 &&
|
||||
sent_report_count == 2,
|
||||
"failed TinyUSB send was reported as successful");
|
||||
expect(!generic_hid_is_ready(kInvalidInstance) &&
|
||||
!generic_hid_task(kInvalidInstance),
|
||||
"invalid HID instance was ready or attempted a send");
|
||||
for (uint8_t instance = 0; instance < 3; ++instance) {
|
||||
expect(send_attempts[instance] == 0,
|
||||
"task send leaked to a different HID interface");
|
||||
}
|
||||
}
|
||||
|
||||
void test_reset_and_invalid_inputs() {
|
||||
reset_harness();
|
||||
ControllerState zero{};
|
||||
zero.button_south = true;
|
||||
zero.left_stick_x = 1234;
|
||||
ControllerState one{};
|
||||
one.button_east = true;
|
||||
one.right_trigger = 4321;
|
||||
generic_hid_set_input(0, zero);
|
||||
generic_hid_set_input(1, one);
|
||||
|
||||
generic_hid_init(0);
|
||||
const GenericHid::InputReport reset = get_report(0);
|
||||
const GenericHid::InputReport preserved = get_report(1);
|
||||
expect(reset.data[12] == GenericHid::kHatCenter &&
|
||||
read_u16(reset.data + 13) == 0 &&
|
||||
read_u16(reset.data) == 0,
|
||||
"init did not reset the addressed HID context");
|
||||
expect(read_u16(preserved.data + 13) == GenericHid::kButtonEast &&
|
||||
read_u16(preserved.data + 10) == 4321,
|
||||
"reset crossed into another HID context");
|
||||
|
||||
ControllerState invalid{};
|
||||
invalid.button_capture = true;
|
||||
generic_hid_set_input(kInvalidInstance, invalid);
|
||||
generic_hid_init(kInvalidInstance);
|
||||
expect(std::memcmp(get_report(1).data, preserved.data,
|
||||
sizeof(preserved)) == 0,
|
||||
"invalid instance input/reset mutated a valid context");
|
||||
}
|
||||
|
||||
void test_get_report_rejections_and_truncation() {
|
||||
reset_harness();
|
||||
ControllerState state{};
|
||||
state.left_stick_x = static_cast<int16_t>(0x1234);
|
||||
state.left_stick_y = static_cast<int16_t>(0x5678);
|
||||
generic_hid_set_input(0, state);
|
||||
|
||||
std::array<uint8_t, GenericHid::kReportSize> buffer{};
|
||||
buffer.fill(0xa5);
|
||||
expect(generic_hid_get_report(kInvalidInstance, 0,
|
||||
HID_REPORT_TYPE_INPUT, buffer.data(),
|
||||
buffer.size()) == 0,
|
||||
"GetReport accepted an invalid HID instance");
|
||||
expect(generic_hid_get_report(0, 1, HID_REPORT_TYPE_INPUT, buffer.data(),
|
||||
buffer.size()) == 0,
|
||||
"GetReport accepted a nonzero Report ID");
|
||||
expect(generic_hid_get_report(0, 0, HID_REPORT_TYPE_OUTPUT, buffer.data(),
|
||||
buffer.size()) == 0 &&
|
||||
generic_hid_get_report(0, 0, HID_REPORT_TYPE_FEATURE,
|
||||
buffer.data(), buffer.size()) == 0,
|
||||
"input-only driver accepted output or feature GetReport");
|
||||
expect(generic_hid_get_report(0, 0, HID_REPORT_TYPE_INPUT, nullptr,
|
||||
buffer.size()) == 0 &&
|
||||
generic_hid_get_report(0, 0, HID_REPORT_TYPE_INPUT,
|
||||
buffer.data(), 0) == 0,
|
||||
"GetReport accepted an invalid destination");
|
||||
for (uint8_t byte : buffer) {
|
||||
expect(byte == 0xa5, "rejected GetReport modified its destination");
|
||||
}
|
||||
|
||||
std::array<uint8_t, 3> truncated{};
|
||||
expect(generic_hid_get_report(0, 0, HID_REPORT_TYPE_INPUT,
|
||||
truncated.data(), truncated.size()) ==
|
||||
truncated.size() &&
|
||||
truncated[0] == 0x34 && truncated[1] == 0x12 &&
|
||||
truncated[2] == 0x78,
|
||||
"GetReport did not safely truncate the latest input report");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" bool tud_hid_n_ready(uint8_t instance) {
|
||||
return instance < kInstanceCount && hid_ready[instance];
|
||||
}
|
||||
|
||||
extern "C" bool tud_hid_n_report(uint8_t instance, uint8_t report_id,
|
||||
const void* report, uint16_t length) {
|
||||
if (instance >= kInstanceCount || report == nullptr ||
|
||||
length != sizeof(GenericHid::InputReport)) {
|
||||
return false;
|
||||
}
|
||||
++send_attempts[instance];
|
||||
if (!send_succeeds[instance] || sent_report_count >= sent_reports.size()) {
|
||||
return false;
|
||||
}
|
||||
SentReport& sent = sent_reports[sent_report_count++];
|
||||
sent.instance = instance;
|
||||
sent.report_id = report_id;
|
||||
sent.length = length;
|
||||
std::memcpy(&sent.report, report, length);
|
||||
return true;
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_initial_report_and_descriptor_routing();
|
||||
test_latest_get_report_and_instance_isolation();
|
||||
test_ready_and_periodic_send_routing();
|
||||
test_reset_and_invalid_inputs();
|
||||
test_get_report_rejections_and_truncation();
|
||||
if (failures != 0) {
|
||||
std::cerr << failures << " generic HID driver test(s) failed\n";
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -44,6 +44,11 @@ class FakeDevice:
|
|||
)
|
||||
self.configuration_generation = 3
|
||||
self.active_mode = config_manager.ACTIVE_MODE_SWITCH_PROBE
|
||||
self.capabilities = (
|
||||
config_manager.CAPABILITY_INPUT
|
||||
| config_manager.CAPABILITY_RUMBLE
|
||||
| config_manager.CAPABILITY_MOTION
|
||||
)
|
||||
self.transaction_id = 0
|
||||
self.transaction_payload = bytearray()
|
||||
self.transaction_expected_size = 0
|
||||
|
|
@ -215,7 +220,7 @@ class FakeDevice:
|
|||
0,
|
||||
2,
|
||||
self.active_mode,
|
||||
0,
|
||||
self.capabilities,
|
||||
0,
|
||||
2,
|
||||
]
|
||||
|
|
@ -366,6 +371,8 @@ class FakeDevice:
|
|||
config_manager.REQUESTED_MODE_AUTO,
|
||||
config_manager.REQUESTED_MODE_SWITCH,
|
||||
config_manager.REQUESTED_MODE_XINPUT,
|
||||
config_manager.REQUESTED_MODE_DINPUT,
|
||||
config_manager.REQUESTED_MODE_MAC,
|
||||
)
|
||||
self.transaction_payload = bytearray()
|
||||
self.transaction_expected_size = 0
|
||||
|
|
@ -650,17 +657,20 @@ def test_mode_envelopes_and_host_side_validation(
|
|||
for transaction_id in (0, 0x80000000, True):
|
||||
with pytest.raises(config_manager.ConfigManagerError):
|
||||
config_manager.request_reboot(device, transaction_id)
|
||||
for mode in (
|
||||
config_manager.REQUESTED_MODE_DINPUT,
|
||||
config_manager.REQUESTED_MODE_MAC,
|
||||
0xFF,
|
||||
True,
|
||||
):
|
||||
for mode in (0xFF, True):
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError, match="not available"
|
||||
):
|
||||
config_manager.set_mode(device, mode, 1.0)
|
||||
|
||||
for mode in (
|
||||
config_manager.REQUESTED_MODE_DINPUT,
|
||||
config_manager.REQUESTED_MODE_MAC,
|
||||
):
|
||||
status = config_manager.set_mode(device, mode, 1.0)
|
||||
assert status.status == config_manager.STATUS_OK
|
||||
assert device.pending_requested_mode is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("failure_status", "message"),
|
||||
|
|
@ -728,6 +738,14 @@ def test_mode_transaction_must_correlate_before_reboot(
|
|||
config_manager.REQUESTED_MODE_XINPUT,
|
||||
config_manager.ACTIVE_MODE_XINPUT,
|
||||
),
|
||||
(
|
||||
config_manager.REQUESTED_MODE_DINPUT,
|
||||
config_manager.ACTIVE_MODE_DINPUT,
|
||||
),
|
||||
(
|
||||
config_manager.REQUESTED_MODE_MAC,
|
||||
config_manager.ACTIVE_MODE_MAC,
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_mode_noop_accepts_only_mode_appropriate_active_state(
|
||||
|
|
@ -736,6 +754,16 @@ def test_mode_noop_accepts_only_mode_appropriate_active_state(
|
|||
device = FakeDevice()
|
||||
device.configuration = struct.pack("<HB5x", 60, requested_mode)
|
||||
device.active_mode = active_mode
|
||||
if active_mode in (
|
||||
config_manager.ACTIVE_MODE_DINPUT,
|
||||
config_manager.ACTIVE_MODE_MAC,
|
||||
):
|
||||
device.capabilities = config_manager.CAPABILITY_INPUT
|
||||
elif active_mode == config_manager.ACTIVE_MODE_XINPUT:
|
||||
device.capabilities = (
|
||||
config_manager.CAPABILITY_INPUT |
|
||||
config_manager.CAPABILITY_RUMBLE
|
||||
)
|
||||
|
||||
same_device, changed = config_manager.configure_mode(
|
||||
device, requested_mode, 1.0
|
||||
|
|
@ -765,6 +793,14 @@ def test_mode_noop_accepts_only_mode_appropriate_active_state(
|
|||
config_manager.REQUESTED_MODE_XINPUT,
|
||||
config_manager.ACTIVE_MODE_XINPUT,
|
||||
),
|
||||
(
|
||||
config_manager.REQUESTED_MODE_DINPUT,
|
||||
config_manager.ACTIVE_MODE_DINPUT,
|
||||
),
|
||||
(
|
||||
config_manager.REQUESTED_MODE_MAC,
|
||||
config_manager.ACTIVE_MODE_MAC,
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_mode_change_waits_for_disappearance_and_reenumeration(
|
||||
|
|
@ -782,6 +818,16 @@ def test_mode_change_waits_for_disappearance_and_reenumeration(
|
|||
reenumerated.address = 8
|
||||
reenumerated.configuration = struct.pack("<HB5x", 60, requested_mode)
|
||||
reenumerated.active_mode = active_mode
|
||||
if active_mode in (
|
||||
config_manager.ACTIVE_MODE_DINPUT,
|
||||
config_manager.ACTIVE_MODE_MAC,
|
||||
):
|
||||
reenumerated.capabilities = config_manager.CAPABILITY_INPUT
|
||||
elif active_mode == config_manager.ACTIVE_MODE_XINPUT:
|
||||
reenumerated.capabilities = (
|
||||
config_manager.CAPABILITY_INPUT |
|
||||
config_manager.CAPABILITY_RUMBLE
|
||||
)
|
||||
scans = iter(((previous,), (), (reenumerated,)))
|
||||
monkeypatch.setattr(
|
||||
config_manager,
|
||||
|
|
@ -871,22 +917,37 @@ def test_mode_reboot_fails_when_missing_topology_is_ambiguous(
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("stored_mode", "active_mode", "message"),
|
||||
("requested_mode", "stored_mode", "active_mode", "message"),
|
||||
(
|
||||
(
|
||||
config_manager.REQUESTED_MODE_SWITCH,
|
||||
config_manager.REQUESTED_MODE_SWITCH,
|
||||
config_manager.ACTIVE_MODE_SWITCH_PROBE,
|
||||
"activated",
|
||||
),
|
||||
(
|
||||
config_manager.REQUESTED_MODE_SWITCH,
|
||||
config_manager.REQUESTED_MODE_AUTO,
|
||||
config_manager.ACTIVE_MODE_SWITCH,
|
||||
"was not stored",
|
||||
),
|
||||
(
|
||||
config_manager.REQUESTED_MODE_DINPUT,
|
||||
config_manager.REQUESTED_MODE_DINPUT,
|
||||
config_manager.ACTIVE_MODE_MAC,
|
||||
"activated",
|
||||
),
|
||||
(
|
||||
config_manager.REQUESTED_MODE_MAC,
|
||||
config_manager.REQUESTED_MODE_DINPUT,
|
||||
config_manager.ACTIVE_MODE_MAC,
|
||||
"was not stored",
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_mode_verifies_requested_and_active_state_after_reenumeration(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
requested_mode: int,
|
||||
stored_mode: int,
|
||||
active_mode: int,
|
||||
message: str,
|
||||
|
|
@ -906,7 +967,7 @@ def test_mode_verifies_requested_and_active_state_after_reenumeration(
|
|||
|
||||
with pytest.raises(config_manager.ConfigManagerError, match=message):
|
||||
config_manager.configure_mode(
|
||||
previous, config_manager.REQUESTED_MODE_SWITCH, 1.0
|
||||
previous, requested_mode, 1.0
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -944,6 +1005,11 @@ def test_requested_and_active_mode_response_validation() -> None:
|
|||
config_manager.read_info(device).active_mode
|
||||
== config_manager.ACTIVE_MODE_SWITCH_PROBE
|
||||
)
|
||||
assert config_manager.read_info(device).capability_names() == (
|
||||
"input",
|
||||
"rumble",
|
||||
"motion",
|
||||
)
|
||||
|
||||
device.configuration = struct.pack("<HB5x", 60, 0xFF)
|
||||
with pytest.raises(
|
||||
|
|
@ -964,6 +1030,17 @@ def test_requested_and_active_mode_response_validation() -> None:
|
|||
config_manager.ConfigManagerError, match="unknown active USB mode"
|
||||
):
|
||||
config_manager.read_info(device)
|
||||
device.active_mode = config_manager.ACTIVE_MODE_DINPUT
|
||||
device.capabilities = 0x80
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError, match="unknown device capability"
|
||||
):
|
||||
config_manager.read_info(device)
|
||||
device.capabilities = config_manager.CAPABILITY_RUMBLE
|
||||
with pytest.raises(
|
||||
config_manager.ConfigManagerError, match="omit required input"
|
||||
):
|
||||
config_manager.read_info(device)
|
||||
|
||||
|
||||
def test_identity_and_profile_binary_json_round_trip() -> None:
|
||||
|
|
@ -1473,6 +1550,19 @@ def test_status_and_pairing_commands(
|
|||
assert "Pairing window: 60 seconds" in output
|
||||
assert "Requested USB mode: auto" in output
|
||||
assert "Active USB mode: Switch probe" in output
|
||||
assert "Mode capabilities: input, rumble, motion" in output
|
||||
|
||||
device.configuration = struct.pack(
|
||||
"<HB5x", 60, config_manager.REQUESTED_MODE_DINPUT
|
||||
)
|
||||
device.active_mode = config_manager.ACTIVE_MODE_DINPUT
|
||||
device.capabilities = config_manager.CAPABILITY_INPUT
|
||||
assert config_manager.main(["status"]) == 0
|
||||
generic_output = capsys.readouterr().out
|
||||
assert "Requested USB mode: dinput" in generic_output
|
||||
assert "Active USB mode: DInput" in generic_output
|
||||
assert "Mode capabilities: input only\n" in generic_output
|
||||
assert "rumble" not in generic_output.lower()
|
||||
|
||||
assert config_manager.main(["pairings", "list"]) == 0
|
||||
output = capsys.readouterr().out
|
||||
|
|
@ -1515,17 +1605,46 @@ def test_config_cli_preserves_requested_mode(
|
|||
assert "requested_mode=xinput" in output
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mode_name", "requested_mode", "active_mode", "capabilities"),
|
||||
(
|
||||
(
|
||||
"xinput",
|
||||
config_manager.REQUESTED_MODE_XINPUT,
|
||||
config_manager.ACTIVE_MODE_XINPUT,
|
||||
config_manager.CAPABILITY_INPUT |
|
||||
config_manager.CAPABILITY_RUMBLE,
|
||||
),
|
||||
(
|
||||
"dinput",
|
||||
config_manager.REQUESTED_MODE_DINPUT,
|
||||
config_manager.ACTIVE_MODE_DINPUT,
|
||||
config_manager.CAPABILITY_INPUT,
|
||||
),
|
||||
(
|
||||
"mac",
|
||||
config_manager.REQUESTED_MODE_MAC,
|
||||
config_manager.ACTIVE_MODE_MAC,
|
||||
config_manager.CAPABILITY_INPUT,
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_mode_cli_changes_then_noops(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
mode_name: str,
|
||||
requested_mode: int,
|
||||
active_mode: int,
|
||||
capabilities: int,
|
||||
) -> None:
|
||||
previous = FakeDevice()
|
||||
reenumerated = FakeDevice()
|
||||
reenumerated.address = 8
|
||||
reenumerated.configuration = struct.pack(
|
||||
"<HB5x", 60, config_manager.REQUESTED_MODE_XINPUT
|
||||
"<HB5x", 60, requested_mode
|
||||
)
|
||||
reenumerated.active_mode = config_manager.ACTIVE_MODE_XINPUT
|
||||
reenumerated.active_mode = active_mode
|
||||
reenumerated.capabilities = capabilities
|
||||
scans = iter(
|
||||
((previous,), (previous,), (), (reenumerated,))
|
||||
)
|
||||
|
|
@ -1536,29 +1655,36 @@ def test_mode_cli_changes_then_noops(
|
|||
)
|
||||
monkeypatch.setattr(config_manager.time, "sleep", lambda _seconds: None)
|
||||
|
||||
assert config_manager.main(["mode", "xinput"]) == 0
|
||||
assert capsys.readouterr().out == "USB mode changed to xinput.\n"
|
||||
assert config_manager.main(["mode", mode_name]) == 0
|
||||
assert capsys.readouterr().out == f"USB mode changed to {mode_name}.\n"
|
||||
assert previous.reboot_transaction_ids == [previous.transaction_id]
|
||||
|
||||
monkeypatch.setattr(
|
||||
config_manager, "_candidate_devices", lambda: (reenumerated,)
|
||||
)
|
||||
assert config_manager.main(["mode", "xinput"]) == 0
|
||||
assert capsys.readouterr().out == "USB mode is already xinput.\n"
|
||||
assert config_manager.main(["mode", mode_name]) == 0
|
||||
assert capsys.readouterr().out == f"USB mode is already {mode_name}.\n"
|
||||
assert reenumerated.out_requests == []
|
||||
|
||||
|
||||
def test_mode_parser_rejects_unimplemented_modes() -> None:
|
||||
for mode in ("dinput", "mac"):
|
||||
with pytest.raises(SystemExit):
|
||||
config_manager.build_parser().parse_args(["mode", mode])
|
||||
def test_mode_parser_accepts_all_implemented_modes() -> None:
|
||||
for mode in config_manager.REQUESTED_MODE_NAMES:
|
||||
args = config_manager.build_parser().parse_args(["mode", mode])
|
||||
assert args.mode == mode
|
||||
|
||||
|
||||
def test_candidate_discovery_checks_switch_and_xinput_identities(
|
||||
def test_candidate_discovery_checks_all_usb_identities(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
lookups: list[tuple[int, int]] = []
|
||||
|
||||
assert config_manager.USB_IDENTITIES == (
|
||||
(0x057E, 0x2009),
|
||||
(0xCAFE, 0x4010),
|
||||
(0xCAFE, 0x4020),
|
||||
(0xCAFE, 0x4021),
|
||||
)
|
||||
|
||||
def find(**arguments: object) -> tuple[object, ...]:
|
||||
lookups.append(
|
||||
(
|
||||
|
|
|
|||
108
tests/test_generic_hid_native.py
Normal file
108
tests/test_generic_hid_native.py
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def host_compiler() -> str:
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
return compiler
|
||||
|
||||
|
||||
def compile_cpp(
|
||||
root: Path,
|
||||
compiler: str,
|
||||
output: Path,
|
||||
sources: list[Path],
|
||||
definitions: list[str],
|
||||
include_stubs: bool = False,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
command = [
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
*[f"-D{definition}" for definition in definitions],
|
||||
]
|
||||
if include_stubs:
|
||||
command.append(f"-I{root / 'tests' / 'native_stubs'}")
|
||||
command.extend(
|
||||
[
|
||||
f"-I{root}",
|
||||
*[str(source) for source in sources],
|
||||
"-o",
|
||||
str(output),
|
||||
]
|
||||
)
|
||||
return subprocess.run(
|
||||
command,
|
||||
check=False,
|
||||
cwd=root,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
|
||||
def test_generic_hid_descriptor_and_report_contracts(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = host_compiler()
|
||||
source = root / "tests" / "generic_hid_descriptors_test.cpp"
|
||||
for instance_count in range(1, 5):
|
||||
executable = tmp_path / f"generic_hid_descriptors_{instance_count}_test"
|
||||
result = compile_cpp(
|
||||
root,
|
||||
compiler,
|
||||
executable,
|
||||
[source],
|
||||
[
|
||||
f"EXPECTED_HID_INSTANCE_COUNT={instance_count}",
|
||||
f"SWITCH_PICO_HID_INSTANCE_COUNT={instance_count}",
|
||||
],
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
||||
|
||||
def test_generic_hid_rejects_unsupported_interface_counts(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = host_compiler()
|
||||
source = root / "tests" / "generic_hid_descriptors_test.cpp"
|
||||
for instance_count in (0, 5):
|
||||
executable = tmp_path / f"generic_hid_descriptors_{instance_count}_test"
|
||||
result = compile_cpp(
|
||||
root,
|
||||
compiler,
|
||||
executable,
|
||||
[source],
|
||||
[
|
||||
f"EXPECTED_HID_INSTANCE_COUNT={instance_count}",
|
||||
f"SWITCH_PICO_HID_INSTANCE_COUNT={instance_count}",
|
||||
],
|
||||
)
|
||||
assert result.returncode != 0, (
|
||||
f"unsupported interface count {instance_count} compiled successfully"
|
||||
)
|
||||
|
||||
|
||||
def test_generic_hid_driver_contexts(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
executable = tmp_path / "generic_hid_driver_test"
|
||||
result = compile_cpp(
|
||||
root,
|
||||
host_compiler(),
|
||||
executable,
|
||||
[
|
||||
root / "generic_hid_driver.cpp",
|
||||
root / "tests" / "generic_hid_driver_test.cpp",
|
||||
],
|
||||
["SWITCH_PICO_HID_INSTANCE_COUNT=4"],
|
||||
include_stubs=True,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
|
@ -40,6 +40,7 @@ def test_usb_output_driver_contracts(tmp_path: Path) -> None:
|
|||
str(root / "tests" / "usb_output_driver_test.cpp"),
|
||||
str(root / "switch_pro_driver.cpp"),
|
||||
str(root / "usb_output_driver.cpp"),
|
||||
str(root / "generic_hid_driver.cpp"),
|
||||
str(root / "xinput_driver.cpp"),
|
||||
str(root / "switch_haptics.cpp"),
|
||||
"-o",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <vector>
|
||||
|
||||
#include <tusb.h>
|
||||
#include "usb_output_driver.h"
|
||||
|
||||
namespace {
|
||||
|
||||
|
|
@ -15,12 +16,15 @@ ProfileServiceListSnapshot current_profile_list{};
|
|||
ProfileServiceSelectedSnapshot current_profile_selected{};
|
||||
ProfileServiceTransactionSnapshot current_profile_transaction{};
|
||||
AdapterUsbMode current_active_mode = AdapterUsbMode::kSwitchProbe;
|
||||
uint8_t current_capabilities =
|
||||
USB_OUTPUT_CAPABILITY_INPUT | USB_OUTPUT_CAPABILITY_RUMBLE |
|
||||
USB_OUTPUT_CAPABILITY_MOTION;
|
||||
ConfigurationTransactionStatus mode_set_result =
|
||||
ConfigurationTransactionStatus::kPending;
|
||||
uint32_t mode_set_transaction_id = 0;
|
||||
AdapterRequestedMode mode_set_requested_mode = AdapterRequestedMode::kAuto;
|
||||
AdapterModeAvailability mode_set_availability{};
|
||||
AdapterModeAvailability runtime_mode_availability{};
|
||||
AdapterModeAvailability runtime_mode_availability{true, true, true, true};
|
||||
uint32_t mode_availability_query_count = 0;
|
||||
uint32_t mode_set_call_count = 0;
|
||||
uint32_t correlated_reboot_transaction_id = 0;
|
||||
|
|
@ -256,8 +260,24 @@ void test_mode_vendor_requests() {
|
|||
require(usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_payload[kResponseHeaderSize + 4] ==
|
||||
static_cast<uint8_t>(AdapterUsbMode::kSwitchProbe),
|
||||
"info response did not report the active USB mode");
|
||||
static_cast<uint8_t>(AdapterUsbMode::kSwitchProbe) &&
|
||||
control_payload[kResponseHeaderSize + 5] ==
|
||||
current_capabilities,
|
||||
"info response did not report active mode capabilities");
|
||||
|
||||
current_active_mode = AdapterUsbMode::kDInput;
|
||||
current_capabilities = USB_OUTPUT_CAPABILITY_INPUT;
|
||||
require(usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_payload[kResponseHeaderSize + 4] ==
|
||||
static_cast<uint8_t>(AdapterUsbMode::kDInput) &&
|
||||
control_payload[kResponseHeaderSize + 5] ==
|
||||
USB_OUTPUT_CAPABILITY_INPUT,
|
||||
"generic info response promised unsupported output capabilities");
|
||||
current_active_mode = AdapterUsbMode::kSwitchProbe;
|
||||
current_capabilities =
|
||||
USB_OUTPUT_CAPABILITY_INPUT | USB_OUTPUT_CAPABILITY_RUMBLE |
|
||||
USB_OUTPUT_CAPABILITY_MOTION;
|
||||
|
||||
request = setup_request(
|
||||
Operation::kConfigurationRead, TUSB_DIR_IN,
|
||||
|
|
@ -296,8 +316,8 @@ void test_mode_vendor_requests() {
|
|||
AdapterRequestedMode::kXInput &&
|
||||
mode_set_availability.switch_mode &&
|
||||
mode_set_availability.xinput_mode &&
|
||||
!mode_set_availability.dinput_mode &&
|
||||
!mode_set_availability.mac_mode &&
|
||||
mode_set_availability.dinput_mode &&
|
||||
mode_set_availability.mac_mode &&
|
||||
mode_availability_query_count == 1,
|
||||
"XInput mode set did not use runtime availability");
|
||||
write_u32(&mode_set, 0, 0x12345679);
|
||||
|
|
@ -330,17 +350,22 @@ void test_mode_vendor_requests() {
|
|||
"malformed mode request reached runtime mode selection");
|
||||
|
||||
mode_set_result = ConfigurationTransactionStatus::kPending;
|
||||
for (const AdapterRequestedMode unavailable : {
|
||||
for (const AdapterRequestedMode generic_mode : {
|
||||
AdapterRequestedMode::kDInput,
|
||||
AdapterRequestedMode::kMac,
|
||||
}) {
|
||||
mode_set[4] = static_cast<uint8_t>(unavailable);
|
||||
perform_out(Operation::kModeSet, mode_set, false);
|
||||
write_u32(&mode_set, 0,
|
||||
0x12345680u +
|
||||
static_cast<uint8_t>(generic_mode));
|
||||
mode_set[4] = static_cast<uint8_t>(generic_mode);
|
||||
perform_out(Operation::kModeSet, mode_set);
|
||||
require(mode_set_requested_mode == generic_mode,
|
||||
"generic mode request was not dispatched");
|
||||
}
|
||||
require(mode_set_call_count == calls_before_invalid + 2 &&
|
||||
mode_availability_query_count ==
|
||||
availability_queries_before_invalid + 2,
|
||||
"unsupported mode did not use runtime availability");
|
||||
"generic modes did not use runtime availability");
|
||||
|
||||
request = setup_request(
|
||||
Operation::kModeSet, TUSB_DIR_OUT, kRequestHeaderSize + 4);
|
||||
|
|
@ -615,6 +640,9 @@ ConfigurationTransactionStatus configuration_service_set_mode(
|
|||
}
|
||||
|
||||
AdapterUsbMode usb_output_driver_mode() { return current_active_mode; }
|
||||
uint8_t usb_output_driver_capabilities() {
|
||||
return current_capabilities;
|
||||
}
|
||||
|
||||
bool adapter_reboot_for_mode_transaction(uint32_t transaction_id) {
|
||||
reboot_transaction_id = transaction_id;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@
|
|||
#include <iostream>
|
||||
|
||||
#include "adapter_host_probe_state.h"
|
||||
#include "generic_hid_descriptors.h"
|
||||
#include "generic_hid_driver.h"
|
||||
#include "xinput_descriptors.h"
|
||||
#include "xinput_protocol.h"
|
||||
#include "device/usbd_pvt.h"
|
||||
|
|
@ -297,6 +299,11 @@ void test_switch_boundary_dispatch() {
|
|||
std::strcmp(usb_output_driver_mode_name(),
|
||||
"Switch probe") == 0,
|
||||
"Switch boundary mode was not frozen");
|
||||
expect(usb_output_driver_capabilities() ==
|
||||
(USB_OUTPUT_CAPABILITY_INPUT |
|
||||
USB_OUTPUT_CAPABILITY_RUMBLE |
|
||||
USB_OUTPUT_CAPABILITY_MOTION),
|
||||
"Switch probe capabilities were not rumble plus motion");
|
||||
expect(std::memcmp(tud_descriptor_device_cb(),
|
||||
XInput::kSwitchProbeDeviceDescriptor,
|
||||
sizeof(XInput::kSwitchProbeDeviceDescriptor)) == 0,
|
||||
|
|
@ -386,6 +393,11 @@ void test_manual_switch_selection() {
|
|||
"manual Switch did not use the production Switch descriptor");
|
||||
expect(tud_descriptor_string_cb(0xee, 0x0409) == nullptr,
|
||||
"manual Switch exposed the automatic Windows probe string");
|
||||
expect(usb_output_driver_capabilities() ==
|
||||
(USB_OUTPUT_CAPABILITY_INPUT |
|
||||
USB_OUTPUT_CAPABILITY_RUMBLE |
|
||||
USB_OUTPUT_CAPABILITY_MOTION),
|
||||
"manual Switch capabilities lost rumble or motion");
|
||||
}
|
||||
|
||||
void open_xinput_interfaces(usbd_class_driver_t const* driver) {
|
||||
|
|
@ -415,6 +427,12 @@ void test_xinput_boundary_dispatch() {
|
|||
std::strcmp(usb_output_driver_name(), "XINPUT") == 0 &&
|
||||
std::strcmp(usb_output_driver_mode_name(), "XInput") == 0,
|
||||
"XInput boundary mode was not frozen");
|
||||
expect(usb_output_driver_capabilities() ==
|
||||
(USB_OUTPUT_CAPABILITY_INPUT |
|
||||
USB_OUTPUT_CAPABILITY_RUMBLE) &&
|
||||
(usb_output_driver_capabilities() &
|
||||
USB_OUTPUT_CAPABILITY_MOTION) == 0,
|
||||
"XInput capabilities did not report rumble without motion");
|
||||
expect(std::memcmp(tud_descriptor_device_cb(),
|
||||
XInput::kDeviceDescriptor,
|
||||
sizeof(XInput::kDeviceDescriptor)) == 0,
|
||||
|
|
@ -540,6 +558,135 @@ void test_xinput_boundary_dispatch() {
|
|||
"XInput development serial string changed");
|
||||
}
|
||||
|
||||
void test_generic_boundary_dispatch(
|
||||
AdapterUsbMode mode, const uint8_t* expected_device_descriptor,
|
||||
const char* expected_driver_name, const char* expected_mode_name,
|
||||
const char* expected_product, const char* expected_serial) {
|
||||
reset_usb_harness();
|
||||
usb_output_driver_init(mode);
|
||||
|
||||
expect(usb_output_driver_mode() == mode &&
|
||||
std::strcmp(usb_output_driver_name(),
|
||||
expected_driver_name) == 0 &&
|
||||
std::strcmp(usb_output_driver_mode_name(),
|
||||
expected_mode_name) == 0,
|
||||
"generic boundary mode was not frozen");
|
||||
expect(usb_output_driver_capabilities() ==
|
||||
USB_OUTPUT_CAPABILITY_INPUT &&
|
||||
(usb_output_driver_capabilities() &
|
||||
USB_OUTPUT_CAPABILITY_RUMBLE) == 0 &&
|
||||
(usb_output_driver_capabilities() &
|
||||
USB_OUTPUT_CAPABILITY_MOTION) == 0,
|
||||
"generic mode promised rumble or motion capability");
|
||||
const uint8_t* device_descriptor = tud_descriptor_device_cb();
|
||||
expect(device_descriptor != nullptr &&
|
||||
std::memcmp(device_descriptor, expected_device_descriptor,
|
||||
sizeof(GenericHid::kDInputDeviceDescriptor)) == 0,
|
||||
"generic device descriptor was not selected");
|
||||
const uint8_t* configuration_descriptor =
|
||||
tud_descriptor_configuration_cb(0);
|
||||
expect(configuration_descriptor != nullptr &&
|
||||
std::memcmp(configuration_descriptor,
|
||||
GenericHid::kConfigurationDescriptor,
|
||||
GenericHid::kConfigurationDescriptorSize) == 0,
|
||||
"generic configuration descriptor was not selected");
|
||||
const uint8_t* report_descriptor =
|
||||
tud_hid_descriptor_report_cb(0);
|
||||
expect(report_descriptor != nullptr &&
|
||||
std::memcmp(report_descriptor,
|
||||
GenericHid::kReportDescriptor,
|
||||
sizeof(GenericHid::kReportDescriptor)) == 0 &&
|
||||
tud_hid_descriptor_report_cb(kInvalidInstance) == nullptr,
|
||||
"generic report descriptor routing was incorrect");
|
||||
|
||||
uint8_t driver_count = 0xff;
|
||||
expect(usbd_app_driver_get_cb(&driver_count) == nullptr &&
|
||||
driver_count == 0,
|
||||
"generic mode registered the XInput custom class");
|
||||
expect(tud_descriptor_string_cb(0xee, 0x0409) == nullptr,
|
||||
"generic mode exposed the XInput Microsoft OS string");
|
||||
expect_usb_string(1, GenericHid::kManufacturerString,
|
||||
"generic manufacturer string changed");
|
||||
expect_usb_string(2, expected_product,
|
||||
"generic product string changed");
|
||||
expect_usb_string(3, expected_serial,
|
||||
"generic serial string changed");
|
||||
|
||||
std::array<GenericHid::InputReport, kInstanceCount> expected_reports{};
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
ControllerState state{};
|
||||
state.left_stick_x =
|
||||
static_cast<int16_t>(INT16_MIN + instance);
|
||||
state.right_stick_y =
|
||||
static_cast<int16_t>(INT16_MAX - instance);
|
||||
state.left_trigger =
|
||||
static_cast<uint16_t>(0x1111u * (instance + 1u));
|
||||
state.button_south = (instance & 1u) == 0;
|
||||
state.button_north = (instance & 1u) != 0;
|
||||
usb_output_driver_set_input(instance, state, 1, 2);
|
||||
expected_reports[instance] =
|
||||
GenericHid::build_input_report(state);
|
||||
}
|
||||
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
std::array<uint8_t, GenericHid::kReportSize> report{};
|
||||
expect(tud_hid_get_report_cb(
|
||||
instance, 0, HID_REPORT_TYPE_INPUT, report.data(),
|
||||
report.size()) == GenericHid::kReportSize &&
|
||||
std::memcmp(report.data(),
|
||||
expected_reports[instance].data,
|
||||
report.size()) == 0,
|
||||
"generic GET_REPORT crossed instance state");
|
||||
expect(usb_output_driver_is_ready(instance) &&
|
||||
usb_output_driver_task(instance),
|
||||
"generic readiness/task helper was not dispatched");
|
||||
}
|
||||
std::array<uint8_t, GenericHid::kReportSize> invalid_report{};
|
||||
expect(tud_hid_get_report_cb(
|
||||
kInvalidInstance, 0, HID_REPORT_TYPE_INPUT,
|
||||
invalid_report.data(), invalid_report.size()) == 0 &&
|
||||
!usb_output_driver_is_ready(kInvalidInstance) &&
|
||||
!usb_output_driver_task(kInvalidInstance),
|
||||
"generic callbacks accepted an invalid instance");
|
||||
expect(tud_hid_get_report_cb(
|
||||
0, 0, HID_REPORT_TYPE_OUTPUT, invalid_report.data(),
|
||||
invalid_report.size()) == 0 &&
|
||||
tud_hid_get_report_cb(
|
||||
0, 1, HID_REPORT_TYPE_INPUT, invalid_report.data(),
|
||||
invalid_report.size()) == 0,
|
||||
"generic boundary accepted output or report-ID GET_REPORT");
|
||||
|
||||
switch_pro_set_rumble_callback(0,
|
||||
inactive_switch_rumble_callback);
|
||||
usb_output_driver_set_rumble_callback(0, xinput_rumble_callback);
|
||||
std::array<uint8_t, 10> output{};
|
||||
output[0] = REPORT_OUTPUT_10;
|
||||
tud_hid_report_received_cb(0, 0, output.data(), output.size());
|
||||
tud_hid_set_report_cb(0, REPORT_OUTPUT_10, HID_REPORT_TYPE_OUTPUT,
|
||||
output.data() + 1, output.size() - 1);
|
||||
expect(switch_inactive_rumble_count == 0 &&
|
||||
xinput_rumble_events[0].count == 0,
|
||||
"generic input-only mode handled an output report");
|
||||
|
||||
tud_mount_cb();
|
||||
tud_umount_cb();
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
expect(usb_output_driver_is_ready(instance),
|
||||
"generic lifecycle callback disturbed active state");
|
||||
}
|
||||
}
|
||||
|
||||
void test_generic_modes_boundary_dispatch() {
|
||||
test_generic_boundary_dispatch(
|
||||
AdapterUsbMode::kDInput, GenericHid::kDInputDeviceDescriptor,
|
||||
"DINPUT", "DInput", GenericHid::kDInputProductString,
|
||||
GenericHid::kDInputSerialString);
|
||||
test_generic_boundary_dispatch(
|
||||
AdapterUsbMode::kMac, GenericHid::kMacDeviceDescriptor,
|
||||
"MAC", "Mac", GenericHid::kMacProductString,
|
||||
GenericHid::kMacSerialString);
|
||||
}
|
||||
|
||||
void test_vendor_control_boundary() {
|
||||
constexpr uint8_t kSetupStage = 0;
|
||||
constexpr uint8_t kRhport = 2;
|
||||
|
|
@ -687,6 +834,7 @@ int main() {
|
|||
test_switch_boundary_dispatch();
|
||||
test_manual_switch_selection();
|
||||
test_xinput_boundary_dispatch();
|
||||
test_generic_modes_boundary_dispatch();
|
||||
test_vendor_control_boundary();
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// TinyUSB configuration for one to four Switch Pro style HID interfaces.
|
||||
// Each interface uses independent 64-byte interrupt IN and OUT endpoints.
|
||||
// Switch Pro interfaces use independent 64-byte interrupt IN and OUT endpoints;
|
||||
// generic HID interfaces use input-only 15-byte reports/endpoints.
|
||||
#ifndef _TUSB_CONFIG_H_
|
||||
#define _TUSB_CONFIG_H_
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ extern "C" {
|
|||
#define CFG_TUD_MSC 0
|
||||
#define CFG_TUD_MIDI 0
|
||||
#define CFG_TUD_VENDOR 0
|
||||
// Always enable TinyUSB debug at level 2; LOG_PRINTF controls user-facing logs.
|
||||
// TinyUSB debug is disabled by default (CFG_TUSB_DEBUG=0); LOG_PRINTF controls user-facing logs.
|
||||
#ifdef CFG_TUSB_DEBUG
|
||||
#undef CFG_TUSB_DEBUG
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ size_t encode_info(uint8_t* output, size_t output_size) {
|
|||
uint8_t payload[8] = {
|
||||
0, 2, 0, 2,
|
||||
static_cast<uint8_t>(usb_output_driver_mode()),
|
||||
0,
|
||||
usb_output_driver_capabilities(),
|
||||
static_cast<uint8_t>(CONFIGURATION_STORAGE_MAX_PAYLOAD_SIZE),
|
||||
static_cast<uint8_t>(
|
||||
CONFIGURATION_STORAGE_MAX_PAYLOAD_SIZE >> 8),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
|
||||
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
|
||||
#include "adapter_host_probe.h"
|
||||
#include "generic_hid_descriptors.h"
|
||||
#include "generic_hid_driver.h"
|
||||
#include "xinput_descriptors.h"
|
||||
#include "xinput_driver.h"
|
||||
#endif
|
||||
|
|
@ -37,6 +39,19 @@ bool xinput_selected() {
|
|||
#endif
|
||||
}
|
||||
|
||||
bool generic_selected() {
|
||||
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
|
||||
return g_mode == AdapterUsbMode::kDInput ||
|
||||
g_mode == AdapterUsbMode::kMac;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool switch_selected() {
|
||||
return !xinput_selected() && !generic_selected();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void usb_output_driver_init(AdapterUsbMode mode) {
|
||||
|
|
@ -52,6 +67,8 @@ void usb_output_driver_init(AdapterUsbMode mode) {
|
|||
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
|
||||
if (xinput_selected()) {
|
||||
xinput_init(instance);
|
||||
} else if (generic_selected()) {
|
||||
generic_hid_init(instance);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
|
|
@ -68,6 +85,10 @@ const char* usb_output_driver_mode_name() {
|
|||
return "Switch probe";
|
||||
case AdapterUsbMode::kXInput:
|
||||
return "XInput";
|
||||
case AdapterUsbMode::kDInput:
|
||||
return "DInput";
|
||||
case AdapterUsbMode::kMac:
|
||||
return "Mac";
|
||||
}
|
||||
return "Switch";
|
||||
}
|
||||
|
|
@ -77,7 +98,35 @@ AdapterUsbMode usb_output_driver_mode() {
|
|||
}
|
||||
|
||||
const char* usb_output_driver_name() {
|
||||
return xinput_selected() ? "XINPUT" : "SWITCH";
|
||||
switch (g_mode) {
|
||||
case AdapterUsbMode::kXInput:
|
||||
return "XINPUT";
|
||||
case AdapterUsbMode::kDInput:
|
||||
return "DINPUT";
|
||||
case AdapterUsbMode::kMac:
|
||||
return "MAC";
|
||||
case AdapterUsbMode::kSwitch:
|
||||
case AdapterUsbMode::kSwitchProbe:
|
||||
return "SWITCH";
|
||||
}
|
||||
return "SWITCH";
|
||||
}
|
||||
|
||||
uint8_t usb_output_driver_capabilities() {
|
||||
switch (g_mode) {
|
||||
case AdapterUsbMode::kSwitch:
|
||||
case AdapterUsbMode::kSwitchProbe:
|
||||
return USB_OUTPUT_CAPABILITY_INPUT |
|
||||
USB_OUTPUT_CAPABILITY_RUMBLE |
|
||||
USB_OUTPUT_CAPABILITY_MOTION;
|
||||
case AdapterUsbMode::kXInput:
|
||||
return USB_OUTPUT_CAPABILITY_INPUT |
|
||||
USB_OUTPUT_CAPABILITY_RUMBLE;
|
||||
case AdapterUsbMode::kDInput:
|
||||
case AdapterUsbMode::kMac:
|
||||
return USB_OUTPUT_CAPABILITY_INPUT;
|
||||
}
|
||||
return USB_OUTPUT_CAPABILITY_INPUT;
|
||||
}
|
||||
|
||||
void usb_output_driver_set_input(uint8_t instance,
|
||||
|
|
@ -89,6 +138,10 @@ void usb_output_driver_set_input(uint8_t instance,
|
|||
xinput_set_input(instance, state);
|
||||
return;
|
||||
}
|
||||
if (generic_selected()) {
|
||||
generic_hid_set_input(instance, state);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
switch_pro_set_input(instance, state, left_trigger_threshold,
|
||||
right_trigger_threshold);
|
||||
|
|
@ -99,6 +152,9 @@ bool usb_output_driver_task(uint8_t instance) {
|
|||
if (xinput_selected()) {
|
||||
return xinput_task(instance);
|
||||
}
|
||||
if (generic_selected()) {
|
||||
return generic_hid_task(instance);
|
||||
}
|
||||
#endif
|
||||
return switch_pro_task(instance);
|
||||
}
|
||||
|
|
@ -108,6 +164,9 @@ bool usb_output_driver_is_ready(uint8_t instance) {
|
|||
if (xinput_selected()) {
|
||||
return xinput_is_ready(instance);
|
||||
}
|
||||
if (generic_selected()) {
|
||||
return generic_hid_is_ready(instance);
|
||||
}
|
||||
#endif
|
||||
return switch_pro_is_ready(instance);
|
||||
}
|
||||
|
|
@ -119,6 +178,9 @@ void usb_output_driver_set_rumble_callback(
|
|||
xinput_set_rumble_callback(instance, callback);
|
||||
return;
|
||||
}
|
||||
if (generic_selected()) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
switch_pro_set_rumble_callback(instance, callback);
|
||||
}
|
||||
|
|
@ -126,9 +188,15 @@ void usb_output_driver_set_rumble_callback(
|
|||
extern "C" uint16_t tud_hid_get_report_cb(
|
||||
uint8_t instance, uint8_t report_id, hid_report_type_t report_type,
|
||||
uint8_t* buffer, uint16_t requested_length) {
|
||||
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
|
||||
if (xinput_selected()) {
|
||||
return 0;
|
||||
}
|
||||
if (generic_selected()) {
|
||||
return generic_hid_get_report(instance, report_id, report_type,
|
||||
buffer, requested_length);
|
||||
}
|
||||
#endif
|
||||
return switch_pro_hid_get_report(instance, report_id, report_type, buffer,
|
||||
requested_length);
|
||||
}
|
||||
|
|
@ -136,7 +204,7 @@ extern "C" uint16_t tud_hid_get_report_cb(
|
|||
extern "C" void tud_hid_set_report_cb(
|
||||
uint8_t instance, uint8_t report_id, hid_report_type_t report_type,
|
||||
const uint8_t* buffer, uint16_t buffer_size) {
|
||||
if (!xinput_selected()) {
|
||||
if (switch_selected()) {
|
||||
switch_pro_hid_set_report(instance, report_id, report_type, buffer,
|
||||
buffer_size);
|
||||
}
|
||||
|
|
@ -145,16 +213,21 @@ extern "C" void tud_hid_set_report_cb(
|
|||
extern "C" void tud_hid_report_received_cb(
|
||||
uint8_t instance, uint8_t report_id, const uint8_t* buffer,
|
||||
uint16_t buffer_size) {
|
||||
if (!xinput_selected()) {
|
||||
if (switch_selected()) {
|
||||
switch_pro_hid_report_received(instance, report_id, buffer,
|
||||
buffer_size);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" uint8_t const* tud_hid_descriptor_report_cb(uint8_t instance) {
|
||||
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
|
||||
if (xinput_selected()) {
|
||||
return nullptr;
|
||||
}
|
||||
if (generic_selected()) {
|
||||
return generic_hid_report_descriptor(instance);
|
||||
}
|
||||
#endif
|
||||
return switch_pro_hid_report_descriptor(instance);
|
||||
}
|
||||
|
||||
|
|
@ -163,6 +236,11 @@ extern "C" uint8_t const* tud_descriptor_device_cb() {
|
|||
if (xinput_selected()) {
|
||||
return XInput::kDeviceDescriptor;
|
||||
}
|
||||
if (generic_selected()) {
|
||||
return g_mode == AdapterUsbMode::kDInput
|
||||
? GenericHid::kDInputDeviceDescriptor
|
||||
: GenericHid::kMacDeviceDescriptor;
|
||||
}
|
||||
if (g_mode == AdapterUsbMode::kSwitchProbe) {
|
||||
return XInput::kSwitchProbeDeviceDescriptor;
|
||||
}
|
||||
|
|
@ -176,6 +254,9 @@ extern "C" uint8_t const* tud_descriptor_configuration_cb(uint8_t index) {
|
|||
if (xinput_selected()) {
|
||||
return XInput::kConfigurationDescriptor;
|
||||
}
|
||||
if (generic_selected()) {
|
||||
return GenericHid::kConfigurationDescriptor;
|
||||
}
|
||||
#endif
|
||||
return switch_pro_configuration_descriptor;
|
||||
}
|
||||
|
|
@ -186,7 +267,9 @@ extern "C" uint16_t const* tud_descriptor_string_cb(uint8_t index,
|
|||
|
||||
#ifdef SWITCH_PICO_USB_OUTPUT_MODES
|
||||
adapter_host_probe_note_string_descriptor(index);
|
||||
if (index == 0xee && g_mode != AdapterUsbMode::kSwitch) {
|
||||
if (index == 0xee &&
|
||||
(xinput_selected() ||
|
||||
g_mode == AdapterUsbMode::kSwitchProbe)) {
|
||||
static constexpr char kSignature[] = "MSFT100";
|
||||
for (uint8_t i = 0; i < sizeof(kSignature) - 1; ++i) {
|
||||
g_string_descriptor[1 + i] = kSignature[i];
|
||||
|
|
@ -216,6 +299,23 @@ extern "C" uint16_t const* tud_descriptor_string_cb(uint8_t index,
|
|||
return nullptr;
|
||||
}
|
||||
string = kXInputStrings[index];
|
||||
} else if (generic_selected()) {
|
||||
if (index == 1) {
|
||||
string = reinterpret_cast<const uint8_t*>(
|
||||
GenericHid::kManufacturerString);
|
||||
} else if (index == 2) {
|
||||
string = reinterpret_cast<const uint8_t*>(
|
||||
g_mode == AdapterUsbMode::kDInput
|
||||
? GenericHid::kDInputProductString
|
||||
: GenericHid::kMacProductString);
|
||||
} else if (index == 3) {
|
||||
string = reinterpret_cast<const uint8_t*>(
|
||||
g_mode == AdapterUsbMode::kDInput
|
||||
? GenericHid::kDInputSerialString
|
||||
: GenericHid::kMacSerialString);
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
|
|
@ -271,14 +371,14 @@ extern "C" bool tud_control_request_cb(
|
|||
|
||||
extern "C" void tud_mount_cb() {
|
||||
LOG_PRINTF("[USB] mount_cb\n");
|
||||
if (!xinput_selected()) {
|
||||
if (switch_selected()) {
|
||||
switch_pro_mount();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void tud_umount_cb() {
|
||||
LOG_PRINTF("[USB] umount_cb\n");
|
||||
if (!xinput_selected()) {
|
||||
if (switch_selected()) {
|
||||
switch_pro_unmount();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,15 @@
|
|||
#include "controller_state.h"
|
||||
#include "switch_haptics.h"
|
||||
|
||||
// Mode capability bits published in USB management info. Input is present in
|
||||
// every current mode; rumble/motion describe host-visible USB capabilities.
|
||||
constexpr uint8_t USB_OUTPUT_CAPABILITY_INPUT = 1u << 0;
|
||||
constexpr uint8_t USB_OUTPUT_CAPABILITY_RUMBLE = 1u << 1;
|
||||
constexpr uint8_t USB_OUTPUT_CAPABILITY_MOTION = 1u << 2;
|
||||
constexpr uint8_t USB_OUTPUT_CAPABILITY_MASK =
|
||||
USB_OUTPUT_CAPABILITY_INPUT | USB_OUTPUT_CAPABILITY_RUMBLE |
|
||||
USB_OUTPUT_CAPABILITY_MOTION;
|
||||
|
||||
// Select and initialize the static USB output implementation. Call once before
|
||||
// tusb_init(); the selected descriptors and class driver remain fixed for the
|
||||
// lifetime of the USB device stack.
|
||||
|
|
@ -14,6 +23,7 @@ void usb_output_driver_init(AdapterUsbMode mode);
|
|||
const char* usb_output_driver_mode_name();
|
||||
AdapterUsbMode usb_output_driver_mode();
|
||||
const char* usb_output_driver_name();
|
||||
uint8_t usb_output_driver_capabilities();
|
||||
|
||||
void usb_output_driver_set_input(uint8_t instance,
|
||||
const ControllerState& state,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue