feat: DualSense BT microphone + USB 3.0 connection watchdog
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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
89847ed06e
commit
20b41d80a1
+55
-2
@@ -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;
|
||||
|
||||
+58
-3
@@ -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
|
||||
|
||||
@@ -25,6 +25,11 @@ std::vector<uint8_t> 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]);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+19
-5
@@ -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();
|
||||
|
||||
+8
-5
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user