Scale AIO transport to four controllers
This commit is contained in:
parent
18c27b06ae
commit
fac919a798
13 changed files with 617 additions and 342 deletions
|
|
@ -93,7 +93,7 @@ if(SWITCH_PICO_INPUT_BACKEND STREQUAL "BLUEPAD32")
|
||||||
target_sources(switch-pico PRIVATE bluepad32_input_backend.cpp)
|
target_sources(switch-pico PRIVATE bluepad32_input_backend.cpp)
|
||||||
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=2
|
SWITCH_PICO_HID_INSTANCE_COUNT=4
|
||||||
)
|
)
|
||||||
else()
|
else()
|
||||||
target_compile_definitions(switch-pico PRIVATE
|
target_compile_definitions(switch-pico PRIVATE
|
||||||
|
|
|
||||||
|
|
@ -40,12 +40,12 @@
|
||||||
#define MAX_NR_BNEP_CHANNELS 1
|
#define MAX_NR_BNEP_CHANNELS 1
|
||||||
#define MAX_NR_BNEP_SERVICES 1
|
#define MAX_NR_BNEP_SERVICES 1
|
||||||
#define MAX_NR_BTSTACK_LINK_KEY_DB_MEMORY_ENTRIES 2
|
#define MAX_NR_BTSTACK_LINK_KEY_DB_MEMORY_ENTRIES 2
|
||||||
#define MAX_NR_GATT_CLIENTS 2
|
#define MAX_NR_GATT_CLIENTS 4
|
||||||
#define MAX_NR_HCI_CONNECTIONS 4
|
#define MAX_NR_HCI_CONNECTIONS 4
|
||||||
#define MAX_NR_HID_HOST_CONNECTIONS 2
|
#define MAX_NR_HID_HOST_CONNECTIONS 4
|
||||||
#define MAX_NR_HIDS_CLIENTS 2
|
#define MAX_NR_HIDS_CLIENTS 4
|
||||||
#define MAX_NR_HFP_CONNECTIONS 1
|
#define MAX_NR_HFP_CONNECTIONS 1
|
||||||
#define MAX_NR_L2CAP_CHANNELS 6
|
#define MAX_NR_L2CAP_CHANNELS 10
|
||||||
#define MAX_NR_L2CAP_SERVICES 5
|
#define MAX_NR_L2CAP_SERVICES 5
|
||||||
#define MAX_NR_RFCOMM_CHANNELS 1
|
#define MAX_NR_RFCOMM_CHANNELS 1
|
||||||
#define MAX_NR_RFCOMM_MULTIPLEXERS 1
|
#define MAX_NR_RFCOMM_MULTIPLEXERS 1
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
// The AIO firmware exposes one fixed Bluepad32 device slot per USB interface.
|
// The AIO firmware exposes one fixed Bluepad32 device slot per USB interface.
|
||||||
#define CONFIG_BLUEPAD32_MAX_DEVICES 2
|
#define CONFIG_BLUEPAD32_MAX_DEVICES 4
|
||||||
#define CONFIG_BLUEPAD32_MAX_ALLOWLIST 2
|
#define CONFIG_BLUEPAD32_MAX_ALLOWLIST 4
|
||||||
#define CONFIG_BLUEPAD32_GAP_SECURITY 1
|
#define CONFIG_BLUEPAD32_GAP_SECURITY 1
|
||||||
#define CONFIG_BLUEPAD32_ENABLE_BLE_BY_DEFAULT 1
|
#define CONFIG_BLUEPAD32_ENABLE_BLE_BY_DEFAULT 1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ constexpr uint16_t kRumbleDurationMs = 50;
|
||||||
constexpr uint32_t kRumblePollIntervalMs = 5;
|
constexpr uint32_t kRumblePollIntervalMs = 5;
|
||||||
constexpr uint8_t kSlotCount = BLUEPAD32_INPUT_BACKEND_SLOT_COUNT;
|
constexpr uint8_t kSlotCount = BLUEPAD32_INPUT_BACKEND_SLOT_COUNT;
|
||||||
|
|
||||||
static_assert(kSlotCount == 2);
|
static_assert(kSlotCount == 4);
|
||||||
static_assert(SWITCH_PICO_HID_INSTANCE_COUNT == kSlotCount);
|
static_assert(SWITCH_PICO_HID_INSTANCE_COUNT == kSlotCount);
|
||||||
|
|
||||||
enum class ConnectionStatus {
|
enum class ConnectionStatus {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
#include "switch_haptics.h"
|
#include "switch_haptics.h"
|
||||||
#include "switch_pro_driver.h"
|
#include "switch_pro_driver.h"
|
||||||
|
|
||||||
constexpr uint8_t BLUEPAD32_INPUT_BACKEND_SLOT_COUNT = 2;
|
constexpr uint8_t BLUEPAD32_INPUT_BACKEND_SLOT_COUNT = 4;
|
||||||
|
|
||||||
void bluepad32_input_backend_init();
|
void bluepad32_input_backend_init();
|
||||||
void bluepad32_input_backend_start();
|
void bluepad32_input_backend_start();
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@
|
||||||
#define SWITCH_PICO_HID_INSTANCE_COUNT 1
|
#define SWITCH_PICO_HID_INSTANCE_COUNT 1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if SWITCH_PICO_HID_INSTANCE_COUNT != 1 && SWITCH_PICO_HID_INSTANCE_COUNT != 2
|
#if SWITCH_PICO_HID_INSTANCE_COUNT < 1 || SWITCH_PICO_HID_INSTANCE_COUNT > 4
|
||||||
#error "SWITCH_PICO_HID_INSTANCE_COUNT must be 1 or 2"
|
#error "SWITCH_PICO_HID_INSTANCE_COUNT must be between 1 and 4"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -380,9 +380,15 @@ static const uint8_t switch_pro_configuration_descriptor[] =
|
||||||
#if SWITCH_PICO_HID_INSTANCE_COUNT == 1
|
#if SWITCH_PICO_HID_INSTANCE_COUNT == 1
|
||||||
0x29, 0x00, // wTotalLength 41
|
0x29, 0x00, // wTotalLength 41
|
||||||
0x01, // bNumInterfaces 1
|
0x01, // bNumInterfaces 1
|
||||||
#else
|
#elif SWITCH_PICO_HID_INSTANCE_COUNT == 2
|
||||||
0x49, 0x00, // wTotalLength 73
|
0x49, 0x00, // wTotalLength 73
|
||||||
0x02, // bNumInterfaces 2
|
0x02, // bNumInterfaces 2
|
||||||
|
#elif SWITCH_PICO_HID_INSTANCE_COUNT == 3
|
||||||
|
0x69, 0x00, // wTotalLength 105
|
||||||
|
0x03, // bNumInterfaces 3
|
||||||
|
#else
|
||||||
|
0x89, 0x00, // wTotalLength 137
|
||||||
|
0x04, // bNumInterfaces 4
|
||||||
#endif
|
#endif
|
||||||
0x01, // bConfigurationValue
|
0x01, // bConfigurationValue
|
||||||
0x00, // iConfiguration (String Index)
|
0x00, // iConfiguration (String Index)
|
||||||
|
|
@ -421,7 +427,7 @@ static const uint8_t switch_pro_configuration_descriptor[] =
|
||||||
0x40, 0x00, // wMaxPacketSize 64
|
0x40, 0x00, // wMaxPacketSize 64
|
||||||
0x08, // bInterval 8 (unit depends on device speed)
|
0x08, // bInterval 8 (unit depends on device speed)
|
||||||
|
|
||||||
#if SWITCH_PICO_HID_INSTANCE_COUNT == 2
|
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 2
|
||||||
0x09, // bLength
|
0x09, // bLength
|
||||||
0x04, // bDescriptorType (Interface)
|
0x04, // bDescriptorType (Interface)
|
||||||
0x01, // bInterfaceNumber 1
|
0x01, // bInterfaceNumber 1
|
||||||
|
|
@ -454,6 +460,74 @@ static const uint8_t switch_pro_configuration_descriptor[] =
|
||||||
0x40, 0x00, // wMaxPacketSize 64
|
0x40, 0x00, // wMaxPacketSize 64
|
||||||
0x08, // bInterval 8 (unit depends on device speed)
|
0x08, // bInterval 8 (unit depends on device speed)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 3
|
||||||
|
0x09, // bLength
|
||||||
|
0x04, // bDescriptorType (Interface)
|
||||||
|
0x02, // bInterfaceNumber 2
|
||||||
|
0x00, // bAlternateSetting
|
||||||
|
0x02, // bNumEndpoints 2
|
||||||
|
0x03, // bInterfaceClass
|
||||||
|
0x00, // bInterfaceSubClass
|
||||||
|
0x00, // bInterfaceProtocol
|
||||||
|
0x00, // iInterface (String Index)
|
||||||
|
|
||||||
|
0x09, // bLength
|
||||||
|
0x21, // bDescriptorType (HID)
|
||||||
|
0x11, 0x01, // bcdHID 1.11
|
||||||
|
0x00, // bCountryCode
|
||||||
|
0x01, // bNumDescriptors
|
||||||
|
0x22, // bDescriptorType[0] (HID)
|
||||||
|
0xCB, 0x00, // wDescriptorLength[0] 203
|
||||||
|
|
||||||
|
0x07, // bLength
|
||||||
|
0x05, // bDescriptorType (Endpoint)
|
||||||
|
0x83, // bEndpointAddress (IN/D2H)
|
||||||
|
0x03, // bmAttributes (Interrupt)
|
||||||
|
0x40, 0x00, // wMaxPacketSize 64
|
||||||
|
0x08, // bInterval 8 (unit depends on device speed)
|
||||||
|
|
||||||
|
0x07, // bLength
|
||||||
|
0x05, // bDescriptorType (Endpoint)
|
||||||
|
0x03, // bEndpointAddress (OUT/H2D)
|
||||||
|
0x03, // bmAttributes (Interrupt)
|
||||||
|
0x40, 0x00, // wMaxPacketSize 64
|
||||||
|
0x08, // bInterval 8 (unit depends on device speed)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if SWITCH_PICO_HID_INSTANCE_COUNT >= 4
|
||||||
|
0x09, // bLength
|
||||||
|
0x04, // bDescriptorType (Interface)
|
||||||
|
0x03, // bInterfaceNumber 3
|
||||||
|
0x00, // bAlternateSetting
|
||||||
|
0x02, // bNumEndpoints 2
|
||||||
|
0x03, // bInterfaceClass
|
||||||
|
0x00, // bInterfaceSubClass
|
||||||
|
0x00, // bInterfaceProtocol
|
||||||
|
0x00, // iInterface (String Index)
|
||||||
|
|
||||||
|
0x09, // bLength
|
||||||
|
0x21, // bDescriptorType (HID)
|
||||||
|
0x11, 0x01, // bcdHID 1.11
|
||||||
|
0x00, // bCountryCode
|
||||||
|
0x01, // bNumDescriptors
|
||||||
|
0x22, // bDescriptorType[0] (HID)
|
||||||
|
0xCB, 0x00, // wDescriptorLength[0] 203
|
||||||
|
|
||||||
|
0x07, // bLength
|
||||||
|
0x05, // bDescriptorType (Endpoint)
|
||||||
|
0x84, // bEndpointAddress (IN/D2H)
|
||||||
|
0x03, // bmAttributes (Interrupt)
|
||||||
|
0x40, 0x00, // wMaxPacketSize 64
|
||||||
|
0x08, // bInterval 8 (unit depends on device speed)
|
||||||
|
|
||||||
|
0x07, // bLength
|
||||||
|
0x05, // bDescriptorType (Endpoint)
|
||||||
|
0x04, // bEndpointAddress (OUT/H2D)
|
||||||
|
0x03, // bmAttributes (Interrupt)
|
||||||
|
0x40, 0x00, // wMaxPacketSize 64
|
||||||
|
0x08, // bInterval 8 (unit depends on device speed)
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
static const uint8_t switch_pro_report_descriptor[] =
|
static const uint8_t switch_pro_report_descriptor[] =
|
||||||
|
|
|
||||||
|
|
@ -97,10 +97,16 @@ void tick_backend_timer(int ticks) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_ready_order(int first_slot) {
|
void test_ready_order(bool reverse) {
|
||||||
start_backend();
|
start_backend();
|
||||||
uni_hid_device_t devices[2] = {device(0), device(1)};
|
uni_hid_device_t devices[kSlotCount] = {
|
||||||
const int second_slot = 1 - first_slot;
|
device(0), device(1), device(2), device(3)};
|
||||||
|
uni_hid_device_t replacements[kSlotCount] = {
|
||||||
|
device(0), device(1), device(2), device(3)};
|
||||||
|
const int forward[kSlotCount] = {0, 1, 2, 3};
|
||||||
|
const int backward[kSlotCount] = {3, 2, 1, 0};
|
||||||
|
const int* order = reverse ? backward : forward;
|
||||||
|
|
||||||
tick_backend_timer(99);
|
tick_backend_timer(99);
|
||||||
require(observed_status_led_on,
|
require(observed_status_led_on,
|
||||||
"scanning LED must stay on for the first slow-blink half-cycle");
|
"scanning LED must stay on for the first slow-blink half-cycle");
|
||||||
|
|
@ -108,7 +114,7 @@ void test_ready_order(int first_slot) {
|
||||||
require(!observed_status_led_on,
|
require(!observed_status_led_on,
|
||||||
"scanning LED must turn off at the slow-blink half-cycle");
|
"scanning LED must turn off at the slow-blink half-cycle");
|
||||||
|
|
||||||
platform_on_device_connected(&devices[first_slot]);
|
platform_on_device_connected(&devices[order[0]]);
|
||||||
tick_backend_timer(19);
|
tick_backend_timer(19);
|
||||||
require(observed_status_led_on,
|
require(observed_status_led_on,
|
||||||
"connecting LED must stay on for the first fast-blink half-cycle");
|
"connecting LED must stay on for the first fast-blink half-cycle");
|
||||||
|
|
@ -119,53 +125,83 @@ void test_ready_order(int first_slot) {
|
||||||
require(observed_status_led_on,
|
require(observed_status_led_on,
|
||||||
"connecting LED must turn on for the next fast-blink cycle");
|
"connecting LED must turn on for the next fast-blink cycle");
|
||||||
|
|
||||||
require(platform_on_device_ready(&devices[first_slot]) ==
|
for (int position = 0; position < kSlotCount; ++position) {
|
||||||
UNI_ERROR_SUCCESS,
|
const int slot = order[position];
|
||||||
"first ready device must bind to its Bluepad index");
|
require(platform_on_device_ready(&devices[slot]) == UNI_ERROR_SUCCESS,
|
||||||
require(scan_stops == 0,
|
"ready device must bind to its Bluepad index");
|
||||||
"scanning must continue while one slot remains free");
|
|
||||||
require(incoming_connections,
|
|
||||||
"incoming connections must remain enabled with one ready slot");
|
|
||||||
tick_backend_timer(99);
|
|
||||||
require(observed_status_led_on,
|
|
||||||
"one ready slot must leave the LED in the slow scanning cycle");
|
|
||||||
tick_backend_timer(1);
|
|
||||||
require(!observed_status_led_on,
|
|
||||||
"one open slot must produce the scanning LED off transition");
|
|
||||||
|
|
||||||
SwitchInputState first{};
|
for (int candidate = 0; candidate < kSlotCount; ++candidate) {
|
||||||
SwitchInputState second{};
|
SwitchInputState snapshot{};
|
||||||
require(bluepad32_input_backend_snapshot(first_slot, &first),
|
bool expected_active = false;
|
||||||
"first ready slot must be active");
|
for (int ready = 0; ready <= position; ++ready) {
|
||||||
require(!bluepad32_input_backend_snapshot(second_slot, &second),
|
expected_active = expected_active || order[ready] == candidate;
|
||||||
"other slot must remain independently inactive");
|
}
|
||||||
|
require(bluepad32_input_backend_snapshot(candidate, &snapshot) ==
|
||||||
|
expected_active,
|
||||||
|
"only ready indexed slots may become active");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position + 1 < kSlotCount) {
|
||||||
|
require(scan_stops == 0,
|
||||||
|
"scanning must continue while any slot remains free");
|
||||||
|
require(incoming_connections,
|
||||||
|
"incoming connections must remain enabled before all slots are ready");
|
||||||
|
if (position == 0) {
|
||||||
|
tick_backend_timer(99);
|
||||||
|
require(observed_status_led_on,
|
||||||
|
"a partially full backend must use the scanning LED on half-cycle");
|
||||||
|
tick_backend_timer(1);
|
||||||
|
require(!observed_status_led_on,
|
||||||
|
"a partially full backend must use the scanning LED off half-cycle");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
require(platform_on_device_ready(&devices[second_slot]) ==
|
|
||||||
UNI_ERROR_SUCCESS,
|
|
||||||
"second ready device must bind to its Bluepad index");
|
|
||||||
require(scan_stops == 1,
|
require(scan_stops == 1,
|
||||||
"scanning must stop exactly when both slots are ready");
|
"scanning must stop exactly when all four slots are ready");
|
||||||
require(!incoming_connections,
|
require(!incoming_connections,
|
||||||
"incoming connections must be disabled only when full");
|
"incoming connections must be disabled only when all slots are full");
|
||||||
tick_backend_timer(1);
|
tick_backend_timer(1);
|
||||||
require(observed_status_led_on,
|
require(observed_status_led_on,
|
||||||
"both ready slots must turn the status LED on");
|
"four ready slots must turn the status LED on");
|
||||||
const int ready_led_writes = observed_status_led_writes;
|
const int ready_led_writes = observed_status_led_writes;
|
||||||
tick_backend_timer(200);
|
tick_backend_timer(200);
|
||||||
require(observed_status_led_on &&
|
require(observed_status_led_on &&
|
||||||
observed_status_led_writes == ready_led_writes,
|
observed_status_led_writes == ready_led_writes,
|
||||||
"both ready slots must keep the status LED solid");
|
"four ready slots must keep the status LED solid");
|
||||||
|
|
||||||
|
for (int slot = 0; slot < kSlotCount; ++slot) {
|
||||||
|
const int starts_before_disconnect = scan_starts;
|
||||||
|
platform_on_device_disconnected(&devices[slot]);
|
||||||
|
require(scan_starts == starts_before_disconnect + 1 &&
|
||||||
|
scanning_enabled && incoming_connections,
|
||||||
|
"disconnecting any slot must resume connection policy");
|
||||||
|
|
||||||
|
for (int candidate = 0; candidate < kSlotCount; ++candidate) {
|
||||||
|
SwitchInputState snapshot{};
|
||||||
|
require(bluepad32_input_backend_snapshot(candidate, &snapshot) ==
|
||||||
|
(candidate != slot),
|
||||||
|
"disconnect must preserve every surviving slot");
|
||||||
|
}
|
||||||
|
|
||||||
|
require(platform_on_device_ready(&replacements[slot]) ==
|
||||||
|
UNI_ERROR_SUCCESS,
|
||||||
|
"replacement must bind to each freed indexed slot");
|
||||||
|
require(!scanning_enabled && !incoming_connections,
|
||||||
|
"restoring four ready slots must stop connection policy");
|
||||||
|
devices[slot] = replacements[slot];
|
||||||
|
}
|
||||||
|
|
||||||
bd_addr_t address{};
|
bd_addr_t address{};
|
||||||
require(platform_on_device_discovered(address, "extra", 0, 0) ==
|
require(platform_on_device_discovered(address, "extra", 0, 0) ==
|
||||||
UNI_ERROR_IGNORE_DEVICE,
|
UNI_ERROR_IGNORE_DEVICE,
|
||||||
"discovery must reject devices while both slots are occupied");
|
"discovery must reject devices while all four slots are occupied");
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_rejections() {
|
void test_rejections() {
|
||||||
start_backend();
|
start_backend();
|
||||||
uni_hid_device_t non_gamepad = device(0, false);
|
uni_hid_device_t non_gamepad = device(0, false);
|
||||||
uni_hid_device_t out_of_range = device(2);
|
uni_hid_device_t out_of_range = device(4);
|
||||||
uni_hid_device_t slot_zero = device(0);
|
uni_hid_device_t slot_zero = device(0);
|
||||||
uni_hid_device_t collision = device(0);
|
uni_hid_device_t collision = device(0);
|
||||||
|
|
||||||
|
|
@ -173,21 +209,39 @@ void test_rejections() {
|
||||||
UNI_ERROR_INVALID_CONTROLLER,
|
UNI_ERROR_INVALID_CONTROLLER,
|
||||||
"non-gamepad must be rejected");
|
"non-gamepad must be rejected");
|
||||||
require(platform_on_device_ready(&out_of_range) == UNI_ERROR_NO_SLOTS,
|
require(platform_on_device_ready(&out_of_range) == UNI_ERROR_NO_SLOTS,
|
||||||
"out-of-range Bluepad index must be rejected");
|
"Bluepad index 4 must be rejected");
|
||||||
require(platform_on_device_ready(&slot_zero) == UNI_ERROR_SUCCESS,
|
require(platform_on_device_ready(&slot_zero) == UNI_ERROR_SUCCESS,
|
||||||
"valid device must occupy its indexed slot");
|
"valid device must occupy its indexed slot");
|
||||||
require(platform_on_device_ready(&collision) == UNI_ERROR_NO_SLOTS,
|
require(platform_on_device_ready(&collision) == UNI_ERROR_NO_SLOTS,
|
||||||
"different device cannot replace an occupied slot");
|
"different device cannot replace an occupied slot");
|
||||||
|
|
||||||
uni_controller_t data{};
|
uni_controller_t collision_data{};
|
||||||
data.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
collision_data.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||||
data.gamepad.buttons = BUTTON_B;
|
collision_data.gamepad.buttons = BUTTON_B;
|
||||||
platform_on_controller_data(&collision, &data);
|
platform_on_controller_data(&collision, &collision_data);
|
||||||
SwitchInputState snapshot{};
|
SwitchInputState snapshot{};
|
||||||
require(bluepad32_input_backend_snapshot(0, &snapshot),
|
require(bluepad32_input_backend_snapshot(0, &snapshot),
|
||||||
"occupied slot must stay active");
|
"occupied slot must stay active");
|
||||||
require(!snapshot.button_a,
|
require(!snapshot.button_a,
|
||||||
"mismatched device input must not enter the occupied slot");
|
"mismatched device input must not enter the occupied slot");
|
||||||
|
|
||||||
|
uni_controller_t slot_zero_data{};
|
||||||
|
slot_zero_data.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||||
|
slot_zero_data.gamepad.accel[0] = 8192;
|
||||||
|
platform_on_controller_data(&slot_zero, &slot_zero_data);
|
||||||
|
require(bluepad32_input_backend_snapshot(0, &snapshot) &&
|
||||||
|
snapshot.imu_sample_count == 3,
|
||||||
|
"valid slot input must remain observable");
|
||||||
|
require(!bluepad32_input_backend_snapshot(4, &snapshot),
|
||||||
|
"public snapshot must reject slot 4");
|
||||||
|
bluepad32_input_backend_report_sent(4);
|
||||||
|
require(bluepad32_input_backend_snapshot(0, &snapshot) &&
|
||||||
|
snapshot.imu_sample_count == 3,
|
||||||
|
"slot 4 acknowledgement must not consume slot 0 IMU");
|
||||||
|
bluepad32_input_backend_queue_rumble(4, SwitchRumbleOutput{1, 2});
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(slot_zero.rumble_calls == 0,
|
||||||
|
"slot 4 rumble must not reach a valid controller");
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_independent_lifecycle() {
|
void test_independent_lifecycle() {
|
||||||
|
|
@ -222,13 +276,14 @@ void test_independent_lifecycle() {
|
||||||
require(!observed_status_led_on,
|
require(!observed_status_led_on,
|
||||||
"pre-ready disconnect must restore the slow LED off half-cycle");
|
"pre-ready disconnect must restore the slow LED off half-cycle");
|
||||||
|
|
||||||
uni_hid_device_t first = device(0);
|
uni_hid_device_t devices[kSlotCount] = {
|
||||||
uni_hid_device_t survivor = device(1);
|
device(0), device(1), device(2), device(3)};
|
||||||
platform_on_device_connected(&first);
|
for (int slot = 0; slot < kSlotCount; ++slot) {
|
||||||
platform_on_device_connected(&survivor);
|
platform_on_device_connected(&devices[slot]);
|
||||||
require(g_slots[0].device == &first && !g_slots[0].active &&
|
require(g_slots[slot].device == &devices[slot] &&
|
||||||
g_slots[1].device == &survivor && !g_slots[1].active,
|
!g_slots[slot].active,
|
||||||
"concurrent pending devices must retain independent identities");
|
"each pending device must retain its indexed identity");
|
||||||
|
}
|
||||||
tick_backend_timer(19);
|
tick_backend_timer(19);
|
||||||
require(observed_status_led_on,
|
require(observed_status_led_on,
|
||||||
"concurrent pending devices must use the fast LED on half-cycle");
|
"concurrent pending devices must use the fast LED on half-cycle");
|
||||||
|
|
@ -239,182 +294,246 @@ void test_independent_lifecycle() {
|
||||||
const uint32_t first_pending_generation =
|
const uint32_t first_pending_generation =
|
||||||
g_slots[0].connection_generation;
|
g_slots[0].connection_generation;
|
||||||
const int starts_before_first_pending_disconnect = scan_starts;
|
const int starts_before_first_pending_disconnect = scan_starts;
|
||||||
platform_on_device_disconnected(&first);
|
platform_on_device_disconnected(&devices[0]);
|
||||||
require(g_slots[0].device == nullptr && !g_slots[0].active &&
|
require(g_slots[0].device == nullptr && !g_slots[0].active,
|
||||||
g_slots[1].device == &survivor && !g_slots[1].active,
|
"pre-ready disconnect must clear only its own pending identity");
|
||||||
"pre-ready disconnect must preserve the other pending identity");
|
for (int slot = 1; slot < kSlotCount; ++slot) {
|
||||||
|
require(g_slots[slot].device == &devices[slot] &&
|
||||||
|
!g_slots[slot].active,
|
||||||
|
"pre-ready disconnect must preserve all pending survivors");
|
||||||
|
}
|
||||||
require(g_slots[0].connection_generation ==
|
require(g_slots[0].connection_generation ==
|
||||||
first_pending_generation + 1,
|
first_pending_generation + 1,
|
||||||
"pending disconnect beside a peer must invalidate its generation");
|
"pending disconnect beside peers must invalidate its generation");
|
||||||
require(g_connection_status == ConnectionStatus::Connecting &&
|
require(g_connection_status == ConnectionStatus::Connecting &&
|
||||||
scanning_enabled && incoming_connections &&
|
scanning_enabled && incoming_connections &&
|
||||||
scan_starts == starts_before_first_pending_disconnect + 1,
|
scan_starts == starts_before_first_pending_disconnect + 1,
|
||||||
"open slot must scan while another slot remains connecting");
|
"open slot must scan while other slots remain connecting");
|
||||||
tick_backend_timer(19);
|
tick_backend_timer(19);
|
||||||
require(observed_status_led_on,
|
require(observed_status_led_on,
|
||||||
"surviving pending device must retain the fast LED on half-cycle");
|
"surviving pending devices must retain the fast LED on half-cycle");
|
||||||
tick_backend_timer(1);
|
tick_backend_timer(1);
|
||||||
require(!observed_status_led_on,
|
require(!observed_status_led_on,
|
||||||
"surviving pending device must retain the fast LED off half-cycle");
|
"surviving pending devices must retain the fast LED off half-cycle");
|
||||||
|
|
||||||
require(platform_on_device_ready(&survivor) == UNI_ERROR_SUCCESS,
|
for (int slot = 1; slot < kSlotCount; ++slot) {
|
||||||
"surviving pending device must still become ready");
|
require(platform_on_device_ready(&devices[slot]) == UNI_ERROR_SUCCESS,
|
||||||
platform_on_device_connected(&first);
|
"each surviving pending device must still become ready");
|
||||||
require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS,
|
}
|
||||||
"reconnected slot 0 device must complete the pair");
|
platform_on_device_connected(&devices[0]);
|
||||||
|
require(platform_on_device_ready(&devices[0]) == UNI_ERROR_SUCCESS,
|
||||||
|
"reconnected slot 0 device must complete all four slots");
|
||||||
|
require(g_connection_status == ConnectionStatus::Ready &&
|
||||||
|
!scanning_enabled && !incoming_connections,
|
||||||
|
"four ready lifecycle devices must stop connection policy");
|
||||||
|
|
||||||
uni_controller_t data0{};
|
const uint32_t buttons[kSlotCount] = {
|
||||||
data0.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
BUTTON_B, BUTTON_A, BUTTON_X, BUTTON_Y};
|
||||||
data0.gamepad.buttons = BUTTON_B;
|
uni_controller_t data[kSlotCount]{};
|
||||||
data0.gamepad.accel[0] = 8192;
|
for (int slot = 0; slot < kSlotCount; ++slot) {
|
||||||
uni_controller_t data1{};
|
data[slot].klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||||
data1.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
data[slot].gamepad.buttons = buttons[slot];
|
||||||
data1.gamepad.buttons = BUTTON_A;
|
data[slot].gamepad.accel[slot % 3] = 8192 + slot;
|
||||||
data1.gamepad.gyro[1] = 1024;
|
data[slot].gamepad.gyro[(slot + 1) % 3] = 1024 + slot;
|
||||||
platform_on_controller_data(&first, &data0);
|
platform_on_controller_data(&devices[slot], &data[slot]);
|
||||||
platform_on_controller_data(&survivor, &data1);
|
}
|
||||||
|
|
||||||
SwitchInputState state0{};
|
SwitchInputState states[kSlotCount]{};
|
||||||
SwitchInputState state1{};
|
for (int slot = 0; slot < kSlotCount; ++slot) {
|
||||||
require(bluepad32_input_backend_snapshot(0, &state0) && state0.button_a &&
|
require(bluepad32_input_backend_snapshot(slot, &states[slot]) &&
|
||||||
state0.imu_sample_count == 3,
|
states[slot].imu_sample_count == 3,
|
||||||
"slot 0 input and IMU must map only to slot 0");
|
"every slot must expose independent input and IMU");
|
||||||
require(bluepad32_input_backend_snapshot(1, &state1) && state1.button_b &&
|
}
|
||||||
state1.imu_sample_count == 3,
|
require(states[0].button_a && !states[0].button_b &&
|
||||||
"slot 1 input and IMU must map only to slot 1");
|
!states[0].button_y && !states[0].button_x,
|
||||||
|
"slot 0 must contain only slot 0 input");
|
||||||
|
require(states[1].button_b && !states[1].button_a &&
|
||||||
|
!states[1].button_y && !states[1].button_x,
|
||||||
|
"slot 1 must contain only slot 1 input");
|
||||||
|
require(states[2].button_y && !states[2].button_a &&
|
||||||
|
!states[2].button_b && !states[2].button_x,
|
||||||
|
"slot 2 must contain only slot 2 input");
|
||||||
|
require(states[3].button_x && !states[3].button_a &&
|
||||||
|
!states[3].button_b && !states[3].button_y,
|
||||||
|
"slot 3 must contain only slot 3 input");
|
||||||
|
|
||||||
bluepad32_input_backend_report_sent(0);
|
bluepad32_input_backend_report_sent(3);
|
||||||
require(bluepad32_input_backend_snapshot(0, &state0) &&
|
for (int slot = 0; slot < kSlotCount; ++slot) {
|
||||||
state0.imu_sample_count == 0,
|
require(bluepad32_input_backend_snapshot(slot, &states[slot]) &&
|
||||||
"slot 0 report acknowledgement must consume only slot 0 IMU");
|
states[slot].imu_sample_count == (slot == 3 ? 0 : 3),
|
||||||
require(bluepad32_input_backend_snapshot(1, &state1) &&
|
"slot 3 acknowledgement must not consume slots 0-2 IMU");
|
||||||
state1.imu_sample_count == 3,
|
}
|
||||||
"slot 0 acknowledgement must not consume slot 1 IMU");
|
for (int slot = 0; slot < 3; ++slot) {
|
||||||
|
bluepad32_input_backend_report_sent(slot);
|
||||||
|
require(bluepad32_input_backend_snapshot(slot, &states[slot]) &&
|
||||||
|
states[slot].imu_sample_count == 0,
|
||||||
|
"each slot acknowledgement must consume only its own IMU");
|
||||||
|
}
|
||||||
|
|
||||||
const SwitchRumbleOutput rumble0{11, 22};
|
const SwitchRumbleOutput initial_rumble[kSlotCount] = {
|
||||||
const SwitchRumbleOutput rumble1{33, 44};
|
{11, 21}, {12, 22}, {13, 23}, {14, 24}};
|
||||||
bluepad32_input_backend_queue_rumble(0, rumble0);
|
for (int slot = 0; slot < kSlotCount; ++slot) {
|
||||||
bluepad32_input_backend_queue_rumble(1, rumble1);
|
bluepad32_input_backend_queue_rumble(slot, initial_rumble[slot]);
|
||||||
|
}
|
||||||
process_rumble_timer(&g_rumble_timer);
|
process_rumble_timer(&g_rumble_timer);
|
||||||
require(first.rumble_calls == 1 && first.last_low == 11 &&
|
for (int slot = 0; slot < kSlotCount; ++slot) {
|
||||||
first.last_high == 22,
|
require(devices[slot].rumble_calls == 1 &&
|
||||||
"slot 0 rumble must reach only controller 0");
|
devices[slot].last_low == 11 + slot &&
|
||||||
require(survivor.rumble_calls == 1 && survivor.last_low == 33 &&
|
devices[slot].last_high == 21 + slot,
|
||||||
survivor.last_high == 44,
|
"each slot rumble must reach only its indexed controller");
|
||||||
"slot 1 rumble must reach only controller 1");
|
}
|
||||||
|
|
||||||
bluepad32_input_backend_queue_rumble(0, SwitchRumbleOutput{55, 66});
|
bluepad32_input_backend_queue_rumble(3, SwitchRumbleOutput{55, 66});
|
||||||
const uint32_t disconnected_generation =
|
const uint32_t disconnected_generation =
|
||||||
g_slots[0].connection_generation;
|
g_slots[3].connection_generation;
|
||||||
const int starts_before_disconnect = scan_starts;
|
const int starts_before_slot_three_disconnect = scan_starts;
|
||||||
platform_on_device_disconnected(&first);
|
platform_on_device_disconnected(&devices[3]);
|
||||||
require(scan_starts == starts_before_disconnect + 1 &&
|
require(scan_starts == starts_before_slot_three_disconnect + 1 &&
|
||||||
incoming_connections,
|
scanning_enabled && incoming_connections,
|
||||||
"disconnect must resume scanning and incoming connections");
|
"slot 3 disconnect must resume scanning and incoming connections");
|
||||||
require(!bluepad32_input_backend_snapshot(0, &state0) &&
|
require(!bluepad32_input_backend_snapshot(3, &states[3]) &&
|
||||||
!state0.button_a && state0.lx == 32768,
|
!states[3].button_x && states[3].lx == 32768,
|
||||||
"disconnect must neutralize only its own slot");
|
"slot 3 disconnect must neutralize only slot 3");
|
||||||
require(bluepad32_input_backend_snapshot(1, &state1) && state1.button_b,
|
require(bluepad32_input_backend_snapshot(0, &states[0]) &&
|
||||||
"disconnect must preserve survivor state and activity");
|
states[0].button_a &&
|
||||||
|
bluepad32_input_backend_snapshot(1, &states[1]) &&
|
||||||
uni_hid_device_t replacement = device(0);
|
states[1].button_b &&
|
||||||
require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS,
|
bluepad32_input_backend_snapshot(2, &states[2]) &&
|
||||||
"replacement must bind to the freed indexed slot");
|
states[2].button_y,
|
||||||
process_rumble_timer(&g_rumble_timer);
|
"slot 3 disconnect must preserve slots 0-2");
|
||||||
require(replacement.rumble_calls == 0,
|
platform_on_controller_data(&devices[0], &data[0]);
|
||||||
"replacement must not receive disconnected device rumble");
|
require(bluepad32_input_backend_snapshot(0, &states[0]) &&
|
||||||
|
states[0].button_a,
|
||||||
g_slots[0].pending_rumble = {
|
"slot 0 input must continue while slot 3 is disconnected");
|
||||||
0, disconnected_generation, SwitchRumbleOutput{77, 88}};
|
const int slot_zero_calls_while_scanning = devices[0].rumble_calls;
|
||||||
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");
|
|
||||||
|
|
||||||
const int starts_before_slot_one_disconnect = scan_starts;
|
|
||||||
platform_on_device_disconnected(&survivor);
|
|
||||||
require(scan_starts == starts_before_slot_one_disconnect + 1 &&
|
|
||||||
incoming_connections,
|
|
||||||
"slot 1 disconnect must resume scanning for its open slot");
|
|
||||||
require(bluepad32_input_backend_snapshot(0, &state0) && state0.button_x,
|
|
||||||
"slot 1 disconnect must preserve slot 0 state and activity");
|
|
||||||
require(!bluepad32_input_backend_snapshot(1, &state1) &&
|
|
||||||
!state1.button_b && state1.lx == 32768,
|
|
||||||
"slot 1 disconnect must neutralize only slot 1");
|
|
||||||
|
|
||||||
uni_controller_t continuing_slot_zero_data{};
|
|
||||||
continuing_slot_zero_data.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
|
||||||
continuing_slot_zero_data.gamepad.buttons = BUTTON_B;
|
|
||||||
platform_on_controller_data(&replacement, &continuing_slot_zero_data);
|
|
||||||
require(bluepad32_input_backend_snapshot(0, &state0) && state0.button_a,
|
|
||||||
"slot 0 input must continue while slot 1 is disconnected");
|
|
||||||
|
|
||||||
const int slot_zero_calls_while_scanning = replacement.rumble_calls;
|
|
||||||
bluepad32_input_backend_queue_rumble(0, SwitchRumbleOutput{115, 116});
|
bluepad32_input_backend_queue_rumble(0, SwitchRumbleOutput{115, 116});
|
||||||
tick_backend_timer(99);
|
tick_backend_timer(99);
|
||||||
require(replacement.rumble_calls == slot_zero_calls_while_scanning + 1 &&
|
require(devices[0].rumble_calls == slot_zero_calls_while_scanning + 1 &&
|
||||||
replacement.last_low == 115 && replacement.last_high == 116,
|
devices[0].last_low == 115 &&
|
||||||
"slot 0 rumble must continue while slot 1 is disconnected");
|
devices[0].last_high == 116,
|
||||||
|
"slot 0 rumble must continue while slot 3 is disconnected");
|
||||||
require(observed_status_led_on,
|
require(observed_status_led_on,
|
||||||
"disconnect scanning must use the slow LED on half-cycle");
|
"disconnect scanning must use the slow LED on half-cycle");
|
||||||
tick_backend_timer(1);
|
tick_backend_timer(1);
|
||||||
require(!observed_status_led_on,
|
require(!observed_status_led_on,
|
||||||
"disconnect scanning must reach the slow LED off half-cycle");
|
"disconnect scanning must reach the slow LED off half-cycle");
|
||||||
|
|
||||||
uni_hid_device_t first_slot_one_replacement = device(1);
|
uni_hid_device_t slot_three_replacement = device(3);
|
||||||
require(platform_on_device_ready(&first_slot_one_replacement) ==
|
require(platform_on_device_ready(&slot_three_replacement) ==
|
||||||
UNI_ERROR_SUCCESS,
|
UNI_ERROR_SUCCESS,
|
||||||
"slot 1 replacement must bind without disturbing slot 0");
|
"slot 3 replacement must bind to the freed indexed slot");
|
||||||
tick_backend_timer(1);
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(slot_three_replacement.rumble_calls == 0,
|
||||||
|
"slot 3 replacement must not receive disconnected device rumble");
|
||||||
require(observed_status_led_on,
|
require(observed_status_led_on,
|
||||||
"replacing the open slot must return the LED to solid ready");
|
"slot 3 replacement must restore the solid ready LED");
|
||||||
const int replacement_ready_led_writes = observed_status_led_writes;
|
const int replacement_ready_led_writes = observed_status_led_writes;
|
||||||
tick_backend_timer(100);
|
tick_backend_timer(100);
|
||||||
require(observed_status_led_on &&
|
require(observed_status_led_on &&
|
||||||
observed_status_led_writes == replacement_ready_led_writes,
|
observed_status_led_writes == replacement_ready_led_writes,
|
||||||
"replacement pair must keep the ready LED solid");
|
"replacement quartet must keep the ready LED solid");
|
||||||
|
|
||||||
bluepad32_input_backend_queue_rumble(1, SwitchRumbleOutput{117, 118});
|
g_slots[3].pending_rumble = {
|
||||||
platform_on_device_disconnected(&first_slot_one_replacement);
|
3, disconnected_generation, SwitchRumbleOutput{77, 88}};
|
||||||
uni_hid_device_t second_slot_one_replacement = device(1);
|
g_slots[3].rumble_pending = true;
|
||||||
require(platform_on_device_ready(&second_slot_one_replacement) ==
|
|
||||||
UNI_ERROR_SUCCESS,
|
|
||||||
"a subsequent slot 1 replacement must bind to the freed slot");
|
|
||||||
process_rumble_timer(&g_rumble_timer);
|
process_rumble_timer(&g_rumble_timer);
|
||||||
require(second_slot_one_replacement.rumble_calls == 0,
|
require(slot_three_replacement.rumble_calls == 0,
|
||||||
"slot 1 replacement must not receive prior-generation rumble");
|
"stale slot 3 connection generation must be rejected");
|
||||||
|
|
||||||
const int slot_zero_calls_before_mailboxes = replacement.rumble_calls;
|
uni_controller_t replacement_data{};
|
||||||
bluepad32_input_backend_queue_rumble(1, SwitchRumbleOutput{119, 120});
|
replacement_data.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||||
bluepad32_input_backend_queue_rumble(1, SwitchRumbleOutput{121, 122});
|
replacement_data.gamepad.buttons = BUTTON_Y;
|
||||||
|
replacement_data.gamepad.accel[0] = 9000;
|
||||||
|
platform_on_controller_data(&slot_three_replacement, &replacement_data);
|
||||||
|
require(bluepad32_input_backend_snapshot(3, &states[3]) &&
|
||||||
|
states[3].button_x && states[3].imu_sample_count == 3,
|
||||||
|
"replacement input and IMU must populate only slot 3");
|
||||||
|
require(bluepad32_input_backend_snapshot(0, &states[0]) &&
|
||||||
|
states[0].button_a &&
|
||||||
|
bluepad32_input_backend_snapshot(1, &states[1]) &&
|
||||||
|
states[1].button_b &&
|
||||||
|
bluepad32_input_backend_snapshot(2, &states[2]) &&
|
||||||
|
states[2].button_y,
|
||||||
|
"slot 3 replacement must not disturb slots 0-2");
|
||||||
|
|
||||||
|
const int survivor_calls[kSlotCount - 1] = {
|
||||||
|
devices[0].rumble_calls,
|
||||||
|
devices[1].rumble_calls,
|
||||||
|
devices[2].rumble_calls};
|
||||||
|
bluepad32_input_backend_queue_rumble(3, SwitchRumbleOutput{90, 91});
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
require(slot_three_replacement.rumble_calls == 1 &&
|
||||||
|
slot_three_replacement.last_low == 90 &&
|
||||||
|
slot_three_replacement.last_high == 91,
|
||||||
|
"new-generation slot 3 rumble must reach its replacement");
|
||||||
|
for (int slot = 0; slot < 3; ++slot) {
|
||||||
|
require(devices[slot].rumble_calls == survivor_calls[slot],
|
||||||
|
"slot 3 rumble must not affect slots 0-2");
|
||||||
|
}
|
||||||
|
const int all_slot_calls[kSlotCount] = {
|
||||||
|
devices[0].rumble_calls,
|
||||||
|
devices[1].rumble_calls,
|
||||||
|
devices[2].rumble_calls,
|
||||||
|
slot_three_replacement.rumble_calls};
|
||||||
|
for (int slot = 0; slot < kSlotCount; ++slot) {
|
||||||
|
bluepad32_input_backend_queue_rumble(
|
||||||
|
slot, SwitchRumbleOutput{static_cast<uint8_t>(100 + slot),
|
||||||
|
static_cast<uint8_t>(110 + slot)});
|
||||||
|
}
|
||||||
|
process_rumble_timer(&g_rumble_timer);
|
||||||
|
for (int slot = 0; slot < kSlotCount; ++slot) {
|
||||||
|
const uni_hid_device_t& target =
|
||||||
|
slot == 3 ? slot_three_replacement : devices[slot];
|
||||||
|
require(target.rumble_calls == all_slot_calls[slot] + 1 &&
|
||||||
|
target.last_low == 100 + slot &&
|
||||||
|
target.last_high == 110 + slot,
|
||||||
|
"survivor rumble must continue after slot 3 replacement");
|
||||||
|
}
|
||||||
|
|
||||||
|
uni_hid_device_t replacements[kSlotCount] = {
|
||||||
|
device(0), device(1), device(2), device(3)};
|
||||||
|
for (int slot = 0; slot < 3; ++slot) {
|
||||||
|
const int starts_before_disconnect = scan_starts;
|
||||||
|
platform_on_device_disconnected(&devices[slot]);
|
||||||
|
require(scan_starts == starts_before_disconnect + 1 &&
|
||||||
|
scanning_enabled && incoming_connections,
|
||||||
|
"disconnecting slots 0-2 must resume connection policy");
|
||||||
|
require(!bluepad32_input_backend_snapshot(slot, &states[slot]) &&
|
||||||
|
states[slot].lx == 32768,
|
||||||
|
"disconnect must neutralize its indexed slot");
|
||||||
|
for (int survivor = 0; survivor < kSlotCount; ++survivor) {
|
||||||
|
if (survivor == slot) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
require(bluepad32_input_backend_snapshot(survivor,
|
||||||
|
&states[survivor]),
|
||||||
|
"disconnect must preserve all three survivors");
|
||||||
|
}
|
||||||
|
require(platform_on_device_ready(&replacements[slot]) ==
|
||||||
|
UNI_ERROR_SUCCESS,
|
||||||
|
"replacement must bind to each freed slot");
|
||||||
|
require(g_connection_status == ConnectionStatus::Ready &&
|
||||||
|
!scanning_enabled && !incoming_connections,
|
||||||
|
"replacement must restore the full four-slot policy");
|
||||||
|
}
|
||||||
|
|
||||||
|
const int slot_zero_calls_before_mailboxes = replacements[0].rumble_calls;
|
||||||
|
const int slot_three_calls_before_mailboxes =
|
||||||
|
slot_three_replacement.rumble_calls;
|
||||||
|
bluepad32_input_backend_queue_rumble(3, SwitchRumbleOutput{119, 120});
|
||||||
|
bluepad32_input_backend_queue_rumble(3, SwitchRumbleOutput{121, 122});
|
||||||
bluepad32_input_backend_queue_rumble(0, SwitchRumbleOutput{123, 124});
|
bluepad32_input_backend_queue_rumble(0, SwitchRumbleOutput{123, 124});
|
||||||
process_rumble_timer(&g_rumble_timer);
|
process_rumble_timer(&g_rumble_timer);
|
||||||
require(second_slot_one_replacement.rumble_calls == 1 &&
|
require(slot_three_replacement.rumble_calls ==
|
||||||
second_slot_one_replacement.last_low == 121 &&
|
slot_three_calls_before_mailboxes + 1 &&
|
||||||
second_slot_one_replacement.last_high == 122,
|
slot_three_replacement.last_low == 121 &&
|
||||||
"slot 1 mailbox must dispatch only its latest queued value");
|
slot_three_replacement.last_high == 122,
|
||||||
require(replacement.rumble_calls == slot_zero_calls_before_mailboxes + 1 &&
|
"slot 3 mailbox must dispatch only its latest queued value");
|
||||||
replacement.last_low == 123 && replacement.last_high == 124,
|
require(replacements[0].rumble_calls ==
|
||||||
"slot 0 activity must not evict the slot 1 mailbox");
|
slot_zero_calls_before_mailboxes + 1 &&
|
||||||
|
replacements[0].last_low == 123 &&
|
||||||
|
replacements[0].last_high == 124,
|
||||||
|
"slot 0 activity must not evict the slot 3 mailbox");
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
@ -422,10 +541,10 @@ void test_independent_lifecycle() {
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
require(argc == 2, "scenario argument required");
|
require(argc == 2, "scenario argument required");
|
||||||
const std::string scenario = argv[1];
|
const std::string scenario = argv[1];
|
||||||
if (scenario == "ready-0-1") {
|
if (scenario == "ready-forward") {
|
||||||
test_ready_order(0);
|
test_ready_order(false);
|
||||||
} else if (scenario == "ready-1-0") {
|
} else if (scenario == "ready-reverse") {
|
||||||
test_ready_order(1);
|
test_ready_order(true);
|
||||||
} else if (scenario == "rejections") {
|
} else if (scenario == "rejections") {
|
||||||
test_rejections();
|
test_rejections();
|
||||||
} else if (scenario == "lifecycle") {
|
} else if (scenario == "lifecycle") {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,16 @@ constexpr uint8_t kInterfaceDescriptor = 0x04;
|
||||||
constexpr uint8_t kEndpointDescriptor = 0x05;
|
constexpr uint8_t kEndpointDescriptor = 0x05;
|
||||||
constexpr uint8_t kHidDescriptor = 0x21;
|
constexpr uint8_t kHidDescriptor = 0x21;
|
||||||
|
|
||||||
|
#if EXPECTED_HID_INSTANCE_COUNT == 1
|
||||||
|
constexpr std::array<uint8_t, 41> kUartConfigurationDescriptor = {
|
||||||
|
0x09, 0x02, 0x29, 0x00, 0x01, 0x01, 0x00, 0xA0, 0xFA,
|
||||||
|
0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00,
|
||||||
|
0x09, 0x21, 0x11, 0x01, 0x00, 0x01, 0x22, 0xCB, 0x00,
|
||||||
|
0x07, 0x05, 0x81, 0x03, 0x40, 0x00, 0x08,
|
||||||
|
0x07, 0x05, 0x01, 0x03, 0x40, 0x00, 0x08,
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
int failures = 0;
|
int failures = 0;
|
||||||
|
|
||||||
void expect(bool condition, const char* message) {
|
void expect(bool condition, const char* message) {
|
||||||
|
|
@ -52,6 +62,11 @@ void inspect_configuration_descriptor() {
|
||||||
const auto* descriptor = switch_pro_configuration_descriptor;
|
const auto* descriptor = switch_pro_configuration_descriptor;
|
||||||
constexpr size_t descriptor_size =
|
constexpr size_t descriptor_size =
|
||||||
sizeof(switch_pro_configuration_descriptor);
|
sizeof(switch_pro_configuration_descriptor);
|
||||||
|
#if EXPECTED_HID_INSTANCE_COUNT == 1
|
||||||
|
expect(std::memcmp(descriptor, kUartConfigurationDescriptor.data(),
|
||||||
|
descriptor_size) == 0,
|
||||||
|
"UART configuration descriptor bytes changed");
|
||||||
|
#endif
|
||||||
|
|
||||||
expect(descriptor[0] == 9 && descriptor[1] == kConfigurationDescriptor,
|
expect(descriptor[0] == 9 && descriptor[1] == kConfigurationDescriptor,
|
||||||
"configuration header is malformed");
|
"configuration header is malformed");
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,11 @@
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
constexpr uint8_t kInstanceCount = SWITCH_PICO_HID_INSTANCE_COUNT;
|
||||||
|
constexpr uint8_t kInvalidInstance = kInstanceCount;
|
||||||
|
static_assert(kInstanceCount == 4,
|
||||||
|
"the native driver harness must exercise four HID instances");
|
||||||
|
|
||||||
|
|
||||||
struct SentReport {
|
struct SentReport {
|
||||||
uint8_t instance = 0;
|
uint8_t instance = 0;
|
||||||
|
|
@ -24,13 +29,12 @@ struct RumbleEvent {
|
||||||
|
|
||||||
uint64_t now_ms = 0;
|
uint64_t now_ms = 0;
|
||||||
uint32_t random_value = 1;
|
uint32_t random_value = 1;
|
||||||
bool hid_ready[SWITCH_PICO_HID_INSTANCE_COUNT] = {true, true};
|
std::array<bool, kInstanceCount> hid_ready{};
|
||||||
bool hid_report_succeeds[SWITCH_PICO_HID_INSTANCE_COUNT] = {true, true};
|
std::array<bool, kInstanceCount> hid_report_succeeds{};
|
||||||
std::array<unsigned, SWITCH_PICO_HID_INSTANCE_COUNT> hid_report_attempts{};
|
std::array<unsigned, kInstanceCount> hid_report_attempts{};
|
||||||
std::array<SentReport, 32> sent_reports{};
|
std::array<SentReport, 32> sent_reports{};
|
||||||
unsigned sent_report_count = 0;
|
unsigned sent_report_count = 0;
|
||||||
RumbleEvent rumble_zero{};
|
std::array<RumbleEvent, kInstanceCount> rumble_events{};
|
||||||
RumbleEvent rumble_one{};
|
|
||||||
int failures = 0;
|
int failures = 0;
|
||||||
|
|
||||||
void expect(bool condition, const char* message) {
|
void expect(bool condition, const char* message) {
|
||||||
|
|
@ -47,13 +51,12 @@ void clear_sent_reports() {
|
||||||
|
|
||||||
void initialize_contexts() {
|
void initialize_contexts() {
|
||||||
now_ms = 0;
|
now_ms = 0;
|
||||||
hid_ready[0] = true;
|
|
||||||
hid_ready[1] = true;
|
|
||||||
hid_report_succeeds[0] = true;
|
|
||||||
hid_report_succeeds[1] = true;
|
|
||||||
hid_report_attempts = {};
|
hid_report_attempts = {};
|
||||||
switch_pro_init(0);
|
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||||
switch_pro_init(1);
|
hid_ready[instance] = true;
|
||||||
|
hid_report_succeeds[instance] = true;
|
||||||
|
switch_pro_init(instance);
|
||||||
|
}
|
||||||
clear_sent_reports();
|
clear_sent_reports();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,40 +181,34 @@ std::array<uint8_t, 10> complete_rumble_report(
|
||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
|
|
||||||
void rumble_callback_zero(uint8_t instance,
|
void rumble_callback(uint8_t instance, const SwitchRumbleOutput& output) {
|
||||||
const SwitchRumbleOutput& output) {
|
expect(instance < rumble_events.size(),
|
||||||
++rumble_zero.count;
|
"rumble callback received an invalid instance");
|
||||||
rumble_zero.instance = instance;
|
if (instance >= rumble_events.size()) {
|
||||||
rumble_zero.output = output;
|
return;
|
||||||
}
|
}
|
||||||
|
RumbleEvent& event = rumble_events[instance];
|
||||||
void rumble_callback_one(uint8_t instance,
|
++event.count;
|
||||||
const SwitchRumbleOutput& output) {
|
event.instance = instance;
|
||||||
++rumble_one.count;
|
event.output = output;
|
||||||
rumble_one.instance = instance;
|
|
||||||
rumble_one.output = output;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_reset_materializes_neutral_sticks() {
|
void test_reset_materializes_neutral_sticks() {
|
||||||
initialize_contexts();
|
initialize_contexts();
|
||||||
SwitchProReport init_zero =
|
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||||
get_current_report(0, "GET_REPORT failed immediately after init");
|
SwitchProReport initialized = get_current_report(
|
||||||
SwitchProReport init_one =
|
instance, "GET_REPORT failed immediately after init");
|
||||||
get_current_report(1, "GET_REPORT failed for second initialized context");
|
expect_neutral_sticks(
|
||||||
expect_neutral_sticks(
|
initialized, "instance sticks were not neutral after init");
|
||||||
init_zero, "instance 0 sticks were not neutral immediately after init");
|
}
|
||||||
expect_neutral_sticks(
|
|
||||||
init_one, "instance 1 sticks were not neutral immediately after init");
|
|
||||||
|
|
||||||
tud_mount_cb();
|
tud_mount_cb();
|
||||||
SwitchProReport mount_zero =
|
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||||
get_current_report(0, "GET_REPORT failed immediately after mount");
|
SwitchProReport mounted = get_current_report(
|
||||||
SwitchProReport mount_one =
|
instance, "GET_REPORT failed immediately after mount");
|
||||||
get_current_report(1, "GET_REPORT failed for second mounted context");
|
expect_neutral_sticks(
|
||||||
expect_neutral_sticks(
|
mounted, "instance sticks were not neutral after mount");
|
||||||
mount_zero, "instance 0 sticks were not neutral immediately after mount");
|
}
|
||||||
expect_neutral_sticks(
|
|
||||||
mount_one, "instance 1 sticks were not neutral immediately after mount");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_startup_identify_preserves_first_reply_counter() {
|
void test_startup_identify_preserves_first_reply_counter() {
|
||||||
|
|
@ -259,51 +256,81 @@ void test_failed_startup_identify_retries_preserve_counter() {
|
||||||
|
|
||||||
void test_input_reports_and_timers_are_isolated() {
|
void test_input_reports_and_timers_are_isolated() {
|
||||||
initialize_contexts();
|
initialize_contexts();
|
||||||
SwitchInputState zero{};
|
std::array<SwitchInputState, kInstanceCount> states{};
|
||||||
zero.lx = 0x1111;
|
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||||
zero.ly = 0x2222;
|
SwitchInputState& state = states[instance];
|
||||||
zero.rx = 0x3333;
|
state.lx = static_cast<uint16_t>(0x1111u * (instance + 1u));
|
||||||
zero.ry = 0x4444;
|
state.ly = static_cast<uint16_t>(0x2222u + 0x1111u * instance);
|
||||||
zero.button_a = true;
|
state.rx = static_cast<uint16_t>(0x5555u + 0x1111u * instance);
|
||||||
SwitchInputState one{};
|
state.ry = static_cast<uint16_t>(0x8888u + 0x1111u * instance);
|
||||||
one.lx = 0xaaaa;
|
}
|
||||||
one.ly = 0xbbbb;
|
states[0].button_a = true;
|
||||||
one.rx = 0xcccc;
|
states[1].button_b = true;
|
||||||
one.ry = 0xdddd;
|
states[2].button_x = true;
|
||||||
one.button_b = true;
|
states[3].button_y = true;
|
||||||
one.dpad_right = true;
|
|
||||||
switch_pro_set_input(0, zero);
|
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||||
switch_pro_set_input(1, one);
|
switch_pro_set_input(instance, states[instance]);
|
||||||
|
}
|
||||||
|
|
||||||
now_ms = 15;
|
now_ms = 15;
|
||||||
expect(switch_pro_task(0), "instance 0 did not send its timed report");
|
std::array<SwitchProReport, kInstanceCount> sent{};
|
||||||
expect(switch_pro_task(1), "instance 1 timer was changed by instance 0");
|
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||||
const SentReport* sent_zero = latest_regular_report(0);
|
expect(switch_pro_task(instance),
|
||||||
const SentReport* sent_one = latest_regular_report(1);
|
"configured instance did not send its timed report");
|
||||||
expect(sent_zero != nullptr, "instance 0 report used the wrong HID route");
|
const SentReport* routed = latest_regular_report(instance);
|
||||||
expect(sent_one != nullptr, "instance 1 report used the wrong HID route");
|
expect(routed != nullptr, "input report used the wrong HID route");
|
||||||
SwitchProReport report_zero = copy_switch_report(sent_zero);
|
sent[instance] = copy_switch_report(routed);
|
||||||
SwitchProReport report_one = copy_switch_report(sent_one);
|
expect(sent[instance].inputs.buttonA == (instance == 0) &&
|
||||||
expect(report_zero.inputs.buttonA && !report_zero.inputs.buttonB,
|
sent[instance].inputs.buttonB == (instance == 1) &&
|
||||||
"instance 0 button state crossed with instance 1");
|
sent[instance].inputs.buttonX == (instance == 2) &&
|
||||||
expect(report_one.inputs.buttonB && !report_one.inputs.buttonA,
|
sent[instance].inputs.buttonY == (instance == 3),
|
||||||
"instance 1 button state crossed with instance 0");
|
"button state crossed HID instances");
|
||||||
expect(report_zero.inputs.leftStick.getX() !=
|
}
|
||||||
report_one.inputs.leftStick.getX(),
|
|
||||||
"instance stick reports were merged");
|
|
||||||
|
|
||||||
std::array<uint8_t, SWITCH_PRO_ENDPOINT_SIZE> get_zero{};
|
std::array<std::array<uint8_t, SWITCH_PRO_ENDPOINT_SIZE>, kInstanceCount>
|
||||||
std::array<uint8_t, SWITCH_PRO_ENDPOINT_SIZE> get_one{};
|
current{};
|
||||||
expect(tud_hid_get_report_cb(0, 0, HID_REPORT_TYPE_INPUT,
|
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||||
get_zero.data(), get_zero.size()) ==
|
expect(tud_hid_get_report_cb(instance, 0, HID_REPORT_TYPE_INPUT,
|
||||||
sizeof(SwitchProReport),
|
current[instance].data(),
|
||||||
"GET_REPORT rejected valid instance 0");
|
current[instance].size()) ==
|
||||||
expect(tud_hid_get_report_cb(1, 0, HID_REPORT_TYPE_INPUT,
|
sizeof(SwitchProReport),
|
||||||
get_one.data(), get_one.size()) ==
|
"GET_REPORT rejected a configured instance");
|
||||||
sizeof(SwitchProReport),
|
}
|
||||||
"GET_REPORT rejected valid instance 1");
|
for (uint8_t left = 0; left < kInstanceCount; ++left) {
|
||||||
expect(std::memcmp(get_zero.data(), get_one.data(), get_zero.size()) != 0,
|
for (uint8_t right = static_cast<uint8_t>(left + 1u);
|
||||||
"GET_REPORT returned a shared report for both instances");
|
right < kInstanceCount; ++right) {
|
||||||
|
expect(std::memcmp(current[left].data(), current[right].data(),
|
||||||
|
current[left].size()) != 0,
|
||||||
|
"GET_REPORT returned shared state across HID instances");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitchInputState changed_zero = states[0];
|
||||||
|
changed_zero.button_a = false;
|
||||||
|
changed_zero.button_home = true;
|
||||||
|
switch_pro_set_input(0, changed_zero);
|
||||||
|
now_ms = 30;
|
||||||
|
expect(switch_pro_task(0),
|
||||||
|
"instance 0 did not apply its changed input state");
|
||||||
|
SwitchProReport unchanged_three = get_current_report(
|
||||||
|
3, "GET_REPORT failed for instance 3 after instance 0 changed");
|
||||||
|
expect(unchanged_three.inputs.buttonY &&
|
||||||
|
!unchanged_three.inputs.buttonHome,
|
||||||
|
"instance 0 input change leaked into instance 3");
|
||||||
|
|
||||||
|
SwitchInputState changed_three = states[3];
|
||||||
|
changed_three.button_y = false;
|
||||||
|
changed_three.button_capture = true;
|
||||||
|
switch_pro_set_input(3, changed_three);
|
||||||
|
now_ms = 45;
|
||||||
|
expect(switch_pro_task(3),
|
||||||
|
"instance 3 did not apply its changed input state");
|
||||||
|
SwitchProReport unchanged_zero = get_current_report(
|
||||||
|
0, "GET_REPORT failed for instance 0 after instance 3 changed");
|
||||||
|
expect(unchanged_zero.inputs.buttonHome &&
|
||||||
|
!unchanged_zero.inputs.buttonCapture,
|
||||||
|
"instance 3 input change leaked into instance 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_callback_send_and_imu_modes_are_isolated() {
|
void test_callback_send_and_imu_modes_are_isolated() {
|
||||||
|
|
@ -381,75 +408,108 @@ void test_callback_send_and_imu_modes_are_isolated() {
|
||||||
|
|
||||||
void test_rumble_callbacks_and_decoders_are_isolated() {
|
void test_rumble_callbacks_and_decoders_are_isolated() {
|
||||||
initialize_contexts();
|
initialize_contexts();
|
||||||
rumble_zero = {};
|
rumble_events = {};
|
||||||
rumble_one = {};
|
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||||
switch_pro_set_rumble_callback(0, rumble_callback_zero);
|
switch_pro_set_rumble_callback(instance, rumble_callback);
|
||||||
switch_pro_set_rumble_callback(1, rumble_callback_one);
|
}
|
||||||
constexpr uint32_t neutral = 0x40400100u;
|
constexpr uint32_t neutral = 0x40400100u;
|
||||||
auto full_payload = rumble_payload(type_2(64, 16, 64, 16), neutral);
|
auto full_payload = rumble_payload(type_2(64, 16, 64, 16), neutral);
|
||||||
auto full_report = complete_rumble_report(full_payload);
|
auto full_report = complete_rumble_report(full_payload);
|
||||||
tud_hid_report_received_cb(2, 0, full_report.data(), full_report.size());
|
tud_hid_report_received_cb(kInvalidInstance, 0, full_report.data(),
|
||||||
expect(rumble_zero.count == 0 && rumble_one.count == 0,
|
full_report.size());
|
||||||
"invalid output instance reached a rumble callback");
|
for (const auto& event : rumble_events) {
|
||||||
|
expect(event.count == 0,
|
||||||
|
"invalid output instance reached a rumble callback");
|
||||||
|
}
|
||||||
|
|
||||||
std::array<uint8_t, 9> stripped{};
|
std::array<uint8_t, 9> stripped{};
|
||||||
std::memcpy(stripped.data() + 1, full_payload.data(), full_payload.size());
|
std::memcpy(stripped.data() + 1, full_payload.data(), full_payload.size());
|
||||||
tud_hid_set_report_cb(0, REPORT_OUTPUT_10, HID_REPORT_TYPE_OUTPUT,
|
tud_hid_set_report_cb(0, REPORT_OUTPUT_10, HID_REPORT_TYPE_OUTPUT,
|
||||||
stripped.data(), stripped.size());
|
stripped.data(), stripped.size());
|
||||||
expect(rumble_zero.count == 1 && rumble_zero.instance == 0,
|
expect(rumble_events[0].count == 1 && rumble_events[0].instance == 0,
|
||||||
"control output did not route to instance 0 callback");
|
"control output did not route to instance 0 callback");
|
||||||
expect(rumble_zero.output.low_frequency_magnitude == 16 &&
|
expect(rumble_events[0].output.low_frequency_magnitude == 16 &&
|
||||||
rumble_zero.output.high_frequency_magnitude == 16,
|
rumble_events[0].output.high_frequency_magnitude == 16,
|
||||||
"instance 0 full rumble state decoded incorrectly");
|
"instance 0 full rumble state decoded incorrectly");
|
||||||
expect(rumble_one.count == 0,
|
for (uint8_t instance = 1; instance < kInstanceCount; ++instance) {
|
||||||
"instance 0 rumble invoked instance 1 callback");
|
expect(rumble_events[instance].count == 0,
|
||||||
|
"instance 0 rumble invoked another instance callback");
|
||||||
|
}
|
||||||
|
|
||||||
auto delta_payload = rumble_payload(type_1_one_sample(17, 20), neutral);
|
auto delta_payload = rumble_payload(type_1_one_sample(17, 20), neutral);
|
||||||
auto delta_report = complete_rumble_report(delta_payload);
|
auto delta_report = complete_rumble_report(delta_payload);
|
||||||
tud_hid_report_received_cb(1, 0, delta_report.data(), delta_report.size());
|
tud_hid_report_received_cb(1, 0, delta_report.data(), delta_report.size());
|
||||||
expect(rumble_one.count == 1 && rumble_one.instance == 1,
|
expect(rumble_events[1].count == 1 && rumble_events[1].instance == 1,
|
||||||
"interrupt output did not route to instance 1 callback");
|
"interrupt output did not route to instance 1 callback");
|
||||||
expect(rumble_one.output.low_frequency_magnitude == 0 &&
|
expect(rumble_events[1].output.low_frequency_magnitude == 0 &&
|
||||||
rumble_one.output.high_frequency_magnitude == 1,
|
rumble_events[1].output.high_frequency_magnitude == 1,
|
||||||
"instance 1 decoder inherited instance 0 rumble state");
|
"instance 1 decoder inherited instance 0 rumble state");
|
||||||
tud_hid_report_received_cb(0, 0, delta_report.data(), delta_report.size());
|
tud_hid_report_received_cb(0, 0, delta_report.data(), delta_report.size());
|
||||||
expect(rumble_zero.count == 2 &&
|
expect(rumble_events[0].count == 2 &&
|
||||||
rumble_zero.output.low_frequency_magnitude == 17 &&
|
rumble_events[0].output.low_frequency_magnitude == 17 &&
|
||||||
rumble_zero.output.high_frequency_magnitude == 18,
|
rumble_events[0].output.high_frequency_magnitude == 18,
|
||||||
"instance 0 decoder lost its own prior rumble state");
|
"instance 0 decoder lost its own prior rumble state");
|
||||||
expect(rumble_one.count == 1,
|
|
||||||
"instance 0 delta reached instance 1 callback");
|
for (uint8_t instance = 2; instance < kInstanceCount; ++instance) {
|
||||||
|
const uint8_t magnitude = instance == 2 ? 16 : 32;
|
||||||
|
auto payload =
|
||||||
|
rumble_payload(type_2(64, magnitude, 64, magnitude), neutral);
|
||||||
|
auto report = complete_rumble_report(payload);
|
||||||
|
tud_hid_report_received_cb(instance, 0, report.data(), report.size());
|
||||||
|
expect(rumble_events[instance].count == 1 &&
|
||||||
|
rumble_events[instance].instance == instance,
|
||||||
|
"rumble output did not route to its configured instance");
|
||||||
|
expect(rumble_events[instance].output.low_frequency_magnitude ==
|
||||||
|
magnitude &&
|
||||||
|
rumble_events[instance].output.high_frequency_magnitude ==
|
||||||
|
magnitude,
|
||||||
|
"configured instance decoded another rumble context");
|
||||||
|
}
|
||||||
|
expect(rumble_events[1].count == 1,
|
||||||
|
"another instance's rumble reached instance 1 callback");
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_lifecycle_and_invalid_instances() {
|
void test_lifecycle_and_invalid_instances() {
|
||||||
initialize_contexts();
|
initialize_contexts();
|
||||||
expect(switch_pro_is_ready(0) && switch_pro_is_ready(1),
|
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||||
"initialized contexts were not ready");
|
expect(switch_pro_is_ready(instance),
|
||||||
|
"initialized context was not ready");
|
||||||
|
}
|
||||||
|
|
||||||
tud_mount_cb();
|
tud_mount_cb();
|
||||||
expect(!switch_pro_is_ready(0) && !switch_pro_is_ready(1),
|
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||||
"mount did not reset every configured context");
|
expect(!switch_pro_is_ready(instance),
|
||||||
send_config(0, DISABLE_USB_TIMEOUT);
|
"mount did not reset every configured context");
|
||||||
expect(switch_pro_is_ready(0) && !switch_pro_is_ready(1),
|
}
|
||||||
"instance 0 handshake changed instance 1 readiness");
|
|
||||||
send_config(1, DISABLE_USB_TIMEOUT);
|
for (uint8_t addressed = 0; addressed < kInstanceCount; ++addressed) {
|
||||||
expect(switch_pro_is_ready(0) && switch_pro_is_ready(1),
|
send_config(addressed, DISABLE_USB_TIMEOUT);
|
||||||
"instance 1 handshake did not address its context");
|
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||||
|
expect(switch_pro_is_ready(instance) == (instance <= addressed),
|
||||||
|
"handshake readiness crossed configured contexts");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tud_umount_cb();
|
tud_umount_cb();
|
||||||
expect(!switch_pro_is_ready(0) && !switch_pro_is_ready(1),
|
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||||
"unmount did not reset every configured context");
|
expect(!switch_pro_is_ready(instance),
|
||||||
|
"unmount did not reset every configured context");
|
||||||
|
}
|
||||||
|
|
||||||
SwitchInputState ignored{};
|
SwitchInputState ignored{};
|
||||||
ignored.button_home = true;
|
ignored.button_home = true;
|
||||||
switch_pro_init(2);
|
switch_pro_init(kInvalidInstance);
|
||||||
switch_pro_set_input(2, ignored);
|
switch_pro_set_input(kInvalidInstance, ignored);
|
||||||
switch_pro_set_rumble_callback(2, rumble_callback_zero);
|
switch_pro_set_rumble_callback(kInvalidInstance, rumble_callback);
|
||||||
expect(!switch_pro_task(2), "invalid instance ran a driver task");
|
expect(!switch_pro_task(kInvalidInstance),
|
||||||
expect(!switch_pro_is_ready(2), "invalid instance reported ready");
|
"invalid instance ran a driver task");
|
||||||
|
expect(!switch_pro_is_ready(kInvalidInstance),
|
||||||
|
"invalid instance reported ready");
|
||||||
std::array<uint8_t, SWITCH_PRO_ENDPOINT_SIZE> buffer{};
|
std::array<uint8_t, SWITCH_PRO_ENDPOINT_SIZE> buffer{};
|
||||||
expect(tud_hid_get_report_cb(2, 0, HID_REPORT_TYPE_INPUT,
|
expect(tud_hid_get_report_cb(kInvalidInstance, 0, HID_REPORT_TYPE_INPUT,
|
||||||
buffer.data(), buffer.size()) == 0,
|
buffer.data(), buffer.size()) == 0,
|
||||||
"invalid instance served GET_REPORT data");
|
"invalid instance served GET_REPORT data");
|
||||||
expect(tud_hid_descriptor_report_cb(2) == nullptr,
|
expect(tud_hid_descriptor_report_cb(kInvalidInstance) == nullptr,
|
||||||
"invalid instance served a report descriptor");
|
"invalid instance served a report descriptor");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
||||||
"-Wextra",
|
"-Wextra",
|
||||||
"-Werror",
|
"-Werror",
|
||||||
"-pedantic",
|
"-pedantic",
|
||||||
"-DSWITCH_PICO_HID_INSTANCE_COUNT=2",
|
"-DSWITCH_PICO_HID_INSTANCE_COUNT=4",
|
||||||
f"-I{root / 'tests' / 'bluepad32_native_stubs'}",
|
f"-I{root / 'tests' / 'bluepad32_native_stubs'}",
|
||||||
f"-I{root}",
|
f"-I{root}",
|
||||||
str(root / "tests" / "bluepad32_backend_lifecycle_test.cpp"),
|
str(root / "tests" / "bluepad32_backend_lifecycle_test.cpp"),
|
||||||
|
|
@ -30,5 +30,10 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
||||||
cwd=root,
|
cwd=root,
|
||||||
)
|
)
|
||||||
|
|
||||||
for scenario in ("ready-0-1", "ready-1-0", "rejections", "lifecycle"):
|
for scenario in (
|
||||||
|
"ready-forward",
|
||||||
|
"ready-reverse",
|
||||||
|
"rejections",
|
||||||
|
"lifecycle",
|
||||||
|
):
|
||||||
subprocess.run([str(executable), scenario], check=True, cwd=root)
|
subprocess.run([str(executable), scenario], check=True, cwd=root)
|
||||||
|
|
|
||||||
|
|
@ -54,26 +54,28 @@ def test_default_descriptor_contract_is_single_hid(tmp_path: Path) -> None:
|
||||||
subprocess.run([str(executable)], check=True, cwd=root)
|
subprocess.run([str(executable)], check=True, cwd=root)
|
||||||
|
|
||||||
|
|
||||||
def test_explicit_single_descriptor_contract(tmp_path: Path) -> None:
|
def test_supported_descriptor_contracts(tmp_path: Path) -> None:
|
||||||
root = Path(__file__).resolve().parents[1]
|
root = Path(__file__).resolve().parents[1]
|
||||||
executable = tmp_path / "switch_pro_descriptors_single_test"
|
compiler = host_compiler()
|
||||||
result = compile_descriptor_test(root, host_compiler(), executable, 1, 1)
|
for instance_count in range(1, 5):
|
||||||
assert result.returncode == 0, result.stderr
|
executable = (
|
||||||
subprocess.run([str(executable)], check=True, cwd=root)
|
tmp_path / f"switch_pro_descriptors_{instance_count}_test"
|
||||||
|
)
|
||||||
|
result = compile_descriptor_test(
|
||||||
def test_dual_descriptor_contract(tmp_path: Path) -> None:
|
root,
|
||||||
root = Path(__file__).resolve().parents[1]
|
compiler,
|
||||||
executable = tmp_path / "switch_pro_descriptors_dual_test"
|
executable,
|
||||||
result = compile_descriptor_test(root, host_compiler(), executable, 2, 2)
|
instance_count,
|
||||||
assert result.returncode == 0, result.stderr
|
instance_count,
|
||||||
subprocess.run([str(executable)], check=True, cwd=root)
|
)
|
||||||
|
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:
|
def test_unsupported_hid_instance_counts_fail_to_compile(tmp_path: Path) -> None:
|
||||||
root = Path(__file__).resolve().parents[1]
|
root = Path(__file__).resolve().parents[1]
|
||||||
compiler = host_compiler()
|
compiler = host_compiler()
|
||||||
for unsupported_count in (0, 3):
|
for unsupported_count in (0, 5):
|
||||||
executable = tmp_path / f"switch_pro_descriptors_invalid_{unsupported_count}"
|
executable = tmp_path / f"switch_pro_descriptors_invalid_{unsupported_count}"
|
||||||
result = compile_descriptor_test(
|
result = compile_descriptor_test(
|
||||||
root,
|
root,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def test_switch_pro_driver_two_contexts_native(tmp_path: Path) -> None:
|
def test_switch_pro_driver_four_contexts_native(tmp_path: Path) -> None:
|
||||||
root = Path(__file__).resolve().parents[1]
|
root = Path(__file__).resolve().parents[1]
|
||||||
compiler = shutil.which("c++") or shutil.which("g++")
|
compiler = shutil.which("c++") or shutil.which("g++")
|
||||||
assert compiler is not None, "a host C++ compiler is required"
|
assert compiler is not None, "a host C++ compiler is required"
|
||||||
|
|
@ -19,7 +19,7 @@ def test_switch_pro_driver_two_contexts_native(tmp_path: Path) -> None:
|
||||||
"-Wextra",
|
"-Wextra",
|
||||||
"-Werror",
|
"-Werror",
|
||||||
"-pedantic",
|
"-pedantic",
|
||||||
"-DSWITCH_PICO_HID_INSTANCE_COUNT=2",
|
"-DSWITCH_PICO_HID_INSTANCE_COUNT=4",
|
||||||
f"-I{root / 'tests' / 'native_stubs'}",
|
f"-I{root / 'tests' / 'native_stubs'}",
|
||||||
f"-I{root}",
|
f"-I{root}",
|
||||||
str(root / "switch_pro_driver.cpp"),
|
str(root / "switch_pro_driver.cpp"),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// TinyUSB configuration for one or two Switch Pro style HID interfaces.
|
// TinyUSB configuration for one to four Switch Pro style HID interfaces.
|
||||||
// Each interface uses independent 64-byte interrupt IN and OUT endpoints.
|
// Each interface uses independent 64-byte interrupt IN and OUT endpoints.
|
||||||
#ifndef _TUSB_CONFIG_H_
|
#ifndef _TUSB_CONFIG_H_
|
||||||
#define _TUSB_CONFIG_H_
|
#define _TUSB_CONFIG_H_
|
||||||
|
|
@ -10,8 +10,8 @@ extern "C" {
|
||||||
#define SWITCH_PICO_HID_INSTANCE_COUNT 1
|
#define SWITCH_PICO_HID_INSTANCE_COUNT 1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if SWITCH_PICO_HID_INSTANCE_COUNT != 1 && SWITCH_PICO_HID_INSTANCE_COUNT != 2
|
#if SWITCH_PICO_HID_INSTANCE_COUNT < 1 || SWITCH_PICO_HID_INSTANCE_COUNT > 4
|
||||||
#error "SWITCH_PICO_HID_INSTANCE_COUNT must be 1 or 2"
|
#error "SWITCH_PICO_HID_INSTANCE_COUNT must be between 1 and 4"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue