Add Pico 2 W Bluetooth status diagnostics

Expose scanning, connecting, ready, and nonzero rumble activity on the onboard LED; publish the physically verified AIO firmware.
This commit is contained in:
Joey Yakimowich-Payne 2026-08-30 16:21:24 -06:00
commit 7bd3244006
4 changed files with 61 additions and 0 deletions

View file

@ -54,6 +54,9 @@ Both `build.py --aio` and direct AIO CMake configuration apply `patches/bluepad3
- 8BitDo: use a Bluetooth mode supported by Bluepad32; use Switch/S mode when motion is required. - 8BitDo: use a Bluetooth mode supported by Bluepad32; use Switch/S mode when motion is required.
4. Wait for the controller to connect. Pairing keys persist across Pico reboots. 4. Wait for the controller to connect. Pairing keys persist across Pico reboots.
The Pico 2 W onboard LED reports Bluetooth state: a slow 0.5-second blink means scanning, a fast 0.1-second blink means a controller connected but is not ready, and solid means the controller is ready. A solid LED immediately after boot that never starts blinking indicates Bluepad32 initialization did not complete.
While a controller is ready, a brief dark pulse indicates that a nonzero decoded rumble packet reached the wireless backend.
Only one wireless controller owns the emulated Pro Controller. Turn off or disconnect it before pairing another; scanning resumes automatically after disconnect. A disconnect immediately publishes neutral buttons, sticks, and motion. Only one wireless controller owns the emulated Pro Controller. Turn off or disconnect it before pairing another; scanning resumes automatically after disconnect. A disconnect immediately publishes neutral buttons, sticks, and motion.
### Controller capabilities ### Controller capabilities

View file

@ -21,6 +21,14 @@ constexpr int32_t kTriggerThreshold = (kTriggerMaximum * 35) / 100;
constexpr uint16_t kRumbleDurationMs = 50; constexpr uint16_t kRumbleDurationMs = 50;
constexpr uint32_t kRumblePollIntervalMs = 5; constexpr uint32_t kRumblePollIntervalMs = 5;
constexpr uint kRumbleQueueDepth = 8; constexpr uint kRumbleQueueDepth = 8;
constexpr uint8_t kRumbleActivityLedTicks = 20;
enum class ConnectionStatus {
Initializing,
Scanning,
Connecting,
Ready,
};
critical_section_t g_state_lock; critical_section_t g_state_lock;
queue_t g_rumble_queue; queue_t g_rumble_queue;
@ -37,6 +45,10 @@ bool g_started = false;
// This pointer and the timer are only read or written by Core 1 / BTstack. // This pointer and the timer are only read or written by Core 1 / BTstack.
uni_hid_device_t* g_active_device = nullptr; uni_hid_device_t* g_active_device = nullptr;
btstack_timer_source_t g_rumble_timer{}; btstack_timer_source_t g_rumble_timer{};
ConnectionStatus g_connection_status = ConnectionStatus::Initializing;
uint16_t g_status_led_tick = 0;
bool g_status_led_on = false;
uint8_t g_rumble_activity_led_ticks = 0;
SwitchInputState make_neutral_state() { SwitchInputState make_neutral_state() {
SwitchInputState state{}; SwitchInputState state{};
@ -167,6 +179,34 @@ SwitchInputState map_gamepad(const uni_gamepad_t& gamepad) {
return state; return state;
} }
void update_status_led() {
++g_status_led_tick;
bool led_on = false;
switch (g_connection_status) {
case ConnectionStatus::Initializing:
led_on = true;
break;
case ConnectionStatus::Ready:
if (g_rumble_activity_led_ticks > 0) {
--g_rumble_activity_led_ticks;
led_on = false;
} else {
led_on = true;
}
break;
case ConnectionStatus::Scanning:
led_on = (g_status_led_tick % 200) < 100;
break;
case ConnectionStatus::Connecting:
led_on = (g_status_led_tick % 40) < 20;
break;
}
if (led_on != g_status_led_on) {
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, led_on);
g_status_led_on = led_on;
}
}
void process_rumble_timer(btstack_timer_source_t* timer) { void process_rumble_timer(btstack_timer_source_t* timer) {
SwitchRumbleOutput packet{}; SwitchRumbleOutput packet{};
SwitchRumbleOutput latest{}; SwitchRumbleOutput latest{};
@ -175,6 +215,11 @@ void process_rumble_timer(btstack_timer_source_t* timer) {
latest = packet; latest = packet;
have_packet = true; have_packet = true;
} }
if (have_packet &&
(latest.low_frequency_magnitude != 0 ||
latest.high_frequency_magnitude != 0)) {
g_rumble_activity_led_ticks = kRumbleActivityLedTicks;
}
if (have_packet && g_active_device != nullptr && if (have_packet && g_active_device != nullptr &&
g_active_device->report_parser.play_dual_rumble != nullptr) { g_active_device->report_parser.play_dual_rumble != nullptr) {
@ -182,6 +227,7 @@ void process_rumble_timer(btstack_timer_source_t* timer) {
g_active_device, 0, kRumbleDurationMs, g_active_device, 0, kRumbleDurationMs,
latest.high_frequency_magnitude, latest.low_frequency_magnitude); latest.high_frequency_magnitude, latest.low_frequency_magnitude);
} }
update_status_led();
btstack_run_loop_set_timer(timer, kRumblePollIntervalMs); btstack_run_loop_set_timer(timer, kRumblePollIntervalMs);
btstack_run_loop_add_timer(timer); btstack_run_loop_add_timer(timer);
@ -196,6 +242,8 @@ void platform_on_init_complete() {
btstack_run_loop_set_timer_handler(&g_rumble_timer, process_rumble_timer); btstack_run_loop_set_timer_handler(&g_rumble_timer, process_rumble_timer);
btstack_run_loop_set_timer(&g_rumble_timer, kRumblePollIntervalMs); btstack_run_loop_set_timer(&g_rumble_timer, kRumblePollIntervalMs);
btstack_run_loop_add_timer(&g_rumble_timer); btstack_run_loop_add_timer(&g_rumble_timer);
g_connection_status = ConnectionStatus::Scanning;
g_status_led_tick = 0;
uni_bt_allow_incoming_connections(true); uni_bt_allow_incoming_connections(true);
uni_bt_start_scanning_and_autoconnect_unsafe(); uni_bt_start_scanning_and_autoconnect_unsafe();
@ -211,6 +259,8 @@ uni_error_t platform_on_device_discovered(bd_addr_t addr, const char* name, uint
void platform_on_device_connected(uni_hid_device_t* device) { void platform_on_device_connected(uni_hid_device_t* device) {
(void)device; (void)device;
g_connection_status = ConnectionStatus::Connecting;
g_status_led_tick = 0;
} }
void resume_connections() { void resume_connections() {
@ -222,9 +272,13 @@ void platform_on_device_disconnected(uni_hid_device_t* device) {
if (device == g_active_device) { if (device == g_active_device) {
g_active_device = nullptr; g_active_device = nullptr;
publish_state(make_neutral_state(), false); publish_state(make_neutral_state(), false);
g_connection_status = ConnectionStatus::Scanning;
g_status_led_tick = 0;
resume_connections(); resume_connections();
} else if (g_active_device == nullptr) { } else if (g_active_device == nullptr) {
resume_connections(); resume_connections();
g_connection_status = ConnectionStatus::Scanning;
g_status_led_tick = 0;
} }
} }
@ -237,6 +291,8 @@ uni_error_t platform_on_device_ready(uni_hid_device_t* device) {
} }
g_active_device = device; g_active_device = device;
g_connection_status = ConnectionStatus::Ready;
g_status_led_tick = 0;
publish_state(make_neutral_state(), true); publish_state(make_neutral_state(), true);
uni_bt_stop_scanning_unsafe(); uni_bt_stop_scanning_unsafe();
uni_bt_allow_incoming_connections(false); uni_bt_allow_incoming_connections(false);
@ -286,6 +342,8 @@ uni_platform* get_platform() {
tight_loop_contents(); tight_loop_contents();
} }
} }
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, true);
g_status_led_on = true;
uni_platform_set_custom(get_platform()); uni_platform_set_custom(get_platform());
if (uni_init(0, nullptr) != 0) { if (uni_init(0, nullptr) != 0) {

Binary file not shown.

Binary file not shown.