diff --git a/CMakeLists.txt b/CMakeLists.txt index 4df380a..05b9f85 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/adapter_host_probe.cpp b/adapter_host_probe.cpp index daabb4f..9464042 100644 --- a/adapter_host_probe.cpp +++ b/adapter_host_probe.cpp @@ -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( - 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(XInputFeasibility::kMsCompatIdDescriptor), - sizeof(XInputFeasibility::kMsCompatIdDescriptor)); + const_cast(XInput::kMsCompatIdDescriptor), + sizeof(XInput::kMsCompatIdDescriptor)); } diff --git a/switch-pico.cpp b/switch-pico.cpp index d315996..3114caa 100644 --- a/switch-pico.cpp +++ b/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(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(); } diff --git a/switch_pro_driver.cpp b/switch_pro_driver.cpp index fe8a1f2..590e92d 100644 --- a/switch_pro_driver.cpp +++ b/switch_pro_driver.cpp @@ -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((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((0x03 << 8) | (2 * chr_count + 2)); - return desc_str; -} diff --git a/switch_pro_driver.h b/switch_pro_driver.h index 5cc51f5..3c3561a 100644 --- a/switch_pro_driver.h +++ b/switch_pro_driver.h @@ -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); \ No newline at end of file + 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(); \ No newline at end of file diff --git a/tests/native_stubs/device/usbd_pvt.h b/tests/native_stubs/device/usbd_pvt.h new file mode 100644 index 0000000..efe47c2 --- /dev/null +++ b/tests/native_stubs/device/usbd_pvt.h @@ -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 diff --git a/tests/native_stubs/tusb.h b/tests/native_stubs/tusb.h index 0905dee..4e144e7 100644 --- a/tests/native_stubs/tusb.h +++ b/tests/native_stubs/tusb.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #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 } diff --git a/tests/switch_pro_driver_context_test.cpp b/tests/switch_pro_driver_context_test.cpp index ce7dfac..fb595ed 100644 --- a/tests/switch_pro_driver_context_test.cpp +++ b/tests/switch_pro_driver_context_test.cpp @@ -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), diff --git a/tests/test_switch_pro_driver_context_native.py b/tests/test_switch_pro_driver_context_native.py index 45102de..d3c2963 100644 --- a/tests/test_switch_pro_driver_context_native.py +++ b/tests/test_switch_pro_driver_context_native.py @@ -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", diff --git a/tests/test_usb_output_driver_native.py b/tests/test_usb_output_driver_native.py new file mode 100644 index 0000000..3199001 --- /dev/null +++ b/tests/test_usb_output_driver_native.py @@ -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) diff --git a/tests/test_xinput_feasibility_native.py b/tests/test_xinput_feasibility_native.py deleted file mode 100644 index 7a94c20..0000000 --- a/tests/test_xinput_feasibility_native.py +++ /dev/null @@ -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) diff --git a/tests/usb_configuration_management_test.cpp b/tests/usb_configuration_management_test.cpp index 27d9e2c..d46f904 100644 --- a/tests/usb_configuration_management_test.cpp +++ b/tests/usb_configuration_management_test.cpp @@ -158,10 +158,10 @@ void perform_out(UsbConfigurationManagement::Operation operation, tusb_control_request_t request = setup_request( operation, TUSB_DIR_OUT, static_cast(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(Operation::kPairingRead) && @@ -221,14 +221,14 @@ void test_vendor_requests() { request = setup_request( Operation::kPairingRefresh, TUSB_DIR_OUT, static_cast(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(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"); } diff --git a/tests/usb_management_native_stubs/tusb.h b/tests/usb_management_native_stubs/tusb.h index 2ec477e..3db2bb0 100644 --- a/tests/usb_management_native_stubs/tusb.h +++ b/tests/usb_management_native_stubs/tusb.h @@ -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 diff --git a/tests/usb_output_driver_test.cpp b/tests/usb_output_driver_test.cpp new file mode 100644 index 0000000..3efb0a5 --- /dev/null +++ b/tests/usb_output_driver_test.cpp @@ -0,0 +1,677 @@ +#include +#include +#include +#include +#include + +#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 last_transfer{}; + uint16_t last_transfer_length = 0; + unsigned transfer_count = 0; +}; + +struct RumbleEvent { + unsigned count = 0; + ControllerRumbleOutput output{}; +}; + +std::array endpoint_harness{}; +std::array 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(data[0] | (data[1] << 8)); +} + +uint32_t read_le32(const uint8_t *data) { + return static_cast(data[0]) | + (static_cast(data[1]) << 8) | + (static_cast(data[2]) << 16) | + (static_cast(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 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(0x81 + instance), + "input endpoint mismatch"); + expect(out_endpoint == static_cast(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(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 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( + &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(0x81 + instance)] + .opened && + endpoint_harness[static_cast(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 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(0x1000u * (instance + 1u)); + state.right_trigger = + static_cast(0x0800u * (instance + 1u)); + state.left_stick_x = static_cast(100 + instance); + state.right_stick_y = static_cast(-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(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 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(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(0x20 + instance); + output_endpoint.armed_buffer[4] = + static_cast(0x40 + instance); + expect(driver->xfer_cb( + 0, static_cast(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(0x20 + instance) && + xinput_rumble_events[instance] + .output.high_frequency_magnitude == + static_cast(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(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(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; +} diff --git a/tests/xinput_feasibility_test.cpp b/tests/xinput_feasibility_test.cpp deleted file mode 100644 index 41ce9da..0000000 --- a/tests/xinput_feasibility_test.cpp +++ /dev/null @@ -1,211 +0,0 @@ -#include -#include -#include -#include -#include - -#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(data[0] | (data[1] << 8)); -} - -uint32_t read_le32(const uint8_t *data) { - return static_cast(data[0]) | - (static_cast(data[1]) << 8) | - (static_cast(data[2]) << 16) | - (static_cast(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 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(0x81 + instance), - "input endpoint mismatch"); - expect(out_endpoint == static_cast(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; -} diff --git a/usb_configuration_management.cpp b/usb_configuration_management.cpp index 517651e..967e887 100644 --- a/usb_configuration_management.cpp +++ b/usb_configuration_management.cpp @@ -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 diff --git a/usb_configuration_management.h b/usb_configuration_management.h index 21e07f5..f42e4d1 100644 --- a/usb_configuration_management.h +++ b/usb_configuration_management.h @@ -3,6 +3,8 @@ #include #include +#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); diff --git a/usb_output_driver.cpp b/usb_output_driver.cpp new file mode 100644 index 0000000..f5d6018 --- /dev/null +++ b/usb_output_driver.cpp @@ -0,0 +1,290 @@ +#include "usb_output_driver.h" + +#include +#include + +#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((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( + (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; +} diff --git a/usb_output_driver.h b/usb_output_driver.h new file mode 100644 index 0000000..6b55d26 --- /dev/null +++ b/usb_output_driver.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +#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); diff --git a/xinput_feasibility_descriptors.h b/xinput_descriptors.h similarity index 78% rename from xinput_feasibility_descriptors.h rename to xinput_descriptors.h index 785c8d1..5776548 100644 --- a/xinput_feasibility_descriptors.h +++ b/xinput_descriptors.h @@ -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(kPrototypeVendorId & 0xff), - static_cast(kPrototypeVendorId >> 8), - static_cast(kPrototypeProductId & 0xff), - static_cast(kPrototypeProductId >> 8), - static_cast(kPrototypeDeviceRevision & 0xff), - static_cast(kPrototypeDeviceRevision >> 8), + static_cast(kDevelopmentVendorId & 0xff), + static_cast(kDevelopmentVendorId >> 8), + static_cast(kDevelopmentProductId & 0xff), + static_cast(kDevelopmentProductId >> 8), + static_cast(kDevelopmentDeviceRevision & 0xff), + static_cast(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(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 diff --git a/xinput_feasibility_driver.cpp b/xinput_driver.cpp similarity index 80% rename from xinput_feasibility_driver.cpp rename to xinput_driver.cpp index bc0b27e..8844ad4 100644 --- a/xinput_feasibility_driver.cpp +++ b/xinput_driver.cpp @@ -1,12 +1,12 @@ -#include "xinput_feasibility_driver.h" +#include "xinput_driver.h" #include #include #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(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; } diff --git a/xinput_driver.h b/xinput_driver.h new file mode 100644 index 0000000..a6158fb --- /dev/null +++ b/xinput_driver.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +#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(); diff --git a/xinput_feasibility_driver.h b/xinput_feasibility_driver.h deleted file mode 100644 index 33e15b8..0000000 --- a/xinput_feasibility_driver.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include - -#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); diff --git a/xinput_feasibility_protocol.h b/xinput_protocol.h similarity index 97% rename from xinput_feasibility_protocol.h rename to xinput_protocol.h index 43315ca..03ca0a9 100644 --- a/xinput_feasibility_protocol.h +++ b/xinput_protocol.h @@ -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