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/README.md b/README.md index 088e370..3e29bbd 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 blink also fires when `disable_pico_led` is set — the warning is treated as critical and overrides the LED-off preference; the LED returns to its disabled (off) state once the battery recovers or the controller starts charging. + +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..7e457a2 --- /dev/null +++ b/src/battery_led.cpp @@ -0,0 +1,73 @@ +// +// 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) { + 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) { + // Critical warning: override disable_pico_led so the user always sees it. + 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; + // Battery recovered or now charging — restore steady-state LED per the user + // preference flag (LED off when disabled, otherwise the bt.cpp connected = on state). + cyw43_arch_gpio_put(CYW43_WL_GPIO_LED_PIN, !get_config().disable_pico_led); + } +} 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/bt.cpp b/src/bt.cpp index cc4c4f1..f0b19c4 100644 --- a/src/bt.cpp +++ b/src/bt.cpp @@ -17,8 +17,8 @@ #include "config.h" #include "pico/util/queue.h" -#define MTU_CONTROL 256 -#define MTU_INTERRUPT 1691 +#define MTU_CONTROL 672 +#define MTU_INTERRUPT 672 using std::unordered_map; using std::vector; @@ -395,6 +395,9 @@ static void l2cap_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t if (psm == PSM_HID_CONTROL) { printf("[L2CAP] HID Control opened cid=0x%04X\n", local_cid); hid_control_cid = local_cid; + + const auto mtu = l2cap_get_remote_mtu_for_local_cid(hid_control_cid); + printf("[L2CAP] Remote Control MTU: %d\n",mtu); } else if (psm == PSM_HID_INTERRUPT) { printf("[L2CAP] HID Interrupt opened cid=0x%04X\n", local_cid); hid_interrupt_cid = local_cid; @@ -429,6 +432,9 @@ static void l2cap_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t memcpy(report32 + 2, packet_0x10, sizeof(packet_0x10)); bt_write(report32, sizeof(report32)); + const auto mtu = l2cap_get_remote_mtu_for_local_cid(hid_interrupt_cid); + printf("[L2CAP] Remote Interrupt MTU: %d\n",mtu); + // tud_connect(); } else { printf("[L2CAP] Unknown Channel psm: 0x%02X", psm); 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 } }