#include #include #include #include #include #ifdef SWITCH_PICO_HAPTICS_EXPERIMENT #include #include #endif #include #include "bluetooth_transport_config.h" #include "parser/uni_hid_parser_switch2.h" #include "parser/uni_switch2_haptics.h" #include "parser/uni_switch2_pairing.h" #include "parser/uni_hid_parser_wii.h" #include "platform/pico/controller_color_config.h" #include "input/switch2_wake.h" #include "pico/critical_section.h" #include "profile/profile_storage.h" #include "profile/controller_profile_runtime.h" namespace { bool incoming_connections = false; bool scanning_enabled = false; bool aggregate_scanning_enabled = false; bool background_scan_parameters = false; bool classic_scanning_enabled = false; uni_platform* installed_platform = nullptr; bool observed_status_led_on = false; int observed_status_led_writes = 0; uint32_t now_ms = 0; uint32_t now_sub_ms_us = 0; struct WiiAccelFixture { uni_hid_device_t* device = nullptr; int32_t acceleration[3]{}; uint32_t sequence = 0; bool valid = false; }; WiiAccelFixture remote_accel_fixture; WiiAccelFixture nunchuk_accel_fixture; #ifdef SWITCH2_BRIDGE_WII_INPUT WiiAccelFixture gyro_fixture; bool wii_rumble_ready = true; #endif bool wii_accel_fixture_snapshot(const WiiAccelFixture& fixture, uni_hid_device_t* controller, int32_t acceleration[3], uint32_t* sequence) { if (!fixture.valid || fixture.device != controller) return false; memcpy(acceleration, fixture.acceleration, sizeof(fixture.acceleration)); *sequence = fixture.sequence; return true; } bool bondable = true; bool ssp_auto_accept = true; uint8_t accepted_stk_methods = 0xff; uint16_t link_supervision_timeout = 0; btstack_packet_handler_t pairing_event_handler = nullptr; btstack_packet_handler_t identity_event_handler = nullptr; int confirmation_accepts = 0; int confirmation_rejections = 0; int passkey_accepts = 0; int passkey_rejections = 0; int delete_key_calls = 0; bd_addr_t classic_bonds[4]{}; int classic_bond_count = 0; bd_addr_t ble_bonds[4]{}; int ble_bond_types[4]{}; int ble_bond_count = 0; bool flash_core_init_result = true; int flash_core_init_calls = 0; int core1_launch_calls = 0; bool expect_configuration_timer_prearmed = false; uint32_t expected_configuration_timer_add_count = 0; int cyw43_init_calls = 0; int uni_init_calls = 0; void (*during_uni_init)() = nullptr; int switch2_wake_initializations = 0; int switch2_wake_requests = 0; Switch2WakeDiagnostics wake_diagnostics{}; bool wake_dispatch_available = true; bool switch2_connections_ready = true; int device_disconnect_calls = 0; uni_hid_device_t* last_disconnected_device = nullptr; uni_hid_device_t* lookup_devices[8]{}; size_t lookup_device_count = 0; uint32_t expected_pending_clear_token = 0; uint32_t repeated_in_progress_clear_token = 0; bool repeat_clear_during_disconnect = false; void require_clear_completion_pending(); void require_clear_snapshot_published(); void request_repeated_clear_during_disconnect(); gap_connection_type_t gap_connection_types[256]{}; uint16_t negotiated_intervals[256]{}; uint16_t pending_intervals[256]{}; unsigned interval_requests[256]{}; bool defer_interval_updates = false; bool reject_interval_updates = false; uint8_t xbox_left_trigger = 0; uint8_t xbox_right_trigger = 0; unsigned xbox_quad_calls = 0; unsigned ordinary_smp_requests = 0; bd_addr_t switch2_pairings[UNI_SWITCH2_PAIRING_CAPACITY]{}; uint8_t switch2_pairing_types[UNI_SWITCH2_PAIRING_CAPACITY]{}; uint8_t switch2_pairing_count = 0; bool switch2_clear_succeeds = true; struct Switch2HostEvent { uni_hid_device_t* device; uni_switch2_haptics_frame_t frame; uint32_t received_ms; uint16_t duration_ms; uint8_t weak; uint8_t strong; bool hd; }; std::vector switch2_host_events; uint32_t switch2_output_drops = 0; struct LocalRumbleEvent { uni_hid_device_t* device; uint16_t duration_ms; uint8_t high; uint8_t low; }; std::vector local_rumble_events; struct WiiModeEvent { uni_hid_device_t* device; wii_mode_t mode; }; std::vector wii_mode_events; bool wii_dispatch_active = false; struct CoreStopped {}; void require(bool condition, const char* message) { if (!condition) { std::cerr << message << '\n'; std::exit(1); } } void play_rumble(uni_hid_device_t* device, uint16_t, uint16_t duration_ms, uint8_t high, uint8_t low) { local_rumble_events.push_back({device, duration_ms, high, low}); ++device->rumble_calls; device->last_high = high; device->last_low = low; device->last_rumble_duration_ms = duration_ms; } void set_lightbar(uni_hid_device_t* device, uint8_t red, uint8_t green, uint8_t blue) { ++device->lightbar_calls; device->lightbar_red = red; device->lightbar_green = green; device->lightbar_blue = blue; } void set_player_leds(uni_hid_device_t* device, uint8_t leds) { ++device->player_led_calls; device->player_leds = leds; } uni_hid_device_t device( int idx, bool gamepad = true, uni_bt_conn_protocol_t protocol = UNI_BT_CONN_PROTOCOL_NONE) { uni_hid_device_t result{}; result.idx = idx; result.gamepad = gamepad; result.conn.protocol = protocol; result.conn.handle = static_cast(0x40 + idx); negotiated_intervals[result.conn.handle] = 6; pending_intervals[result.conn.handle] = 0; result.conn.btaddr[5] = static_cast(idx + 1); result.vendor_id = static_cast(0x1000 + idx); result.product_id = static_cast(0x2000 + idx); result.report_parser.play_dual_rumble = play_rumble; gap_connection_types[result.conn.handle] = protocol == UNI_BT_CONN_PROTOCOL_BR_EDR ? GAP_CONNECTION_ACL : protocol == UNI_BT_CONN_PROTOCOL_BLE ? GAP_CONNECTION_LE : GAP_CONNECTION_INVALID; return result; } void register_lookup_device(uni_hid_device_t* candidate) { require(lookup_device_count < sizeof(lookup_devices) / sizeof(lookup_devices[0]), "test BLE lookup registry overflow"); lookup_devices[lookup_device_count++] = candidate; } } // namespace void xboxone_play_quad_rumble(uni_hid_device_t* device, uint16_t delay, uint16_t duration, uint8_t left, uint8_t right, uint8_t weak, uint8_t strong) { ++xbox_quad_calls; xbox_left_trigger = left; xbox_right_trigger = right; play_rumble(device, delay, duration, weak, strong); } void uni_hid_parser_xboxone_play_dual_rumble( uni_hid_device_t* device, uint16_t delay, uint16_t duration, uint8_t weak, uint8_t strong) { xboxone_play_quad_rumble(device, delay, duration, 0, 0, weak, strong); } extern "C" void __real_sm_request_pairing(hci_con_handle_t) { require(SWITCH_PICO_ENABLE_BLE, "disabled BLE transport entered SMP"); ++ordinary_smp_requests; } extern "C" bool uni_switch2_pairing_get( uint8_t index, uint8_t* address_type, uint8_t address[6]) { if (index >= switch2_pairing_count) { return false; } *address_type = switch2_pairing_types[index]; memcpy(address, switch2_pairings[index], sizeof(bd_addr_t)); return true; } extern "C" bool uni_switch2_pairing_known( uint8_t address_type, const uint8_t address[6]) { for (uint8_t index = 0; index < switch2_pairing_count; ++index) { if (switch2_pairing_types[index] == address_type && memcmp(switch2_pairings[index], address, sizeof(bd_addr_t)) == 0) { return true; } } return false; } extern "C" bool uni_switch2_pairing_clear(void) { if (!switch2_clear_succeeds) { return false; } switch2_pairing_count = 0; return true; } extern "C" bool uni_hid_parser_switch2_is_ble_device( const uni_hid_device_t* device) { return device != nullptr && device->conn.protocol == UNI_BT_CONN_PROTOCOL_BLE && device->vendor_id == UNI_SW2_NINTENDO_VID && (device->product_id == UNI_SW2_PRO_PID || device->product_id == UNI_SW2_JOYCON_L_PID || device->product_id == UNI_SW2_JOYCON_R_PID); } extern "C" bool uni_hid_parser_switch2_queue_haptics( uni_hid_device_t* device, const uni_switch2_haptics_frame_t* frame, uint32_t received_ms) { require(uni_switch2_haptics_valid(frame), "backend emitted invalid physical HD frame"); const bool stop = device->product_id == UNI_SW2_PRO_PID ? uni_switch2_haptics_is_stop(frame) : [&]() { uni_switch2_haptics_frame_t stereo = *frame; stereo.sides[1] = stereo.sides[0]; return uni_switch2_haptics_is_stop(&stereo); }(); if (device->switch2_host_blocked && !stop) return false; ++device->switch2_host_calls; switch2_host_events.push_back({device, *frame, received_ms, 0, 0, 0, true}); return true; } extern "C" bool uni_hid_parser_switch2_queue_rumble( uni_hid_device_t* device, uint8_t weak, uint8_t strong, uint16_t duration_ms, uint32_t received_ms) { if (device->switch2_host_blocked && (weak | strong) != 0) return false; ++device->switch2_host_calls; device->last_high = weak; device->last_low = strong; device->last_rumble_duration_ms = duration_ms; switch2_host_events.push_back({device, {}, received_ms, duration_ms, weak, strong, false}); return true; } extern "C" void uni_hid_parser_switch2_reset_haptics(uni_hid_device_t* device) { ++device->switch2_haptics_resets; device->last_high = 0; device->last_low = 0; device->last_rumble_duration_ms = 0; } extern "C" uint32_t uni_hid_parser_switch2_haptics_dropped(void) { return switch2_output_drops; } extern "C" uint8_t uni_hid_parser_switch2_extra_buttons( const uni_hid_device_t* device) { return uni_hid_parser_switch2_is_ble_device(device) ? device->switch2_extra_buttons : 0; } extern "C" bool uni_hid_parser_switch2_identity_address_type( const uni_hid_device_t* device, uint8_t* output) { if (!uni_hid_parser_switch2_is_ble_device(device) || !device->switch2_identity_valid || output == nullptr) { return false; } *output = device->switch2_identity_address_type; return true; } bool uni_hid_device_is_gamepad(const uni_hid_device_t* device) { return device != nullptr && device->gamepad; } int uni_hid_device_get_idx_for_instance(const uni_hid_device_t* device) { return device == nullptr ? -1 : device->idx; } uni_hid_device_t* uni_hid_device_get_instance_for_connection_handle( hci_con_handle_t handle) { for (size_t index = 0; index < lookup_device_count; ++index) { if (lookup_devices[index]->conn.handle == handle) { return lookup_devices[index]; } } return nullptr; } uni_hid_device_t* uni_hid_device_get_instance_for_address(const bd_addr_t address) { for (size_t index = 0; index < lookup_device_count; ++index) { if (memcmp(lookup_devices[index]->conn.btaddr, address, sizeof(bd_addr_t)) == 0) { return lookup_devices[index]; } } return nullptr; } void uni_hid_device_disconnect(uni_hid_device_t* device) { require_clear_completion_pending(); request_repeated_clear_during_disconnect(); ++device_disconnect_calls; last_disconnected_device = device; } void uni_bt_allow_incoming_connections(bool enabled) { require(!enabled || SWITCH_PICO_ENABLE_CLASSIC, "disabled Classic transport enabled incoming page scanning"); incoming_connections = enabled; } void uni_bt_bredr_scan_start() { require(SWITCH_PICO_ENABLE_CLASSIC, "disabled Classic inquiry started"); classic_scanning_enabled = true; } void uni_bt_bredr_scan_stop() { require(SWITCH_PICO_ENABLE_CLASSIC, "disabled Classic inquiry was accessed"); classic_scanning_enabled = false; } void uni_bt_le_scan_start() { require(SWITCH_PICO_ENABLE_BLE, "disabled BLE scanning started"); scanning_enabled = true; } void uni_bt_le_scan_stop() { require(SWITCH_PICO_ENABLE_BLE, "disabled BLE scanning was accessed"); scanning_enabled = false; } void uni_bt_le_set_background_scan(bool enabled) { require(SWITCH_PICO_ENABLE_BLE, "disabled BLE scan parameters were accessed"); require(!scanning_enabled, "LE scan timing must change only while scanning is stopped"); background_scan_parameters = enabled; } void uni_bt_start_scanning_and_autoconnect_unsafe() { require_clear_completion_pending(); require_clear_snapshot_published(); if (aggregate_scanning_enabled) { return; } aggregate_scanning_enabled = true; if (SWITCH_PICO_ENABLE_CLASSIC) uni_bt_bredr_scan_start(); if (SWITCH_PICO_ENABLE_BLE) uni_bt_le_scan_start(); } void uni_bt_stop_scanning_unsafe() { if (!aggregate_scanning_enabled) { return; } aggregate_scanning_enabled = false; if (SWITCH_PICO_ENABLE_CLASSIC) uni_bt_bredr_scan_stop(); if (SWITCH_PICO_ENABLE_BLE) uni_bt_le_scan_stop(); } void uni_bt_del_keys_unsafe() { require_clear_completion_pending(); ++delete_key_calls; classic_bond_count = 0; ble_bond_count = 0; } gap_connection_type_t gap_get_connection_type( hci_con_handle_t connection_handle) { return connection_handle < sizeof(gap_connection_types) / sizeof(gap_connection_types[0]) ? gap_connection_types[connection_handle] : GAP_CONNECTION_INVALID; } uint16_t gap_le_connection_interval(hci_con_handle_t handle) { require(handle < 256, "interval read must target a valid fake connection"); return negotiated_intervals[handle]; } int gap_update_connection_parameters(hci_con_handle_t handle, uint16_t minimum, uint16_t maximum, uint16_t latency, uint16_t supervision_timeout) { require(handle < 256 && gap_get_connection_type(handle) == GAP_CONNECTION_LE, "interval policy must not update a Classic or invalid link"); require(minimum == maximum && latency == 0 && supervision_timeout >= 100, "interval request must retain valid supervision and peripheral latency"); ++interval_requests[handle]; if (reject_interval_updates) return 0x0c; if (defer_interval_updates) pending_intervals[handle] = minimum; else negotiated_intervals[handle] = minimum; return ERROR_CODE_SUCCESS; } int gap_link_key_iterator_init(btstack_link_key_iterator_t* iterator) { iterator->index = 0; return 1; } int gap_link_key_iterator_get_next( btstack_link_key_iterator_t* iterator, bd_addr_t address, link_key_t link_key, link_key_type_t* type) { if (iterator->index >= classic_bond_count) { return 0; } memcpy(address, classic_bonds[iterator->index], sizeof(bd_addr_t)); memset(link_key, iterator->index + 1, sizeof(link_key_t)); *type = 0; ++iterator->index; return 1; } void gap_link_key_iterator_done(btstack_link_key_iterator_t*) { } int le_device_db_max_count() { return 4; } void le_device_db_info( int index, int* address_type, bd_addr_t address, sm_key_t irk) { if (index < ble_bond_count) { *address_type = ble_bond_types[index]; memcpy(address, ble_bonds[index], sizeof(bd_addr_t)); if (irk != nullptr) { memset(irk, index + 1, sizeof(sm_key_t)); } return; } *address_type = BD_ADDR_TYPE_UNKNOWN; } void gap_set_bondable_mode(int enabled) { bondable = enabled != 0; } void gap_set_link_supervision_timeout(uint16_t timeout) { require(SWITCH_PICO_ENABLE_CLASSIC, "disabled Classic link policy was accessed"); link_supervision_timeout = timeout; } void gap_ssp_set_auto_accept(int auto_accept) { require(SWITCH_PICO_ENABLE_CLASSIC, "disabled Classic SSP was accessed"); ssp_auto_accept = auto_accept != 0; } void sm_set_accepted_stk_generation_methods(uint8_t methods) { require(SWITCH_PICO_ENABLE_BLE, "uninitialized SM pairing policy was accessed"); accepted_stk_methods = methods; } int gap_ssp_confirmation_response(const bd_addr_t) { require(SWITCH_PICO_ENABLE_CLASSIC, "disabled Classic confirmation was accepted"); ++confirmation_accepts; return 0; } int gap_ssp_confirmation_negative(const bd_addr_t) { require(SWITCH_PICO_ENABLE_CLASSIC, "disabled Classic confirmation was accessed"); ++confirmation_rejections; return 0; } int gap_ssp_passkey_response(const bd_addr_t, uint32_t) { require(SWITCH_PICO_ENABLE_CLASSIC, "disabled Classic passkey was accepted"); ++passkey_accepts; return 0; } int gap_ssp_passkey_negative(const bd_addr_t) { require(SWITCH_PICO_ENABLE_CLASSIC, "disabled Classic passkey was accessed"); ++passkey_rejections; return 0; } void hci_add_event_handler( btstack_packet_callback_registration_t* callback_handler) { pairing_event_handler = callback_handler->callback; } void sm_add_event_handler( btstack_packet_callback_registration_t* callback_handler) { require(SWITCH_PICO_ENABLE_BLE, "event handler added to uninitialized SM"); identity_event_handler = callback_handler->callback; } uint8_t hci_event_packet_get_type(const uint8_t* packet) { return packet[0]; } void copy_event_address(const uint8_t* packet, bd_addr_t address) { for (size_t index = 0; index < sizeof(bd_addr_t); ++index) { address[index] = packet[7 - index]; } } void hci_event_user_confirmation_request_get_bd_addr( const uint8_t* packet, bd_addr_t address) { copy_event_address(packet, address); } void hci_event_user_passkey_request_get_bd_addr( const uint8_t* packet, bd_addr_t address) { copy_event_address(packet, address); } hci_con_handle_t sm_event_handle(const uint8_t* packet) { return static_cast(packet[2]) | static_cast(packet[3] << 8); } void copy_sm_event_address(const uint8_t* packet, size_t offset, bd_addr_t address) { for (size_t index = 0; index < sizeof(bd_addr_t); ++index) { address[index] = packet[offset + sizeof(bd_addr_t) - 1 - index]; } } hci_con_handle_t sm_event_identity_resolving_started_get_handle( const uint8_t* packet) { return sm_event_handle(packet); } hci_con_handle_t sm_event_identity_resolving_failed_get_handle( const uint8_t* packet) { return sm_event_handle(packet); } hci_con_handle_t sm_event_identity_resolving_succeeded_get_handle( const uint8_t* packet) { return sm_event_handle(packet); } uint8_t sm_event_identity_resolving_succeeded_get_addr_type( const uint8_t* packet) { return packet[4]; } void sm_event_identity_resolving_succeeded_get_address( const uint8_t* packet, bd_addr_t address) { copy_sm_event_address(packet, 5, address); } uint8_t sm_event_identity_resolving_succeeded_get_identity_addr_type( const uint8_t* packet) { return packet[11]; } void sm_event_identity_resolving_succeeded_get_identity_address( const uint8_t* packet, bd_addr_t address) { copy_sm_event_address(packet, 12, address); } hci_con_handle_t sm_event_identity_created_get_handle( const uint8_t* packet) { return sm_event_handle(packet); } void sm_event_identity_created_get_address( const uint8_t* packet, bd_addr_t address) { copy_sm_event_address(packet, 5, address); } uint8_t sm_event_identity_created_get_identity_addr_type( const uint8_t* packet) { return packet[11]; } void sm_event_identity_created_get_identity_address( const uint8_t* packet, bd_addr_t address) { copy_sm_event_address(packet, 12, address); } hci_con_handle_t sm_event_reencryption_started_get_handle( const uint8_t* packet) { return sm_event_handle(packet); } uint8_t sm_event_reencryption_started_get_addr_type( const uint8_t* packet) { return packet[4]; } void sm_event_reencryption_started_get_address( const uint8_t* packet, bd_addr_t address) { copy_sm_event_address(packet, 5, address); } hci_con_handle_t sm_event_reencryption_complete_get_handle( const uint8_t* packet) { return sm_event_handle(packet); } uint8_t sm_event_reencryption_complete_get_addr_type( const uint8_t* packet) { return packet[4]; } void sm_event_reencryption_complete_get_address( const uint8_t* packet, bd_addr_t address) { copy_sm_event_address(packet, 5, address); } uint8_t sm_event_reencryption_complete_get_status( const uint8_t* packet) { return packet[11]; } void uni_platform_set_custom(uni_platform* platform) { installed_platform = platform; } int uni_init(int, const char**) { ++uni_init_calls; if (during_uni_init != nullptr) during_uni_init(); return 0; } bool flash_safe_execute_core_init() { ++flash_core_init_calls; return flash_core_init_result; } int cyw43_arch_init() { ++cyw43_init_calls; return 0; } void cyw43_arch_gpio_put(int, bool enabled) { observed_status_led_on = enabled; ++observed_status_led_writes; } void multicore_launch_core1_with_stack(void (*)(), uint32_t* stack, size_t size) { require(stack != nullptr && size % 8 == 0, "core 1 launch needs an aligned bounded stack"); ++core1_launch_calls; } void tight_loop_contents() { throw CoreStopped{}; } uint32_t btstack_run_loop_get_time_ms() { return now_ms; } uint32_t time_us_32() { return now_ms * 1000u; } uint64_t time_us_64() { return uint64_t{now_ms} * 1000 + now_sub_ms_us; } void switch2_wake_initialize() { ++switch2_wake_initializations; wake_diagnostics.configured = SWITCH_PICO_ENABLE_BLE; if (!SWITCH_PICO_ENABLE_BLE) switch2_connections_ready = true; } bool switch2_wake_ready_for_connections() { return switch2_connections_ready; } bool switch2_wake_request() { ++switch2_wake_requests; if (!wake_diagnostics.configured || wake_diagnostics.busy || !wake_dispatch_available) return false; ++wake_diagnostics.accepted_requests; wake_diagnostics.busy = true; return true; } void switch2_wake_diagnostics(Switch2WakeDiagnostics* output) { *output = wake_diagnostics; } #include "core/controller_identity.cpp" namespace { unsigned state_lock_depth = 0; void (*after_backend_state_unlock)() = nullptr; void tracked_state_lock_enter(critical_section_t* lock) { critical_section_enter_blocking(lock); ++state_lock_depth; } void tracked_state_lock_exit(critical_section_t* lock) { require(state_lock_depth != 0, "state lock exit must match an enter"); --state_lock_depth; critical_section_exit(lock); if (state_lock_depth == 0 && after_backend_state_unlock) after_backend_state_unlock(); } } // namespace #define critical_section_enter_blocking tracked_state_lock_enter #define critical_section_exit tracked_state_lock_exit #include "input/bluepad32_input_backend.cpp" #undef critical_section_enter_blocking #undef critical_section_exit extern "C" void uni_hid_parser_wii_set_mode( uni_hid_device_t* device, wii_mode_t mode) { require(wii_dispatch_active && state_lock_depth == 0, "Wii parser mode changes must run in the Bluetooth timer without the state lock"); wii_mode_events.push_back({device, mode}); device->controller_subtype = mode == WII_MODE_VERTICAL ? CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL : CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL; // The real parser's report reconfiguration can synchronously announce ready. require(platform_on_device_ready(device) == UNI_ERROR_SUCCESS, "Wii mode reconfiguration must retain the ready connection"); } extern "C" bool uni_hid_parser_wii_accel_snapshot( uni_hid_device_t* controller, int32_t acceleration[3], uint32_t* sequence) { return wii_accel_fixture_snapshot(remote_accel_fixture, controller, acceleration, sequence); } extern "C" bool uni_hid_parser_wii_nunchuk_accel_snapshot( uni_hid_device_t* controller, int32_t acceleration[3], uint32_t* sequence) { return wii_accel_fixture_snapshot(nunchuk_accel_fixture, controller, acceleration, sequence); } #ifdef SWITCH2_BRIDGE_WII_INPUT extern "C" bool uni_hid_parser_wii_gyro_snapshot( uni_hid_device_t* controller, int32_t gyro[3], uint32_t* sequence) { return wii_accel_fixture_snapshot(gyro_fixture, controller, gyro, sequence); } extern "C" bool uni_hid_parser_wii_rumble_ready(uni_hid_device_t*) { require(state_lock_depth == 0, "Wii dispatch readiness must be checked on Core 1 outside the state lock"); return wii_rumble_ready; } #endif #ifdef SWITCH_PICO_HAPTICS_EXPERIMENT namespace { std::vector native_timers; std::array last_native_packet{}; uint16_t last_native_cid = 0; struct NativePacketObservation { uint64_t sent_us; uint16_t cid; std::array data; }; std::vector native_packets; } void native_test_add_timer(btstack_timer_source_t* timer) { require(state_lock_depth == 0, "HD timer scheduling must not hold the backend lock"); btstack_run_loop_remove_timer(timer); timer->due_ms = uint64_t{now_ms} + timer->timeout_ms + 1; native_timers.push_back(timer); } int btstack_run_loop_remove_timer(btstack_timer_source_t* timer) { require(state_lock_depth == 0, "HD timer cancellation must not hold the backend lock"); const auto found = std::find(native_timers.begin(), native_timers.end(), timer); if (found == native_timers.end()) return 0; native_timers.erase(found); return 1; } uint16_t l2cap_get_remote_mtu_for_local_cid(uint16_t) { return 143; } bool l2cap_can_send_packet_now(uint16_t) { return true; } int hci_number_free_acl_slots_for_handle(uint16_t) { return 8; } uint8_t l2cap_request_can_send_now_event(uint16_t cid) { require(state_lock_depth == 0, "HD permission callbacks must not hold the backend lock"); for (const auto& slot : g_slots) { if (slot.device != nullptr && slot.device->conn.interrupt_cid == cid) { (void)uni_platform_on_l2cap_can_send_now(slot.device, cid); return ERROR_CODE_SUCCESS; } } require(false, "native permission targeted a detached connection"); return 1; } uint8_t l2cap_send(uint16_t cid, const uint8_t* data, uint16_t size) { require(state_lock_depth == 0, "HD packet writes must not hold the backend lock"); require(size == last_native_packet.size(), "native report size changed"); std::copy(data, data + size, last_native_packet.begin()); last_native_cid = cid; native_packets.push_back({time_us_64(), cid, last_native_packet}); return ERROR_CODE_SUCCESS; } void haptics_transport_probe_prepare() {} void haptics_transport_probe_begin(uint32_t, uint32_t, uint16_t) {} void haptics_transport_probe_end() {} void haptics_transport_probe_timer(uint32_t) {} void haptics_transport_probe_permission(uint32_t) {} void haptics_transport_probe_send(uint32_t, uint32_t, bool) {} #endif namespace { uint8_t profile_flash[PROFILE_STORAGE_ARENA_COUNT][PROFILE_STORAGE_ARENA_SIZE]{}; ProfileStorage runtime_profile_storage; bool runtime_profile_storage_initialized = false; bool fail_profile_program = false; unsigned profile_write_attempts = 0; unsigned pair_observation_count = 0; void (*before_pair_seed)(const ControllerIdentity&) = nullptr; bool runtime_profile_read(void*, uint8_t arena, size_t offset, uint8_t* output, size_t size) { require(state_lock_depth == 0, "profile reads must not hold the input state lock"); if (arena >= PROFILE_STORAGE_ARENA_COUNT || offset > PROFILE_STORAGE_ARENA_SIZE || size > PROFILE_STORAGE_ARENA_SIZE - offset) { return false; } memcpy(output, &profile_flash[arena][offset], size); return true; } bool runtime_profile_erase(void*, uint8_t arena) { ++profile_write_attempts; require(state_lock_depth == 0, "profile erases must not hold the input state lock"); if (arena >= PROFILE_STORAGE_ARENA_COUNT) return false; memset(profile_flash[arena], 0xff, PROFILE_STORAGE_ARENA_SIZE); return true; } bool runtime_profile_program(void*, uint8_t arena, size_t offset, const uint8_t* page, size_t size) { ++profile_write_attempts; require(state_lock_depth == 0, "profile writes must not hold the input state lock"); if (fail_profile_program || arena >= PROFILE_STORAGE_ARENA_COUNT || size != PROFILE_STORAGE_PAGE_SIZE || offset % PROFILE_STORAGE_PAGE_SIZE != 0 || offset > PROFILE_STORAGE_ARENA_SIZE - size) { return false; } for (size_t index = 0; index < size; ++index) { profile_flash[arena][offset + index] &= page[index]; } return memcmp(&profile_flash[arena][offset], page, size) == 0; } ProfileStorageIo runtime_profile_io() { return {nullptr, PROFILE_STORAGE_ARENA_SIZE, PROFILE_STORAGE_SECTOR_SIZE, PROFILE_STORAGE_PAGE_SIZE, runtime_profile_read, runtime_profile_erase, runtime_profile_program}; } void initialize_runtime_profile_storage() { if (runtime_profile_storage_initialized) return; memset(profile_flash, 0xff, sizeof(profile_flash)); require(runtime_profile_storage.initialize(runtime_profile_io()), "runtime profile catalog must initialize"); runtime_profile_storage_initialized = true; } void require_clear_completion_pending() { if (expected_pending_clear_token != 0) { require(!bluepad32_input_backend_clear_pairings_completed( g_pairing_snapshot, expected_pending_clear_token), "pairing clear token completed before Core1 work finished"); } } void require_clear_snapshot_published() { if (expected_pending_clear_token != 0) { require(g_pairing_snapshot.status == Bluepad32PairingSnapshotStatus::kReady && g_pairing_snapshot.record_count == 0, "pairing clear policy ran before empty snapshot publication"); } } void request_repeated_clear_during_disconnect() { if (repeat_clear_during_disconnect) { repeat_clear_during_disconnect = false; repeated_in_progress_clear_token = bluepad32_input_backend_clear_pairings(); } } } // namespace ControllerIdentity observed_profile_identities[8]{}; size_t observed_profile_identity_count = 0; AdapterConfiguration runtime_configuration{}; uint32_t runtime_configuration_generation = 1; void configuration_service_prepare() {} void configuration_service_initialize_on_storage_core() {} void configuration_service_task_on_storage_core(uint32_t) { if (expect_configuration_timer_prearmed) { require(g_configuration_timer.add_count == expected_configuration_timer_add_count, "configuration work ran before its timer was rearmed"); } } void profile_service_prepare() {} void profile_service_initialize_on_storage_core() {} void profile_service_task_on_storage_core(uint32_t) { if (expect_configuration_timer_prearmed) { require(g_configuration_timer.add_count == expected_configuration_timer_add_count, "profile work ran before its timer was rearmed"); } } bool profile_service_observe_identity_on_storage_core( const ControllerIdentity& identity) { require(observed_profile_identity_count < sizeof(observed_profile_identities) / sizeof(observed_profile_identities[0]), "profile identity observation fixture overflow"); observed_profile_identities[observed_profile_identity_count++] = identity; if (runtime_profile_storage_initialized) { const ProfileStorageResult result = runtime_profile_storage.ensure_identity(identity); return result == ProfileStorageResult::kOk || result == ProfileStorageResult::kUnchanged; } return true; } bool profile_service_observe_joycon_pair_on_storage_core( const ControllerIdentity& identity) { require(state_lock_depth == 0, "pair admission must release the input state lock"); ++pair_observation_count; if (before_pair_seed != nullptr) before_pair_seed(identity); initialize_runtime_profile_storage(); const ProfileStorageResult result = runtime_profile_storage.ensure_joycon_pair(identity); return result == ProfileStorageResult::kOk || result == ProfileStorageResult::kUnchanged; } void configuration_service_snapshot(ConfigurationServiceSnapshot* output) { *output = {}; output->state = ConfigurationServiceState::kReady; output->configuration = runtime_configuration; output->generation = runtime_configuration_generation; } uint32_t configuration_service_reset_generation() { return 0; } uint32_t profile_service_database_generation() { return runtime_profile_storage.snapshot().generation; } void profile_service_active_profile_snapshot( const ControllerIdentity& identity, ProfileServiceActiveProfileSnapshot* output) { *output = {}; output->metadata.state = ProfileServiceState::kReady; output->metadata.generation = profile_service_database_generation(); const auto* owner = runtime_profile_storage.find(identity); if (owner == nullptr) return; output->profile_index = owner->active_profile; output->valid = runtime_profile_storage.get( identity, owner->active_profile, &output->profile) == ProfileStorageResult::kOk; } ConfigurationTransactionStatus profile_service_activate_internal( uint32_t, const ControllerIdentity& identity, uint8_t profile_index) { const auto result = runtime_profile_storage.activate(identity, profile_index); return result == ProfileStorageResult::kOk || result == ProfileStorageResult::kUnchanged ? ConfigurationTransactionStatus::kCommitted : ConfigurationTransactionStatus::kStorageError; } #ifdef SWITCH_PICO_USB_OUTPUT_MODES 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, SWITCH_COLOR_SLOT_1_B}, {SWITCH_COLOR_SLOT_2_R, SWITCH_COLOR_SLOT_2_G, SWITCH_COLOR_SLOT_2_B}, {SWITCH_COLOR_SLOT_3_R, SWITCH_COLOR_SLOT_3_G, SWITCH_COLOR_SLOT_3_B}, {SWITCH_COLOR_SLOT_4_R, SWITCH_COLOR_SLOT_4_G, SWITCH_COLOR_SLOT_4_B}, }; return instance < sizeof(grips) / sizeof(grips[0]) ? switch_pro_calibrate_light_color(grips[instance]) : SwitchRgbColor{}; } namespace { bool read_controller_state(uint8_t slot, ControllerState* output) { Bluepad32SlotSnapshot snapshot{}; bluepad32_input_backend_snapshot(slot, &snapshot); if (output != nullptr) { *output = snapshot.state; } return snapshot.active; } void start_backend() { bluepad32_input_backend_init(); platform_on_init_complete(); require(incoming_connections == (SWITCH_PICO_ENABLE_CLASSIC != 0) && scanning_enabled == (SWITCH_PICO_ENABLE_BLE != 0) && classic_scanning_enabled == (SWITCH_PICO_ENABLE_CLASSIC != 0) && !bondable && switch2_wake_initializations == 1, "initialization must scan and admit only the selected transports"); if (SWITCH_PICO_ENABLE_CLASSIC) { require(link_supervision_timeout == kClassicLinkSupervisionTimeout && !ssp_auto_accept && pairing_event_handler != nullptr, "Classic initialization must retain its reconnect and SSP policy"); } else { require(pairing_event_handler == nullptr, "BLE-only initialization registered a Classic pairing handler"); } if (SWITCH_PICO_ENABLE_BLE) { require(accepted_stk_methods == 0 && identity_event_handler != nullptr, "BLE initialization must reject pairing until its window opens"); } else { require(identity_event_handler == nullptr, "Classic-only initialization registered an uninitialized SM handler"); } } void start_pairing_backend() { start_backend(); bluepad32_input_backend_open_pairing_window(); process_rumble_timer(&g_rumble_timer); require(g_connection_policy_state == ConnectionPolicyState::Open && scanning_enabled == (SWITCH_PICO_ENABLE_BLE != 0) && classic_scanning_enabled == (SWITCH_PICO_ENABLE_CLASSIC != 0) && incoming_connections == (SWITCH_PICO_ENABLE_CLASSIC != 0), "test connection setup requires an open pairing window"); } void dispatch_pairing_event(uint8_t event_type) { uint8_t packet[8] = {event_type, 6, 1, 2, 3, 4, 5, 6}; pairing_event_handler(HCI_EVENT_PACKET, 0, packet, sizeof(packet)); } void write_event_address(uint8_t* packet, size_t offset, const bd_addr_t address) { for (size_t index = 0; index < sizeof(bd_addr_t); ++index) { packet[offset + index] = address[sizeof(bd_addr_t) - 1 - index]; } } void dispatch_identity_event(uint8_t event_type, const uni_hid_device_t& controller, uint8_t identity_address_type, const bd_addr_t identity_address, uint8_t status = ERROR_CODE_SUCCESS) { uint8_t packet[20]{}; size_t packet_size = 0; packet[0] = event_type; packet[2] = static_cast(controller.conn.handle); packet[3] = static_cast(controller.conn.handle >> 8); switch (event_type) { case SM_EVENT_IDENTITY_RESOLVING_STARTED: case SM_EVENT_IDENTITY_RESOLVING_FAILED: packet_size = 11; break; case SM_EVENT_IDENTITY_RESOLVING_SUCCEEDED: packet_size = sizeof(packet); packet[4] = BD_ADDR_TYPE_LE_RANDOM; write_event_address(packet, 5, controller.conn.btaddr); packet[11] = identity_address_type; write_event_address(packet, 12, identity_address); break; case SM_EVENT_IDENTITY_CREATED: packet_size = sizeof(packet); packet[4] = identity_address_type; write_event_address(packet, 5, identity_address); packet[11] = identity_address_type; write_event_address(packet, 12, identity_address); break; case SM_EVENT_REENCRYPTION_STARTED: packet_size = 11; packet[4] = identity_address_type; write_event_address(packet, 5, identity_address); break; case SM_EVENT_REENCRYPTION_COMPLETE: packet_size = 12; packet[4] = identity_address_type; write_event_address(packet, 5, identity_address); packet[11] = status; break; default: require(false, "unsupported identity event fixture"); } packet[1] = static_cast(packet_size - 2); identity_event_handler( HCI_EVENT_PACKET, 0, packet, static_cast(packet_size)); } void require_identity(const ControllerIdentity& actual, bool stable, ControllerTransport transport, uint8_t address_type, const bd_addr_t address, uint16_t vendor_id, uint16_t product_id, const char* message) { ControllerIdentity expected{}; expected.stable = stable; expected.transport = transport; expected.address_type = address_type; memcpy(expected.address, address, sizeof(expected.address)); expected.vendor_id = vendor_id; expected.product_id = product_id; require(controller_identity_equal(actual, expected), message); } void test_identity_encoding_contract() { ControllerIdentity identity{}; identity.stable = true; identity.transport = ControllerTransport::kBle; identity.address_type = BD_ADDR_TYPE_LE_RANDOM_IDENTITY; const bd_addr_t address = {0x10, 0x11, 0x12, 0x13, 0x14, 0x15}; memcpy(identity.address, address, sizeof(address)); identity.vendor_id = 0x1234; identity.product_id = 0xabcd; uint8_t encoded[CONTROLLER_IDENTITY_ENCODED_SIZE]{}; const uint8_t expected[CONTROLLER_IDENTITY_ENCODED_SIZE] = { 1, 2, 3, 0, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x34, 0x12, 0xcd, 0xab}; require(controller_identity_encode(identity, encoded, sizeof(encoded)) && memcmp(encoded, expected, sizeof(expected)) == 0, "controller identity wire encoding changed"); ControllerIdentity decoded{}; require(controller_identity_decode(encoded, sizeof(encoded), &decoded) && controller_identity_equal(identity, decoded), "controller identity wire round trip failed"); decoded.product_id ^= 1; require(!controller_identity_equal(identity, decoded), "controller identity equality must include every field"); encoded[3] = 1; require(!controller_identity_decode(encoded, sizeof(encoded), &decoded), "controller identity decoder must reject a nonzero reserved byte"); const ControllerIdentity global = controller_identity_global(); memset(encoded, 0xff, sizeof(encoded)); require(controller_identity_is_global(global) && controller_identity_encode(global, encoded, sizeof(encoded)), "global identity helper must produce an encodable fallback"); for (uint8_t byte : encoded) { require(byte == 0, "global fallback identity must encode as all zeroes"); } } void tick_backend_timer(int ticks) { for (int tick = 0; tick < ticks; ++tick) { process_rumble_timer(&g_rumble_timer); } } uni_hid_device_t switch2_device(int index, uint16_t product) { uni_hid_device_t result = device(index, true, UNI_BT_CONN_PROTOCOL_BLE); result.vendor_id = UNI_SW2_NINTENDO_VID; result.product_id = product; result.switch2_identity_valid = true; result.switch2_identity_address_type = BD_ADDR_TYPE_LE_PUBLIC; result.report_parser.set_player_leds = set_player_leds; return result; } void ready_switch2(uni_hid_device_t& controller) { platform_on_device_connected(&controller); require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS, "Switch2 physical device must become ready"); } void remember_switch2(const uni_hid_device_t& controller) { require(switch2_pairing_count < UNI_SWITCH2_PAIRING_CAPACITY, "test proprietary trust inventory overflow"); switch2_pairing_types[switch2_pairing_count] = controller.switch2_identity_address_type; memcpy(switch2_pairings[switch2_pairing_count], controller.conn.btaddr, sizeof(bd_addr_t)); ++switch2_pairing_count; } Bluepad32SlotSnapshot slot_snapshot(uint8_t index) { Bluepad32SlotSnapshot result{}; bluepad32_input_backend_snapshot(index, &result); return result; } void require_pair_owner(const ControllerIdentity& identity, const uni_hid_device_t& left, const uni_hid_device_t& right) { ControllerIdentity left_member{}; ControllerIdentity right_member{}; require(controller_identity_is_joycon_pair(identity) && controller_identity_joycon_pair_members( identity, &left_member, &right_member) && controller_identity_equal(left_member, identity_for_device(&left)) && controller_identity_equal(right_member, identity_for_device(&right)) && !controller_identity_equal(identity, left_member) && !controller_identity_equal(identity, right_member), "logical owner must distinguish both typed physical members from their solo owners"); } void require_active_profile(const ControllerIdentity& identity, uint8_t expected_index, uint8_t expected_scale) { const ProfileStorageIdentityIndex* owner = runtime_profile_storage.find(identity); ControllerProfile profile{}; require(owner != nullptr && owner->active_profile == expected_index && runtime_profile_storage.get(identity, owner->active_profile, &profile) == ProfileStorageResult::kOk && profile.weak_rumble_scale == expected_scale, "live owner must resolve its independent persisted active profile"); } void set_runtime_joycon_mode(JoyConMode mode) { runtime_configuration.joycon_mode = mode; ++runtime_configuration_generation; process_configuration_timer(&g_configuration_timer); ConfigurationServiceSnapshot saved{}; configuration_service_snapshot(&saved); require(saved.configuration.joycon_mode == mode && saved.generation == runtime_configuration_generation && device_disconnect_calls == 0 && uni_init_calls == 0 && core1_launch_calls == 0, "live mode updates must retain the committed preference without disconnect or restart"); } void test_switch2_individual_core_start() { runtime_configuration.joycon_mode = JoyConMode::kIndividual; bluepad32_input_backend_init(); auto right = switch2_device(0, UNI_SW2_JOYCON_R_PID); auto left = switch2_device(1, UNI_SW2_JOYCON_L_PID); register_lookup_device(&right); register_lookup_device(&left); during_uni_init = []() { require(installed_platform != nullptr && installed_platform->on_device_ready(lookup_devices[0]) == UNI_ERROR_SUCCESS && installed_platform->on_device_ready(lookup_devices[1]) == UNI_ERROR_SUCCESS && slot_snapshot(0).active && slot_snapshot(1).active && pair_observation_count == 0, "persisted Individual must govern ready callbacks before BTstack initialization completes"); }; bool stopped = false; try { core1_main(); } catch (const CoreStopped&) { stopped = true; } during_uni_init = nullptr; require(stopped && uni_init_calls == 1 && slot_snapshot(0).active && slot_snapshot(1).active, "Core1 initialization must retain both solo players without an initial pair race"); } void test_switch2_individual_boot(bool right_first) { runtime_configuration.joycon_mode = JoyConMode::kIndividual; start_backend(); auto left = switch2_device(right_first ? 1 : 0, UNI_SW2_JOYCON_L_PID); auto right = switch2_device(right_first ? 0 : 1, UNI_SW2_JOYCON_R_PID); auto& first = right_first ? right : left; auto& second = right_first ? left : right; remember_switch2(left); remember_switch2(right); register_lookup_device(&left); register_lookup_device(&right); ready_switch2(first); require(scanning_enabled && background_scan_parameters && !classic_scanning_enabled && !bondable && accepted_stk_methods == 0 && !switch_pico_switch2_pairing_allowed() && platform_on_device_discovered(second.conn.btaddr, nullptr, 0, 0) == UNI_ERROR_SUCCESS, "Individual must still admit a remembered opposite half without BOOTSEL or authentication"); const auto first_solo = slot_snapshot(0); platform_on_device_connected(&second); require(!scanning_enabled && platform_on_device_ready(&second) == UNI_ERROR_SUCCESS, "remembered Individual mate setup must pause scanning and become ready"); process_configuration_timer(&g_configuration_timer); require(slot_snapshot(0).active && slot_snapshot(1).active && slot_snapshot(0).connection_generation == first_solo.connection_generation && !scanning_enabled && !classic_scanning_enabled && pair_observation_count == 0, "persisted Individual must avoid transient boot merging and stop scanning once balanced"); require(controller_identity_equal(slot_snapshot(left.idx).identity, identity_for_device(&left)) && controller_identity_equal(slot_snapshot(right.idx).identity, identity_for_device(&right)), "both Individual players must publish their own solo profile identity"); uni_controller_t data{}; data.klass = UNI_CONTROLLER_CLASS_GAMEPAD; data.gamepad.dpad = DPAD_LEFT; data.gamepad.axis_x = -512; data.gamepad.axis_y = 100; left.switch2_extra_buttons = UNI_SW2_BUTTON_LEFT_SL; platform_on_controller_data(&left, &data); const auto left_state = slot_snapshot(left.idx).state; data.gamepad = {}; data.gamepad.buttons = BUTTON_B | BUTTON_THUMB_R; data.gamepad.axis_rx = 200; data.gamepad.axis_ry = -512; right.switch2_extra_buttons = UNI_SW2_BUTTON_RIGHT_SR; platform_on_controller_data(&right, &data); const auto right_state = slot_snapshot(right.idx).state; require(left_state.button_south && !left_state.dpad_left && left_state.button_left_shoulder && !left_state.button_right_shoulder && left_state.left_stick_x == scale_axis(100) && left_state.left_stick_y == INT16_MAX && right_state.button_south && right_state.button_left_stick && !right_state.button_right_stick && right_state.button_right_shoulder && !right_state.button_left_shoulder && right_state.left_stick_x == INT16_MAX && right_state.left_stick_y == scale_axis(200) && slot_snapshot(left.idx).state.left_stick_x == left_state.left_stick_x, "two Individual halves must keep independent sideways input and rail routing"); Bluepad32PlaytestSnapshot playtest{}; bluepad32_input_backend_playtest_snapshot(left.idx, &playtest); require(playtest.controller_layout == Bluepad32ControllerLayout::kJoyCon2LeftSolo, "Individual left must report actual solo topology"); bluepad32_input_backend_playtest_snapshot(right.idx, &playtest); require(playtest.controller_layout == Bluepad32ControllerLayout::kJoyCon2RightSolo, "Individual right must report actual solo topology"); bluepad32_input_backend_queue_rumble(left.idx, ControllerRumbleOutput{17, 27}); process_rumble_timer(&g_rumble_timer); require(left.last_low == 17 && right.last_low == 0, "Individual player rumble must not fan out to its opposite half"); platform_on_device_disconnected(&second); require(scanning_enabled && !classic_scanning_enabled && platform_on_device_discovered(second.conn.btaddr, nullptr, 0, 0) == UNI_ERROR_SUCCESS, "losing an Individual half must resume only remembered opposite-half scanning"); ready_switch2(second); require(!scanning_enabled && !classic_scanning_enabled && slot_snapshot(0).active && slot_snapshot(1).active, "remembered Individual reconnection must stop scanning again without merging"); } void test_switch2_mode_roundtrip(bool right_first) { start_backend(); initialize_runtime_profile_storage(); auto left = switch2_device(right_first ? 1 : 0, UNI_SW2_JOYCON_L_PID); auto right = switch2_device(right_first ? 0 : 1, UNI_SW2_JOYCON_R_PID); const auto left_identity = identity_for_device(&left); const auto right_identity = identity_for_device(&right); auto left_profile = controller_profile_default(left_identity, 2); left_profile.weak_rumble_scale = 37; auto right_profile = controller_profile_default(right_identity, 5); right_profile.weak_rumble_scale = 63; require(runtime_profile_storage.set(left_identity, 2, left_profile) == ProfileStorageResult::kOk && runtime_profile_storage.activate(left_identity, 2) == ProfileStorageResult::kOk && runtime_profile_storage.set(right_identity, 5, right_profile) == ProfileStorageResult::kOk && runtime_profile_storage.activate(right_identity, 5) == ProfileStorageResult::kOk, "roundtrip requires independently selected saved solo banks"); ready_switch2(right_first ? right : left); ready_switch2(right_first ? left : right); const auto pair_identity = slot_snapshot(0).identity; auto pair_profile = controller_profile_default(pair_identity, 7); pair_profile.weak_rumble_scale = 95; require(runtime_profile_storage.set(pair_identity, 7, pair_profile) == ProfileStorageResult::kOk && runtime_profile_storage.activate(pair_identity, 7) == ProfileStorageResult::kOk, "roundtrip requires a distinct saved composite-bank selection"); auto classic = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR); platform_on_device_connected(&classic); require(platform_on_device_ready(&classic) == UNI_ERROR_SUCCESS, "Classic must coexist with either Joy-Con player mode"); uni_controller_t data{}; data.klass = UNI_CONTROLLER_CLASS_GAMEPAD; data.gamepad.buttons = BUTTON_Y; platform_on_controller_data(&classic, &data); data.gamepad = {}; data.gamepad.dpad = DPAD_UP; data.gamepad.axis_x = -512; data.gamepad.axis_y = 100; left.switch2_extra_buttons = UNI_SW2_BUTTON_GL | UNI_SW2_BUTTON_LEFT_SL; platform_on_controller_data(&left, &data); data.gamepad = {}; data.gamepad.buttons = BUTTON_B; data.gamepad.axis_rx = 200; data.gamepad.axis_ry = -512; right.switch2_extra_buttons = UNI_SW2_BUTTON_C | UNI_SW2_BUTTON_RIGHT_SR; platform_on_controller_data(&right, &data); bluepad32_input_backend_queue_rumble(2, ControllerRumbleOutput{71, 81}); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{31, 41}); process_rumble_timer(&g_rumble_timer); const auto pair_before = slot_snapshot(0); const auto spare_before = slot_snapshot(1); const auto classic_before = slot_snapshot(2); const int classic_calls = classic.rumble_calls; const unsigned left_resets = left.switch2_haptics_resets; const unsigned right_resets = right.switch2_haptics_resets; require(bluepad32_input_backend_capture_start( 0, pair_before.connection_generation, CaptureOptions{}), "pair capture must start before splitting"); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{91, 101}); bluepad32_input_backend_queue_profile_feedback( 0, pair_before.connection_generation, 8, ControllerProfileConfirmationPolicy::kRumbleAndLed); set_runtime_joycon_mode(JoyConMode::kIndividual); const auto left_solo = slot_snapshot(0); const auto right_solo = slot_snapshot(1); require(left_solo.active && right_solo.active && left_solo.connection_generation != pair_before.connection_generation && right_solo.connection_generation != spare_before.connection_generation && controller_identity_equal(left_solo.identity, left_identity) && controller_identity_equal(right_solo.identity, right_identity) && left_solo.state.button_west && !left_solo.state.dpad_up && left_solo.state.left_stick_x == scale_axis(100) && left_solo.state.left_stick_y == INT16_MAX && right_solo.state.button_south && !right_solo.state.button_east && right_solo.state.left_stick_x == INT16_MAX && right_solo.state.left_stick_y == scale_axis(200) && left_solo.state.motion_sample_count == 0 && right_solo.state.motion_sample_count == 0, "split must retain both raw halves and immediately restore rotated solo owners at stable slots"); require_active_profile(left_solo.identity, 2, 37); require_active_profile(right_solo.identity, 5, 63); Bluepad32CaptureSnapshot capture{}; require(bluepad32_input_backend_capture_page(0, 0, &capture) && capture.state == CaptureState::kDisconnected && left.switch2_haptics_resets > left_resets && right.switch2_haptics_resets > right_resets && left.last_rumble_duration_ms == 0 && right.last_rumble_duration_ms == 0, "split must cancel recording and both physical haptics epochs"); const int stops = left.rumble_calls + right.rumble_calls; bluepad32_input_backend_queue_profile_feedback( 0, pair_before.connection_generation, 8, ControllerProfileConfirmationPolicy::kRumbleAndLed); process_rumble_timer(&g_rumble_timer); require(left.rumble_calls + right.rumble_calls == stops && left.last_low == 0 && right.last_low == 0 && left.player_leds == 1 && right.player_leds == 2 && !bluepad32_input_backend_identify(pair_identity), "retired pair feedback and identify must not leak into Individual players"); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{17, 27}); bluepad32_input_backend_queue_rumble(1, ControllerRumbleOutput{47, 57}); process_rumble_timer(&g_rumble_timer); require(left.last_low == 17 && right.last_low == 47, "split outputs must route independent player haptics"); const uint8_t capture_slot = right_first ? 1 : 0; require(bluepad32_input_backend_capture_start( capture_slot, slot_snapshot(capture_slot).connection_generation, CaptureOptions{}), "either Individual slot must remain recordable before remerging"); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{91, 101}); bluepad32_input_backend_queue_rumble(1, ControllerRumbleOutput{111, 121}); bluepad32_input_backend_queue_profile_feedback( 1, right_solo.connection_generation, 8, ControllerProfileConfirmationPolicy::kRumbleAndLed); before_pair_seed = [](const ControllerIdentity&) { require(slot_snapshot(0).active && slot_snapshot(1).active && !controller_identity_is_joycon_pair(slot_snapshot(0).identity) && !controller_identity_is_joycon_pair(slot_snapshot(1).identity), "live pair storage admission must precede publication of either topology change"); }; set_runtime_joycon_mode(JoyConMode::kPaired); before_pair_seed = nullptr; const auto restored = slot_snapshot(0); require_pair_owner(restored.identity, left, right); require(restored.connection_generation != left_solo.connection_generation && !slot_snapshot(1).active && slot_snapshot(1).connection_generation != right_solo.connection_generation && !slot_snapshot(1).state.button_south && slot_snapshot(1).state.extra_buttons == 0 && restored.state.dpad_up && restored.state.button_east && restored.state.left_stick_x == INT16_MIN && restored.state.right_stick_x == scale_axis(200) && restored.state.motion_sample_count == 0 && bluepad32_input_backend_capture_page(0, 0, &capture) && capture.state == CaptureState::kDisconnected && left.last_rumble_duration_ms == 0 && right.last_rumble_duration_ms == 0, "remerge must retain raw pair inputs, cancel either recording, and neutralize the retired solo"); require_active_profile(restored.identity, 7, 95); require_active_profile(left_identity, 2, 37); require_active_profile(right_identity, 5, 63); process_rumble_timer(&g_rumble_timer); require(left.last_low == 0 && right.last_low == 0 && slot_snapshot(2).connection_generation == classic_before.connection_generation && slot_snapshot(2).state.button_north && classic.rumble_calls == classic_calls && classic.last_low == 71 && negotiated_intervals[left.conn.handle] == 6 && negotiated_intervals[right.conn.handle] == 6, "mode roundtrip must preserve unrelated Classic state, haptics and fast Joy-Con links"); ControllerProfile inactive{}; require(runtime_profile_storage.get(pair_identity, 2, &inactive) == ProfileStorageResult::kOk && inactive.weak_rumble_scale == 37, "existing composite profiles, including inactive seeded rows, must survive mode roundtrips"); } void test_switch2_mode_two_pairs() { start_pairing_backend(); auto right0 = switch2_device(0, UNI_SW2_JOYCON_R_PID); auto right1 = switch2_device(1, UNI_SW2_JOYCON_R_PID); auto left0 = switch2_device(2, UNI_SW2_JOYCON_L_PID); auto left1 = switch2_device(3, UNI_SW2_JOYCON_L_PID); ready_switch2(right0); ready_switch2(right1); ready_switch2(left0); ready_switch2(left1); const auto pair0 = slot_snapshot(0).identity; const auto pair1 = slot_snapshot(1).identity; require(runtime_profile_storage.activate(pair0, 6) == ProfileStorageResult::kOk && runtime_profile_storage.activate(pair1, 3) == ProfileStorageResult::kOk, "two pairs need distinguishable saved selections"); for (unsigned roundtrip = 0; roundtrip < 2; ++roundtrip) { set_runtime_joycon_mode(JoyConMode::kIndividual); for (uint8_t slot = 0; slot < 4; ++slot) { require(slot_snapshot(slot).active, "splitting two pairs must expose all four live players"); } require(controller_identity_equal(slot_snapshot(0).identity, identity_for_device(&left0)) && controller_identity_equal(slot_snapshot(1).identity, identity_for_device(&left1)) && controller_identity_equal(slot_snapshot(2).identity, identity_for_device(&right0)) && controller_identity_equal(slot_snapshot(3).identity, identity_for_device(&right1)) && !scanning_enabled && !incoming_connections, "full-capacity split must choose lowest free slots without moving existing left owners"); set_runtime_joycon_mode(JoyConMode::kPaired); require_pair_owner(slot_snapshot(0).identity, left0, right0); require_pair_owner(slot_snapshot(1).identity, left1, right1); require(!slot_snapshot(2).active && !slot_snapshot(3).active && !scanning_enabled && !incoming_connections, "live pairing hints must restore both exact member combinations and original output slots"); require_active_profile(pair0, 6, UINT8_MAX); require_active_profile(pair1, 3, UINT8_MAX); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{11, 21}); bluepad32_input_backend_queue_rumble(1, ControllerRumbleOutput{31, 41}); process_rumble_timer(&g_rumble_timer); require(left0.last_low == 11 && right0.last_low == 11 && left1.last_low == 31 && right1.last_low == 31, "roundtripped pair membership must remain visible in physical haptic routing"); } } void test_switch2_live_pair_failure(bool invalid_identity) { runtime_configuration.joycon_mode = JoyConMode::kIndividual; start_backend(); initialize_runtime_profile_storage(); auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID); auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID); ready_switch2(left); ready_switch2(right); uni_controller_t data{}; data.klass = UNI_CONTROLLER_CLASS_GAMEPAD; data.gamepad.dpad = DPAD_LEFT; platform_on_controller_data(&left, &data); data.gamepad = {}; data.gamepad.buttons = BUTTON_B; platform_on_controller_data(&right, &data); const auto left_before = slot_snapshot(0); const auto right_before = slot_snapshot(1); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{17, 27}); bluepad32_input_backend_queue_rumble(1, ControllerRumbleOutput{47, 57}); require(bluepad32_input_backend_capture_start( 1, right_before.connection_generation, CaptureOptions{}), "active solo capture must survive a rejected live merge"); before_pair_seed = [](const ControllerIdentity&) { require(slot_snapshot(0).active && slot_snapshot(1).active && slot_snapshot(0).state.button_south && slot_snapshot(1).state.button_south, "both active solos must stay intact while a new composite bank is seeded"); }; if (invalid_identity) right.switch2_identity_valid = false; else fail_profile_program = true; set_runtime_joycon_mode(JoyConMode::kPaired); before_pair_seed = nullptr; Bluepad32CaptureSnapshot capture{}; require(slot_snapshot(0).active && slot_snapshot(1).active && slot_snapshot(0).connection_generation == left_before.connection_generation && slot_snapshot(1).connection_generation == right_before.connection_generation && controller_identity_equal(slot_snapshot(0).identity, left_before.identity) && controller_identity_equal(slot_snapshot(1).identity, right_before.identity) && slot_snapshot(0).state.button_south && slot_snapshot(1).state.button_south && bluepad32_input_backend_capture_page(0, 0, &capture) && capture.state == CaptureState::kRecording && left.switch2_haptics_resets == 0 && right.switch2_haptics_resets == 0, "failed live pair admission must preserve both solo identities, states, generations and recording"); const unsigned expected_observations = invalid_identity ? 0 : 1; require(pair_observation_count == expected_observations, "invalid pair identity must fail before I/O and failed storage admission must be attempted once"); process_rumble_timer(&g_rumble_timer); require(left.last_low == 17 && right.last_low == 47, "failed admission must preserve both already-queued solo haptic commands"); for (unsigned tick = 0; tick < 100; ++tick) { now_ms += 50; process_configuration_timer(&g_configuration_timer); } ++runtime_configuration.pairing_window_seconds; ++runtime_configuration_generation; process_configuration_timer(&g_configuration_timer); require(pair_observation_count == expected_observations, "pending Paired preference must not create timer or unrelated-configuration retry storms"); fail_profile_program = false; right.switch2_identity_valid = true; if (!invalid_identity) { require(runtime_profile_storage.initialize(runtime_profile_io()), "pair failure recovery must replay intact solo profile banks"); } set_runtime_joycon_mode(JoyConMode::kIndividual); set_runtime_joycon_mode(JoyConMode::kPaired); require_pair_owner(slot_snapshot(0).identity, left, right); require(!slot_snapshot(1).active, "explicit mode retry must recover without Bluetooth reconnect"); } int live_identity_slot(const ControllerIdentity& identity) { for (uint8_t index = 0; index < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++index) { const auto snapshot = slot_snapshot(index); if (snapshot.active && controller_identity_equal(snapshot.identity, identity)) return index; } return -1; } int live_pair_slot(const uni_hid_device_t& left, const uni_hid_device_t& right) { ControllerIdentity pair{}; require(controller_identity_make_joycon_pair( identity_for_device(&left), identity_for_device(&right), &pair), "gesture participants must have a valid typed composite identity"); return live_identity_slot(pair); } void require_live_solo(const uni_hid_device_t& controller, const char* message) { require(live_identity_slot(identity_for_device(&controller)) >= 0, message); } uni_controller_t gesture_input(bool right, bool trigger = true, bool menu = true) { uni_controller_t report{}; report.klass = UNI_CONTROLLER_CLASS_GAMEPAD; if (right) { report.gamepad.buttons = BUTTON_B | (trigger ? BUTTON_TRIGGER_R : 0); report.gamepad.misc_buttons = menu ? MISC_BUTTON_START : 0; report.gamepad.throttle = trigger ? 1023 : 0; report.gamepad.axis_rx = 200; report.gamepad.axis_ry = -512; } else { report.gamepad.buttons = trigger ? BUTTON_TRIGGER_L : 0; report.gamepad.misc_buttons = menu ? MISC_BUTTON_SELECT : 0; report.gamepad.brake = trigger ? 1023 : 0; report.gamepad.dpad = DPAD_UP; report.gamepad.axis_x = -512; report.gamepad.axis_y = 100; } return report; } bool gesture_profile_consumer_enabled = false; ControllerState gesture_profile_outputs[BLUEPAD32_INPUT_BACKEND_SLOT_COUNT]{}; void consume_gesture_profiles() { if (!gesture_profile_consumer_enabled) return; for (uint8_t index = 0; index < BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; ++index) { gesture_profile_outputs[index] = controller_profile_runtime_transform( index, slot_snapshot(index), now_ms, AdapterUsbMode::kXInput).state; } } void gesture_report(uni_hid_device_t& controller, bool trigger = true, bool menu = true) { auto report = gesture_input(controller.product_id == UNI_SW2_JOYCON_R_PID, trigger, menu); platform_on_controller_data(&controller, &report); consume_gesture_profiles(); } void gesture_tick() { process_rumble_timer(&g_rumble_timer); process_configuration_timer(&g_configuration_timer); consume_gesture_profiles(); } void gesture_reports(uni_hid_device_t& left, uni_hid_device_t& right) { gesture_report(left); gesture_report(right); gesture_tick(); } void hold_gesture(uni_hid_device_t& left, uni_hid_device_t& right, uint32_t duration_ms) { gesture_reports(left, right); for (uint32_t elapsed = 0; elapsed < duration_ms;) { const uint32_t step = duration_ms - elapsed < 50 ? duration_ms - elapsed : 50; elapsed += step; now_ms += step; gesture_reports(left, right); } } void release_gesture(uni_hid_device_t& left, uni_hid_device_t& right) { ++now_ms; gesture_report(left, false, false); gesture_report(right, false, false); gesture_tick(); } void require_gesture_masked(const Bluepad32SlotSnapshot& snapshot) { const uint16_t menus = logical_button_bit(ControllerProfileLogicalButton::kSelect) | logical_button_bit(ControllerProfileLogicalButton::kStart); require(!snapshot.state.button_select && !snapshot.state.button_start && snapshot.state.left_trigger == 0 && snapshot.state.right_trigger == 0 && (snapshot.pre_hotkey_button_mask & menus) == 0, "reserved physical chord must not reach host state or pre-profile hotkeys"); } void require_gesture_confirmation(size_t first, const uni_hid_device_t& left, const uni_hid_device_t& right) { unsigned left_pulses = 0; unsigned right_pulses = 0; for (size_t index = first; index < local_rumble_events.size(); ++index) { const auto& event = local_rumble_events[index]; if ((event.high | event.low) == 0 || event.duration_ms == 0) continue; require(event.duration_ms <= 150 && (event.device == &left || event.device == &right), "gesture confirmation must be short and restricted to its two physical participants"); if (event.device == &left) ++left_pulses; else ++right_pulses; } require(left_pulses == 1 && right_pulses == 1, "one successful gesture must acknowledge each physical half exactly once"); } void test_switch2_gesture_timing() { runtime_configuration.joycon_mode = JoyConMode::kIndividual; start_backend(); auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID); auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID); ready_switch2(left); ready_switch2(right); const uint32_t configuration_generation = runtime_configuration_generation; gesture_report(left, true, false); require(slot_snapshot(0).state.left_trigger == UINT16_MAX, "a trigger alone is not a reserved chord"); for (unsigned elapsed = 0; elapsed <= 2500; elapsed += 50) { now_ms = elapsed; gesture_report(left); gesture_report(right, true, false); gesture_tick(); } require_live_solo(left, "one complete half and one partial half must not join"); require_live_solo(right, "partial participant must retain its solo owner"); require(pair_observation_count == 0, "a partial gesture must never enroll a composite bank"); release_gesture(left, right); for (unsigned elapsed = 0; elapsed < 500; elapsed += 50) { now_ms += 50; gesture_report(left); gesture_report(right, false, false); gesture_tick(); } const uint32_t shared_start = now_ms; hold_gesture(left, right, 1999); require_live_solo(left, "two seconds must start at concurrent hold, not the earlier left press"); require(pair_observation_count == 0, "1999ms must not seed or publish a pair"); now_ms = shared_start + 2000; gesture_report(left); gesture_tick(); require_live_solo(right, "a fresh left report must not stand in for the right's 2000ms held report"); const size_t confirmation_start = local_rumble_events.size(); gesture_report(right); gesture_tick(); require(live_pair_slot(left, right) >= 0, "both fresh held spans at 2000ms must join Individual halves"); require_gesture_confirmation(confirmation_start, left, right); const uint32_t joined_generation = slot_snapshot(live_pair_slot(left, right)).connection_generation; hold_gesture(left, right, 2500); gesture_report(left, false, false); gesture_report(right, false, true); gesture_tick(); hold_gesture(left, right, 2500); require(live_pair_slot(left, right) >= 0 && slot_snapshot(live_pair_slot(left, right)).connection_generation == joined_generation && pair_observation_count == 1, "held input and one participant's incomplete release must not toggle or reseed again"); release_gesture(left, right); hold_gesture(left, right, 2000); require_live_solo(left, "both participants' full release must permit a second toggle"); require_live_solo(right, "second toggle must expose the other physical solo"); require(runtime_configuration.joycon_mode == JoyConMode::kIndividual && runtime_configuration_generation == configuration_generation, "connection gestures must never write the saved adapter preference"); } void test_switch2_gesture_slot_order(bool left_first, uint8_t lower_slot) { runtime_configuration.joycon_mode = JoyConMode::kIndividual; start_backend(); auto ordinary = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR); auto left = switch2_device(lower_slot + 1, UNI_SW2_JOYCON_L_PID); auto right = switch2_device(lower_slot, UNI_SW2_JOYCON_R_PID); if (lower_slot != 0) ready_switch2(ordinary); // Transport indices can put the left half in the higher player slot, // even when its ready callback arrives first. ready_switch2(left_first ? left : right); ready_switch2(left_first ? right : left); const auto lower_before = slot_snapshot(lower_slot); const auto higher_before = slot_snapshot(lower_slot + 1); const auto unrelated_before = slot_snapshot(0); hold_gesture(left, right, 2000); require(live_pair_slot(left, right) == lower_slot && !slot_snapshot(lower_slot + 1).active, "gesture join must retain the lower participating player slot regardless of side or ready order"); const auto joined = slot_snapshot(lower_slot); require_pair_owner(joined.identity, left, right); require(joined.connection_generation != lower_before.connection_generation && slot_snapshot(lower_slot + 1).connection_generation != higher_before.connection_generation && slot_snapshot(lower_slot + 1).state.left_stick_x == 0 && slot_snapshot(lower_slot + 1).state.right_stick_x == 0 && left.player_leds == (1u << lower_slot) && right.player_leds == (1u << lower_slot), "join must invalidate old player epochs, neutralize the retired slot and light both halves for the retained player"); if (lower_slot != 0) { require(slot_snapshot(0).active && slot_snapshot(0).connection_generation == unrelated_before.connection_generation && controller_identity_equal(slot_snapshot(0).identity, unrelated_before.identity), "gesture join must not take an earlier slot occupied by an unrelated controller"); } } void test_switch2_gesture_stale() { runtime_configuration.joycon_mode = JoyConMode::kIndividual; start_backend(); auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID); auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID); ready_switch2(left); ready_switch2(right); hold_gesture(left, right, 1000); for (unsigned elapsed = 0; elapsed < 2500; elapsed += 50) { now_ms += 50; gesture_report(left); gesture_tick(); } require_live_solo(left, "cached right input must not fire after it goes stale"); hold_gesture(left, right, 2200); require_live_solo(right, "resumed held reports must not rearm a stale attempt without release"); require(pair_observation_count == 0, "stale attempts must not touch pair storage"); release_gesture(left, right); hold_gesture(left, right, 1000); now_ms += 1100; gesture_reports(left, right); require_live_solo(left, "new reports must not hide a stale inter-report gap from a delayed timer"); hold_gesture(left, right, 2200); require_live_solo(right, "a delayed-timer stale attempt must also require full release before retry"); release_gesture(left, right); hold_gesture(left, right, 1900); now_ms += 100; gesture_tick(); require_live_solo(left, "fresh cached reports alone cannot establish two full held spans"); gesture_report(left); gesture_tick(); require_live_solo(right, "a timer and one participant cannot complete the other's held span"); gesture_report(right); gesture_tick(); require(live_pair_slot(left, right) >= 0, "release and fresh report spans must recover a stale attempt"); } void test_switch2_gesture_clock_wrap() { runtime_configuration.joycon_mode = JoyConMode::kIndividual; start_backend(); auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID); auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID); ready_switch2(left); ready_switch2(right); now_ms = UINT32_MAX - 1000u; hold_gesture(left, right, 1999); require_live_solo(left, "clock wrap must not shorten the hold"); ++now_ms; gesture_reports(left, right); require(live_pair_slot(left, right) >= 0, "continuous held reports must complete across millisecond wrap"); } void configure_gesture_profile(const ControllerIdentity& identity, uint8_t index, uint8_t scale) { auto profile = controller_profile_default(identity, index); profile.weak_rumble_scale = scale; profile.button_map[static_cast(ControllerProfileLogicalButton::kSelect)] = static_cast(ControllerProfileLogicalButton::kNorth); profile.button_map[static_cast(ControllerProfileLogicalButton::kStart)] = static_cast(ControllerProfileLogicalButton::kCapture); profile.switching_chord = logical_button_bit(ControllerProfileLogicalButton::kSelect) | (1u << CONTROLLER_PROFILE_LEFT_TRIGGER_CONTROL); profile.motion_toggle_chord = logical_button_bit(ControllerProfileLogicalButton::kStart) | (1u << CONTROLLER_PROFILE_RIGHT_TRIGGER_CONTROL); profile.macros[0].trigger_mask = logical_button_bit(ControllerProfileLogicalButton::kSouth); profile.macros[0].first_step = 0; profile.macros[0].step_count = 1; profile.macros[0].mode = ControllerProfileMacroMode::kToggle; profile.macro_step_count = 1; for (uint8_t macro = 1; macro < CONTROLLER_PROFILE_MACRO_COUNT; ++macro) { profile.macros[macro].first_step = 1; } profile.macro_steps[0].override_flags = kControllerProfileOverrideButtons; profile.macro_steps[0].duration_ms = 10000; profile.macro_steps[0].output_button_mask = logical_button_bit(ControllerProfileLogicalButton::kSystem); require(runtime_profile_storage.set(identity, index, profile) == ProfileStorageResult::kOk && runtime_profile_storage.activate(identity, index) == ProfileStorageResult::kOk, "gesture fixture needs independent profiles with observable macro and reserved-button mappings"); } void test_switch2_gesture_masking_epochs() { start_backend(); initialize_runtime_profile_storage(); auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID); auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID); const auto left_identity = identity_for_device(&left); const auto right_identity = identity_for_device(&right); configure_gesture_profile(left_identity, 2, 37); configure_gesture_profile(right_identity, 5, 63); ready_switch2(left); ready_switch2(right); const auto pair_identity = slot_snapshot(0).identity; configure_gesture_profile(pair_identity, 7, 95); auto ordinary = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR); ready_switch2(ordinary); auto ordinary_report = gesture_input(true, false, false); ordinary_report.gamepad.buttons = BUTTON_Y; platform_on_controller_data(&ordinary, &ordinary_report); const auto ordinary_before = slot_snapshot(2); bluepad32_input_backend_queue_rumble(2, ControllerRumbleOutput{71, 81}); gesture_tick(); const int ordinary_rumble_calls = ordinary.rumble_calls; const unsigned writes_before = profile_write_attempts; const uint32_t saved_generation = runtime_configuration_generation; gesture_profile_consumer_enabled = true; controller_profile_runtime_reset(); release_gesture(left, right); auto macro_press = gesture_input(true, false, false); macro_press.gamepad.buttons = BUTTON_A; ++now_ms; platform_on_controller_data(&right, ¯o_press); consume_gesture_profiles(); require(gesture_profile_outputs[0].button_system, "a real profile macro must be running before the topology epoch"); release_gesture(left, right); CaptureOptions options{}; options.channels = 0x1f; options.max_events = 32; require(bluepad32_input_backend_capture_start( 0, slot_snapshot(0).connection_generation, options), "pair capture must record the arming path before splitting"); const auto pair_before = slot_snapshot(0); ++now_ms; gesture_report(left); require_gesture_masked(slot_snapshot(0)); require(slot_snapshot(0).state.dpad_up && slot_snapshot(0).state.button_east && slot_snapshot(0).state.left_stick_x == INT16_MIN && slot_snapshot(0).state.right_stick_x == scale_axis(200), "masking a single physical chord must preserve unrelated buttons, axes and companion input"); gesture_report(right); gesture_tick(); require_gesture_masked(slot_snapshot(0)); hold_gesture(left, right, 1999); require(gesture_profile_outputs[0].button_system && !gesture_profile_outputs[0].button_north && !gesture_profile_outputs[0].button_capture, "arming must not leak remapped menus or cancel the preexisting macro"); Bluepad32CaptureSnapshot capture{}; require(bluepad32_input_backend_capture_page(0, 0, &capture) && capture.state == CaptureState::kRecording, "reserved chord consumption must not interrupt recording before success"); for (uint8_t index = 0; index < capture.event_count; ++index) { require((capture.events[index].buttons & (logical_button_bit(ControllerProfileLogicalButton::kSelect) | logical_button_bit(ControllerProfileLogicalButton::kStart))) == 0 && capture.events[index].left_trigger == 0 && capture.events[index].right_trigger == 0, "recorded macros must never contain reserved gesture components"); } bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{91, 101}); bluepad32_input_backend_queue_profile_feedback( 0, pair_before.connection_generation, 8, ControllerProfileConfirmationPolicy::kRumbleAndLed); const size_t host_events_before = switch2_host_events.size(); const size_t confirmation_start = local_rumble_events.size(); ++now_ms; gesture_reports(left, right); require_live_solo(left, "Paired default must allow a gesture split"); require_live_solo(right, "gesture split must expose both members"); require_gesture_masked(slot_snapshot(0)); require_gesture_masked(slot_snapshot(1)); require(slot_snapshot(0).state.button_west && slot_snapshot(1).state.button_south && slot_snapshot(0).state.left_stick_x == scale_axis(100) && slot_snapshot(1).state.left_stick_x == INT16_MAX && !gesture_profile_outputs[0].button_system && !gesture_profile_outputs[1].button_system && bluepad32_input_backend_capture_page(0, 0, &capture) && capture.state == CaptureState::kDisconnected && switch2_host_events.size() == host_events_before, "successful split must rotate retained input, cancel macro/capture epochs and discard queued old rumble"); require_gesture_confirmation(confirmation_start, left, right); bluepad32_input_backend_queue_profile_feedback( 0, pair_before.connection_generation, 8, ControllerProfileConfirmationPolicy::kRumbleAndLed); now_ms += 200; gesture_report(left, false, true); gesture_report(right, true, false); gesture_tick(); require_gesture_masked(slot_snapshot(0)); require_gesture_masked(slot_snapshot(1)); require_gesture_confirmation(confirmation_start, left, right); require(slot_snapshot(2).connection_generation == ordinary_before.connection_generation && slot_snapshot(2).state.button_north && ordinary.rumble_calls == ordinary_rumble_calls && ordinary.last_low == 71, "gesture confirmation and retired feedback must not disturb unrelated input or rumble"); release_gesture(left, right); gesture_report(left, true, false); gesture_report(right, false, true); require(slot_snapshot(0).state.left_trigger == UINT16_MAX && slot_snapshot(1).state.button_start, "each half must return its component inputs after both components have released"); release_gesture(left, right); auto solo_macro_press = gesture_input(false, false, false); solo_macro_press.gamepad.dpad = DPAD_LEFT; ++now_ms; platform_on_controller_data(&left, &solo_macro_press); consume_gesture_profiles(); require(gesture_profile_outputs[0].button_system, "a real solo profile macro must be running before rejoining"); release_gesture(left, right); require(bluepad32_input_backend_capture_start( 1, slot_snapshot(1).connection_generation, options), "the other solo must remain recordable before gesture join"); hold_gesture(left, right, 1999); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{111, 121}); bluepad32_input_backend_queue_rumble(1, ControllerRumbleOutput{131, 141}); const size_t solo_host_events_before = switch2_host_events.size(); ++now_ms; gesture_reports(left, right); require(live_pair_slot(left, right) >= 0 && !gesture_profile_outputs[0].button_system && !gesture_profile_outputs[1].button_system, "rejoining must retire both solo macro contexts without reviving the former pair macro"); require(bluepad32_input_backend_capture_page(0, 0, &capture) && capture.state == CaptureState::kDisconnected && switch2_host_events.size() == solo_host_events_before, "gesture join must disconnect either solo recorder and discard both queued solo rumble epochs"); require_active_profile(pair_identity, 7, 95); require_active_profile(left_identity, 2, 37); require_active_profile(right_identity, 5, 63); require(profile_write_attempts == writes_before && runtime_configuration.joycon_mode == JoyConMode::kPaired && runtime_configuration_generation == saved_generation, "gesture roundtrip must preserve all existing profile banks and saved preference without flash writes"); } void test_switch2_gesture_override_lifetime() { runtime_configuration.joycon_mode = JoyConMode::kIndividual; start_backend(); auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID); auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID); remember_switch2(left); remember_switch2(right); register_lookup_device(&left); register_lookup_device(&right); ready_switch2(left); ready_switch2(right); hold_gesture(left, right, 1900); platform_on_device_disconnected(&right); now_ms += 100; gesture_reports(left, right); require_live_solo(left, "disconnect during arming must leave the survivor solo"); require(pair_observation_count == 0, "late detached reports cannot complete a pending gesture"); ready_switch2(right); release_gesture(left, right); hold_gesture(left, right, 2000); require(live_pair_slot(left, right) >= 0, "reconnected halves must support a new connection-only join"); auto ordinary = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR); ready_switch2(ordinary); ++runtime_configuration.pairing_window_seconds; ++runtime_configuration_generation; gesture_tick(); require(live_pair_slot(left, right) >= 0, "unrelated ready and configuration reconciliation must retain a join override"); platform_on_device_disconnected(&left); gesture_tick(); ready_switch2(left); gesture_tick(); require_live_solo(left, "disconnect must restore Individual for the returning participant"); require_live_solo(right, "disconnect must restore Individual for its still-connected partner"); release_gesture(left, right); hold_gesture(left, right, 2000); set_runtime_joycon_mode(JoyConMode::kPaired); release_gesture(left, right); hold_gesture(left, right, 2000); require_live_solo(left, "a gesture must split while the saved default remains Paired"); for (unsigned tick = 0; tick < 20; ++tick) { now_ms += 50; gesture_tick(); require(!scanning_enabled && !classic_scanning_enabled && platform_on_device_discovered(right.conn.btaddr, nullptr, 0, 0) == UNI_ERROR_IGNORE_DEVICE, "forced solos under Paired must stop background mate discovery despite free transport capacity"); } require_live_solo(left, "background reconciliation must not rematch a manually split pair"); require_live_solo(right, "the second forced solo must stay independent beside an unrelated Classic link"); auto pro = switch2_device(3, UNI_SW2_PRO_PID); ready_switch2(pro); gesture_tick(); require_live_solo(right, "new ready events must not immediately undo the forced split"); platform_on_device_disconnected(&right); gesture_tick(); require(scanning_enabled && background_scan_parameters && !classic_scanning_enabled && !bondable && !switch_pico_switch2_pairing_allowed() && platform_on_device_discovered(right.conn.btaddr, nullptr, 0, 0) == UNI_ERROR_SUCCESS, "participant disconnect must clear the override and resume remembered-mate discovery without fresh pairing"); ready_switch2(right); gesture_tick(); require(live_pair_slot(left, right) >= 0, "either split participant disconnecting must restore Paired for both"); require(!scanning_enabled && !classic_scanning_enabled, "default reconnection must stop background discovery after restoring the pair"); release_gesture(left, right); hold_gesture(left, right, 2000); platform_on_device_disconnected(&right); auto replacement = switch2_device(1, UNI_SW2_JOYCON_R_PID); replacement.conn.btaddr[5] = 0x55; ready_switch2(replacement); gesture_tick(); require(live_pair_slot(left, replacement) >= 0 && live_pair_slot(left, right) < 0, "reusing the physical index for a different identity must not inherit either member's former override"); platform_on_device_disconnected(&right); gesture_report(right); gesture_tick(); require(live_pair_slot(left, replacement) >= 0, "late reports and disconnects from the retired participant must not clear its replacement's topology"); release_gesture(left, replacement); hold_gesture(left, replacement, 2000); set_runtime_joycon_mode(JoyConMode::kIndividual); set_runtime_joycon_mode(JoyConMode::kPaired); require(live_pair_slot(left, replacement) >= 0, "changing the saved default must clear a forced-solo override"); set_runtime_joycon_mode(JoyConMode::kIndividual); release_gesture(left, replacement); hold_gesture(left, replacement, 2000); require(live_pair_slot(left, replacement) >= 0, "Individual must still permit an explicit current-pair join"); set_runtime_joycon_mode(JoyConMode::kPaired); set_runtime_joycon_mode(JoyConMode::kIndividual); require_live_solo(left, "changing the saved default must also clear a forced-pair override"); require_live_solo(replacement, "clearing a forced-pair override must restore both Individual owners"); } void test_switch2_gesture_two_pairs() { start_backend(); auto right0 = switch2_device(0, UNI_SW2_JOYCON_R_PID); auto right1 = switch2_device(1, UNI_SW2_JOYCON_R_PID); auto left0 = switch2_device(2, UNI_SW2_JOYCON_L_PID); auto left1 = switch2_device(3, UNI_SW2_JOYCON_L_PID); ready_switch2(right0); ready_switch2(right1); ready_switch2(left0); ready_switch2(left1); const auto pair0 = slot_snapshot(live_pair_slot(left0, right0)); const auto pair1 = slot_snapshot(live_pair_slot(left1, right1)); hold_gesture(left0, right1, 2000); require(live_pair_slot(left0, right0) >= 0 && live_pair_slot(left1, right1) >= 0 && slot_snapshot(live_pair_slot(left0, right0)).connection_generation == pair0.connection_generation && slot_snapshot(live_pair_slot(left1, right1)).connection_generation == pair1.connection_generation, "chords from different live pairs must never steal or split either pair's members"); release_gesture(left0, right1); const size_t first_confirmation = local_rumble_events.size(); hold_gesture(left0, right0, 2000); require_live_solo(left0, "only the first current pair must split for its own two participants"); require_live_solo(right0, "first pair's right member must become independent"); require(live_pair_slot(left1, right1) >= 0 && slot_snapshot(live_pair_slot(left1, right1)).connection_generation == pair1.connection_generation, "unjoining one pair must not split or invalidate the unrelated pair"); require_gesture_confirmation(first_confirmation, left0, right0); const size_t second_confirmation = local_rumble_events.size(); hold_gesture(left1, right1, 2000); require_gesture_confirmation(second_confirmation, left1, right1); require_live_solo(left1, "second current pair must split independently"); require_live_solo(right1, "second pair's right member must become independent"); release_gesture(left0, right0); release_gesture(left1, right1); const auto former_right = slot_snapshot(live_identity_slot(identity_for_device(&right0))); const auto former_left = slot_snapshot(live_identity_slot(identity_for_device(&left1))); const size_t confirmation_start = local_rumble_events.size(); hold_gesture(left0, right1, 2000); require(live_pair_slot(left0, right1) >= 0, "explicit solo participants must be allowed to choose a new cross-pair"); require_gesture_confirmation(confirmation_start, left0, right1); gesture_tick(); require_live_solo(right0, "cross-joining must not regroup the untouched former right partner"); require_live_solo(left1, "cross-joining must not regroup the untouched former left partner"); require(slot_snapshot(live_identity_slot(identity_for_device(&right0))).connection_generation == former_right.connection_generation && slot_snapshot(live_identity_slot(identity_for_device(&left1))).connection_generation == former_left.connection_generation, "changing partners must leave former partners' logical epochs intact"); platform_on_device_disconnected(&right1); auto replacement = switch2_device(1, UNI_SW2_JOYCON_R_PID); replacement.conn.btaddr[5] = 0x65; ready_switch2(replacement); gesture_tick(); require(live_pair_slot(left0, replacement) >= 0, "a former pair hint must not pin a cleared survivor to a stolen or disconnected member"); require_live_solo(right0, "clearing the current override must preserve the untouched former right's solo override"); require_live_solo(left1, "clearing the current override must preserve the untouched former left's solo override"); hold_gesture(left1, right0, 2000); require(live_pair_slot(left1, right0) >= 0 && live_pair_slot(left0, replacement) >= 0, "orphaned former partners must be independently joinable without disturbing the current pair"); } void test_switch2_gesture_ambiguous() { runtime_configuration.joycon_mode = JoyConMode::kIndividual; start_backend(); auto left0 = switch2_device(0, UNI_SW2_JOYCON_L_PID); auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID); auto left1 = switch2_device(2, UNI_SW2_JOYCON_L_PID); ready_switch2(left0); ready_switch2(right); ready_switch2(left1); const auto spare_before = slot_snapshot(2); for (unsigned elapsed = 0; elapsed <= 2500; elapsed += 50) { now_ms = elapsed; gesture_report(left0); gesture_report(right); gesture_report(left1); gesture_tick(); } require_live_solo(left0, "two armed left solos must not choose an arbitrary pairing"); require_live_solo(right, "ambiguous solo arming must preserve the only right player"); require_live_solo(left1, "ambiguous solo arming must preserve the other left player"); gesture_report(left1, false, false); hold_gesture(left0, right, 2200); require(pair_observation_count == 0 && local_rumble_events.empty(), "resolving ambiguity without releasing the attempt must not retry storage or acknowledge success"); release_gesture(left0, right); hold_gesture(left0, right, 2000); require(live_pair_slot(left0, right) >= 0 && slot_snapshot(2).connection_generation == spare_before.connection_generation && controller_identity_equal(slot_snapshot(2).identity, spare_before.identity), "full release must allow one eligible L/R while preserving the unarmed extra solo"); } void test_switch2_gesture_seed_failure() { runtime_configuration.joycon_mode = JoyConMode::kIndividual; start_backend(); initialize_runtime_profile_storage(); auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID); auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID); const auto left_identity = identity_for_device(&left); const auto right_identity = identity_for_device(&right); configure_gesture_profile(left_identity, 2, 37); configure_gesture_profile(right_identity, 5, 63); ready_switch2(left); ready_switch2(right); // Establish the current USB host mode before testing a gesture failure; // the adapter variant legitimately resets its initial haptics epoch. bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{}); bluepad32_input_backend_queue_rumble(1, ControllerRumbleOutput{}); release_gesture(left, right); const unsigned left_resets_before = left.switch2_haptics_resets; const unsigned right_resets_before = right.switch2_haptics_resets; const auto left_before = slot_snapshot(0); const auto right_before = slot_snapshot(1); CaptureOptions options{}; options.max_duration_ms = 20000; require(bluepad32_input_backend_capture_start(1, right_before.connection_generation, options), "failed admission must preserve a live solo capture"); hold_gesture(left, right, 1999); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{17, 27}); bluepad32_input_backend_queue_rumble(1, ControllerRumbleOutput{47, 57}); fail_profile_program = true; ++now_ms; gesture_reports(left, right); Bluepad32CaptureSnapshot capture{}; require_live_solo(left, "failed seed must not retire the left solo"); require_live_solo(right, "failed seed must not retire the right solo"); require(slot_snapshot(0).connection_generation == left_before.connection_generation && slot_snapshot(1).connection_generation == right_before.connection_generation && slot_snapshot(0).state.button_west && slot_snapshot(1).state.button_south && bluepad32_input_backend_capture_page(0, 0, &capture) && capture.state == CaptureState::kRecording && left.switch2_haptics_resets == left_resets_before && right.switch2_haptics_resets == right_resets_before && left.last_low == 17 && right.last_low == 47 && pair_observation_count == 1, "seed failure must preserve identities, input, generations, recording and queued independent rumble"); const unsigned failed_writes = profile_write_attempts; hold_gesture(left, right, 5000); ++runtime_configuration.pairing_window_seconds; ++runtime_configuration_generation; gesture_tick(); require(pair_observation_count == 1 && profile_write_attempts == failed_writes && local_rumble_events.empty(), "failed held gesture must not retry flash, churn topology or emit confirmation pulses"); fail_profile_program = false; require(runtime_profile_storage.initialize(runtime_profile_io()), "catalog replay must retain intact solo banks after failed pair admission"); require_active_profile(left_identity, 2, 37); require_active_profile(right_identity, 5, 63); release_gesture(left, right); hold_gesture(left, right, 2000); require(live_pair_slot(left, right) >= 0 && pair_observation_count == 2, "explicit release and retry must recover once profile storage is available"); require_active_profile(slot_snapshot(live_pair_slot(left, right)).identity, 2, 37); require_active_profile(right_identity, 5, 63); require(runtime_configuration.joycon_mode == JoyConMode::kIndividual, "failed and successful connection gestures must both leave the saved preference Individual"); } void test_switch2_gesture_device_scope() { start_backend(); auto original_left = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR); auto original_right = device(1, true, UNI_BT_CONN_PROTOCOL_BR_EDR); original_left.vendor_id = original_right.vendor_id = UNI_SW2_NINTENDO_VID; original_left.product_id = 0x2006; original_right.product_id = 0x2007; auto pro = switch2_device(2, UNI_SW2_PRO_PID); auto other = device(3, true, UNI_BT_CONN_PROTOCOL_BLE); uni_hid_device_t* controllers[] = {&original_left, &original_right, &pro, &other}; for (auto* controller : controllers) ready_switch2(*controller); auto report = gesture_input(false); report.gamepad.buttons |= BUTTON_TRIGGER_R; report.gamepad.misc_buttons |= MISC_BUTTON_START; report.gamepad.throttle = 1023; for (unsigned elapsed = 0; elapsed <= 2500; elapsed += 50) { now_ms = elapsed; for (auto* controller : controllers) platform_on_controller_data(controller, &report); gesture_tick(); } for (uint8_t index = 0; index < 4; ++index) { const auto snapshot = slot_snapshot(index); require(snapshot.active && snapshot.state.button_select && snapshot.state.button_start && snapshot.state.left_trigger == UINT16_MAX && snapshot.state.right_trigger == UINT16_MAX, "original JoyCons, Switch2 Pro and unrelated controllers must retain all chord inputs"); } require(pair_observation_count == 0 && local_rumble_events.empty(), "the connection gesture must never operate on non-JoyCon2 controllers"); } ControllerRumbleOutput ordered_switch2_hd(uint8_t frequency = 40) { ControllerRumbleOutput rumble{17, 29}; rumble.hd.actuators[0].sample_count = 3; rumble.hd.actuators[1].sample_count = 2; rumble.hd.actuators[0].samples[0] = {frequency, 61, 24000, 4000}; rumble.hd.actuators[0].samples[1] = {41, 62, 5000, 25000}; rumble.hd.actuators[0].samples[2] = {42, 63, 20000, 14000}; rumble.hd.actuators[1].samples[0] = {81, 91, 3000, 21000}; rumble.hd.actuators[1].samples[1] = {82, 92, 23000, 7000}; return rumble; } void require_switch2_sample(const uint8_t encoded[5], const SwitchHapticsSample& source) { uint64_t bits = 0; for (unsigned index = 0; index < 5; ++index) { bits |= uint64_t{encoded[index]} << (index * 8); } require((bits & 1023u) == 193u + 3u * source.low_frequency_index && ((bits >> 20) & 1023u) == 289u + 3u * source.high_frequency_index && ((bits >> 10) & 1023u) == ((uint32_t{source.low_amplitude_q15} * 29000u / 32767u) >> 6) && ((bits >> 30) & 1023u) == ((uint32_t{source.high_amplitude_q15} * 29000u / 32767u) >> 6), "native physical sample lost its measured frequency or independent band amplitude"); } void test_switch2_hd_pro() { start_pairing_backend(); auto pro = switch2_device(0, UNI_SW2_PRO_PID); ready_switch2(pro); now_ms = 100; const auto first = ordered_switch2_hd(40); const auto second = ordered_switch2_hd(50); bluepad32_input_backend_queue_rumble(0, first); now_ms = 101; bluepad32_input_backend_queue_rumble(0, second); now_ms = 105; process_rumble_timer(&g_rumble_timer); require(switch2_host_events.size() == 2 && pro.rumble_calls == 0 && switch2_host_events[0].received_ms == 100 && switch2_host_events[1].received_ms == 101, "rapid HD commands must bypass the compatibility mailbox in original order and time"); for (unsigned event = 0; event < 2; ++event) { const auto& input = event == 0 ? first : second; const auto& output = switch2_host_events[event].frame; require(output.sides[0].count == 3 && output.sides[1].count == 2, "Pro output must preserve both native side counts"); for (unsigned side = 0; side < 2; ++side) { for (unsigned step = 0; step < output.sides[side].count; ++step) { require_switch2_sample(output.sides[side].samples[step], input.hd.actuators[side].samples[step]); } } } ControllerRumbleOutput partial{}; partial.hd.actuators[0].sample_count = 1; bluepad32_input_backend_queue_rumble(0, first); bluepad32_input_backend_queue_rumble(0, partial); process_rumble_timer(&g_rumble_timer); require(switch2_host_events.size() == 4 && switch2_host_events.back().frame.sides[1].count == 0, "a silent partial update must neither flush older commands nor invent a right stop"); pro.switch2_host_blocked = true; bluepad32_input_backend_queue_rumble(0, first); ControllerRumbleOutput stop{}; stop.hd.actuators[0].sample_count = 1; stop.hd.actuators[1].sample_count = 1; bluepad32_input_backend_queue_rumble(0, stop); now_ms += 100; process_rumble_timer(&g_rumble_timer); require(switch2_host_events.size() == 5 && uni_switch2_haptics_is_stop(&switch2_host_events.back().frame), "explicit native stop must clear older ingress and bypass age/full barriers"); pro.switch2_host_blocked = false; bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{19, 23}); process_rumble_timer(&g_rumble_timer); require(!switch2_host_events.back().hd && switch2_host_events.back().strong == 19 && switch2_host_events.back().weak == 23 && pro.rumble_calls == 0, "both empty HD sides must use the dedicated conventional host API"); } void test_switch2_hd_solo(bool right) { start_pairing_backend(); auto solo = switch2_device(0, right ? UNI_SW2_JOYCON_R_PID : UNI_SW2_JOYCON_L_PID); ready_switch2(solo); const auto input = ordered_switch2_hd(); bluepad32_input_backend_queue_rumble(0, input); process_rumble_timer(&g_rumble_timer); require(switch2_host_events.size() == 1 && switch2_host_events[0].frame.sides[0].count == 3 && switch2_host_events[0].frame.sides[1].count == 0, "either solo half must receive a mono sequence in physical side zero"); const SwitchHapticsSample expected[] = { {40, 91, 24000, 21000}, {82, 62, 23000, 25000}, {82, 63, 23000, 14000}}; for (unsigned index = 0; index < 3; ++index) { require_switch2_sample(switch2_host_events[0].frame.sides[0].samples[index], expected[index]); } auto tie = input; tie.hd.actuators[0].sample_count = 1; tie.hd.actuators[1].sample_count = 1; tie.hd.actuators[1].samples[0].low_amplitude_q15 = 24000; tie.hd.actuators[1].samples[0].high_amplitude_q15 = 4000; bluepad32_input_backend_queue_rumble(0, tie); process_rumble_timer(&g_rumble_timer); require_switch2_sample(switch2_host_events.back().frame.sides[0].samples[0], tie.hd.actuators[0].samples[0]); auto one_side = input; one_side.hd.actuators[0].sample_count = 0; bluepad32_input_backend_queue_rumble(0, one_side); process_rumble_timer(&g_rumble_timer); require(switch2_host_events.back().frame.sides[0].count == 2, "solo absent source side must not fabricate substeps"); require_switch2_sample(switch2_host_events.back().frame.sides[0].samples[1], one_side.hd.actuators[1].samples[1]); } void test_switch2_hd_pair() { start_pairing_backend(); auto right = switch2_device(0, UNI_SW2_JOYCON_R_PID); auto left = switch2_device(1, UNI_SW2_JOYCON_L_PID); ready_switch2(right); ready_switch2(left); right.switch2_host_blocked = true; now_ms = 10; const auto input = ordered_switch2_hd(); bluepad32_input_backend_queue_rumble(0, input); process_rumble_timer(&g_rumble_timer); now_ms = 15; process_rumble_timer(&g_rumble_timer); require(switch2_host_events.size() == 1 && switch2_host_events[0].device == &left, "one blocked half must not duplicate acceptance on its ready partner"); right.switch2_host_blocked = false; now_ms = 20; process_rumble_timer(&g_rumble_timer); require(switch2_host_events.size() == 2 && switch2_host_events[1].device == &right && switch2_host_events[1].received_ms == 10, "paired retry must retain the source timestamp and original missing half"); for (unsigned half = 0; half < 2; ++half) { const auto& output = switch2_host_events[half].frame; require(output.sides[0].count == (half == 0 ? 3 : 2) && output.sides[1].count == 0, "pair stereo must map selected source side onto each physical side zero"); for (unsigned step = 0; step < output.sides[0].count; ++step) { require_switch2_sample(output.sides[0].samples[step], input.hd.actuators[half].samples[step]); } } auto right_only = input; right_only.hd.actuators[0].sample_count = 0; bluepad32_input_backend_queue_rumble(0, right_only); process_rumble_timer(&g_rumble_timer); require(switch2_host_events.size() == 3 && switch2_host_events.back().device == &right, "count zero on one paired half must not submit an invented neutral update"); right.switch2_host_blocked = true; bluepad32_input_backend_queue_rumble(0, input); process_rumble_timer(&g_rumble_timer); now_ms += 50; right.switch2_host_blocked = false; process_rumble_timer(&g_rumble_timer); Bluepad32BackendDiagnostics diagnostics{}; bluepad32_input_backend_diagnostics(&diagnostics); require(switch2_host_events.size() == 4 && diagnostics.switch2_ingress_drops == 1, "an expired partly accepted pair must drop visibly without late or duplicate delivery"); right.switch2_host_blocked = true; bluepad32_input_backend_queue_rumble(0, input); process_rumble_timer(&g_rumble_timer); ControllerRumbleOutput stop{}; stop.hd.actuators[0].sample_count = 1; stop.hd.actuators[1].sample_count = 1; bluepad32_input_backend_queue_rumble(0, stop); process_rumble_timer(&g_rumble_timer); require(switch2_host_events.size() == 7 && switch2_host_events[5].device == &left && switch2_host_events[6].device == &right && switch2_host_events[5].frame.sides[0].count == 1 && switch2_host_events[6].frame.sides[0].count == 1, "paired explicit stop must reach both halves even after partial acceptance and backpressure"); } void test_switch2_hd_overflow() { start_pairing_backend(); auto pro = switch2_device(0, UNI_SW2_PRO_PID); ready_switch2(pro); now_ms = 100; for (uint8_t command = 0; command < 17; ++command) { bluepad32_input_backend_queue_rumble(0, ordered_switch2_hd(40 + command)); } Bluepad32BackendDiagnostics diagnostics{}; bluepad32_input_backend_diagnostics(&diagnostics); require(diagnostics.switch2_ingress_drops == 1 && diagnostics.rumble_pending_slots == 1, "bounded ingress overflow must expose one oldest-command drop"); process_rumble_timer(&g_rumble_timer); require(switch2_host_events.size() == 16, "one drain must preserve all sixteen surviving commands, not a latest-value mailbox"); for (uint8_t index = 0; index < 16; ++index) { const auto input = ordered_switch2_hd(41 + index); require_switch2_sample(switch2_host_events[index].frame.sides[0].samples[0], input.hd.actuators[0].samples[0]); } pro.switch2_host_blocked = true; now_ms = UINT32_MAX - 20; bluepad32_input_backend_queue_rumble(0, ordered_switch2_hd()); now_ms = 28; // 49ms across wrap. process_rumble_timer(&g_rumble_timer); bluepad32_input_backend_diagnostics(&diagnostics); require(diagnostics.rumble_pending_slots == 1 && diagnostics.switch2_ingress_drops == 1, "backpressure must retain an unexpired command across clock wrap"); now_ms = 29; process_rumble_timer(&g_rumble_timer); bluepad32_input_backend_diagnostics(&diagnostics); require(diagnostics.rumble_pending_slots == 0 && diagnostics.switch2_ingress_drops == 2 && switch2_host_events.size() == 16, "original 50ms age must release a backpressured ingress head exactly at expiry"); pro.switch2_host_blocked = false; bluepad32_input_backend_queue_rumble(0, ordered_switch2_hd()); auto& stale = g_slots[0].switch2_ingress; --stale.commands[stale.head].envelope.connection_generation; process_rumble_timer(&g_rumble_timer); bluepad32_input_backend_diagnostics(&diagnostics); require(switch2_host_events.size() == 16 && diagnostics.switch2_ingress_drops == 3, "stale logical generation must be rejected before physical submission"); bluepad32_input_backend_queue_rumble(0, ordered_switch2_hd()); --stale.commands[stale.head].generation; process_rumble_timer(&g_rumble_timer); bluepad32_input_backend_diagnostics(&diagnostics); require(switch2_host_events.size() == 16 && diagnostics.switch2_ingress_drops == 4, "stale haptics epoch must not survive a same-connection mode reset"); switch2_output_drops = 7; bluepad32_input_backend_diagnostics(&diagnostics); require(diagnostics.switch2_output_drops == 7, "physical output loss must remain visible separately from ingress loss"); } void test_switch2_hd_epochs() { start_pairing_backend(); auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID); auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID); ready_switch2(left); bluepad32_input_backend_queue_rumble(0, ordered_switch2_hd()); process_rumble_timer(&g_rumble_timer); const unsigned left_resets = left.switch2_haptics_resets; bluepad32_input_backend_queue_rumble(0, ordered_switch2_hd()); ready_switch2(right); process_rumble_timer(&g_rumble_timer); require(switch2_host_events.size() == 1 && left.switch2_haptics_resets > left_resets && right.switch2_haptics_resets != 0, "merge must reset accepted physical output and drop queued solo commands"); bluepad32_input_backend_queue_rumble(0, ordered_switch2_hd()); process_rumble_timer(&g_rumble_timer); const unsigned pair_resets = left.switch2_haptics_resets; bluepad32_input_backend_queue_rumble(0, ordered_switch2_hd()); platform_on_device_disconnected(&right); process_rumble_timer(&g_rumble_timer); require(switch2_host_events.size() == 3 && left.switch2_haptics_resets > pair_resets, "survivor transition must flush both physical and ingress pair state"); bluepad32_input_backend_queue_rumble(0, ordered_switch2_hd()); platform_on_device_disconnected(&left); auto replacement = switch2_device(0, UNI_SW2_PRO_PID); ready_switch2(replacement); process_rumble_timer(&g_rumble_timer); require(switch2_host_events.size() == 3, "reconnect must not inherit a former logical generation's host output"); #ifdef SWITCH_PICO_USB_OUTPUT_MODES bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{27, 35}); process_rumble_timer(&g_rumble_timer); const unsigned mode_resets = replacement.switch2_haptics_resets; bluepad32_input_backend_queue_rumble(0, ordered_switch2_hd()); test_adapter_mode = AdapterUsbMode::kSwitchProbe; process_rumble_timer(&g_rumble_timer); require(switch2_host_events.size() == 4 && replacement.switch2_haptics_resets > mode_resets, "mode boundary without new input must neutralize held output and queued old-mode HD"); bluepad32_input_backend_queue_rumble(0, ordered_switch2_hd(50)); test_adapter_mode = AdapterUsbMode::kXInput; bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{45, 55}); process_rumble_timer(&g_rumble_timer); require(switch2_host_events.size() == 5 && !switch2_host_events.back().hd && switch2_host_events.back().duration_ms == UINT16_MAX && switch2_host_events.back().strong == 45, "producer-side mode transition must discard old epoch before accepting new host state"); #endif } void test_switch2_hd_feedback() { start_pairing_backend(); auto pro = switch2_device(0, UNI_SW2_PRO_PID); ready_switch2(pro); bluepad32_input_backend_queue_profile_feedback( 0, slot_snapshot(0).connection_generation, 2, ControllerProfileConfirmationPolicy::kRumble); process_rumble_timer(&g_rumble_timer); require(pro.rumble_calls == 1, "profile confirmation must retain local-feedback ownership"); now_ms = 10; bluepad32_input_backend_queue_rumble(0, ordered_switch2_hd()); process_rumble_timer(&g_rumble_timer); now_ms = 20; bluepad32_input_backend_queue_rumble(0, ordered_switch2_hd(50)); process_rumble_timer(&g_rumble_timer); require(switch2_host_events.size() == 2 && pro.rumble_calls == 1 && switch2_host_events[0].received_ms == 10 && switch2_host_events[1].received_ms == 20, "host substeps must advance during feedback without compatibility dispatch or fresh timestamps"); now_ms = 300; process_rumble_timer(&g_rumble_timer); require(switch2_host_events.size() == 2, "feedback completion must not replay historical host substeps"); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{33, 44}); process_rumble_timer(&g_rumble_timer); require(switch2_host_events.size() == 3 && !switch2_host_events.back().hd && switch2_host_events.back().duration_ms == host_rumble_duration_ms(), "conventional host vibration must share the bounded host queue, not local feedback"); now_ms += 1000; process_rumble_timer(&g_rumble_timer); require(switch2_host_events.size() == 3, "stateful host output must not require backend periodic resubmission"); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{}); process_rumble_timer(&g_rumble_timer); require(switch2_host_events.size() == 4 && switch2_host_events.back().duration_ms == 0 && switch2_host_events.back().weak == 0 && switch2_host_events.back().strong == 0, "conventional all-zero host command must explicitly stop retained state"); pro.switch2_host_blocked = true; const uint32_t received_ms = now_ms; bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{65, 75}); now_ms += 1000; process_rumble_timer(&g_rumble_timer); pro.switch2_host_blocked = false; process_rumble_timer(&g_rumble_timer); if (host_rumble_duration_ms() == UINT16_MAX) { require(switch2_host_events.size() == 5 && switch2_host_events.back().received_ms == received_ms && switch2_host_events.back().strong == 65, "held XInput host state must survive backpressure without retimestamping"); } else { Bluepad32BackendDiagnostics diagnostics{}; bluepad32_input_backend_diagnostics(&diagnostics); require(switch2_host_events.size() == 4 && diagnostics.switch2_ingress_drops == 1, "finite conventional host state must expire under feedback/backpressure, not revive"); } } void test_switch2_pair_lifecycle(bool right_first) { start_pairing_backend(); uni_hid_device_t left = switch2_device( right_first ? 1 : 0, UNI_SW2_JOYCON_L_PID); uni_hid_device_t right = switch2_device( right_first ? 0 : 1, UNI_SW2_JOYCON_R_PID); left.conn.btaddr[0] = 0xc1; left.switch2_identity_address_type = BD_ADDR_TYPE_LE_RANDOM; initialize_runtime_profile_storage(); const ControllerIdentity left_identity = identity_for_device(&left); const ControllerIdentity right_identity = identity_for_device(&right); ControllerProfile left_profile = controller_profile_default(left_identity, 2); left_profile.weak_rumble_scale = 37; require(runtime_profile_storage.set(left_identity, 2, left_profile) == ProfileStorageResult::kOk && runtime_profile_storage.activate(left_identity, 2) == ProfileStorageResult::kOk && runtime_profile_storage.activate(right_identity, 5) == ProfileStorageResult::kOk, "solo owners must have independently selected profiles before pairing"); uni_hid_device_t& first = right_first ? right : left; uni_hid_device_t& second = right_first ? left : right; ready_switch2(first); uni_controller_t left_data{}; left_data.klass = UNI_CONTROLLER_CLASS_GAMEPAD; left_data.gamepad.dpad = DPAD_UP; left_data.gamepad.axis_x = -512; left_data.gamepad.axis_y = 100; left_data.gamepad.buttons = BUTTON_TRIGGER_L; left_data.gamepad.accel[0] = 8192; left.switch2_extra_buttons = UNI_SW2_BUTTON_GL | UNI_SW2_BUTTON_LEFT_SL | UNI_SW2_BUTTON_LEFT_SR; uni_controller_t right_data{}; right_data.klass = UNI_CONTROLLER_CLASS_GAMEPAD; right_data.gamepad.buttons = BUTTON_B | BUTTON_THUMB_R | BUTTON_TRIGGER_R; right_data.gamepad.axis_rx = 200; right_data.gamepad.axis_ry = -512; right_data.gamepad.accel[2] = 8192; right.switch2_extra_buttons = UNI_SW2_BUTTON_C | UNI_SW2_BUTTON_GR | UNI_SW2_BUTTON_RIGHT_SL | UNI_SW2_BUTTON_RIGHT_SR; platform_on_controller_data(&first, right_first ? &right_data : &left_data); const Bluepad32SlotSnapshot solo = slot_snapshot(0); require(solo.active && solo.state.button_left_shoulder && solo.state.button_right_shoulder && solo.state.button_left_stick == right_first && solo.state.right_stick_x == 0 && solo.state.right_stick_y == 0 && (right_first ? solo.state.button_south : solo.state.button_west), "solo JoyCon must rotate face controls, stick click and rail shoulders"); Bluepad32PlaytestSnapshot playtest{}; bluepad32_input_backend_playtest_snapshot(0, &playtest); require(playtest.controller_layout == (right_first ? Bluepad32ControllerLayout::kJoyCon2RightSolo : Bluepad32ControllerLayout::kJoyCon2LeftSolo), "playtest must identify the live rotated solo half"); require(solo.state.left_stick_x == (right_first ? INT16_MAX : scale_axis(100)) && solo.state.left_stick_y == (right_first ? scale_axis(200) : INT16_MAX), "solo JoyCon stick must rotate with its physical sideways orientation"); require(bluepad32_input_backend_capture_start( 0, solo.connection_generation, CaptureOptions{}), "solo capture must start on its logical generation"); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{31, 41}); bluepad32_input_backend_queue_profile_feedback( 0, solo.connection_generation, 8, ControllerProfileConfirmationPolicy::kRumbleAndLed); ready_switch2(second); Bluepad32SlotSnapshot merged = slot_snapshot(0); require(merged.active && !slot_snapshot(1).active && merged.connection_generation != solo.connection_generation, "either connection order must merge into the first-ready output"); require_pair_owner(merged.identity, left, right); require_active_profile(merged.identity, 2, 37); ControllerProfile pair_profile = controller_profile_default(merged.identity, 7); pair_profile.weak_rumble_scale = 95; require(runtime_profile_storage.set(merged.identity, 7, pair_profile) == ProfileStorageResult::kOk && runtime_profile_storage.activate(merged.identity, 7) == ProfileStorageResult::kOk, "the merged bank must be independently editable"); require_active_profile(left_identity, 2, 37); require_active_profile(right_identity, 5, UINT8_MAX); bluepad32_input_backend_playtest_snapshot(0, &playtest); require(playtest.controller_layout == Bluepad32ControllerLayout::kJoyCon2MergedPair && playtest.state.motion_sample_count == 0 && controller_identity_equal(playtest.identity, merged.identity), "playtest must detect a live companion before any merged motion report"); Bluepad32CaptureSnapshot capture{}; require(bluepad32_input_backend_capture_page(0, 0, &capture) && capture.state == CaptureState::kDisconnected, "solo recording must not silently cross into the merged generation"); require(!slot_snapshot(1).state.extra_buttons && !slot_snapshot(1).state.button_south && !slot_snapshot(1).state.button_west && left.last_rumble_duration_ms == 0 && right.last_rumble_duration_ms == 0, "pair merge must neutralize ghost output and stop old feedback"); const int calls_after_merge = left.rumble_calls + right.rumble_calls; bluepad32_input_backend_queue_profile_feedback( 0, solo.connection_generation, 8, ControllerProfileConfirmationPolicy::kRumbleAndLed); process_rumble_timer(&g_rumble_timer); require(left.rumble_calls + right.rumble_calls == calls_after_merge && left.player_leds == 1 && right.player_leds == 1, "old-generation solo feedback must not reach the pair"); require(!bluepad32_input_backend_identify(left_identity) && !bluepad32_input_backend_identify(right_identity) && bluepad32_input_backend_identify(merged.identity), "identify must match the pair owner, not either physical solo owner"); process_rumble_timer(&g_rumble_timer); require(left.last_low == UINT8_MAX && right.last_low == UINT8_MAX && left.player_leds == 1 && right.player_leds == 1, "identifying the pair owner must reach both physical halves"); now_ms += 150; process_rumble_timer(&g_rumble_timer); dispatch_identity_event(SM_EVENT_IDENTITY_RESOLVING_SUCCEEDED, left, left.switch2_identity_address_type, left.conn.btaddr); dispatch_identity_event(SM_EVENT_IDENTITY_RESOLVING_SUCCEEDED, right, right.switch2_identity_address_type, right.conn.btaddr); require(controller_identity_equal(slot_snapshot(0).identity, merged.identity), "physical identity publication must not replace an enrolled composite owner"); dispatch_identity_event(SM_EVENT_IDENTITY_RESOLVING_FAILED, left, left.switch2_identity_address_type, left.conn.btaddr); dispatch_identity_event(SM_EVENT_IDENTITY_RESOLVING_STARTED, right, right.switch2_identity_address_type, right.conn.btaddr); require(controller_identity_equal(slot_snapshot(0).identity, merged.identity), "physical identity resolution resets must not demote a live pair to global"); platform_on_controller_data(&left, &left_data); platform_on_controller_data(&right, &right_data); merged = slot_snapshot(0); require(merged.state.dpad_up && merged.state.button_east && merged.state.button_right_stick && !merged.state.button_left_stick && !merged.state.button_left_shoulder && !merged.state.button_right_shoulder && merged.state.left_stick_x == INT16_MIN && merged.state.right_stick_x == scale_axis(200) && merged.state.left_trigger == UINT16_MAX && merged.state.right_trigger == UINT16_MAX && merged.state.extra_buttons == 0x7f && merged.state.motion_samples[0].accel_x == -4096 && merged.state.motion_samples[0].accel_y == 0, "merged native controls and extras must combine with right-only aim motion"); bluepad32_input_backend_report_sent(0); platform_on_controller_data(&left, &left_data); require(slot_snapshot(0).state.motion_sample_count == 0, "left reports must not replay the last right motion sample"); uni_hid_device_t ordinary = device(2); platform_on_device_connected(&ordinary); require(platform_on_device_ready(&ordinary) == UNI_ERROR_SUCCESS, "ordinary device must coexist with a pair"); uni_controller_t ordinary_data{}; ordinary_data.klass = UNI_CONTROLLER_CLASS_GAMEPAD; ordinary_data.gamepad.buttons = BUTTON_Y; platform_on_controller_data(&ordinary, &ordinary_data); const Bluepad32SlotSnapshot ordinary_before = slot_snapshot(2); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{51, 61}); bluepad32_input_backend_queue_rumble(2, ControllerRumbleOutput{71, 81}); process_rumble_timer(&g_rumble_timer); require(left.last_low == 51 && right.last_low == 51 && left.last_high == 61 && right.last_high == 61 && ordinary.last_low == 71 && ordinary.last_high == 81, "pair feedback must fan out without reaching an ordinary player"); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{}); process_rumble_timer(&g_rumble_timer); require(left.last_rumble_duration_ms == 0 && right.last_rumble_duration_ms == 0 && ordinary.last_low == 71, "pair stop must stop both physical motors only"); bluepad32_input_backend_queue_profile_feedback( 0, merged.connection_generation, 2, ControllerProfileConfirmationPolicy::kRumbleAndLed); process_rumble_timer(&g_rumble_timer); require(left.player_leds == 3 && right.player_leds == 3 && left.last_low == UINT8_MAX && right.last_low == UINT8_MAX, "profile feedback must light and rumble both halves"); now_ms += 300; process_rumble_timer(&g_rumble_timer); require(left.player_leds == 1 && right.player_leds == 1, "profile completion must restore both halves' player indication"); uni_hid_device_t& lost = right_first ? left : right; uni_hid_device_t& survivor = right_first ? right : left; bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{91, 101}); platform_on_device_disconnected(&lost); const Bluepad32SlotSnapshot detached = slot_snapshot(0); require(detached.active && !slot_snapshot(1).active && detached.connection_generation != merged.connection_generation && controller_identity_equal(detached.identity, identity_for_device(&survivor)) && detached.state.extra_buttons == survivor.switch2_extra_buttons && detached.state.motion_sample_count == 0 && !detached.state.dpad_up && !detached.state.button_east && (right_first ? detached.state.button_south : detached.state.button_west), "either half detach must immediately publish only the rotated survivor and own profile"); require_active_profile(detached.identity, right_first ? 5 : 2, right_first ? UINT8_MAX : 37); require(!bluepad32_input_backend_identify(merged.identity), "an offline pair bank must not identify its surviving solo member"); bluepad32_input_backend_playtest_snapshot(0, &playtest); require(playtest.controller_layout == (right_first ? Bluepad32ControllerLayout::kJoyCon2RightSolo : Bluepad32ControllerLayout::kJoyCon2LeftSolo), "playtest must stop presenting a pair immediately after companion loss"); require(survivor.last_rumble_duration_ms == 0 && slot_snapshot(2).connection_generation == ordinary_before.connection_generation && slot_snapshot(2).state.button_north, "detach must cancel survivor feedback without changing unrelated player state"); const int survivor_calls = survivor.rumble_calls; platform_on_controller_data(&lost, right_first ? &left_data : &right_data); require(platform_on_device_ready(&lost) == UNI_ERROR_NO_SLOTS, "late ready for a detached half must not resurrect its old generation"); bluepad32_input_backend_queue_profile_feedback( 0, merged.connection_generation, 8, ControllerProfileConfirmationPolicy::kRumbleAndLed); process_rumble_timer(&g_rumble_timer); require(survivor.rumble_calls == survivor_calls && slot_snapshot(0).state.extra_buttons == survivor.switch2_extra_buttons, "late detached input and generation-bound feedback must be ignored"); uni_hid_device_t replacement = switch2_device(lost.idx, lost.product_id); memcpy(replacement.conn.btaddr, lost.conn.btaddr, sizeof(bd_addr_t)); replacement.switch2_identity_address_type = lost.switch2_identity_address_type; left_profile.weak_rumble_scale = 61; require(runtime_profile_storage.set(left_identity, 2, left_profile) == ProfileStorageResult::kOk, "changing the source solo must remain independent while detached"); ready_switch2(replacement); platform_on_device_disconnected(&lost); require(slot_snapshot(0).active && !slot_snapshot(1).active && slot_snapshot(0).state.extra_buttons == survivor.switch2_extra_buttons && slot_snapshot(0).connection_generation != detached.connection_generation, "replacement must re-pair without stale presses or a late old disconnect"); require(controller_identity_equal(slot_snapshot(0).identity, merged.identity), "rejoining the same typed physical members must restore the same pair owner"); require_active_profile(slot_snapshot(0).identity, 7, 95); const uint32_t clear_token = bluepad32_input_backend_clear_pairings(); process_rumble_timer(&g_rumble_timer); Bluepad32PairingSnapshot cleared{}; bluepad32_input_backend_pairing_snapshot(&cleared); require(bluepad32_input_backend_clear_pairings_completed(cleared, clear_token) && device_disconnect_calls == 3 && !slot_snapshot(0).active && !slot_snapshot(2).active && !switch_pico_switch2_pairing_allowed(), "clear must disconnect every physical half and ordinary device, not just outputs"); } void test_switch2_multiple_pairs() { start_pairing_backend(); uni_hid_device_t right0 = switch2_device(0, UNI_SW2_JOYCON_R_PID); uni_hid_device_t right1 = switch2_device(1, UNI_SW2_JOYCON_R_PID); uni_hid_device_t left0 = switch2_device(2, UNI_SW2_JOYCON_L_PID); uni_hid_device_t left1 = switch2_device(3, UNI_SW2_JOYCON_L_PID); ready_switch2(right0); ready_switch2(right1); ready_switch2(left0); ready_switch2(left1); require(slot_snapshot(0).active && slot_snapshot(1).active && !slot_snapshot(2).active && !slot_snapshot(3).active && g_connection_policy_state == ConnectionPolicyState::Paused && !scanning_enabled && !incoming_connections, "two deterministic pairs must consume four physical resources but only two outputs"); require_pair_owner(slot_snapshot(0).identity, left0, right0); require_pair_owner(slot_snapshot(1).identity, left1, right1); uni_controller_t data{}; data.klass = UNI_CONTROLLER_CLASS_GAMEPAD; data.gamepad.buttons = BUTTON_A; right0.switch2_extra_buttons = UNI_SW2_BUTTON_C; platform_on_controller_data(&right0, &data); data.gamepad.buttons = BUTTON_Y; right1.switch2_extra_buttons = UNI_SW2_BUTTON_GR; platform_on_controller_data(&right1, &data); require(slot_snapshot(0).state.button_south && !slot_snapshot(0).state.button_north && slot_snapshot(0).state.extra_buttons == UNI_SW2_BUTTON_C && slot_snapshot(1).state.button_north && !slot_snapshot(1).state.button_south && slot_snapshot(1).state.extra_buttons == UNI_SW2_BUTTON_GR, "two pairs must not cross-contaminate normal or extra source input"); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{11, 21}); bluepad32_input_backend_queue_rumble(1, ControllerRumbleOutput{31, 41}); process_rumble_timer(&g_rumble_timer); require(left0.last_low == 11 && right0.last_low == 11 && left1.last_low == 31 && right1.last_low == 31 && left0.player_leds == 1 && right0.player_leds == 1 && left1.player_leds == 2 && right1.player_leds == 2, "multiple pairs must retain isolated rumble and player lighting"); const Bluepad32SlotSnapshot other_pair = slot_snapshot(1); platform_on_device_disconnected(&right0); uni_hid_device_t ordinary = device(0); platform_on_device_connected(&ordinary); require(platform_on_device_ready(&ordinary) == UNI_ERROR_SUCCESS && slot_snapshot(0).active && slot_snapshot(2).active && !slot_snapshot(3).active && controller_identity_equal(slot_snapshot(0).identity, identity_for_device(&left0)), "reusing freed physical index must allocate a free output, not evict the surviving half"); data.gamepad.buttons = BUTTON_B; platform_on_controller_data(&ordinary, &data); bluepad32_input_backend_queue_rumble(2, ControllerRumbleOutput{91, 101}); process_rumble_timer(&g_rumble_timer); require(slot_snapshot(2).state.button_east && !slot_snapshot(0).state.button_east && ordinary.last_low == 91 && left1.last_low == 31 && right1.last_low == 31 && slot_snapshot(1).connection_generation == other_pair.connection_generation && slot_snapshot(1).state.button_north, "remapped ordinary physical index must isolate input and feedback from both pairs"); } void test_switch2_pair_admission_failure(bool right_first) { start_pairing_backend(); initialize_runtime_profile_storage(); auto left = switch2_device(right_first ? 1 : 0, UNI_SW2_JOYCON_L_PID); auto right = switch2_device(right_first ? 0 : 1, UNI_SW2_JOYCON_R_PID); auto& first = right_first ? right : left; auto& second = right_first ? left : right; ready_switch2(first); const ControllerIdentity solo_identity = identity_for_device(&first); require(runtime_profile_storage.activate(solo_identity, 3) == ProfileStorageResult::kOk, "the first solo must have an existing independent active profile"); uni_controller_t data{}; data.klass = UNI_CONTROLLER_CLASS_GAMEPAD; data.gamepad.buttons = BUTTON_TRIGGER_L | BUTTON_TRIGGER_R; platform_on_controller_data(&first, &data); const auto solo = slot_snapshot(0); require(bluepad32_input_backend_capture_start( 0, solo.connection_generation, CaptureOptions{}), "the first solo must remain recordable during pair admission"); bluepad32_input_backend_queue_profile_feedback( 0, solo.connection_generation, 2, ControllerProfileConfirmationPolicy::kRumble); const int initial_rumble_calls = first.rumble_calls; platform_on_device_connected(&second); second.switch2_identity_valid = false; require(platform_on_device_ready(&second) == UNI_ERROR_INIT_FAILED && pair_observation_count == 0 && first.rumble_calls == initial_rumble_calls, "an unstable member must reject pairing before storage or solo output changes"); second.switch2_identity_valid = true; first.switch2_identity_valid = false; require(platform_on_device_ready(&second) == UNI_ERROR_INIT_FAILED && pair_observation_count == 0 && controller_identity_equal(slot_snapshot(0).identity, solo_identity), "an unresolved first-ready member must not merge using its stale solo owner"); first.switch2_identity_valid = true; require(runtime_profile_storage.ensure_identity(identity_for_device(&second)) == ProfileStorageResult::kOk, "preexisting member rows must allow exercising failure of the actual pair seed"); before_pair_seed = [](const ControllerIdentity&) { const auto first_solo = slot_snapshot(0); Bluepad32CaptureSnapshot capture{}; require(first_solo.active && first_solo.identity.transport == ControllerTransport::kBle && !slot_snapshot(1).active && bluepad32_input_backend_capture_page(0, 0, &capture) && capture.state == CaptureState::kRecording, "persistent pair setup must precede any visible solo topology change"); }; fail_profile_program = true; require(platform_on_device_ready(&second) == UNI_ERROR_INIT_FAILED && pair_observation_count == 1, "pair seed I/O failure must explicitly reject the second ready callback"); const auto rejected = slot_snapshot(0); require(rejected.active && !slot_snapshot(1).active && rejected.connection_generation == solo.connection_generation && controller_identity_equal(rejected.identity, solo_identity) && rejected.state.left_trigger == solo.state.left_trigger && first.rumble_calls == initial_rumble_calls && first.player_leds == 1 && second.player_leds == 0, "seed failure must leave the first solo's owner, input, generation and outputs intact"); ControllerIdentity pair_identity{}; require(controller_identity_make_joycon_pair( identity_for_device(&left), identity_for_device(&right), &pair_identity) && runtime_profile_storage.find(pair_identity) == nullptr, "a failed pair seed must not expose a partial persistent owner"); process_rumble_timer(&g_rumble_timer); require(first.last_low == UINT8_MAX && second.rumble_calls == 0, "a rejected merge must retain the first solo's queued generation-bound feedback"); fail_profile_program = false; require(platform_on_device_ready(&second) == UNI_ERROR_INIT_FAILED, "a retry before catalog replay must not expose a failed seed"); require(runtime_profile_storage.initialize(runtime_profile_io()), "storage replay must recover intact member banks after admission failure"); require_active_profile(solo_identity, 3, UINT8_MAX); require(platform_on_device_ready(&second) == UNI_ERROR_SUCCESS, "a replayed catalog must allow the pending physical member to merge on retry"); before_pair_seed = nullptr; require_pair_owner(slot_snapshot(0).identity, left, right); require_active_profile(slot_snapshot(0).identity, right_first ? 0 : 3, UINT8_MAX); } void test_switch2_pair_member_replacement() { start_pairing_backend(); auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID); auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID); right.conn.btaddr[0] = 0xc2; ready_switch2(left); ready_switch2(right); const ControllerIdentity original = slot_snapshot(0).identity; require(runtime_profile_storage.activate(original, 6) == ProfileStorageResult::kOk, "the original physical pair must have a distinct saved selection"); platform_on_device_disconnected(&right); auto replacement = switch2_device(1, UNI_SW2_JOYCON_R_PID); replacement.conn.btaddr[5] = 0x35; ready_switch2(replacement); const ControllerIdentity replaced = slot_snapshot(0).identity; require_pair_owner(replaced, left, replacement); require(!controller_identity_equal(original, replaced) && !bluepad32_input_backend_identify(original), "swapping only the right member must select a different pair bank and owner"); require_active_profile(replaced, 0, UINT8_MAX); require(runtime_profile_storage.activate(replaced, 4) == ProfileStorageResult::kOk, "the replacement pair bank must be independently selectable"); platform_on_device_disconnected(&replacement); ready_switch2(right); require(controller_identity_equal(slot_snapshot(0).identity, original), "restoring the original right member must restore the original pair key"); require_active_profile(slot_snapshot(0).identity, 6, UINT8_MAX); require_active_profile(replaced, 4, UINT8_MAX); platform_on_device_disconnected(&right); auto typed_right = switch2_device(1, UNI_SW2_JOYCON_R_PID); memcpy(typed_right.conn.btaddr, right.conn.btaddr, sizeof(bd_addr_t)); typed_right.switch2_identity_address_type = BD_ADDR_TYPE_LE_RANDOM; ready_switch2(typed_right); const ControllerIdentity retyped = slot_snapshot(0).identity; require_pair_owner(retyped, left, typed_right); require(!controller_identity_equal(retyped, original), "a member's address type must participate in the pair key even with the same MAC"); require_active_profile(retyped, 0, UINT8_MAX); require_active_profile(original, 6, UINT8_MAX); } void test_switch2_admission() { start_backend(); require(!switch_pico_switch2_pairing_allowed(), "idle scanning must not grant fresh proprietary pairing"); bluepad32_input_backend_open_pairing_window(); require(!switch_pico_switch2_pairing_allowed(), "pending Core0 request must not grant pairing before policy consumes it"); process_rumble_timer(&g_rumble_timer); require(switch_pico_switch2_pairing_allowed(), "consumed explicit pairing window must permit proprietary pairing"); now_ms = g_pairing_window_deadline_ms; require(!switch_pico_switch2_pairing_allowed(), "fresh pairing must close at its deadline even before timer processing"); uni_hid_device_t pro = switch2_device(0, UNI_SW2_PRO_PID); uni_hid_device_t ordinary = device(1, true, UNI_BT_CONN_PROTOCOL_BLE); register_lookup_device(&pro); register_lookup_device(&ordinary); __wrap_sm_request_pairing(pro.conn.handle); require(device_disconnect_calls == 1 && last_disconnected_device == &pro && ordinary_smp_requests == 0 && delete_key_calls == 0, "Switch2 GATT auth failure must disconnect without SMP or bond deletion"); __wrap_sm_request_pairing(ordinary.conn.handle); __wrap_sm_request_pairing(0x99); require(ordinary_smp_requests == 2 && device_disconnect_calls == 1, "ordinary and unknown handles must preserve standard SMP behavior"); require(identity_for_device(&pro).stable && controller_identity_is_global(identity_for_device(&ordinary)), "only parser-validated proprietary public identity may bypass SMP resolution"); pro.switch2_identity_address_type = BD_ADDR_TYPE_LE_RANDOM; pro.conn.btaddr[0] = 0xc1; require(identity_for_device(&pro).stable && identity_for_device(&pro).address_type == BD_ADDR_TYPE_LE_RANDOM, "parser-validated static random identity must retain its address type"); pro.switch2_identity_valid = false; pro.conn.btaddr[0] = 0x41; require(controller_identity_is_global(identity_for_device(&pro)), "unvalidated proprietary RPA must remain on the global profile"); pro.switch2_identity_valid = true; pro.conn.btaddr[0] = 0xc1; ready_switch2(pro); uni_controller_t input{}; input.klass = UNI_CONTROLLER_CLASS_GAMEPAD; input.gamepad.buttons = BUTTON_B; input.gamepad.axis_x = 511; input.gamepad.axis_ry = -512; pro.switch2_extra_buttons = UNI_SW2_BUTTON_C | UNI_SW2_BUTTON_GL | UNI_SW2_BUTTON_GR; platform_on_controller_data(&pro, &input); require(slot_snapshot(0).state.button_east && slot_snapshot(0).state.left_stick_x == INT16_MAX && slot_snapshot(0).state.right_stick_y == INT16_MIN && slot_snapshot(0).state.extra_buttons == 7, "Pro2 must preserve vertical normal controls and ingest remappable extras"); } void test_switch2_radio_policy(bool individual) { start_backend(); if (individual) set_runtime_joycon_mode(JoyConMode::kIndividual); auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID); auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID); auto classic = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR); auto other_ble = device(3, true, UNI_BT_CONN_PROTOCOL_BLE); negotiated_intervals[other_ble.conn.handle] = 24; ready_switch2(left); require(negotiated_intervals[left.conn.handle] == 6, "a single Switch2 link must retain fast scheduling"); ready_switch2(right); require(incoming_connections && negotiated_intervals[left.conn.handle] == 6 && negotiated_intervals[right.conn.handle] == 6, "paired and individual Joy-Cons must retain fast links before Classic arrival"); platform_on_device_connected(&other_ble); require(platform_on_device_ready(&other_ble) == UNI_ERROR_SUCCESS, "unrelated BLE controller must join"); platform_on_device_connected(&classic); require(negotiated_intervals[left.conn.handle] == 6 && negotiated_intervals[right.conn.handle] == 6 && negotiated_intervals[other_ble.conn.handle] == 24, "Classic setup must not slow Joy-Cons or retime unrelated BLE"); require(platform_on_device_ready(&classic) == UNI_ERROR_SUCCESS, "Classic controller must complete setup alongside the pair"); const auto pair = slot_snapshot(0); platform_on_device_disconnected(&classic); require(negotiated_intervals[left.conn.handle] == 6 && negotiated_intervals[right.conn.handle] == 6 && slot_snapshot(0).connection_generation == pair.connection_generation, "Classic departure must preserve fast Joy-Con links without rebinding the pair"); platform_on_device_connected(&classic); require(platform_on_device_ready(&classic) == UNI_ERROR_SUCCESS, "Classic reconnect must complete"); platform_on_device_disconnected(&left); require(negotiated_intervals[right.conn.handle] == 6, "a single surviving Switch2 link must return to fast scheduling even with Classic"); left = switch2_device(0, UNI_SW2_JOYCON_L_PID); ready_switch2(left); require(negotiated_intervals[left.conn.handle] == 6 && negotiated_intervals[right.conn.handle] == 6, "physical index and handle reuse must retain fast Joy-Con links"); negotiated_intervals[left.conn.handle] = 24; negotiated_intervals[right.conn.handle] = 24; now_ms += 1000; process_configuration_timer(&g_configuration_timer); require(negotiated_intervals[left.conn.handle] == 6 && negotiated_intervals[right.conn.handle] == 6, "peer-negotiated slow Joy-Con links must reconcile to the fast default"); platform_on_device_disconnected(&right); right = switch2_device(1, UNI_SW2_PRO_PID); negotiated_intervals[right.conn.handle] = 24; ready_switch2(right); require(negotiated_intervals[left.conn.handle] == 6 && negotiated_intervals[right.conn.handle] == 6 && negotiated_intervals[other_ble.conn.handle] == 24, "Switch2 Pro must join at the fast interval without retiming unrelated BLE"); platform_on_device_disconnected(&left); left = switch2_device(0, UNI_SW2_PRO_PID); negotiated_intervals[left.conn.handle] = 24; ready_switch2(left); require(negotiated_intervals[left.conn.handle] == 6 && negotiated_intervals[right.conn.handle] == 6, "multiple Switch2 Pro links must remain fast alongside Classic"); } void test_switch2_radio_settling() { start_backend(); auto classic = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR); auto left = switch2_device(0, UNI_SW2_PRO_PID); auto right = switch2_device(1, UNI_SW2_PRO_PID); platform_on_device_connected(&classic); require(platform_on_device_ready(&classic) == UNI_ERROR_SUCCESS, "Classic-first connection must become ready"); negotiated_intervals[left.conn.handle] = 24; negotiated_intervals[right.conn.handle] = 24; defer_interval_updates = true; now_ms = UINT32_MAX - 10; ready_switch2(left); platform_on_device_connected(&right); require(interval_requests[right.conn.handle] == 0, "pending Switch2 setup must retain ownership of its initial interval"); require(platform_on_device_ready(&right) == UNI_ERROR_SUCCESS, "second Switch2 connection must become ready"); require(pending_intervals[left.conn.handle] == 6 && pending_intervals[right.conn.handle] == 6, "both slow Switch2 Pro links must request the fast interval"); const unsigned deferred_attempts = interval_requests[left.conn.handle]; now_ms += 999; process_configuration_timer(&g_configuration_timer); require(interval_requests[left.conn.handle] == deferred_attempts && negotiated_intervals[left.conn.handle] == 24, "an accepted asynchronous request must be allowed to settle across clock wrap"); platform_on_device_disconnected(&right); negotiated_intervals[left.conn.handle] = pending_intervals[left.conn.handle]; process_configuration_timer(&g_configuration_timer); require(negotiated_intervals[left.conn.handle] == 6 && interval_requests[left.conn.handle] == deferred_attempts, "a late fast-interval acknowledgement must survive another controller leaving"); defer_interval_updates = false; reject_interval_updates = true; negotiated_intervals[left.conn.handle] = 24; right = switch2_device(1, UNI_SW2_PRO_PID); negotiated_intervals[right.conn.handle] = 24; ready_switch2(right); const unsigned left_attempts = interval_requests[left.conn.handle]; const unsigned right_attempts = interval_requests[right.conn.handle]; now_ms += 999; process_configuration_timer(&g_configuration_timer); require(interval_requests[left.conn.handle] == left_attempts && interval_requests[right.conn.handle] == right_attempts, "rejected negotiations must not flood the HCI command queue"); reject_interval_updates = false; ++now_ms; process_configuration_timer(&g_configuration_timer); require(negotiated_intervals[left.conn.handle] == 6 && negotiated_intervals[right.conn.handle] == 6, "transiently rejected fast intervals must recover without reconnect or pairing"); } void test_switch2_mate_reconnect() { start_backend(); auto right = switch2_device(0, UNI_SW2_JOYCON_R_PID); auto left = switch2_device(1, UNI_SW2_JOYCON_L_PID); left.conn.btaddr[0] = 0xc1; left.switch2_identity_address_type = BD_ADDR_TYPE_LE_RANDOM; remember_switch2(right); remember_switch2(left); register_lookup_device(&right); register_lookup_device(&left); ready_switch2(right); require(scanning_enabled && background_scan_parameters && !classic_scanning_enabled && incoming_connections && !bondable && accepted_stk_methods == 0 && !switch_pico_switch2_pairing_allowed(), "first remembered half must seek its mate with low-duty BLE and authentication closed"); dispatch_pairing_event(HCI_EVENT_USER_CONFIRMATION_REQUEST); dispatch_pairing_event(HCI_EVENT_USER_PASSKEY_REQUEST); require(confirmation_accepts == 0 && passkey_accepts == 0 && confirmation_rejections == 1 && passkey_rejections == 1, "mate scanning must not authorize new Classic authentication"); bd_addr_t missing = {1, 2, 3, 4, 5, 6}; require(platform_on_device_discovered(missing, "Joy-Con 2 (L)", 0, 0) == UNI_ERROR_IGNORE_DEVICE, "a device name without a prepared parser candidate cannot authorize passive discovery"); const auto reject_left = [&]() { require(platform_on_device_discovered(left.conn.btaddr, "Joy-Con 2 (L)", 0, 0) == UNI_ERROR_IGNORE_DEVICE && scanning_enabled && !classic_scanning_enabled && !switch_pico_switch2_pairing_allowed(), "rejected mate candidates must leave the bounded passive policy unchanged"); }; ++left.conn.btaddr[5]; reject_left(); --left.conn.btaddr[5]; left.product_id = UNI_SW2_JOYCON_R_PID; reject_left(); left.product_id = UNI_SW2_PRO_PID; reject_left(); left.product_id = UNI_SW2_JOYCON_L_PID; left.conn.protocol = UNI_BT_CONN_PROTOCOL_BR_EDR; reject_left(); left.conn.protocol = UNI_BT_CONN_PROTOCOL_BLE; ++left.vendor_id; reject_left(); --left.vendor_id; left.switch2_identity_valid = false; reject_left(); left.switch2_identity_valid = true; left.switch2_identity_address_type = BD_ADDR_TYPE_LE_PUBLIC; reject_left(); left.switch2_identity_address_type = BD_ADDR_TYPE_LE_RANDOM; require(platform_on_device_discovered(left.conn.btaddr, nullptr, 0, 0) == UNI_ERROR_SUCCESS, "remembered opposite static-random half must be admitted outside the pairing window"); // The parser stops GAP scan before it has a backend slot to reserve. uni_bt_le_scan_stop(); platform_on_device_connected(&left); require(!scanning_enabled && !classic_scanning_enabled, "remembered mate setup must pause scan even when GAP already stopped it"); platform_on_device_disconnected(&left); require(scanning_enabled && background_scan_parameters && !classic_scanning_enabled, "failed setup must resume remembered mate scanning"); uni_bt_le_scan_stop(); platform_on_device_connected(&left); require(!scanning_enabled && !classic_scanning_enabled && incoming_connections, "mate setup must pause background scanning while reserving physical capacity"); require(platform_on_device_ready(&left) == UNI_ERROR_SUCCESS && slot_snapshot(0).active && !slot_snapshot(1).active && !scanning_enabled && !classic_scanning_enabled, "ready remembered mate must merge without a pairing window and stop background scanning"); require_pair_owner(slot_snapshot(0).identity, left, right); auto ordinary = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR); platform_on_device_connected(&ordinary); require(platform_on_device_ready(&ordinary) == UNI_ERROR_SUCCESS && !scanning_enabled && !classic_scanning_enabled, "a complete pair plus incoming Classic controller must not keep LE scanning"); platform_on_device_disconnected(&left); require(scanning_enabled && background_scan_parameters && !classic_scanning_enabled && slot_snapshot(2).active, "losing a half must resume its mate scan without disturbing an ordinary controller"); } void test_switch2_mate_pending() { start_backend(); auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID); auto right = switch2_device(3, UNI_SW2_JOYCON_R_PID); remember_switch2(left); remember_switch2(right); register_lookup_device(&right); ready_switch2(left); auto first = device(1, true, UNI_BT_CONN_PROTOCOL_BR_EDR); auto second = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR); auto third = device(3, true, UNI_BT_CONN_PROTOCOL_BR_EDR); platform_on_device_connected(&first); platform_on_device_connected(&second); require(!scanning_enabled && !classic_scanning_enabled && incoming_connections && platform_on_device_discovered(right.conn.btaddr, nullptr, 0, 0) == UNI_ERROR_IGNORE_DEVICE, "any pending setup must pause scanning and reject otherwise valid mates"); require(platform_on_device_ready(&first) == UNI_ERROR_SUCCESS && !scanning_enabled, "one resolved setup must not resume mate scanning while another is pending"); platform_on_device_disconnected(&second); require(scanning_enabled && background_scan_parameters && !classic_scanning_enabled && platform_on_device_discovered(right.conn.btaddr, nullptr, 0, 0) == UNI_ERROR_SUCCESS, "last pending setup failure must resume passive mate discovery and admission"); platform_on_device_connected(&second); require(!scanning_enabled && platform_on_device_ready(&second) == UNI_ERROR_SUCCESS && scanning_enabled && !classic_scanning_enabled, "last pending setup becoming ready must resume the unmatched half scan"); platform_on_device_connected(&third); require(platform_on_device_ready(&third) == UNI_ERROR_SUCCESS && !scanning_enabled && !classic_scanning_enabled && !incoming_connections && platform_on_device_discovered(right.conn.btaddr, nullptr, 0, 0) == UNI_ERROR_IGNORE_DEVICE, "four physical controllers must stop mate scanning even with an unmatched half"); platform_on_device_disconnected(&third); require(scanning_enabled && background_scan_parameters && incoming_connections, "free physical capacity must restore the waiting half scan"); switch2_clear_succeeds = false; bluepad32_input_backend_clear_pairings(); process_rumble_timer(&g_rumble_timer); require(!scanning_enabled && !classic_scanning_enabled && !incoming_connections, "failed trust deletion must explicitly stop a direct background LE scan"); platform_on_device_disconnected(&right); platform_on_device_disconnected(&left); bluepad32_input_backend_open_pairing_window(); process_rumble_timer(&g_rumble_timer); require(!scanning_enabled && !classic_scanning_enabled && !incoming_connections && !switch_pico_switch2_pairing_allowed() && platform_on_device_discovered(right.conn.btaddr, nullptr, 0, 0) == UNI_ERROR_IGNORE_DEVICE, "late disconnects and pairing requests must not clear the failed-closed latch"); } void test_switch2_mate_pairing_window() { start_backend(); auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID); ready_switch2(left); require(scanning_enabled && background_scan_parameters && !classic_scanning_enabled, "solo half must enter background discovery before opening pairing"); bluepad32_input_backend_open_pairing_window(); process_rumble_timer(&g_rumble_timer); require(scanning_enabled && !background_scan_parameters && classic_scanning_enabled && bondable && accepted_stk_methods == kAllBlePairingMethods, "explicit pairing must restore full discovery timing and the existing authentication window"); now_ms = g_pairing_window_deadline_ms; process_rumble_timer(&g_rumble_timer); require(scanning_enabled && background_scan_parameters && !classic_scanning_enabled && !bondable && accepted_stk_methods == 0 && !switch_pico_switch2_pairing_allowed(), "pairing expiry must restore low-duty mate discovery without extending authentication"); platform_on_device_disconnected(&left); require(scanning_enabled && !background_scan_parameters && classic_scanning_enabled && !bondable && accepted_stk_methods == 0, "last controller loss must restore idle discovery defaults without opening pairing"); } void test_switch2_pairing_inventory() { start_pairing_backend(); switch2_pairing_count = 2; switch2_pairing_types[0] = BD_ADDR_TYPE_LE_PUBLIC; switch2_pairing_types[1] = BD_ADDR_TYPE_LE_RANDOM; switch2_pairings[0][5] = 1; switch2_pairings[1][0] = 0xc1; switch2_pairings[1][5] = 2; bluepad32_input_backend_request_pairing_snapshot(); process_rumble_timer(&g_rumble_timer); Bluepad32PairingSnapshot snapshot{}; bluepad32_input_backend_pairing_snapshot(&snapshot); require(snapshot.record_count == 2 && !snapshot.overflow && snapshot.records[0].transport == Bluepad32PairingTransport::kBle && snapshot.records[0].address_type == BD_ADDR_TYPE_LE_PUBLIC && snapshot.records[0].address[5] == 1 && snapshot.records[1].address_type == BD_ADDR_TYPE_LE_RANDOM && snapshot.records[1].address[0] == 0xc1, "pairing inventory must include proprietary trust records with real BLE address types"); switch2_pairing_count = UNI_SWITCH2_PAIRING_CAPACITY; classic_bond_count = 1; bluepad32_input_backend_request_pairing_snapshot(); process_rumble_timer(&g_rumble_timer); bluepad32_input_backend_pairing_snapshot(&snapshot); require(snapshot.record_count == BLUEPAD32_PAIRING_RECORD_CAPACITY && snapshot.overflow, "combined ordinary and proprietary inventory must preserve bounded overflow reporting"); const uint32_t successful_token = bluepad32_input_backend_clear_pairings(); process_rumble_timer(&g_rumble_timer); bluepad32_input_backend_pairing_snapshot(&snapshot); require(snapshot.record_count == 0 && !snapshot.overflow && switch2_pairing_count == 0 && bluepad32_input_backend_clear_pairings_completed(snapshot, successful_token), "clear completion must follow deletion of proprietary and ordinary pairing records"); switch2_pairing_count = 1; switch2_clear_succeeds = false; const uint32_t failed_token = bluepad32_input_backend_clear_pairings(); process_rumble_timer(&g_rumble_timer); bluepad32_input_backend_pairing_snapshot(&snapshot); require(snapshot.status == Bluepad32PairingSnapshotStatus::kFailed && snapshot.completed_clear_pairings_token == successful_token && !bluepad32_input_backend_clear_pairings_completed(snapshot, failed_token) && !incoming_connections && !scanning_enabled && !switch_pico_switch2_pairing_allowed(), "failed trust deletion must never acknowledge clear or admit reconnects"); bluepad32_input_backend_open_pairing_window(); bluepad32_input_backend_request_pairing_snapshot(); process_rumble_timer(&g_rumble_timer); bluepad32_input_backend_pairing_snapshot(&snapshot); require(snapshot.status == Bluepad32PairingSnapshotStatus::kFailed && snapshot.completed_clear_pairings_token == successful_token && !switch_pico_switch2_pairing_allowed() && g_connection_policy_state == ConnectionPolicyState::FailedClosed, "inventory refresh and pairing requests must not erase failed-clear state"); switch2_clear_succeeds = true; const uint32_t retry_token = bluepad32_input_backend_clear_pairings(); require(retry_token != failed_token, "explicit retry must receive a fresh clear token"); process_rumble_timer(&g_rumble_timer); bluepad32_input_backend_pairing_snapshot(&snapshot); require(snapshot.status == Bluepad32PairingSnapshotStatus::kReady && snapshot.record_count == 0 && bluepad32_input_backend_clear_pairings_completed(snapshot, retry_token) && incoming_connections && scanning_enabled && !switch_pico_switch2_pairing_allowed(), "successful explicit retry must restore normal reconnect policy, not fresh pairing"); } void test_ready_order(bool reverse) { start_pairing_backend(); uni_hid_device_t devices[kSlotCount] = { 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; platform_on_device_connected(&devices[order[0]]); for (int position = 0; position < kSlotCount; ++position) { const int slot = order[position]; require(platform_on_device_ready(&devices[slot]) == UNI_ERROR_SUCCESS, "ready device must bind to its Bluepad index"); for (int candidate = 0; candidate < kSlotCount; ++candidate) { ControllerState snapshot{}; bool expected_active = false; for (int ready = 0; ready <= position; ++ready) { expected_active = expected_active || order[ready] == candidate; } require(read_controller_state(candidate, &snapshot) == expected_active, "only ready indexed slots may become active"); } if (position + 1 < kSlotCount) { require(scanning_enabled && classic_scanning_enabled, "pairing-window discovery must continue while a physical slot remains free"); require(incoming_connections, "incoming connections must remain enabled before all slots are ready"); } } require(!scanning_enabled && !classic_scanning_enabled, "discovery must stop when all four physical slots are full"); require(!incoming_connections, "incoming connections must be disabled only when all slots are full"); for (int slot = 0; slot < kSlotCount; ++slot) { platform_on_device_disconnected(&devices[slot]); require(scanning_enabled && classic_scanning_enabled && incoming_connections, "disconnecting any slot must resume connection policy"); for (int candidate = 0; candidate < kSlotCount; ++candidate) { ControllerState snapshot{}; require(read_controller_state(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{}; require(platform_on_device_discovered(address, "extra", 0, 0) == UNI_ERROR_IGNORE_DEVICE, "discovery must reject devices while all four slots are occupied"); } void test_rejections() { start_backend(); uni_hid_device_t non_gamepad = device(0, false); uni_hid_device_t out_of_range = device(4); uni_hid_device_t slot_zero = device(0); uni_hid_device_t collision = device(0); require(platform_on_device_ready(&non_gamepad) == UNI_ERROR_INVALID_CONTROLLER, "non-gamepad must be rejected"); require(platform_on_device_ready(&out_of_range) == UNI_ERROR_NO_SLOTS, "Bluepad index 4 must be rejected"); require(platform_on_device_ready(&slot_zero) == UNI_ERROR_SUCCESS, "valid device must occupy its indexed slot"); require(platform_on_device_ready(&collision) == UNI_ERROR_NO_SLOTS, "different device cannot replace an occupied slot"); g_slots[0].identity.stable = true; g_slots[0].identity.transport = ControllerTransport::kClassic; g_slots[0].identity.address[5] = 1; Bluepad32SlotSnapshot identity_snapshot{}; bluepad32_input_backend_snapshot(0, &identity_snapshot); require(bluepad32_input_backend_identify( identity_snapshot.identity) && g_slots[0].pending_profile_feedback_count == 1 && !bluepad32_input_backend_identify( controller_identity_global()), "Identify did not target only the selected live controller"); g_slots[0].pending_profile_feedback_count = 0; g_slots[0].pending_profile_feedback[0] = {}; uni_controller_t collision_data{}; collision_data.klass = UNI_CONTROLLER_CLASS_GAMEPAD; collision_data.gamepad.buttons = BUTTON_B; platform_on_controller_data(&collision, &collision_data); ControllerState snapshot{}; require(read_controller_state(0, &snapshot), "occupied slot must stay active"); require(!snapshot.button_east, "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(read_controller_state(0, &snapshot) && snapshot.motion_sample_count == 3, "valid slot input must remain observable"); slot_zero.controller.battery = 201; g_last_snapshot_generation[0] = 0; Bluepad32PlaytestSnapshot playtest{}; bluepad32_input_backend_playtest_snapshot(0, &playtest); require(playtest.active && playtest.state_generation != 0 && playtest.state.motion_sample_count == 3 && playtest.battery == 201 && (playtest.capabilities & 0x08u) != 0, "playtest snapshot did not expose input capabilities"); struct LayoutCase { uni_controller_subtype_t subtype; Bluepad32ControllerLayout layout; }; const LayoutCase layouts[] = { {CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL, Bluepad32ControllerLayout::kWiiHorizontal}, {CONTROLLER_SUBTYPE_WIIMOTE_VERTICAL, Bluepad32ControllerLayout::kWiiVertical}, {CONTROLLER_SUBTYPE_WIIMOTE_ACCEL, Bluepad32ControllerLayout::kWiiHorizontal}, {CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK, Bluepad32ControllerLayout::kWiiNunchuk}, {CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL, Bluepad32ControllerLayout::kWiiNunchuk}, {CONTROLLER_SUBTYPE_WII_CLASSIC, Bluepad32ControllerLayout::kUnspecified}, {CONTROLLER_SUBTYPE_WIIUPRO, Bluepad32ControllerLayout::kUnspecified}, {CONTROLLER_SUBTYPE_WII_BALANCE_BOARD, Bluepad32ControllerLayout::kUnspecified}, {CONTROLLER_SUBTYPE_WIIMOTE_UDRAW_TABLET, Bluepad32ControllerLayout::kUnspecified}, {CONTROLLER_SUBTYPE_NONE, Bluepad32ControllerLayout::kUnspecified}, }; for (const LayoutCase& expected : layouts) { slot_zero.controller_subtype = expected.subtype; bluepad32_input_backend_playtest_snapshot(0, &playtest); require(playtest.controller_layout == expected.layout && playtest.state.motion_sample_count == 3, "Wii layout must follow the live subtype, not motion or previous extension"); } bluepad32_input_backend_report_sent(0); require(read_controller_state(0, &snapshot) && snapshot.motion_sample_count == 3, "playtest snapshot consumed report-path motion"); bluepad32_input_backend_playtest_snapshot(4, &playtest); require(!playtest.active && playtest.state_generation == 0, "playtest snapshot accepted slot 4"); require(!read_controller_state(4, &snapshot), "public snapshot must reject slot 4"); bluepad32_input_backend_report_sent(4); require(read_controller_state(0, &snapshot) && snapshot.motion_sample_count == 3, "slot 4 acknowledgement must not consume slot 0 IMU"); 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"); } void test_independent_lifecycle() { test_identity_encoding_contract(); start_pairing_backend(); uni_hid_device_t invalid_transport = device(6, true, UNI_BT_CONN_PROTOCOL_BLE); invalid_transport.conn.handle = 0xffff; require(controller_identity_is_global( identity_for_device(&invalid_transport)), "invalid GAP handles must remain on the global identity"); uni_hid_device_t sco_transport = device(7, true, UNI_BT_CONN_PROTOCOL_BR_EDR); gap_connection_types[sco_transport.conn.handle] = GAP_CONNECTION_SCO; require(controller_identity_is_global( identity_for_device(&sco_transport)), "SCO links must remain on the global identity"); uni_hid_device_t aborted = device(0); const uint32_t aborted_generation = g_slots[0].connection_generation; platform_on_device_connected(&aborted); require(g_slots[0].device == &aborted && !g_slots[0].active, "connected device must remain identifiable while becoming ready"); platform_on_device_disconnected(&aborted); require(g_slots[0].device == nullptr && !g_slots[0].active, "pre-ready disconnect must clear its pending slot identity"); require(g_slots[0].connection_generation == aborted_generation + 1, "pre-ready disconnect must invalidate its connection generation"); require(g_connection_status == ConnectionStatus::Scanning && scanning_enabled && classic_scanning_enabled && incoming_connections, "pre-ready disconnect must restart Classic and BLE scans"); uni_hid_device_t devices[kSlotCount] = { device(0, true, UNI_BT_CONN_PROTOCOL_NONE), device(1, true, UNI_BT_CONN_PROTOCOL_BR_EDR), device(2, true, UNI_BT_CONN_PROTOCOL_NONE), device(3, true, UNI_BT_CONN_PROTOCOL_BR_EDR)}; gap_connection_types[devices[0].conn.handle] = GAP_CONNECTION_ACL; gap_connection_types[devices[1].conn.handle] = GAP_CONNECTION_LE; gap_connection_types[devices[2].conn.handle] = GAP_CONNECTION_LE; gap_connection_types[devices[3].conn.handle] = GAP_CONNECTION_LE; const bd_addr_t classic_address = {0x10, 0x11, 0x12, 0x13, 0x14, 0x15}; const bd_addr_t resolved_connection_address = {0x41, 0x21, 0x22, 0x23, 0x24, 0x25}; const bd_addr_t created_connection_address = {0x42, 0x31, 0x32, 0x33, 0x34, 0x35}; const bd_addr_t reencrypted_connection_address = {0x43, 0x41, 0x42, 0x43, 0x44, 0x45}; memcpy(devices[0].conn.btaddr, classic_address, sizeof(classic_address)); memcpy(devices[1].conn.btaddr, resolved_connection_address, sizeof(resolved_connection_address)); memcpy(devices[2].conn.btaddr, created_connection_address, sizeof(created_connection_address)); memcpy(devices[3].conn.btaddr, reencrypted_connection_address, sizeof(reencrypted_connection_address)); require( devices[0].conn.protocol == UNI_BT_CONN_PROTOCOL_NONE && gap_get_connection_type(devices[0].conn.handle) == GAP_CONNECTION_ACL && devices[1].conn.protocol == UNI_BT_CONN_PROTOCOL_BR_EDR && gap_get_connection_type(devices[1].conn.handle) == GAP_CONNECTION_LE && devices[2].conn.protocol == UNI_BT_CONN_PROTOCOL_NONE && gap_get_connection_type(devices[2].conn.handle) == GAP_CONNECTION_LE, "identity fixtures must expose authoritative GAP transports over " "missing or stale cached protocols"); const bd_addr_t resolved_address = {0x20, 0x21, 0x22, 0x23, 0x24, 0x25}; const bd_addr_t reencrypted_address = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35}; dispatch_identity_event( SM_EVENT_IDENTITY_RESOLVING_SUCCEEDED, devices[1], BD_ADDR_TYPE_LE_PUBLIC, resolved_address); register_lookup_device(&devices[3]); dispatch_identity_event( SM_EVENT_REENCRYPTION_STARTED, devices[3], BD_ADDR_TYPE_LE_RANDOM, reencrypted_address); dispatch_identity_event( SM_EVENT_REENCRYPTION_COMPLETE, devices[3], BD_ADDR_TYPE_LE_RANDOM, reencrypted_address); for (int slot = 0; slot < kSlotCount; ++slot) { platform_on_device_connected(&devices[slot]); require(g_slots[slot].device == &devices[slot] && !g_slots[slot].active, "each pending device must retain its indexed identity"); } const uint32_t first_pending_generation = g_slots[0].connection_generation; platform_on_device_disconnected(&devices[0]); require(g_slots[0].device == nullptr && !g_slots[0].active, "pre-ready disconnect must clear only its own 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 == first_pending_generation + 1, "pending disconnect beside peers must invalidate its generation"); require(g_connection_status == ConnectionStatus::Connecting && scanning_enabled && classic_scanning_enabled && incoming_connections, "open pairing slot must preserve pending peers and resume scanning"); for (int slot = 1; slot < kSlotCount; ++slot) { require(platform_on_device_ready(&devices[slot]) == UNI_ERROR_SUCCESS, "each surviving pending device must still become ready"); } 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"); Bluepad32SlotSnapshot lifecycle_snapshots[kSlotCount]{}; uint32_t baseline_connection_generations[kSlotCount]{}; for (int slot = 0; slot < kSlotCount; ++slot) { bluepad32_input_backend_snapshot( static_cast(slot), &lifecycle_snapshots[slot]); require(lifecycle_snapshots[slot].active, "ready slot snapshot must publish active state"); baseline_connection_generations[slot] = lifecycle_snapshots[slot].connection_generation; } require_identity( lifecycle_snapshots[0].identity, true, ControllerTransport::kClassic, BD_ADDR_TYPE_UNKNOWN, devices[0].conn.btaddr, devices[0].vendor_id, devices[0].product_id, "Classic snapshot identity must use the connected device address"); require_identity( lifecycle_snapshots[1].identity, true, ControllerTransport::kBle, BD_ADDR_TYPE_LE_PUBLIC, resolved_address, devices[1].vendor_id, devices[1].product_id, "resolved BLE snapshot must use the stable identity address"); require(controller_identity_is_global( lifecycle_snapshots[2].identity), "unresolved BLE snapshot must use the global unstable identity"); require_identity( lifecycle_snapshots[3].identity, true, ControllerTransport::kBle, BD_ADDR_TYPE_LE_RANDOM, reencrypted_address, devices[3].vendor_id, devices[3].product_id, "reencrypted BLE snapshot must use the bonded identity address"); require(observed_profile_identity_count == 3 && controller_identity_equal( observed_profile_identities[0], lifecycle_snapshots[1].identity) && controller_identity_equal( observed_profile_identities[1], lifecycle_snapshots[3].identity) && controller_identity_equal( observed_profile_identities[2], lifecycle_snapshots[0].identity), "only stable ready identities must be enrolled for profiles"); size_t classic_identity_observations = 0; for (size_t index = 0; index < observed_profile_identity_count; ++index) { if (controller_identity_equal( observed_profile_identities[index], lifecycle_snapshots[0].identity)) { ++classic_identity_observations; } } require(classic_identity_observations == 1, "active GAP ACL with no cached protocol must enroll its stable " "Classic identity exactly once"); require(baseline_connection_generations[0] == first_pending_generation + 1 && baseline_connection_generations[1] == baseline_connection_generations[2] && baseline_connection_generations[2] == baseline_connection_generations[3], "connection generations must isolate each slot lifecycle"); const bd_addr_t created_address = {0x40, 0x41, 0x42, 0x43, 0x44, 0x45}; register_lookup_device(&devices[2]); dispatch_identity_event( SM_EVENT_IDENTITY_CREATED, devices[2], BD_ADDR_TYPE_LE_RANDOM, created_address); bluepad32_input_backend_snapshot(2, &lifecycle_snapshots[2]); require_identity( lifecycle_snapshots[2].identity, true, ControllerTransport::kBle, BD_ADDR_TYPE_LE_RANDOM, created_address, devices[2].vendor_id, devices[2].product_id, "new BLE identity event must update an active slot snapshot"); require(observed_profile_identity_count == 4 && controller_identity_equal( observed_profile_identities[3], lifecycle_snapshots[2].identity), "late BLE identity creation must enroll the stable identity"); const uint32_t buttons[kSlotCount] = { BUTTON_B, BUTTON_A, BUTTON_X, BUTTON_Y}; uni_controller_t data[kSlotCount]{}; for (int slot = 0; slot < kSlotCount; ++slot) { data[slot].klass = UNI_CONTROLLER_CLASS_GAMEPAD; data[slot].gamepad.buttons = buttons[slot]; data[slot].gamepad.accel[slot % 3] = 8192 + slot; data[slot].gamepad.gyro[(slot + 1) % 3] = 1024 + slot; platform_on_controller_data(&devices[slot], &data[slot]); } ControllerState states[kSlotCount]{}; for (int slot = 0; slot < kSlotCount; ++slot) { require(read_controller_state(slot, &states[slot]) && states[slot].motion_sample_count == 3, "every slot must expose independent input and IMU"); } 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_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_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_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(read_controller_state(slot, &states[slot]) && 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(read_controller_state(slot, &states[slot]) && states[slot].motion_sample_count == 0, "each slot acknowledgement must consume only its own IMU"); } 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]); } process_rumble_timer(&g_rumble_timer); for (int slot = 0; slot < kSlotCount; ++slot) { require(devices[slot].rumble_calls == 1 && devices[slot].last_low == 11 + slot && devices[slot].last_high == 21 + slot && devices[slot].last_rumble_duration_ms == host_rumble_duration_ms(), "each slot rumble must reach only its indexed controller"); } 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, ControllerRumbleOutput{55, 66}); const uint32_t disconnected_generation = baseline_connection_generations[3]; platform_on_device_disconnected(&devices[3]); require(scanning_enabled && classic_scanning_enabled && incoming_connections, "slot 3 disconnect must resume scanning and incoming connections"); require(!read_controller_state(3, &states[3]) && !states[3].button_north && states[3].left_stick_x == 0, "slot 3 disconnect must publish protocol-neutral state"); bluepad32_input_backend_snapshot(3, &lifecycle_snapshots[3]); require(!lifecycle_snapshots[3].active && lifecycle_snapshots[3].connection_generation == disconnected_generation + 1 && controller_identity_is_global( lifecycle_snapshots[3].identity) && !lifecycle_snapshots[3].state.button_north && lifecycle_snapshots[3].state.left_stick_x == 0, "disconnect snapshot must atomically publish neutral state, " "cleared identity, and a new connection generation"); for (int survivor = 0; survivor < 3; ++survivor) { bluepad32_input_backend_snapshot( static_cast(survivor), &lifecycle_snapshots[survivor]); require(lifecycle_snapshots[survivor].active && lifecycle_snapshots[survivor].connection_generation == baseline_connection_generations[survivor], "disconnect generation must not leak into surviving slots"); } require(read_controller_state(0, &states[0]) && states[0].button_east && read_controller_state(1, &states[1]) && states[1].button_south && read_controller_state(2, &states[2]) && states[2].button_west, "slot 3 disconnect must preserve slots 0-2"); platform_on_controller_data(&devices[0], &data[0]); require(read_controller_state(0, &states[0]) && 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, ControllerRumbleOutput{115, 116}); tick_backend_timer(99); require(devices[0].rumble_calls == slot_zero_calls_while_scanning + 1 && devices[0].last_low == 115 && devices[0].last_high == 116, "slot 0 rumble must continue while slot 3 is disconnected"); uni_hid_device_t slot_three_replacement = device(3, true, UNI_BT_CONN_PROTOCOL_BLE); memcpy(slot_three_replacement.conn.btaddr, devices[3].conn.btaddr, sizeof(devices[3].conn.btaddr)); require( slot_three_replacement.conn.handle == devices[3].conn.handle && gap_get_connection_type(slot_three_replacement.conn.handle) == GAP_CONNECTION_LE && memcmp(slot_three_replacement.conn.btaddr, devices[3].conn.btaddr, sizeof(devices[3].conn.btaddr)) == 0, "replacement isolation fixture must reuse the active BLE handle " "and address"); require(platform_on_device_ready(&slot_three_replacement) == UNI_ERROR_SUCCESS, "slot 3 replacement must bind to the freed indexed slot"); process_rumble_timer(&g_rumble_timer); require(slot_three_replacement.rumble_calls == 0, "slot 3 replacement must not receive disconnected device rumble"); bluepad32_input_backend_snapshot(3, &lifecycle_snapshots[3]); require(lifecycle_snapshots[3].active && lifecycle_snapshots[3].connection_generation == disconnected_generation + 1 && controller_identity_is_global( lifecycle_snapshots[3].identity), "replacement must keep the new generation and cannot inherit " "the disconnected BLE identity"); require(observed_profile_identity_count == 4, "unstable replacement must not be enrolled for profiles"); g_slots[3].pending_rumble = { 3, disconnected_generation, ControllerRumbleOutput{77, 88}, kXInputHostRumbleDurationMs}; g_slots[3].rumble_pending = true; process_rumble_timer(&g_rumble_timer); require(slot_three_replacement.rumble_calls == 0, "stale slot 3 connection generation must be rejected"); uni_controller_t replacement_data{}; replacement_data.klass = UNI_CONTROLLER_CLASS_GAMEPAD; replacement_data.gamepad.buttons = BUTTON_Y; replacement_data.gamepad.accel[0] = 9000; platform_on_controller_data(&slot_three_replacement, &replacement_data); require(read_controller_state(3, &states[3]) && states[3].button_north && states[3].motion_sample_count == 3, "replacement input and IMU must populate only slot 3"); require(read_controller_state(0, &states[0]) && states[0].button_east && read_controller_state(1, &states[1]) && states[1].button_south && read_controller_state(2, &states[2]) && 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, ControllerRumbleOutput{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, ControllerRumbleOutput{static_cast(100 + slot), static_cast(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) { platform_on_device_disconnected(&devices[slot]); require(scanning_enabled && classic_scanning_enabled && incoming_connections, "disconnecting slots 0-2 must resume connection policy"); require(!read_controller_state(slot, &states[slot]) && states[slot].left_stick_x == 0, "disconnect must publish protocol-neutral state"); for (int survivor = 0; survivor < kSlotCount; ++survivor) { if (survivor == slot) { continue; } require(read_controller_state(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, 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 && slot_three_replacement.last_low == 121 && slot_three_replacement.last_high == 122, "slot 3 mailbox must dispatch only its latest queued value"); require(replacements[0].rumble_calls == 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"); } void test_pairing_window_policy() { bd_addr_t address = {1, 2, 3, 4, 5, 6}; bluepad32_input_backend_init(); require(platform_on_device_discovered(address, "controller", 0, 0) == UNI_ERROR_IGNORE_DEVICE, "discovery must remain closed before backend initialization"); start_backend(); require(g_connection_policy_state == ConnectionPolicyState::Open && classic_scanning_enabled && scanning_enabled && incoming_connections, "boot must allow normal Bluepad32 autoconnect"); require(platform_on_device_discovered(address, "controller", 0, 0) == UNI_ERROR_SUCCESS, "boot policy must accept a discovered controller"); dispatch_pairing_event(HCI_EVENT_USER_CONFIRMATION_REQUEST); dispatch_pairing_event(HCI_EVENT_USER_PASSKEY_REQUEST); require(confirmation_rejections == 1 && passkey_rejections == 1 && confirmation_accepts == 0 && passkey_accepts == 0, "closed BOOTSEL window must reject new SSP authentication"); uni_hid_device_t reconnecting = device(0); platform_on_device_connected(&reconnecting); require(device_disconnect_calls == 0 && g_slots[0].device == &reconnecting, "boot policy must retain a reconnecting controller"); platform_on_device_disconnected(&reconnecting); bluepad32_input_backend_open_pairing_window(); require(!g_pairing_window_open, "Core0 request must wait for Core1 consumption"); process_rumble_timer(&g_rumble_timer); require(g_pairing_window_open && bondable && accepted_stk_methods == kAllBlePairingMethods && g_pairing_window_deadline_ms == 60000 && g_connection_policy_state == ConnectionPolicyState::Open && classic_scanning_enabled && scanning_enabled && incoming_connections, "BOOTSEL must enable Classic and BLE pairing without interrupting autoconnect"); dispatch_pairing_event(HCI_EVENT_USER_CONFIRMATION_REQUEST); dispatch_pairing_event(HCI_EVENT_USER_PASSKEY_REQUEST); require(confirmation_accepts == 1 && passkey_accepts == 1, "open BOOTSEL window must accept new SSP authentication"); tick_backend_timer(20); require(!observed_status_led_on, "pairing double blink must finish its first pulse"); tick_backend_timer(20); require(observed_status_led_on, "pairing double blink must start its second pulse"); tick_backend_timer(20); require(!observed_status_led_on, "pairing double blink must finish its second pulse"); now_ms = 30000; bluepad32_input_backend_open_pairing_window(); process_rumble_timer(&g_rumble_timer); require(g_pairing_window_deadline_ms == 90000, "pairing request must extend deadline from current Core1 time"); uni_hid_device_t devices[kSlotCount] = { device(0), device(1), device(2), device(3)}; for (int slot = 0; slot < kSlotCount; ++slot) { require(platform_on_device_ready(&devices[slot]) == UNI_ERROR_SUCCESS, "policy test devices must fill all slots"); } require(g_connection_policy_state == ConnectionPolicyState::Paused && g_pairing_window_open && !scanning_enabled && !classic_scanning_enabled && !incoming_connections, "full slots must pause scanning without closing the deadline"); now_ms = 90000; process_rumble_timer(&g_rumble_timer); require(!g_pairing_window_open && !bondable && accepted_stk_methods == 0 && g_connection_policy_state == ConnectionPolicyState::Paused, "Classic and BLE pairing authentication must close at the deadline"); platform_on_device_disconnected(&devices[3]); require(g_connection_policy_state == ConnectionPolicyState::Passive && !classic_scanning_enabled && !scanning_enabled && incoming_connections, "a freed slot with active controllers must remain passive"); require(platform_on_device_discovered(address, "controller", 0, 0) == UNI_ERROR_IGNORE_DEVICE, "passive policy must reject inquiry discoveries"); bluepad32_input_backend_open_pairing_window(); process_rumble_timer(&g_rumble_timer); require(g_connection_policy_state == ConnectionPolicyState::Open && classic_scanning_enabled && scanning_enabled && incoming_connections, "explicit BOOTSEL window must resume active discovery"); require(platform_on_device_discovered(address, "controller", 0, 0) == UNI_ERROR_SUCCESS, "pairing window discovery must accept a controller"); } void test_slot_lighting() { start_pairing_backend(); uni_hid_device_t devices[kSlotCount] = { device(0), device(1), device(2), device(3)}; for (uint8_t slot = 0; slot < kSlotCount; ++slot) { devices[slot].report_parser.set_lightbar_color = set_lightbar; devices[slot].report_parser.set_player_leds = set_player_leds; require(platform_on_device_ready(&devices[slot]) == UNI_ERROR_SUCCESS, "color-capable controller did not become ready"); const SwitchRgbColor expected = switch_pro_get_slot_light_color(slot); require(devices[slot].lightbar_calls == 1 && devices[slot].lightbar_red == expected.red && devices[slot].lightbar_green == expected.green && devices[slot].lightbar_blue == expected.blue && devices[slot].player_led_calls == 0, "slot color did not reach the controller lightbar"); require(platform_on_device_ready(&devices[slot]) == UNI_ERROR_SUCCESS && devices[slot].lightbar_calls == 1, "duplicate ready event rewrote controller lighting"); } platform_on_device_disconnected(&devices[2]); uni_hid_device_t fallback = device(2); fallback.report_parser.set_player_leds = set_player_leds; require(platform_on_device_ready(&fallback) == UNI_ERROR_SUCCESS && fallback.lightbar_calls == 0 && fallback.player_led_calls == 1 && fallback.player_leds == (1u << 2u), "controller without RGB support did not receive its slot LED"); } void test_profile_chord_remains_raw() { start_pairing_backend(); uni_hid_device_t controller = device(0); require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS, "raw profile chord controller did not become ready"); uni_controller_t input{}; input.klass = UNI_CONTROLLER_CLASS_GAMEPAD; input.gamepad.buttons = BUTTON_A | BUTTON_SHOULDER_L | BUTTON_SHOULDER_R; input.gamepad.misc_buttons = MISC_BUTTON_SELECT | MISC_BUTTON_START; platform_on_controller_data(&controller, &input); ControllerState snapshot{}; require(read_controller_state(0, &snapshot) && snapshot.button_south && !snapshot.button_east && snapshot.button_left_shoulder && snapshot.button_right_shoulder && snapshot.button_select && snapshot.button_start, "Core 1 suppressed the profile chord or mutated ABXY mapping"); process_rumble_timer(&g_rumble_timer); require(controller.rumble_calls == 0 && !g_slots[0].feedback_pending, "legacy ABXY chord still produced local feedback"); } void test_profile_feedback_scheduler() { start_pairing_backend(); uni_hid_device_t devices[kSlotCount] = { device(0), device(1), device(2), device(3)}; devices[0].report_parser.set_lightbar_color = set_lightbar; devices[1].report_parser.set_player_leds = set_player_leds; devices[2].report_parser.set_lightbar_color = set_lightbar; devices[3].report_parser.set_player_leds = set_player_leds; uint32_t generations[kSlotCount]{}; for (uint8_t slot = 0; slot < kSlotCount; ++slot) { require(platform_on_device_ready(&devices[slot]) == UNI_ERROR_SUCCESS, "profile feedback controller did not become ready"); Bluepad32SlotSnapshot snapshot{}; bluepad32_input_backend_snapshot(slot, &snapshot); generations[slot] = snapshot.connection_generation; require(devices[slot].rumble_calls == 0 && !g_slots[slot].profile_feedback.active && g_slots[slot].pending_profile_feedback_count == 0, "initial controller/profile load scheduled confirmation feedback"); bluepad32_input_backend_queue_profile_feedback( slot, generations[slot], static_cast(slot + 1u), ControllerProfileConfirmationPolicy::kRumble); bluepad32_input_backend_queue_rumble( slot, ControllerRumbleOutput{ static_cast(0x10u + slot), static_cast(0x20u + slot)}); } now_ms = 0; process_rumble_timer(&g_rumble_timer); for (uint8_t slot = 0; slot < kSlotCount; ++slot) { require(devices[slot].rumble_calls == 1 && devices[slot].last_rumble_duration_ms == kProfileFeedbackPhaseDurationMs && devices[slot].last_high == UINT8_MAX && devices[slot].last_low == UINT8_MAX && g_slots[slot].rumble_pending && g_slots[slot].profile_feedback.active, "profile pulse sequence did not start at full strength"); } now_ms = 74; process_rumble_timer(&g_rumble_timer); now_ms = 75; process_rumble_timer(&g_rumble_timer); for (const uni_hid_device_t& controller : devices) { require(controller.rumble_calls == 1, "profile pulse did not retain a 75 ms on phase"); } now_ms = 149; process_rumble_timer(&g_rumble_timer); now_ms = 150; process_rumble_timer(&g_rumble_timer); require(devices[0].rumble_calls == 2 && devices[0].last_high == 0x20 && devices[0].last_low == 0x10 && !g_slots[0].rumble_pending, "one-pulse confirmation did not defer host rumble through its off phase"); for (uint8_t slot = 1; slot < kSlotCount; ++slot) { require(devices[slot].rumble_calls == 2 && devices[slot].last_high == UINT8_MAX && g_slots[slot].rumble_pending, "second profile pulse did not start after 75 ms off"); } now_ms = 225; process_rumble_timer(&g_rumble_timer); now_ms = 300; process_rumble_timer(&g_rumble_timer); require(devices[1].rumble_calls == 3 && devices[1].last_high == 0x21 && !g_slots[1].rumble_pending && devices[2].rumble_calls == 3 && devices[3].rumble_calls == 3, "two/three/four-pulse sequences diverged at 300 ms"); now_ms = 375; process_rumble_timer(&g_rumble_timer); now_ms = 450; process_rumble_timer(&g_rumble_timer); require(devices[2].rumble_calls == 4 && devices[2].last_high == 0x22 && !g_slots[2].rumble_pending && devices[3].rumble_calls == 4 && devices[3].last_high == UINT8_MAX, "three/four-pulse sequences diverged at 450 ms"); now_ms = 525; process_rumble_timer(&g_rumble_timer); now_ms = 600; process_rumble_timer(&g_rumble_timer); require(devices[3].rumble_calls == 5 && devices[3].last_high == 0x23 && !g_slots[3].rumble_pending && devices[0].lightbar_calls == 1 && devices[1].player_led_calls == 1 && devices[2].lightbar_calls == 1 && devices[3].player_led_calls == 1, "four-pulse confirmation did not release host rumble at 600 ms"); const int slot_zero_lightbar_calls = devices[0].lightbar_calls; const int slot_one_player_led_calls_before_slot_zero = devices[1].player_led_calls; const int slot_two_lightbar_calls_before_slot_zero = devices[2].lightbar_calls; const int slot_three_player_led_calls_before_slot_zero = devices[3].player_led_calls; bluepad32_input_backend_queue_profile_feedback( 0, generations[0], 2, ControllerProfileConfirmationPolicy::kNone); bluepad32_input_backend_queue_rumble( 0, ControllerRumbleOutput{0x31, 0x41}); now_ms = 700; process_rumble_timer(&g_rumble_timer); require(devices[0].rumble_calls == 3 && devices[0].last_high == 0x41 && devices[0].lightbar_calls == slot_zero_lightbar_calls && !g_slots[0].profile_feedback.active, "none policy scheduled profile rumble or lighting"); bluepad32_input_backend_queue_profile_feedback( 0, generations[0], 2, ControllerProfileConfirmationPolicy::kLed); bluepad32_input_backend_queue_rumble( 0, ControllerRumbleOutput{0x32, 0x42}); now_ms = 800; process_rumble_timer(&g_rumble_timer); require(devices[0].rumble_calls == 3 && devices[0].lightbar_calls == slot_zero_lightbar_calls + 1 && devices[0].lightbar_red == kProfileLightbarPalette[1].red && devices[0].lightbar_green == kProfileLightbarPalette[1].green && devices[0].lightbar_blue == kProfileLightbarPalette[1].blue && observed_status_led_on && g_slots[0].rumble_pending, "LED policy did not set transient profile color and first " "onboard blink"); now_ms = 875; process_rumble_timer(&g_rumble_timer); require(!observed_status_led_on && devices[0].rumble_calls == 3, "onboard profile blink did not enter its 75 ms off phase"); now_ms = 950; process_rumble_timer(&g_rumble_timer); require(observed_status_led_on, "second onboard profile blink did not start"); now_ms = 1025; process_rumble_timer(&g_rumble_timer); require(!observed_status_led_on && g_slots[0].rumble_pending, "host rumble interrupted the final onboard off phase"); now_ms = 1100; process_rumble_timer(&g_rumble_timer); const SwitchRgbColor slot_zero_color = switch_pro_get_slot_light_color(0); require(devices[0].rumble_calls == 4 && devices[0].last_high == 0x42 && !g_slots[0].rumble_pending && devices[0].lightbar_calls == slot_zero_lightbar_calls + 2 && devices[0].lightbar_red == slot_zero_color.red && devices[0].lightbar_green == slot_zero_color.green && devices[0].lightbar_blue == slot_zero_color.blue && devices[1].player_led_calls == slot_one_player_led_calls_before_slot_zero && devices[2].lightbar_calls == slot_two_lightbar_calls_before_slot_zero && devices[3].player_led_calls == slot_three_player_led_calls_before_slot_zero, "LED-only sequence did not restore its slot color in " "isolation after the final gap"); const int slot_one_player_led_calls = devices[1].player_led_calls; bluepad32_input_backend_queue_profile_feedback( 1, generations[1], 3, ControllerProfileConfirmationPolicy::kRumbleAndLed); now_ms = 1200; process_rumble_timer(&g_rumble_timer); require(devices[1].rumble_calls == 4 && devices[1].last_high == UINT8_MAX && devices[1].player_led_calls == slot_one_player_led_calls + 1 && devices[1].player_leds == 0x07 && observed_status_led_on, "combined policy did not drive rumble, onboard LED, and player LEDs"); for (uint32_t deadline = 1275; deadline <= 1650; deadline += 75) { now_ms = deadline; process_rumble_timer(&g_rumble_timer); } require(devices[1].rumble_calls == (host_rumble_duration_ms() == kXInputHostRumbleDurationMs ? 7 : 6) && !g_slots[1].profile_feedback.active && devices[1].player_led_calls == slot_one_player_led_calls + 2 && devices[1].player_leds == (1u << 1u), "combined three-pulse sequence did not terminate and " "restore slot player lighting"); const int old_rumble_calls = devices[2].rumble_calls; bluepad32_input_backend_queue_profile_feedback( 2, generations[2], 4, ControllerProfileConfirmationPolicy::kRumbleAndLed); bluepad32_input_backend_queue_profile_feedback( 2, generations[2], 1, ControllerProfileConfirmationPolicy::kLed); require(g_slots[2].pending_profile_feedback_count == 2, "two queued profile events did not fill the bounded FIFO"); platform_on_device_disconnected(&devices[2]); uni_hid_device_t replacement = device(2); replacement.report_parser.set_lightbar_color = set_lightbar; require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS, "replacement feedback controller did not become ready"); now_ms = 1700; process_rumble_timer(&g_rumble_timer); require(devices[2].rumble_calls == old_rumble_calls && replacement.rumble_calls == 0 && replacement.lightbar_calls == 1 && !g_slots[2].profile_feedback.active && g_slots[2].pending_profile_feedback_count == 0, "slot replacement accepted stale queued profile feedback"); bluepad32_input_backend_queue_profile_feedback( 2, generations[2], 4, ControllerProfileConfirmationPolicy::kRumbleAndLed); now_ms = 1705; process_rumble_timer(&g_rumble_timer); require(replacement.rumble_calls == 0 && replacement.lightbar_calls == 1, "stale generation feedback reached a replacement controller"); const int slot_three_rumble_calls = devices[3].rumble_calls; const int slot_three_player_led_calls = devices[3].player_led_calls; bluepad32_input_backend_queue_profile_feedback( 3, generations[3], 1, ControllerProfileConfirmationPolicy::kLed); now_ms = 1800; process_rumble_timer(&g_rumble_timer); require(observed_status_led_on && g_slots[3].profile_feedback.pulses_started == 1 && devices[3].player_led_calls == slot_three_player_led_calls + 1 && devices[3].player_leds == 0x01, "one-blink LED profile indication did not start"); now_ms = 1875; process_rumble_timer(&g_rumble_timer); require(!observed_status_led_on, "one-blink LED profile indication did not turn off"); now_ms = 1950; process_rumble_timer(&g_rumble_timer); require(!g_slots[3].profile_feedback.active && devices[3].rumble_calls == slot_three_rumble_calls && devices[3].player_led_calls == slot_three_player_led_calls + 2 && devices[3].player_leds == (1u << 3u), "one-blink LED-only profile indication did not restore " "slot lighting cleanly"); bluepad32_input_backend_queue_profile_feedback( 3, generations[3], 4, ControllerProfileConfirmationPolicy::kLed); now_ms = 2000; process_rumble_timer(&g_rumble_timer); require(devices[3].player_leds == 0x0f, "profile 4 player count was not shown during its sequence"); for (uint8_t pulse = 1; pulse <= 4; ++pulse) { require(observed_status_led_on && g_slots[3].profile_feedback.on && g_slots[3].profile_feedback.pulses_started == pulse, "four-blink LED sequence missed an on phase"); now_ms = static_cast( 2075u + static_cast(pulse - 1u) * 150u); process_rumble_timer(&g_rumble_timer); require(!observed_status_led_on && !g_slots[3].profile_feedback.on, "four-blink LED sequence missed an off phase"); if (pulse != 4) { now_ms += 75; process_rumble_timer(&g_rumble_timer); } } now_ms = 2600; process_rumble_timer(&g_rumble_timer); require(!g_slots[3].profile_feedback.active && devices[3].rumble_calls == slot_three_rumble_calls && devices[3].player_led_calls == slot_three_player_led_calls + 4 && devices[3].player_leds == (1u << 3u), "four-blink LED-only profile indication did not restore " "slot lighting"); Bluepad32SlotSnapshot replacement_snapshot{}; bluepad32_input_backend_snapshot(2, &replacement_snapshot); const int replacement_lightbar_calls = replacement.lightbar_calls; bluepad32_input_backend_queue_profile_feedback( 2, replacement_snapshot.connection_generation, 1, ControllerProfileConfirmationPolicy::kLed); now_ms = 2700; process_rumble_timer(&g_rumble_timer); require(replacement.lightbar_calls == replacement_lightbar_calls + 1, "current-generation profile lighting was not applied"); now_ms = 2775; process_rumble_timer(&g_rumble_timer); platform_on_device_disconnected(&replacement); uni_hid_device_t second_replacement = device(2); second_replacement.report_parser.set_lightbar_color = set_lightbar; require(platform_on_device_ready(&second_replacement) == UNI_ERROR_SUCCESS && second_replacement.lightbar_calls == 1, "second replacement did not receive steady slot lighting"); now_ms = 2850; process_rumble_timer(&g_rumble_timer); require(second_replacement.lightbar_calls == 1 && replacement.lightbar_calls == replacement_lightbar_calls + 1 && !g_slots[2].profile_feedback.active, "stale final-gap restore touched a replacement connection"); const int fifo_lightbar_calls = devices[0].lightbar_calls; const int fifo_rumble_calls = devices[0].rumble_calls; const int isolated_slot_one_lighting = devices[1].player_led_calls; const int isolated_slot_two_lighting = second_replacement.lightbar_calls; const int isolated_slot_three_lighting = devices[3].player_led_calls; bluepad32_input_backend_queue_profile_feedback( 0, generations[0], 1, ControllerProfileConfirmationPolicy::kLed); bluepad32_input_backend_queue_profile_feedback( 0, generations[0], 2, ControllerProfileConfirmationPolicy::kRumbleAndLed); require(g_slots[0].pending_profile_feedback_count == 2, "initial and switched profile events were not queued"); now_ms = 3000; process_rumble_timer(&g_rumble_timer); require(g_slots[0].profile_feedback.active && g_slots[0].profile_feedback.pulse_count == 1 && !g_slots[0].profile_feedback.rumble_enabled && g_slots[0].pending_profile_feedback_count == 1 && devices[0].rumble_calls == fifo_rumble_calls && devices[0].lightbar_calls == fifo_lightbar_calls + 1 && devices[0].lightbar_red == kProfileLightbarPalette[0].red, "LED-only initial event did not run first from the FIFO"); now_ms = 3075; process_rumble_timer(&g_rumble_timer); now_ms = 3150; process_rumble_timer(&g_rumble_timer); require(g_slots[0].profile_feedback.active && g_slots[0].profile_feedback.pulse_count == 2 && g_slots[0].profile_feedback.rumble_enabled && g_slots[0].pending_profile_feedback_count == 0 && devices[0].rumble_calls == fifo_rumble_calls + 1 && devices[0].lightbar_calls == fifo_lightbar_calls + 3 && devices[0].lightbar_red == kProfileLightbarPalette[1].red, "switched profile event did not follow initial indication " "after its final gap"); now_ms = 3225; process_rumble_timer(&g_rumble_timer); now_ms = 3300; process_rumble_timer(&g_rumble_timer); now_ms = 3375; process_rumble_timer(&g_rumble_timer); now_ms = 3450; process_rumble_timer(&g_rumble_timer); const SwitchRgbColor final_slot_zero_color = switch_pro_get_slot_light_color(0); const bool stateful_host_rumble = host_rumble_duration_ms() == kXInputHostRumbleDurationMs; require(!g_slots[0].profile_feedback.active && devices[0].rumble_calls == fifo_rumble_calls + (stateful_host_rumble ? 3 : 2) && (!stateful_host_rumble || (devices[0].last_high == 0x42 && devices[0].last_low == 0x32)) && devices[0].lightbar_calls == fifo_lightbar_calls + 4 && devices[0].lightbar_red == final_slot_zero_color.red && devices[0].lightbar_green == final_slot_zero_color.green && devices[0].lightbar_blue == final_slot_zero_color.blue && devices[1].player_led_calls == isolated_slot_one_lighting && second_replacement.lightbar_calls == isolated_slot_two_lighting && devices[3].player_led_calls == isolated_slot_three_lighting, "ordered profile FIFO did not restore or remain slot-local"); } void test_stateful_host_rumble_restore() { start_pairing_backend(); uni_hid_device_t devices[kSlotCount] = { device(0), device(1), device(2), device(3)}; uint32_t generations[kSlotCount]{}; for (uint8_t slot = 0; slot < kSlotCount; ++slot) { require(platform_on_device_ready(&devices[slot]) == UNI_ERROR_SUCCESS, "rumble restore controller did not become ready"); Bluepad32SlotSnapshot snapshot{}; bluepad32_input_backend_snapshot(slot, &snapshot); generations[slot] = snapshot.connection_generation; } #ifdef SWITCH_PICO_USB_OUTPUT_MODES test_adapter_mode = AdapterUsbMode::kXInput; const ControllerRumbleOutput desired[kSlotCount] = { {0x11, 0x21}, {0x12, 0x22}, {0x13, 0x23}, {0x14, 0x24}}; for (uint8_t slot = 0; slot < kSlotCount; ++slot) { bluepad32_input_backend_queue_rumble(slot, desired[slot]); } now_ms = 0; process_rumble_timer(&g_rumble_timer); for (uint8_t slot = 0; slot < kSlotCount; ++slot) { require(devices[slot].rumble_calls == 1 && devices[slot].last_rumble_duration_ms == kXInputHostRumbleDurationMs && g_slots[slot].retained_host_rumble_valid, "initial XInput rumble was not dispatched and retained"); bluepad32_input_backend_queue_profile_feedback( slot, generations[slot], static_cast(slot + 1u), ControllerProfileConfirmationPolicy::kRumble); } now_ms = 10; process_rumble_timer(&g_rumble_timer); for (now_ms = 85; now_ms <= 610; now_ms += 75) { process_rumble_timer(&g_rumble_timer); } for (uint8_t slot = 0; slot < kSlotCount; ++slot) { require(devices[slot].rumble_calls == static_cast(slot + 3u) && devices[slot].last_low == desired[slot].low_frequency_magnitude && devices[slot].last_high == desired[slot].high_frequency_magnitude && devices[slot].last_rumble_duration_ms == kXInputHostRumbleDurationMs && !g_slots[slot].profile_feedback.active, "XInput rumble did not resume after its profile pulse count"); } const int stop_calls_before = devices[0].rumble_calls; bluepad32_input_backend_queue_rumble( 0, ControllerRumbleOutput{0x51, 0x61}); now_ms = 700; process_rumble_timer(&g_rumble_timer); bluepad32_input_backend_queue_profile_feedback( 0, generations[0], 2, ControllerProfileConfirmationPolicy::kRumble); now_ms = 705; process_rumble_timer(&g_rumble_timer); bluepad32_input_backend_queue_rumble( 0, ControllerRumbleOutput{0, 0}); for (now_ms = 780; now_ms <= 1005; now_ms += 75) { process_rumble_timer(&g_rumble_timer); } require(devices[0].rumble_calls == stop_calls_before + 4 && devices[0].last_rumble_duration_ms == 0 && devices[0].last_low == 0 && devices[0].last_high == 0 && g_slots[0].retained_host_rumble_valid, "newest XInput stop did not win during profile pulses"); test_adapter_mode = AdapterUsbMode::kSwitchProbe; const int switch_calls_before = devices[1].rumble_calls; bluepad32_input_backend_queue_rumble( 1, ControllerRumbleOutput{0x31, 0x41}); now_ms = 1100; process_rumble_timer(&g_rumble_timer); require(!g_slots[1].retained_host_rumble_valid, "Switch rumble retained stale XInput desired state"); bluepad32_input_backend_queue_profile_feedback( 1, generations[1], 1, ControllerProfileConfirmationPolicy::kRumble); now_ms = 1105; process_rumble_timer(&g_rumble_timer); now_ms = 1180; process_rumble_timer(&g_rumble_timer); now_ms = 1255; process_rumble_timer(&g_rumble_timer); require(devices[1].rumble_calls == switch_calls_before + 2 && devices[1].last_rumble_duration_ms == kProfileFeedbackPhaseDurationMs, "finite Switch rumble resumed after profile feedback"); test_adapter_mode = AdapterUsbMode::kXInput; bluepad32_input_backend_queue_rumble( 2, ControllerRumbleOutput{0x71, 0x81}); now_ms = 1300; process_rumble_timer(&g_rumble_timer); bluepad32_input_backend_queue_profile_feedback( 2, generations[2], 3, ControllerProfileConfirmationPolicy::kRumble); now_ms = 1305; process_rumble_timer(&g_rumble_timer); const int old_device_calls = devices[2].rumble_calls; platform_on_device_disconnected(&devices[2]); uni_hid_device_t replacement = device(2); require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS, "rumble restore replacement did not become ready"); now_ms = 2000; process_rumble_timer(&g_rumble_timer); require(devices[2].rumble_calls == old_device_calls && replacement.rumble_calls == 0 && !g_slots[2].retained_host_rumble_valid && !g_slots[2].profile_feedback.active, "stale XInput rumble resumed on a replacement connection"); #else bluepad32_input_backend_queue_rumble( 0, ControllerRumbleOutput{0x31, 0x41}); now_ms = 0; process_rumble_timer(&g_rumble_timer); bluepad32_input_backend_queue_profile_feedback( 0, generations[0], 1, ControllerProfileConfirmationPolicy::kRumble); now_ms = 5; process_rumble_timer(&g_rumble_timer); now_ms = 80; process_rumble_timer(&g_rumble_timer); now_ms = 155; process_rumble_timer(&g_rumble_timer); require(devices[0].rumble_calls == 2 && !g_slots[0].retained_host_rumble_valid, "bounded Switch rumble resumed after profile feedback"); #endif } void test_motion_toggle_action() { start_pairing_backend(); uni_hid_device_t slot_zero = device(0); uni_hid_device_t slot_one = device(1); require(platform_on_device_ready(&slot_zero) == UNI_ERROR_SUCCESS && platform_on_device_ready(&slot_one) == UNI_ERROR_SUCCESS, "motion action test controllers did not become ready"); Bluepad32SlotSnapshot backend_snapshot{}; bluepad32_input_backend_snapshot(0, &backend_snapshot); require(!bluepad32_input_backend_toggle_motion( 0, backend_snapshot.connection_generation + 1u), "stale connection toggled motion"); require(bluepad32_input_backend_toggle_motion( 0, backend_snapshot.connection_generation), "live connection did not toggle motion"); uni_controller_t input{}; input.klass = UNI_CONTROLLER_CLASS_GAMEPAD; input.gamepad.accel[0] = 8192; platform_on_controller_data(&slot_zero, &input); ControllerState snapshot{}; require(read_controller_state(0, &snapshot) && snapshot.motion_sample_count == (kDefaultMotionEnabled ? 0 : 3), "motion toggle did not change slot motion publication"); process_rumble_timer(&g_rumble_timer); require(slot_zero.rumble_calls == 1 && slot_zero.last_rumble_duration_ms == (kDefaultMotionEnabled ? kMotionDisabledFeedbackDurationMs : kMotionEnabledFeedbackDurationMs) && slot_zero.last_high == (kDefaultMotionEnabled ? kMotionDisabledFeedbackWeakMagnitude : kMotionEnabledFeedbackWeakMagnitude) && slot_zero.last_low == (kDefaultMotionEnabled ? kMotionDisabledFeedbackStrongMagnitude : kMotionEnabledFeedbackStrongMagnitude), "motion toggle did not send distinct state feedback"); uni_controller_t peer_input{}; peer_input.klass = UNI_CONTROLLER_CLASS_GAMEPAD; peer_input.gamepad.accel[0] = 8192; platform_on_controller_data(&slot_one, &peer_input); require(read_controller_state(1, &snapshot) && snapshot.motion_sample_count == (kDefaultMotionEnabled ? 3 : 0), "slot 0 motion action changed slot 1 motion state"); require(bluepad32_input_backend_toggle_motion( 0, backend_snapshot.connection_generation), "second live motion toggle failed"); platform_on_controller_data(&slot_zero, &input); require(read_controller_state(0, &snapshot) && snapshot.motion_sample_count == (kDefaultMotionEnabled ? 3 : 0), "second motion toggle did not restore configured state"); platform_on_device_disconnected(&slot_zero); require(!bluepad32_input_backend_toggle_motion( 0, backend_snapshot.connection_generation), "disconnected generation toggled motion"); uni_hid_device_t replacement = device(0); require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS && g_slots[0].motion_enabled == kDefaultMotionEnabled && g_slots[0].pre_hotkey_button_mask == 0 && !g_slots[0].feedback_pending, "disconnect did not reset slot 0 motion action state"); } void test_protocol_neutral_analog_state() { start_pairing_backend(); uni_hid_device_t controller = device(0); uni_hid_device_t peer = device(1); platform_on_device_connected(&controller); platform_on_device_connected(&peer); require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS && platform_on_device_ready(&peer) == UNI_ERROR_SUCCESS, "analog-state controllers did not become ready"); uni_controller_t peer_input{}; peer_input.klass = UNI_CONTROLLER_CLASS_GAMEPAD; peer_input.gamepad.brake = 256; peer_input.gamepad.throttle = 768; peer_input.gamepad.buttons = BUTTON_TRIGGER_L | BUTTON_TRIGGER_R; platform_on_controller_data(&peer, &peer_input); ControllerState peer_state{}; require(read_controller_state(1, &peer_state) && peer_state.left_trigger == 16399 && peer_state.right_trigger == 49199, "peer analog trigger state was not published exactly"); 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.buttons = BUTTON_TRIGGER_L | BUTTON_TRIGGER_R; struct TriggerCase { int32_t brake; int32_t throttle; uint16_t expected_left; uint16_t expected_right; }; constexpr TriggerCase trigger_cases[] = { {4, 512, 256, 32799}, {512, 1020, 32799, UINT16_MAX}, {1020, 1016, UINT16_MAX, 65086}, {1016, 1023, 65086, UINT16_MAX}, {1023, 4, UINT16_MAX, 256}, }; ControllerState state{}; for (const TriggerCase& trigger_case : trigger_cases) { input.gamepad.brake = trigger_case.brake; input.gamepad.throttle = trigger_case.throttle; platform_on_controller_data(&controller, &input); require(read_controller_state(0, &state) && state.left_trigger == trigger_case.expected_left && state.right_trigger == trigger_case.expected_right, "digital trigger bits flattened analog trigger precision"); require(read_controller_state(1, &peer_state) && peer_state.left_trigger == 16399 && peer_state.right_trigger == 49199, "slot 0 trigger update changed slot 1"); } 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"); input.gamepad.brake = 0; input.gamepad.throttle = 0; input.gamepad.buttons = BUTTON_TRIGGER_L; platform_on_controller_data(&controller, &input); require(read_controller_state(0, &state) && state.left_trigger == UINT16_MAX && state.right_trigger == 0, "left digital-only trigger did not map independently"); input.gamepad.buttons = BUTTON_TRIGGER_R; platform_on_controller_data(&controller, &input); require(read_controller_state(0, &state) && state.left_trigger == 0 && state.right_trigger == UINT16_MAX, "right digital-only trigger did not map independently"); input.gamepad.buttons = 0; platform_on_controller_data(&controller, &input); require(read_controller_state(0, &state) && state.left_trigger == 0 && state.right_trigger == 0, "released triggers did not return to zero"); require(read_controller_state(1, &peer_state) && peer_state.left_trigger == 16399 && peer_state.right_trigger == 49199, "slot 0 digital trigger fallback changed slot 1"); input.gamepad.misc_buttons = MISC_BUTTON_CAPTURE; platform_on_controller_data(&controller, &input); require(read_controller_state(0, &state) && state.button_capture, "touchpad/capture input did not reach the logical capture button"); input.gamepad.misc_buttons = 0; platform_on_controller_data(&controller, &input); require(read_controller_state(0, &state) && !state.button_capture, "released touchpad/capture input remained pressed"); } 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_USB_OUTPUT_MODES 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_USB_OUTPUT_MODES 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_xbox_trigger_rumble() { start_pairing_backend(); #ifdef SWITCH_PICO_USB_OUTPUT_MODES test_adapter_mode = AdapterUsbMode::kSwitchProbe; #endif auto controller = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR); controller.vendor_id = 0x045e; controller.product_id = 0x02e0; controller.report_parser.play_dual_rumble = uni_hid_parser_xboxone_play_dual_rumble; require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS, "Xbox did not become ready"); ControllerRumbleOutput hd{80, 100}; hd.hd.actuators[0].sample_count = 3; hd.hd.actuators[1].sample_count = 1; hd.hd.actuators[0].samples[1].high_amplitude_q15 = 16384; bluepad32_input_backend_queue_rumble(0, hd); process_rumble_timer(&g_rumble_timer); require(xbox_left_trigger == 63 && xbox_right_trigger == 0 && controller.last_low == 80 && controller.last_high == 100 && controller.last_rumble_duration_ms == 50, "left high-band substep must drive only left trigger, preserving grips"); hd = {}; hd.hd.actuators[0].sample_count = 1; hd.hd.actuators[1].sample_count = 1; hd.hd.actuators[0].samples[0].low_amplitude_q15 = 32767; hd.hd.actuators[1].samples[0].high_amplitude_q15 = 65535; bluepad32_input_backend_queue_rumble(0, hd); process_rumble_timer(&g_rumble_timer); require(xbox_left_trigger == 0 && xbox_right_trigger == 127 && controller.last_rumble_duration_ms == 50, "right trigger-only effect must not stop or overflow after amplification"); dispatch_rumble(&controller, 75, 100, 100); require(xbox_left_trigger == 0 && xbox_right_trigger == 0, "local feedback must clear trigger vibration"); #ifdef SWITCH_PICO_USB_OUTPUT_MODES test_adapter_mode = AdapterUsbMode::kXInput; #endif bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{200, 180}); process_rumble_timer(&g_rumble_timer); require(xbox_left_trigger == 90 && xbox_right_trigger == 90 && controller.last_rumble_duration_ms == host_rumble_duration_ms(), "conventional high-frequency rumble must feed both triggers"); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{}); process_rumble_timer(&g_rumble_timer); require(xbox_left_trigger == 0 && xbox_right_trigger == 0 && controller.last_high == 0 && controller.last_low == 0 && controller.last_rumble_duration_ms == 0, "explicit stop must stop all four motors"); const unsigned calls = xbox_quad_calls; controller.report_parser.play_dual_rumble = play_rumble; bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{45, 67}); process_rumble_timer(&g_rumble_timer); require(xbox_quad_calls == calls && controller.last_low == 45 && controller.last_high == 67, "Microsoft VID alone must not route an unrelated parser to Xbox output"); controller.report_parser.play_dual_rumble = uni_hid_parser_xboxone_play_dual_rumble; bluepad32_input_backend_queue_rumble(0, hd); platform_on_device_disconnected(&controller); process_rumble_timer(&g_rumble_timer); require(xbox_quad_calls == calls, "pending Xbox output must not reach a disconnected controller"); } void test_clear_pairings() { classic_bond_count = 1; classic_bonds[0][0] = 0x10; ble_bond_count = 1; ble_bond_types[0] = BD_ADDR_TYPE_LE_PUBLIC; ble_bonds[0][0] = 0x20; start_pairing_backend(); require(g_pairing_snapshot.status == Bluepad32PairingSnapshotStatus::kReady && g_pairing_snapshot.record_count == 2 && g_pairing_snapshot.records[0].transport == Bluepad32PairingTransport::kClassic && g_pairing_snapshot.records[1].transport == Bluepad32PairingTransport::kBle, "initial pairing snapshot must enumerate Classic and BLE bonds"); require(!bluepad32_input_backend_clear_pairings_completed( g_pairing_snapshot, 0), "zero pairing-clear token must never be a valid completion query"); const uint32_t snapshot_generation = g_pairing_snapshot.generation; g_next_clear_pairings_request_token = UINT32_MAX; uni_hid_device_t devices[2] = {device(0), device(1)}; for (uni_hid_device_t& controller : devices) { require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS, "pairing reset controller did not become ready"); } bluepad32_input_backend_queue_rumble( 0, ControllerRumbleOutput{100, 101}); const uint32_t clear_token = bluepad32_input_backend_clear_pairings(); const uint32_t repeated_token = bluepad32_input_backend_clear_pairings(); expected_pending_clear_token = clear_token; repeat_clear_during_disconnect = true; g_connection_policy_state = ConnectionPolicyState::Uninitialized; require(clear_token == UINT32_MAX && repeated_token == clear_token && g_clear_pairings_requested_token == clear_token && g_pairing_snapshot.completed_clear_pairings_token == 0 && !bluepad32_input_backend_clear_pairings_completed( g_pairing_snapshot, clear_token) && delete_key_calls == 0 && device_disconnect_calls == 0, "Core0 repeated pending clear calls must share one nonzero token, " "remain incomplete, and defer the operation to Core1"); process_rumble_timer(&g_rumble_timer); require(repeated_in_progress_clear_token == clear_token, "clear repeated during Core1 work did not share its token"); require(delete_key_calls == 1 && device_disconnect_calls == 2, "pairing reset must delete bonds and disconnect every session"); require(g_pairing_snapshot.status == Bluepad32PairingSnapshotStatus::kReady && g_pairing_snapshot.record_count == 0 && g_pairing_snapshot.generation == snapshot_generation + 1 && g_pairing_snapshot.completed_clear_pairings_token == clear_token && bluepad32_input_backend_clear_pairings_completed( g_pairing_snapshot, clear_token), "pairing reset must publish its completion token with an empty " "refreshed snapshot after policy update"); expected_pending_clear_token = 0; for (const BackendSlot& slot : g_slots) { require(slot.device == nullptr && !slot.active && !slot.rumble_pending && !slot.feedback_pending && 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 && accepted_stk_methods == 0 && g_connection_policy_state == ConnectionPolicyState::Open && scanning_enabled && classic_scanning_enabled && incoming_connections && observed_status_led_on, "pairing reset must close authentication and resume autoconnect"); tick_backend_timer(9); require(!observed_status_led_on, "pairing reset confirmation must use the rapid blink pattern"); process_rumble_timer(&g_rumble_timer); require(delete_key_calls == 1 && device_disconnect_calls == 2 && g_pairing_snapshot.completed_clear_pairings_token == clear_token, "pairing reset request must execute only once"); const uint32_t completed_generation = g_pairing_snapshot.generation; bluepad32_input_backend_request_pairing_snapshot(); require(g_pairing_snapshot.status == Bluepad32PairingSnapshotStatus::kPending && bluepad32_input_backend_clear_pairings_completed( g_pairing_snapshot, clear_token), "unrelated refresh revoked an already completed clear"); process_rumble_timer(&g_rumble_timer); require(g_pairing_snapshot.generation == completed_generation + 1 && g_pairing_snapshot.completed_clear_pairings_token == clear_token && bluepad32_input_backend_clear_pairings_completed( g_pairing_snapshot, clear_token), "ordinary pairing refresh fabricated or revoked clear completion"); const uint32_t refreshed_generation = g_pairing_snapshot.generation; const uint32_t wrapped_token = bluepad32_input_backend_clear_pairings(); require(wrapped_token == 1 && g_pairing_snapshot.status == Bluepad32PairingSnapshotStatus::kPending && g_pairing_snapshot.completed_clear_pairings_token == clear_token && !bluepad32_input_backend_clear_pairings_completed( g_pairing_snapshot, wrapped_token) && bluepad32_input_backend_clear_pairings_completed( g_pairing_snapshot, clear_token), "UINT32_MAX-to-1 wrap reused the prior completion or revoked it"); expected_pending_clear_token = wrapped_token; process_rumble_timer(&g_rumble_timer); expected_pending_clear_token = 0; require(delete_key_calls == 2 && device_disconnect_calls == 2 && g_pairing_snapshot.generation == refreshed_generation + 1 && g_pairing_snapshot.completed_clear_pairings_token == wrapped_token && bluepad32_input_backend_clear_pairings_completed( g_pairing_snapshot, wrapped_token) && bluepad32_input_backend_clear_pairings_completed( g_pairing_snapshot, clear_token), "wrapped clear completion did not acknowledge both itself and " "the earlier pre-wrap request exactly once"); } void test_configuration_timer_rearms_before_storage_work() { const uint32_t adds_before = g_configuration_timer.add_count; expected_configuration_timer_add_count = adds_before + 1; expect_configuration_timer_prearmed = true; process_configuration_timer(&g_configuration_timer); expect_configuration_timer_prearmed = false; require(g_configuration_timer.add_count == expected_configuration_timer_add_count && g_configuration_timer.timeout_ms == kConfigurationPollIntervalMs, "configuration timer did not remain recurring"); } void test_flash_core_start_contract() { bluepad32_input_backend_init(); flash_core_init_result = false; bluepad32_input_backend_start(); require(flash_core_init_calls == 1 && core1_launch_calls == 0 && g_connection_policy_state == ConnectionPolicyState::FailedClosed, "Core0 flash-safe init failure must prevent Core1 launch"); flash_core_init_result = true; bluepad32_input_backend_start(); require(flash_core_init_calls == 2 && core1_launch_calls == 1, "Core0 must register as a flash-safe victim before Core1 launch"); bluepad32_input_backend_start(); require(flash_core_init_calls == 2 && core1_launch_calls == 1, "backend start must remain idempotent"); } void test_wake_identity_gates_connections() { switch2_connections_ready = false; bluepad32_input_backend_init(); platform_on_init_complete(); require(switch2_wake_initializations == 1 && g_connection_policy_state == ConnectionPolicyState::Uninitialized && !scanning_enabled && !classic_scanning_enabled && !incoming_connections, "controller discovery started before wake identity was ready"); switch2_connections_ready = true; process_rumble_timer(&g_rumble_timer); require(g_connection_policy_state == ConnectionPolicyState::Open && scanning_enabled && classic_scanning_enabled && incoming_connections, "controller discovery did not start after wake identity setup"); } void test_system_button_wake_trigger() { start_pairing_backend(); uni_hid_device_t controller = device(0); require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS, "wake trigger controller did not become ready"); uni_controller_t input{}; input.klass = UNI_CONTROLLER_CLASS_GAMEPAD; platform_on_controller_data(&controller, &input); require(switch2_wake_requests == 0, "neutral input requested a wake burst"); input.gamepad.misc_buttons = MISC_BUTTON_SYSTEM; platform_on_controller_data(&controller, &input); require(switch2_wake_requests == 0, "plain system button requested a wake burst"); input.gamepad.buttons = BUTTON_SHOULDER_L | BUTTON_SHOULDER_R; platform_on_controller_data(&controller, &input); platform_on_controller_data(&controller, &input); require(switch2_wake_requests == 1, "held L+R+System chord did not produce exactly one wake request"); input.gamepad.buttons = 0; input.gamepad.misc_buttons = 0; platform_on_controller_data(&controller, &input); input.gamepad.buttons = BUTTON_SHOULDER_L | BUTTON_SHOULDER_R; platform_on_controller_data(&controller, &input); require(switch2_wake_requests == 1, "L+R without System requested wake"); input.gamepad.misc_buttons = MISC_BUTTON_SYSTEM; platform_on_controller_data(&controller, &input); require(switch2_wake_requests == 2, "second L+R+System chord edge did not request wake"); input.gamepad.buttons = 0; input.gamepad.misc_buttons = MISC_BUTTON_CAPTURE; platform_on_controller_data(&controller, &input); require(switch2_wake_requests == 2, "non-system misc button requested wake"); } void test_flash_core_init_fatal() { bluepad32_input_backend_init(); flash_core_init_result = false; bool stopped = false; try { core1_main(); } catch (const CoreStopped&) { stopped = true; } require(stopped && flash_core_init_calls == 1 && cyw43_init_calls == 0 && uni_init_calls == 0 && g_connection_policy_state == ConnectionPolicyState::FailedClosed, "flash-safe Core1 init failure must halt before CYW43 init"); } #if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) && defined(SWITCH_PICO_USB_OUTPUT_MODES) void advance_native_backend(uint32_t duration_ms) { const uint32_t end_ms = now_ms + duration_ms; while (now_ms < end_ms) { ++now_ms; for (;;) { const auto due = std::find_if( native_timers.begin(), native_timers.end(), [](const auto* timer) { return timer != &g_rumble_timer && timer != &g_configuration_timer && timer->due_ms <= now_ms; }); if (due == native_timers.end()) break; auto* timer = *due; native_timers.erase(due); timer->handler(timer); } if (now_ms % kRumblePollIntervalMs == 0) process_rumble_timer(&g_rumble_timer); } } void require_native_channels(bool left, bool right) { HapticsExperimentDiagnostics status; haptics_experiment_snapshot(&status); require(status.state == HapticsExperimentState::kRunning && status.mode == 1, "stateful host rumble lost native gameplay ownership"); require(status.packet_frames == 32, "native gameplay requires the supported 32-frame transport"); constexpr unsigned sample_offset = 14; require(last_native_packet[3] == 0x91 && last_native_packet[sample_offset - 2] == 0x92 && last_native_packet[sample_offset - 1] == 64, "stateful channel inspection requires a native PCM block"); unsigned active[2]{}; for (unsigned frame = 0; frame < status.packet_frames; ++frame) { active[0] += last_native_packet[sample_offset + frame * 2] != 0; active[1] += last_native_packet[sample_offset + frame * 2 + 1] != 0; } require((left ? active[0] > status.packet_frames / 2u : active[0] == 0) && (right ? active[1] > status.packet_frames / 2u : active[1] == 0), "native PCM did not preserve the requested stateful channels"); } void test_native_stateful_routing() { start_pairing_backend(); test_adapter_mode = AdapterUsbMode::kXInput; auto selected = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR); selected.vendor_id = 0x054c; selected.product_id = 0x0ce6; selected.conn.connected = true; selected.conn.interrupt_cid = 0x80; selected.outgoing_buffer.queued = 1; require(platform_on_device_ready(&selected) == UNI_ERROR_SUCCESS, "selected DualSense was rejected"); process_rumble_timer(&g_rumble_timer); HapticsExperimentDiagnostics status; haptics_experiment_snapshot(&status); require(status.mode == 1 && status.state == HapticsExperimentState::kPending, "XInput DualSense did not auto-arm into native Prepare"); const uint32_t generation = g_slots[0].connection_generation; bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{180, 0}); advance_native_backend(70); const int prepare_calls = selected.rumble_calls; selected.outgoing_buffer.queued = 0; advance_native_backend(350); require_native_channels(true, false); require(selected.rumble_calls == prepare_calls && last_native_cid == selected.conn.interrupt_cid, "XInput interleaved compatibility rumble into native PCM"); auto unselected = device(1, true, UNI_BT_CONN_PROTOCOL_BR_EDR); unselected.vendor_id = selected.vendor_id; unselected.product_id = selected.product_id; unselected.conn.connected = true; unselected.conn.interrupt_cid = 0x82; require(platform_on_device_ready(&unselected) == UNI_ERROR_SUCCESS, "unselected DualSense was rejected"); bluepad32_input_backend_queue_rumble(1, ControllerRumbleOutput{22, 33}); advance_native_backend(10); require(unselected.rumble_calls == 1 && unselected.last_low == 22 && unselected.last_high == 33, "unselected controller lost compatibility fallback"); require(!haptics_experiment_submit_rumble(1, generation, time_us_64(), 255, 255), "native stream accepted an unselected slot"); bluepad32_input_backend_queue_profile_feedback( 0, generation, 1, ControllerProfileConfirmationPolicy::kRumble); advance_native_backend(40); require_native_channels(true, true); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{0, 170}); advance_native_backend(220); require_native_channels(false, true); require(selected.rumble_calls == prepare_calls, "profile feedback escaped the native overlay"); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{0, 0}); advance_native_backend(80); require_native_channels(false, false); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{150, 0}); advance_native_backend(80); require(haptics_experiment_request(0, 0), "native stop was rejected"); advance_native_backend(20); require(!haptics_experiment_owns(&selected) && selected.last_low == 150 && selected.last_high == 0 && selected.last_rumble_duration_ms == kXInputHostRumbleDurationMs, "explicit native stop lost the latest compatibility fallback"); require(haptics_experiment_request(2, 0), "manual gameplay rearm failed"); advance_native_backend(300); require_native_channels(true, false); const int rearm_calls = selected.rumble_calls; advance_native_backend(300); require(selected.rumble_calls == rearm_calls, "held XInput state required periodic compatibility refresh"); require(haptics_experiment_request(0, 0), "second native stop failed"); advance_native_backend(20); require(selected.last_low == 150 && selected.last_high == 0 && selected.last_rumble_duration_ms == kXInputHostRumbleDurationMs, "manual rearm discarded held state needed by the next Stop"); require(haptics_experiment_request(1, 0), "fixture start failed"); advance_native_backend(10); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{0, 160}); advance_native_backend(6200); require(haptics_experiment_request(2, 0), "post-fixture gameplay arm failed"); advance_native_backend(300); require_native_channels(false, true); // A real HD frame replaces stateful XInput semantics and keeps its watchdog. test_adapter_mode = AdapterUsbMode::kSwitchProbe; ControllerRumbleOutput hd{}; hd.hd.actuators[0].sample_count = 1; hd.hd.actuators[1].sample_count = 1; hd.hd.actuators[0].samples[0].low_amplitude_q15 = 20000; hd.hd.actuators[0].samples[0].low_frequency_index = 64; bluepad32_input_backend_queue_rumble(0, hd); advance_native_backend(40); require_native_channels(true, false); advance_native_backend(100); require_native_channels(false, false); test_adapter_mode = AdapterUsbMode::kXInput; bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{255, 0}); platform_on_device_disconnected(&selected); require(!haptics_experiment_submit_rumble(0, generation, time_us_64(), 255, 255), "disconnected generation remained eligible for native output"); require(platform_on_device_ready(&selected) == UNI_ERROR_SUCCESS, "replacement DualSense was rejected"); advance_native_backend(300); require_native_channels(false, false); require(!haptics_experiment_submit_rumble(0, generation, time_us_64(), 255, 255), "old generation reached a replacement native stream"); require(haptics_experiment_request(0, 0), "replacement stop failed"); advance_native_backend(20); platform_on_device_disconnected(&selected); platform_on_device_disconnected(&unselected); } void test_native_second_slot_selection() { start_pairing_backend(); test_adapter_mode = AdapterUsbMode::kXInput; auto pro = device(0, true, UNI_BT_CONN_PROTOCOL_BR_EDR); pro.vendor_id = 0x057e; pro.product_id = 0x2009; pro.conn.connected = true; pro.conn.interrupt_cid = 0x80; require(platform_on_device_ready(&pro) == UNI_ERROR_SUCCESS, "first-slot Switch Pro was rejected"); auto dualsense = device(1, true, UNI_BT_CONN_PROTOCOL_BR_EDR); dualsense.vendor_id = 0x054c; dualsense.product_id = 0x0ce6; dualsense.conn.connected = true; dualsense.conn.interrupt_cid = 0x82; require(platform_on_device_ready(&dualsense) == UNI_ERROR_SUCCESS, "second-slot DualSense was rejected"); advance_native_backend(30); HapticsExperimentDiagnostics status; haptics_experiment_snapshot(&status); require(status.state == HapticsExperimentState::kRunning && status.slot == 1, "native auto-arm ignored the first eligible DualSense outside slot zero"); bluepad32_input_backend_queue_rumble(1, ControllerRumbleOutput{90, 0}); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{20, 0}); advance_native_backend(200); require_native_channels(true, false); require(pro.rumble_calls == 1 && last_native_cid == 0x82, "mixed controller slots lost their independent rumble paths"); auto later = device(2, true, UNI_BT_CONN_PROTOCOL_BR_EDR); later.vendor_id = dualsense.vendor_id; later.product_id = dualsense.product_id; later.conn.connected = true; later.conn.interrupt_cid = 0x84; require(platform_on_device_ready(&later) == UNI_ERROR_SUCCESS, "later DualSense was rejected"); advance_native_backend(30); haptics_experiment_snapshot(&status); require(status.slot == 1 && last_native_cid == 0x82, "a later DualSense stole the selected native stream"); require(haptics_experiment_request(0, 1), "selected stream did not stop"); advance_native_backend(20); platform_on_device_disconnected(&dualsense); platform_on_device_disconnected(&pro); platform_on_device_disconnected(&later); } #endif uni_hid_device_t wii_device(int index) { auto result = device(index, true, UNI_BT_CONN_PROTOCOL_BR_EDR); result.vendor_id = 0x057e; result.product_id = 0x0330; result.controller_type = CONTROLLER_TYPE_WiiController; result.controller_subtype = CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL; return result; } void test_wii_accelerometer_streams() { start_pairing_backend(); auto remote = wii_device(0); remote.controller_subtype = CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL; require(platform_on_device_ready(&remote) == UNI_ERROR_SUCCESS, "Wii must become ready"); const auto generation = slot_snapshot(0).connection_generation; if (kDefaultMotionEnabled) { require(bluepad32_input_backend_toggle_motion(0, generation), "Wii console motion must be disabled for independent gesture streams"); } remote_accel_fixture = {&remote, {8192, 0, 0}, 7, true}; nunchuk_accel_fixture = {&remote, {0, 8192, 0}, 9, false}; uni_controller_t input{}; input.klass = UNI_CONTROLLER_CLASS_GAMEPAD; input.gamepad.accel[0] = 8192; input.gamepad.gyro[2] = 1024; now_ms = 100; platform_on_controller_data(&remote, &input); auto snapshot = slot_snapshot(0); require(snapshot.state.motion_sample_count == 0 && snapshot.accelerometer.valid && !snapshot.nunchuk_accelerometer.valid && snapshot.accelerometer.y == -4096 && snapshot.accelerometer.timestamp_ms == 100, "Remote gesture acceleration must remain calibrated with console motion off"); now_ms = 125; nunchuk_accel_fixture.valid = true; platform_on_controller_data(&remote, &input); snapshot = slot_snapshot(0); require(snapshot.state.motion_sample_count == 0 && snapshot.nunchuk_accelerometer.valid && snapshot.nunchuk_accelerometer.z == 4096 && snapshot.nunchuk_accelerometer.timestamp_ms == 125 && snapshot.accelerometer.timestamp_ms == 100, "Nunchuk interleave must publish its own sample without refreshing the Remote"); now_ms = 150; ++remote_accel_fixture.sequence; // Same physical values, genuinely new report. platform_on_controller_data(&remote, &input); now_ms = 200; // Status/ack callback exposes the same parser snapshots. platform_on_controller_data(&remote, &input); snapshot = slot_snapshot(0); bluepad32_input_backend_report_sent(0); snapshot = slot_snapshot(0); require(snapshot.accelerometer.timestamp_ms == 150 && snapshot.nunchuk_accelerometer.timestamp_ms == 125 && snapshot.accelerometer.valid && snapshot.nunchuk_accelerometer.valid, "cached parser callbacks and USB consumption must not freshen either sensor"); now_ms = 225; nunchuk_accel_fixture.valid = false; platform_on_controller_data(&remote, &input); snapshot = slot_snapshot(0); require(!snapshot.nunchuk_accelerometer.valid && snapshot.accelerometer.valid && snapshot.accelerometer.timestamp_ms == 150, "Nunchuk detach/calibration failure must immediately invalidate only its own sensor"); now_ms = 250; nunchuk_accel_fixture.valid = true; ++nunchuk_accel_fixture.sequence; platform_on_controller_data(&remote, &input); snapshot = slot_snapshot(0); require(snapshot.nunchuk_accelerometer.valid && snapshot.nunchuk_accelerometer.timestamp_ms == 250 && snapshot.accelerometer.timestamp_ms == 150, "replacement Nunchuk must resume with its own fresh timestamp"); platform_on_device_disconnected(&remote); snapshot = slot_snapshot(0); require(!snapshot.accelerometer.valid && !snapshot.nunchuk_accelerometer.valid, "Remote disconnect must invalidate both accelerometer streams"); auto replacement = wii_device(0); require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS, "replacement Wii must become ready"); snapshot = slot_snapshot(0); require(!snapshot.accelerometer.valid && !snapshot.nunchuk_accelerometer.valid, "reused backend slot must not inherit either sensor sample"); } void dispatch_wii_requests() { wii_dispatch_active = true; now_ms += g_rumble_timer.timeout_ms; g_rumble_timer.handler(&g_rumble_timer); wii_dispatch_active = false; } #ifdef SWITCH2_BRIDGE_WII_INPUT void test_wii_bridge_sensors() { auto selected = wii_device(1); auto other = wii_device(0); bluepad32_input_backend_init(); bluepad32_input_backend_select_wii_source(selected.conn.btaddr); start_pairing_backend(); require(platform_on_device_ready(&other) == UNI_ERROR_SUCCESS, "unselected Wii must remain available normally"); uni_controller_t input{}; input.klass = UNI_CONTROLLER_CLASS_GAMEPAD; input.gamepad.buttons = BUTTON_A; remote_accel_fixture = {&other, {8193, -42, 91}, 7, true}; gyro_fixture = {&other, {123456, -45678, 91011}, 11, true}; platform_on_controller_data(&other, &input); Bluepad32WiiBridgeSnapshot snapshot{}; bluepad32_input_backend_wii_snapshot(&snapshot); require(!snapshot.controller.active && snapshot.slot == 0xff, "an unrelated Wii cannot claim the selected native source"); selected.controller_type = CONTROLLER_TYPE_UnknownController; require(platform_on_device_ready(&selected) == UNI_ERROR_SUCCESS, "non-Wii address match must connect normally"); platform_on_controller_data(&selected, &input); bluepad32_input_backend_wii_snapshot(&snapshot); require(!snapshot.controller.active, "matching address alone must not identify a Wii"); platform_on_device_disconnected(&selected); auto replacement = wii_device(1); require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS, "selected physical Wii must become ready"); const uint32_t generation = slot_snapshot(1).connection_generation; if (kDefaultMotionEnabled) { require(bluepad32_input_backend_toggle_motion(1, generation), "legacy motion toggle must be independent"); } remote_accel_fixture = {&replacement, {8193, -42, 91}, 7, true}; gyro_fixture = {&replacement, {123456, -45678, 91011}, 11, true}; now_ms = 100; platform_on_controller_data(&replacement, &input); bluepad32_input_backend_wii_snapshot(&snapshot); require(snapshot.controller.active && snapshot.slot == 1 && snapshot.controller.state.button_south && snapshot.controller.state.motion_sample_count == 0 && snapshot.accel_valid && snapshot.gyro_valid && snapshot.accel_q13[0] == 8193 && snapshot.gyro_q10[0] == 123456, "native raw precision and ordinary buttons must survive legacy motion disable"); require(snapshot.accel_received_us == 100000 && snapshot.gyro_received_us == 100000, "both first genuine sensor samples need ingress timestamps"); now_ms = 250; ++remote_accel_fixture.sequence; platform_on_controller_data(&replacement, &input); now_ms = 300; // Status/ACK callback has no new sensor sequence. platform_on_controller_data(&replacement, &input); bluepad32_input_backend_report_sent(1); bluepad32_input_backend_wii_snapshot(&snapshot); require(snapshot.received_us == 300000 && snapshot.accel_received_us == 250000 && snapshot.gyro_received_us == 100000 && snapshot.gyro_sequence == 11, "accel interleaves, status and USB consumption cannot rejuvenate gyro"); require(bluepad32_input_backend_set_wii_orientation( snapshot.controller.identity, generation, true), "orientation change must queue"); dispatch_wii_requests(); platform_on_controller_data(&replacement, &input); bluepad32_input_backend_wii_snapshot(&snapshot); require(snapshot.controller.connection_generation != generation && !snapshot.accel_valid && !snapshot.gyro_valid && !snapshot.controller.accelerometer.valid, "a logical epoch must not republish cached parser samples as fresh"); ++remote_accel_fixture.sequence; ++gyro_fixture.sequence; now_ms = 320; platform_on_controller_data(&replacement, &input); bluepad32_input_backend_wii_snapshot(&snapshot); require(snapshot.accel_valid && snapshot.gyro_valid && snapshot.gyro_received_us == 320000, "distinct samples must recover the new logical epoch"); platform_on_device_disconnected(&replacement); bluepad32_input_backend_wii_snapshot(&snapshot); require(!snapshot.controller.active && !snapshot.accel_valid && !snapshot.gyro_valid && snapshot.slot == 0xff, "disconnect must retire source identity and samples together"); } void (*during_wii_rumble)() = nullptr; void observe_wii_rumble(uni_hid_device_t* target, uint16_t delay, uint16_t duration, uint8_t weak, uint8_t strong) { require(state_lock_depth == 0, "Wii transport must run without the backend lock"); play_rumble(target, delay, duration, weak, strong); if (during_wii_rumble != nullptr) during_wii_rumble(); } void test_wii_bridge_cues() { auto selected = wii_device(1); selected.report_parser.play_dual_rumble = observe_wii_rumble; auto other = wii_device(0); bluepad32_input_backend_init(); bluepad32_input_backend_select_wii_source(selected.conn.btaddr); start_pairing_backend(); require(platform_on_device_ready(&other) == UNI_ERROR_SUCCESS && platform_on_device_ready(&selected) == UNI_ERROR_SUCCESS, "cue fixtures must connect"); uint64_t token = 0; require(bluepad32_input_backend_wii_sample_request(3, &token) && bluepad32_input_backend_wii_sample_result(token) == 0 && selected.rumble_calls == 0, "Core 0 queue acceptance is not cue completion"); process_rumble_timer(&g_rumble_timer); require(bluepad32_input_backend_wii_sample_result(token) == 1 && bluepad32_input_backend_wii_sample_result(token) == -1 && selected.rumble_calls == 1 && selected.last_rumble_duration_ms == 25, "double-click completion must be consumable once after its first driver dispatch"); now_ms = 60; process_rumble_timer(&g_rumble_timer); require(selected.rumble_calls == 1, "double-click gap must not vibrate"); now_ms = 115; process_rumble_timer(&g_rumble_timer); require(selected.rumble_calls == 2 && selected.last_rumble_duration_ms == 25 && other.rumble_calls == 0, "double-click must dispatch exactly its second selected-Wii pulse"); now_ms = 2000; process_rumble_timer(&g_rumble_timer); uint64_t newer; require(bluepad32_input_backend_wii_sample_request(1, &newer) && newer != token, "finite patterns must release the cue ledger without reusing tokens"); process_rumble_timer(&g_rumble_timer); require(selected.last_rumble_duration_ms == 1000, "buzz must have a finite one-second driver timer"); require(bluepad32_input_backend_wii_sample_request(0, &token), "stop must replace a running pattern"); process_rumble_timer(&g_rumble_timer); require(selected.last_rumble_duration_ms == 0 && bluepad32_input_backend_wii_sample_result(token) == 1 && bluepad32_input_backend_wii_sample_result(newer) == -1, "stop must retire the old ledger and complete only after dispatch"); const int stopped_calls = selected.rumble_calls; now_ms += 3000; process_rumble_timer(&g_rumble_timer); require(selected.rumble_calls == stopped_calls, "stopped patterns must never replay"); } void test_wii_bridge_cue_races() { auto selected = wii_device(0); selected.report_parser.play_dual_rumble = observe_wii_rumble; bluepad32_input_backend_init(); bluepad32_input_backend_select_wii_source(selected.conn.btaddr); start_pairing_backend(); require(platform_on_device_ready(&selected) == UNI_ERROR_SUCCESS, "selected Wii must connect"); const auto connection = slot_snapshot(0); bluepad32_input_backend_queue_profile_feedback( 0, connection.connection_generation, 1, ControllerProfileConfirmationPolicy::kRumble); uint64_t token; require(bluepad32_input_backend_wii_sample_request(3, &token), "cue must wait behind profile feedback"); process_rumble_timer(&g_rumble_timer); require(selected.last_rumble_duration_ms == 75 && bluepad32_input_backend_wii_sample_result(token) == 0, "profile confirmation has priority and cannot count as cue dispatch"); now_ms = 150; during_wii_rumble = [] { bluepad32_input_backend_wii_sample_cancel(); }; process_rumble_timer(&g_rumble_timer); during_wii_rumble = nullptr; require(bluepad32_input_backend_wii_sample_result(token) == -1, "cancellation during driver dispatch must defeat its late completion"); process_rumble_timer(&g_rumble_timer); require(selected.last_rumble_duration_ms == 0, "canceled in-flight cue must receive a finite stop"); wii_rumble_ready = false; require(bluepad32_input_backend_wii_sample_request(1, &token), "topology setup may defer cue dispatch"); const int calls = selected.rumble_calls; process_rumble_timer(&g_rumble_timer); require(bluepad32_input_backend_wii_sample_result(token) == 0 && selected.rumble_calls == calls, "a Wii void-hook early return must not masquerade as dispatch"); now_ms += 2000; require(bluepad32_input_backend_wii_sample_result(token) == -1, "undispatched cue deadline must expire"); wii_rumble_ready = true; process_rumble_timer(&g_rumble_timer); require(selected.rumble_calls == calls, "expired cue must not dispatch when setup finally completes"); uint64_t replacement_token; require(bluepad32_input_backend_wii_sample_request(2, &replacement_token) && replacement_token != token, "expiration must permit a uniquely identified new request"); during_wii_rumble = [] { platform_on_device_disconnected(g_slots[0].device); }; process_rumble_timer(&g_rumble_timer); during_wii_rumble = nullptr; auto replacement = wii_device(0); require(platform_on_device_ready(&replacement) == UNI_ERROR_SUCCESS, "replacement must connect"); process_rumble_timer(&g_rumble_timer); require(bluepad32_input_backend_wii_sample_result(replacement_token) == -1 && replacement.rumble_calls == 0, "late driver completion must never cross a physical generation"); replacement.report_parser.play_dual_rumble = nullptr; require(!bluepad32_input_backend_wii_sample_request(1, &token), "a missing Wii rumble driver cannot accept a cue"); } #endif void test_wii_orientation() { start_pairing_backend(); initialize_runtime_profile_storage(); auto remote = wii_device(0); auto ordinary = device(1, true, UNI_BT_CONN_PROTOCOL_BR_EDR); ready_switch2(remote); ready_switch2(ordinary); const auto identity = slot_snapshot(0).identity; const auto ordinary_identity = slot_snapshot(1).identity; configure_gesture_profile(identity, 2, 37); configure_gesture_profile(ordinary_identity, 5, 63); controller_profile_runtime_reset(); for (uint8_t index = 0; index < 2; ++index) { controller_profile_runtime_transform( index, slot_snapshot(index), now_ms, AdapterUsbMode::kXInput); } uni_controller_t report{}; report.klass = UNI_CONTROLLER_CLASS_GAMEPAD; report.gamepad.buttons = BUTTON_A; ++now_ms; platform_on_controller_data(&remote, &report); platform_on_controller_data(&ordinary, &report); for (uint8_t index = 0; index < 2; ++index) { require(controller_profile_runtime_transform( index, slot_snapshot(index), now_ms, AdapterUsbMode::kXInput) .state.button_system, "both controllers need active toggle macros before changing only Wii orientation"); } report.gamepad.buttons = BUTTON_Y; report.gamepad.dpad = DPAD_UP; report.gamepad.axis_x = 511; report.gamepad.brake = 1023; platform_on_controller_data(&remote, &report); const auto before = slot_snapshot(0); const auto ordinary_before = slot_snapshot(1); require(bluepad32_input_backend_capture_start( 0, before.connection_generation, CaptureOptions{}), "orientation transition must have an active recorder to retire"); bluepad32_input_backend_queue_rumble(0, ControllerRumbleOutput{91, 101}); bluepad32_input_backend_queue_profile_feedback( 0, before.connection_generation, 8, ControllerProfileConfirmationPolicy::kRumbleAndLed); const unsigned writes_before = profile_write_attempts; const size_t observations_before = observed_profile_identity_count; require(bluepad32_input_backend_set_wii_orientation( identity, before.connection_generation, true), "live standalone Wii must accept a vertical request"); Bluepad32PlaytestSnapshot playtest{}; bluepad32_input_backend_playtest_snapshot(0, &playtest); require(wii_mode_events.empty() && playtest.controller_layout == Bluepad32ControllerLayout::kWiiHorizontal && playtest.connection_generation == before.connection_generation && playtest.state.button_north && playtest.state.dpad_up, "enqueue must not call the parser or claim a new mapping on the management core"); dispatch_wii_requests(); bluepad32_input_backend_playtest_snapshot(0, &playtest); require(wii_mode_events.size() == 1 && wii_mode_events[0].device == &remote && wii_mode_events[0].mode == WII_MODE_VERTICAL && playtest.active && playtest.controller_layout == Bluepad32ControllerLayout::kWiiVertical && playtest.connection_generation == before.connection_generation + 1u && controller_identity_equal(playtest.identity, identity) && playtest.physical_button_mask == 0 && !playtest.state.button_north && !playtest.state.dpad_up && playtest.state.left_stick_x == 0 && playtest.state.left_trigger == 0 && playtest.state.motion_sample_count == 0, "timer dispatch must atomically retire old input and publish the new live mapping epoch"); Bluepad32CaptureSnapshot capture{}; require(bluepad32_input_backend_capture_page(0, 0, &capture) && capture.state == CaptureState::kDisconnected && !controller_profile_runtime_transform( 0, slot_snapshot(0), now_ms, AdapterUsbMode::kXInput).state.button_system && controller_profile_runtime_transform( 1, slot_snapshot(1), now_ms, AdapterUsbMode::kXInput).state.button_system && slot_snapshot(1).connection_generation == ordinary_before.connection_generation && remote.last_high == 0 && remote.last_low == 0 && !bluepad32_input_backend_toggle_motion(0, before.connection_generation), "mapping changes must cancel old macro/capture/hotkey/feedback epochs without disturbing peers"); report.gamepad = {}; report.gamepad.buttons = BUTTON_B; platform_on_controller_data(&remote, &report); require(slot_snapshot(0).state.button_east && !slot_snapshot(0).state.button_north, "fresh input must use the new mapping without reviving old controls"); const uint32_t vertical_generation = playtest.connection_generation; require(bluepad32_input_backend_set_wii_orientation( identity, vertical_generation, false), "current vertical mapping must be reversible without reconnecting"); dispatch_wii_requests(); bluepad32_input_backend_playtest_snapshot(0, &playtest); require(playtest.controller_layout == Bluepad32ControllerLayout::kWiiHorizontal && playtest.connection_generation == vertical_generation + 1u && wii_mode_events.size() == 2 && wii_mode_events[1].mode == WII_MODE_HORIZONTAL, "horizontal selection must return the real parser and metadata to horizontal"); const uint32_t horizontal_generation = playtest.connection_generation; require(bluepad32_input_backend_set_wii_orientation( identity, horizontal_generation, false), "reselection must still reach a parser with a potentially deferred opposite choice"); dispatch_wii_requests(); require(wii_mode_events.size() == 3 && wii_mode_events[2].mode == WII_MODE_HORIZONTAL && slot_snapshot(0).connection_generation == horizontal_generation + 1u && profile_write_attempts == writes_before && observed_profile_identity_count == observations_before && device_disconnect_calls == 0, "reselection and orientation roundtrip must preserve profiles, flash and physical connections"); require_active_profile(identity, 2, 37); require_active_profile(ordinary_identity, 5, 63); } void test_wii_orientation_races() { auto remote = wii_device(0); const auto identity = identity_for_device(&remote); require(!bluepad32_input_backend_set_wii_orientation(identity, 0, true), "orientation requests must reject an uninitialized backend"); start_pairing_backend(); platform_on_device_connected(&remote); require(!bluepad32_input_backend_set_wii_orientation( identity, slot_snapshot(0).connection_generation, true), "orientation requests must reject connections that are not ready"); require(platform_on_device_ready(&remote) == UNI_ERROR_SUCCESS, "Wii race fixture must become ready"); const auto before = slot_snapshot(0); auto ordinary = device(1, true, UNI_BT_CONN_PROTOCOL_BR_EDR); ordinary.controller_subtype = CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL; ready_switch2(ordinary); auto unstable = wii_device(2); unstable.conn.protocol = UNI_BT_CONN_PROTOCOL_NONE; gap_connection_types[unstable.conn.handle] = GAP_CONNECTION_INVALID; ready_switch2(unstable); auto wrong_identity = identity; wrong_identity.product_id ^= 1u; require(!bluepad32_input_backend_set_wii_orientation( identity, before.connection_generation - 1u, true) && !bluepad32_input_backend_set_wii_orientation( wrong_identity, before.connection_generation, true) && !bluepad32_input_backend_set_wii_orientation( slot_snapshot(1).identity, slot_snapshot(1).connection_generation, true) && !bluepad32_input_backend_set_wii_orientation( slot_snapshot(2).identity, slot_snapshot(2).connection_generation, true), "stale generation, wrong owner, non-Wii and global fallback must not target a parser"); const uni_controller_subtype_t extensions[] = { CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK, CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK_ACCEL, CONTROLLER_SUBTYPE_WII_CLASSIC, }; for (auto subtype : extensions) { remote.controller_subtype = subtype; require(!bluepad32_input_backend_set_wii_orientation( identity, before.connection_generation, false), "Nunchuk and other extension mappings cannot be changed by standalone orientation"); } remote.controller_subtype = CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL; require(bluepad32_input_backend_set_wii_orientation( identity, before.connection_generation, true), "race fixture must enqueue while still standalone"); remote.controller_subtype = CONTROLLER_SUBTYPE_WIIMOTE_NUNCHUK; dispatch_wii_requests(); Bluepad32PlaytestSnapshot playtest{}; bluepad32_input_backend_playtest_snapshot(0, &playtest); require(wii_mode_events.empty() && playtest.controller_layout == Bluepad32ControllerLayout::kWiiNunchuk && playtest.connection_generation == before.connection_generation, "extension attachment before dispatch must reject the queued request without changing topology"); remote.controller_subtype = CONTROLLER_SUBTYPE_WIIMOTE_HORIZONTAL; dispatch_wii_requests(); require(wii_mode_events.empty(), "a request rejected during attachment must not revive when the Nunchuk detaches"); require(bluepad32_input_backend_set_wii_orientation( identity, before.connection_generation, true), "standalone connection must accept another request after detach"); const WiiOrientationRequest stale = g_slots[0].pending_wii_orientation; platform_on_device_disconnected(&remote); remote = wii_device(0); ready_switch2(remote); dispatch_wii_requests(); require(wii_mode_events.empty() && !bluepad32_input_backend_set_wii_orientation( identity, before.connection_generation, true), "physical object reuse with the same identity must discard old pending requests and reject its old epoch"); // Exercise the dispatch guard even if an already accepted old envelope // survives a future mailbox refactor. g_slots[0].pending_wii_orientation = stale; g_slots[0].wii_orientation_pending = true; dispatch_wii_requests(); g_slots[0].pending_wii_orientation = { wrong_identity, slot_snapshot(0).connection_generation, true}; g_slots[0].wii_orientation_pending = true; dispatch_wii_requests(); require(wii_mode_events.empty() && slot_snapshot(0).connection_generation == before.connection_generation + 1u, "dispatch must independently revalidate both connection generation and identity"); const auto replacement = slot_snapshot(0); require(bluepad32_input_backend_set_wii_orientation( replacement.identity, replacement.connection_generation, true) && bluepad32_input_backend_set_wii_orientation( replacement.identity, replacement.connection_generation, false), "rapid standalone choices must coalesce before dispatch"); dispatch_wii_requests(); require(wii_mode_events.size() == 1 && wii_mode_events[0].mode == WII_MODE_HORIZONTAL && slot_snapshot(0).connection_generation == replacement.connection_generation + 1u, "coalescing back to the current mapping must not apply an obsolete intermediate choice"); } void require_selected_radio(bool active_scan, bool free_slot) { require(scanning_enabled == (active_scan && SWITCH_PICO_ENABLE_BLE) && classic_scanning_enabled == (active_scan && SWITCH_PICO_ENABLE_CLASSIC) && incoming_connections == (free_slot && SWITCH_PICO_ENABLE_CLASSIC), "radio policy activated a disabled transport or lost an enabled one"); } void test_transport_mode_policy() { classic_bond_count = 1; classic_bonds[0][5] = 0xa1; ble_bond_count = 1; ble_bond_types[0] = BD_ADDR_TYPE_LE_RANDOM; ble_bonds[0][5] = 0xb1; auto remembered_switch2 = switch2_device(3, UNI_SW2_PRO_PID); remember_switch2(remembered_switch2); initialize_runtime_profile_storage(); ControllerIdentity saved_identities[2]{}; for (unsigned index = 0; index < 2; ++index) { auto& identity = saved_identities[index]; identity.stable = true; identity.transport = index == 0 ? ControllerTransport::kClassic : ControllerTransport::kBle; identity.address_type = index == 0 ? BD_ADDR_TYPE_UNKNOWN : BD_ADDR_TYPE_LE_RANDOM; memcpy(identity.address, index == 0 ? classic_bonds[0] : ble_bonds[0], sizeof(bd_addr_t)); identity.vendor_id = 0x1234; identity.product_id = 0x5678; auto profile = controller_profile_default(identity, 2 + index); profile.weak_rumble_scale = 37 + index; require(runtime_profile_storage.set(identity, 2 + index, profile) == ProfileStorageResult::kOk && runtime_profile_storage.activate(identity, 2 + index) == ProfileStorageResult::kOk, "mode fixture must persist distinct Classic and BLE profiles"); } if (!SWITCH_PICO_ENABLE_BLE) switch2_connections_ready = false; start_backend(); Bluepad32PairingSnapshot bonds{}; bluepad32_input_backend_pairing_snapshot(&bonds); require(bonds.record_count == 3 && bonds.records[0].transport == Bluepad32PairingTransport::kClassic && bonds.records[1].transport == Bluepad32PairingTransport::kBle && bonds.records[2].transport == Bluepad32PairingTransport::kBle && memcmp(bonds.records[0].address, classic_bonds[0], sizeof(bd_addr_t)) == 0 && memcmp(bonds.records[1].address, ble_bonds[0], sizeof(bd_addr_t)) == 0 && memcmp(bonds.records[2].address, remembered_switch2.conn.btaddr, sizeof(bd_addr_t)) == 0 && delete_key_calls == 0, "boot must expose remembered keys from both transports without deleting them"); for (bool ble : {false, true}) { const bool enabled = ble ? SWITCH_PICO_ENABLE_BLE : SWITCH_PICO_ENABLE_CLASSIC; auto candidate = device(0, true, ble ? UNI_BT_CONN_PROTOCOL_BLE : UNI_BT_CONN_PROTOCOL_BR_EDR); lookup_devices[0] = &candidate; lookup_device_count = 1; // A discovery candidate need not have a live GAP handle yet. const auto handle = candidate.conn.handle; candidate.conn.handle = HCI_CON_HANDLE_INVALID; require(platform_on_device_discovered(candidate.conn.btaddr, "candidate", 0, 0) == (enabled ? UNI_ERROR_SUCCESS : UNI_ERROR_IGNORE_DEVICE), "discovery admitted a known disabled-transport candidate"); candidate.conn.handle = handle; // A stale protocol hint must never overrule the actual live GAP link. candidate.conn.protocol = ble ? UNI_BT_CONN_PROTOCOL_BR_EDR : UNI_BT_CONN_PROTOCOL_BLE; const int disconnected_before = device_disconnect_calls; platform_on_device_connected(&candidate); require(device_disconnect_calls == disconnected_before + !enabled, "connected admission failed to tear down a disabled live transport"); require(platform_on_device_ready(&candidate) == (enabled ? UNI_ERROR_SUCCESS : UNI_ERROR_INVALID_CONTROLLER) && slot_snapshot(0).active == enabled, "ready callback bypassed transport admission"); uni_controller_t report{}; report.klass = UNI_CONTROLLER_CLASS_GAMEPAD; report.gamepad.buttons = BUTTON_A; platform_on_controller_data(&candidate, &report); require(slot_snapshot(0).state.button_south == enabled, "rejected transport published controller input"); if (enabled) platform_on_device_disconnected(&candidate); require_selected_radio(true, true); } auto ordinary_ble = device(0, true, UNI_BT_CONN_PROTOCOL_BLE); lookup_devices[0] = &ordinary_ble; __wrap_sm_request_pairing(ordinary_ble.conn.handle); __wrap_sm_request_pairing(HCI_CON_HANDLE_INVALID); require(ordinary_smp_requests == (SWITCH_PICO_ENABLE_BLE ? 2u : 0u), "Classic-only mode forwarded an SMP authentication request"); lookup_device_count = 0; if (!SWITCH_PICO_ENABLE_BLE || !SWITCH_PICO_ENABLE_CLASSIC) { auto unknown = device(0); const int disconnected_before = device_disconnect_calls; platform_on_device_connected(&unknown); require(device_disconnect_calls == disconnected_before + 1 && platform_on_device_ready(&unknown) == UNI_ERROR_INVALID_CONTROLLER && !slot_snapshot(0).active, "single-transport mode admitted an unidentified connection"); } uint8_t confirmation[] = {HCI_EVENT_USER_CONFIRMATION_REQUEST, 6, 1, 2, 3, 4, 5, 6}; uint8_t passkey[] = {HCI_EVENT_USER_PASSKEY_REQUEST, 6, 1, 2, 3, 4, 5, 6}; handle_btstack_event(HCI_EVENT_PACKET, 0, confirmation, sizeof(confirmation)); handle_btstack_event(HCI_EVENT_PACKET, 0, passkey, sizeof(passkey)); require(confirmation_accepts == 0 && passkey_accepts == 0 && confirmation_rejections == SWITCH_PICO_ENABLE_CLASSIC && passkey_rejections == SWITCH_PICO_ENABLE_CLASSIC, "closed pairing policy must reject only the enabled Classic authentication"); bluepad32_input_backend_open_pairing_window(); process_rumble_timer(&g_rumble_timer); require(bondable && switch_pico_switch2_pairing_allowed() == (SWITCH_PICO_ENABLE_BLE != 0) && (!SWITCH_PICO_ENABLE_BLE || accepted_stk_methods == kAllBlePairingMethods), "pairing window did not enable only the selected authentication policy"); handle_btstack_event(HCI_EVENT_PACKET, 0, confirmation, sizeof(confirmation)); handle_btstack_event(HCI_EVENT_PACKET, 0, passkey, sizeof(passkey)); require(confirmation_accepts == SWITCH_PICO_ENABLE_CLASSIC && passkey_accepts == SWITCH_PICO_ENABLE_CLASSIC, "pairing window accepted disabled Classic authentication"); uni_hid_device_t controllers[kSlotCount]{}; for (uint8_t index = 0; index < kSlotCount; ++index) { const bool ble = SWITCH_PICO_ENABLE_BLE && (!SWITCH_PICO_ENABLE_CLASSIC || (index & 1u)); controllers[index] = device(index, true, ble ? UNI_BT_CONN_PROTOCOL_BLE : UNI_BT_CONN_PROTOCOL_BR_EDR); platform_on_device_connected(&controllers[index]); require(platform_on_device_ready(&controllers[index]) == UNI_ERROR_SUCCESS, "enabled transport failed to fill the available slots"); } require_selected_radio(false, false); require(platform_on_device_discovered(controllers[0].conn.btaddr, "full", 0, 0) == UNI_ERROR_IGNORE_DEVICE, "full slots admitted another discovery"); now_ms = g_pairing_window_deadline_ms; process_rumble_timer(&g_rumble_timer); require(!bondable && !switch_pico_switch2_pairing_allowed() && (!SWITCH_PICO_ENABLE_BLE || accepted_stk_methods == 0), "pairing authentication remained enabled after the deadline"); platform_on_device_disconnected(&controllers[3]); require_selected_radio(false, true); bluepad32_input_backend_open_pairing_window(); process_rumble_timer(&g_rumble_timer); require_selected_radio(true, true); for (uint8_t index = 0; index < 3; ++index) { platform_on_device_disconnected(&controllers[index]); } require_selected_radio(true, true); require(classic_bond_count == 1 && ble_bond_count == 1 && switch2_pairing_count == 1 && delete_key_calls == 0, "pairing windows or disconnects erased inactive-mode bonds"); for (unsigned index = 0; index < 2; ++index) { require_active_profile(saved_identities[index], 2 + index, 37 + index); } const uint32_t token = bluepad32_input_backend_clear_pairings(); process_rumble_timer(&g_rumble_timer); bluepad32_input_backend_pairing_snapshot(&bonds); require(bluepad32_input_backend_clear_pairings_completed(bonds, token) && bonds.record_count == 0 && classic_bond_count == 0 && ble_bond_count == 0 && switch2_pairing_count == 0, "explicit clearing must still cover all stored transports"); for (unsigned index = 0; index < 2; ++index) { require_active_profile(saved_identities[index], 2 + index, 37 + index); } require_selected_radio(true, true); } void test_transport_background_scan() { start_backend(); auto left = switch2_device(0, UNI_SW2_JOYCON_L_PID); auto right = switch2_device(1, UNI_SW2_JOYCON_R_PID); remember_switch2(right); register_lookup_device(&right); negotiated_intervals[left.conn.handle] = 24; ready_switch2(left); require(scanning_enabled && background_scan_parameters && !classic_scanning_enabled && incoming_connections == (SWITCH_PICO_ENABLE_CLASSIC != 0) && negotiated_intervals[left.conn.handle] == 6 && platform_on_device_discovered(right.conn.btaddr, "mate", 0, 0) == UNI_ERROR_SUCCESS, "ready solo must find its remembered mate using only low-duty BLE"); platform_on_device_connected(&right); require(!scanning_enabled && !classic_scanning_enabled, "pending mate setup must stop discovery"); require(platform_on_device_ready(&right) == UNI_ERROR_SUCCESS && !scanning_enabled && !classic_scanning_enabled, "a complete pair must not continue background discovery"); platform_on_device_disconnected(&right); require(scanning_enabled && background_scan_parameters && !classic_scanning_enabled, "departed mate must restore low-duty BLE discovery"); bluepad32_input_backend_open_pairing_window(); process_rumble_timer(&g_rumble_timer); require_selected_radio(true, true); require(!background_scan_parameters, "explicit pairing must replace background scan timing"); now_ms = g_pairing_window_deadline_ms; process_rumble_timer(&g_rumble_timer); require(scanning_enabled && background_scan_parameters && !classic_scanning_enabled, "pairing expiry must restore only remembered-mate BLE discovery"); platform_on_device_disconnected(&left); require_selected_radio(true, true); require(!background_scan_parameters, "last disconnect must restore foreground scan timing"); } Bluepad32Switch2WakeStatus usb_wake_status() { Bluepad32Switch2WakeStatus status{}; bluepad32_input_backend_switch2_wake_snapshot(&status); return status; } void test_usb_switch2_wake() { using State = Bluepad32Switch2WakeState; require(usb_wake_status().state == State::kIdle && !bluepad32_input_backend_request_switch2_wake(0) && !bluepad32_input_backend_request_switch2_wake(0x80000000u), "invalid IDs must not admit wake work"); require(bluepad32_input_backend_request_switch2_wake(17) && usb_wake_status().request_id == 17 && usb_wake_status().state == State::kQueued && !usb_wake_status().configured && switch2_wake_initializations == 0 && switch2_wake_requests == 0, "USB must publish correlation before radio startup without running radio work"); require(bluepad32_input_backend_request_switch2_wake(17) && !bluepad32_input_backend_request_switch2_wake(18), "queued work must be idempotent and cannot be superseded"); start_backend(); process_rumble_timer(&g_rumble_timer); auto status = usb_wake_status(); if (!SWITCH_PICO_ENABLE_BLE) { require(status.request_id == 17 && status.state == State::kUnconfigured && !status.configured && switch2_wake_requests == 0, "Classic-only firmware must report unavailable wake without a radio request"); return; } Bluepad32BackendDiagnostics backend{}; bluepad32_input_backend_diagnostics(&backend); require(backend.active_slots == 0 && status.request_id == 17 && status.state == State::kBroadcasting && status.configured && status.busy && status.accepted_requests == 1 && switch2_wake_requests == 1, "the owning timer must start exactly one real wake request without a controller"); require(bluepad32_input_backend_request_switch2_wake(17) && !bluepad32_input_backend_request_switch2_wake(18), "broadcasting work must remain correlated and bounded"); process_rumble_timer(&g_rumble_timer); require(switch2_wake_requests == 1 && usb_wake_status().state == State::kBroadcasting, "polling cannot replay an active wake burst"); wake_diagnostics.busy = false; ++wake_diagnostics.completed_bursts; process_rumble_timer(&g_rumble_timer); require(usb_wake_status().state == State::kComplete && usb_wake_status().completed_bursts == 1 && bluepad32_input_backend_request_switch2_wake(17), "the originating request must retain its completion and be retry-safe"); // An unrelated chord after completion must not change the retained result. require(switch2_wake_request(), "the chord fixture must start a separate burst"); wake_diagnostics.busy = false; ++wake_diagnostics.completed_bursts; process_rumble_timer(&g_rumble_timer); require(usb_wake_status().request_id == 17 && usb_wake_status().state == State::kComplete && usb_wake_status().accepted_requests == 1 && usb_wake_status().completed_bursts == 1, "terminal USB snapshots must not be rewritten by a later chord"); require(bluepad32_input_backend_request_switch2_wake(18), "a new ID must replace a completed request"); process_rumble_timer(&g_rumble_timer); require(usb_wake_status().state == State::kBroadcasting && usb_wake_status().completed_bursts == 2, "prior chord completions must not complete a new USB request"); // Real wake cleanup may increment both counters between backend ticks. ++wake_diagnostics.failures; ++wake_diagnostics.completed_bursts; wake_diagnostics.busy = false; process_rumble_timer(&g_rumble_timer); const int attempts_after_failure = switch2_wake_requests; require(usb_wake_status().request_id == 18 && usb_wake_status().state == State::kFailed && bluepad32_input_backend_request_switch2_wake(18), "failed HCI work must not become Complete when cleanup finishes"); process_rumble_timer(&g_rumble_timer); require(switch2_wake_requests == attempts_after_failure && usb_wake_status().state == State::kFailed, "failed requests must remain terminal without automatic retry"); require(switch2_wake_request(), "the chord fixture must own the radio"); const int attempts_with_chord = switch2_wake_requests; require(bluepad32_input_backend_request_switch2_wake(19), "USB must defer the chord-busy decision to the radio owner"); process_rumble_timer(&g_rumble_timer); require(usb_wake_status().request_id == 19 && usb_wake_status().state == State::kBusy && switch2_wake_requests == attempts_with_chord, "a chord-owned burst must reject USB without another broadcast"); wake_diagnostics.busy = false; ++wake_diagnostics.completed_bursts; process_rumble_timer(&g_rumble_timer); require(usb_wake_status().state == State::kBusy, "a chord completion must never complete a rejected USB request"); wake_diagnostics.configured = false; require(bluepad32_input_backend_request_switch2_wake(20), "availability is decided by the radio owner, not stale USB diagnostics"); process_rumble_timer(&g_rumble_timer); require(usb_wake_status().state == State::kUnconfigured && switch2_wake_requests == attempts_with_chord, "missing wake configuration must be explicit and never start a burst"); wake_diagnostics.configured = true; wake_dispatch_available = false; require(bluepad32_input_backend_request_switch2_wake(21), "USB must admit a radio-owner dispatch decision"); process_rumble_timer(&g_rumble_timer); require(usb_wake_status().state == State::kFailed, "an idle but failed radio machine must report Failed, not completion"); } void test_usb_wake_chord_correlation() { using State = Bluepad32Switch2WakeState; start_pairing_backend(); auto controller = device(0); require(platform_on_device_ready(&controller) == UNI_ERROR_SUCCESS, "chord correlation requires a live controller"); require(bluepad32_input_backend_request_switch2_wake(1), "USB wake must queue before the chord"); process_rumble_timer(&g_rumble_timer); wake_diagnostics.busy = false; ++wake_diagnostics.completed_bursts; uni_controller_t input{}; input.klass = UNI_CONTROLLER_CLASS_GAMEPAD; input.gamepad.buttons = BUTTON_SHOULDER_L | BUTTON_SHOULDER_R; input.gamepad.misc_buttons = MISC_BUTTON_SYSTEM; platform_on_controller_data(&controller, &input); // No backend timer ran between USB completion and the new chord. ++wake_diagnostics.failures; wake_diagnostics.busy = false; process_rumble_timer(&g_rumble_timer); require(usb_wake_status().request_id == 1 && usb_wake_status().state == State::kComplete && usb_wake_status().failures == 0 && wake_diagnostics.accepted_requests == 2, "a subsequent chord failure must not be attributed to the completed USB burst"); input.gamepad.buttons = 0; input.gamepad.misc_buttons = 0; platform_on_controller_data(&controller, &input); require(bluepad32_input_backend_request_switch2_wake(2), "a second USB request must queue"); input.gamepad.buttons = BUTTON_SHOULDER_L | BUTTON_SHOULDER_R; input.gamepad.misc_buttons = MISC_BUTTON_SYSTEM; platform_on_controller_data(&controller, &input); require(usb_wake_status().state == State::kQueued && wake_diagnostics.accepted_requests == 3, "queued USB work must never preempt a rising chord"); process_rumble_timer(&g_rumble_timer); require(usb_wake_status().request_id == 2 && usb_wake_status().state == State::kBusy && wake_diagnostics.accepted_requests == 3, "the chord must retain its burst while the queued USB request becomes Busy"); } } // namespace int main(int argc, char** argv) { require(argc == 2, "scenario argument required"); const std::string scenario = argv[1]; if (scenario == "usb-wake") { test_usb_switch2_wake(); return 0; } if (scenario == "usb-wake-chord") { test_usb_wake_chord_correlation(); return 0; } #ifdef SWITCH2_BRIDGE_WII_INPUT if (scenario == "wii-bridge-sensors") { test_wii_bridge_sensors(); return 0; } if (scenario == "wii-bridge-cues") { test_wii_bridge_cues(); return 0; } if (scenario == "wii-bridge-cue-races") { test_wii_bridge_cue_races(); return 0; } #endif if (scenario == "transport-policy") { test_transport_mode_policy(); return 0; } if (scenario == "transport-background") { test_transport_background_scan(); return 0; } #if defined(SWITCH_PICO_HAPTICS_EXPERIMENT) && defined(SWITCH_PICO_USB_OUTPUT_MODES) if (scenario == "native-stateful") { test_native_stateful_routing(); return 0; } if (scenario == "native-second-slot") { test_native_second_slot_selection(); return 0; } #endif if (scenario == "wii-orientation") { test_wii_orientation(); return 0; } if (scenario == "wii-orientation-races") { test_wii_orientation_races(); return 0; } if (scenario == "wii-accelerometers") { test_wii_accelerometer_streams(); return 0; } if (scenario == "switch2-hd-pro") { test_switch2_hd_pro(); } else if (scenario == "switch2-gesture-timing") { test_switch2_gesture_timing(); } else if (scenario == "switch2-gesture-slot-left-first") { test_switch2_gesture_slot_order(true, 0); } else if (scenario == "switch2-gesture-slot-right-first") { test_switch2_gesture_slot_order(false, 0); } else if (scenario == "switch2-gesture-slot-occupied") { test_switch2_gesture_slot_order(true, 1); } else if (scenario == "switch2-gesture-stale") { test_switch2_gesture_stale(); } else if (scenario == "switch2-gesture-clock-wrap") { test_switch2_gesture_clock_wrap(); } else if (scenario == "switch2-gesture-masking-epochs") { test_switch2_gesture_masking_epochs(); } else if (scenario == "switch2-gesture-override-lifetime") { test_switch2_gesture_override_lifetime(); } else if (scenario == "switch2-gesture-two-pairs") { test_switch2_gesture_two_pairs(); } else if (scenario == "switch2-gesture-ambiguous") { test_switch2_gesture_ambiguous(); } else if (scenario == "switch2-gesture-seed-failure") { test_switch2_gesture_seed_failure(); } else if (scenario == "switch2-gesture-device-scope") { test_switch2_gesture_device_scope(); } else if (scenario == "switch2-hd-solo-left") { test_switch2_hd_solo(false); } else if (scenario == "switch2-hd-solo-right") { test_switch2_hd_solo(true); } else if (scenario == "switch2-hd-pair") { test_switch2_hd_pair(); } else if (scenario == "switch2-hd-overflow") { test_switch2_hd_overflow(); } else if (scenario == "switch2-hd-epochs") { test_switch2_hd_epochs(); } else if (scenario == "switch2-hd-feedback") { test_switch2_hd_feedback(); } else if (scenario == "switch2-individual-core-start") { test_switch2_individual_core_start(); } else if (scenario == "switch2-individual-forward") { test_switch2_individual_boot(false); } else if (scenario == "switch2-individual-reverse") { test_switch2_individual_boot(true); } else if (scenario == "switch2-mode-forward") { test_switch2_mode_roundtrip(false); } else if (scenario == "switch2-mode-reverse") { test_switch2_mode_roundtrip(true); } else if (scenario == "switch2-mode-two-pairs") { test_switch2_mode_two_pairs(); } else if (scenario == "switch2-mode-seed-failure") { test_switch2_live_pair_failure(false); } else if (scenario == "switch2-mode-identity-failure") { test_switch2_live_pair_failure(true); } else if (scenario == "switch2-forward") { test_switch2_pair_lifecycle(false); } else if (scenario == "switch2-reverse") { test_switch2_pair_lifecycle(true); } else if (scenario == "switch2-multiple-pairs") { test_switch2_multiple_pairs(); } else if (scenario == "switch2-pair-failure-left") { test_switch2_pair_admission_failure(false); } else if (scenario == "switch2-pair-failure-right") { test_switch2_pair_admission_failure(true); } else if (scenario == "switch2-pair-replacement") { test_switch2_pair_member_replacement(); } else if (scenario == "switch2-admission") { test_switch2_admission(); } else if (scenario == "switch2-radio-policy") { test_switch2_radio_policy(false); } else if (scenario == "switch2-radio-individual") { test_switch2_radio_policy(true); } else if (scenario == "switch2-radio-settling") { test_switch2_radio_settling(); } else if (scenario == "switch2-mate-reconnect") { test_switch2_mate_reconnect(); } else if (scenario == "switch2-mate-pending") { test_switch2_mate_pending(); } else if (scenario == "switch2-mate-pairing-window") { test_switch2_mate_pairing_window(); } else if (scenario == "switch2-pairing-inventory") { test_switch2_pairing_inventory(); } else if (scenario == "ready-forward") { test_ready_order(false); } else if (scenario == "ready-reverse") { test_ready_order(true); } else if (scenario == "rejections") { test_rejections(); } else if (scenario == "lifecycle") { test_independent_lifecycle(); } else if (scenario == "pairing-policy") { test_pairing_window_policy(); } else if (scenario == "slot-lighting") { test_slot_lighting(); } else if (scenario == "stateful-rumble") { test_stateful_host_rumble_restore(); } else if (scenario == "profile-chord-raw") { test_profile_chord_remains_raw(); } else if (scenario == "profile-feedback") { test_profile_feedback_scheduler(); } else if (scenario == "motion-hotkey") { test_motion_toggle_action(); } else if (scenario == "analog-state") { test_protocol_neutral_analog_state(); } else if (scenario == "rumble-mode") { test_host_rumble_mode_duration(); } else if (scenario == "xbox-rumble") { test_xbox_trigger_rumble(); } else if (scenario == "clear-pairings") { test_clear_pairings(); } else if (scenario == "configuration-timer") { test_configuration_timer_rearms_before_storage_work(); } else if (scenario == "flash-core-start") { test_flash_core_start_contract(); } else if (scenario == "wake-identity-gate") { test_wake_identity_gates_connections(); } else if (scenario == "system-wake") { test_system_button_wake_trigger(); } else if (scenario == "flash-core-failure") { test_flash_core_init_fatal(); } else { require(false, "unknown scenario"); } return 0; }