Introduce static USB output driver boundary
This commit is contained in:
parent
1837f42e21
commit
31a8cfb9d3
24 changed files with 1292 additions and 534 deletions
|
|
@ -94,6 +94,7 @@ endif()
|
|||
add_executable(switch-pico
|
||||
switch-pico.cpp
|
||||
switch_pro_driver.cpp
|
||||
usb_output_driver.cpp
|
||||
switch_haptics.cpp
|
||||
)
|
||||
if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
||||
|
|
@ -118,7 +119,7 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
|||
if(SWITCH_PICO_ADAPTER_FEASIBILITY)
|
||||
target_sources(switch-pico PRIVATE
|
||||
adapter_host_probe.cpp
|
||||
xinput_feasibility_driver.cpp
|
||||
xinput_driver.cpp
|
||||
)
|
||||
target_compile_definitions(switch-pico PRIVATE
|
||||
SWITCH_PICO_ADAPTER_FEASIBILITY=1
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
#include "hardware/structs/watchdog.h"
|
||||
#include "hardware/watchdog.h"
|
||||
#include "pico/time.h"
|
||||
#include "xinput_feasibility_descriptors.h"
|
||||
#include "xinput_descriptors.h"
|
||||
|
||||
namespace {
|
||||
|
||||
|
|
@ -82,8 +82,8 @@ bool adapter_host_probe_vendor_control(uint8_t rhport, uint8_t stage,
|
|||
sizeof(g_status_response));
|
||||
}
|
||||
|
||||
if (request->bRequest != XInputFeasibility::kMsVendorRequest ||
|
||||
request->wIndex != XInputFeasibility::kMsCompatIdIndex) {
|
||||
if (request->bRequest != XInput::kMsVendorRequest ||
|
||||
request->wIndex != XInput::kMsCompatIdIndex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -93,8 +93,8 @@ bool adapter_host_probe_vendor_control(uint8_t rhport, uint8_t stage,
|
|||
const bool queued = tud_control_xfer(
|
||||
rhport, request,
|
||||
const_cast<uint8_t *>(
|
||||
XInputFeasibility::kProbeMsCompatIdDescriptor),
|
||||
sizeof(XInputFeasibility::kProbeMsCompatIdDescriptor));
|
||||
XInput::kProbeMsCompatIdDescriptor),
|
||||
sizeof(XInput::kProbeMsCompatIdDescriptor));
|
||||
if (queued && g_reboot_alarm == 0) {
|
||||
g_reboot_alarm = add_alarm_in_ms(
|
||||
AdapterHostProbeState::kRebootDelayMs,
|
||||
|
|
@ -107,6 +107,6 @@ bool adapter_host_probe_vendor_control(uint8_t rhport, uint8_t stage,
|
|||
|
||||
return tud_control_xfer(
|
||||
rhport, request,
|
||||
const_cast<uint8_t *>(XInputFeasibility::kMsCompatIdDescriptor),
|
||||
sizeof(XInputFeasibility::kMsCompatIdDescriptor));
|
||||
const_cast<uint8_t *>(XInput::kMsCompatIdDescriptor),
|
||||
sizeof(XInput::kMsCompatIdDescriptor));
|
||||
}
|
||||
|
|
|
|||
105
switch-pico.cpp
105
switch-pico.cpp
|
|
@ -3,6 +3,7 @@
|
|||
#include "pico/stdlib.h"
|
||||
#include "tusb.h"
|
||||
#include "switch_pro_driver.h"
|
||||
#include "usb_output_driver.h"
|
||||
#ifndef SWITCH_PICO_BLUEPAD32
|
||||
#include "hardware/uart.h"
|
||||
#else
|
||||
|
|
@ -11,7 +12,6 @@
|
|||
#include "bootsel_pairing_button.h"
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
#include "adapter_host_probe.h"
|
||||
#include "xinput_feasibility_driver.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
|
@ -79,8 +79,8 @@ static void send_rumble_uart_frame(const ControllerRumbleOutput& rumble) {
|
|||
}
|
||||
#endif
|
||||
|
||||
static void on_rumble_from_switch(uint8_t instance,
|
||||
const ControllerRumbleOutput& rumble) {
|
||||
static void on_rumble_from_usb(uint8_t instance,
|
||||
const ControllerRumbleOutput& rumble) {
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
if (instance >= BLUEPAD32_INPUT_BACKEND_SLOT_COUNT) {
|
||||
return;
|
||||
|
|
@ -188,22 +188,13 @@ static void log_usb_state() {
|
|||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
for (uint8_t instance = 0;
|
||||
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
const bool ready =
|
||||
adapter_host_probe_mode() == AdapterUsbMode::kXInput
|
||||
? xinput_feasibility_is_ready(instance)
|
||||
: switch_pro_is_ready(instance);
|
||||
#else
|
||||
const bool ready = switch_pro_is_ready(instance);
|
||||
#endif
|
||||
const bool ready = usb_output_driver_is_ready(instance);
|
||||
if (ready != g_last_ready[instance]) {
|
||||
g_last_ready[instance] = ready;
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
LOG_PRINTF("[%s %u] driver %s\n",
|
||||
adapter_host_probe_mode() == AdapterUsbMode::kXInput
|
||||
? "XINPUT"
|
||||
: "SWITCH",
|
||||
instance, ready ? "ready" : "not ready");
|
||||
usb_output_driver_name(), instance,
|
||||
ready ? "ready" : "not ready");
|
||||
#else
|
||||
LOG_PRINTF("[SWITCH %u] driver %s\n", instance,
|
||||
ready ? "ready (handshake OK)" : "not ready");
|
||||
|
|
@ -211,7 +202,7 @@ static void log_usb_state() {
|
|||
}
|
||||
}
|
||||
#else
|
||||
const bool ready = switch_pro_is_ready(SWITCH_HID_INSTANCE);
|
||||
const bool ready = usb_output_driver_is_ready(SWITCH_HID_INSTANCE);
|
||||
if (ready != g_last_ready) {
|
||||
g_last_ready = ready;
|
||||
LOG_PRINTF("[SWITCH] driver %s\n",
|
||||
|
|
@ -233,56 +224,38 @@ int main() {
|
|||
#else
|
||||
init_uart_input();
|
||||
#endif
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
usb_output_driver_init(adapter_host_probe_mode());
|
||||
#else
|
||||
usb_output_driver_init(AdapterUsbMode::kSwitchProbe);
|
||||
#endif
|
||||
|
||||
tusb_init();
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
for (uint8_t instance = 0;
|
||||
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) {
|
||||
xinput_feasibility_init(instance);
|
||||
xinput_feasibility_set_rumble_callback(
|
||||
instance, on_rumble_from_switch);
|
||||
g_user_states[instance] = neutral_input();
|
||||
xinput_feasibility_set_input(instance,
|
||||
g_user_states[instance]);
|
||||
} else {
|
||||
switch_pro_init(instance);
|
||||
switch_pro_set_rumble_callback(instance,
|
||||
on_rumble_from_switch);
|
||||
g_user_states[instance] = neutral_input();
|
||||
switch_pro_set_input(
|
||||
instance, g_user_states[instance],
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD,
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD);
|
||||
}
|
||||
#else
|
||||
switch_pro_init(instance);
|
||||
switch_pro_set_rumble_callback(instance, on_rumble_from_switch);
|
||||
usb_output_driver_set_rumble_callback(instance,
|
||||
on_rumble_from_usb);
|
||||
g_user_states[instance] = neutral_input();
|
||||
switch_pro_set_input(
|
||||
usb_output_driver_set_input(
|
||||
instance, g_user_states[instance],
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD,
|
||||
CONTROLLER_PROFILE_DEFAULT_DIGITAL_THRESHOLD);
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
switch_pro_init(SWITCH_HID_INSTANCE);
|
||||
switch_pro_set_rumble_callback(SWITCH_HID_INSTANCE,
|
||||
on_rumble_from_switch);
|
||||
usb_output_driver_set_rumble_callback(SWITCH_HID_INSTANCE,
|
||||
on_rumble_from_usb);
|
||||
g_user_state = neutral_input();
|
||||
switch_pro_set_input(SWITCH_HID_INSTANCE, g_user_state,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
usb_output_driver_set_input(SWITCH_HID_INSTANCE, g_user_state,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
#endif
|
||||
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
bluepad32_input_backend_start();
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
LOG_PRINTF("[BOOT] adapter feasibility mode=%s\n",
|
||||
adapter_host_probe_mode() == AdapterUsbMode::kXInput
|
||||
? "XInput"
|
||||
: "Switch probe");
|
||||
usb_output_driver_mode_name());
|
||||
#else
|
||||
LOG_PRINTF("[BOOT] switch-pico starting (Bluepad32 wireless @ 115200)\n");
|
||||
#endif
|
||||
|
|
@ -308,11 +281,7 @@ int main() {
|
|||
}
|
||||
const uint32_t now_ms =
|
||||
static_cast<uint32_t>(to_ms_since_boot(get_absolute_time()));
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
const AdapterUsbMode output_mode = adapter_host_probe_mode();
|
||||
#else
|
||||
constexpr AdapterUsbMode output_mode = AdapterUsbMode::kSwitchProbe;
|
||||
#endif
|
||||
const AdapterUsbMode output_mode = usb_output_driver_mode();
|
||||
for (uint8_t instance = 0;
|
||||
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
|
||||
Bluepad32SlotSnapshot snapshot{};
|
||||
|
|
@ -341,40 +310,22 @@ int main() {
|
|||
}
|
||||
}
|
||||
g_user_states[instance] = transformed.state;
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
bool sent = false;
|
||||
if (output_mode == AdapterUsbMode::kXInput) {
|
||||
xinput_feasibility_set_input(instance,
|
||||
g_user_states[instance]);
|
||||
sent = xinput_feasibility_task(instance);
|
||||
} else {
|
||||
switch_pro_set_input(
|
||||
instance, g_user_states[instance],
|
||||
transformed.left_trigger_digital_threshold,
|
||||
transformed.right_trigger_digital_threshold);
|
||||
sent = switch_pro_task(instance);
|
||||
}
|
||||
if (sent) {
|
||||
bluepad32_input_backend_report_sent(instance);
|
||||
}
|
||||
#else
|
||||
switch_pro_set_input(
|
||||
usb_output_driver_set_input(
|
||||
instance, g_user_states[instance],
|
||||
transformed.left_trigger_digital_threshold,
|
||||
transformed.right_trigger_digital_threshold);
|
||||
if (switch_pro_task(instance)) {
|
||||
if (usb_output_driver_task(instance)) {
|
||||
bluepad32_input_backend_report_sent(instance);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
bool new_data = poll_uart_frames(); // Pull controller state from UART1
|
||||
(void)new_data;
|
||||
ControllerState state = g_user_state;
|
||||
switch_pro_set_input(SWITCH_HID_INSTANCE, state,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
(void)switch_pro_task(SWITCH_HID_INSTANCE);
|
||||
usb_output_driver_set_input(SWITCH_HID_INSTANCE, state,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
(void)usb_output_driver_task(SWITCH_HID_INSTANCE);
|
||||
#endif
|
||||
log_usb_state();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,6 @@
|
|||
#include "pico/rand.h"
|
||||
#include "pico/time.h"
|
||||
#include "tusb.h"
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
#include "adapter_host_probe.h"
|
||||
#include "xinput_feasibility_descriptors.h"
|
||||
#endif
|
||||
|
||||
#ifdef SWITCH_PICO_LOG
|
||||
#define LOG_PRINTF(...) printf(__VA_ARGS__)
|
||||
|
|
@ -1034,10 +1030,11 @@ bool switch_pro_is_ready(uint8_t instance) {
|
|||
return context != nullptr && context->is_ready;
|
||||
}
|
||||
|
||||
// HID callbacks
|
||||
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) {
|
||||
// TinyUSB callback helpers routed by usb_output_driver.
|
||||
uint16_t switch_pro_hid_get_report(uint8_t instance, uint8_t report_id,
|
||||
hid_report_type_t report_type,
|
||||
uint8_t* buffer,
|
||||
uint16_t requested_length) {
|
||||
(void)report_id;
|
||||
(void)report_type;
|
||||
SwitchProContext* context = context_for(instance);
|
||||
|
|
@ -1095,9 +1092,9 @@ static void process_output_report(uint8_t instance,
|
|||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
void switch_pro_hid_set_report(uint8_t instance, uint8_t report_id,
|
||||
hid_report_type_t report_type,
|
||||
const uint8_t* buffer, uint16_t buffer_size) {
|
||||
SwitchProContext* context = context_for(instance);
|
||||
if (context == nullptr || report_type != HID_REPORT_TYPE_OUTPUT) {
|
||||
return;
|
||||
|
|
@ -1105,9 +1102,9 @@ void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id,
|
|||
process_output_report(instance, *context, report_id, buffer, buffer_size);
|
||||
}
|
||||
|
||||
void tud_hid_report_received_cb(uint8_t instance, uint8_t report_id,
|
||||
const uint8_t* buffer,
|
||||
uint16_t buffer_size) {
|
||||
void switch_pro_hid_report_received(uint8_t instance, uint8_t report_id,
|
||||
const uint8_t* buffer,
|
||||
uint16_t buffer_size) {
|
||||
SwitchProContext* context = context_for(instance);
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
|
|
@ -1115,114 +1112,17 @@ void tud_hid_report_received_cb(uint8_t instance, uint8_t report_id,
|
|||
process_output_report(instance, *context, report_id, buffer, buffer_size);
|
||||
}
|
||||
|
||||
uint8_t const* tud_hid_descriptor_report_cb(uint8_t instance) {
|
||||
uint8_t const* switch_pro_hid_report_descriptor(uint8_t instance) {
|
||||
if (context_for(instance) == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return switch_pro_report_descriptor;
|
||||
}
|
||||
|
||||
uint8_t const* tud_descriptor_device_cb(void) {
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) {
|
||||
return XInputFeasibility::kDeviceDescriptor;
|
||||
}
|
||||
return XInputFeasibility::kSwitchProbeDeviceDescriptor;
|
||||
#else
|
||||
return switch_pro_device_descriptor;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint8_t const* tud_descriptor_configuration_cb(uint8_t index) {
|
||||
(void)index;
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) {
|
||||
return XInputFeasibility::kConfigurationDescriptor;
|
||||
}
|
||||
#endif
|
||||
return switch_pro_configuration_descriptor;
|
||||
}
|
||||
|
||||
bool tud_control_request_cb(uint8_t rhport,
|
||||
tusb_control_request_t const* request) {
|
||||
(void)rhport;
|
||||
(void)request;
|
||||
LOG_PRINTF(
|
||||
"[CTRL] bmReq=0x%02x bReq=0x%02x wValue=0x%04x wIndex=0x%04x "
|
||||
"wLen=%u\n",
|
||||
request->bmRequestType, request->bRequest, request->wValue,
|
||||
request->wIndex, request->wLength);
|
||||
return false; // let TinyUSB handle it normally
|
||||
}
|
||||
|
||||
void tud_mount_cb(void) {
|
||||
LOG_PRINTF("[USB] mount_cb\n");
|
||||
void switch_pro_mount() {
|
||||
reset_all_contexts(false);
|
||||
}
|
||||
|
||||
void tud_umount_cb(void) {
|
||||
LOG_PRINTF("[USB] umount_cb\n");
|
||||
void switch_pro_unmount() {
|
||||
reset_all_contexts(false);
|
||||
}
|
||||
|
||||
static uint16_t desc_str[32];
|
||||
|
||||
uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) {
|
||||
(void)langid;
|
||||
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
adapter_host_probe_note_string_descriptor(index);
|
||||
if (index == 0xee) {
|
||||
static constexpr char kSignature[] = "MSFT100";
|
||||
for (uint8_t i = 0; i < sizeof(kSignature) - 1; ++i) {
|
||||
desc_str[1 + i] = kSignature[i];
|
||||
}
|
||||
desc_str[8] = XInputFeasibility::kMsVendorRequest;
|
||||
desc_str[0] = static_cast<uint16_t>((0x03 << 8) | 18);
|
||||
return desc_str;
|
||||
}
|
||||
#endif
|
||||
|
||||
uint8_t chr_count = 0;
|
||||
if (index == 0) {
|
||||
memcpy(&desc_str[1], switch_pro_string_language, 2);
|
||||
chr_count = 1;
|
||||
} else {
|
||||
const uint8_t* str = nullptr;
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
static const uint8_t kManufacturer[] = "Switch Pico";
|
||||
static const uint8_t kProduct[] = "XInput Feasibility";
|
||||
static const uint8_t kSerial[] = "XINPUT-PROTOTYPE";
|
||||
static const uint8_t* const kXInputStrings[] = {
|
||||
nullptr, kManufacturer, kProduct, kSerial};
|
||||
if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) {
|
||||
if (index >= sizeof(kXInputStrings) /
|
||||
sizeof(kXInputStrings[0])) {
|
||||
return nullptr;
|
||||
}
|
||||
str = kXInputStrings[index];
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
if (index >= sizeof(switch_pro_string_descriptors) /
|
||||
sizeof(switch_pro_string_descriptors[0])) {
|
||||
return nullptr;
|
||||
}
|
||||
str = switch_pro_string_descriptors[index];
|
||||
}
|
||||
|
||||
while (str[chr_count] != 0) {
|
||||
++chr_count;
|
||||
}
|
||||
if (chr_count > 31) {
|
||||
chr_count = 31;
|
||||
}
|
||||
for (uint8_t i = 0; i < chr_count; ++i) {
|
||||
desc_str[1 + i] = str[i];
|
||||
}
|
||||
}
|
||||
|
||||
desc_str[0] =
|
||||
static_cast<uint16_t>((0x03 << 8) | (2 * chr_count + 2));
|
||||
return desc_str;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include "controller_color.h"
|
||||
#include "controller_state.h"
|
||||
#include "switch_haptics.h"
|
||||
#include "tusb.h"
|
||||
#include "switch_pro_descriptors.h"
|
||||
// Preserve the pre-neutral-state 35%-of-1023 digital trigger boundary.
|
||||
constexpr uint32_t SWITCH_PRO_LEGACY_TRIGGER_RANGE_MAXIMUM = 1023;
|
||||
|
|
@ -45,4 +46,19 @@ bool switch_pro_apply_uart_packet(const uint8_t* packet, uint8_t length,
|
|||
bool switch_pro_is_ready(uint8_t instance);
|
||||
|
||||
void switch_pro_set_rumble_callback(uint8_t instance,
|
||||
ControllerRumbleCallback callback);
|
||||
ControllerRumbleCallback callback);
|
||||
|
||||
// TinyUSB-facing helpers called only by usb_output_driver.
|
||||
uint16_t switch_pro_hid_get_report(uint8_t instance, uint8_t report_id,
|
||||
hid_report_type_t report_type,
|
||||
uint8_t* buffer,
|
||||
uint16_t requested_length);
|
||||
void switch_pro_hid_set_report(uint8_t instance, uint8_t report_id,
|
||||
hid_report_type_t report_type,
|
||||
const uint8_t* buffer, uint16_t buffer_size);
|
||||
void switch_pro_hid_report_received(uint8_t instance, uint8_t report_id,
|
||||
const uint8_t* buffer,
|
||||
uint16_t buffer_size);
|
||||
uint8_t const* switch_pro_hid_report_descriptor(uint8_t instance);
|
||||
void switch_pro_mount();
|
||||
void switch_pro_unmount();
|
||||
36
tests/native_stubs/device/usbd_pvt.h
Normal file
36
tests/native_stubs/device/usbd_pvt.h
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#pragma once
|
||||
|
||||
#include "tusb.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
char const* name;
|
||||
void (*init)(void);
|
||||
bool (*deinit)(void);
|
||||
void (*reset)(uint8_t rhport);
|
||||
uint16_t (*open)(uint8_t rhport,
|
||||
tusb_desc_interface_t const* interface_descriptor,
|
||||
uint16_t max_length);
|
||||
bool (*control_xfer_cb)(uint8_t rhport, uint8_t stage,
|
||||
tusb_control_request_t const* request);
|
||||
bool (*xfer_cb)(uint8_t rhport, uint8_t endpoint,
|
||||
xfer_result_t result, uint32_t transferred);
|
||||
void (*sof)(uint8_t rhport, uint32_t frame_count);
|
||||
} usbd_class_driver_t;
|
||||
|
||||
usbd_class_driver_t const* usbd_app_driver_get_cb(uint8_t* driver_count);
|
||||
|
||||
bool usbd_edpt_open(uint8_t rhport,
|
||||
tusb_desc_endpoint_t const* endpoint_descriptor);
|
||||
bool usbd_edpt_xfer(uint8_t rhport, uint8_t endpoint, uint8_t* buffer,
|
||||
uint16_t total_bytes);
|
||||
bool usbd_edpt_busy(uint8_t rhport, uint8_t endpoint);
|
||||
bool usbd_edpt_claim(uint8_t rhport, uint8_t endpoint);
|
||||
bool usbd_edpt_release(uint8_t rhport, uint8_t endpoint);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
|
@ -14,6 +15,54 @@ typedef enum {
|
|||
HID_REPORT_TYPE_FEATURE = 3,
|
||||
} hid_report_type_t;
|
||||
|
||||
typedef enum {
|
||||
XFER_RESULT_SUCCESS = 0,
|
||||
XFER_RESULT_FAILED,
|
||||
} xfer_result_t;
|
||||
|
||||
enum {
|
||||
TUSB_DIR_OUT = 0,
|
||||
TUSB_DIR_IN = 1,
|
||||
TUSB_DESC_ENDPOINT = 5,
|
||||
TUSB_DESC_STRING = 3,
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
typedef struct {
|
||||
uint8_t bLength;
|
||||
uint8_t bDescriptorType;
|
||||
uint8_t bInterfaceNumber;
|
||||
uint8_t bAlternateSetting;
|
||||
uint8_t bNumEndpoints;
|
||||
uint8_t bInterfaceClass;
|
||||
uint8_t bInterfaceSubClass;
|
||||
uint8_t bInterfaceProtocol;
|
||||
uint8_t iInterface;
|
||||
} tusb_desc_interface_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t bLength;
|
||||
uint8_t bDescriptorType;
|
||||
uint8_t bEndpointAddress;
|
||||
uint8_t bmAttributes;
|
||||
uint16_t wMaxPacketSize;
|
||||
uint8_t bInterval;
|
||||
} tusb_desc_endpoint_t;
|
||||
#pragma pack(pop)
|
||||
|
||||
static inline uint8_t const* tu_desc_next(void const* descriptor) {
|
||||
uint8_t const* bytes = (uint8_t const*)descriptor;
|
||||
return bytes + bytes[0];
|
||||
}
|
||||
|
||||
static inline uint8_t tu_desc_type(void const* descriptor) {
|
||||
return ((uint8_t const*)descriptor)[1];
|
||||
}
|
||||
|
||||
static inline uint8_t tu_edpt_dir(uint8_t endpoint) {
|
||||
return (endpoint & 0x80u) != 0 ? TUSB_DIR_IN : TUSB_DIR_OUT;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
uint8_t bmRequestType;
|
||||
uint8_t bRequest;
|
||||
|
|
@ -27,6 +76,7 @@ bool tud_hid_n_report(uint8_t instance, uint8_t report_id,
|
|||
const void* report, uint16_t length);
|
||||
bool tud_suspended(void);
|
||||
bool tud_remote_wakeup(void);
|
||||
bool tud_ready(void);
|
||||
|
||||
uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id,
|
||||
hid_report_type_t report_type, uint8_t* buffer,
|
||||
|
|
@ -39,6 +89,14 @@ void tud_hid_report_received_cb(uint8_t instance, uint8_t report_id,
|
|||
uint8_t const* tud_hid_descriptor_report_cb(uint8_t instance);
|
||||
void tud_mount_cb(void);
|
||||
void tud_umount_cb(void);
|
||||
uint8_t const* tud_descriptor_device_cb(void);
|
||||
uint8_t const* tud_descriptor_configuration_cb(uint8_t index);
|
||||
uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid);
|
||||
bool tud_vendor_control_xfer_cb(
|
||||
uint8_t rhport, uint8_t stage,
|
||||
tusb_control_request_t const* request);
|
||||
bool tud_control_request_cb(uint8_t rhport,
|
||||
tusb_control_request_t const* request);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "switch_pro_driver.h"
|
||||
#include "usb_output_driver.h"
|
||||
#include "controller_color_config.h"
|
||||
#include "tusb.h"
|
||||
#include "pico/time.h"
|
||||
|
|
@ -57,8 +58,8 @@ void initialize_contexts() {
|
|||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
hid_ready[instance] = true;
|
||||
hid_report_succeeds[instance] = true;
|
||||
switch_pro_init(instance);
|
||||
}
|
||||
usb_output_driver_init(AdapterUsbMode::kSwitchProbe);
|
||||
clear_sent_reports();
|
||||
}
|
||||
|
||||
|
|
@ -500,7 +501,7 @@ void test_rumble_callbacks_and_decoders_are_isolated() {
|
|||
initialize_contexts();
|
||||
rumble_events = {};
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
switch_pro_set_rumble_callback(instance, rumble_callback);
|
||||
usb_output_driver_set_rumble_callback(instance, rumble_callback);
|
||||
}
|
||||
constexpr uint32_t neutral = 0x40400100u;
|
||||
auto full_payload = rumble_payload(type_2(64, 16, 64, 16), neutral);
|
||||
|
|
@ -592,7 +593,7 @@ void test_lifecycle_and_invalid_instances() {
|
|||
switch_pro_set_input(kInvalidInstance, ignored,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
switch_pro_set_rumble_callback(kInvalidInstance, rumble_callback);
|
||||
usb_output_driver_set_rumble_callback(kInvalidInstance, rumble_callback);
|
||||
expect(!switch_pro_task(kInvalidInstance),
|
||||
"invalid instance ran a driver task");
|
||||
expect(!switch_pro_is_ready(kInvalidInstance),
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ def test_switch_pro_driver_four_contexts_native(tmp_path: Path) -> None:
|
|||
f"-I{root / 'tests' / 'native_stubs'}",
|
||||
f"-I{root}",
|
||||
str(root / "switch_pro_driver.cpp"),
|
||||
str(root / "usb_output_driver.cpp"),
|
||||
str(root / "switch_haptics.cpp"),
|
||||
str(root / "tests" / "switch_pro_driver_context_test.cpp"),
|
||||
"-o",
|
||||
|
|
|
|||
54
tests/test_usb_output_driver_native.py
Normal file
54
tests/test_usb_output_driver_native.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
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 test_usb_output_driver_contracts(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = host_compiler()
|
||||
backends = (
|
||||
("uart", []),
|
||||
("bluepad32", ["-DSWITCH_PICO_BLUEPAD32=1"]),
|
||||
)
|
||||
for backend, backend_definitions in backends:
|
||||
for instance_count in range(1, 5):
|
||||
executable = (
|
||||
tmp_path
|
||||
/ f"usb_output_driver_{backend}_{instance_count}_test"
|
||||
)
|
||||
result = subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-DSWITCH_PICO_HID_INSTANCE_COUNT={instance_count}",
|
||||
"-DSWITCH_PICO_ADAPTER_FEASIBILITY=1",
|
||||
*backend_definitions,
|
||||
f"-I{root / 'tests' / 'native_stubs'}",
|
||||
f"-I{root}",
|
||||
str(root / "tests" / "usb_output_driver_test.cpp"),
|
||||
str(root / "switch_pro_driver.cpp"),
|
||||
str(root / "usb_output_driver.cpp"),
|
||||
str(root / "xinput_driver.cpp"),
|
||||
str(root / "switch_haptics.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=False,
|
||||
cwd=root,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
_ = subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
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 test_xinput_feasibility_contracts(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = host_compiler()
|
||||
for instance_count in range(1, 5):
|
||||
executable = tmp_path / f"xinput_feasibility_{instance_count}_test"
|
||||
result = subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-DSWITCH_PICO_HID_INSTANCE_COUNT={instance_count}",
|
||||
f"-I{root}",
|
||||
str(root / "tests" / "xinput_feasibility_test.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=False,
|
||||
cwd=root,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
_ = subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
|
@ -158,10 +158,10 @@ void perform_out(UsbConfigurationManagement::Operation operation,
|
|||
tusb_control_request_t request = setup_request(
|
||||
operation, TUSB_DIR_OUT,
|
||||
static_cast<uint16_t>(next_out_payload.size()));
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
require(usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request),
|
||||
"valid OUT setup was rejected");
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
require(usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_ACK, &request) == expected_ack,
|
||||
"OUT acknowledgement result was incorrect");
|
||||
}
|
||||
|
|
@ -177,7 +177,7 @@ void test_vendor_requests() {
|
|||
|
||||
tusb_control_request_t request = setup_request(
|
||||
Operation::kPairingRead, TUSB_DIR_IN, kMaximumResponseSize);
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
require(usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_payload[5] ==
|
||||
static_cast<uint8_t>(Operation::kPairingRead) &&
|
||||
|
|
@ -221,14 +221,14 @@ void test_vendor_requests() {
|
|||
request = setup_request(
|
||||
Operation::kPairingRefresh, TUSB_DIR_OUT,
|
||||
static_cast<uint16_t>(next_out_payload.size()));
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
require(usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
!tud_vendor_control_xfer_cb(
|
||||
!usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_ACK, &request),
|
||||
"bad request CRC was accepted");
|
||||
|
||||
request.wValue = 0;
|
||||
require(!tud_vendor_control_xfer_cb(
|
||||
require(!usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request),
|
||||
"request with invalid magic was accepted");
|
||||
}
|
||||
|
|
@ -251,7 +251,7 @@ void test_profile_vendor_requests() {
|
|||
current_profile_list.rows[1].active_profile = 2;
|
||||
tusb_control_request_t request = setup_request(
|
||||
Operation::kProfileList, TUSB_DIR_IN, kMaximumResponseSize);
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
require(usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_payload.size() == kResponseHeaderSize + 33 &&
|
||||
control_payload[5] ==
|
||||
|
|
@ -274,7 +274,7 @@ void test_profile_vendor_requests() {
|
|||
controller_profile_default(expected_identity, 2);
|
||||
request = setup_request(
|
||||
Operation::kProfileRead, TUSB_DIR_IN, kMaximumResponseSize);
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
require(usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_payload.size() ==
|
||||
kResponseHeaderSize +
|
||||
|
|
@ -299,7 +299,7 @@ void test_profile_vendor_requests() {
|
|||
request = setup_request(
|
||||
Operation::kProfileTransactionStatus, TUSB_DIR_IN,
|
||||
kMaximumResponseSize);
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
require(usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_payload.size() == kResponseHeaderSize + 20 &&
|
||||
control_payload[6] ==
|
||||
|
|
@ -313,7 +313,7 @@ void test_profile_vendor_requests() {
|
|||
current_profile_transaction.transaction.stored_generation =
|
||||
0x11223344;
|
||||
current_profile_transaction.transaction.stored_crc = 0xaabbccdd;
|
||||
require(tud_vendor_control_xfer_cb(
|
||||
require(usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request) &&
|
||||
control_payload[6] == static_cast<uint8_t>(Status::kOk) &&
|
||||
read_u32(control_payload, kResponseHeaderSize) ==
|
||||
|
|
@ -391,7 +391,7 @@ void test_profile_vendor_requests() {
|
|||
perform_out(Operation::kProfileActivate, mutation, false);
|
||||
request = setup_request(
|
||||
Operation::kProfileReset, TUSB_DIR_OUT, kRequestHeaderSize + 15);
|
||||
require(!tud_vendor_control_xfer_cb(
|
||||
require(!usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request),
|
||||
"legacy profile reset payload was accepted");
|
||||
|
||||
|
|
@ -400,7 +400,7 @@ void test_profile_vendor_requests() {
|
|||
request = setup_request(
|
||||
Operation::kProfileSelect, TUSB_DIR_OUT,
|
||||
kRequestHeaderSize + 14);
|
||||
require(!tud_vendor_control_xfer_cb(
|
||||
require(!usb_configuration_management_vendor_control(
|
||||
0, CONTROL_STAGE_SETUP, &request),
|
||||
"short profile selection request was accepted");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,9 +36,6 @@ bool tud_control_xfer(uint8_t rhport,
|
|||
void* buffer, uint16_t length);
|
||||
bool tud_control_status(uint8_t rhport,
|
||||
const tusb_control_request_t* request);
|
||||
bool tud_vendor_control_xfer_cb(
|
||||
uint8_t rhport, uint8_t stage,
|
||||
const tusb_control_request_t* request);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
677
tests/usb_output_driver_test.cpp
Normal file
677
tests/usb_output_driver_test.cpp
Normal file
|
|
@ -0,0 +1,677 @@
|
|||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
#include "adapter_host_probe_state.h"
|
||||
#include "xinput_descriptors.h"
|
||||
#include "xinput_protocol.h"
|
||||
#include "device/usbd_pvt.h"
|
||||
#include "pico/time.h"
|
||||
#include "switch_pro_driver.h"
|
||||
#include "tusb.h"
|
||||
#include "usb_output_driver.h"
|
||||
#include "xinput_driver.h"
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
|
||||
constexpr uint8_t kInstanceCount = SWITCH_PICO_HID_INSTANCE_COUNT;
|
||||
constexpr uint8_t kInvalidInstance = kInstanceCount;
|
||||
|
||||
struct EndpointHarness {
|
||||
bool opened = false;
|
||||
bool busy = false;
|
||||
bool claimed = false;
|
||||
uint8_t* armed_buffer = nullptr;
|
||||
uint16_t armed_length = 0;
|
||||
std::array<uint8_t, 64> last_transfer{};
|
||||
uint16_t last_transfer_length = 0;
|
||||
unsigned transfer_count = 0;
|
||||
};
|
||||
|
||||
struct RumbleEvent {
|
||||
unsigned count = 0;
|
||||
ControllerRumbleOutput output{};
|
||||
};
|
||||
|
||||
std::array<EndpointHarness, 256> endpoint_harness{};
|
||||
std::array<RumbleEvent, kInstanceCount> xinput_rumble_events{};
|
||||
uint64_t now_ms = 0;
|
||||
uint32_t random_value = 1;
|
||||
bool usb_ready = true;
|
||||
unsigned hid_report_count = 0;
|
||||
unsigned switch_inactive_rumble_count = 0;
|
||||
unsigned observed_string_count = 0;
|
||||
uint8_t last_observed_string = 0;
|
||||
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
bool management_vendor_control_result = false;
|
||||
unsigned management_vendor_control_count = 0;
|
||||
uint8_t management_vendor_control_rhport = 0;
|
||||
uint8_t management_vendor_control_stage = 0;
|
||||
const tusb_control_request_t* management_vendor_control_request = nullptr;
|
||||
#endif
|
||||
|
||||
void expect(bool condition, const char *message) {
|
||||
if (!condition) {
|
||||
std::cerr << "FAIL: " << message << '\n';
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t read_le16(const uint8_t *data) {
|
||||
return static_cast<uint16_t>(data[0] | (data[1] << 8));
|
||||
}
|
||||
|
||||
uint32_t read_le32(const uint8_t *data) {
|
||||
return static_cast<uint32_t>(data[0]) |
|
||||
(static_cast<uint32_t>(data[1]) << 8) |
|
||||
(static_cast<uint32_t>(data[2]) << 16) |
|
||||
(static_cast<uint32_t>(data[3]) << 24);
|
||||
}
|
||||
|
||||
void test_device_and_configuration_descriptors() {
|
||||
using namespace XInput;
|
||||
expect(read_le16(&kSwitchProbeDeviceDescriptor[8]) ==
|
||||
kSwitchProbeVendorId,
|
||||
"Switch probe VID mismatch");
|
||||
expect(read_le16(&kSwitchProbeDeviceDescriptor[10]) ==
|
||||
kSwitchProbeProductId,
|
||||
"Switch probe PID mismatch");
|
||||
expect(read_le16(&kSwitchProbeDeviceDescriptor[12]) ==
|
||||
kSwitchProbeDeviceRevision,
|
||||
"Switch probe revision mismatch");
|
||||
expect(kSwitchProbeDeviceRevision != 0x0210,
|
||||
"Switch probe reuses the genuine controller cache identity");
|
||||
expect(read_le16(&kDeviceDescriptor[8]) == kDevelopmentVendorId,
|
||||
"development VID mismatch");
|
||||
expect(read_le16(&kDeviceDescriptor[10]) == kDevelopmentProductId,
|
||||
"development PID mismatch");
|
||||
expect(read_le16(&kDeviceDescriptor[12]) ==
|
||||
kDevelopmentDeviceRevision,
|
||||
"development revision mismatch");
|
||||
expect(kDeviceDescriptor[4] == 0 && kDeviceDescriptor[5] == 0 &&
|
||||
kDeviceDescriptor[6] == 0,
|
||||
"multi-interface development device is not composite");
|
||||
expect(kDevelopmentVendorId != 0x045e,
|
||||
"development device must not impersonate Microsoft's VID");
|
||||
expect(read_le16(&kConfigurationDescriptor[2]) ==
|
||||
sizeof(kConfigurationDescriptor),
|
||||
"configuration total length mismatch");
|
||||
expect(kConfigurationDescriptor[4] == SWITCH_PICO_HID_INSTANCE_COUNT,
|
||||
"configuration interface count mismatch");
|
||||
|
||||
std::array<bool, 16> endpoints{};
|
||||
for (uint8_t instance = 0; instance < SWITCH_PICO_HID_INSTANCE_COUNT;
|
||||
++instance) {
|
||||
const size_t offset = 9 + instance * kInterfaceDescriptorSize;
|
||||
const uint8_t *interface = &kConfigurationDescriptor[offset];
|
||||
expect(interface[0] == 9 && interface[1] == 4,
|
||||
"missing interface descriptor");
|
||||
expect(interface[2] == instance, "interface number mismatch");
|
||||
expect(interface[5] == 0xff && interface[6] == 0x5d &&
|
||||
interface[7] == 0x01,
|
||||
"XInput interface class tuple mismatch");
|
||||
expect(interface[9] == 0x10 && interface[10] == 0x21,
|
||||
"XInput capability descriptor missing");
|
||||
const uint8_t in_endpoint = interface[27];
|
||||
const uint8_t out_endpoint = interface[34];
|
||||
expect(in_endpoint == static_cast<uint8_t>(0x81 + instance),
|
||||
"input endpoint mismatch");
|
||||
expect(out_endpoint == static_cast<uint8_t>(0x01 + instance),
|
||||
"output endpoint mismatch");
|
||||
expect(interface[15] == in_endpoint && interface[21] == out_endpoint,
|
||||
"capability descriptor endpoint mismatch");
|
||||
expect(!endpoints[in_endpoint & 0x0f] &&
|
||||
!endpoints[out_endpoint & 0x0f],
|
||||
"endpoint number reused");
|
||||
endpoints[in_endpoint & 0x0f] = true;
|
||||
}
|
||||
}
|
||||
|
||||
void test_microsoft_compatible_id_descriptor() {
|
||||
using namespace XInput;
|
||||
expect(read_le32(kMsCompatIdDescriptor) == sizeof(kMsCompatIdDescriptor),
|
||||
"Microsoft descriptor total length mismatch");
|
||||
expect(read_le16(&kMsCompatIdDescriptor[4]) == 0x0100,
|
||||
"Microsoft descriptor version mismatch");
|
||||
expect(read_le16(&kMsCompatIdDescriptor[6]) == kMsCompatIdIndex,
|
||||
"Microsoft descriptor index mismatch");
|
||||
expect(kMsCompatIdDescriptor[8] == SWITCH_PICO_HID_INSTANCE_COUNT,
|
||||
"Microsoft function count mismatch");
|
||||
for (uint8_t instance = 0;
|
||||
instance < SWITCH_PICO_HID_INSTANCE_COUNT; ++instance) {
|
||||
const uint8_t *function = &kMsCompatIdDescriptor[16 + instance * 24];
|
||||
expect(function[0] == instance,
|
||||
"Microsoft descriptor interface mismatch");
|
||||
expect(std::memcmp(&function[2], "XUSB10", 6) == 0,
|
||||
"XUSB10 compatible ID missing");
|
||||
}
|
||||
expect(read_le32(kProbeMsCompatIdDescriptor) == 16 &&
|
||||
kProbeMsCompatIdDescriptor[8] == 0,
|
||||
"probe descriptor must expose no compatible functions");
|
||||
}
|
||||
|
||||
void test_input_report_mapping() {
|
||||
ControllerState state{};
|
||||
auto report = XInput::build_input_report(state);
|
||||
expect(report.report_id == 0 && report.report_size == 20,
|
||||
"neutral report header mismatch");
|
||||
expect(report.buttons == 0 && report.left_trigger == 0 &&
|
||||
report.right_trigger == 0,
|
||||
"neutral report controls mismatch");
|
||||
expect(report.left_x == 0 && report.left_y == 0 && report.right_x == 0 &&
|
||||
report.right_y == 0,
|
||||
"neutral axes mismatch");
|
||||
|
||||
state.dpad_up = true;
|
||||
state.button_south = true;
|
||||
state.button_east = true;
|
||||
state.button_west = true;
|
||||
state.button_north = true;
|
||||
state.button_start = true;
|
||||
state.button_select = true;
|
||||
state.button_system = true;
|
||||
state.left_trigger = UINT16_MAX;
|
||||
state.right_trigger = UINT16_MAX;
|
||||
state.left_stick_x = INT16_MIN;
|
||||
state.left_stick_y = INT16_MIN;
|
||||
state.right_stick_x = INT16_MAX;
|
||||
state.right_stick_y = INT16_MAX;
|
||||
report = XInput::build_input_report(state);
|
||||
expect((report.buttons & XInput::kDpadUp) != 0,
|
||||
"D-pad mapping missing");
|
||||
expect((report.buttons & XInput::kButtonA) != 0 &&
|
||||
(report.buttons & XInput::kButtonB) != 0 &&
|
||||
(report.buttons & XInput::kButtonX) != 0 &&
|
||||
(report.buttons & XInput::kButtonY) != 0,
|
||||
"positional face-button mapping mismatch");
|
||||
expect(report.left_trigger == 0xff && report.right_trigger == 0xff,
|
||||
"full analog trigger mapping mismatch");
|
||||
expect(report.left_x == INT16_MIN && report.left_y == INT16_MAX &&
|
||||
report.right_x == INT16_MAX &&
|
||||
report.right_y == -INT16_MAX,
|
||||
"axis endpoint mapping mismatch");
|
||||
|
||||
state.left_trigger = 0x8000;
|
||||
state.right_trigger = 0x7fff;
|
||||
report = XInput::build_input_report(state);
|
||||
expect(report.left_trigger == 0x80 && report.right_trigger == 0x7f,
|
||||
"analog trigger precision was discarded");
|
||||
}
|
||||
|
||||
void test_rumble_report() {
|
||||
const uint8_t packet[8] = {0x00, 0x08, 0x00, 0xa5, 0x5a, 0x00, 0x00, 0x00};
|
||||
ControllerRumbleOutput output{};
|
||||
expect(XInput::parse_rumble_report(packet, sizeof(packet), &output),
|
||||
"valid rumble report rejected");
|
||||
expect(output.low_frequency_magnitude == 0xa5 &&
|
||||
output.high_frequency_magnitude == 0x5a,
|
||||
"rumble magnitudes mapped incorrectly");
|
||||
expect(!XInput::parse_rumble_report(packet, 4, &output),
|
||||
"truncated rumble report accepted");
|
||||
uint8_t wrong_type[8]{};
|
||||
expect(!XInput::parse_rumble_report(wrong_type, sizeof(wrong_type),
|
||||
&output),
|
||||
"wrong rumble report type accepted");
|
||||
}
|
||||
|
||||
void test_host_probe_sequence() {
|
||||
AdapterHostProbeState state;
|
||||
state.note_ms_compat_id_request(10);
|
||||
expect(!state.windows_confirmed(),
|
||||
"compatible-ID request without signature confirmed Windows");
|
||||
state.note_ms_os_string();
|
||||
expect(state.saw_ms_os_string(),
|
||||
"Microsoft OS string observation was not retained");
|
||||
state.note_ms_compat_id_request(20);
|
||||
expect(state.windows_confirmed(),
|
||||
"two-stage Windows signature not confirmed");
|
||||
expect(!state.should_reboot(119), "probe rebooted before delay");
|
||||
expect(state.should_reboot(120), "probe did not reboot at deadline");
|
||||
|
||||
AdapterHostProbeState wrapped;
|
||||
wrapped.note_ms_os_string();
|
||||
wrapped.note_ms_compat_id_request(UINT32_MAX - 50);
|
||||
expect(!wrapped.should_reboot(48),
|
||||
"wrapped timer rebooted before deadline");
|
||||
expect(wrapped.should_reboot(49), "wrapped timer missed deadline");
|
||||
}
|
||||
|
||||
void reset_usb_harness() {
|
||||
endpoint_harness = {};
|
||||
xinput_rumble_events = {};
|
||||
now_ms = 0;
|
||||
usb_ready = true;
|
||||
hid_report_count = 0;
|
||||
switch_inactive_rumble_count = 0;
|
||||
observed_string_count = 0;
|
||||
last_observed_string = 0;
|
||||
}
|
||||
|
||||
void xinput_rumble_callback(uint8_t instance,
|
||||
const ControllerRumbleOutput& output) {
|
||||
expect(instance < xinput_rumble_events.size(),
|
||||
"XInput rumble used an invalid instance");
|
||||
if (instance < xinput_rumble_events.size()) {
|
||||
++xinput_rumble_events[instance].count;
|
||||
xinput_rumble_events[instance].output = output;
|
||||
}
|
||||
}
|
||||
|
||||
void inactive_switch_rumble_callback(
|
||||
uint8_t instance, const ControllerRumbleOutput& output) {
|
||||
(void)instance;
|
||||
(void)output;
|
||||
++switch_inactive_rumble_count;
|
||||
}
|
||||
|
||||
void expect_usb_string(uint8_t index, const char* expected,
|
||||
const char* message) {
|
||||
const uint16_t* descriptor = tud_descriptor_string_cb(index, 0x0409);
|
||||
const size_t length = std::strlen(expected);
|
||||
bool matches = descriptor != nullptr &&
|
||||
(descriptor[0] & 0xffu) == 2u * length + 2u &&
|
||||
(descriptor[0] >> 8u) == TUSB_DESC_STRING;
|
||||
if (matches) {
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
if (descriptor[i + 1] !=
|
||||
static_cast<uint8_t>(expected[i])) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(matches, message);
|
||||
}
|
||||
|
||||
void test_switch_boundary_dispatch() {
|
||||
reset_usb_harness();
|
||||
usb_output_driver_init(AdapterUsbMode::kSwitchProbe);
|
||||
expect(usb_output_driver_mode() == AdapterUsbMode::kSwitchProbe &&
|
||||
std::strcmp(usb_output_driver_name(), "SWITCH") == 0 &&
|
||||
std::strcmp(usb_output_driver_mode_name(),
|
||||
"Switch probe") == 0,
|
||||
"Switch boundary mode was not frozen");
|
||||
expect(std::memcmp(tud_descriptor_device_cb(),
|
||||
XInput::kSwitchProbeDeviceDescriptor,
|
||||
sizeof(XInput::kSwitchProbeDeviceDescriptor)) == 0,
|
||||
"Switch probe device descriptor changed at the boundary");
|
||||
expect(std::memcmp(tud_descriptor_configuration_cb(0),
|
||||
switch_pro_configuration_descriptor,
|
||||
sizeof(switch_pro_configuration_descriptor)) == 0,
|
||||
"Switch configuration descriptor changed at the boundary");
|
||||
const uint8_t* hid_descriptor = tud_hid_descriptor_report_cb(0);
|
||||
expect(hid_descriptor != nullptr &&
|
||||
std::memcmp(hid_descriptor, switch_pro_report_descriptor,
|
||||
sizeof(switch_pro_report_descriptor)) == 0,
|
||||
"Switch HID report descriptor changed at the boundary");
|
||||
expect(tud_hid_descriptor_report_cb(kInvalidInstance) == nullptr,
|
||||
"Switch HID callback accepted an invalid instance");
|
||||
|
||||
std::array<uint8_t, SWITCH_PRO_ENDPOINT_SIZE> report{};
|
||||
expect(tud_hid_get_report_cb(0, 0, HID_REPORT_TYPE_INPUT,
|
||||
report.data(), report.size()) ==
|
||||
sizeof(SwitchProReport),
|
||||
"Switch GET_REPORT was not dispatched");
|
||||
expect(tud_hid_get_report_cb(kInvalidInstance, 0,
|
||||
HID_REPORT_TYPE_INPUT, report.data(),
|
||||
report.size()) == 0,
|
||||
"Switch GET_REPORT accepted an invalid instance");
|
||||
|
||||
uint8_t driver_count = 0xff;
|
||||
expect(usbd_app_driver_get_cb(&driver_count) == nullptr &&
|
||||
driver_count == 0,
|
||||
"Switch mode registered the XInput custom class");
|
||||
expect(usbd_app_driver_get_cb(nullptr) == nullptr,
|
||||
"custom class callback accepted a null count");
|
||||
|
||||
ControllerState switch_state{};
|
||||
switch_state.button_south = true;
|
||||
usb_output_driver_set_input(0, switch_state,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD,
|
||||
SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD);
|
||||
now_ms = 15;
|
||||
expect(usb_output_driver_task(0) && hid_report_count == 1,
|
||||
"Switch input task was not dispatched");
|
||||
expect(tud_hid_get_report_cb(0, 0, HID_REPORT_TYPE_INPUT,
|
||||
report.data(), report.size()) ==
|
||||
sizeof(SwitchProReport),
|
||||
"dispatched Switch report was unavailable");
|
||||
SwitchProReport switch_report{};
|
||||
std::memcpy(&switch_report, report.data(), sizeof(switch_report));
|
||||
expect(switch_report.inputs.buttonB,
|
||||
"Switch boundary did not apply input state");
|
||||
hid_report_count = 0;
|
||||
|
||||
expect(usb_output_driver_is_ready(0),
|
||||
"Switch context was not ready after initialization");
|
||||
tud_mount_cb();
|
||||
expect(!usb_output_driver_is_ready(0),
|
||||
"Switch mount did not reset handshake readiness");
|
||||
expect(!usb_output_driver_task(0) && hid_report_count == 1,
|
||||
"Switch startup identify was not dispatched");
|
||||
tud_umount_cb();
|
||||
expect(!usb_output_driver_is_ready(0),
|
||||
"Switch unmount did not reset handshake readiness");
|
||||
expect(!usb_output_driver_task(kInvalidInstance),
|
||||
"Switch task accepted an invalid instance");
|
||||
|
||||
const uint16_t* os_string = tud_descriptor_string_cb(0xee, 0x0409);
|
||||
expect(os_string != nullptr && os_string[0] == 0x0312 &&
|
||||
os_string[1] == 'M' && os_string[2] == 'S' &&
|
||||
os_string[3] == 'F' && os_string[4] == 'T' &&
|
||||
os_string[5] == '1' && os_string[6] == '0' &&
|
||||
os_string[7] == '0' &&
|
||||
os_string[8] == XInput::kMsVendorRequest,
|
||||
"Microsoft OS string changed at the boundary");
|
||||
expect(observed_string_count == 1 && last_observed_string == 0xee,
|
||||
"host probe did not observe the Microsoft OS string");
|
||||
expect(!tud_control_request_cb(0, nullptr),
|
||||
"generic control routing claimed an unhandled request");
|
||||
}
|
||||
|
||||
void open_xinput_interfaces(usbd_class_driver_t const* driver) {
|
||||
endpoint_harness = {};
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
auto const* interface_descriptor =
|
||||
reinterpret_cast<tusb_desc_interface_t const*>(
|
||||
&XInput::kConfigurationDescriptor[
|
||||
9 + instance * XInput::kInterfaceDescriptorSize]);
|
||||
expect(driver->open(0, interface_descriptor,
|
||||
XInput::kInterfaceDescriptorSize) ==
|
||||
XInput::kInterfaceDescriptorSize,
|
||||
"XInput interface did not open");
|
||||
expect(endpoint_harness[static_cast<uint8_t>(0x81 + instance)]
|
||||
.opened &&
|
||||
endpoint_harness[static_cast<uint8_t>(0x01 + instance)]
|
||||
.opened,
|
||||
"XInput interface endpoints were not opened");
|
||||
}
|
||||
}
|
||||
|
||||
void test_xinput_boundary_dispatch() {
|
||||
reset_usb_harness();
|
||||
usb_output_driver_init(AdapterUsbMode::kXInput);
|
||||
|
||||
expect(usb_output_driver_mode() == AdapterUsbMode::kXInput &&
|
||||
std::strcmp(usb_output_driver_name(), "XINPUT") == 0 &&
|
||||
std::strcmp(usb_output_driver_mode_name(), "XInput") == 0,
|
||||
"XInput boundary mode was not frozen");
|
||||
expect(std::memcmp(tud_descriptor_device_cb(),
|
||||
XInput::kDeviceDescriptor,
|
||||
sizeof(XInput::kDeviceDescriptor)) == 0,
|
||||
"XInput device descriptor changed at the boundary");
|
||||
expect(std::memcmp(tud_descriptor_configuration_cb(0),
|
||||
XInput::kConfigurationDescriptor,
|
||||
sizeof(XInput::kConfigurationDescriptor)) == 0,
|
||||
"XInput configuration descriptor changed at the boundary");
|
||||
expect(tud_hid_descriptor_report_cb(0) == nullptr,
|
||||
"inactive HID class claimed an XInput report descriptor");
|
||||
std::array<uint8_t, SWITCH_PRO_ENDPOINT_SIZE> hid_report{};
|
||||
expect(tud_hid_get_report_cb(0, 0, HID_REPORT_TYPE_INPUT,
|
||||
hid_report.data(), hid_report.size()) == 0,
|
||||
"inactive HID GET_REPORT handled XInput mode");
|
||||
|
||||
uint8_t driver_count = 0;
|
||||
usbd_class_driver_t const* driver =
|
||||
usbd_app_driver_get_cb(&driver_count);
|
||||
expect(driver == xinput_class_driver() && driver_count == 1,
|
||||
"XInput custom class was not selected");
|
||||
expect(driver != nullptr && std::strcmp(driver->name, "XINPUT") == 0,
|
||||
"XInput custom class retained a feasibility name");
|
||||
if (driver == nullptr) {
|
||||
return;
|
||||
}
|
||||
driver->init();
|
||||
open_xinput_interfaces(driver);
|
||||
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
usb_output_driver_set_rumble_callback(
|
||||
instance, xinput_rumble_callback);
|
||||
ControllerState state{};
|
||||
state.button_south = (instance & 1u) == 0;
|
||||
state.button_north = (instance & 1u) != 0;
|
||||
state.left_trigger =
|
||||
static_cast<uint16_t>(0x1000u * (instance + 1u));
|
||||
state.right_trigger =
|
||||
static_cast<uint16_t>(0x0800u * (instance + 1u));
|
||||
state.left_stick_x = static_cast<int16_t>(100 + instance);
|
||||
state.right_stick_y = static_cast<int16_t>(-200 - instance);
|
||||
usb_output_driver_set_input(instance, state, 1, 2);
|
||||
expect(usb_output_driver_is_ready(instance),
|
||||
"configured XInput instance was not ready");
|
||||
expect(usb_output_driver_task(instance),
|
||||
"XInput input report was not dispatched");
|
||||
|
||||
const EndpointHarness& input_endpoint =
|
||||
endpoint_harness[static_cast<uint8_t>(0x81 + instance)];
|
||||
const XInput::InputReport expected =
|
||||
XInput::build_input_report(state);
|
||||
expect(input_endpoint.last_transfer_length == sizeof(expected) &&
|
||||
std::memcmp(input_endpoint.last_transfer.data(),
|
||||
&expected, sizeof(expected)) == 0,
|
||||
"XInput boundary changed an input report");
|
||||
}
|
||||
expect(!usb_output_driver_task(kInvalidInstance),
|
||||
"XInput task accepted an invalid instance");
|
||||
|
||||
switch_pro_set_rumble_callback(0,
|
||||
inactive_switch_rumble_callback);
|
||||
std::array<uint8_t, 10> switch_output{};
|
||||
switch_output[0] = REPORT_OUTPUT_10;
|
||||
tud_hid_report_received_cb(0, 0, switch_output.data(),
|
||||
switch_output.size());
|
||||
tud_hid_set_report_cb(0, REPORT_OUTPUT_10, HID_REPORT_TYPE_OUTPUT,
|
||||
switch_output.data() + 1,
|
||||
switch_output.size() - 1);
|
||||
expect(switch_inactive_rumble_count == 0,
|
||||
"inactive Switch HID callbacks handled XInput output");
|
||||
|
||||
tud_mount_cb();
|
||||
tud_umount_cb();
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
expect(usb_output_driver_is_ready(instance),
|
||||
"generic mount callback reset the active XInput class");
|
||||
}
|
||||
|
||||
driver->reset(0);
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
expect(!usb_output_driver_is_ready(instance),
|
||||
"XInput bus reset retained configured state");
|
||||
}
|
||||
open_xinput_interfaces(driver);
|
||||
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
EndpointHarness& output_endpoint =
|
||||
endpoint_harness[static_cast<uint8_t>(0x01 + instance)];
|
||||
expect(output_endpoint.armed_buffer != nullptr &&
|
||||
output_endpoint.armed_length == 32,
|
||||
"XInput output endpoint was not armed");
|
||||
if (output_endpoint.armed_buffer == nullptr) {
|
||||
continue;
|
||||
}
|
||||
std::memset(output_endpoint.armed_buffer, 0,
|
||||
output_endpoint.armed_length);
|
||||
output_endpoint.armed_buffer[0] = 0x00;
|
||||
output_endpoint.armed_buffer[1] = 0x08;
|
||||
output_endpoint.armed_buffer[3] =
|
||||
static_cast<uint8_t>(0x20 + instance);
|
||||
output_endpoint.armed_buffer[4] =
|
||||
static_cast<uint8_t>(0x40 + instance);
|
||||
expect(driver->xfer_cb(
|
||||
0, static_cast<uint8_t>(0x01 + instance),
|
||||
XFER_RESULT_SUCCESS, 8),
|
||||
"XInput output transfer was not rearmed");
|
||||
}
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
expect(xinput_rumble_events[instance].count == 1 &&
|
||||
xinput_rumble_events[instance]
|
||||
.output.low_frequency_magnitude ==
|
||||
static_cast<uint8_t>(0x20 + instance) &&
|
||||
xinput_rumble_events[instance]
|
||||
.output.high_frequency_magnitude ==
|
||||
static_cast<uint8_t>(0x40 + instance),
|
||||
"XInput rumble crossed instance boundaries");
|
||||
}
|
||||
|
||||
expect_usb_string(1, "Switch Pico",
|
||||
"XInput manufacturer string changed");
|
||||
expect_usb_string(2, "XInput Feasibility",
|
||||
"XInput development product string changed");
|
||||
expect_usb_string(3, "XINPUT-PROTOTYPE",
|
||||
"XInput development serial string changed");
|
||||
}
|
||||
|
||||
void test_vendor_control_boundary() {
|
||||
constexpr uint8_t kSetupStage = 0;
|
||||
constexpr uint8_t kRhport = 2;
|
||||
tusb_control_request_t request{};
|
||||
request.bRequest = 0x42;
|
||||
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
management_vendor_control_result = true;
|
||||
management_vendor_control_count = 0;
|
||||
expect(tud_vendor_control_xfer_cb(kRhport, kSetupStage, &request),
|
||||
"BLUEPAD32 vendor control was not forwarded");
|
||||
expect(management_vendor_control_count == 1 &&
|
||||
management_vendor_control_rhport == kRhport &&
|
||||
management_vendor_control_stage == kSetupStage &&
|
||||
management_vendor_control_request == &request,
|
||||
"BLUEPAD32 vendor control forwarding changed its arguments");
|
||||
|
||||
management_vendor_control_result = false;
|
||||
expect(!tud_vendor_control_xfer_cb(kRhport, kSetupStage, &request) &&
|
||||
management_vendor_control_count == 2,
|
||||
"inactive BLUEPAD32 vendor control was claimed");
|
||||
#else
|
||||
expect(!tud_vendor_control_xfer_cb(kRhport, kSetupStage, &request),
|
||||
"UART vendor control was claimed");
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" absolute_time_t get_absolute_time(void) {
|
||||
return {now_ms};
|
||||
}
|
||||
|
||||
extern "C" uint32_t to_ms_since_boot(absolute_time_t time) {
|
||||
return static_cast<uint32_t>(time.milliseconds);
|
||||
}
|
||||
|
||||
extern "C" uint32_t get_rand_32(void) {
|
||||
return random_value++;
|
||||
}
|
||||
|
||||
extern "C" bool tud_hid_n_ready(uint8_t instance) {
|
||||
return instance < kInstanceCount;
|
||||
}
|
||||
|
||||
extern "C" bool tud_hid_n_report(uint8_t instance, uint8_t report_id,
|
||||
const void* report, uint16_t length) {
|
||||
(void)report_id;
|
||||
if (instance >= kInstanceCount || report == nullptr || length == 0) {
|
||||
return false;
|
||||
}
|
||||
++hid_report_count;
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C" bool tud_suspended(void) {
|
||||
return false;
|
||||
}
|
||||
|
||||
extern "C" bool tud_remote_wakeup(void) {
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C" bool tud_ready(void) {
|
||||
return usb_ready;
|
||||
}
|
||||
|
||||
extern "C" bool usbd_edpt_open(
|
||||
uint8_t rhport, tusb_desc_endpoint_t const* endpoint_descriptor) {
|
||||
(void)rhport;
|
||||
if (endpoint_descriptor == nullptr) {
|
||||
return false;
|
||||
}
|
||||
endpoint_harness[endpoint_descriptor->bEndpointAddress].opened = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C" bool usbd_edpt_xfer(uint8_t rhport, uint8_t endpoint,
|
||||
uint8_t* buffer, uint16_t total_bytes) {
|
||||
(void)rhport;
|
||||
if (buffer == nullptr) {
|
||||
return false;
|
||||
}
|
||||
EndpointHarness& harness = endpoint_harness[endpoint];
|
||||
++harness.transfer_count;
|
||||
harness.claimed = false;
|
||||
if (tu_edpt_dir(endpoint) == TUSB_DIR_IN) {
|
||||
harness.last_transfer_length =
|
||||
total_bytes < harness.last_transfer.size()
|
||||
? total_bytes
|
||||
: static_cast<uint16_t>(harness.last_transfer.size());
|
||||
std::memcpy(harness.last_transfer.data(), buffer,
|
||||
harness.last_transfer_length);
|
||||
} else {
|
||||
harness.armed_buffer = buffer;
|
||||
harness.armed_length = total_bytes;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C" bool usbd_edpt_busy(uint8_t rhport, uint8_t endpoint) {
|
||||
(void)rhport;
|
||||
return endpoint_harness[endpoint].busy;
|
||||
}
|
||||
|
||||
extern "C" bool usbd_edpt_claim(uint8_t rhport, uint8_t endpoint) {
|
||||
(void)rhport;
|
||||
EndpointHarness& harness = endpoint_harness[endpoint];
|
||||
if (harness.claimed) {
|
||||
return false;
|
||||
}
|
||||
harness.claimed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C" bool usbd_edpt_release(uint8_t rhport, uint8_t endpoint) {
|
||||
(void)rhport;
|
||||
endpoint_harness[endpoint].claimed = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void adapter_host_probe_note_string_descriptor(uint8_t index) {
|
||||
++observed_string_count;
|
||||
last_observed_string = index;
|
||||
}
|
||||
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
bool usb_configuration_management_vendor_control(
|
||||
uint8_t rhport, uint8_t stage,
|
||||
tusb_control_request_t const* request) {
|
||||
++management_vendor_control_count;
|
||||
management_vendor_control_rhport = rhport;
|
||||
management_vendor_control_stage = stage;
|
||||
management_vendor_control_request = request;
|
||||
return management_vendor_control_result;
|
||||
}
|
||||
#endif
|
||||
|
||||
int main() {
|
||||
test_device_and_configuration_descriptors();
|
||||
test_microsoft_compatible_id_descriptor();
|
||||
test_input_report_mapping();
|
||||
test_rumble_report();
|
||||
test_host_probe_sequence();
|
||||
test_switch_boundary_dispatch();
|
||||
test_xinput_boundary_dispatch();
|
||||
test_vendor_control_boundary();
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
#include "adapter_host_probe_state.h"
|
||||
#include "xinput_feasibility_descriptors.h"
|
||||
#include "xinput_feasibility_protocol.h"
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
|
||||
void expect(bool condition, const char *message) {
|
||||
if (!condition) {
|
||||
std::cerr << "FAIL: " << message << '\n';
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t read_le16(const uint8_t *data) {
|
||||
return static_cast<uint16_t>(data[0] | (data[1] << 8));
|
||||
}
|
||||
|
||||
uint32_t read_le32(const uint8_t *data) {
|
||||
return static_cast<uint32_t>(data[0]) |
|
||||
(static_cast<uint32_t>(data[1]) << 8) |
|
||||
(static_cast<uint32_t>(data[2]) << 16) |
|
||||
(static_cast<uint32_t>(data[3]) << 24);
|
||||
}
|
||||
|
||||
void test_device_and_configuration_descriptors() {
|
||||
using namespace XInputFeasibility;
|
||||
expect(read_le16(&kSwitchProbeDeviceDescriptor[8]) ==
|
||||
kSwitchProbeVendorId,
|
||||
"Switch probe VID mismatch");
|
||||
expect(read_le16(&kSwitchProbeDeviceDescriptor[10]) ==
|
||||
kSwitchProbeProductId,
|
||||
"Switch probe PID mismatch");
|
||||
expect(read_le16(&kSwitchProbeDeviceDescriptor[12]) ==
|
||||
kSwitchProbeDeviceRevision,
|
||||
"Switch probe revision mismatch");
|
||||
expect(kSwitchProbeDeviceRevision != 0x0210,
|
||||
"Switch probe reuses the genuine controller cache identity");
|
||||
expect(read_le16(&kDeviceDescriptor[8]) == kPrototypeVendorId,
|
||||
"prototype VID mismatch");
|
||||
expect(read_le16(&kDeviceDescriptor[10]) == kPrototypeProductId,
|
||||
"prototype PID mismatch");
|
||||
expect(read_le16(&kDeviceDescriptor[12]) ==
|
||||
kPrototypeDeviceRevision,
|
||||
"prototype revision mismatch");
|
||||
expect(kDeviceDescriptor[4] == 0 && kDeviceDescriptor[5] == 0 &&
|
||||
kDeviceDescriptor[6] == 0,
|
||||
"multi-interface prototype is not a composite USB device");
|
||||
expect(kPrototypeVendorId != 0x045e,
|
||||
"prototype must not impersonate Microsoft's VID");
|
||||
expect(read_le16(&kConfigurationDescriptor[2]) ==
|
||||
sizeof(kConfigurationDescriptor),
|
||||
"configuration total length mismatch");
|
||||
expect(kConfigurationDescriptor[4] == SWITCH_PICO_HID_INSTANCE_COUNT,
|
||||
"configuration interface count mismatch");
|
||||
|
||||
std::array<bool, 16> endpoints{};
|
||||
for (uint8_t instance = 0; instance < SWITCH_PICO_HID_INSTANCE_COUNT;
|
||||
++instance) {
|
||||
const size_t offset = 9 + instance * kInterfaceDescriptorSize;
|
||||
const uint8_t *interface = &kConfigurationDescriptor[offset];
|
||||
expect(interface[0] == 9 && interface[1] == 4,
|
||||
"missing interface descriptor");
|
||||
expect(interface[2] == instance, "interface number mismatch");
|
||||
expect(interface[5] == 0xff && interface[6] == 0x5d &&
|
||||
interface[7] == 0x01,
|
||||
"XInput interface class tuple mismatch");
|
||||
expect(interface[9] == 0x10 && interface[10] == 0x21,
|
||||
"XInput capability descriptor missing");
|
||||
const uint8_t in_endpoint = interface[27];
|
||||
const uint8_t out_endpoint = interface[34];
|
||||
expect(in_endpoint == static_cast<uint8_t>(0x81 + instance),
|
||||
"input endpoint mismatch");
|
||||
expect(out_endpoint == static_cast<uint8_t>(0x01 + instance),
|
||||
"output endpoint mismatch");
|
||||
expect(interface[15] == in_endpoint && interface[21] == out_endpoint,
|
||||
"capability descriptor endpoint mismatch");
|
||||
expect(!endpoints[in_endpoint & 0x0f] &&
|
||||
!endpoints[out_endpoint & 0x0f],
|
||||
"endpoint number reused");
|
||||
endpoints[in_endpoint & 0x0f] = true;
|
||||
}
|
||||
}
|
||||
|
||||
void test_microsoft_compatible_id_descriptor() {
|
||||
using namespace XInputFeasibility;
|
||||
expect(read_le32(kMsCompatIdDescriptor) == sizeof(kMsCompatIdDescriptor),
|
||||
"Microsoft descriptor total length mismatch");
|
||||
expect(read_le16(&kMsCompatIdDescriptor[4]) == 0x0100,
|
||||
"Microsoft descriptor version mismatch");
|
||||
expect(read_le16(&kMsCompatIdDescriptor[6]) == kMsCompatIdIndex,
|
||||
"Microsoft descriptor index mismatch");
|
||||
expect(kMsCompatIdDescriptor[8] == SWITCH_PICO_HID_INSTANCE_COUNT,
|
||||
"Microsoft function count mismatch");
|
||||
for (uint8_t instance = 0;
|
||||
instance < SWITCH_PICO_HID_INSTANCE_COUNT; ++instance) {
|
||||
const uint8_t *function = &kMsCompatIdDescriptor[16 + instance * 24];
|
||||
expect(function[0] == instance,
|
||||
"Microsoft descriptor interface mismatch");
|
||||
expect(std::memcmp(&function[2], "XUSB10", 6) == 0,
|
||||
"XUSB10 compatible ID missing");
|
||||
}
|
||||
expect(read_le32(kProbeMsCompatIdDescriptor) == 16 &&
|
||||
kProbeMsCompatIdDescriptor[8] == 0,
|
||||
"probe descriptor must expose no compatible functions");
|
||||
}
|
||||
|
||||
void test_input_report_mapping() {
|
||||
ControllerState state{};
|
||||
auto report = XInputFeasibility::build_input_report(state);
|
||||
expect(report.report_id == 0 && report.report_size == 20,
|
||||
"neutral report header mismatch");
|
||||
expect(report.buttons == 0 && report.left_trigger == 0 &&
|
||||
report.right_trigger == 0,
|
||||
"neutral report controls mismatch");
|
||||
expect(report.left_x == 0 && report.left_y == 0 && report.right_x == 0 &&
|
||||
report.right_y == 0,
|
||||
"neutral axes mismatch");
|
||||
|
||||
state.dpad_up = true;
|
||||
state.button_south = true;
|
||||
state.button_east = true;
|
||||
state.button_west = true;
|
||||
state.button_north = true;
|
||||
state.button_start = true;
|
||||
state.button_select = true;
|
||||
state.button_system = true;
|
||||
state.left_trigger = UINT16_MAX;
|
||||
state.right_trigger = UINT16_MAX;
|
||||
state.left_stick_x = INT16_MIN;
|
||||
state.left_stick_y = INT16_MIN;
|
||||
state.right_stick_x = INT16_MAX;
|
||||
state.right_stick_y = INT16_MAX;
|
||||
report = XInputFeasibility::build_input_report(state);
|
||||
expect((report.buttons & XInputFeasibility::kDpadUp) != 0,
|
||||
"D-pad mapping missing");
|
||||
expect((report.buttons & XInputFeasibility::kButtonA) != 0 &&
|
||||
(report.buttons & XInputFeasibility::kButtonB) != 0 &&
|
||||
(report.buttons & XInputFeasibility::kButtonX) != 0 &&
|
||||
(report.buttons & XInputFeasibility::kButtonY) != 0,
|
||||
"positional face-button mapping mismatch");
|
||||
expect(report.left_trigger == 0xff && report.right_trigger == 0xff,
|
||||
"full analog trigger mapping mismatch");
|
||||
expect(report.left_x == INT16_MIN && report.left_y == INT16_MAX &&
|
||||
report.right_x == INT16_MAX &&
|
||||
report.right_y == -INT16_MAX,
|
||||
"axis endpoint mapping mismatch");
|
||||
|
||||
state.left_trigger = 0x8000;
|
||||
state.right_trigger = 0x7fff;
|
||||
report = XInputFeasibility::build_input_report(state);
|
||||
expect(report.left_trigger == 0x80 && report.right_trigger == 0x7f,
|
||||
"analog trigger precision was discarded");
|
||||
}
|
||||
|
||||
void test_rumble_report() {
|
||||
const uint8_t packet[8] = {0x00, 0x08, 0x00, 0xa5, 0x5a, 0x00, 0x00, 0x00};
|
||||
ControllerRumbleOutput output{};
|
||||
expect(
|
||||
XInputFeasibility::parse_rumble_report(packet, sizeof(packet), &output),
|
||||
"valid rumble report rejected");
|
||||
expect(output.low_frequency_magnitude == 0xa5 &&
|
||||
output.high_frequency_magnitude == 0x5a,
|
||||
"rumble magnitudes mapped incorrectly");
|
||||
expect(!XInputFeasibility::parse_rumble_report(packet, 4, &output),
|
||||
"truncated rumble report accepted");
|
||||
uint8_t wrong_type[8]{};
|
||||
expect(!XInputFeasibility::parse_rumble_report(wrong_type,
|
||||
sizeof(wrong_type), &output),
|
||||
"wrong rumble report type accepted");
|
||||
}
|
||||
|
||||
void test_host_probe_sequence() {
|
||||
AdapterHostProbeState state;
|
||||
state.note_ms_compat_id_request(10);
|
||||
expect(!state.windows_confirmed(),
|
||||
"compatible-ID request without signature confirmed Windows");
|
||||
state.note_ms_os_string();
|
||||
expect(state.saw_ms_os_string(),
|
||||
"Microsoft OS string observation was not retained");
|
||||
state.note_ms_compat_id_request(20);
|
||||
expect(state.windows_confirmed(),
|
||||
"two-stage Windows signature not confirmed");
|
||||
expect(!state.should_reboot(119), "probe rebooted before delay");
|
||||
expect(state.should_reboot(120), "probe did not reboot at deadline");
|
||||
|
||||
AdapterHostProbeState wrapped;
|
||||
wrapped.note_ms_os_string();
|
||||
wrapped.note_ms_compat_id_request(UINT32_MAX - 50);
|
||||
expect(!wrapped.should_reboot(48),
|
||||
"wrapped timer rebooted before deadline");
|
||||
expect(wrapped.should_reboot(49), "wrapped timer missed deadline");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
test_device_and_configuration_descriptors();
|
||||
test_microsoft_compatible_id_descriptor();
|
||||
test_input_report_mapping();
|
||||
test_rumble_report();
|
||||
test_host_probe_sequence();
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
|
|
@ -479,7 +479,7 @@ bool process_out_request() {
|
|||
|
||||
} // namespace
|
||||
|
||||
extern "C" bool tud_vendor_control_xfer_cb(
|
||||
bool usb_configuration_management_vendor_control(
|
||||
uint8_t rhport, uint8_t stage,
|
||||
tusb_control_request_t const* request) {
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "tusb.h"
|
||||
|
||||
#include "bluepad32_input_backend.h"
|
||||
#include "configuration_service.h"
|
||||
#include "profile_service.h"
|
||||
|
|
@ -83,3 +85,7 @@ size_t encode_profile_transaction(
|
|||
uint8_t* output, size_t output_size);
|
||||
|
||||
} // namespace UsbConfigurationManagement
|
||||
|
||||
bool usb_configuration_management_vendor_control(
|
||||
uint8_t rhport, uint8_t stage,
|
||||
tusb_control_request_t const* request);
|
||||
|
|
|
|||
290
usb_output_driver.cpp
Normal file
290
usb_output_driver.cpp
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
#include "usb_output_driver.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "device/usbd_pvt.h"
|
||||
#include "switch_pro_driver.h"
|
||||
#include "switch_pro_descriptors.h"
|
||||
#include "tusb.h"
|
||||
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
#include "usb_configuration_management.h"
|
||||
#endif
|
||||
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
#include "adapter_host_probe.h"
|
||||
#include "xinput_descriptors.h"
|
||||
#include "xinput_driver.h"
|
||||
#endif
|
||||
|
||||
#ifdef SWITCH_PICO_LOG
|
||||
#define LOG_PRINTF(...) printf(__VA_ARGS__)
|
||||
#else
|
||||
#define LOG_PRINTF(...) ((void)0)
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
AdapterUsbMode g_mode = AdapterUsbMode::kSwitchProbe;
|
||||
uint16_t g_string_descriptor[32]{};
|
||||
|
||||
bool xinput_selected() {
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
return g_mode == AdapterUsbMode::kXInput;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void usb_output_driver_init(AdapterUsbMode mode) {
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
g_mode = mode;
|
||||
#else
|
||||
(void)mode;
|
||||
g_mode = AdapterUsbMode::kSwitchProbe;
|
||||
#endif
|
||||
|
||||
for (uint8_t instance = 0;
|
||||
instance < SWITCH_PICO_HID_INSTANCE_COUNT; ++instance) {
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
if (xinput_selected()) {
|
||||
xinput_init(instance);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
switch_pro_init(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char* usb_output_driver_mode_name() {
|
||||
return xinput_selected() ? "XInput" : "Switch probe";
|
||||
}
|
||||
|
||||
AdapterUsbMode usb_output_driver_mode() {
|
||||
return g_mode;
|
||||
}
|
||||
|
||||
const char* usb_output_driver_name() {
|
||||
return xinput_selected() ? "XINPUT" : "SWITCH";
|
||||
}
|
||||
|
||||
void usb_output_driver_set_input(uint8_t instance,
|
||||
const ControllerState& state,
|
||||
uint16_t left_trigger_threshold,
|
||||
uint16_t right_trigger_threshold) {
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
if (xinput_selected()) {
|
||||
xinput_set_input(instance, state);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
switch_pro_set_input(instance, state, left_trigger_threshold,
|
||||
right_trigger_threshold);
|
||||
}
|
||||
|
||||
bool usb_output_driver_task(uint8_t instance) {
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
if (xinput_selected()) {
|
||||
return xinput_task(instance);
|
||||
}
|
||||
#endif
|
||||
return switch_pro_task(instance);
|
||||
}
|
||||
|
||||
bool usb_output_driver_is_ready(uint8_t instance) {
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
if (xinput_selected()) {
|
||||
return xinput_is_ready(instance);
|
||||
}
|
||||
#endif
|
||||
return switch_pro_is_ready(instance);
|
||||
}
|
||||
|
||||
void usb_output_driver_set_rumble_callback(
|
||||
uint8_t instance, ControllerRumbleCallback callback) {
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
if (xinput_selected()) {
|
||||
xinput_set_rumble_callback(instance, callback);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
switch_pro_set_rumble_callback(instance, 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) {
|
||||
if (xinput_selected()) {
|
||||
return 0;
|
||||
}
|
||||
return switch_pro_hid_get_report(instance, report_id, report_type, buffer,
|
||||
requested_length);
|
||||
}
|
||||
|
||||
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()) {
|
||||
switch_pro_hid_set_report(instance, report_id, report_type, buffer,
|
||||
buffer_size);
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
switch_pro_hid_report_received(instance, report_id, buffer,
|
||||
buffer_size);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" uint8_t const* tud_hid_descriptor_report_cb(uint8_t instance) {
|
||||
if (xinput_selected()) {
|
||||
return nullptr;
|
||||
}
|
||||
return switch_pro_hid_report_descriptor(instance);
|
||||
}
|
||||
|
||||
extern "C" uint8_t const* tud_descriptor_device_cb() {
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
if (xinput_selected()) {
|
||||
return XInput::kDeviceDescriptor;
|
||||
}
|
||||
return XInput::kSwitchProbeDeviceDescriptor;
|
||||
#else
|
||||
return switch_pro_device_descriptor;
|
||||
#endif
|
||||
}
|
||||
|
||||
extern "C" uint8_t const* tud_descriptor_configuration_cb(uint8_t index) {
|
||||
(void)index;
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
if (xinput_selected()) {
|
||||
return XInput::kConfigurationDescriptor;
|
||||
}
|
||||
#endif
|
||||
return switch_pro_configuration_descriptor;
|
||||
}
|
||||
|
||||
extern "C" uint16_t const* tud_descriptor_string_cb(uint8_t index,
|
||||
uint16_t langid) {
|
||||
(void)langid;
|
||||
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
adapter_host_probe_note_string_descriptor(index);
|
||||
if (index == 0xee) {
|
||||
static constexpr char kSignature[] = "MSFT100";
|
||||
for (uint8_t i = 0; i < sizeof(kSignature) - 1; ++i) {
|
||||
g_string_descriptor[1 + i] = kSignature[i];
|
||||
}
|
||||
g_string_descriptor[8] = XInput::kMsVendorRequest;
|
||||
g_string_descriptor[0] =
|
||||
static_cast<uint16_t>((TUSB_DESC_STRING << 8) | 18);
|
||||
return g_string_descriptor;
|
||||
}
|
||||
#endif
|
||||
|
||||
uint8_t character_count = 0;
|
||||
if (index == 0) {
|
||||
memcpy(&g_string_descriptor[1], switch_pro_string_language, 2);
|
||||
character_count = 1;
|
||||
} else {
|
||||
const uint8_t* string = nullptr;
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
static const uint8_t kManufacturer[] = "Switch Pico";
|
||||
static const uint8_t kProduct[] = "XInput Feasibility";
|
||||
static const uint8_t kSerial[] = "XINPUT-PROTOTYPE";
|
||||
static const uint8_t* const kXInputStrings[] = {
|
||||
nullptr, kManufacturer, kProduct, kSerial};
|
||||
if (xinput_selected()) {
|
||||
if (index >= sizeof(kXInputStrings) /
|
||||
sizeof(kXInputStrings[0])) {
|
||||
return nullptr;
|
||||
}
|
||||
string = kXInputStrings[index];
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
if (index >= sizeof(switch_pro_string_descriptors) /
|
||||
sizeof(switch_pro_string_descriptors[0])) {
|
||||
return nullptr;
|
||||
}
|
||||
string = switch_pro_string_descriptors[index];
|
||||
}
|
||||
|
||||
while (string[character_count] != 0) {
|
||||
++character_count;
|
||||
}
|
||||
if (character_count > 31) {
|
||||
character_count = 31;
|
||||
}
|
||||
for (uint8_t i = 0; i < character_count; ++i) {
|
||||
g_string_descriptor[1 + i] = string[i];
|
||||
}
|
||||
}
|
||||
|
||||
g_string_descriptor[0] = static_cast<uint16_t>(
|
||||
(TUSB_DESC_STRING << 8) | (2 * character_count + 2));
|
||||
return g_string_descriptor;
|
||||
}
|
||||
|
||||
extern "C" bool tud_vendor_control_xfer_cb(
|
||||
uint8_t rhport, uint8_t stage,
|
||||
tusb_control_request_t const* request) {
|
||||
#ifdef SWITCH_PICO_BLUEPAD32
|
||||
return usb_configuration_management_vendor_control(rhport, stage,
|
||||
request);
|
||||
#else
|
||||
(void)rhport;
|
||||
(void)stage;
|
||||
(void)request;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
extern "C" bool tud_control_request_cb(
|
||||
uint8_t rhport, tusb_control_request_t const* request) {
|
||||
(void)rhport;
|
||||
if (request != nullptr) {
|
||||
LOG_PRINTF(
|
||||
"[CTRL] bmReq=0x%02x bReq=0x%02x wValue=0x%04x "
|
||||
"wIndex=0x%04x wLen=%u\n",
|
||||
request->bmRequestType, request->bRequest, request->wValue,
|
||||
request->wIndex, request->wLength);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
extern "C" void tud_mount_cb() {
|
||||
LOG_PRINTF("[USB] mount_cb\n");
|
||||
if (!xinput_selected()) {
|
||||
switch_pro_mount();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void tud_umount_cb() {
|
||||
LOG_PRINTF("[USB] umount_cb\n");
|
||||
if (!xinput_selected()) {
|
||||
switch_pro_unmount();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" usbd_class_driver_t const* usbd_app_driver_get_cb(
|
||||
uint8_t* driver_count) {
|
||||
if (driver_count == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
if (xinput_selected()) {
|
||||
*driver_count = 1;
|
||||
return xinput_class_driver();
|
||||
}
|
||||
#endif
|
||||
*driver_count = 0;
|
||||
return nullptr;
|
||||
}
|
||||
25
usb_output_driver.h
Normal file
25
usb_output_driver.h
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "adapter_usb_mode.h"
|
||||
#include "controller_state.h"
|
||||
#include "switch_haptics.h"
|
||||
|
||||
// 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.
|
||||
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();
|
||||
|
||||
void usb_output_driver_set_input(uint8_t instance,
|
||||
const ControllerState& state,
|
||||
uint16_t left_trigger_threshold,
|
||||
uint16_t right_trigger_threshold);
|
||||
bool usb_output_driver_task(uint8_t instance);
|
||||
bool usb_output_driver_is_ready(uint8_t instance);
|
||||
void usb_output_driver_set_rumble_callback(
|
||||
uint8_t instance, ControllerRumbleCallback callback);
|
||||
|
|
@ -11,11 +11,11 @@
|
|||
#error "SWITCH_PICO_HID_INSTANCE_COUNT must be between 1 and 4"
|
||||
#endif
|
||||
|
||||
namespace XInputFeasibility {
|
||||
namespace XInput {
|
||||
|
||||
constexpr uint16_t kPrototypeVendorId = 0xcafe;
|
||||
constexpr uint16_t kPrototypeProductId = 0x4010;
|
||||
constexpr uint16_t kPrototypeDeviceRevision = 0x0101;
|
||||
constexpr uint16_t kDevelopmentVendorId = 0xcafe;
|
||||
constexpr uint16_t kDevelopmentProductId = 0x4010;
|
||||
constexpr uint16_t kDevelopmentDeviceRevision = 0x0101;
|
||||
constexpr uint8_t kInterfaceDescriptorSize = 39;
|
||||
constexpr uint16_t kConfigurationDescriptorSize =
|
||||
9 + SWITCH_PICO_HID_INSTANCE_COUNT * kInterfaceDescriptorSize;
|
||||
|
|
@ -61,19 +61,19 @@ static const uint8_t kDeviceDescriptor[] = {
|
|||
0x00,
|
||||
0x00, // Composite device; each interface binds to the XUSB driver
|
||||
0x40, // Endpoint zero packet size
|
||||
static_cast<uint8_t>(kPrototypeVendorId & 0xff),
|
||||
static_cast<uint8_t>(kPrototypeVendorId >> 8),
|
||||
static_cast<uint8_t>(kPrototypeProductId & 0xff),
|
||||
static_cast<uint8_t>(kPrototypeProductId >> 8),
|
||||
static_cast<uint8_t>(kPrototypeDeviceRevision & 0xff),
|
||||
static_cast<uint8_t>(kPrototypeDeviceRevision >> 8),
|
||||
static_cast<uint8_t>(kDevelopmentVendorId & 0xff),
|
||||
static_cast<uint8_t>(kDevelopmentVendorId >> 8),
|
||||
static_cast<uint8_t>(kDevelopmentProductId & 0xff),
|
||||
static_cast<uint8_t>(kDevelopmentProductId >> 8),
|
||||
static_cast<uint8_t>(kDevelopmentDeviceRevision & 0xff),
|
||||
static_cast<uint8_t>(kDevelopmentDeviceRevision >> 8),
|
||||
0x01,
|
||||
0x02,
|
||||
0x03, // Manufacturer, product, serial strings
|
||||
0x01, // One configuration
|
||||
};
|
||||
|
||||
#define XINPUT_FEASIBILITY_INTERFACE(number, endpoint) \
|
||||
#define XINPUT_INTERFACE(number, endpoint) \
|
||||
0x09, 0x04, number, 0x00, 0x02, 0xff, 0x5d, 0x01, 0x00, 0x10, 0x21, 0x10, \
|
||||
0x01, 0x01, 0x24, static_cast<uint8_t>(0x80 | endpoint), 0x14, 0x03, \
|
||||
0x00, 0x03, 0x13, endpoint, 0x00, 0x03, 0x00, 0x07, 0x05, \
|
||||
|
|
@ -90,21 +90,21 @@ static const uint8_t kConfigurationDescriptor[] = {
|
|||
0x00,
|
||||
0x80,
|
||||
0xfa,
|
||||
XINPUT_FEASIBILITY_INTERFACE(0x00, 0x01),
|
||||
XINPUT_INTERFACE(0x00, 0x01),
|
||||
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 2
|
||||
XINPUT_FEASIBILITY_INTERFACE(0x01, 0x02),
|
||||
XINPUT_INTERFACE(0x01, 0x02),
|
||||
#endif
|
||||
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 3
|
||||
XINPUT_FEASIBILITY_INTERFACE(0x02, 0x03),
|
||||
XINPUT_INTERFACE(0x02, 0x03),
|
||||
#endif
|
||||
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 4
|
||||
XINPUT_FEASIBILITY_INTERFACE(0x03, 0x04),
|
||||
XINPUT_INTERFACE(0x03, 0x04),
|
||||
#endif
|
||||
};
|
||||
|
||||
#undef XINPUT_FEASIBILITY_INTERFACE
|
||||
#undef XINPUT_INTERFACE
|
||||
|
||||
#define XINPUT_FEASIBILITY_COMPAT_FUNCTION(number) \
|
||||
#define XINPUT_COMPAT_FUNCTION(number) \
|
||||
number, 0x01, 'X', 'U', 'S', 'B', '1', '0', 0x00, 0x00, 0x00, 0x00, 0x00, \
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
|
||||
|
|
@ -125,19 +125,19 @@ static const uint8_t kMsCompatIdDescriptor[] = {
|
|||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
XINPUT_FEASIBILITY_COMPAT_FUNCTION(0x00),
|
||||
XINPUT_COMPAT_FUNCTION(0x00),
|
||||
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 2
|
||||
XINPUT_FEASIBILITY_COMPAT_FUNCTION(0x01),
|
||||
XINPUT_COMPAT_FUNCTION(0x01),
|
||||
#endif
|
||||
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 3
|
||||
XINPUT_FEASIBILITY_COMPAT_FUNCTION(0x02),
|
||||
XINPUT_COMPAT_FUNCTION(0x02),
|
||||
#endif
|
||||
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 4
|
||||
XINPUT_FEASIBILITY_COMPAT_FUNCTION(0x03),
|
||||
XINPUT_COMPAT_FUNCTION(0x03),
|
||||
#endif
|
||||
};
|
||||
|
||||
#undef XINPUT_FEASIBILITY_COMPAT_FUNCTION
|
||||
#undef XINPUT_COMPAT_FUNCTION
|
||||
|
||||
static const uint8_t kProbeMsCompatIdDescriptor[] = {
|
||||
0x10, 0x00, 0x00, 0x00, 0x00, 0x01, 0x04, 0x00,
|
||||
|
|
@ -150,4 +150,4 @@ static_assert(sizeof(kConfigurationDescriptor) == kConfigurationDescriptorSize);
|
|||
static_assert(sizeof(kMsCompatIdDescriptor) == kMsCompatIdDescriptorSize);
|
||||
static_assert(sizeof(kProbeMsCompatIdDescriptor) == 16);
|
||||
|
||||
} // namespace XInputFeasibility
|
||||
} // namespace XInput
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
#include "xinput_feasibility_driver.h"
|
||||
#include "xinput_driver.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "device/usbd_pvt.h"
|
||||
#include "tusb.h"
|
||||
#include "xinput_feasibility_descriptors.h"
|
||||
#include "xinput_feasibility_protocol.h"
|
||||
#include "xinput_descriptors.h"
|
||||
#include "xinput_protocol.h"
|
||||
|
||||
namespace {
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ constexpr uint8_t kEndpointBufferSize = 32;
|
|||
|
||||
struct XInputContext {
|
||||
ControllerState input{};
|
||||
XInputFeasibility::InputReport input_report{};
|
||||
XInput::InputReport input_report{};
|
||||
uint8_t output_report[kEndpointBufferSize]{};
|
||||
ControllerRumbleCallback rumble_callback = nullptr;
|
||||
uint8_t endpoint_in = 0;
|
||||
|
|
@ -73,7 +73,7 @@ uint16_t driver_open(uint8_t rhport,
|
|||
interface_descriptor->bInterfaceProtocol != 0x01 ||
|
||||
interface_descriptor->bInterfaceNumber >=
|
||||
SWITCH_PICO_HID_INSTANCE_COUNT ||
|
||||
max_length < XInputFeasibility::kInterfaceDescriptorSize) {
|
||||
max_length < XInput::kInterfaceDescriptorSize) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -83,11 +83,11 @@ uint16_t driver_open(uint8_t rhport,
|
|||
uint16_t consumed = sizeof(tusb_desc_interface_t);
|
||||
uint8_t const *descriptor = tu_desc_next(interface_descriptor);
|
||||
uint8_t endpoints_found = 0;
|
||||
while (consumed < XInputFeasibility::kInterfaceDescriptorSize) {
|
||||
while (consumed < XInput::kInterfaceDescriptorSize) {
|
||||
const uint8_t descriptor_length = descriptor[0];
|
||||
if (descriptor_length == 0 ||
|
||||
consumed + descriptor_length >
|
||||
XInputFeasibility::kInterfaceDescriptorSize) {
|
||||
XInput::kInterfaceDescriptorSize) {
|
||||
reset_context(context);
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -139,8 +139,8 @@ bool driver_transfer(uint8_t rhport, uint8_t endpoint, xfer_result_t result,
|
|||
}
|
||||
if (endpoint == context->endpoint_out) {
|
||||
ControllerRumbleOutput rumble{};
|
||||
if (XInputFeasibility::parse_rumble_report(context->output_report,
|
||||
transferred, &rumble) &&
|
||||
if (XInput::parse_rumble_report(context->output_report, transferred,
|
||||
&rumble) &&
|
||||
context->rumble_callback != nullptr) {
|
||||
const uint8_t instance = static_cast<uint8_t>(context - g_contexts);
|
||||
context->rumble_callback(instance, rumble);
|
||||
|
|
@ -154,43 +154,41 @@ bool driver_transfer(uint8_t rhport, uint8_t endpoint, xfer_result_t result,
|
|||
}
|
||||
|
||||
usbd_class_driver_t const kDriver = {
|
||||
"XINPUT-FEASIBILITY", driver_init, driver_deinit, driver_reset,
|
||||
driver_open, driver_control, driver_transfer, nullptr,
|
||||
"XINPUT", driver_init, driver_deinit, driver_reset,
|
||||
driver_open, driver_control, driver_transfer, nullptr,
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void xinput_feasibility_init(uint8_t instance) {
|
||||
void xinput_init(uint8_t instance) {
|
||||
XInputContext *context = context_for(instance);
|
||||
if (context != nullptr) {
|
||||
reset_context(*context);
|
||||
}
|
||||
}
|
||||
|
||||
void xinput_feasibility_set_rumble_callback(uint8_t instance,
|
||||
ControllerRumbleCallback callback) {
|
||||
void xinput_set_rumble_callback(uint8_t instance,
|
||||
ControllerRumbleCallback callback) {
|
||||
XInputContext *context = context_for(instance);
|
||||
if (context != nullptr) {
|
||||
context->rumble_callback = callback;
|
||||
}
|
||||
}
|
||||
void xinput_feasibility_set_input(uint8_t instance,
|
||||
const ControllerState& state) {
|
||||
void xinput_set_input(uint8_t instance, const ControllerState& state) {
|
||||
XInputContext *context = context_for(instance);
|
||||
if (context != nullptr) {
|
||||
context->input = state;
|
||||
}
|
||||
}
|
||||
|
||||
bool xinput_feasibility_task(uint8_t instance) {
|
||||
bool xinput_task(uint8_t instance) {
|
||||
XInputContext *context = context_for(instance);
|
||||
if (context == nullptr || !context->configured || !tud_ready() ||
|
||||
usbd_edpt_busy(kRhport, context->endpoint_in)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
context->input_report =
|
||||
XInputFeasibility::build_input_report(context->input);
|
||||
context->input_report = XInput::build_input_report(context->input);
|
||||
if (!usbd_edpt_claim(kRhport, context->endpoint_in)) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -203,16 +201,11 @@ bool xinput_feasibility_task(uint8_t instance) {
|
|||
return true;
|
||||
}
|
||||
|
||||
bool xinput_feasibility_is_ready(uint8_t instance) {
|
||||
bool xinput_is_ready(uint8_t instance) {
|
||||
XInputContext *context = context_for(instance);
|
||||
return context != nullptr && context->configured && tud_ready();
|
||||
}
|
||||
|
||||
extern "C" usbd_class_driver_t const *
|
||||
usbd_app_driver_get_cb(uint8_t *driver_count) {
|
||||
if (driver_count == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
*driver_count = 1;
|
||||
usbd_class_driver_t const* xinput_class_driver() {
|
||||
return &kDriver;
|
||||
}
|
||||
16
xinput_driver.h
Normal file
16
xinput_driver.h
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "controller_state.h"
|
||||
#include "device/usbd_pvt.h"
|
||||
#include "switch_haptics.h"
|
||||
|
||||
void xinput_init(uint8_t instance);
|
||||
void xinput_set_rumble_callback(uint8_t instance,
|
||||
ControllerRumbleCallback callback);
|
||||
void xinput_set_input(uint8_t instance, const ControllerState& state);
|
||||
bool xinput_task(uint8_t instance);
|
||||
bool xinput_is_ready(uint8_t instance);
|
||||
|
||||
usbd_class_driver_t const* xinput_class_driver();
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "controller_state.h"
|
||||
#include "switch_haptics.h"
|
||||
|
||||
void xinput_feasibility_init(uint8_t instance);
|
||||
void xinput_feasibility_set_rumble_callback(uint8_t instance,
|
||||
ControllerRumbleCallback callback);
|
||||
void xinput_feasibility_set_input(uint8_t instance,
|
||||
const ControllerState& state);
|
||||
bool xinput_feasibility_task(uint8_t instance);
|
||||
bool xinput_feasibility_is_ready(uint8_t instance);
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
#include "controller_state.h"
|
||||
#include "switch_haptics.h"
|
||||
|
||||
namespace XInputFeasibility {
|
||||
namespace XInput {
|
||||
|
||||
constexpr uint16_t kDpadUp = 0x0001;
|
||||
constexpr uint16_t kDpadDown = 0x0002;
|
||||
|
|
@ -84,4 +84,4 @@ inline bool parse_rumble_report(const uint8_t *data, uint32_t size,
|
|||
return true;
|
||||
}
|
||||
|
||||
} // namespace XInputFeasibility
|
||||
} // namespace XInput
|
||||
Loading…
Add table
Add a link
Reference in a new issue