Prototype adapter XInput feasibility
This commit is contained in:
parent
fd23842327
commit
17b3d7399c
13 changed files with 953 additions and 13 deletions
|
|
@ -24,6 +24,8 @@ if (EXISTS ${picoVscode})
|
||||||
endif()
|
endif()
|
||||||
# ====================================================================================
|
# ====================================================================================
|
||||||
option(SWITCH_PICO_LOG "Enable UART debug logging" OFF)
|
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(SWITCH_PICO_INPUT_BACKEND "UART" CACHE STRING "Controller input backend")
|
||||||
set_property(CACHE SWITCH_PICO_INPUT_BACKEND PROPERTY STRINGS UART BLUEPAD32)
|
set_property(CACHE SWITCH_PICO_INPUT_BACKEND PROPERTY STRINGS UART BLUEPAD32)
|
||||||
if(NOT SWITCH_PICO_INPUT_BACKEND STREQUAL "UART"
|
if(NOT SWITCH_PICO_INPUT_BACKEND STREQUAL "UART"
|
||||||
|
|
@ -38,6 +40,11 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32"
|
||||||
message(FATAL_ERROR
|
message(FATAL_ERROR
|
||||||
"SWITCH_PICO_INPUT_BACKEND=BLUEPAD32 requires PICO_BOARD=pico2_w")
|
"SWITCH_PICO_INPUT_BACKEND=BLUEPAD32 requires PICO_BOARD=pico2_w")
|
||||||
endif()
|
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)
|
# Pull in Raspberry Pi Pico SDK (must be before project)
|
||||||
include(pico_sdk_import.cmake)
|
include(pico_sdk_import.cmake)
|
||||||
|
|
@ -95,6 +102,15 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
||||||
bootsel_pairing_button.cpp
|
bootsel_pairing_button.cpp
|
||||||
usb_pairing_management.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
|
target_compile_definitions(switch-pico PRIVATE
|
||||||
SWITCH_PICO_BLUEPAD32=1
|
SWITCH_PICO_BLUEPAD32=1
|
||||||
SWITCH_PICO_HID_INSTANCE_COUNT=4
|
SWITCH_PICO_HID_INSTANCE_COUNT=4
|
||||||
|
|
@ -132,6 +148,9 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
||||||
pico_multicore
|
pico_multicore
|
||||||
pico_flash
|
pico_flash
|
||||||
)
|
)
|
||||||
|
if(SWITCH_PICO_ADAPTER_FEASIBILITY)
|
||||||
|
target_link_libraries(switch-pico hardware_watchdog)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (SWITCH_PICO_LOG)
|
if (SWITCH_PICO_LOG)
|
||||||
|
|
|
||||||
87
adapter_host_probe.cpp
Normal file
87
adapter_host_probe.cpp
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
#include "adapter_host_probe.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#ifdef SWITCH_PICO_LOG
|
||||||
|
#include <stdio.h>
|
||||||
|
#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<uint32_t>(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<uint8_t *>(
|
||||||
|
XInputFeasibility::kProbeMsCompatIdDescriptor),
|
||||||
|
sizeof(XInputFeasibility::kProbeMsCompatIdDescriptor));
|
||||||
|
}
|
||||||
|
|
||||||
|
return tud_control_xfer(
|
||||||
|
rhport, request,
|
||||||
|
const_cast<uint8_t *>(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);
|
||||||
|
}
|
||||||
17
adapter_host_probe.h
Normal file
17
adapter_host_probe.h
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#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();
|
||||||
30
adapter_host_probe_state.h
Normal file
30
adapter_host_probe_state.h
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
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<int32_t>(now_ms - reboot_deadline_ms_) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint32_t reboot_deadline_ms_ = 0;
|
||||||
|
bool saw_ms_os_string_ = false;
|
||||||
|
bool confirmed_windows_ = false;
|
||||||
|
};
|
||||||
|
|
@ -8,6 +8,10 @@
|
||||||
#else
|
#else
|
||||||
#include "bluepad32_input_backend.h"
|
#include "bluepad32_input_backend.h"
|
||||||
#include "bootsel_pairing_button.h"
|
#include "bootsel_pairing_button.h"
|
||||||
|
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||||
|
#include "adapter_host_probe.h"
|
||||||
|
#include "xinput_feasibility_driver.h"
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef SWITCH_PICO_LOG
|
#ifdef SWITCH_PICO_LOG
|
||||||
|
|
@ -177,11 +181,26 @@ static void log_usb_state() {
|
||||||
#ifdef SWITCH_PICO_BLUEPAD32
|
#ifdef SWITCH_PICO_BLUEPAD32
|
||||||
for (uint8_t instance = 0;
|
for (uint8_t instance = 0;
|
||||||
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
|
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);
|
const bool ready = switch_pro_is_ready(instance);
|
||||||
|
#endif
|
||||||
if (ready != g_last_ready[instance]) {
|
if (ready != g_last_ready[instance]) {
|
||||||
g_last_ready[instance] = ready;
|
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,
|
LOG_PRINTF("[SWITCH %u] driver %s\n", instance,
|
||||||
ready ? "ready (handshake OK)" : "not ready");
|
ready ? "ready (handshake OK)" : "not ready");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
|
|
@ -200,6 +219,9 @@ int main() {
|
||||||
|
|
||||||
#ifdef SWITCH_PICO_BLUEPAD32
|
#ifdef SWITCH_PICO_BLUEPAD32
|
||||||
bluepad32_input_backend_init();
|
bluepad32_input_backend_init();
|
||||||
|
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||||
|
adapter_host_probe_init();
|
||||||
|
#endif
|
||||||
#else
|
#else
|
||||||
init_uart_input();
|
init_uart_input();
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -208,10 +230,27 @@ int main() {
|
||||||
#ifdef SWITCH_PICO_BLUEPAD32
|
#ifdef SWITCH_PICO_BLUEPAD32
|
||||||
for (uint8_t instance = 0;
|
for (uint8_t instance = 0;
|
||||||
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
|
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_init(instance);
|
||||||
switch_pro_set_rumble_callback(instance, on_rumble_from_switch);
|
switch_pro_set_rumble_callback(instance, on_rumble_from_switch);
|
||||||
g_user_states[instance] = neutral_input();
|
g_user_states[instance] = neutral_input();
|
||||||
switch_pro_set_input(instance, g_user_states[instance]);
|
switch_pro_set_input(instance, g_user_states[instance]);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
switch_pro_init(SWITCH_HID_INSTANCE);
|
switch_pro_init(SWITCH_HID_INSTANCE);
|
||||||
|
|
@ -223,7 +262,14 @@ int main() {
|
||||||
|
|
||||||
#ifdef SWITCH_PICO_BLUEPAD32
|
#ifdef SWITCH_PICO_BLUEPAD32
|
||||||
bluepad32_input_backend_start();
|
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");
|
LOG_PRINTF("[BOOT] switch-pico starting (Bluepad32 wireless @ 115200)\n");
|
||||||
|
#endif
|
||||||
#else
|
#else
|
||||||
LOG_PRINTF("[BOOT] switch-pico starting (UART0 log @ 115200)\n");
|
LOG_PRINTF("[BOOT] switch-pico starting (UART0 log @ 115200)\n");
|
||||||
LOG_PRINTF("[INFO] UART1 pins TX=%d RX=%d baud=%d\n",
|
LOG_PRINTF("[INFO] UART1 pins TX=%d RX=%d baud=%d\n",
|
||||||
|
|
@ -232,6 +278,9 @@ int main() {
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
tud_task(); // USB device tasks
|
tud_task(); // USB device tasks
|
||||||
|
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||||
|
adapter_host_probe_task();
|
||||||
|
#endif
|
||||||
#ifdef SWITCH_PICO_BLUEPAD32
|
#ifdef SWITCH_PICO_BLUEPAD32
|
||||||
switch (bootsel_pairing_button_task()) {
|
switch (bootsel_pairing_button_task()) {
|
||||||
case BootselPairingButtonEvent::kOpenPairing:
|
case BootselPairingButtonEvent::kOpenPairing:
|
||||||
|
|
@ -247,10 +296,25 @@ int main() {
|
||||||
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
|
instance < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++instance) {
|
||||||
bluepad32_input_backend_snapshot(instance,
|
bluepad32_input_backend_snapshot(instance,
|
||||||
&g_user_states[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]);
|
switch_pro_set_input(instance, g_user_states[instance]);
|
||||||
if (switch_pro_task(instance)) {
|
if (switch_pro_task(instance)) {
|
||||||
bluepad32_input_backend_report_sent(instance);
|
bluepad32_input_backend_report_sent(instance);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
bool new_data = poll_uart_frames(); // Pull controller state from UART1
|
bool new_data = poll_uart_frames(); // Pull controller state from UART1
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@
|
||||||
#include "pico/rand.h"
|
#include "pico/rand.h"
|
||||||
#include "pico/time.h"
|
#include "pico/time.h"
|
||||||
#include "tusb.h"
|
#include "tusb.h"
|
||||||
|
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||||
|
#include "adapter_host_probe.h"
|
||||||
|
#include "xinput_feasibility_descriptors.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef SWITCH_PICO_LOG
|
#ifdef SWITCH_PICO_LOG
|
||||||
#define LOG_PRINTF(...) printf(__VA_ARGS__)
|
#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) {
|
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;
|
return switch_pro_device_descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t const* tud_descriptor_configuration_cb(uint8_t index) {
|
uint8_t const* tud_descriptor_configuration_cb(uint8_t index) {
|
||||||
(void)index;
|
(void)index;
|
||||||
|
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||||
|
if (adapter_host_probe_mode() == AdapterUsbMode::kXInput) {
|
||||||
|
return XInputFeasibility::kConfigurationDescriptor;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
return switch_pro_configuration_descriptor;
|
return switch_pro_configuration_descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1114,28 +1128,62 @@ void tud_umount_cb(void) {
|
||||||
|
|
||||||
static uint16_t desc_str[32];
|
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;
|
(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<uint16_t>((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);
|
memcpy(&desc_str[1], switch_pro_string_language, 2);
|
||||||
chr_count = 1;
|
chr_count = 1;
|
||||||
} else {
|
} 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];
|
while (str[chr_count] != 0) {
|
||||||
|
++chr_count;
|
||||||
chr_count = 0;
|
}
|
||||||
while ( str[chr_count] ) chr_count++;
|
if (chr_count > 31) {
|
||||||
if ( chr_count > 31 ) chr_count = 31;
|
chr_count = 31;
|
||||||
|
}
|
||||||
for(uint8_t i=0; i<chr_count; i++) {
|
for (uint8_t i = 0; i < chr_count; ++i) {
|
||||||
desc_str[1+i] = str[i];
|
desc_str[1 + i] = str[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
desc_str[0] = (uint16_t) ((0x03 << 8 ) | (2*chr_count + 2));
|
desc_str[0] =
|
||||||
|
static_cast<uint16_t>((0x03 << 8) | (2 * chr_count + 2));
|
||||||
return desc_str;
|
return desc_str;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
39
tests/test_xinput_feasibility_native.py
Normal file
39
tests/test_xinput_feasibility_native.py
Normal file
|
|
@ -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)
|
||||||
186
tests/xinput_feasibility_test.cpp
Normal file
186
tests/xinput_feasibility_test.cpp
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#include "adapter_host_probe_state.h"
|
||||||
|
#include "xinput_feasibility_descriptors.h"
|
||||||
|
#include "xinput_feasibility_protocol.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
void expect(bool condition, const char *message) {
|
||||||
|
if (!condition) {
|
||||||
|
std::cerr << "FAIL: " << message << '\n';
|
||||||
|
++failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t read_le16(const uint8_t *data) {
|
||||||
|
return static_cast<uint16_t>(data[0] | (data[1] << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t read_le32(const uint8_t *data) {
|
||||||
|
return static_cast<uint32_t>(data[0]) |
|
||||||
|
(static_cast<uint32_t>(data[1]) << 8) |
|
||||||
|
(static_cast<uint32_t>(data[2]) << 16) |
|
||||||
|
(static_cast<uint32_t>(data[3]) << 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_device_and_configuration_descriptors() {
|
||||||
|
using namespace XInputFeasibility;
|
||||||
|
expect(read_le16(&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<bool, 16> endpoints{};
|
||||||
|
for (uint8_t instance = 0; instance < SWITCH_PICO_HID_INSTANCE_COUNT;
|
||||||
|
++instance) {
|
||||||
|
const size_t offset = 9 + instance * kInterfaceDescriptorSize;
|
||||||
|
const uint8_t *interface = &kConfigurationDescriptor[offset];
|
||||||
|
expect(interface[0] == 9 && interface[1] == 4,
|
||||||
|
"missing interface descriptor");
|
||||||
|
expect(interface[2] == instance, "interface number mismatch");
|
||||||
|
expect(interface[5] == 0xff && interface[6] == 0x5d &&
|
||||||
|
interface[7] == 0x01,
|
||||||
|
"XInput interface class tuple mismatch");
|
||||||
|
expect(interface[9] == 0x10 && interface[10] == 0x21,
|
||||||
|
"XInput capability descriptor missing");
|
||||||
|
const uint8_t in_endpoint = interface[27];
|
||||||
|
const uint8_t out_endpoint = interface[34];
|
||||||
|
expect(in_endpoint == static_cast<uint8_t>(0x81 + instance),
|
||||||
|
"input endpoint mismatch");
|
||||||
|
expect(out_endpoint == static_cast<uint8_t>(0x01 + instance),
|
||||||
|
"output endpoint mismatch");
|
||||||
|
expect(interface[15] == in_endpoint && interface[21] == out_endpoint,
|
||||||
|
"capability descriptor endpoint mismatch");
|
||||||
|
expect(!endpoints[in_endpoint & 0x0f] &&
|
||||||
|
!endpoints[out_endpoint & 0x0f],
|
||||||
|
"endpoint number reused");
|
||||||
|
endpoints[in_endpoint & 0x0f] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_microsoft_compatible_id_descriptor() {
|
||||||
|
using namespace XInputFeasibility;
|
||||||
|
expect(read_le32(kMsCompatIdDescriptor) == sizeof(kMsCompatIdDescriptor),
|
||||||
|
"Microsoft descriptor total length mismatch");
|
||||||
|
expect(read_le16(&kMsCompatIdDescriptor[4]) == 0x0100,
|
||||||
|
"Microsoft descriptor version mismatch");
|
||||||
|
expect(read_le16(&kMsCompatIdDescriptor[6]) == kMsCompatIdIndex,
|
||||||
|
"Microsoft descriptor index mismatch");
|
||||||
|
expect(kMsCompatIdDescriptor[8] == SWITCH_PICO_HID_INSTANCE_COUNT,
|
||||||
|
"Microsoft function count mismatch");
|
||||||
|
for (uint8_t instance = 0; instance < SWITCH_PICO_HID_INSTANCE_COUNT;
|
||||||
|
++instance) {
|
||||||
|
const uint8_t *function = &kMsCompatIdDescriptor[16 + instance * 24];
|
||||||
|
expect(function[0] == instance,
|
||||||
|
"Microsoft descriptor interface mismatch");
|
||||||
|
expect(std::memcmp(&function[2], "XUSB10", 6) == 0,
|
||||||
|
"XUSB10 compatible ID missing");
|
||||||
|
}
|
||||||
|
expect(read_le32(kProbeMsCompatIdDescriptor) == 16 &&
|
||||||
|
kProbeMsCompatIdDescriptor[8] == 0,
|
||||||
|
"probe descriptor must expose no compatible functions");
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_input_report_mapping() {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "tusb.h"
|
#include "tusb.h"
|
||||||
|
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||||
|
#include "adapter_host_probe.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace UsbPairingManagement {
|
namespace UsbPairingManagement {
|
||||||
|
|
||||||
|
|
@ -45,6 +48,11 @@ size_t encode_snapshot(const Bluepad32PairingSnapshot& snapshot,
|
||||||
extern "C" bool tud_vendor_control_xfer_cb(
|
extern "C" bool tud_vendor_control_xfer_cb(
|
||||||
uint8_t rhport, uint8_t stage,
|
uint8_t rhport, uint8_t stage,
|
||||||
tusb_control_request_t const* request) {
|
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) {
|
if (stage != CONTROL_STAGE_SETUP) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
123
xinput_feasibility_descriptors.h
Normal file
123
xinput_feasibility_descriptors.h
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifndef SWITCH_PICO_HID_INSTANCE_COUNT
|
||||||
|
#define SWITCH_PICO_HID_INSTANCE_COUNT 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if SWITCH_PICO_HID_INSTANCE_COUNT < 1 || SWITCH_PICO_HID_INSTANCE_COUNT > 4
|
||||||
|
#error "SWITCH_PICO_HID_INSTANCE_COUNT must be between 1 and 4"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace 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<uint8_t>(kPrototypeVendorId & 0xff),
|
||||||
|
static_cast<uint8_t>(kPrototypeVendorId >> 8),
|
||||||
|
static_cast<uint8_t>(kPrototypeProductId & 0xff),
|
||||||
|
static_cast<uint8_t>(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<uint8_t>(0x80 | endpoint), 0x14, 0x03, \
|
||||||
|
0x00, 0x03, 0x13, endpoint, 0x00, 0x03, 0x00, 0x07, 0x05, \
|
||||||
|
static_cast<uint8_t>(0x80 | endpoint), 0x03, 0x20, 0x00, 0x04, 0x07, \
|
||||||
|
0x05, endpoint, 0x03, 0x20, 0x00, 0x08
|
||||||
|
|
||||||
|
static const uint8_t kConfigurationDescriptor[] = {
|
||||||
|
0x09,
|
||||||
|
0x02,
|
||||||
|
static_cast<uint8_t>(kConfigurationDescriptorSize & 0xff),
|
||||||
|
static_cast<uint8_t>(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<uint8_t>(kMsCompatIdDescriptorSize & 0xff),
|
||||||
|
static_cast<uint8_t>(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
|
||||||
219
xinput_feasibility_driver.cpp
Normal file
219
xinput_feasibility_driver.cpp
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
#include "xinput_feasibility_driver.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "device/usbd_pvt.h"
|
||||||
|
#include "tusb.h"
|
||||||
|
#include "xinput_feasibility_descriptors.h"
|
||||||
|
#include "xinput_feasibility_protocol.h"
|
||||||
|
|
||||||
|
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<tusb_desc_endpoint_t const *>(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<uint16_t>(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<uint8_t>(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<uint8_t *>(&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;
|
||||||
|
}
|
||||||
13
xinput_feasibility_driver.h
Normal file
13
xinput_feasibility_driver.h
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#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);
|
||||||
87
xinput_feasibility_protocol.h
Normal file
87
xinput_feasibility_protocol.h
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#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<int16_t>(static_cast<int32_t>(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<int16_t>(-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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue