diff --git a/README.md b/README.md index ccbbe54..e248ac5 100644 --- a/README.md +++ b/README.md @@ -50,22 +50,23 @@ Both `build.py --aio` and direct AIO CMake configuration apply `patches/bluepad3 1. Flash and connect the Pico 2 W to the Switch. 2. Enable `System Settings → Controllers and Sensors → Pro Controller Wired Communication`. -3. Put a controller into Bluetooth pairing mode: +3. Hold BOOTSEL for about two seconds until the onboard LED starts double-blinking. This enables new Bluetooth authentication for 60 seconds. +4. Put a controller into Bluetooth pairing mode: - DualSense: hold Create + PS. - DualShock 4: hold Share + PS. - Switch Pro: press its sync button. - Xbox Bluetooth controller: hold its pair button. - 8BitDo: use a Bluetooth mode supported by Bluepad32; use Switch/S mode when motion is required. -4. Wait for the controller's player light to settle. Repeat step 3 for additional controllers while slots remain free. Optionally hold BOOTSEL for about two seconds to show the 60-second double-blink pairing indication. +5. Wait for the controller's player light to settle. Repeat step 4 for additional controllers while the window remains open. Holding BOOTSEL again extends the deadline by 60 seconds from that point. Pairing order determines the initial USB slot assignment. Up to four controllers map 1:1 to the four emulated Switch Pro Controller interfaces. -While a slot is free, the Pico continuously runs Bluepad32's normal Bluetooth discovery and autoconnect path. Pairing keys persist across Pico power cycles, so reconnect a previously paired controller by pressing its normal Home, PS, or Xbox power button; BOOTSEL is not required. A controller placed into explicit Bluetooth pairing mode can also pair during this scan. +While a slot is free, the Pico continuously runs Bluepad32's normal Bluetooth discovery and autoconnect path. Pairing keys persist across Pico power cycles, so reconnect a previously paired controller by pressing its normal Home, PS, or Xbox power button; BOOTSEL is not required. Outside the BOOTSEL window, BTstack remains non-bondable, rejects new Classic SSP or legacy PIN authentication, and disables every BLE STK generation method. A controller in explicit pairing mode therefore cannot create a new Classic or BLE bond while the window is closed. ### LED meanings and device state The Pico 2 W onboard LED reports the overall Bluetooth state: -- **Double blink**: the bounded pairing window is open. +- **Double blink**: new controller authentication is enabled for the bounded pairing window. - **Fast blink**: a controller connection is still completing its handshake. - **Solid**: at least one controller is active. - **Slow blink**: no controller is active; Bluetooth discovery and autoconnect are running. @@ -75,8 +76,8 @@ The Pico 2 W onboard LED reports the overall Bluetooth state: - **Disconnect a controller**: its slot immediately publishes neutral buttons, sticks, and motion. Other connected controllers are unaffected. - **Reconnect a paired controller**: power it on normally with its Home, PS, or Xbox button. -- **Pair a new controller**: put it into its explicit Bluetooth pairing mode while a slot is free. BOOTSEL is optional and only changes the LED to the bounded double-blink indication. -- **Pairing indication expires**: the LED returns to its normal state; scanning continues while a slot is free. +- **Pair a new controller**: hold BOOTSEL until the LED double-blinks, then put the controller into its explicit Bluetooth pairing mode. +- **Pairing window expires**: new authentication is disabled; discovery and remembered-controller autoconnect continue while a slot is free. ### Per-controller ABXY layout diff --git a/bluepad32_input_backend.cpp b/bluepad32_input_backend.cpp index 5a21512..bb8f160 100644 --- a/bluepad32_input_backend.cpp +++ b/bluepad32_input_backend.cpp @@ -24,6 +24,11 @@ constexpr uint16_t kRumbleDurationMs = 50; constexpr uint32_t kRumblePollIntervalMs = 5; constexpr uint8_t kSlotCount = BLUEPAD32_INPUT_BACKEND_SLOT_COUNT; constexpr uint32_t kPairingWindowDurationMs = 60000; +constexpr uint8_t kAllBlePairingMethods = + SM_STK_GENERATION_METHOD_JUST_WORKS | + SM_STK_GENERATION_METHOD_OOB | + SM_STK_GENERATION_METHOD_PASSKEY | + SM_STK_GENERATION_METHOD_NUMERIC_COMPARISON; constexpr uint32_t kAbxyHotkeyButtonMask = SWITCH_ABXY_HOTKEY_BUTTON_MASK; constexpr uint32_t kAbxyHotkeyMiscMask = SWITCH_ABXY_HOTKEY_MISC_MASK; @@ -126,6 +131,7 @@ bool g_started = false; // These fields are only read or written by Core 1 / BTstack. btstack_timer_source_t g_rumble_timer{}; ConnectionStatus g_connection_status = ConnectionStatus::Initializing; +btstack_packet_callback_registration_t g_pairing_event_callback{}; ConnectionPolicyState g_connection_policy_state = ConnectionPolicyState::Uninitialized; uint32_t g_pairing_window_deadline_ms = 0; @@ -446,6 +452,38 @@ bool pairing_window_active_at(uint32_t now_ms) { static_cast(now_ms - g_pairing_window_deadline_ms) < 0; } +void handle_pairing_hci_event(uint8_t packet_type, uint16_t channel, + uint8_t* packet, uint16_t size) { + (void)channel; + if (packet_type != HCI_EVENT_PACKET || packet == nullptr || size < 8) { + return; + } + + bd_addr_t address{}; + const bool pairing_open = + pairing_window_active_at(btstack_run_loop_get_time_ms()); + switch (hci_event_packet_get_type(packet)) { + case HCI_EVENT_USER_CONFIRMATION_REQUEST: + hci_event_user_confirmation_request_get_bd_addr(packet, address); + if (pairing_open) { + gap_ssp_confirmation_response(address); + } else { + gap_ssp_confirmation_negative(address); + } + break; + case HCI_EVENT_USER_PASSKEY_REQUEST: + hci_event_user_passkey_request_get_bd_addr(packet, address); + if (pairing_open) { + gap_ssp_passkey_response(address, 0); + } else { + gap_ssp_passkey_negative(address); + } + break; + default: + break; + } +} + bool update_pairing_window(uint32_t now_ms) { critical_section_enter_blocking(&g_state_lock); const bool requested = g_pairing_window_requested; @@ -455,12 +493,16 @@ bool update_pairing_window(uint32_t now_ms) { if (requested) { g_pairing_window_open = true; g_pairing_window_deadline_ms = now_ms + kPairingWindowDurationMs; + gap_set_bondable_mode(true); + sm_set_accepted_stk_generation_methods(kAllBlePairingMethods); g_status_led_tick = 0; return true; } if (g_pairing_window_open && !pairing_window_active_at(now_ms)) { g_pairing_window_open = false; + sm_set_accepted_stk_generation_methods(0); g_status_led_tick = 0; + gap_set_bondable_mode(false); return true; } return false; @@ -589,6 +631,11 @@ void platform_init(int argc, const char** argv) { } void platform_on_init_complete() { + gap_set_bondable_mode(false); + sm_set_accepted_stk_generation_methods(0); + gap_ssp_set_auto_accept(false); + g_pairing_event_callback.callback = handle_pairing_hci_event; + hci_add_event_handler(&g_pairing_event_callback); // Keep Bluepad32 autoconnect active whenever at least one slot is free. btstack_run_loop_set_timer_handler(&g_rumble_timer, process_rumble_timer); btstack_run_loop_set_timer(&g_rumble_timer, kRumblePollIntervalMs); diff --git a/firmware/switch-pico-aio.elf b/firmware/switch-pico-aio.elf index f13722e..9b97169 100755 Binary files a/firmware/switch-pico-aio.elf and b/firmware/switch-pico-aio.elf differ diff --git a/firmware/switch-pico-aio.uf2 b/firmware/switch-pico-aio.uf2 index 1ed7980..675a264 100644 Binary files a/firmware/switch-pico-aio.uf2 and b/firmware/switch-pico-aio.uf2 differ diff --git a/tests/bluepad32_backend_lifecycle_test.cpp b/tests/bluepad32_backend_lifecycle_test.cpp index 7bd2b12..e1ab6b8 100644 --- a/tests/bluepad32_backend_lifecycle_test.cpp +++ b/tests/bluepad32_backend_lifecycle_test.cpp @@ -20,6 +20,14 @@ uni_platform* installed_platform = nullptr; bool observed_status_led_on = false; int observed_status_led_writes = 0; uint32_t now_ms = 0; +bool bondable = true; +bool ssp_auto_accept = true; +uint8_t accepted_stk_methods = 0xff; +btstack_packet_handler_t pairing_event_handler = nullptr; +int confirmation_accepts = 0; +int confirmation_rejections = 0; +int passkey_accepts = 0; +int passkey_rejections = 0; bool flash_core_init_result = true; int flash_core_init_calls = 0; @@ -126,6 +134,63 @@ void uni_bt_stop_scanning_unsafe() { uni_bt_bredr_scan_stop(); uni_bt_le_scan_stop(); } +void gap_set_bondable_mode(int enabled) { + bondable = enabled != 0; +} + +void gap_ssp_set_auto_accept(int auto_accept) { + ssp_auto_accept = auto_accept != 0; +} +void sm_set_accepted_stk_generation_methods(uint8_t methods) { + accepted_stk_methods = methods; +} + + +int gap_ssp_confirmation_response(const bd_addr_t) { + ++confirmation_accepts; + return 0; +} + +int gap_ssp_confirmation_negative(const bd_addr_t) { + ++confirmation_rejections; + return 0; +} + +int gap_ssp_passkey_response(const bd_addr_t, uint32_t) { + ++passkey_accepts; + return 0; +} + +int gap_ssp_passkey_negative(const bd_addr_t) { + ++passkey_rejections; + return 0; +} + +void hci_add_event_handler( + btstack_packet_callback_registration_t* callback_handler) { + pairing_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); +} + void uni_platform_set_custom(uni_platform* platform) { installed_platform = platform; @@ -187,8 +252,10 @@ void start_backend() { bluepad32_input_backend_init(); platform_on_init_complete(); require(incoming_connections && scanning_enabled && - classic_scanning_enabled && scan_starts == 1, - "initialization must start Bluepad32 autoconnect"); + classic_scanning_enabled && scan_starts == 1 && + !bondable && accepted_stk_methods == 0 && + !ssp_auto_accept && pairing_event_handler != nullptr, + "initialization must scan while keeping new pairing disabled"); } void start_pairing_backend() { start_backend(); @@ -199,6 +266,11 @@ void start_pairing_backend() { incoming_connections, "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)); +} + @@ -597,6 +669,12 @@ void test_pairing_window_policy() { 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); @@ -609,12 +687,17 @@ void test_pairing_window_policy() { require(!g_pairing_window_open, "Core0 request must wait for Core1 consumption"); process_rumble_timer(&g_rumble_timer); - require(g_pairing_window_open && + 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 expose pairing indication without interrupting scan"); + "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, @@ -645,9 +728,10 @@ void test_pairing_window_policy() { now_ms = 90000; process_rumble_timer(&g_rumble_timer); - require(!g_pairing_window_open && + require(!g_pairing_window_open && !bondable && + accepted_stk_methods == 0 && g_connection_policy_state == ConnectionPolicyState::Paused, - "pairing indication must expire while slots remain full"); + "Classic and BLE pairing authentication must close at the deadline"); platform_on_device_disconnected(&devices[3]); require(g_connection_policy_state == ConnectionPolicyState::Open && classic_scanning_enabled && scanning_enabled && diff --git a/tests/bluepad32_native_stubs/uni.h b/tests/bluepad32_native_stubs/uni.h index d8ee451..021dacf 100644 --- a/tests/bluepad32_native_stubs/uni.h +++ b/tests/bluepad32_native_stubs/uni.h @@ -26,6 +26,13 @@ struct btstack_link_key_iterator_t { enum { ERROR_CODE_SUCCESS = 0, + HCI_EVENT_PACKET = 4, + HCI_EVENT_USER_CONFIRMATION_REQUEST = 0x33, + HCI_EVENT_USER_PASSKEY_REQUEST = 0x34, + SM_STK_GENERATION_METHOD_JUST_WORKS = 0x01, + SM_STK_GENERATION_METHOD_OOB = 0x02, + SM_STK_GENERATION_METHOD_PASSKEY = 0x04, + SM_STK_GENERATION_METHOD_NUMERIC_COMPARISON = 0x08, }; typedef int uni_property_idx_t; typedef int uni_platform_oob_event_t; @@ -80,6 +87,12 @@ struct uni_controller_t { }; struct uni_hid_device_t; +typedef void (*btstack_packet_handler_t)(uint8_t, uint16_t, uint8_t*, + uint16_t); +struct btstack_packet_callback_registration_t { + void* item; + btstack_packet_handler_t callback; +}; typedef void (*uni_play_dual_rumble_t)(uni_hid_device_t*, uint16_t, uint16_t, uint8_t, uint8_t); typedef void (*uni_set_player_leds_t)(uni_hid_device_t*, uint8_t); @@ -148,5 +161,20 @@ void uni_bt_bredr_scan_start(); void uni_bt_bredr_scan_stop(); void uni_bt_le_scan_start(); void uni_bt_le_scan_stop(); +void gap_set_bondable_mode(int enabled); +void gap_ssp_set_auto_accept(int auto_accept); +void sm_set_accepted_stk_generation_methods( + uint8_t accepted_stk_generation_methods); +int gap_ssp_confirmation_response(const bd_addr_t address); +int gap_ssp_confirmation_negative(const bd_addr_t address); +int gap_ssp_passkey_response(const bd_addr_t address, uint32_t passkey); +int gap_ssp_passkey_negative(const bd_addr_t address); +void hci_add_event_handler( + btstack_packet_callback_registration_t* callback_handler); +uint8_t hci_event_packet_get_type(const uint8_t* packet); +void hci_event_user_confirmation_request_get_bd_addr( + const uint8_t* packet, bd_addr_t address); +void hci_event_user_passkey_request_get_bd_addr( + const uint8_t* packet, bd_addr_t address); void uni_platform_set_custom(uni_platform* platform); int uni_init(int argc, const char** argv);