diff --git a/README.md b/README.md index 1ad5c6d..ccbbe54 100644 --- a/README.md +++ b/README.md @@ -50,18 +50,17 @@ 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. Hold the Pico's BOOTSEL button for about two seconds, until the onboard LED starts double-blinking. This opens a 60-second pairing window. -4. Put a controller into Bluetooth pairing mode: +3. 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. -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 window by 60 seconds from that point. +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. Pairing order determines the initial USB slot assignment. Up to four controllers map 1:1 to the four emulated Switch Pro Controller interfaces. -Outside the BOOTSEL-open window, Bluetooth discovery and incoming connections are disabled. The Pico does not scan for or reconnect disconnected controllers while locked. Pairing keys still persist, but reconnecting a previously paired controller also requires opening the BOOTSEL window before pressing its normal power button. +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. ### LED meanings and device state @@ -69,15 +68,15 @@ The Pico 2 W onboard LED reports the overall Bluetooth state: - **Double blink**: the bounded pairing window is open. - **Fast blink**: a controller connection is still completing its handshake. - **Solid**: at least one controller is active. -- **Slow blink**: no controller is active and pairing is locked. +- **Slow blink**: no controller is active; Bluetooth discovery and autoconnect are running. - **Solid immediately after boot that never transitions**: Bluepad32 initialization did not complete; check firmware flashing and UART logs. ### Managing controller disconnect and reconnect - **Disconnect a controller**: its slot immediately publishes neutral buttons, sticks, and motion. Other connected controllers are unaffected. -- **Reconnect a paired controller**: hold BOOTSEL until the LED double-blinks, then power on the controller normally. -- **Pair a new controller**: hold BOOTSEL until the LED double-blinks, then put the controller into its explicit Bluetooth pairing mode. -- **Pairing window expires**: scanning and incoming connections stop; already connected controllers remain connected. +- **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. ### Per-controller ABXY layout diff --git a/bluepad32_input_backend.cpp b/bluepad32_input_backend.cpp index c3c9b07..5a21512 100644 --- a/bluepad32_input_backend.cpp +++ b/bluepad32_input_backend.cpp @@ -77,7 +77,6 @@ enum class ConnectionStatus { enum class ConnectionPolicyState { Uninitialized, Open, - Locked, Paused, FailedClosed, }; @@ -467,15 +466,12 @@ bool update_pairing_window(uint32_t now_ms) { return false; } -void apply_connection_policy(uint32_t now_ms) { +void apply_connection_policy() { const bool free_slot = has_free_slot(); - const bool pairing_open = pairing_window_active_at(now_ms); if ((!free_slot && g_connection_policy_state == ConnectionPolicyState::Paused) || - (free_slot && pairing_open && - g_connection_policy_state == ConnectionPolicyState::Open) || - (free_slot && !pairing_open && - g_connection_policy_state == ConnectionPolicyState::Locked)) { + (free_slot && + g_connection_policy_state == ConnectionPolicyState::Open)) { return; } @@ -487,13 +483,8 @@ void apply_connection_policy(uint32_t now_ms) { return; } - if (!pairing_open) { - g_connection_policy_state = ConnectionPolicyState::Locked; - return; - } - - // Use Bluepad32's normal pairing/autoconnect path while the physical - // BOOTSEL gesture has explicitly opened the pairing window. + // Bluepad32's normal scan/autoconnect path handles both remembered + // controllers powering on and controllers in explicit pairing mode. uni_bt_allow_incoming_connections(true); uni_bt_start_scanning_and_autoconnect_unsafe(); g_connection_policy_state = ConnectionPolicyState::Open; @@ -523,9 +514,7 @@ void update_status_led() { void process_rumble_timer(btstack_timer_source_t* timer) { const uint32_t now_ms = btstack_run_loop_get_time_ms(); - if (update_pairing_window(now_ms)) { - apply_connection_policy(now_ms); - } + update_pairing_window(now_ms); for (uint8_t slot_index = 0; slot_index < kSlotCount; ++slot_index) { RumbleEnvelope envelope{}; @@ -591,7 +580,7 @@ void recompute_connection_status() { update_pairing_window(now_ms); g_connection_status = compute_connection_status(); g_status_led_tick = 0; - apply_connection_policy(now_ms); + apply_connection_policy(); } void platform_init(int argc, const char** argv) { @@ -600,8 +589,7 @@ void platform_init(int argc, const char** argv) { } void platform_on_init_complete() { - // Discovery and incoming connections remain disabled until BOOTSEL opens - // the bounded pairing window. + // 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); btstack_run_loop_add_timer(&g_rumble_timer); diff --git a/firmware/switch-pico-aio.elf b/firmware/switch-pico-aio.elf index 562ab9b..f13722e 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 9ce74a1..1ed7980 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 86bcadd..7bd2b12 100644 --- a/tests/bluepad32_backend_lifecycle_test.cpp +++ b/tests/bluepad32_backend_lifecycle_test.cpp @@ -186,10 +186,9 @@ namespace { void start_backend() { bluepad32_input_backend_init(); platform_on_init_complete(); - require(!incoming_connections, - "initialization must keep incoming connections closed"); - require(scan_starts == 0, - "initialization must not scan before a BOOTSEL request"); + require(incoming_connections && scanning_enabled && + classic_scanning_enabled && scan_starts == 1, + "initialization must start Bluepad32 autoconnect"); } void start_pairing_backend() { start_backend(); @@ -591,20 +590,20 @@ void test_pairing_window_policy() { "discovery must remain closed before backend initialization"); start_backend(); - require(g_connection_policy_state == ConnectionPolicyState::Locked && - !classic_scanning_enabled && !scanning_enabled && - !incoming_connections, - "boot must disable all discovery and incoming connections"); + 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_IGNORE_DEVICE, - "locked policy must reject every discovery"); + UNI_ERROR_SUCCESS, + "boot policy must accept a discovered controller"); - uni_hid_device_t rejected = device(0); - platform_on_device_connected(&rejected); - require(device_disconnect_calls == 1 && - last_disconnected_device == &rejected && - g_slots[0].device == nullptr, - "locked policy must disconnect every incoming controller"); + 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, @@ -615,17 +614,7 @@ void test_pairing_window_policy() { g_connection_policy_state == ConnectionPolicyState::Open && classic_scanning_enabled && scanning_enabled && incoming_connections, - "BOOTSEL window must run Bluepad32's normal pairing scan"); - require(platform_on_device_discovered(address, "controller", 0, 0) == - UNI_ERROR_SUCCESS, - "open pairing window must accept a discovered controller"); - - uni_hid_device_t paired = device(0); - platform_on_device_connected(&paired); - require(device_disconnect_calls == 1 && - g_slots[0].device == &paired, - "open pairing window must retain a connected controller"); - platform_on_device_disconnected(&paired); + "BOOTSEL must expose pairing indication without interrupting scan"); tick_backend_timer(20); require(!observed_status_led_on, @@ -652,21 +641,21 @@ void test_pairing_window_policy() { require(g_connection_policy_state == ConnectionPolicyState::Paused && g_pairing_window_open && !scanning_enabled && !classic_scanning_enabled && !incoming_connections, - "full slots must pause pairing without closing the deadline"); + "full slots must pause scanning without closing the deadline"); now_ms = 90000; process_rumble_timer(&g_rumble_timer); require(!g_pairing_window_open && g_connection_policy_state == ConnectionPolicyState::Paused, - "deadline must expire while slots remain full"); + "pairing indication must expire while slots remain full"); platform_on_device_disconnected(&devices[3]); - require(g_connection_policy_state == ConnectionPolicyState::Locked && - !classic_scanning_enabled && !scanning_enabled && - !incoming_connections, - "a freed slot after expiry must remain closed"); + require(g_connection_policy_state == ConnectionPolicyState::Open && + classic_scanning_enabled && scanning_enabled && + incoming_connections, + "a freed slot must resume autoconnect after pairing indication expires"); require(platform_on_device_discovered(address, "controller", 0, 0) == - UNI_ERROR_IGNORE_DEVICE, - "expired pairing policy must reject discovery"); + UNI_ERROR_SUCCESS, + "resumed autoconnect must accept a discovered controller"); }