feat: low-battery LED indicator (ENABLE_BATT_LED, default ON)

Blink the Pico onboard LED at 1 Hz when the connected DualSense
reports PowerPercent <= 1 (i.e. <= 10%) and PowerState == Discharging.
Source data is byte 52 of the BT 0x31 input report, already copied
into interrupt_in_data; no new BT parsing required.

The new module owns the LED only while it is actively blinking; it
detects controller disconnection via stale-report timeout and steps
out, so bt.cpp's existing connect/disconnect LED handling stays in
charge in all other states. Honors disable_pico_led.

Gated by -DENABLE_BATT_LED=ON (default). With the option off, the
source file is not compiled and behavior is identical to upstream.
CI gains a compile-only check for the OFF flavor.

(cherry picked from commit 2f8ea73c9fb695e24e7cc3329db7cb925e82e1c9)
This commit is contained in:
Thierry Perraut
2026-05-13 18:09:31 +08:00
committed by awalol
parent d3311a35d9
commit 63c62081eb
6 changed files with 136 additions and 0 deletions
+8
View File
@@ -94,6 +94,14 @@ jobs:
cmake --build build/debug --target ds5-bridge cmake --build build/debug --target ds5-bridge
cp build/debug/ds5-bridge.uf2 artifacts/ds5-bridge-debug.uf2 cp build/debug/ds5-bridge.uf2 artifacts/ds5-bridge-debug.uf2
- name: Build no-batt-led firmware (compile check only)
run: |
cmake -S . -B build/no-batt-led -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DPICO_SDK_PATH="$PICO_SDK_PATH" \
-DENABLE_BATT_LED=OFF
cmake --build build/no-batt-led --target ds5-bridge
- name: Upload standard UF2 artifact - name: Upload standard UF2 artifact
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
+11
View File
@@ -40,6 +40,12 @@ if(ENABLE_VERBOSE)
else() else()
set(ENABLE_VERBOSE_VALUE 0) set(ENABLE_VERBOSE_VALUE 0)
endif () endif ()
option(ENABLE_BATT_LED "Blink Pico LED when DualSense battery is low (<=10%)" ON)
if (ENABLE_BATT_LED)
set(ENABLE_BATT_LED_VALUE 1)
else ()
set(ENABLE_BATT_LED_VALUE 0)
endif ()
# Pull in Raspberry Pi Pico SDK (must be before project) # Pull in Raspberry Pi Pico SDK (must be before project)
include(pico_sdk_import.cmake) include(pico_sdk_import.cmake)
@@ -61,6 +67,10 @@ add_executable(ds5-bridge
src/cmd.cpp src/cmd.cpp
) )
if (ENABLE_BATT_LED)
target_sources(ds5-bridge PRIVATE src/battery_led.cpp)
endif ()
# Cockos WDLResampler # Cockos WDLResampler
add_library(wdl_resampler STATIC add_library(wdl_resampler STATIC
lib/WDL/WDL/resample.cpp lib/WDL/WDL/resample.cpp
@@ -88,6 +98,7 @@ target_compile_definitions(ds5-bridge PRIVATE
PICO_STDIO_USB_ENABLE_RESET_VIA_BAUD_RATE=0 PICO_STDIO_USB_ENABLE_RESET_VIA_BAUD_RATE=0
PICO_STDIO_USB_ENABLE_RESET_VIA_VENDOR_INTERFACE=0 PICO_STDIO_USB_ENABLE_RESET_VIA_VENDOR_INTERFACE=0
ENABLE_VERBOSE=${ENABLE_VERBOSE_VALUE} ENABLE_VERBOSE=${ENABLE_VERBOSE_VALUE}
ENABLE_BATT_LED=${ENABLE_BATT_LED_VALUE}
) )
pico_set_program_name(ds5-bridge "ds5-bridge") pico_set_program_name(ds5-bridge "ds5-bridge")
+6
View File
@@ -57,6 +57,12 @@ The Pico device will only be visible to the system after the controller is conne
Some behaviors depend on reconnection cycles to take effect Some behaviors depend on reconnection cycles to take effect
### Low-battery LED indicator
When the connected DualSense reports its battery at or below 10% (and it is not charging), the Pico onboard LED switches from solid-on to a 1 Hz blink so you can see the warning at a glance. The LED returns to solid-on as soon as the controller is plugged in or its reported level rises again. The indicator respects the existing `disable_pico_led` setting (Speaker mute toggle).
To opt out at build time, configure with `-DENABLE_BATT_LED=OFF`. Default is ON.
## Known Issues ## Known Issues
- ⚠️ Audio may experience slight stuttering - ⚠️ Audio may experience slight stuttering
+77
View File
@@ -0,0 +1,77 @@
//
// Low-battery LED indicator. See battery_led.h.
//
#include "battery_led.h"
#include <cstdint>
#include "config.h"
#include "pico/cyw43_arch.h"
#include "pico/time.h"
extern uint8_t interrupt_in_data[63];
namespace {
constexpr uint64_t REPORT_STALE_US = 2'000'000; // assume disconnected if no report for 2 s
constexpr uint64_t BLINK_PERIOD_US = 500'000; // 1 Hz, 50% duty
constexpr uint8_t THRESHOLD_LEVEL = 1; // PowerPercent <= 1 (i.e. <= 10%)
constexpr uint8_t POWER_STATE_DISCHARGING = 0x0;
uint64_t last_report_us = 0;
uint64_t last_toggle_us = 0;
bool blinking = false;
bool led_state = false;
} // namespace
void battery_led_init(void) {
last_report_us = 0;
last_toggle_us = 0;
blinking = false;
led_state = false;
}
void battery_led_note_report(void) {
last_report_us = time_us_64();
}
void battery_led_tick(void) {
if (get_config().disable_pico_led) {
blinking = false;
return;
}
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;
return;
}
const uint8_t b = interrupt_in_data[52];
const uint8_t pct = b & 0x0F;
const uint8_t st = (b >> 4) & 0x0F;
const bool low = (st == POWER_STATE_DISCHARGING) && (pct <= THRESHOLD_LEVEL);
if (low) {
if (!blinking) {
blinking = true;
led_state = true;
last_toggle_us = now;
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, true);
return;
}
if ((now - last_toggle_us) >= BLINK_PERIOD_US) {
led_state = !led_state;
last_toggle_us = now;
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, led_state);
}
} else if (blinking) {
blinking = false;
// We were blinking and are still receiving fresh reports => still connected.
// Restore the LED to the bt.cpp "connected = solid on" state.
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, true);
}
}
+18
View File
@@ -0,0 +1,18 @@
//
// Low-battery LED indicator for the Pico onboard LED.
// Reads PowerPercent / PowerState from interrupt_in_data[52]
// (DualSense BT 0x31 report, see USBGetStateData in utils.h).
//
#pragma once
void battery_led_init(void);
// Call once per main-loop iteration. Drives the LED blink while the
// battery is low and the controller is connected; otherwise no-op.
void battery_led_tick(void);
// Call from the BT input-report callback whenever a fresh 0x31 report
// has been copied into interrupt_in_data. Used to detect disconnection
// via stale-report timeout.
void battery_led_note_report(void);
+16
View File
@@ -17,6 +17,9 @@
#endif #endif
#include "config.h" #include "config.h"
#include "cmd.h" #include "cmd.h"
#if ENABLE_BATT_LED
#include "battery_led.h"
#endif
// Pico SDK speciifically for waiting on conditions // Pico SDK speciifically for waiting on conditions
#include "pico/critical_section.h" #include "pico/critical_section.h"
@@ -85,6 +88,9 @@ void on_bt_data(CHANNEL_TYPE channel, uint8_t *data, uint16_t len) {
if (get_config().polling_rate_mode != 2) { if (get_config().polling_rate_mode != 2) {
memcpy(interrupt_in_data, data + 3, 63); memcpy(interrupt_in_data, data + 3, 63);
#if ENABLE_BATT_LED
battery_led_note_report();
#endif
return; return;
} }
@@ -98,6 +104,9 @@ void on_bt_data(CHANNEL_TYPE channel, uint8_t *data, uint16_t len) {
memcpy(interrupt_in_data, data + 3, 63); memcpy(interrupt_in_data, data + 3, 63);
report_dirty = true; report_dirty = true;
critical_section_exit(&report_cs); critical_section_exit(&report_cs);
#if ENABLE_BATT_LED
battery_led_note_report();
#endif
} }
} }
@@ -204,6 +213,10 @@ int main() {
} }
cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, false); cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, false);
#if ENABLE_BATT_LED
battery_led_init();
#endif
#if !ENABLE_SERIAL #if !ENABLE_SERIAL
if (watchdog_caused_reboot()) { if (watchdog_caused_reboot()) {
printf("Rebooted by Watchdog!\n"); printf("Rebooted by Watchdog!\n");
@@ -243,5 +256,8 @@ int main() {
tud_task(); tud_task();
audio_loop(); audio_loop();
interrupt_loop(); interrupt_loop();
#if ENABLE_BATT_LED
battery_led_tick();
#endif
} }
} }