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()
|
||||
# ====================================================================================
|
||||
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)
|
||||
|
|
|
|||
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
|
||||
#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
|
||||
|
|
|
|||
|
|
@ -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<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);
|
||||
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<chr_count; i++) {
|
||||
desc_str[1+i] = str[i];
|
||||
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] = (uint16_t) ((0x03 << 8 ) | (2*chr_count + 2));
|
||||
desc_str[0] =
|
||||
static_cast<uint16_t>((0x03 << 8) | (2 * chr_count + 2));
|
||||
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 "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;
|
||||
}
|
||||
|
|
|
|||
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