Allow controller reconnect without BOOTSEL
This commit is contained in:
parent
81f6ac1236
commit
146c377311
5 changed files with 39 additions and 63 deletions
15
README.md
15
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue