Gate new controller pairing behind BOOTSEL
This commit is contained in:
parent
146c377311
commit
74a62f035b
6 changed files with 172 additions and 12 deletions
13
README.md
13
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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<int32_t>(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);
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue