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) <noreply@anthropic.com>
bt.h: add OLED accessors (bt_is_connected, bt_get_addr, bt_hci_err_count)
and 6 multi-slot accessors (bt_get_slot, bt_set_slot, bt_forget_slot,
bt_wipe_all_slots, bt_slot_occupied, bt_slot_get_addr).
bt.cpp:
- #include "slots.h"; add g_current_slot + update_discoverable()
helper that gates gap_discoverable_control on slots_any_empty().
- 3 OLED accessor impls + 6 slot accessor impls.
- bt_init: slots_load() + g_current_slot from Config_body.current_slot,
then update_discoverable() instead of bare gap_discoverable_control(1).
- HCI_EVENT_INQUIRY_RESULT: slot-ownership filter (skip devices owned
by other slots; if our slot is occupied, only accept matching addr).
- L2CAP_EVENT_CHANNEL_OPENED HID_CONTROL: slot_assign on empty slot
(Phase G auto-bond pattern).
- L2CAP_EVENT_CHANNEL_OPENED HID_INTERRUPT: replace upstream's
gap_*_control(false) pair with update_discoverable() — they wanted
"non-discoverable once connected"; we want "non-discoverable once
all 4 slots are full". Strictly looser, which is correct since
empty slots need to accept new pairings.
- Inquiry-complete + disconnection-complete handlers similarly
swapped to update_discoverable().
Upstream's state_mgr hooks (state_init / state_set / state_update
calls in L2CAP_EVENT_CHANNEL_OPENED and elsewhere) are preserved
as-is — they're the upstream rumble-fix infrastructure.
Build still fails on oled.cpp references to audio_peak_* / counter
accessors that Phase E adds.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upstream only checks left stick X axis, so pressing buttons, using
the d-pad, or moving the right stick won't prevent disconnection.
Extends the same dead zone pattern to cover both sticks (all 4 axes),
both button bytes, and d-pad/misc bytes.
When the inactive_time configuration value exceeds 35, the expression
`get_config().inactive_time * 60 * 1000 * 1000`
overflows the maximum int value due to implicit integer conversion.
l2cap_send was passed the caller's `len` (expected response size) instead of
the actual array size. The get_feature array is only 2 bytes (transaction
header + report ID), so this read up to 62 bytes of uninitialized stack.
Use sizeof(get_feature) to send only the 2-byte GET_REPORT request.