Introduce protocol-neutral controller state
This commit is contained in:
parent
df515934ab
commit
b11ae076a8
28 changed files with 833 additions and 501 deletions
|
|
@ -285,6 +285,13 @@ uint32_t btstack_run_loop_get_time_ms() {
|
|||
|
||||
|
||||
#include "../bluepad32_input_backend.cpp"
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
AdapterUsbMode test_adapter_mode = AdapterUsbMode::kXInput;
|
||||
AdapterUsbMode adapter_host_probe_mode() {
|
||||
return test_adapter_mode;
|
||||
}
|
||||
#endif
|
||||
|
||||
SwitchRgbColor switch_pro_get_slot_light_color(uint8_t instance) {
|
||||
static constexpr SwitchRgbColor grips[] = {
|
||||
{SWITCH_COLOR_SLOT_1_R, SWITCH_COLOR_SLOT_1_G,
|
||||
|
|
@ -355,7 +362,7 @@ void test_ready_order(bool reverse) {
|
|||
"ready device must bind to its Bluepad index");
|
||||
|
||||
for (int candidate = 0; candidate < kSlotCount; ++candidate) {
|
||||
SwitchInputState snapshot{};
|
||||
ControllerState snapshot{};
|
||||
bool expected_active = false;
|
||||
for (int ready = 0; ready <= position; ++ready) {
|
||||
expected_active = expected_active || order[ready] == candidate;
|
||||
|
|
@ -386,7 +393,7 @@ void test_ready_order(bool reverse) {
|
|||
"disconnecting any slot must resume connection policy");
|
||||
|
||||
for (int candidate = 0; candidate < kSlotCount; ++candidate) {
|
||||
SwitchInputState snapshot{};
|
||||
ControllerState snapshot{};
|
||||
require(bluepad32_input_backend_snapshot(candidate, &snapshot) ==
|
||||
(candidate != slot),
|
||||
"disconnect must preserve every surviving slot");
|
||||
|
|
@ -427,10 +434,10 @@ void test_rejections() {
|
|||
collision_data.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
collision_data.gamepad.buttons = BUTTON_B;
|
||||
platform_on_controller_data(&collision, &collision_data);
|
||||
SwitchInputState snapshot{};
|
||||
ControllerState snapshot{};
|
||||
require(bluepad32_input_backend_snapshot(0, &snapshot),
|
||||
"occupied slot must stay active");
|
||||
require(!snapshot.button_a,
|
||||
require(!snapshot.button_east,
|
||||
"mismatched device input must not enter the occupied slot");
|
||||
|
||||
uni_controller_t slot_zero_data{};
|
||||
|
|
@ -438,15 +445,15 @@ void test_rejections() {
|
|||
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,
|
||||
snapshot.motion_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,
|
||||
snapshot.motion_sample_count == 3,
|
||||
"slot 4 acknowledgement must not consume slot 0 IMU");
|
||||
bluepad32_input_backend_queue_rumble(4, SwitchRumbleOutput{1, 2});
|
||||
bluepad32_input_backend_queue_rumble(4, ControllerRumbleOutput{1, 2});
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(slot_zero.rumble_calls == 0,
|
||||
"slot 4 rumble must not reach a valid controller");
|
||||
|
|
@ -534,39 +541,39 @@ void test_independent_lifecycle() {
|
|||
platform_on_controller_data(&devices[slot], &data[slot]);
|
||||
}
|
||||
|
||||
SwitchInputState states[kSlotCount]{};
|
||||
ControllerState states[kSlotCount]{};
|
||||
for (int slot = 0; slot < kSlotCount; ++slot) {
|
||||
require(bluepad32_input_backend_snapshot(slot, &states[slot]) &&
|
||||
states[slot].imu_sample_count == 3,
|
||||
states[slot].motion_sample_count == 3,
|
||||
"every slot must expose independent input and IMU");
|
||||
}
|
||||
require(states[0].button_a && !states[0].button_b &&
|
||||
!states[0].button_y && !states[0].button_x,
|
||||
require(states[0].button_east && !states[0].button_south &&
|
||||
!states[0].button_west && !states[0].button_north,
|
||||
"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,
|
||||
require(states[1].button_south && !states[1].button_east &&
|
||||
!states[1].button_west && !states[1].button_north,
|
||||
"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,
|
||||
require(states[2].button_west && !states[2].button_east &&
|
||||
!states[2].button_south && !states[2].button_north,
|
||||
"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,
|
||||
require(states[3].button_north && !states[3].button_east &&
|
||||
!states[3].button_south && !states[3].button_west,
|
||||
"slot 3 must contain only slot 3 input");
|
||||
|
||||
bluepad32_input_backend_report_sent(3);
|
||||
for (int slot = 0; slot < kSlotCount; ++slot) {
|
||||
require(bluepad32_input_backend_snapshot(slot, &states[slot]) &&
|
||||
states[slot].imu_sample_count == (slot == 3 ? 0 : 3),
|
||||
states[slot].motion_sample_count == (slot == 3 ? 0 : 3),
|
||||
"slot 3 acknowledgement must not consume slots 0-2 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,
|
||||
states[slot].motion_sample_count == 0,
|
||||
"each slot acknowledgement must consume only its own IMU");
|
||||
}
|
||||
|
||||
const SwitchRumbleOutput initial_rumble[kSlotCount] = {
|
||||
const ControllerRumbleOutput initial_rumble[kSlotCount] = {
|
||||
{11, 21}, {12, 22}, {13, 23}, {14, 24}};
|
||||
for (int slot = 0; slot < kSlotCount; ++slot) {
|
||||
bluepad32_input_backend_queue_rumble(slot, initial_rumble[slot]);
|
||||
|
|
@ -577,17 +584,17 @@ void test_independent_lifecycle() {
|
|||
devices[slot].last_low == 11 + slot &&
|
||||
devices[slot].last_high == 21 + slot &&
|
||||
devices[slot].last_rumble_duration_ms ==
|
||||
kRumbleDurationMs,
|
||||
host_rumble_duration_ms(),
|
||||
"each slot rumble must reach only its indexed controller");
|
||||
}
|
||||
|
||||
bluepad32_input_backend_queue_rumble(0, SwitchRumbleOutput{0, 0});
|
||||
bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{0, 0});
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(devices[0].rumble_calls == 2 &&
|
||||
devices[0].last_rumble_duration_ms == 0,
|
||||
"zero XInput magnitude must stop rumble immediately");
|
||||
|
||||
bluepad32_input_backend_queue_rumble(3, SwitchRumbleOutput{55, 66});
|
||||
bluepad32_input_backend_queue_rumble(3, ControllerRumbleOutput{55, 66});
|
||||
const uint32_t disconnected_generation =
|
||||
g_slots[3].connection_generation;
|
||||
const int starts_before_slot_three_disconnect = scan_starts;
|
||||
|
|
@ -596,21 +603,21 @@ void test_independent_lifecycle() {
|
|||
scanning_enabled && incoming_connections,
|
||||
"slot 3 disconnect must resume scanning and incoming connections");
|
||||
require(!bluepad32_input_backend_snapshot(3, &states[3]) &&
|
||||
!states[3].button_x && states[3].lx == 32768,
|
||||
"slot 3 disconnect must neutralize only slot 3");
|
||||
!states[3].button_north && states[3].left_stick_x == 0,
|
||||
"slot 3 disconnect must publish protocol-neutral state");
|
||||
require(bluepad32_input_backend_snapshot(0, &states[0]) &&
|
||||
states[0].button_a &&
|
||||
states[0].button_east &&
|
||||
bluepad32_input_backend_snapshot(1, &states[1]) &&
|
||||
states[1].button_b &&
|
||||
states[1].button_south &&
|
||||
bluepad32_input_backend_snapshot(2, &states[2]) &&
|
||||
states[2].button_y,
|
||||
states[2].button_west,
|
||||
"slot 3 disconnect must preserve slots 0-2");
|
||||
platform_on_controller_data(&devices[0], &data[0]);
|
||||
require(bluepad32_input_backend_snapshot(0, &states[0]) &&
|
||||
states[0].button_a,
|
||||
states[0].button_east,
|
||||
"slot 0 input must continue while slot 3 is disconnected");
|
||||
const int slot_zero_calls_while_scanning = devices[0].rumble_calls;
|
||||
bluepad32_input_backend_queue_rumble(0, SwitchRumbleOutput{115, 116});
|
||||
bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{115, 116});
|
||||
tick_backend_timer(99);
|
||||
require(devices[0].rumble_calls == slot_zero_calls_while_scanning + 1 &&
|
||||
devices[0].last_low == 115 &&
|
||||
|
|
@ -626,7 +633,7 @@ void test_independent_lifecycle() {
|
|||
"slot 3 replacement must not receive disconnected device rumble");
|
||||
|
||||
g_slots[3].pending_rumble = {
|
||||
3, disconnected_generation, SwitchRumbleOutput{77, 88}};
|
||||
3, disconnected_generation, ControllerRumbleOutput{77, 88}};
|
||||
g_slots[3].rumble_pending = true;
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(slot_three_replacement.rumble_calls == 0,
|
||||
|
|
@ -638,21 +645,21 @@ void test_independent_lifecycle() {
|
|||
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,
|
||||
states[3].button_north && states[3].motion_sample_count == 3,
|
||||
"replacement input and IMU must populate only slot 3");
|
||||
require(bluepad32_input_backend_snapshot(0, &states[0]) &&
|
||||
states[0].button_a &&
|
||||
states[0].button_east &&
|
||||
bluepad32_input_backend_snapshot(1, &states[1]) &&
|
||||
states[1].button_b &&
|
||||
states[1].button_south &&
|
||||
bluepad32_input_backend_snapshot(2, &states[2]) &&
|
||||
states[2].button_y,
|
||||
states[2].button_west,
|
||||
"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});
|
||||
bluepad32_input_backend_queue_rumble(3, ControllerRumbleOutput{90, 91});
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(slot_three_replacement.rumble_calls == 1 &&
|
||||
slot_three_replacement.last_low == 90 &&
|
||||
|
|
@ -669,8 +676,8 @@ void test_independent_lifecycle() {
|
|||
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)});
|
||||
slot, ControllerRumbleOutput{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) {
|
||||
|
|
@ -691,8 +698,8 @@ void test_independent_lifecycle() {
|
|||
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");
|
||||
states[slot].left_stick_x == 0,
|
||||
"disconnect must publish protocol-neutral state");
|
||||
for (int survivor = 0; survivor < kSlotCount; ++survivor) {
|
||||
if (survivor == slot) {
|
||||
continue;
|
||||
|
|
@ -712,9 +719,9 @@ void test_independent_lifecycle() {
|
|||
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(3, ControllerRumbleOutput{119, 120});
|
||||
bluepad32_input_backend_queue_rumble(3, ControllerRumbleOutput{121, 122});
|
||||
bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{123, 124});
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(slot_three_replacement.rumble_calls ==
|
||||
slot_three_calls_before_mailboxes + 1 &&
|
||||
|
|
@ -861,11 +868,11 @@ void test_slot_lighting() {
|
|||
"controller without RGB support did not receive its slot LED");
|
||||
}
|
||||
|
||||
void require_south_button_mapping(const SwitchInputState& state,
|
||||
void require_south_button_mapping(const ControllerState& state,
|
||||
bool swapped,
|
||||
const char* message) {
|
||||
require(state.button_a == swapped && state.button_b == !swapped &&
|
||||
!state.button_x && !state.button_y,
|
||||
require(state.button_east == swapped && state.button_south == !swapped &&
|
||||
!state.button_north && !state.button_west,
|
||||
message);
|
||||
}
|
||||
|
||||
|
|
@ -881,7 +888,7 @@ void test_abxy_hotkey() {
|
|||
input.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
input.gamepad.buttons = BUTTON_A;
|
||||
platform_on_controller_data(&slot_zero, &input);
|
||||
SwitchInputState snapshot{};
|
||||
ControllerState snapshot{};
|
||||
require(bluepad32_input_backend_snapshot(0, &snapshot),
|
||||
"slot 0 ABXY state was not published");
|
||||
require_south_button_mapping(
|
||||
|
|
@ -897,12 +904,12 @@ void test_abxy_hotkey() {
|
|||
require_south_button_mapping(
|
||||
snapshot, !kDefaultSwapAbxy,
|
||||
"hotkey did not toggle slot 0 ABXY mapping");
|
||||
require(!snapshot.button_l && !snapshot.button_r &&
|
||||
!snapshot.button_minus && !snapshot.button_plus,
|
||||
require(!snapshot.button_left_shoulder && !snapshot.button_right_shoulder &&
|
||||
!snapshot.button_select && !snapshot.button_start,
|
||||
"hotkey chord leaked into the Switch report");
|
||||
|
||||
bluepad32_input_backend_queue_rumble(
|
||||
0, SwitchRumbleOutput{0x11, 0x22});
|
||||
0, ControllerRumbleOutput{0x11, 0x22});
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(slot_zero.rumble_calls == 1 &&
|
||||
slot_zero.last_high == kAbxyFeedbackWeakMagnitude &&
|
||||
|
|
@ -971,9 +978,9 @@ void test_motion_hotkey() {
|
|||
input.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
input.gamepad.accel[0] = 8192;
|
||||
platform_on_controller_data(&slot_zero, &input);
|
||||
SwitchInputState snapshot{};
|
||||
ControllerState snapshot{};
|
||||
require(bluepad32_input_backend_snapshot(0, &snapshot) &&
|
||||
snapshot.imu_sample_count ==
|
||||
snapshot.motion_sample_count ==
|
||||
(kDefaultMotionEnabled ? 3 : 0),
|
||||
"slot 0 did not start with configured motion state");
|
||||
|
||||
|
|
@ -982,10 +989,10 @@ void test_motion_hotkey() {
|
|||
input.gamepad.misc_buttons = kMotionHotkeyMiscMask;
|
||||
platform_on_controller_data(&slot_zero, &input);
|
||||
require(bluepad32_input_backend_snapshot(0, &snapshot) &&
|
||||
snapshot.imu_sample_count ==
|
||||
snapshot.motion_sample_count ==
|
||||
(kDefaultMotionEnabled ? 0 : 3) &&
|
||||
!snapshot.dpad_up && !snapshot.button_r &&
|
||||
!snapshot.button_plus,
|
||||
!snapshot.dpad_up && !snapshot.button_right_shoulder &&
|
||||
!snapshot.button_start,
|
||||
"motion chord did not toggle motion or suppress its inputs");
|
||||
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
|
|
@ -1015,7 +1022,7 @@ void test_motion_hotkey() {
|
|||
peer_input.gamepad.accel[0] = 8192;
|
||||
platform_on_controller_data(&slot_one, &peer_input);
|
||||
require(bluepad32_input_backend_snapshot(1, &snapshot) &&
|
||||
snapshot.imu_sample_count ==
|
||||
snapshot.motion_sample_count ==
|
||||
(kDefaultMotionEnabled ? 3 : 0),
|
||||
"slot 0 motion chord changed slot 1 motion state");
|
||||
|
||||
|
|
@ -1027,7 +1034,7 @@ void test_motion_hotkey() {
|
|||
input.gamepad.misc_buttons = kMotionHotkeyMiscMask;
|
||||
platform_on_controller_data(&slot_zero, &input);
|
||||
require(bluepad32_input_backend_snapshot(0, &snapshot) &&
|
||||
snapshot.imu_sample_count ==
|
||||
snapshot.motion_sample_count ==
|
||||
(kDefaultMotionEnabled ? 3 : 0),
|
||||
"released motion chord did not re-arm or restore motion");
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
|
|
@ -1044,6 +1051,76 @@ void test_motion_hotkey() {
|
|||
"disconnect did not reset slot 0 motion hotkey state");
|
||||
}
|
||||
|
||||
void test_protocol_neutral_analog_state() {
|
||||
start_pairing_backend();
|
||||
uni_hid_device_t controller = device(0);
|
||||
platform_on_device_connected(&controller);
|
||||
require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS,
|
||||
"analog-state controller did not become ready");
|
||||
|
||||
uni_controller_t input{};
|
||||
input.klass = UNI_CONTROLLER_CLASS_GAMEPAD;
|
||||
input.gamepad.axis_x = -512;
|
||||
input.gamepad.axis_y = 0;
|
||||
input.gamepad.axis_rx = 511;
|
||||
input.gamepad.axis_ry = -256;
|
||||
input.gamepad.brake = 1;
|
||||
input.gamepad.throttle = 512;
|
||||
platform_on_controller_data(&controller, &input);
|
||||
|
||||
ControllerState state{};
|
||||
require(bluepad32_input_backend_snapshot(0, &state),
|
||||
"analog state was not published");
|
||||
require(state.left_stick_x == INT16_MIN &&
|
||||
state.left_stick_y == 0 &&
|
||||
state.right_stick_x == INT16_MAX &&
|
||||
state.right_stick_y == -16384,
|
||||
"stick axes were not normalized to signed full range");
|
||||
require(state.left_trigger == scale_trigger(1) &&
|
||||
state.left_trigger > 0 &&
|
||||
state.right_trigger == scale_trigger(512) &&
|
||||
state.right_trigger < UINT16_MAX,
|
||||
"analog trigger precision was discarded");
|
||||
|
||||
input.gamepad.brake = 0;
|
||||
input.gamepad.throttle = 0;
|
||||
input.gamepad.buttons =
|
||||
BUTTON_TRIGGER_L | BUTTON_TRIGGER_R;
|
||||
platform_on_controller_data(&controller, &input);
|
||||
require(bluepad32_input_backend_snapshot(0, &state) &&
|
||||
state.left_trigger == UINT16_MAX &&
|
||||
state.right_trigger == UINT16_MAX,
|
||||
"digital trigger buttons did not map to full analog range");
|
||||
}
|
||||
|
||||
void test_host_rumble_mode_duration() {
|
||||
start_pairing_backend();
|
||||
uni_hid_device_t controller = device(0);
|
||||
platform_on_device_connected(&controller);
|
||||
require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS,
|
||||
"rumble-mode controller did not become ready");
|
||||
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
test_adapter_mode = AdapterUsbMode::kSwitchProbe;
|
||||
#endif
|
||||
bluepad32_input_backend_queue_rumble(
|
||||
0, ControllerRumbleOutput{100, 101});
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(controller.last_rumble_duration_ms ==
|
||||
kSwitchHostRumbleDurationMs,
|
||||
"Switch mode did not use bounded host rumble");
|
||||
|
||||
#ifdef SWITCH_PICO_ADAPTER_FEASIBILITY
|
||||
test_adapter_mode = AdapterUsbMode::kXInput;
|
||||
bluepad32_input_backend_queue_rumble(
|
||||
0, ControllerRumbleOutput{102, 103});
|
||||
process_rumble_timer(&g_rumble_timer);
|
||||
require(controller.last_rumble_duration_ms ==
|
||||
kXInputHostRumbleDurationMs,
|
||||
"XInput mode did not retain stateful host rumble");
|
||||
#endif
|
||||
}
|
||||
|
||||
void test_clear_pairings() {
|
||||
classic_bond_count = 1;
|
||||
classic_bonds[0][0] = 0x10;
|
||||
|
|
@ -1067,7 +1144,7 @@ void test_clear_pairings() {
|
|||
"pairing reset controller did not become ready");
|
||||
}
|
||||
bluepad32_input_backend_queue_rumble(
|
||||
0, SwitchRumbleOutput{100, 101});
|
||||
0, ControllerRumbleOutput{100, 101});
|
||||
|
||||
bluepad32_input_backend_clear_pairings();
|
||||
require(g_clear_pairings_requested && delete_key_calls == 0 &&
|
||||
|
|
@ -1086,11 +1163,11 @@ void test_clear_pairings() {
|
|||
for (const BackendSlot& slot : g_slots) {
|
||||
require(slot.device == nullptr && !slot.active &&
|
||||
!slot.rumble_pending && !slot.feedback_pending &&
|
||||
slot.state.lx == kStickMidpoint &&
|
||||
slot.state.ly == kStickMidpoint &&
|
||||
slot.state.rx == kStickMidpoint &&
|
||||
slot.state.ry == kStickMidpoint &&
|
||||
slot.state.imu_sample_count == 0,
|
||||
slot.state.left_stick_x == 0 &&
|
||||
slot.state.left_stick_y == 0 &&
|
||||
slot.state.right_stick_x == 0 &&
|
||||
slot.state.right_stick_y == 0 &&
|
||||
slot.state.motion_sample_count == 0,
|
||||
"pairing reset must publish neutral empty slots");
|
||||
}
|
||||
require(!g_pairing_window_open && !bondable &&
|
||||
|
|
@ -1163,6 +1240,10 @@ int main(int argc, char** argv) {
|
|||
test_abxy_hotkey();
|
||||
} else if (scenario == "motion-hotkey") {
|
||||
test_motion_hotkey();
|
||||
} else if (scenario == "analog-state") {
|
||||
test_protocol_neutral_analog_state();
|
||||
} else if (scenario == "rumble-mode") {
|
||||
test_host_rumble_mode_duration();
|
||||
} else if (scenario == "clear-pairings") {
|
||||
test_clear_pairings();
|
||||
} else if (scenario == "flash-core-start") {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ namespace {
|
|||
|
||||
int failures = 0;
|
||||
|
||||
void expect_output(const char* scenario, SwitchRumbleOutput actual,
|
||||
void expect_output(const char* scenario, ControllerRumbleOutput actual,
|
||||
uint8_t expected_low, uint8_t expected_high) {
|
||||
if (actual.low_frequency_magnitude == expected_low &&
|
||||
actual.high_frequency_magnitude == expected_high) {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ struct SentReport {
|
|||
struct RumbleEvent {
|
||||
unsigned count = 0;
|
||||
uint8_t instance = 0xff;
|
||||
SwitchRumbleOutput output{};
|
||||
ControllerRumbleOutput output{};
|
||||
};
|
||||
|
||||
uint64_t now_ms = 0;
|
||||
|
|
@ -95,7 +95,8 @@ SwitchProReport get_current_report(uint8_t instance,
|
|||
|
||||
void expect_neutral_sticks(SwitchProReport& report,
|
||||
const char* state_failure) {
|
||||
constexpr uint16_t packed_mid = SWITCH_PRO_JOYSTICK_MID >> 4u;
|
||||
constexpr uint16_t packed_mid =
|
||||
CONTROLLER_AXIS_UNSIGNED_CENTER >> 4u;
|
||||
constexpr uint16_t packed_inverted_mid =
|
||||
static_cast<uint16_t>(-static_cast<int32_t>(packed_mid)) & 0x0fffu;
|
||||
expect(report.inputs.leftStick.getX() == packed_mid &&
|
||||
|
|
@ -195,7 +196,7 @@ std::array<uint8_t, 10> complete_rumble_report(
|
|||
return report;
|
||||
}
|
||||
|
||||
void rumble_callback(uint8_t instance, const SwitchRumbleOutput& output) {
|
||||
void rumble_callback(uint8_t instance, const ControllerRumbleOutput& output) {
|
||||
expect(instance < rumble_events.size(),
|
||||
"rumble callback received an invalid instance");
|
||||
if (instance >= rumble_events.size()) {
|
||||
|
|
@ -270,18 +271,22 @@ void test_failed_startup_identify_retries_preserve_counter() {
|
|||
|
||||
void test_input_reports_and_timers_are_isolated() {
|
||||
initialize_contexts();
|
||||
std::array<SwitchInputState, kInstanceCount> states{};
|
||||
std::array<ControllerState, kInstanceCount> states{};
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
SwitchInputState& state = states[instance];
|
||||
state.lx = static_cast<uint16_t>(0x1111u * (instance + 1u));
|
||||
state.ly = static_cast<uint16_t>(0x2222u + 0x1111u * instance);
|
||||
state.rx = static_cast<uint16_t>(0x5555u + 0x1111u * instance);
|
||||
state.ry = static_cast<uint16_t>(0x8888u + 0x1111u * instance);
|
||||
ControllerState& state = states[instance];
|
||||
state.left_stick_x = controller_axis_from_unsigned(
|
||||
static_cast<uint16_t>(0x1111u * (instance + 1u)));
|
||||
state.left_stick_y = controller_axis_from_unsigned(
|
||||
static_cast<uint16_t>(0x2222u + 0x1111u * instance));
|
||||
state.right_stick_x = controller_axis_from_unsigned(
|
||||
static_cast<uint16_t>(0x5555u + 0x1111u * instance));
|
||||
state.right_stick_y = controller_axis_from_unsigned(
|
||||
static_cast<uint16_t>(0x8888u + 0x1111u * instance));
|
||||
}
|
||||
states[0].button_a = true;
|
||||
states[1].button_b = true;
|
||||
states[2].button_x = true;
|
||||
states[3].button_y = true;
|
||||
states[0].button_east = true;
|
||||
states[1].button_south = true;
|
||||
states[2].button_north = true;
|
||||
states[3].button_west = true;
|
||||
|
||||
for (uint8_t instance = 0; instance < kInstanceCount; ++instance) {
|
||||
switch_pro_set_input(instance, states[instance]);
|
||||
|
|
@ -320,9 +325,9 @@ void test_input_reports_and_timers_are_isolated() {
|
|||
}
|
||||
}
|
||||
|
||||
SwitchInputState changed_zero = states[0];
|
||||
changed_zero.button_a = false;
|
||||
changed_zero.button_home = true;
|
||||
ControllerState changed_zero = states[0];
|
||||
changed_zero.button_east = false;
|
||||
changed_zero.button_system = true;
|
||||
switch_pro_set_input(0, changed_zero);
|
||||
now_ms = 30;
|
||||
expect(switch_pro_task(0),
|
||||
|
|
@ -333,8 +338,8 @@ void test_input_reports_and_timers_are_isolated() {
|
|||
!unchanged_three.inputs.buttonHome,
|
||||
"instance 0 input change leaked into instance 3");
|
||||
|
||||
SwitchInputState changed_three = states[3];
|
||||
changed_three.button_y = false;
|
||||
ControllerState changed_three = states[3];
|
||||
changed_three.button_west = false;
|
||||
changed_three.button_capture = true;
|
||||
switch_pro_set_input(3, changed_three);
|
||||
now_ms = 45;
|
||||
|
|
@ -357,13 +362,14 @@ void test_callback_send_and_imu_modes_are_isolated() {
|
|||
expect(reports_for_instance(1) == 0,
|
||||
"feature callback queued a reply on instance 1");
|
||||
|
||||
SwitchInputState zero{};
|
||||
zero.lx = zero.ly = zero.rx = zero.ry = SWITCH_PRO_JOYSTICK_MID;
|
||||
zero.imu_sample_count = 1;
|
||||
zero.imu_samples[0] = {101, 202, 303, 404, 505, 606};
|
||||
SwitchInputState one = zero;
|
||||
one.button_x = true;
|
||||
one.imu_samples[0] = {1001, 2002, 3003, 4004, 5005, 6006};
|
||||
ControllerState zero{};
|
||||
zero.left_stick_x = zero.left_stick_y =
|
||||
zero.right_stick_x = zero.right_stick_y = 0;
|
||||
zero.motion_sample_count = 1;
|
||||
zero.motion_samples[0] = {101, 202, 303, 404, 505, 606};
|
||||
ControllerState one = zero;
|
||||
one.button_north = true;
|
||||
one.motion_samples[0] = {1001, 2002, 3003, 4004, 5005, 6006};
|
||||
switch_pro_set_input(0, zero);
|
||||
switch_pro_set_input(1, one);
|
||||
now_ms = 21;
|
||||
|
|
@ -384,15 +390,16 @@ void test_callback_send_and_imu_modes_are_isolated() {
|
|||
now_ms = 6;
|
||||
switch_pro_task(0);
|
||||
switch_pro_task(1);
|
||||
SwitchInputState moving{};
|
||||
moving.lx = moving.ly = moving.rx = moving.ry = SWITCH_PRO_JOYSTICK_MID;
|
||||
moving.imu_sample_count = 1;
|
||||
moving.imu_samples[0] = {100, 200, 300, 20000, 0, 0};
|
||||
SwitchInputState stationary{};
|
||||
stationary.lx = stationary.ly = stationary.rx = stationary.ry =
|
||||
SWITCH_PRO_JOYSTICK_MID;
|
||||
stationary.imu_sample_count = 1;
|
||||
stationary.imu_samples[0] = {1000, 2000, 3000, 0, 0, 0};
|
||||
ControllerState moving{};
|
||||
moving.left_stick_x = moving.left_stick_y =
|
||||
moving.right_stick_x = moving.right_stick_y = 0;
|
||||
moving.motion_sample_count = 1;
|
||||
moving.motion_samples[0] = {100, 200, 300, 20000, 0, 0};
|
||||
ControllerState stationary{};
|
||||
stationary.left_stick_x = stationary.left_stick_y =
|
||||
stationary.right_stick_x = stationary.right_stick_y = 0;
|
||||
stationary.motion_sample_count = 1;
|
||||
stationary.motion_samples[0] = {1000, 2000, 3000, 0, 0, 0};
|
||||
switch_pro_set_input(0, moving);
|
||||
switch_pro_set_input(1, stationary);
|
||||
now_ms = 21;
|
||||
|
|
@ -568,8 +575,8 @@ void test_lifecycle_and_invalid_instances() {
|
|||
"unmount did not reset every configured context");
|
||||
}
|
||||
|
||||
SwitchInputState ignored{};
|
||||
ignored.button_home = true;
|
||||
ControllerState ignored{};
|
||||
ignored.button_system = true;
|
||||
switch_pro_init(kInvalidInstance);
|
||||
switch_pro_set_input(kInvalidInstance, ignored);
|
||||
switch_pro_set_rumble_callback(kInvalidInstance, rumble_callback);
|
||||
|
|
@ -579,18 +586,41 @@ void test_lifecycle_and_invalid_instances() {
|
|||
"invalid instance reported ready");
|
||||
std::array<uint8_t, SWITCH_PRO_ENDPOINT_SIZE> buffer{};
|
||||
expect(tud_hid_get_report_cb(kInvalidInstance, 0, HID_REPORT_TYPE_INPUT,
|
||||
|
||||
buffer.data(), buffer.size()) == 0,
|
||||
"invalid instance served GET_REPORT data");
|
||||
expect(tud_hid_descriptor_report_cb(kInvalidInstance) == nullptr,
|
||||
"invalid instance served a report descriptor");
|
||||
}
|
||||
void test_protocol_neutral_trigger_threshold() {
|
||||
initialize_contexts();
|
||||
ControllerState state{};
|
||||
state.left_trigger =
|
||||
static_cast<uint16_t>(SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD - 1u);
|
||||
state.right_trigger = SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
|
||||
switch_pro_set_input(0, state);
|
||||
now_ms = 15;
|
||||
expect(switch_pro_task(0), "trigger threshold report was not sent");
|
||||
SwitchProReport report = copy_switch_report(latest_regular_report(0));
|
||||
expect(!report.inputs.buttonZL && report.inputs.buttonZR,
|
||||
"Switch trigger threshold changed at the lower boundary");
|
||||
|
||||
state.left_trigger = SWITCH_PRO_DIGITAL_TRIGGER_THRESHOLD;
|
||||
state.right_trigger = CONTROLLER_TRIGGER_MIN;
|
||||
switch_pro_set_input(0, state);
|
||||
now_ms = 30;
|
||||
expect(switch_pro_task(0), "second trigger threshold report was not sent");
|
||||
report = copy_switch_report(latest_regular_report(0));
|
||||
expect(report.inputs.buttonZL && !report.inputs.buttonZR,
|
||||
"Switch trigger threshold changed at the upper boundary");
|
||||
}
|
||||
|
||||
void test_uart_parser_is_pure() {
|
||||
initialize_contexts();
|
||||
SwitchInputState driver_state{};
|
||||
driver_state.lx = driver_state.ly = driver_state.rx = driver_state.ry =
|
||||
SWITCH_PRO_JOYSTICK_MID;
|
||||
driver_state.button_x = true;
|
||||
ControllerState driver_state{};
|
||||
driver_state.left_stick_x = driver_state.left_stick_y =
|
||||
driver_state.right_stick_x = driver_state.right_stick_y = 0;
|
||||
driver_state.button_north = true;
|
||||
switch_pro_set_input(0, driver_state);
|
||||
now_ms = 15;
|
||||
switch_pro_task(0);
|
||||
|
|
@ -610,14 +640,20 @@ void test_uart_parser_is_pure() {
|
|||
for (unsigned i = 0; i < packet.size() - 1; ++i) {
|
||||
packet.back() = static_cast<uint8_t>(packet.back() + packet[i]);
|
||||
}
|
||||
SwitchInputState parsed{};
|
||||
ControllerState parsed{};
|
||||
expect(switch_pro_apply_uart_packet(packet.data(), packet.size(), parsed),
|
||||
"valid UART packet was rejected");
|
||||
expect(parsed.button_a && parsed.button_l && parsed.dpad_down &&
|
||||
expect(parsed.button_east && parsed.button_left_shoulder && parsed.dpad_down &&
|
||||
parsed.dpad_left,
|
||||
"UART buttons or hat were parsed incorrectly");
|
||||
expect(parsed.lx == 0x1212 && parsed.ly == 0x3434 &&
|
||||
parsed.rx == 0x5656 && parsed.ry == 0x7878,
|
||||
expect(parsed.left_stick_x ==
|
||||
controller_axis_from_unsigned(0x1212) &&
|
||||
parsed.left_stick_y ==
|
||||
controller_axis_from_unsigned(0x3434) &&
|
||||
parsed.right_stick_x ==
|
||||
controller_axis_from_unsigned(0x5656) &&
|
||||
parsed.right_stick_y ==
|
||||
controller_axis_from_unsigned(0x7878),
|
||||
"UART axes were parsed incorrectly");
|
||||
|
||||
std::array<uint8_t, SWITCH_PRO_ENDPOINT_SIZE> current{};
|
||||
|
|
@ -628,14 +664,14 @@ void test_uart_parser_is_pure() {
|
|||
expect(current_report.inputs.buttonX && !current_report.inputs.buttonA,
|
||||
"UART parsing mutated driver context state");
|
||||
|
||||
SwitchInputState unchanged{};
|
||||
unchanged.button_home = true;
|
||||
unchanged.lx = 123;
|
||||
ControllerState unchanged{};
|
||||
unchanged.button_system = true;
|
||||
unchanged.left_stick_x = 123;
|
||||
packet.back() ^= 0xffu;
|
||||
expect(!switch_pro_apply_uart_packet(packet.data(), packet.size(),
|
||||
unchanged),
|
||||
"invalid UART checksum was accepted");
|
||||
expect(unchanged.button_home && unchanged.lx == 123,
|
||||
expect(unchanged.button_system && unchanged.left_stick_x == 123,
|
||||
"failed UART parse modified its output reference");
|
||||
}
|
||||
|
||||
|
|
@ -687,6 +723,7 @@ int main() {
|
|||
test_rumble_callbacks_and_decoders_are_isolated();
|
||||
test_grip_colors_are_isolated();
|
||||
test_lifecycle_and_invalid_instances();
|
||||
test_protocol_neutral_trigger_threshold();
|
||||
test_uart_parser_is_pure();
|
||||
if (failures != 0) {
|
||||
std::cerr << failures << " driver context test(s) failed\n";
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ def test_bluepad32_backend_lifecycle_native(tmp_path: Path) -> None:
|
|||
"slot-lighting",
|
||||
"abxy-hotkey",
|
||||
"motion-hotkey",
|
||||
"analog-state",
|
||||
"rumble-mode",
|
||||
"clear-pairings",
|
||||
"flash-core-start",
|
||||
"flash-core-failure",
|
||||
|
|
|
|||
|
|
@ -99,8 +99,8 @@ void test_microsoft_compatible_id_descriptor() {
|
|||
"Microsoft descriptor index mismatch");
|
||||
expect(kMsCompatIdDescriptor[8] == SWITCH_PICO_HID_INSTANCE_COUNT,
|
||||
"Microsoft function count mismatch");
|
||||
for (uint8_t instance = 0; instance < SWITCH_PICO_HID_INSTANCE_COUNT;
|
||||
++instance) {
|
||||
for (uint8_t instance = 0;
|
||||
instance < SWITCH_PICO_HID_INSTANCE_COUNT; ++instance) {
|
||||
const uint8_t *function = &kMsCompatIdDescriptor[16 + instance * 24];
|
||||
expect(function[0] == instance,
|
||||
"Microsoft descriptor interface mismatch");
|
||||
|
|
@ -113,8 +113,7 @@ void test_microsoft_compatible_id_descriptor() {
|
|||
}
|
||||
|
||||
void test_input_report_mapping() {
|
||||
SwitchInputState state{};
|
||||
state.lx = state.ly = state.rx = state.ry = 32768;
|
||||
ControllerState state{};
|
||||
auto report = XInputFeasibility::build_input_report(state);
|
||||
expect(report.report_id == 0 && report.report_size == 20,
|
||||
"neutral report header mismatch");
|
||||
|
|
@ -126,19 +125,19 @@ void test_input_report_mapping() {
|
|||
"neutral axes mismatch");
|
||||
|
||||
state.dpad_up = true;
|
||||
state.button_b = true;
|
||||
state.button_a = true;
|
||||
state.button_y = true;
|
||||
state.button_x = true;
|
||||
state.button_plus = true;
|
||||
state.button_minus = true;
|
||||
state.button_home = true;
|
||||
state.button_zl = true;
|
||||
state.button_zr = true;
|
||||
state.lx = 0;
|
||||
state.ly = 0;
|
||||
state.rx = UINT16_MAX;
|
||||
state.ry = UINT16_MAX;
|
||||
state.button_south = true;
|
||||
state.button_east = true;
|
||||
state.button_west = true;
|
||||
state.button_north = true;
|
||||
state.button_start = true;
|
||||
state.button_select = true;
|
||||
state.button_system = true;
|
||||
state.left_trigger = UINT16_MAX;
|
||||
state.right_trigger = UINT16_MAX;
|
||||
state.left_stick_x = INT16_MIN;
|
||||
state.left_stick_y = INT16_MIN;
|
||||
state.right_stick_x = INT16_MAX;
|
||||
state.right_stick_y = INT16_MAX;
|
||||
report = XInputFeasibility::build_input_report(state);
|
||||
expect((report.buttons & XInputFeasibility::kDpadUp) != 0,
|
||||
"D-pad mapping missing");
|
||||
|
|
@ -148,15 +147,22 @@ void test_input_report_mapping() {
|
|||
(report.buttons & XInputFeasibility::kButtonY) != 0,
|
||||
"positional face-button mapping mismatch");
|
||||
expect(report.left_trigger == 0xff && report.right_trigger == 0xff,
|
||||
"digital trigger mapping mismatch");
|
||||
"full analog trigger mapping mismatch");
|
||||
expect(report.left_x == INT16_MIN && report.left_y == INT16_MAX &&
|
||||
report.right_x == INT16_MAX && report.right_y == -INT16_MAX,
|
||||
report.right_x == INT16_MAX &&
|
||||
report.right_y == -INT16_MAX,
|
||||
"axis endpoint mapping mismatch");
|
||||
|
||||
state.left_trigger = 0x8000;
|
||||
state.right_trigger = 0x7fff;
|
||||
report = XInputFeasibility::build_input_report(state);
|
||||
expect(report.left_trigger == 0x80 && report.right_trigger == 0x7f,
|
||||
"analog trigger precision was discarded");
|
||||
}
|
||||
|
||||
void test_rumble_report() {
|
||||
const uint8_t packet[8] = {0x00, 0x08, 0x00, 0xa5, 0x5a, 0x00, 0x00, 0x00};
|
||||
SwitchRumbleOutput output{};
|
||||
ControllerRumbleOutput output{};
|
||||
expect(
|
||||
XInputFeasibility::parse_rumble_report(packet, sizeof(packet), &output),
|
||||
"valid rumble report rejected");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue