From fb68ea560853c081259990965626ae808eb193a3 Mon Sep 17 00:00:00 2001 From: MarcelineVPQ Date: Sun, 17 May 2026 11:19:55 -0600 Subject: [PATCH] fix(batt-led): force LED off + disarm on disconnect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The low-battery blink would stay frozen mid-cycle (or briefly resume during reconnect retries) after a controller died and lost connection, because the stale-report check stopped toggling but never turned the LED off, and the cached interrupt_in_data[52] still read low. New `battery_led_on_disconnect()` clears blink state, forces the LED off, and zeros last_report_us so the stale-check early-return blocks any new blink until a fresh 0x31 report arrives on the next connection. Called from bt.cpp's HCI_EVENT_DISCONNECTION_COMPLETE handler. Stale-check in the tick also now forces LED off when it fires while a blink was in progress (defense in depth for unclean disconnects). Same bug exists in upstream awalol/DS5Dongle (their battery_led.cpp is byte-identical to ours pre-fix) — Sura Academy reported it in their Discord. Plan to send this back as a PR after we validate it locally. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/battery_led.cpp | 22 ++++++++++++++++++++-- src/battery_led.h | 8 ++++++++ src/bt.cpp | 6 ++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/battery_led.cpp b/src/battery_led.cpp index 7e457a2..3a0c512 100644 --- a/src/battery_led.cpp +++ b/src/battery_led.cpp @@ -37,11 +37,29 @@ void battery_led_note_report(void) { last_report_us = time_us_64(); } +void battery_led_on_disconnect(void) { + // Stop any in-progress blink and force the LED off immediately. Zero + // last_report_us so the tick's stale-check early-returns until a fresh + // 0x31 report arrives on the next connection — prevents the cached + // low-battery byte from re-arming a blink during reconnect retries. + blinking = false; + led_state = false; + last_report_us = 0; + last_toggle_us = 0; + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, false); +} + void battery_led_tick(void) { const uint64_t now = time_us_64(); if (last_report_us == 0 || (now - last_report_us) >= REPORT_STALE_US) { - // No fresh data — bt.cpp owns the LED while disconnected. - blinking = false; + // No fresh data — bt.cpp owns the LED while disconnected. If we + // were mid-blink when the report went stale, force the LED off + // so it doesn't freeze in whichever half-cycle it was in. + if (blinking) { + blinking = false; + led_state = false; + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, false); + } return; } diff --git a/src/battery_led.h b/src/battery_led.h index 8100a5f..f1a591f 100644 --- a/src/battery_led.h +++ b/src/battery_led.h @@ -16,3 +16,11 @@ void battery_led_tick(void); // has been copied into interrupt_in_data. Used to detect disconnection // via stale-report timeout. void battery_led_note_report(void); + +// Call from the BT disconnect handler. Cancels any in-progress blink, +// forces the LED off, and arms the module so it ignores the cached +// (now-stale) battery byte until a fresh report arrives on the next +// connection. Without this, the LED can stay frozen in whichever state +// it was at the moment of disconnect, or briefly resume blinking during +// reconnect retries while interrupt_in_data[52] still reads low. +void battery_led_on_disconnect(void); diff --git a/src/bt.cpp b/src/bt.cpp index e546f0e..400dbfb 100644 --- a/src/bt.cpp +++ b/src/bt.cpp @@ -19,6 +19,9 @@ #include "state_mgr.h" #include "pico/util/queue.h" #include "slots.h" +#if ENABLE_BATT_LED +#include "battery_led.h" +#endif #define MTU_CONTROL 672 #define MTU_INTERRUPT 672 @@ -454,6 +457,9 @@ static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *p hid_interrupt_cid = 0; feature_data.clear(); cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, false); +#if ENABLE_BATT_LED + battery_led_on_disconnect(); +#endif printf("[HCI] Disconnected reason=0x%02X, start inquiry\n", reason); gap_inquiry_start(30); break;