Add dual-controller AIO USB transport
This commit is contained in:
parent
7941294a8d
commit
edf6ecaae1
19 changed files with 1007 additions and 116 deletions
261
tests/bluepad32_backend_lifecycle_test.cpp
Normal file
261
tests/bluepad32_backend_lifecycle_test.cpp
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include <uni.h>
|
||||
|
||||
namespace {
|
||||
|
||||
bool incoming_connections = false;
|
||||
int scan_starts = 0;
|
||||
int scan_stops = 0;
|
||||
uni_platform* installed_platform = nullptr;
|
||||
|
||||
void require(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void play_rumble(uni_hid_device_t* device, uint16_t, uint16_t,
|
||||
uint8_t high, uint8_t low) {
|
||||
++device->rumble_calls;
|
||||
device->last_high = high;
|
||||
device->last_low = low;
|
||||
}
|
||||
|
||||
uni_hid_device_t device(int idx, bool gamepad = true) {
|
||||
uni_hid_device_t result{};
|
||||
result.idx = idx;
|
||||
result.gamepad = gamepad;
|
||||
result.report_parser.play_dual_rumble = play_rumble;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool uni_hid_device_is_gamepad(const uni_hid_device_t* device) {
|
||||
return device != nullptr && device->gamepad;
|
||||
}
|
||||
|
||||
int uni_hid_device_get_idx_for_instance(const uni_hid_device_t* device) {
|
||||
return device == nullptr ? -1 : device->idx;
|
||||
}
|
||||
|
||||
void uni_bt_allow_incoming_connections(bool enabled) {
|
||||
incoming_connections = enabled;
|
||||
}
|
||||
|
||||
void uni_bt_start_scanning_and_autoconnect_unsafe() {
|
||||
++scan_starts;
|
||||
}
|
||||
|
||||
void uni_bt_stop_scanning_unsafe() {
|
||||
++scan_stops;
|
||||
}
|
||||
|
||||
void uni_platform_set_custom(uni_platform* platform) {
|
||||
installed_platform = platform;
|
||||
}
|
||||
|
||||
int uni_init(int, const char**) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cyw43_arch_init() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void cyw43_arch_gpio_put(int, bool) {}
|
||||
void multicore_launch_core1(void (*)()) {}
|
||||
|
||||
#include "../bluepad32_input_backend.cpp"
|
||||
|
||||
namespace {
|
||||
|
||||
void start_backend() {
|
||||
bluepad32_input_backend_init();
|
||||
platform_on_init_complete();
|
||||
require(incoming_connections, "initialization must allow connections");
|
||||
require(scan_starts == 1, "initialization must start scanning");
|
||||
}
|
||||
|
||||
void test_ready_order(int first_slot) {
|
||||
start_backend();
|
||||
uni_hid_device_t devices[2] = {device(0), device(1)};
|
||||
const int second_slot = 1 - first_slot;
|
||||
|
||||
require(platform_on_device_ready(&devices[first_slot]) ==
|
||||
UNI_ERROR_SUCCESS,
|
||||
"first ready device must bind to its Bluepad index");
|
||||
require(scan_stops == 0,
|
||||
"scanning must continue while one slot remains free");
|
||||
require(incoming_connections,
|
||||
"incoming connections must remain enabled with one ready slot");
|
||||
|
||||
SwitchInputState first{};
|
||||
SwitchInputState second{};
|
||||
require(bluepad32_input_backend_snapshot(first_slot, &first),
|
||||
"first ready slot must be active");
|
||||
require(!bluepad32_input_backend_snapshot(second_slot, &second),
|
||||
"other slot must remain independently inactive");
|
||||
|
||||
require(platform_on_device_ready(&devices[second_slot]) ==
|
||||
UNI_ERROR_SUCCESS,
|
||||
"second ready device must bind to its Bluepad index");
|
||||
require(scan_stops == 1,
|
||||
"scanning must stop exactly when both slots are ready");
|
||||
require(!incoming_connections,
|
||||
"incoming connections must be disabled only when full");
|
||||
|
||||
bd_addr_t address{};
|
||||
require(platform_on_device_discovered(address, "extra", 0, 0) ==
|
||||
UNI_ERROR_IGNORE_DEVICE,
|
||||
"discovery must reject devices while both slots are occupied");
|
||||
}
|
||||
|
||||
void test_rejections() {
|
||||
start_backend();
|
||||
uni_hid_device_t non_gamepad = device(0, false);
|
||||
uni_hid_device_t out_of_range = device(2);
|
||||
uni_hid_device_t slot_zero = device(0);
|
||||
uni_hid_device_t collision = device(0);
|
||||
|
||||
require(platform_on_device_ready(&non_gamepad) ==
|
||||
UNI_ERROR_INVALID_CONTROLLER,
|
||||
"non-gamepad must be rejected");
|
||||
require(platform_on_device_ready(&out_of_range) == UNI_ERROR_NO_SLOTS,
|
||||
"out-of-range Bluepad index must be rejected");
|
||||
require(platform_on_device_ready(&slot_zero) == UNI_ERROR_SUCCESS,
|
||||
"valid device must occupy its indexed slot");
|
||||
require(platform_on_device_ready(&collision) == UNI_ERROR_NO_SLOTS,
|
||||
"different device cannot replace an occupied slot");
|
||||
|
||||
uni_controller_t data{};
|
||||
data.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
data.gamepad.buttons = BUTTON_B;
|
||||
platform_on_controller_data(&collision, &data);
|
||||
SwitchInputState snapshot{};
|
||||
require(bluepad32_input_backend_snapshot(0, &snapshot),
|
||||
"occupied slot must stay active");
|
||||
require(!snapshot.button_a,
|
||||
"mismatched device input must not enter the occupied slot");
|
||||
}
|
||||
|
||||
void test_independent_lifecycle() {
|
||||
start_backend();
|
||||
uni_hid_device_t first = device(0);
|
||||
uni_hid_device_t survivor = device(1);
|
||||
require(platform_on_device_ready(&survivor) == UNI_ERROR_SUCCESS,
|
||||
"slot 1 must be accepted before slot 0");
|
||||
require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS,
|
||||
"slot 0 must complete the pair");
|
||||
|
||||
uni_controller_t data0{};
|
||||
data0.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
data0.gamepad.buttons = BUTTON_B;
|
||||
data0.gamepad.accel[0] = 8192;
|
||||
uni_controller_t data1{};
|
||||
data1.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
data1.gamepad.buttons = BUTTON_A;
|
||||
data1.gamepad.gyro[1] = 1024;
|
||||
platform_on_controller_data(&first, &data0);
|
||||
platform_on_controller_data(&survivor, &data1);
|
||||
|
||||
SwitchInputState state0{};
|
||||
SwitchInputState state1{};
|
||||
require(bluepad32_input_backend_snapshot(0, &state0) && state0.button_a &&
|
||||
state0.imu_sample_count == 3,
|
||||
"slot 0 input and IMU must map only to slot 0");
|
||||
require(bluepad32_input_backend_snapshot(1, &state1) && state1.button_b &&
|
||||
state1.imu_sample_count == 3,
|
||||
"slot 1 input and IMU must map only to slot 1");
|
||||
|
||||
bluepad32_input_backend_report_sent(0);
|
||||
require(bluepad32_input_backend_snapshot(0, &state0) &&
|
||||
state0.imu_sample_count == 0,
|
||||
"slot 0 report acknowledgement must consume only slot 0 IMU");
|
||||
require(bluepad32_input_backend_snapshot(1, &state1) &&
|
||||
state1.imu_sample_count == 3,
|
||||
"slot 0 acknowledgement must not consume slot 1 IMU");
|
||||
|
||||
const SwitchRumbleOutput rumble0{11, 22};
|
||||
const SwitchRumbleOutput rumble1{33, 44};
|
||||
bluepad32_input_backend_queue_rumble(0, rumble0);
|
||||
bluepad32_input_backend_queue_rumble(1, rumble1);
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(first.rumble_calls == 1 && first.last_low == 11 &&
|
||||
first.last_high == 22,
|
||||
"slot 0 rumble must reach only controller 0");
|
||||
require(survivor.rumble_calls == 1 && survivor.last_low == 33 &&
|
||||
survivor.last_high == 44,
|
||||
"slot 1 rumble must reach only controller 1");
|
||||
|
||||
bluepad32_input_backend_queue_rumble(0, SwitchRumbleOutput{55, 66});
|
||||
const uint32_t disconnected_generation =
|
||||
g_slots[0].connection_generation;
|
||||
const int starts_before_disconnect = scan_starts;
|
||||
platform_on_device_disconnected(&first);
|
||||
require(scan_starts == starts_before_disconnect + 1 &&
|
||||
incoming_connections,
|
||||
"disconnect must resume scanning and incoming connections");
|
||||
require(!bluepad32_input_backend_snapshot(0, &state0) &&
|
||||
!state0.button_a && state0.lx == 32768,
|
||||
"disconnect must neutralize only its own slot");
|
||||
require(bluepad32_input_backend_snapshot(1, &state1) && state1.button_b,
|
||||
"disconnect must preserve survivor state and activity");
|
||||
|
||||
uni_hid_device_t replacement = device(0);
|
||||
require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS,
|
||||
"replacement must bind to the freed indexed slot");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(replacement.rumble_calls == 0,
|
||||
"replacement must not receive disconnected device rumble");
|
||||
|
||||
g_slots[0].pending_rumble = {
|
||||
0, disconnected_generation, SwitchRumbleOutput{77, 88}};
|
||||
g_slots[0].rumble_pending = true;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(replacement.rumble_calls == 0,
|
||||
"stale connection generation must be rejected at dispatch");
|
||||
|
||||
uni_controller_t replacement_data{};
|
||||
replacement_data.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
replacement_data.gamepad.buttons = BUTTON_Y;
|
||||
platform_on_controller_data(&replacement, &replacement_data);
|
||||
require(bluepad32_input_backend_snapshot(0, &state0) && state0.button_x,
|
||||
"replacement input must populate only the freed slot");
|
||||
require(bluepad32_input_backend_snapshot(1, &state1) && state1.button_b,
|
||||
"replacement must not disturb survivor input");
|
||||
|
||||
bluepad32_input_backend_queue_rumble(0, SwitchRumbleOutput{90, 91});
|
||||
bluepad32_input_backend_queue_rumble(1, SwitchRumbleOutput{92, 93});
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(replacement.rumble_calls == 1 && replacement.last_low == 90 &&
|
||||
replacement.last_high == 91,
|
||||
"replacement must receive only new-generation slot 0 rumble");
|
||||
require(survivor.rumble_calls == 2 && survivor.last_low == 92 &&
|
||||
survivor.last_high == 93,
|
||||
"survivor rumble must continue after peer replacement");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
require(argc == 2, "scenario argument required");
|
||||
const std::string scenario = argv[1];
|
||||
if (scenario == "ready-0-1") {
|
||||
test_ready_order(0);
|
||||
} else if (scenario == "ready-1-0") {
|
||||
test_ready_order(1);
|
||||
} else if (scenario == "rejections") {
|
||||
test_rejections();
|
||||
} else if (scenario == "lifecycle") {
|
||||
test_independent_lifecycle();
|
||||
} else {
|
||||
require(false, "unknown scenario");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
22
tests/bluepad32_native_stubs/btstack_run_loop.h
Normal file
22
tests/bluepad32_native_stubs/btstack_run_loop.h
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
struct btstack_timer_source_t {
|
||||
void (*handler)(btstack_timer_source_t*);
|
||||
uint32_t timeout_ms;
|
||||
};
|
||||
|
||||
inline void btstack_run_loop_set_timer_handler(
|
||||
btstack_timer_source_t* timer,
|
||||
void (*handler)(btstack_timer_source_t*)) {
|
||||
timer->handler = handler;
|
||||
}
|
||||
|
||||
inline void btstack_run_loop_set_timer(btstack_timer_source_t* timer,
|
||||
uint32_t timeout_ms) {
|
||||
timer->timeout_ms = timeout_ms;
|
||||
}
|
||||
|
||||
inline void btstack_run_loop_add_timer(btstack_timer_source_t*) {}
|
||||
inline void btstack_run_loop_execute() {}
|
||||
7
tests/bluepad32_native_stubs/pico/critical_section.h
Normal file
7
tests/bluepad32_native_stubs/pico/critical_section.h
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
struct critical_section_t {};
|
||||
|
||||
inline void critical_section_init(critical_section_t*) {}
|
||||
inline void critical_section_enter_blocking(critical_section_t*) {}
|
||||
inline void critical_section_exit(critical_section_t*) {}
|
||||
6
tests/bluepad32_native_stubs/pico/cyw43_arch.h
Normal file
6
tests/bluepad32_native_stubs/pico/cyw43_arch.h
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#define CYW43_WL_GPIO_LED_PIN 0
|
||||
|
||||
int cyw43_arch_init();
|
||||
void cyw43_arch_gpio_put(int pin, bool value);
|
||||
3
tests/bluepad32_native_stubs/pico/multicore.h
Normal file
3
tests/bluepad32_native_stubs/pico/multicore.h
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#pragma once
|
||||
|
||||
void multicore_launch_core1(void (*entry)());
|
||||
3
tests/bluepad32_native_stubs/pico/stdlib.h
Normal file
3
tests/bluepad32_native_stubs/pico/stdlib.h
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#pragma once
|
||||
|
||||
inline void tight_loop_contents() {}
|
||||
98
tests/bluepad32_native_stubs/uni.h
Normal file
98
tests/bluepad32_native_stubs/uni.h
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef uint8_t bd_addr_t[6];
|
||||
typedef int uni_property_idx_t;
|
||||
typedef int uni_platform_oob_event_t;
|
||||
struct uni_property_t {};
|
||||
|
||||
enum uni_error_t {
|
||||
UNI_ERROR_SUCCESS = 0,
|
||||
UNI_ERROR_IGNORE_DEVICE = 1,
|
||||
UNI_ERROR_INVALID_CONTROLLER = 2,
|
||||
UNI_ERROR_NO_SLOTS = 3,
|
||||
};
|
||||
|
||||
enum {
|
||||
UNI_CONTROLLER_CLASS_GAMEPAD = 1,
|
||||
DPAD_UP = 1 << 0,
|
||||
DPAD_DOWN = 1 << 1,
|
||||
DPAD_LEFT = 1 << 2,
|
||||
DPAD_RIGHT = 1 << 3,
|
||||
BUTTON_A = 1 << 0,
|
||||
BUTTON_B = 1 << 1,
|
||||
BUTTON_X = 1 << 2,
|
||||
BUTTON_Y = 1 << 3,
|
||||
BUTTON_SHOULDER_L = 1 << 4,
|
||||
BUTTON_SHOULDER_R = 1 << 5,
|
||||
BUTTON_TRIGGER_L = 1 << 6,
|
||||
BUTTON_TRIGGER_R = 1 << 7,
|
||||
BUTTON_THUMB_L = 1 << 8,
|
||||
BUTTON_THUMB_R = 1 << 9,
|
||||
MISC_BUTTON_SELECT = 1 << 0,
|
||||
MISC_BUTTON_START = 1 << 1,
|
||||
MISC_BUTTON_SYSTEM = 1 << 2,
|
||||
MISC_BUTTON_CAPTURE = 1 << 3,
|
||||
};
|
||||
|
||||
struct uni_gamepad_t {
|
||||
uint32_t dpad;
|
||||
uint32_t buttons;
|
||||
uint32_t misc_buttons;
|
||||
int32_t axis_x;
|
||||
int32_t axis_y;
|
||||
int32_t axis_rx;
|
||||
int32_t axis_ry;
|
||||
int32_t brake;
|
||||
int32_t throttle;
|
||||
int32_t accel[3];
|
||||
int32_t gyro[3];
|
||||
};
|
||||
|
||||
struct uni_controller_t {
|
||||
int klass;
|
||||
uni_gamepad_t gamepad;
|
||||
};
|
||||
|
||||
struct uni_hid_device_t;
|
||||
typedef void (*uni_play_dual_rumble_t)(uni_hid_device_t*, uint16_t,
|
||||
uint16_t, uint8_t, uint8_t);
|
||||
|
||||
struct uni_report_parser_t {
|
||||
uni_play_dual_rumble_t play_dual_rumble;
|
||||
};
|
||||
|
||||
struct uni_hid_device_t {
|
||||
int idx;
|
||||
bool gamepad;
|
||||
uni_report_parser_t report_parser;
|
||||
int rumble_calls;
|
||||
uint8_t last_high;
|
||||
uint8_t last_low;
|
||||
};
|
||||
|
||||
struct uni_platform {
|
||||
const char* name;
|
||||
void (*init)(int, const char**);
|
||||
void (*on_init_complete)();
|
||||
uni_error_t (*on_device_discovered)(bd_addr_t, const char*, uint16_t,
|
||||
uint8_t);
|
||||
void (*on_device_connected)(uni_hid_device_t*);
|
||||
void (*on_device_disconnected)(uni_hid_device_t*);
|
||||
uni_error_t (*on_device_ready)(uni_hid_device_t*);
|
||||
void* on_device_oob_event;
|
||||
void (*on_controller_data)(uni_hid_device_t*, uni_controller_t*);
|
||||
const uni_property_t* (*get_property)(uni_property_idx_t);
|
||||
void (*on_oob_event)(uni_platform_oob_event_t, void*);
|
||||
void* on_device_dump;
|
||||
void* on_gamepad_seat;
|
||||
};
|
||||
|
||||
bool uni_hid_device_is_gamepad(const uni_hid_device_t* device);
|
||||
int uni_hid_device_get_idx_for_instance(const uni_hid_device_t* device);
|
||||
void uni_bt_allow_incoming_connections(bool enabled);
|
||||
void uni_bt_start_scanning_and_autoconnect_unsafe();
|
||||
void uni_bt_stop_scanning_unsafe();
|
||||
void uni_platform_set_custom(uni_platform* platform);
|
||||
int uni_init(int argc, const char** argv);
|
||||
169
tests/switch_pro_descriptors_test.cpp
Normal file
169
tests/switch_pro_descriptors_test.cpp
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
#include "switch_pro_descriptors.h"
|
||||
#include "tusb_config.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
|
||||
#ifndef EXPECTED_HID_INSTANCE_COUNT
|
||||
#error "EXPECTED_HID_INSTANCE_COUNT must be defined by the test build"
|
||||
#endif
|
||||
|
||||
static_assert(SWITCH_PICO_HID_INSTANCE_COUNT == EXPECTED_HID_INSTANCE_COUNT,
|
||||
"the requested HID instance count did not reach the descriptors");
|
||||
static_assert(CFG_TUD_HID == EXPECTED_HID_INSTANCE_COUNT,
|
||||
"TinyUSB HID count differs from the descriptor count");
|
||||
static_assert(sizeof(switch_pro_configuration_descriptor) ==
|
||||
9u + 32u * EXPECTED_HID_INSTANCE_COUNT,
|
||||
"configuration descriptor has the wrong total size");
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t kConfigurationDescriptor = 0x02;
|
||||
constexpr uint8_t kInterfaceDescriptor = 0x04;
|
||||
constexpr uint8_t kEndpointDescriptor = 0x05;
|
||||
constexpr uint8_t kHidDescriptor = 0x21;
|
||||
|
||||
int failures = 0;
|
||||
|
||||
void expect(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << message << '\n';
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t read_u16(const uint8_t* bytes) {
|
||||
return static_cast<uint16_t>(bytes[0]) |
|
||||
(static_cast<uint16_t>(bytes[1]) << 8u);
|
||||
}
|
||||
|
||||
struct InterfaceContract {
|
||||
bool present = false;
|
||||
bool in_endpoint = false;
|
||||
bool out_endpoint = false;
|
||||
uint8_t endpoint_count = 0;
|
||||
uint8_t hid_count = 0;
|
||||
};
|
||||
|
||||
void inspect_configuration_descriptor() {
|
||||
const auto* descriptor = switch_pro_configuration_descriptor;
|
||||
constexpr size_t descriptor_size =
|
||||
sizeof(switch_pro_configuration_descriptor);
|
||||
|
||||
expect(descriptor[0] == 9 && descriptor[1] == kConfigurationDescriptor,
|
||||
"configuration header is malformed");
|
||||
expect(read_u16(descriptor + 2) == descriptor_size,
|
||||
"wTotalLength does not match the emitted descriptor");
|
||||
expect(descriptor[4] == EXPECTED_HID_INSTANCE_COUNT,
|
||||
"bNumInterfaces does not match the HID instance count");
|
||||
|
||||
std::array<InterfaceContract, EXPECTED_HID_INSTANCE_COUNT> interfaces{};
|
||||
std::array<bool, 256> endpoint_addresses{};
|
||||
int current_interface = -1;
|
||||
size_t offset = descriptor[0];
|
||||
|
||||
while (offset < descriptor_size) {
|
||||
const uint8_t length = descriptor[offset];
|
||||
expect(length >= 2, "descriptor block has an invalid length");
|
||||
if (length < 2) {
|
||||
break;
|
||||
}
|
||||
expect(offset + length <= descriptor_size,
|
||||
"descriptor block extends beyond wTotalLength");
|
||||
if (offset + length > descriptor_size) {
|
||||
break;
|
||||
}
|
||||
|
||||
const uint8_t type = descriptor[offset + 1];
|
||||
if (type == kInterfaceDescriptor) {
|
||||
expect(length == 9, "interface descriptor has the wrong length");
|
||||
const uint8_t number = descriptor[offset + 2];
|
||||
expect(number < interfaces.size(),
|
||||
"interface number is outside the configured range");
|
||||
if (number < interfaces.size()) {
|
||||
expect(!interfaces[number].present,
|
||||
"interface number is duplicated");
|
||||
interfaces[number].present = true;
|
||||
current_interface = number;
|
||||
} else {
|
||||
current_interface = -1;
|
||||
}
|
||||
expect(descriptor[offset + 3] == 0,
|
||||
"interface uses an unexpected alternate setting");
|
||||
expect(descriptor[offset + 4] == 2,
|
||||
"interface does not declare two endpoints");
|
||||
expect(descriptor[offset + 5] == 0x03,
|
||||
"interface is not HID class");
|
||||
} else if (type == kHidDescriptor) {
|
||||
expect(current_interface >= 0,
|
||||
"HID descriptor appears before an interface");
|
||||
expect(length == sizeof(switch_pro_hid_descriptor),
|
||||
"HID descriptor has the wrong length");
|
||||
expect(std::memcmp(descriptor + offset, switch_pro_hid_descriptor,
|
||||
sizeof(switch_pro_hid_descriptor)) == 0,
|
||||
"interfaces do not reuse the shared HID/report contract");
|
||||
expect(read_u16(descriptor + offset + 7) ==
|
||||
sizeof(switch_pro_report_descriptor),
|
||||
"HID descriptor advertises the wrong report descriptor size");
|
||||
if (current_interface >= 0) {
|
||||
++interfaces[static_cast<size_t>(current_interface)].hid_count;
|
||||
}
|
||||
} else if (type == kEndpointDescriptor) {
|
||||
expect(current_interface >= 0,
|
||||
"endpoint descriptor appears before an interface");
|
||||
expect(length == 7, "endpoint descriptor has the wrong length");
|
||||
const uint8_t address = descriptor[offset + 2];
|
||||
expect(!endpoint_addresses[address],
|
||||
"endpoint address is duplicated across interfaces");
|
||||
endpoint_addresses[address] = true;
|
||||
expect(descriptor[offset + 3] == 0x03,
|
||||
"endpoint is not interrupt type");
|
||||
expect(read_u16(descriptor + offset + 4) ==
|
||||
SWITCH_PRO_ENDPOINT_SIZE,
|
||||
"endpoint has the wrong maximum packet size");
|
||||
expect(descriptor[offset + 6] == 8,
|
||||
"endpoint has the wrong polling interval");
|
||||
|
||||
if (current_interface >= 0) {
|
||||
auto& interface =
|
||||
interfaces[static_cast<size_t>(current_interface)];
|
||||
++interface.endpoint_count;
|
||||
const uint8_t endpoint_number =
|
||||
static_cast<uint8_t>(current_interface + 1);
|
||||
if ((address & 0x80u) != 0) {
|
||||
expect(address == static_cast<uint8_t>(0x80u | endpoint_number),
|
||||
"IN endpoint does not belong to its interface");
|
||||
interface.in_endpoint = true;
|
||||
} else {
|
||||
expect(address == endpoint_number,
|
||||
"OUT endpoint does not belong to its interface");
|
||||
interface.out_endpoint = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
offset += length;
|
||||
}
|
||||
|
||||
expect(offset == descriptor_size,
|
||||
"descriptor parser did not finish at wTotalLength");
|
||||
for (const auto& interface : interfaces) {
|
||||
expect(interface.present, "configured HID interface is missing");
|
||||
expect(interface.hid_count == 1,
|
||||
"interface does not contain exactly one HID descriptor");
|
||||
expect(interface.endpoint_count == 2,
|
||||
"interface does not contain exactly two endpoints");
|
||||
expect(interface.in_endpoint && interface.out_endpoint,
|
||||
"interface is missing an IN or OUT endpoint");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
inspect_configuration_descriptor();
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
34
tests/test_bluepad32_backend_lifecycle_native.py
Normal file
34
tests/test_bluepad32_backend_lifecycle_native.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = shutil.which("c++") or shutil.which("g++")
|
||||
assert compiler is not None, "a host C++ compiler is required"
|
||||
|
||||
executable = tmp_path / "bluepad32_backend_lifecycle_test"
|
||||
subprocess.run(
|
||||
[
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
"-DSWITCH_PICO_HID_INSTANCE_COUNT=2",
|
||||
f"-I{root / 'tests' / 'bluepad32_native_stubs'}",
|
||||
f"-I{root}",
|
||||
str(root / "tests" / "bluepad32_backend_lifecycle_test.cpp"),
|
||||
"-o",
|
||||
str(executable),
|
||||
],
|
||||
check=True,
|
||||
cwd=root,
|
||||
)
|
||||
|
||||
for scenario in ("ready-0-1", "ready-1-0", "rejections", "lifecycle"):
|
||||
subprocess.run([str(executable), scenario], check=True, cwd=root)
|
||||
87
tests/test_switch_pro_descriptors_native.py
Normal file
87
tests/test_switch_pro_descriptors_native.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def compile_descriptor_test(
|
||||
root: Path,
|
||||
compiler: str,
|
||||
output: Path,
|
||||
expected_count: int,
|
||||
configured_count: int | None,
|
||||
) -> subprocess.CompletedProcess[str]:
|
||||
command = [
|
||||
compiler,
|
||||
"-std=c++17",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Werror",
|
||||
"-pedantic",
|
||||
f"-DEXPECTED_HID_INSTANCE_COUNT={expected_count}",
|
||||
]
|
||||
if configured_count is not None:
|
||||
command.append(f"-DSWITCH_PICO_HID_INSTANCE_COUNT={configured_count}")
|
||||
command.extend(
|
||||
[
|
||||
f"-I{root}",
|
||||
str(root / "tests" / "switch_pro_descriptors_test.cpp"),
|
||||
"-o",
|
||||
str(output),
|
||||
]
|
||||
)
|
||||
return subprocess.run(
|
||||
command,
|
||||
check=False,
|
||||
cwd=root,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
|
||||
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_default_descriptor_contract_is_single_hid(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
executable = tmp_path / "switch_pro_descriptors_default_test"
|
||||
result = compile_descriptor_test(root, host_compiler(), executable, 1, None)
|
||||
assert result.returncode == 0, result.stderr
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
||||
|
||||
def test_explicit_single_descriptor_contract(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
executable = tmp_path / "switch_pro_descriptors_single_test"
|
||||
result = compile_descriptor_test(root, host_compiler(), executable, 1, 1)
|
||||
assert result.returncode == 0, result.stderr
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
||||
|
||||
def test_dual_descriptor_contract(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
executable = tmp_path / "switch_pro_descriptors_dual_test"
|
||||
result = compile_descriptor_test(root, host_compiler(), executable, 2, 2)
|
||||
assert result.returncode == 0, result.stderr
|
||||
subprocess.run([str(executable)], check=True, cwd=root)
|
||||
|
||||
|
||||
def test_unsupported_hid_instance_counts_fail_to_compile(tmp_path: Path) -> None:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
compiler = host_compiler()
|
||||
for unsupported_count in (0, 3):
|
||||
executable = tmp_path / f"switch_pro_descriptors_invalid_{unsupported_count}"
|
||||
result = compile_descriptor_test(
|
||||
root,
|
||||
compiler,
|
||||
executable,
|
||||
unsupported_count,
|
||||
unsupported_count,
|
||||
)
|
||||
assert result.returncode != 0, (
|
||||
f"unsupported HID instance count {unsupported_count} compiled successfully"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue