From 20b41d80a141f8ea8024209ec9be401df5deee59 Mon Sep 17 00:00:00 2001 From: MarcelineVPQ Date: Sun, 24 May 2026 01:30:37 -0600 Subject: [PATCH] feat: DualSense BT microphone + USB 3.0 connection watchdog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BT microphone over Bluetooth: the DS5 mic now works over the dongle's BT pairing — decoded from the controller's Opus stream to the USB capture endpoint. Hinges on pkt[4] bit 0 (mic-enable) in the outbound 0x36 audio report; credit to awalol (upstream) for identifying it. Mic-tagged 0x31 frames ((data[2]>>1)&1) are ALWAYS diverted out of the input path (decoded when on, dropped when off) so Opus payload can never corrupt sticks/buttons. Always-on via a sticky-latch keep-alive that only runs post-enumeration (tud_mounted) so it never floods the fresh-pair handshake (which otherwise delayed controller detection past the watchdog and tore the link down). Toggle: bt_mic_enable config field (default on) — OLED Settings + web config. README gains a "DualSense Microphone over Bluetooth" section; BLUETOOTH_AUDIO_NOTES.md rewritten from "dead end" to the working mechanism. USB 3.0 connection watchdog: auto-recovers a stalled connection (re-inquiry) instead of hanging on the amber lightbar, for USB 3.0 ~2.4 GHz RF interference that desensitizes the CYW43 BT radio. Re-enabled the ACL-fail / auth-fail / create-connection-reject recovery paths. README "USB 3.0 ports & Bluetooth interference" section with mitigations. Co-Authored-By: Claude Opus 4.7 (1M context) --- BLUETOOTH_AUDIO_NOTES.md | 88 +++++++++++----------------------------- README.md | 34 +++++++++++++++- src/audio.cpp | 57 +++++++++++++++++++++++++- src/bt.cpp | 61 ++++++++++++++++++++++++++-- src/bt.h | 5 +++ src/config.cpp | 4 ++ src/config.h | 5 +++ src/main.cpp | 24 ++++++++--- src/oled.cpp | 13 +++--- 9 files changed, 211 insertions(+), 80 deletions(-) diff --git a/BLUETOOTH_AUDIO_NOTES.md b/BLUETOOTH_AUDIO_NOTES.md index a05efc7..08a48b9 100644 --- a/BLUETOOTH_AUDIO_NOTES.md +++ b/BLUETOOTH_AUDIO_NOTES.md @@ -1,77 +1,37 @@ -# Bluetooth microphone investigation — current status +# Bluetooth microphone — SOLVED -**TL;DR:** The DualSense's built-in microphone does **not** work when the controller is paired to this dongle over Bluetooth. It works fine when the controller is connected directly to a host over USB. This is a Sony / DS5-firmware-side limitation we currently can't work around without reverse engineering or BT-sniffer access to PS5 ↔ DS5 traffic. The same limitation is documented in the upstream Linux kernel driver (`drivers/hid/hid-playstation.c` line ~1509: *"Bluetooth audio is currently not supported"*). +**TL;DR:** The DualSense's built-in microphone **works over this dongle's Bluetooth pairing** as of v0.6.8. It is decoded and presented to the host as the standard DualSense USB capture device. Earlier versions of this document concluded the opposite — that it was a Sony-side limitation, probably encrypted, a dead end. **That conclusion was wrong.** The whole thing hinged on a single enable bit. Full credit to **[awalol](https://github.com/awalol/DS5Dongle)** (upstream `mic` branch) for identifying it. -This file is a hand-off / research log for the next person who tries. +## How it works -## What does work +1. **Enable bit (dongle → DS5).** In the outbound `0x36` BT audio report, the audio-control sub-report's first flag byte (`pkt[4]`) has **bit 0 = mic-enable**. Setting it (`0b11111110` → `0b11111111`; bits 1–7 were already the speaker/haptic enables) tells the DS5 to start streaming its mic. See `src/audio.cpp`. +2. **Mic frames (DS5 → dongle).** Once enabled, the DS5 tags certain `0x31` BT input reports as mic frames by setting **bit 1 of byte 2** (`(data[2] >> 1) & 1`); the payload is a **71-byte Opus packet at offset +4**. `src/main.cpp:on_bt_data()` routes those to `mic_add_queue()`. +3. **Decode + present.** `src/audio.cpp` Opus-decodes each frame to mono 48 kHz (480-sample / 10 ms frames), duplicates mono → stereo, and `tud_audio_write`s it to the UAC1 capture endpoint. The host sees a normal DualSense mic. +4. **Sticky.** Once the DS5 starts streaming it **keeps going for the rest of the session** even after the enable bit / audio output stops (verified: mic stays live with no further `0x36` frames). +5. **Always-on.** Because the enable normally only rides the audio-gated `0x36` frames, the dongle sends a **control-only `0x36` keep-alive** (enable bit + `SetStateData` + silent haptic, no speaker payload) at ~4 Hz *only until mic frames start arriving*, then stops (sticky takes over). So the mic works with no game audio playing, at minimal extra BT traffic. See `mic_enable_keepalive()` in `src/audio.cpp`. +6. **Toggle.** Gated by `Config_body.bt_mic_enable` (default on) — OLED **Settings → BT Mic** and the web config tool. Off = no enable sent, no keep-alive, and inbound mic frames are not routed (so it's off host-side even if a previously-enabled DS5 is still streaming). Off by toggle saves DS5 battery (always-on keeps its audio subsystem awake). -- **Direct USB-C from DS5 → host:** mic enumerates as a UAC1 IN endpoint at 48 kHz / 16-bit / 2 channels on `EP 0x82`, max packet 196 bytes. ALSA recognizes it as `card N: Controller [DualSense Wireless Controller]`. `arecord` captures real audio after raising the `Headset Capture Volume` mixer control (it defaults to 0 dB). -- **Our dongle's USB descriptor** correctly mirrors the DS5's UAC1 layout — same interfaces, same alt settings, same endpoint addresses, same packet sizes. Verified with `lsusb -v` against a real DS5. -- **All the firmware-side decode infrastructure for BT mic is in place** (Opus decoder, `mic_fifo` queue, `tud_audio_write` to the IN endpoint, mono → stereo duplication) — see `src/audio.cpp`. It's currently gated behind `if (false)` in `src/main.cpp`'s `on_bt_data()` because we have nothing to feed it. +## Why the original conclusion was wrong (the lesson) -## What doesn't, and why +The earlier investigation ported awalol's *receive* side (the `(data[2]>>1)&1` trigger + Opus decode) and then watched for that bit — but **never sent the enable bit**, because this fork's `audio.cpp` had diverged (the pre-`3a31bd7` SetStateData revert) and sat at `pkt[4] = 0b11111110`. With nothing telling the DS5 to start, it never streamed, so bit 1 of byte 2 never set — which got misread as "the trigger never fires → the channel must be encrypted / it's a dead end." -The DS5 firmware on the test controller (build date `Jul 4 2025`, queried via feature report 0x20) **does not stream microphone audio over the standard BT-HID L2CAP channels** (PSM 0x11 control + 0x13 interrupt). +It was never encrypted. It was one un-set bit on the *transmit* side. Lesson: when porting a two-sided protocol, confirm **both** halves (enable *and* receive) before concluding the device "can't" do something. -What we tried: +## What we use (host-side) -1. **Upstream `awalol/DS5Dongle` `mic` branch as reference.** That branch claims to extract a 71-byte Opus packet at `data + 4` of any BT input report where `(data[2] >> 1) & 1` is set. On our DS5 firmware, **bit 1 of byte 2 is never set** (verified across thousands of frames via the `g_31_b2_or` OR mask). The upstream RE was likely done on a different (older) DS5 firmware revision. -2. **Bit 0 of byte 2** matches roughly all standard input reports — confirmed by reading the supposed "mic prefix" via `0xFD` feature report and seeing live stick X/Y values (not Opus data). Not a mic flag. -3. **Frame length sweep.** Longest BT 0x31 frame we ever see is 79 bytes — a fully-decoded standard DS5 input report (sticks + IMU + touchpad + battery + sensor timestamp + trailing zeros). No audio bytes appended anywhere. -4. **Other report IDs.** Counted ALL incoming BT input reports by report ID. Only `0x01` (rare) and `0x31` (common). No 0x33 / 0x35 / 0x36 / 0x39 / etc. The DS5 isn't sending anything mic-shaped on a different ID. -5. **State configuration matching the kernel.** Set `AllowAudioControl=1`, `AllowMicVolume=1`, `AllowAudioMute=1`, `MicSelect=Internal`, `VolumeMic=0x40`, `MicMute=0`, `AudioPowerSave=0` — exactly what `hid-playstation.c` sets when calling its "Enable microphone" path. DS5 still doesn't stream. -6. **Bidirectional audio session hypothesis.** Maybe the DS5 only streams mic when there's also active speaker audio (`0x36` packets) flowing. Tested: ran `aplay /dev/zero` simultaneously with `arecord`. No change in BT-side counters, no new report IDs, no longer frames. Disproved. -7. **State refresh on host UAC1 alt-setting change.** Considered hooking `tud_audio_set_itf_cb(itf=2, alt=1)` to send the DS5 a fresh "enable mic" state update. Not implemented — given the kernel comment and our state matching the kernel's own "enable" sequence, this wouldn't have helped. +- **`scripts/mic_diag.sh`** — `status` / `capture [secs]` / `watch` / `bt-trace`. `capture` arecords the DualSense mic card and reports peak/RMS/non-zero, the fastest way to confirm real audio. +- The DualSense capture card's **`Headset` capture control defaults low** — raise it (`amixer -c sset 'Headset' 90%`) or captures look silent. +- **OLED Diagnostics** `Mic in:` (~100/s when streaming) + `Mic dec=` (480 = good Opus decode) are the on-device confirmation. +- Vendor HID feature reports `0xFD` / `0xFE` and the BT counters remain useful general audio-debug infra. -What we **did not** try (real next steps if anyone picks this up): +## Open follow-ups -- **SDP browse the DS5** over BT after pairing. Discover what L2CAP PSMs / services it advertises beyond HID. If there's a Sony proprietary audio PSM we haven't subscribed to, that's where mic traffic might live. -- **Open additional L2CAP channels** (proprietary audio PSM if found, or standard ones like A2DP=0x19 / RFCOMM=0x03) and watch for unsolicited inbound data. -- **Compare DS5 firmware revisions.** Test with an older DS5 (pre-2024 manufacture) and see if it streams mic over BT — that would tell us whether Sony removed the feature or just nobody documented the protocol. (We only have one DS5; can't test.) -- **BT sniffer** between a PS5 console and a DS5 during voice chat. Tells us exactly what L2CAP channels and bytes Sony uses for mic. Equipment-intensive (~$50–200 for an Ubertooth or commercial sniffer). -- **DS5 firmware disassembly.** Legally fraught, almost certainly EULA-violating. - -## Strongest hypothesis: the channel is encrypted - -The shape of all the negative evidence — kernel maintainers giving up, no public RE project succeeding, our matching every documented "enable" bit and getting nothing — strongly suggests the channel is **encrypted with a session key derived during pairing**, not just transported on an undocumented PSM. Sony's incentives line up perfectly: - -- **PR / privacy:** a $40 third-party dongle routing a user's PS5 voice chat to a malicious host is a worst-case PR scenario. Encrypting the mic channel is the obvious defense. -- **GDPR-class regulation:** voice biometrics from a console controller over plaintext BT is the kind of thing EU regulators ask hard questions about. -- **Anti-spoofing:** prevents injecting fake mic data into a PS5 session, which is its own threat model. - -Mechanism that fits the evidence: - -- During pairing, BT Classic SSP produces a link key. The PS5 + DS5 firmware likely run a Sony-proprietary KDF on top of that link key to produce an audio-channel session key. -- Mic audio is transported on a Sony-allocated proprietary L2CAP PSM (not in the standard BT-SIG ranges) and encrypted with that session key (AES-CCM or similar). -- A third-party dongle could connect to the PSM if it knew the number, but without the KDF / session key the payload would be opaque encrypted blobs. - -**Implication:** a BT sniffer might tell us the PSM and packet timing/sizes, but not the payload contents. Building a PS5-impersonating dongle that derives valid session keys would require either Sony system-software disassembly or DS5 firmware disassembly — legally fraught, and a much bigger undertaking than what this project is set up for. - -This re-frames "we can't get mic over BT" from "we haven't tried hard enough" to "the architecture is intentionally hardened against exactly this." That's not nothing — it's a clear answer to give users who ask, and a clear bar to clear if anyone wants to actually pursue it. - -## What we built that's useful regardless - -These all stay shipped — they're general-purpose audio-debug infrastructure now: - -- **`scripts/mic_diag.sh`** with subcommands `status`, `capture [secs]`, `watch`, `bt-trace`. Drives the entire diagnostic loop from the host without needing OLED-relay-through-the-user; reads vendor feature reports via `/dev/hidraw`. -- **Vendor HID feature report `0xFD`** (32 bytes): BT input-report counter, non-0x31 counter, last seen non-0x31 report ID, OR mask of byte 2 across 0x31 frames, length range, hex prefix of last frame. -- **Vendor HID feature report `0xFE`** (82 bytes): full content of the longest 0x31 frame seen, for byte-level inspection. -- **OLED Diagnostics screen** carries BT31/Mic rate + recent frame prefix + opus dec/wrote bytes — useful for any future audio-path debugging at the bench. -- **`src/audio.cpp`** mic-decode infrastructure (Opus decoder on core0, FIFO, mono → stereo duplication, `tud_audio_write` to IN endpoint). Disabled at the `mic_add_queue` call site, ready to re-enable the moment a real mic trigger is identified. -- **`src/state_mgr.cpp`** initial state corrected — `VolumeMic` was `0xff` (out of valid range per spec; max is `0x40`), `MuteControl` had all `*PowerSave` bits set which would have power-gated the audio DSP. These corrections don't enable BT mic but they're the right defaults regardless. +- **No documented "stop" command.** Disabling mid-session relies on gating the receive side; the DS5 keeps streaming until reconnect. If a real stop/disable bit is found, wire it into the toggle to stop the DS5-side battery drain immediately. +- **Mono only.** Decoded mono is duplicated to the stereo endpoint; the DS5 mic is mono so this is fine, but the descriptor could be made truly mono (as awalol's branch does) to halve endpoint bandwidth. +- **Name the `pkt[4]` bits precisely.** `daidr/dualsense-tester`'s `outputStruct.ts` documents the *standard* output report flags but not the `0x36` BT-audio sub-report; the bit meanings beyond bit 0 are inferred from the working speaker/haptic path. ## References -- Linux kernel `drivers/hid/hid-playstation.c`. Quoted lines: ~1407–1420 (mic enable/disable), ~1509 (*"Bluetooth audio is currently not supported"*). [Raw source on GitHub mirror](https://raw.githubusercontent.com/torvalds/linux/master/drivers/hid/hid-playstation.c). -- PSDevWiki [DualSense HID Commands](https://www.psdevwiki.com/ps5/DualSense_HID_Commands) — has factory/manufacturer commands (report IDs 128, 129, 160, 164, 165) for BT patches and audio codec selection, but explicitly notes most "do not work with retail controllers". Not a path forward. -- Upstream `awalol/DS5Dongle` branch `mic` (commits `9c197fc feat: mic work`, `3829163 mic mono channel`). RE'd a working mic path for an older DS5 firmware revision; we ported the data plumbing but the BT-side trigger differs on current firmware. -- dualsensectl: `command_microphone on/off` sets `valid_flag0 |= DS_OUTPUT_VALID_FLAG0_AUDIO_CONTROL_ENABLE` and clears `DS_OUTPUT_POWER_SAVE_CONTROL_MIC_MUTE`. Same as what we already do on connect. - -## For users asking about the mic - -When users report "the mic doesn't work": - -- **Plug the DS5 into the host via USB.** Mic works out of the box. You may need to raise the `Headset Capture Volume` mixer control if it defaults to 0 dB. -- **Over the dongle's Bluetooth pairing, the mic is currently a known limitation** — not something a firmware update on our side can fix without further reverse engineering of the DS5's proprietary BT audio path. -- The diagnostic tools in `scripts/mic_diag.sh` are available if you want to help reverse engineer this; PRs welcome. +- Upstream `awalol/DS5Dongle` branch `mic` (commits `9c197fc`, `3829163`) — the source of the enable bit (`pkt[4]` bit 0) and the receive-side decode. awalol confirmed bit 0 is the key. +- Linux kernel `drivers/hid/hid-playstation.c` (~line 1509, *"Bluetooth audio is currently not supported"*) — still true for the kernel driver; not for this dongle. +- `daidr/dualsense-tester` — `src/router/DualSense/views/_OutputPanel/outputStruct.ts` for the standard output-report flag layout. diff --git a/README.md b/README.md index 9a5733b..632699e 100644 --- a/README.md +++ b/README.md @@ -140,11 +140,43 @@ When the connected DualSense reports its battery at or below 10% (and it is not To opt out at build time, configure with `-DENABLE_BATT_LED=OFF`. Default is ON. +## DualSense Microphone over Bluetooth + +**The DualSense's built-in microphone works over the dongle's Bluetooth pairing** (since v0.6.8). The controller streams its mic as Opus audio; the dongle decodes it and presents it to the host as the standard DualSense USB capture device, so any app (Discord, OBS, in-game voice) can use it like a normal microphone. + +This was long believed impossible — earlier versions of this fork documented it as a hard Sony-firmware limitation (and the Linux `hid-playstation` kernel driver still doesn't support it). It turned out to hinge on a single enable bit in the dongle's outbound audio report. **Full credit to [awalol](https://github.com/awalol/DS5Dongle) (upstream) for identifying it.** The corrected investigation log lives in [BLUETOOTH_AUDIO_NOTES.md](./BLUETOOTH_AUDIO_NOTES.md). + +**Using it:** + +- **On by default.** Pair the controller and the mic begins streaming within a second or two — no game audio required (the dongle keeps the stream alive on its own). +- **Raise the capture volume on the host** — it defaults low. On Linux, find the card with `arecord -l`, then e.g. `amixer -c sset 'Headset' 90%`. Verify capture with `scripts/mic_diag.sh capture`. +- **Toggle it off to save controller battery** — OLED **Settings → BT Mic**, or the **BT microphone** switch in the [web config tool](#web-config-tool). Always-on mic keeps the DS5's audio subsystem awake, which drains its battery noticeably faster, so disable it if you don't use voice. + +**Caveats:** + +- Mic audio is **mono** (decoded mono, duplicated across the stereo capture endpoint). +- Toggling off mid-session stops the host feed immediately, but the controller keeps streaming until it next reconnects (there's no known "stop" command); connecting fresh with the toggle off never enables it. +- The OLED **Diagnostics** screen's `Mic in:` counter reads ~100/s while the mic is streaming — a quick way to confirm it's live. + ## Known Issues - Overclocking to 320 MHz @ 1.20 V is **required** for stable BT pairing. Dropping voltage to 1.10 V or clock to stock breaks the CYW43 PIO SPI bus and BT stops working. A small heatsink on the RP2350 is recommended for sustained gameplay. - HD haptics may not fire in every game on Linux + Steam; this is game-side (some titles only send HD-haptic audio under Windows-specific APIs). Tested working in Spider-Man Remastered; not delivered in Ghost of Tsushima — same firmware, same controller. -- **DualSense microphone does not work over the Bluetooth pairing.** This is a Sony / DS5-firmware-side limitation also documented in the upstream Linux kernel driver (`drivers/hid/hid-playstation.c` line ~1509: *"Bluetooth audio is currently not supported"*). The mic works fine when the controller is connected directly to the host via USB-C. See [BLUETOOTH_AUDIO_NOTES.md](./BLUETOOTH_AUDIO_NOTES.md) for the full investigation log + what's already wired firmware-side if a future contributor cracks the BT-side trigger. +- **USB 3.0 ports can disrupt pairing** — the controller may get stuck on a solid amber/yellow lightbar and never connect, while the same dongle works fine on a USB 2.0 port. This is RF interference, not a firmware bug; see [USB 3.0 ports & Bluetooth interference](#usb-30-ports--bluetooth-interference) below. (As of v0.6.8 the firmware auto-retries a stalled connection instead of hanging, which recovers many — but not all — marginal cases.) + +## USB 3.0 ports & Bluetooth interference + +If the dongle works on one port but not another, **try a USB 2.0 port first.** USB 3.0 ports and (especially) USB 3.0 extension cables emit broadband RF noise centered near 2.4 GHz — the same band the dongle's Bluetooth radio uses to talk to the controller. This is a well-documented industry issue (Intel, *"USB 3.0 Radio Frequency Interference Impact on 2.4 GHz Wireless Devices"*), not specific to this firmware. The noise desensitizes the dongle's BT receiver, so the controller can start connecting (amber lightbar) but the link is too noisy to complete — it hangs on yellow. + +Mitigations, roughly in order of effectiveness: + +1. **Plug the dongle into a USB 2.0 port** (often the simplest fix — many motherboards/cases have both). +2. **Use a short USB 2.0 extension cable** to get the dongle a few inches away from the USB 3.0 ports / metal chassis, improving line-of-sight to the controller. Avoid USB 3.0 extension cables specifically. +3. **Use a powered USB 2.0 hub** plugged into the USB 3.0 port — the hub downshifts the link and adds distance. +4. **Clip a ferrite bead** onto the cable near the dongle. +5. **Keep the controller closer / in line of sight** of the dongle during pairing. + +The firmware will keep retrying a stalled connection on its own, so leaving it plugged in for ~10–20 s after the lightbar goes amber may let it recover without a replug. ## Performance / Overclocking diff --git a/src/audio.cpp b/src/audio.cpp index d16444b..2f60073 100644 --- a/src/audio.cpp +++ b/src/audio.cpp @@ -13,6 +13,7 @@ #include "utils.h" #include "pico/multicore.h" #include "pico/util/queue.h" +#include "pico/time.h" #include "config.h" #include "state_mgr.h" #include "usb.h" @@ -114,6 +115,46 @@ void mic_add_queue(const uint8_t *data) { queue_try_add(&mic_fifo, &packet); } +// Re-assert the DS5 mic-enable (pkt[4] bit 0) so the controller streams its mic +// even when no audio is being output to it. Normally the enable only rides the +// 0x36 audio frames, which are gated on active USB audio — so without this, mic +// only works while a game plays sound. The enable is sticky (the DS5 keeps +// streaming once it starts), so we send a control-only 0x36 (enable + the +// load-bearing SetStateData sub-report + a silent haptic block, no speaker +// payload → makes no sound) at ~4 Hz ONLY until mic frames start arriving, then +// stop — minimizing BT traffic and DS5 battery. Resumes if the stream stalls. +static void mic_enable_keepalive() { + if (!bt_is_connected() || !get_config().bt_mic_enable) return; + const uint64_t now = time_us_64(); + static uint32_t last_frames = 0; + static uint64_t last_frame_us = 0; + static uint64_t last_send_us = 0; + const uint32_t frames = g_mic_frames; + if (frames != last_frames) { last_frames = frames; last_frame_us = now; } + if (last_frame_us != 0 && (now - last_frame_us) < 1000000ULL) return; // streaming → sticky, no resend + if (last_send_us != 0 && (now - last_send_us) < 250000ULL) return; // throttle to ~4 Hz while arming + last_send_us = now; + + uint8_t pkt[REPORT_SIZE]{}; + pkt[0] = REPORT_ID; + pkt[1] = reportSeqCounter << 4; + reportSeqCounter = (reportSeqCounter + 1) & 0x0F; + pkt[2] = 0x11 | 1 << 7; + pkt[3] = 7; + pkt[4] = 0b11111111; // mic-enable (bit 0) + const auto buf_len = get_config().audio_buffer_length; + pkt[5] = pkt[6] = pkt[7] = pkt[8] = pkt[9] = buf_len; + pkt[10] = packetCounter++; + pkt[11] = 0x10 | 1 << 7; // SetStateData sub-report (load-bearing — keeps actuators alive) + pkt[12] = 63; + state_set(pkt + 13, 63); + pkt[76] = 0x12 | 1 << 7; // haptic sub-report; samples left zero = silent + pkt[77] = SAMPLE_SIZE; + // no speaker sub-report (pkt[142..] stays zero) → control-only, no audio out + bt_write(pkt, sizeof(pkt)); + g_bt_packets++; +} + void audio_loop() { // Mic-in path: pull one Opus packet from the BT-side FIFO, decode to // mono PCM, duplicate to stereo (our UAC1 endpoint declares 2 channels), @@ -142,7 +183,16 @@ void audio_loop() { } // 1. 读取 USB 音频数据 - if (!tud_audio_available()) return; + if (!tud_audio_available()) { + // Keep the DS5 mic streaming even without output audio — but ONLY once + // the host has enumerated us (tud_mounted). Running it during the + // fresh-pair feature handshake floods BT TX and delays controller-type + // detection past the connection watchdog's timeout, which then tears the + // link down (~10-15s "shutdown" on fresh pair). After enumeration the + // handshake is done, so it's safe — and always-on mic still works. + if (tud_mounted()) mic_enable_keepalive(); + return; + } int16_t raw[192]; uint32_t bytes_read = tud_audio_read(raw, sizeof(raw)); // 每次读入 384 bytes @@ -276,7 +326,10 @@ void audio_loop() { reportSeqCounter = (reportSeqCounter + 1) & 0x0F; pkt[2] = 0x11 | 0 << 6 | 1 << 7; pkt[3] = 7; - pkt[4] = 0b11111110; + // bit 0 = mic-enable: tells the DS5 to stream its mic over BT (awalol + // confirmed this is the key). Bits 1-7 are the pre-existing speaker/ + // haptic audio-enable flags. Gated on the bt_mic_enable config toggle. + pkt[4] = get_config().bt_mic_enable ? 0b11111111 : 0b11111110; const auto buf_len = get_config().audio_buffer_length; pkt[5] = buf_len; pkt[6] = buf_len; diff --git a/src/bt.cpp b/src/bt.cpp index 400dbfb..c844e61 100644 --- a/src/bt.cpp +++ b/src/bt.cpp @@ -26,6 +26,15 @@ #define MTU_CONTROL 672 #define MTU_INTERRUPT 672 +// Connection-attempt watchdog: if a connection commits to a device (inquiry +// found one / incoming request accepted) but doesn't reach USB-enumeration +// within this window, tear down and retry. Catches the silent stalls caused by +// USB 3.0 2.4 GHz RF interference on the CYW43 BT radio (DualSense stuck on the +// amber init lightbar, never enumerates) — see README troubleshooting. A +// healthy or slow re-pair finishes well under 6 s, so 10 s never trips a real +// connection but heals before the user reaches to replug. +#define CONNECT_WATCHDOG_TIMEOUT_US (10 * 1000 * 1000) + using std::unordered_map; using std::vector; using std::queue; @@ -54,6 +63,12 @@ struct send_element { absolute_time_t inactive_time = 0; // 手柄长时间静默 +// Connection-attempt watchdog timestamp. 0 == not armed; armed == a connection +// attempt is in flight (committed to a device, not yet USB-enumerating). Set +// when an attempt begins, cleared the instant the controller type is identified +// (USB connects) and on every teardown. Checked by bt_connection_watchdog_tick(). +static absolute_time_t connect_attempt_started = 0; + // Multi-slot pairing state. Modeled on zurce/DS5Dongle-OLED. static int g_current_slot = 0; @@ -166,6 +181,35 @@ bool bt_disconnect() { return true; } +// Called every main-loop iteration. If a connection attempt has stalled past +// the timeout, tear it down so the state machine retries instead of hanging +// (e.g. on the amber lightbar under USB 3.0 RF interference). Inert unless a +// connection attempt is in flight, so it never touches a healthy session. +void bt_connection_watchdog_tick() { + if (connect_attempt_started == 0) return; // not armed + if (absolute_time_diff_us(connect_attempt_started, get_absolute_time()) + < CONNECT_WATCHDOG_TIMEOUT_US) { + return; + } + printf("[BT] Connection watchdog: attempt stalled, recovering\n"); + connect_attempt_started = 0; // disarm; the next attempt re-arms + + if (acl_handle != HCI_CON_HANDLE_INVALID) { + // ACL is up but setup stalled (auth/encryption/L2CAP/feature-wait). + // Route through the proven HCI_EVENT_DISCONNECTION_COMPLETE teardown. + bt_disconnect(); + } else { + // No ACL yet (stalled before/at create-connection) — reset by hand + // and kick a fresh inquiry. + device_found = false; + new_pair = false; + gap_inquiry_stop(); + gap_inquiry_start(30); + gap_connectable_control(1); + update_discoverable(); + } +} + void bt_get_signal_strength(int8_t *rssi) { // gap_read_rssi() completes asynchronously, so this function can only // return the last cached RSSI value. Trigger a refresh afterwards so a @@ -298,6 +342,7 @@ static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *p if (device_found) { printf("[HCI] Connecting to %s...\n", bd_addr_to_str(current_device_addr)); new_pair = true; + connect_attempt_started = get_absolute_time(); // arm connection watchdog hci_send_cmd(&hci_create_connection, current_device_addr, hci_usable_acl_packet_types(), 0, 0, 0, 1); break; @@ -317,8 +362,9 @@ static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *p if (opcode == HCI_OPCODE_HCI_CREATE_CONNECTION && status != ERROR_CODE_SUCCESS) { device_found = false; new_pair = false; + connect_attempt_started = 0; // disarm; failed before an ACL existed printf("[HCI] Create connection rejected, restart inquiry\n"); - // gap_inquiry_start(30); + gap_inquiry_start(30); } break; } @@ -350,8 +396,9 @@ static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *p } else { device_found = false; new_pair = false; + connect_attempt_started = 0; // disarm; no ACL was established printf("[HCI] ACL connect failed status=0x%02X, restart inquiry\n", status); - // gap_inquiry_start(30); + gap_inquiry_start(30); } break; } @@ -401,7 +448,11 @@ static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *p if (status != ERROR_CODE_SUCCESS) { printf("[HCI] Authentication failed, drop stored key for %s\n", bd_addr_to_str(current_device_addr)); gap_drop_link_key_for_bd_addr(current_device_addr); - // gap_inquiry_start(30); + connect_attempt_started = 0; // disarm; teardown below re-inquires + // ACL is still up — route through the clean disconnect path + // (HCI_EVENT_DISCONNECTION_COMPLETE restarts inquiry) rather + // than leaving a half-open ACL. + bt_disconnect(); } else { hci_send_cmd(&hci_set_connection_encryption, handle, 1); } @@ -438,6 +489,7 @@ static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *p bd_addr_copy(current_device_addr, addr); gap_inquiry_stop(); hci_send_cmd(&hci_accept_connection_request, addr, 0x01); + connect_attempt_started = get_absolute_time(); // arm watchdog (incoming path) } break; } @@ -451,6 +503,7 @@ static void hci_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t *p const uint8_t reason = hci_event_disconnection_complete_get_reason(packet); device_found = false; new_pair = false; + connect_attempt_started = 0; // disarm — every teardown clears here acl_handle = HCI_CON_HANDLE_INVALID; bt_rssi = 0; hid_control_cid = 0; @@ -508,6 +561,7 @@ static void l2cap_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t printf("Connected DSE Controller\n"); check_dse = false; is_dse = true; + connect_attempt_started = 0; // fully up — disarm watchdog #if !ENABLE_SERIAL tud_connect(); #endif @@ -515,6 +569,7 @@ static void l2cap_packet_handler(uint8_t packet_type, uint16_t channel, uint8_t printf("Connected DS5 Controller\n"); check_dse = false; is_dse = false; + connect_attempt_started = 0; // fully up — disarm watchdog #if !ENABLE_SERIAL tud_connect(); #endif diff --git a/src/bt.h b/src/bt.h index 42aac31..d9cce1e 100644 --- a/src/bt.h +++ b/src/bt.h @@ -25,6 +25,11 @@ std::vector get_feature_data(uint8_t reportId,uint16_t len); void init_feature(); void set_feature_data(uint8_t reportId, uint8_t* data,uint16_t len); +// Connection-attempt watchdog: call once per main-loop iteration. Recovers a +// stalled connection (auto re-inquiry) so a transient RF glitch — e.g. USB 3.0 +// 2.4 GHz interference — doesn't hang the dongle on the amber lightbar. +void bt_connection_watchdog_tick(); + // OLED add-on accessors. bool bt_is_connected(); void bt_get_addr(uint8_t out[6]); diff --git a/src/config.cpp b/src/config.cpp index 81beb1b..e7e1349 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -109,6 +109,10 @@ void config_valid() { body->screen_off_timeout = 15; // mirrors the original 15-min off tier printf("[Config] screen_off_timeout invalid, defaulting to 15 min\n"); } + if (body->bt_mic_enable > 1) { // 0xFF erased / upgrade → default ON + body->bt_mic_enable = 1; + printf("[Config] bt_mic_enable invalid, defaulting to 1 (on)\n"); + } if (body->config_version != CONFIG_VERSION) { body->config_version = CONFIG_VERSION; printf("[Config] Warning: Config may breaking change\n"); diff --git a/src/config.h b/src/config.h index 5376366..bb25967 100644 --- a/src/config.h +++ b/src/config.h @@ -39,6 +39,11 @@ struct __attribute__((packed)) Config_body { // idle timer is 64-bit µs so the full range is representable. Issue #5. uint8_t screen_dim_timeout; uint8_t screen_off_timeout; + // DualSense mic over Bluetooth (Phase I). 0 = off, 1 = on (default). When on, + // the dongle asserts the DS5 mic-enable bit so the controller streams its mic + // over BT and the dongle decodes it to the USB capture endpoint. Costs extra + // DS5 battery (keeps its audio subsystem awake), hence the toggle. + uint8_t bt_mic_enable; }; struct __attribute__((packed)) Config { diff --git a/src/main.cpp b/src/main.cpp index abf6e3e..5a3f33a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -189,11 +189,24 @@ void on_bt_data(CHANNEL_TYPE channel, uint8_t *data, uint16_t len) { } } - // Mic-add tap DISABLED — was decoding standard input (button/stick - // bytes) as Opus and producing INT16_MIN garbage on the USB IN - // endpoint. Re-enable once we identify the actual mic transport. - // (Standard input handling below resumes — Status screen + HID - // reports to host need this.) + // Mic-in tap (TEST): once the dongle asserts the mic-enable bit in the + // outgoing 0x36 audio report (pkt[4] bit 0, see audio.cpp — awalol + // confirmed this is the key), the DS5 streams its mic as a 71-byte Opus + // packet at data+4 of a 0x31 report with bit 1 of data[2] set. Route those + // to the mic decoder instead of treating them as a standard input report. + // The length guard (4-byte header + 71-byte Opus) keeps a stray short + // frame from over-reading. The diagnostic counters above still observe + // these frames, so the Diag screen's data[2] OR-mask will show bit 1 set + // once the enable bit takes effect. + // A mic-tagged 0x31 frame carries Opus audio at data+4, NOT a standard input + // report — so it must ALWAYS be diverted here (decoded when mic is on, dropped + // when off), never fall through to the input handler below. Letting it through + // would copy Opus bytes into interrupt_in_data and corrupt sticks/buttons. + if (channel == INTERRUPT && data[1] == 0x31 && ((data[2] >> 1) & 1) + && len >= 75) { + if (get_config().bt_mic_enable) mic_add_queue(data + 4); + return; + } if (channel == INTERRUPT && data[1] == 0x31) { if ((data[56] & 1) != (interrupt_in_data[53] & 1)) { @@ -381,6 +394,7 @@ int main() { watchdog_update(); #endif cyw43_arch_poll(); + bt_connection_watchdog_tick(); tud_task(); audio_loop(); interrupt_loop(); diff --git a/src/oled.cpp b/src/oled.cpp index 3eb9767..dc25c5d 100644 --- a/src/oled.cpp +++ b/src/oled.cpp @@ -126,14 +126,15 @@ constexpr int kLbModeHost = 8; constexpr int kNumLbModes = 9; // Settings screen state -constexpr int kNumSettingsItems = 15; // 8 fields + 3 auto-haptic + 2 screen-timeout + Reset + Wipe +constexpr int kNumSettingsItems = 16; // 8 fields + 3 auto-haptic + 2 screen-timeout + BT mic + Reset + Wipe constexpr int kSettingsAutoHapEnaIdx = 8; constexpr int kSettingsAutoHapGainIdx = 9; constexpr int kSettingsAutoHapLpIdx = 10; constexpr int kSettingsScrDimIdx = 11; constexpr int kSettingsScrOffIdx = 12; -constexpr int kSettingsResetIdx = 13; -constexpr int kSettingsWipeSlotsIdx = 14; +constexpr int kSettingsBtMicIdx = 13; +constexpr int kSettingsResetIdx = 14; +constexpr int kSettingsWipeSlotsIdx = 15; Config_body settings_local{}; int settings_sel = 0; bool settings_dirty = false; @@ -1416,6 +1417,7 @@ void settings_adjust(int delta) { c.screen_off_timeout = (uint8_t)v; break; } + case 13: c.bt_mic_enable ^= 1; break; // BT mic on/off } } @@ -1517,8 +1519,9 @@ __attribute__((noinline)) void format_settings_item(int idx, char* line, size_t if (c.screen_off_timeout == 0) snprintf(line, n, "%s ScrOff off", cur); else snprintf(line, n, "%s ScrOff %umin", cur, c.screen_off_timeout); break; - case 13: snprintf(line, n, "%s Reset to defaults", cur); break; - case 14: snprintf(line, n, "%s Wipe all slots", cur); break; + case 13: snprintf(line, n, "%s BT Mic %s", cur, c.bt_mic_enable ? "on" : "off"); break; + case 14: snprintf(line, n, "%s Reset to defaults", cur); break; + case 15: snprintf(line, n, "%s Wipe all slots", cur); break; } }