diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f69a35..96e1093 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,8 @@ if (EXISTS ${picoVscode}) endif() # ==================================================================================== option(SWITCH_PICO_LOG "Enable UART debug logging" OFF) +option(SWITCH_PICO_ADAPTER_FEASIBILITY + "Build the automatic Switch/XInput feasibility prototype" OFF) set(SWITCH_PICO_INPUT_BACKEND "UART" CACHE STRING "Controller input backend") set_property(CACHE SWITCH_PICO_INPUT_BACKEND PROPERTY STRINGS UART BLUEPAD32) if(NOT SWITCH_PICO_INPUT_BACKEND STREQUAL "UART" @@ -38,6 +40,11 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32" message(FATAL_ERROR "SWITCH_PICO_INPUT_BACKEND=BLUEPAD32 requires PICO_BOARD=pico2_w") endif() +if(SWITCH_PICO_ADAPTER_FEASIBILITY + AND NOT SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32") + message(FATAL_ERROR + "SWITCH_PICO_ADAPTER_FEASIBILITY requires the BLUEPAD32 backend") +endif() # Pull in Raspberry Pi Pico SDK (must be before project) include(pico_sdk_import.cmake) @@ -95,6 +102,15 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32") bootsel_pairing_button.cpp usb_pairing_management.cpp ) + if(SWITCH_PICO_ADAPTER_FEASIBILITY) + target_sources(switch-pico PRIVATE + adapter_host_probe.cpp + xinput_feasibility_driver.cpp + ) + target_compile_definitions(switch-pico PRIVATE + SWITCH_PICO_ADAPTER_FEASIBILITY=1 + ) + endif() target_compile_definitions(switch-pico PRIVATE SWITCH_PICO_BLUEPAD32=1 SWITCH_PICO_HID_INSTANCE_COUNT=4 @@ -132,6 +148,9 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32") pico_multicore pico_flash ) + if(SWITCH_PICO_ADAPTER_FEASIBILITY) + target_link_libraries(switch-pico hardware_watchdog) + endif() endif() if (SWITCH_PICO_LOG) diff --git a/adapter_host_probe.cpp b/adapter_host_probe.cpp new file mode 100644 index 0000000..08e9954 --- /dev/null +++ b/adapter_host_probe.cpp @@ -0,0 +1,87 @@ +#include "adapter_host_probe.h" + +#include +#ifdef SWITCH_PICO_LOG +#include +#define PROBE_LOG(...) printf(__VA_ARGS__) +#else +#define PROBE_LOG(...) ((void)0) +#endif + +#include "adapter_host_probe_state.h" +#include "hardware/structs/watchdog.h" +#include "hardware/watchdog.h" +#include "pico/time.h" +#include "xinput_feasibility_descriptors.h" + +namespace { + +constexpr uint32_t kXInputBootMagic = 0x58494e50; // "XINP" +constexpr uint8_t kModeScratchRegister = 0; + +AdapterUsbMode g_mode = AdapterUsbMode::kSwitchProbe; +AdapterHostProbeState g_probe; + +uint32_t now_ms() { + return static_cast(to_ms_since_boot(get_absolute_time())); +} + +} // namespace + +void adapter_host_probe_init() { + if (watchdog_hw->scratch[kModeScratchRegister] == kXInputBootMagic) { + watchdog_hw->scratch[kModeScratchRegister] = 0; + g_mode = AdapterUsbMode::kXInput; + } else { + g_mode = AdapterUsbMode::kSwitchProbe; + } + g_probe = {}; + PROBE_LOG("[HOST PROBE] boot mode=%s\n", + g_mode == AdapterUsbMode::kXInput ? "XInput" : "Switch probe"); +} + +AdapterUsbMode adapter_host_probe_mode() { return g_mode; } + +void adapter_host_probe_note_string_descriptor(uint8_t index) { + if (g_mode == AdapterUsbMode::kSwitchProbe && index == 0xee) { + g_probe.note_ms_os_string(); + PROBE_LOG("[HOST PROBE] Microsoft OS string requested\n"); + } +} + +bool adapter_host_probe_vendor_control(uint8_t rhport, uint8_t stage, + tusb_control_request_t const *request) { + if (stage != CONTROL_STAGE_SETUP || request == nullptr || + request->bmRequestType_bit.direction != TUSB_DIR_IN || + request->bmRequestType_bit.type != TUSB_REQ_TYPE_VENDOR || + request->bmRequestType_bit.recipient != TUSB_REQ_RCPT_DEVICE || + request->bRequest != XInputFeasibility::kMsVendorRequest || + request->wIndex != XInputFeasibility::kMsCompatIdIndex) { + return false; + } + + if (g_mode == AdapterUsbMode::kSwitchProbe) { + g_probe.note_ms_compat_id_request(now_ms()); + PROBE_LOG("[HOST PROBE] Microsoft compatible-ID request confirmed\n"); + return tud_control_xfer( + rhport, request, + const_cast( + XInputFeasibility::kProbeMsCompatIdDescriptor), + sizeof(XInputFeasibility::kProbeMsCompatIdDescriptor)); + } + + return tud_control_xfer( + rhport, request, + const_cast(XInputFeasibility::kMsCompatIdDescriptor), + sizeof(XInputFeasibility::kMsCompatIdDescriptor)); +} + +void adapter_host_probe_task() { + if (g_mode != AdapterUsbMode::kSwitchProbe || + !g_probe.should_reboot(now_ms())) { + return; + } + watchdog_hw->scratch[kModeScratchRegister] = kXInputBootMagic; + PROBE_LOG("[HOST PROBE] rebooting once into XInput\n"); + watchdog_reboot(0, 0, 10); +} diff --git a/adapter_host_probe.h b/adapter_host_probe.h new file mode 100644 index 0000000..797605d --- /dev/null +++ b/adapter_host_probe.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "tusb.h" + +enum class AdapterUsbMode : uint8_t { + kSwitchProbe, + kXInput, +}; + +void adapter_host_probe_init(); +AdapterUsbMode adapter_host_probe_mode(); +void adapter_host_probe_note_string_descriptor(uint8_t index); +bool adapter_host_probe_vendor_control(uint8_t rhport, uint8_t stage, + tusb_control_request_t const *request); +void adapter_host_probe_task(); diff --git a/adapter_host_probe_state.h b/adapter_host_probe_state.h new file mode 100644 index 0000000..8dc21cd --- /dev/null +++ b/adapter_host_probe_state.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +class AdapterHostProbeState { + public: + static constexpr uint32_t kRebootDelayMs = 100; + + void note_ms_os_string() { saw_ms_os_string_ = true; } + + void note_ms_compat_id_request(uint32_t now_ms) { + if (!saw_ms_os_string_) { + return; + } + confirmed_windows_ = true; + reboot_deadline_ms_ = now_ms + kRebootDelayMs; + } + + bool windows_confirmed() const { return confirmed_windows_; } + + bool should_reboot(uint32_t now_ms) const { + return confirmed_windows_ && + static_cast(now_ms - reboot_deadline_ms_) >= 0; + } + + private: + uint32_t reboot_deadline_ms_ = 0; + bool saw_ms_os_string_ = false; + bool confirmed_windows_ = false; +}; diff --git a/switch-pico.cpp b/switch-pico.cpp index 04a2dcb..6d29c5d 100644 --- a/switch-pico.cpp +++ b/switch-pico.cpp @@ -8,6 +8,10 @@ #else #include "bluepad32_input_backend.h" #include "bootsel_pairing_button.h" +#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY +#include "adapter_host_probe.h" +#include "xinput_feasibility_driver.h" +#endif #endif #ifdef SWITCH_PICO_LOG @@ -177,11 +181,26 @@ 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 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"); +#else LOG_PRINTF("[SWITCH %u] driver %s\n", instance, ready ? "ready (handshake OK)" : "not ready"); +#endif } } #else @@ -200,6 +219,9 @@ int main() { #ifdef SWITCH_PICO_BLUEPAD32 bluepad32_input_backend_init(); +#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY + adapter_host_probe_init(); +#endif #else init_uart_input(); #endif @@ -208,10 +230,27 @@ int main() { #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]); + } +#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]); +#endif } #else switch_pro_init(SWITCH_HID_INSTANCE); @@ -223,7 +262,14 @@ int main() { #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"); +#else LOG_PRINTF("[BOOT] switch-pico starting (Bluepad32 wireless @ 115200)\n"); +#endif #else LOG_PRINTF("[BOOT] switch-pico starting (UART0 log @ 115200)\n"); LOG_PRINTF("[INFO] UART1 pins TX=%d RX=%d baud=%d\n", @@ -232,6 +278,9 @@ int main() { while (true) { tud_task(); // USB device tasks +#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY + adapter_host_probe_task(); +#endif #ifdef SWITCH_PICO_BLUEPAD32 switch (bootsel_pairing_button_task()) { case BootselPairingButtonEvent::kOpenPairing: @@ -247,10 +296,25 @@ int main() { instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) { bluepad32_input_backend_snapshot(instance, &g_user_states[instance]); +#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY + bool sent = false; + if (adapter_host_probe_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]); + sent = switch_pro_task(instance); + } + if (sent) { + bluepad32_input_backend_report_sent(instance); + } +#else switch_pro_set_input(instance, g_user_states[instance]); if (switch_pro_task(instance)) { bluepad32_input_backend_report_sent(instance); } +#endif } #else bool new_data = poll_uart_frames(); // Pull controller state from UART1 diff --git a/switch_pro_driver.cpp b/switch_pro_driver.cpp index 3cb8a2f..964a682 100644 --- a/switch_pro_driver.cpp +++ b/switch_pro_driver.cpp @@ -8,6 +8,10 @@ #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__) @@ -1082,11 +1086,21 @@ uint8_t const* tud_hid_descriptor_report_cb(uint8_t instance) { } uint8_t const* tud_descriptor_device_cb(void) { +#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY + if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) { + return XInputFeasibility::kDeviceDescriptor; + } +#endif return switch_pro_device_descriptor; } 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; } @@ -1114,28 +1128,62 @@ void tud_umount_cb(void) { static uint16_t desc_str[32]; -uint16_t const * tud_descriptor_string_cb(uint8_t index, uint16_t langid) { +uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) { (void)langid; - uint8_t chr_count; +#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 - if ( index == 0 ) { + uint8_t chr_count = 0; + if (index == 0) { memcpy(&desc_str[1], switch_pro_string_language, 2); chr_count = 1; } else { - if ( index >= sizeof(switch_pro_string_descriptors)/sizeof(switch_pro_string_descriptors[0]) ) return nullptr; + 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]; + } - const uint8_t *str = switch_pro_string_descriptors[index]; - - chr_count = 0; - while ( str[chr_count] ) chr_count++; - if ( chr_count > 31 ) chr_count = 31; - - for(uint8_t i=0; i 31) { + chr_count = 31; + } + for (uint8_t i = 0; i < chr_count; ++i) { + desc_str[1 + i] = str[i]; } } - desc_str[0] = (uint16_t) ((0x03 << 8 ) | (2*chr_count + 2)); + desc_str[0] = + static_cast((0x03 << 8) | (2 * chr_count + 2)); return desc_str; } diff --git a/tests/test_xinput_feasibility_native.py b/tests/test_xinput_feasibility_native.py new file mode 100644 index 0000000..7a94c20 --- /dev/null +++ b/tests/test_xinput_feasibility_native.py @@ -0,0 +1,39 @@ +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/xinput_feasibility_test.cpp b/tests/xinput_feasibility_test.cpp new file mode 100644 index 0000000..4858d70 --- /dev/null +++ b/tests/xinput_feasibility_test.cpp @@ -0,0 +1,186 @@ +#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(&kDeviceDescriptor[8]) == kPrototypeVendorId, + "prototype VID mismatch"); + expect(read_le16(&kDeviceDescriptor[10]) == kPrototypeProductId, + "prototype PID mismatch"); + 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() { + SwitchInputState state{}; + state.lx = state.ly = state.rx = state.ry = 32768; + 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_b = true; + state.button_a = true; + state.button_y = true; + state.button_x = true; + state.button_plus = true; + state.button_minus = true; + state.button_home = true; + state.button_zl = true; + state.button_zr = true; + state.lx = 0; + state.ly = 0; + state.rx = UINT16_MAX; + state.ry = UINT16_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, + "digital 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"); +} + +void test_rumble_report() { + const uint8_t packet[8] = {0x00, 0x08, 0x00, 0xa5, 0x5a, 0x00, 0x00, 0x00}; + SwitchRumbleOutput 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(); + 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_pairing_management.cpp b/usb_pairing_management.cpp index 5360eeb..dcb48e4 100644 --- a/usb_pairing_management.cpp +++ b/usb_pairing_management.cpp @@ -3,6 +3,9 @@ #include #include "tusb.h" +#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY +#include "adapter_host_probe.h" +#endif namespace UsbPairingManagement { @@ -45,6 +48,11 @@ size_t encode_snapshot(const Bluepad32PairingSnapshot& snapshot, extern "C" bool tud_vendor_control_xfer_cb( uint8_t rhport, uint8_t stage, tusb_control_request_t const* request) { +#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY + if (adapter_host_probe_vendor_control(rhport, stage, request)) { + return true; + } +#endif if (stage != CONTROL_STAGE_SETUP) { return true; } diff --git a/xinput_feasibility_descriptors.h b/xinput_feasibility_descriptors.h new file mode 100644 index 0000000..244bbff --- /dev/null +++ b/xinput_feasibility_descriptors.h @@ -0,0 +1,123 @@ +#pragma once + +#include +#include + +#ifndef SWITCH_PICO_HID_INSTANCE_COUNT +#define SWITCH_PICO_HID_INSTANCE_COUNT 1 +#endif + +#if SWITCH_PICO_HID_INSTANCE_COUNT < 1 || SWITCH_PICO_HID_INSTANCE_COUNT > 4 +#error "SWITCH_PICO_HID_INSTANCE_COUNT must be between 1 and 4" +#endif + +namespace XInputFeasibility { + +constexpr uint16_t kPrototypeVendorId = 0xcafe; +constexpr uint16_t kPrototypeProductId = 0x4010; +constexpr uint8_t kInterfaceDescriptorSize = 39; +constexpr uint16_t kConfigurationDescriptorSize = + 9 + SWITCH_PICO_HID_INSTANCE_COUNT * kInterfaceDescriptorSize; +constexpr uint16_t kMsCompatIdDescriptorSize = + 16 + SWITCH_PICO_HID_INSTANCE_COUNT * 24; +constexpr uint8_t kMsVendorRequest = 0x20; +constexpr uint16_t kMsCompatIdIndex = 0x0004; + +static const uint8_t kDeviceDescriptor[] = { + 0x12, + 0x01, // Device descriptor + 0x00, + 0x02, // USB 2.0 + 0xff, + 0xff, + 0xff, // Vendor-specific device + 0x40, // Endpoint zero packet size + static_cast(kPrototypeVendorId & 0xff), + static_cast(kPrototypeVendorId >> 8), + static_cast(kPrototypeProductId & 0xff), + static_cast(kPrototypeProductId >> 8), + 0x00, + 0x01, // Prototype revision 1.00 + 0x01, + 0x02, + 0x03, // Manufacturer, product, serial strings + 0x01, // One configuration +}; + +#define XINPUT_FEASIBILITY_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, \ + static_cast(0x80 | endpoint), 0x03, 0x20, 0x00, 0x04, 0x07, \ + 0x05, endpoint, 0x03, 0x20, 0x00, 0x08 + +static const uint8_t kConfigurationDescriptor[] = { + 0x09, + 0x02, + static_cast(kConfigurationDescriptorSize & 0xff), + static_cast(kConfigurationDescriptorSize >> 8), + SWITCH_PICO_HID_INSTANCE_COUNT, + 0x01, + 0x00, + 0x80, + 0xfa, + XINPUT_FEASIBILITY_INTERFACE(0x00, 0x01), +#if SWITCH_PICO_HID_INSTANCE_COUNT >= 2 + XINPUT_FEASIBILITY_INTERFACE(0x01, 0x02), +#endif +#if SWITCH_PICO_HID_INSTANCE_COUNT >= 3 + XINPUT_FEASIBILITY_INTERFACE(0x02, 0x03), +#endif +#if SWITCH_PICO_HID_INSTANCE_COUNT >= 4 + XINPUT_FEASIBILITY_INTERFACE(0x03, 0x04), +#endif +}; + +#undef XINPUT_FEASIBILITY_INTERFACE + +#define XINPUT_FEASIBILITY_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 + +static const uint8_t kMsCompatIdDescriptor[] = { + static_cast(kMsCompatIdDescriptorSize & 0xff), + static_cast(kMsCompatIdDescriptorSize >> 8), + 0x00, + 0x00, + 0x00, + 0x01, // Microsoft OS descriptor version 1.0 + 0x04, + 0x00, // Extended compatible ID descriptor + SWITCH_PICO_HID_INSTANCE_COUNT, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + XINPUT_FEASIBILITY_COMPAT_FUNCTION(0x00), +#if SWITCH_PICO_HID_INSTANCE_COUNT >= 2 + XINPUT_FEASIBILITY_COMPAT_FUNCTION(0x01), +#endif +#if SWITCH_PICO_HID_INSTANCE_COUNT >= 3 + XINPUT_FEASIBILITY_COMPAT_FUNCTION(0x02), +#endif +#if SWITCH_PICO_HID_INSTANCE_COUNT >= 4 + XINPUT_FEASIBILITY_COMPAT_FUNCTION(0x03), +#endif +}; + +#undef XINPUT_FEASIBILITY_COMPAT_FUNCTION + +static const uint8_t kProbeMsCompatIdDescriptor[] = { + 0x10, 0x00, 0x00, 0x00, 0x00, 0x01, 0x04, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +static_assert(sizeof(kDeviceDescriptor) == 18); +static_assert(sizeof(kConfigurationDescriptor) == kConfigurationDescriptorSize); +static_assert(sizeof(kMsCompatIdDescriptor) == kMsCompatIdDescriptorSize); +static_assert(sizeof(kProbeMsCompatIdDescriptor) == 16); + +} // namespace XInputFeasibility diff --git a/xinput_feasibility_driver.cpp b/xinput_feasibility_driver.cpp new file mode 100644 index 0000000..2a9f485 --- /dev/null +++ b/xinput_feasibility_driver.cpp @@ -0,0 +1,219 @@ +#include "xinput_feasibility_driver.h" + +#include +#include + +#include "device/usbd_pvt.h" +#include "tusb.h" +#include "xinput_feasibility_descriptors.h" +#include "xinput_feasibility_protocol.h" + +namespace { + +constexpr uint8_t kRhport = 0; +constexpr uint8_t kEndpointBufferSize = 32; + +struct XInputContext { + SwitchInputState input{}; + XInputFeasibility::InputReport input_report{}; + uint8_t output_report[kEndpointBufferSize]{}; + SwitchRumbleCallback rumble_callback = nullptr; + uint8_t endpoint_in = 0; + uint8_t endpoint_out = 0; + bool configured = false; +}; + +XInputContext g_contexts[SWITCH_PICO_HID_INSTANCE_COUNT]{}; + +XInputContext *context_for(uint8_t instance) { + if (instance >= SWITCH_PICO_HID_INSTANCE_COUNT) { + return nullptr; + } + return &g_contexts[instance]; +} + +XInputContext *context_for_endpoint(uint8_t endpoint) { + for (XInputContext &context : g_contexts) { + if (context.endpoint_in == endpoint || + context.endpoint_out == endpoint) { + return &context; + } + } + return nullptr; +} + +void reset_context(XInputContext &context) { + const SwitchRumbleCallback callback = context.rumble_callback; + context = {}; + context.rumble_callback = callback; +} + +void driver_init() { + for (XInputContext &context : g_contexts) { + reset_context(context); + } +} + +bool driver_deinit() { + driver_init(); + return true; +} + +void driver_reset(uint8_t rhport) { + (void)rhport; + driver_init(); +} + +uint16_t driver_open(uint8_t rhport, + tusb_desc_interface_t const *interface_descriptor, + uint16_t max_length) { + if (interface_descriptor == nullptr || + interface_descriptor->bInterfaceClass != 0xff || + interface_descriptor->bInterfaceSubClass != 0x5d || + interface_descriptor->bInterfaceProtocol != 0x01 || + interface_descriptor->bInterfaceNumber >= + SWITCH_PICO_HID_INSTANCE_COUNT || + max_length < XInputFeasibility::kInterfaceDescriptorSize) { + return 0; + } + + XInputContext &context = g_contexts[interface_descriptor->bInterfaceNumber]; + reset_context(context); + + 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) { + const uint8_t descriptor_length = descriptor[0]; + if (descriptor_length == 0 || + consumed + descriptor_length > + XInputFeasibility::kInterfaceDescriptorSize) { + reset_context(context); + return 0; + } + if (tu_desc_type(descriptor) == TUSB_DESC_ENDPOINT) { + auto const *endpoint = + reinterpret_cast(descriptor); + if (!usbd_edpt_open(rhport, endpoint)) { + reset_context(context); + return 0; + } + if (tu_edpt_dir(endpoint->bEndpointAddress) == TUSB_DIR_IN) { + context.endpoint_in = endpoint->bEndpointAddress; + } else { + context.endpoint_out = endpoint->bEndpointAddress; + } + ++endpoints_found; + } + consumed = static_cast(consumed + descriptor_length); + descriptor = tu_desc_next(descriptor); + } + + if (endpoints_found != 2 || context.endpoint_in == 0 || + context.endpoint_out == 0) { + reset_context(context); + return 0; + } + context.configured = true; + if (!usbd_edpt_xfer(rhport, context.endpoint_out, context.output_report, + sizeof(context.output_report))) { + reset_context(context); + return 0; + } + return consumed; +} + +bool driver_control(uint8_t rhport, uint8_t stage, + tusb_control_request_t const *request) { + (void)rhport; + (void)stage; + (void)request; + return false; +} + +bool driver_transfer(uint8_t rhport, uint8_t endpoint, xfer_result_t result, + uint32_t transferred) { + XInputContext *context = context_for_endpoint(endpoint); + if (context == nullptr || result != XFER_RESULT_SUCCESS) { + return false; + } + if (endpoint == context->endpoint_out) { + SwitchRumbleOutput rumble{}; + if (XInputFeasibility::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); + } + memset(context->output_report, 0, sizeof(context->output_report)); + return usbd_edpt_xfer(rhport, context->endpoint_out, + context->output_report, + sizeof(context->output_report)); + } + return true; +} + +usbd_class_driver_t const kDriver = { + "XINPUT-FEASIBILITY", driver_init, driver_deinit, driver_reset, + driver_open, driver_control, driver_transfer, nullptr, +}; + +} // namespace + +void xinput_feasibility_init(uint8_t instance) { + XInputContext *context = context_for(instance); + if (context != nullptr) { + reset_context(*context); + } +} + +void xinput_feasibility_set_rumble_callback(uint8_t instance, + SwitchRumbleCallback callback) { + XInputContext *context = context_for(instance); + if (context != nullptr) { + context->rumble_callback = callback; + } +} + +void xinput_feasibility_set_input(uint8_t instance, + const SwitchInputState &state) { + XInputContext *context = context_for(instance); + if (context != nullptr) { + context->input = state; + } +} + +bool xinput_feasibility_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); + if (!usbd_edpt_claim(kRhport, context->endpoint_in)) { + return false; + } + if (!usbd_edpt_xfer(kRhport, context->endpoint_in, + reinterpret_cast(&context->input_report), + sizeof(context->input_report))) { + usbd_edpt_release(kRhport, context->endpoint_in); + return false; + } + return true; +} + +bool xinput_feasibility_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; + return &kDriver; +} diff --git a/xinput_feasibility_driver.h b/xinput_feasibility_driver.h new file mode 100644 index 0000000..b1db301 --- /dev/null +++ b/xinput_feasibility_driver.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +#include "switch_pro_driver.h" + +void xinput_feasibility_init(uint8_t instance); +void xinput_feasibility_set_rumble_callback(uint8_t instance, + SwitchRumbleCallback callback); +void xinput_feasibility_set_input(uint8_t instance, + const SwitchInputState &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_feasibility_protocol.h new file mode 100644 index 0000000..75059c6 --- /dev/null +++ b/xinput_feasibility_protocol.h @@ -0,0 +1,87 @@ +#pragma once + +#include + +#include "switch_pro_driver.h" + +namespace XInputFeasibility { + +constexpr uint16_t kDpadUp = 0x0001; +constexpr uint16_t kDpadDown = 0x0002; +constexpr uint16_t kDpadLeft = 0x0004; +constexpr uint16_t kDpadRight = 0x0008; +constexpr uint16_t kStart = 0x0010; +constexpr uint16_t kBack = 0x0020; +constexpr uint16_t kLeftThumb = 0x0040; +constexpr uint16_t kRightThumb = 0x0080; +constexpr uint16_t kLeftShoulder = 0x0100; +constexpr uint16_t kRightShoulder = 0x0200; +constexpr uint16_t kGuide = 0x0400; +constexpr uint16_t kButtonA = 0x1000; +constexpr uint16_t kButtonB = 0x2000; +constexpr uint16_t kButtonX = 0x4000; +constexpr uint16_t kButtonY = 0x8000; + +#pragma pack(push, 1) +struct InputReport { + uint8_t report_id; + uint8_t report_size; + uint16_t buttons; + uint8_t left_trigger; + uint8_t right_trigger; + int16_t left_x; + int16_t left_y; + int16_t right_x; + int16_t right_y; + uint8_t reserved[6]; +}; +#pragma pack(pop) + +static_assert(sizeof(InputReport) == 20); + +constexpr int16_t horizontal_axis(uint16_t value) { + return static_cast(static_cast(value) - 32768); +} + +constexpr int16_t vertical_axis(uint16_t value) { + const int16_t horizontal = horizontal_axis(value); + return horizontal == INT16_MIN ? INT16_MAX + : static_cast(-horizontal); +} + +inline InputReport build_input_report(const SwitchInputState &state) { + InputReport report{}; + report.report_size = sizeof(report); + report.buttons = + (state.dpad_up ? kDpadUp : 0) | (state.dpad_down ? kDpadDown : 0) | + (state.dpad_left ? kDpadLeft : 0) | + (state.dpad_right ? kDpadRight : 0) | (state.button_plus ? kStart : 0) | + (state.button_minus ? kBack : 0) | (state.button_l3 ? kLeftThumb : 0) | + (state.button_r3 ? kRightThumb : 0) | + (state.button_l ? kLeftShoulder : 0) | + (state.button_r ? kRightShoulder : 0) | + (state.button_home ? kGuide : 0) | + // Switch labels are positional opposites of XInput labels. + (state.button_b ? kButtonA : 0) | (state.button_a ? kButtonB : 0) | + (state.button_y ? kButtonX : 0) | (state.button_x ? kButtonY : 0); + report.left_trigger = state.button_zl ? 0xff : 0x00; + report.right_trigger = state.button_zr ? 0xff : 0x00; + report.left_x = horizontal_axis(state.lx); + report.left_y = vertical_axis(state.ly); + report.right_x = horizontal_axis(state.rx); + report.right_y = vertical_axis(state.ry); + return report; +} + +inline bool parse_rumble_report(const uint8_t *data, uint32_t size, + SwitchRumbleOutput *output) { + if (data == nullptr || output == nullptr || size < 5 || data[0] != 0x00 || + data[1] != 0x08) { + return false; + } + output->low_frequency_magnitude = data[3]; + output->high_frequency_magnitude = data[4]; + return true; +} + +} // namespace XInputFeasibility