diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4cd7072..3f19a28 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -94,6 +94,14 @@ jobs: cmake --build build/debug --target ds5-bridge 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 uses: actions/upload-artifact@v7 with: diff --git a/CMakeLists.txt b/CMakeLists.txt index c6dff95..b9ce0f0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,12 @@ if(ENABLE_VERBOSE) else() set(ENABLE_VERBOSE_VALUE 0) 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) include(pico_sdk_import.cmake) @@ -61,6 +67,10 @@ add_executable(ds5-bridge src/cmd.cpp ) +if (ENABLE_BATT_LED) + target_sources(ds5-bridge PRIVATE src/battery_led.cpp) +endif () + # Cockos WDLResampler add_library(wdl_resampler STATIC 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_VENDOR_INTERFACE=0 ENABLE_VERBOSE=${ENABLE_VERBOSE_VALUE} + ENABLE_BATT_LED=${ENABLE_BATT_LED_VALUE} ) pico_set_program_name(ds5-bridge "ds5-bridge") diff --git a/README.md b/README.md index 088e370..9430b0f 100644 --- a/README.md +++ b/README.md @@ -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 +### 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 - ⚠️ Audio may experience slight stuttering diff --git a/src/battery_led.cpp b/src/battery_led.cpp new file mode 100644 index 0000000..6bbc586 --- /dev/null +++ b/src/battery_led.cpp @@ -0,0 +1,77 @@ +// +// Low-battery LED indicator. See battery_led.h. +// + +#include "battery_led.h" + +#include + +#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); + } +} diff --git a/src/battery_led.h b/src/battery_led.h new file mode 100644 index 0000000..8100a5f --- /dev/null +++ b/src/battery_led.h @@ -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); diff --git a/src/main.cpp b/src/main.cpp index ed635b6..38f2ae3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,6 +17,9 @@ #endif #include "config.h" #include "cmd.h" +#if ENABLE_BATT_LED +#include "battery_led.h" +#endif // Pico SDK speciifically for waiting on conditions #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) { memcpy(interrupt_in_data, data + 3, 63); +#if ENABLE_BATT_LED + battery_led_note_report(); +#endif 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); report_dirty = true; 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); +#if ENABLE_BATT_LED + battery_led_init(); +#endif + #if !ENABLE_SERIAL if (watchdog_caused_reboot()) { printf("Rebooted by Watchdog!\n"); @@ -243,5 +256,8 @@ int main() { tud_task(); audio_loop(); interrupt_loop(); +#if ENABLE_BATT_LED + battery_led_tick(); +#endif } }